Trop rarement utilisés, les secrets offrent pourtant une couche de sécurité supplémentaire - non négligeable - aux applications présentes dans vos nœuds Docker. Et il ne s'agit pas de la seule solution de sécurité que vous pouvez envisager afin de consolider la sécurité de vos conteneurs. Comment je peux les mettre en œuvre ? Est-ce facile ?  

Nous allons voir au cours de cet article, quelques techniques qui vont vous permettre de masquer certaines informations sensibles.

Un secret c'est quoi ?

Les secrets Docker offrent un moyen sécurisé de stocker des informations sensibles, comme par exemple le nom d'utilisateur, les mots de passe et même des fichiers comme des certificats auto-signés ou clés SSH.

Ces informations sensibles sont des informations qui ne devraient pas se trouver dans vos Dockerfiles ou dans des fichiers docker-compose.yml. Et encore plus important, qui ne doivent être déposées en aucun cas sur vos dépôts Git ! Parmi ces informations, nous allons retrouver notamment :

  • Logins/Phrases de passe
  • Certificats TLS
  • Clés SSH, GPG, Tokens d’APIs
  • Infos sensibles : Noms des serveurs, des bases de données, …

Voici un exemple concret d'un fichier docker-compose.yml qui ne devrait jamais se retrouver sur votre dépôt Git ou même encore sur votre serveur de production :

version: '3.7'
services:
  mariadb:
    image: mariadb:10
    environment:
      MYSQL_ROOT_PASSWORD: PasswordRoot
      MYSQL_USER: MyUSER
      MYSQL_PASSWORD: MyUserPass

C'est simple et efficace, et on a tendance en développement à réaliser ce type de fichiers. 🤐

Mais je vous assure, les bonnes pratiques si elles ne sont pas utilisées au plus tôt, vont nuire à la sécurisation de vos applications !

Regardons ensemble comment sécuriser ces données. Est-ce que je dois forcément utiliser des secrets ? Pas forcément... Regardons tout d'abord ensemble d'autres solutions.

Les variables d'environnements

Il est tout simplement possible de ne pas "montrer" vos variables d'environnements dans votre fichier docker-compose.yml. Reprenons notre exemple précédent et utilisons un fichier de variable :

version: '3.7'
services:
  mariadb:
    image: mariadb:10
    env_file:
        - .env_mariadb

Le fichier .env_mariadb pourra alors contenir vos informations :

MYSQL_ROOT_PASSWORD: PasswordRoot
MYSQL_USER: MyUSER
MYSQL_PASSWORD: MyUserPass

On déplace uniquement le problème, c'est vrai. Mais grâce à cela, vous allez pouvoir utiliser votre dépôt pour stocker vos données. Il suffira tout simplement d'exclure le fichier .env_mariadb en utilisant le fichier .gitgnore :

cat .gitignore
.env
.env_mariadb

Bien évidement en utilisant cette méthodologie, il sera nécessaire de mettre un exemple de fichier dans votre dépôt. Afin d'expliquer les variables nécessaires à l'utilisation de votre application. Utilisez cette méthode est déjà un grand pas, pourquoi faire plus ?!

Les secrets

L'utilisation de variables d'environnement, qu'elles soient déclarées dans un fichier env, ou même de façon inline, possède quelques limites.

Par exemple, les variables d'environnement sont accessibles à tous les processus au sein du conteneur. De même, il n'est pas possible de diffuser ces informations de façon sécurisée entre deux services distincts sans avoir à re-saisir deux fois cette information.

Les secrets vont chiffrer cette échange de données et en faciliter la maintenance.

De plus c'est très simple à mettre en oeuvre. Alors pourquoi s'en priver ? Reprenons donc notre exemple de base de données :

version: '3.7'
services:
  db:
    image: mariadb:10
    environment:
      MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
      MYSQL_USER: 'ghost'
      MYSQL_DATABASE: 'blog'
      MYSQL_PASSWORD_FILE: '/run/secrets/mysql-password'
    secrets:
      - mysql-password      


secrets:
  mysql-password:
    file: ./secrets_mysql-password.txt

Comme vous pouvez le voir, j'ajoute une nouvelle section dédiée secrets qui me permet de déclarer un secret :

secrets:
  mysql-password:
    file: ./secrets_mysql-password.txt

Ce secret va s'appeler : mysql-password. Et il va être généré à partir du fichier secrets_mysql-password.txt.

Il est ensuite nécessaire d'autoriser mon service à l'utiliser :

    secrets:
      - mysql-password      

Et enfin je vais pouvoir l'utiliser dans ma variable MYSQL_PASSWORD_FILE.

Vous êtes prêts à utiliser des secrets, ce n'est pas plus compliqué que ça ! Et pourtant ...

Vous remarquerez l'ajout : _FILE à la fin de la variable d'environnement. Pourquoi cet ajout ? Est-ce nécessaire ? Que se passe t-il ? Si vous souhaitez en apprendre plus sur la mécanique des secrets, allons plus loin !

La solution ultime ?

Pour comprendre l'utilisation de cette variable, il est nécessaire de revenir à la création de l'image mariadb. Comment ? En regardant de plus près son fichier Dockerfile et plus encore le fichier exécuté par le conteneur à chaque démarrage d'instance.

Toutes ces informations sont disponibles sur le github de l'image :

https://github.com/docker-library/mariadb/

En regardant le fichier Dockerfile, on peut constater que le script lancé au démarrage est le suivant :

ENTRYPOINT ["docker-entrypoint.sh"]

Dans les premières lignes de ce fichier, nous allons trouver la réponse à notre question :

# usage: file_env VAR [DEFAULT]
#    ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
#  "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
        local var="$1"
        local fileVar="${var}_FILE"
        local def="${2:-}"
        if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
                mysql_error "Both $var and $fileVar are set (but are exclusive)"
        fi
        local val="$def"
        if [ "${!var:-}" ]; then
                val="${!var}"
        elif [ "${!fileVar:-}" ]; then
                val="$(< "${!fileVar}")"
        fi
        export "$var"="$val"
        unset "$fileVar"
}

Cette fonction sera appelée ultérieurement dans le script :

docker_setup_env() {
        # Get config
        declare -g DATADIR SOCKET
        DATADIR="$(mysql_get_config 'datadir' "$@")"
        SOCKET="$(mysql_get_config 'socket' "$@")"

        # Initialize values that might be stored in a file
        file_env 'MYSQL_ROOT_HOST' '%'
        file_env 'MYSQL_DATABASE'
        file_env 'MYSQL_USER'
        file_env 'MYSQL_PASSWORD'
        file_env 'MYSQL_ROOT_PASSWORD'

        declare -g DATABASE_ALREADY_EXISTS
        if [ -d "$DATADIR/mysql" ]; then
                DATABASE_ALREADY_EXISTS='true'
        fi
}

Pour résumer, le contenu de la variable ̀MYSQL_PASSWORD_FILE est transféré vers MYSQL_PASSWORD. Aucun soucis d'un point de vue sécurité, par contre il faut comprendre que l'ajout des secrets doit être géré dans l'image elle-même. Et donc ?

Toutes les images présentes sur le hub docker ne gèrent pas forcément les secrets. Par exemple l'image du CMS ghost!

Dommage 😭

Comment faire dans ce cas ?


Il existe plusieurs solutions pour répondre à ce problème :

  • Modifier le script de démarrage afin de gérer les secrets.
  • Passer les éléments de configuration de Ghost sans passer par des variables.

Nous pourrons voir dans un prochain article comment je passe mes éléments de configuration à Ghost sans passer par des secrets afin de ne pas modifier l'image.  

L'utilisation des secrets restent à privilégier si cette option est disponible dans votre image. Car comme nous avons pu le voir, elle n'est pas complexe à mettre en oeuvre et répond à nos attentes.

Dans tous les cas, et peu importe la solution que vous aurez choisi, prenez en compte au plus tôt dans votre infrastructure ces éléments de sécurité ! Et laissez le moins d'éléments sensibles sur vos serveurs ...

De mon côté, l'utilisation des secrets et/ou des fichiers d'environnement, combinés avec un déploiement par le biais des contextes me permet de déployer des instances sans aucune copie de fichier côté serveur !

Pensez-vous que ce type de solutions peut encore plus sécuriser votre infrastructure ?

En tout cas  n'hésitez pas à m'apporter des remarques ou des commentaires sur Twitter ! C'est toujours un plaisir d'avoir des retours ! 😇