Gestion sécurisée des variables d'environnement dans les unités de service Systemd

Configurez les variables d'environnement systemd avec Environment, EnvironmentFile, les fichiers de remplacement (drop-ins) et une gestion plus sûre des secrets.

Gestion sécurisée des variables d'environnement dans les unités de service Systemd

Les variables d'environnement sont pratiques, mais elles ne sont pas automatiquement privées. Dans un service systemd, elles peuvent apparaître dans les fichiers d'unité, les fichiers de remplacement (drop-ins), systemctl show, les outils d'inspection des processus, les rapports d'incident, les journaux de débogage ou les bundles de support copiés. Cela ne signifie pas que vous ne pouvez jamais les utiliser. Cela signifie que vous devez être délibéré sur ce qui y entre et qui peut lire les fichiers qui les définissent.

Ce guide couvre les deux directives courantes, Environment= et EnvironmentFile=, puis montre comment utiliser les fichiers de remplacement (drop-ins) pour que la configuration locale reste séparée des unités gérées par le gestionnaire de paquets.


Le rôle des variables d'environnement dans Systemd

Les variables d'environnement fournissent un moyen simple de configurer un service sans modifier son code. Lorsque systemd démarre un service, il construit l'environnement du processus et applique les variables définies dans l'unité avant d'exécuter ExecStart=.

Systemd fournit deux directives principales dans la section [Service] d'un fichier d'unité pour gérer ces variables.

1. Définition directe : la directive Environment

Cette méthode vous permet de définir des variables directement dans le fichier d'unité Systemd. Elle convient aux paramètres de configuration non sensibles qui changent rarement.

Utilisation et syntaxe

La directive Environment accepte une liste séparée par des espaces d'assignations de variables au format "KEY=VALUE".

# /etc/systemd/system/my-app.service

[Unit]
Description=Mon service d'application

[Service]
User=myuser
WorkingDirectory=/opt/my-app

# Définir les variables directement dans le fichier d'unité
Environment="APP_PORT=8080" "NODE_ENV=production"

ExecStart=/usr/local/bin/my-app --start

[Install]
WantedBy=multi-user.target

Limitations et sécurité

Bien que pratique, la directive Environment est un mauvais endroit pour les mots de passe, les jetons ou les identifiants de base de données. Les fichiers d'unité sont souvent stockés dans des systèmes de gestion de configuration, copiés dans des tickets, ou lisibles par des opérateurs qui doivent inspecter le comportement du service mais ne devraient pas voir les secrets. Utilisez-la pour des valeurs telles que les ports, les indicateurs de fonctionnalités, les niveaux de journalisation et les chemins.

2. Configuration externe : la directive EnvironmentFile

Pour des configurations plus volumineuses, charger les variables depuis un fichier externe est généralement plus propre. Cela vous permet de gérer les permissions du fichier de variables indépendamment du fichier d'unité principal. Cela permet également de garder les unités fournies par les paquets lisibles tandis que les paramètres locaux résident dans /etc.

Utilisation et syntaxe

La directive EnvironmentFile prend un chemin absolu vers un fichier de configuration. Systemd lit ce fichier ligne par ligne, traitant chaque ligne comme une assignation potentielle KEY=VALUE.

[Service]
# Charger les variables depuis un fichier externe
EnvironmentFile=/etc/config/my-app-settings.conf

ExecStart=/usr/local/bin/my-app --start

Format du fichier d'environnement

Le fichier externe doit adhérer à un format simple de type shell :

  • Les lignes commençant par # sont traitées comme des commentaires.
  • Les lignes commençant par une assignation de variable vide (VAR=) effaceront la variable si elle a été définie précédemment.
  • Les variables sont définies comme KEY=VALUE.
  • La mise entre guillemets de la valeur (KEY="VALUE WITH SPACES") est prise en charge.
# /etc/config/my-app-settings.conf

# Variables non sensibles
MAX_WORKERS=4
LOG_LEVEL=INFO

# Variable sensible (nécessite des permissions de fichier strictes et un contrôle d'accès minutieux)
DB_PASSWORD=SecureRandomString12345

Évitez les habitudes shell que l'analyseur de fichiers d'environnement de systemd ne prend pas en charge comme vous l'attendez. N'écrivez pas export KEY=value. Ne mettez pas d'espaces autour du signe égal. Si la valeur contient des espaces, mettez-la entre guillemets. Si la valeur contient des guillemets littéraux, des barres obliques inverses ou des sauts de ligne, testez-la avant de vous y fier en production.

Gestion des fichiers manquants

Par défaut, si le fichier spécifié par EnvironmentFile n'existe pas, Systemd échouera au démarrage du service. Si le fichier d'environnement est optionnel, vous pouvez préfixer le chemin du fichier avec un trait d'union (-) :

EnvironmentFile=-/etc/config/optional-settings.conf

Si le fichier est préfixé par -, Systemd ignorera les erreurs causées par l'absence du fichier.

Bonne pratique : Utiliser des unités de remplacement (drop-ins) pour les données sensibles

Modifier le fichier d'unité principal (par exemple, /usr/lib/systemd/system/my-app.service) est généralement déconseillé, surtout si le fichier est géré par un gestionnaire de paquets. Utilisez plutôt des fichiers d'unité de remplacement (drop-in) pour appliquer des surcharges ou des ajouts de configuration.

Cette pratique est importante car elle sépare les valeurs par défaut du fournisseur de la configuration locale. Elle facilite également les audits : l'unité indique d'où la configuration est chargée, et les permissions sur ce fichier indiquent qui peut le lire.

Configuration pas à pas d'un fichier de remplacement (drop-in)

1. Localiser/Créer le répertoire de remplacement (drop-in)

Pour un service nommé my-app.service, le répertoire de remplacement doit être nommé my-app.service.d/ et résider dans la hiérarchie /etc/systemd/system/.

sudo mkdir -p /etc/systemd/system/my-app.service.d/

2. Créer la surcharge de configuration

Créez un fichier dans le répertoire de remplacement (par exemple, secrets.conf). Ce fichier n'a besoin que de la section [Service] et des directives spécifiques que vous souhaitez surcharger ou ajouter.

# /etc/systemd/system/my-app.service.d/secrets.conf

[Service]
# Charger le fichier d'identifiants sécurisé
EnvironmentFile=/etc/secrets/my-app-credentials.env

3. Sécuriser le fichier d'environnement externe

C'est l'étape de sécurité la plus critique. Assurez-vous que le fichier externe contenant les secrets a des permissions restrictives. Idéalement, il devrait être détenu par root:root et uniquement lisible par l'utilisateur root ou l'utilisateur du service lui-même.

# Créer le fichier de secrets
sudo touch /etc/secrets/my-app-credentials.env

# Remplir le fichier avec les secrets
sudo sh -c 'echo "DB_PASS=S3cr3tP@ssw0rd" >> /etc/secrets/my-app-credentials.env'

# Définir des permissions restrictives
sudo chmod 600 /etc/secrets/my-app-credentials.env

Si le fichier référencé par EnvironmentFile contient des identifiants, gardez-le lisible uniquement par le compte qui doit gérer le service. 0600 root:root est courant lorsque systemd lit le fichier avant de réduire les privilèges avec User=, mais certains modèles opérationnels utilisent un groupe dédié détenu par root et 0640. L'important est que les utilisateurs ordinaires ne puissent pas lire le fichier.

Soyez également honnête quant au risque restant. Les variables d'environnement sont plus faciles à gérer que les arguments de ligne de commande codés en dur, mais elles ne constituent toujours pas un système complet de gestion des secrets. Pour les identifiants à haut risque, envisagez un magasin de secrets dédié, des identifiants à courte durée de vie, les identifiants systemd sur les distributions plus récentes, ou un mécanisme spécifique à l'application qui lit un fichier protégé directement.

Dépannage et vérification

Après avoir apporté des modifications aux fichiers d'unité ou aux fichiers de remplacement (drop-ins), vous devez recharger la configuration du gestionnaire Systemd.

sudo systemctl daemon-reload
sudo systemctl restart my-app.service

Pour vérifier quelles variables d'environnement ont été chargées avec succès par Systemd pour un service en cours d'exécution, utilisez la commande systemctl show et interrogez spécifiquement la propriété Environment :

systemctl show my-app.service --property=Environment

Exemple de sortie (montrant les variables chargées) :

Environment=APP_PORT=8080 NODE_ENV=production DB_PASS=S3cr3tP@ssw0rd

Cette commande est utile pour le débogage, mais c'est aussi un rappel : toute personne autorisée à exécuter les bonnes commandes d'inspection en tant que root peut voir les valeurs. Ne collez pas cette sortie dans un chat partagé, des tickets ou des rapports de bogues publics sans la caviarder.

Si le service ne démarre pas, vérifiez les journaux du service en utilisant journalctl -xeu my-app.service. Les raisons courantes d'échec liées aux variables d'environnement incluent :

  1. Chemin de fichier incorrect dans EnvironmentFile.
  2. Fichier manquant (et le chemin n'était pas préfixé par -).
  3. Syntaxe de variable incorrecte dans le fichier d'environnement externe (par exemple, des espaces autour du signe =).

Modèles pratiques qui fonctionnent

Scénario Directive à utiliser Bonne pratique d'emplacement Considérations de sécurité
Configuration statique et non sensible Environment Fichier d'unité direct ou fichier de remplacement (drop-in) Risque de sécurité faible.
Identifiants sensibles (Secrets) EnvironmentFile Fichier externe, référencé via un fichier de remplacement (drop-in) (*.service.d/) CRITIQUE : Le fichier d'environnement doit avoir les permissions 0600.
Modularité et surcharges EnvironmentFile Fichier d'unité de remplacement (drop-in) Sépare la configuration des valeurs par défaut du fournisseur.

En utilisant la directive EnvironmentFile dans une unité de remplacement (drop-in) dédiée et en garantissant des permissions de fichier strictes, les administrateurs peuvent gérer les configurations de service de manière sécurisée et flexible, en adhérant aux principes du moindre privilège et de la séparation des préoccupations.

Pour un petit service interne, une configuration raisonnable ressemble souvent à ceci :

# /etc/systemd/system/my-app.service.d/env.conf
[Service]
Environment="APP_ENV=production"
EnvironmentFile=/etc/my-app/runtime.env
EnvironmentFile=-/etc/my-app/local.env

runtime.env contient les valeurs requises. local.env est optionnel et permet à un opérateur de surcharger un paramètre pendant une fenêtre de maintenance sans modifier l'unité principale. Après un changement :

sudo systemctl daemon-reload
sudo systemctl restart my-app.service
sudo journalctl -u my-app.service -n 50 --no-pager

L'habitude la plus sûre est simple : gardez les valeurs par défaut non sensibles dans l'unité ou un fichier de configuration normal, gardez les secrets hors des unités appartenant aux paquets, verrouillez tout fichier contenant des identifiants et vérifiez l'environnement chargé sans le divulguer dans des endroits où il n'a pas sa place.

Erreurs courantes à éviter

La première erreur est de mettre des secrets dans ExecStart= :

ExecStart=/usr/local/bin/my-app --db-password=s3cret

Cela semble inoffensif lorsque vous êtes pressé, mais les arguments de ligne de commande sont souvent plus faciles à exposer que les fichiers d'environnement. Ils peuvent apparaître dans les listes de processus, les outils de surveillance, l'historique du shell, les rapports d'incident ou les définitions de service copiées. Si l'application prend en charge la lecture d'un fichier de configuration protégé, c'est généralement mieux. Si elle attend une variable d'environnement, utilisez un EnvironmentFile= protégé et gardez la valeur hors de la ligne de commande.

La deuxième erreur est de modifier directement l'unité du fournisseur. Une mise à jour du paquet peut remplacer le fichier, et le prochain redémarrage peut silencieusement supprimer vos paramètres d'environnement. Utilisez un fichier de remplacement (drop-in) :

sudo systemctl edit my-app.service

Ajoutez ensuite uniquement la surcharge locale :

[Service]
EnvironmentFile=/etc/my-app/my-app.env

La troisième erreur est de supposer que le service voit le même environnement shell que vous voyez dans votre terminal. Ce n'est généralement pas le cas. Votre shell interactif peut avoir des variables provenant de .bashrc, .profile, une session SSH ou un outil de déploiement. Un service système démarre à partir de l'environnement géré par systemd. Si l'application a besoin de PATH, JAVA_HOME, NODE_ENV, LD_LIBRARY_PATH ou d'une valeur similaire, définissez-la explicitement ou utilisez des chemins absolus.

Par exemple, ceci est fragile :

ExecStart=npm start

Ceci est plus facile à raisonner :

WorkingDirectory=/opt/my-app
Environment="NODE_ENV=production"
ExecStart=/usr/bin/npm start

La quatrième erreur est de rendre le fichier d'environnement accessible en écriture par l'utilisateur du service alors qu'il n'a pas besoin de l'être. Une application web qui peut écraser son propre fichier d'environnement peut transformer un bogue d'application normal en un problème de persistance. Dans de nombreuses configurations, l'utilisateur du service doit lire les données de l'application et écrire des journaux ou des téléchargements, mais il ne devrait pas pouvoir réécrire les identifiants utilisés pour démarrer le service.

Quand les variables d'environnement sont le mauvais outil

Les variables d'environnement sont populaires car elles sont simples, mais elles ne sont pas toujours la meilleure interface. Si une valeur est volumineuse, structurée, changée souvent ou partagée par plusieurs services, un véritable fichier de configuration ou un magasin de secrets est généralement plus facile à gérer.

Une URL de base de données est une variable d'environnement raisonnable :

DATABASE_URL=postgresql://[email protected]:5432/app

Un document JSON complet de compte de service est moins agréable. La mise entre guillemets devient maladroite, les sauts de ligne accidentels provoquent des échecs, et les gens sont plus susceptibles de le coller dans les journaux lors du débogage. Dans ce cas, stockez le JSON dans un fichier protégé et passez le chemin du fichier :

GOOGLE_APPLICATION_CREDENTIALS=/etc/my-app/google-service-account.json

Protégez ensuite le fichier JSON séparément :

sudo chown root:my-app /etc/my-app/google-service-account.json
sudo chmod 640 /etc/my-app/google-service-account.json

Cela ne rend pas le secret magique. L'application peut toujours le lire. Root peut toujours le lire. Mais cela évite de fourrer un secret complexe dans l'analyseur d'environnement de systemd et rend l'audit au niveau du fichier plus clair.

Une liste de vérification pour une révision plus sûre

Avant de redémarrer un service qui utilise des variables d'environnement, vérifiez quatre choses :

systemctl cat my-app.service
sudo ls -l /etc/my-app/my-app.env
sudo systemd-analyze verify /etc/systemd/system/my-app.service
sudo systemctl daemon-reload

systemctl cat confirme quels fichiers de remplacement (drop-ins) sont actifs. ls -l confirme que les permissions sont celles que vous souhaitiez. systemd-analyze verify peut détecter certains problèmes de syntaxe d'unité avant de redémarrer. Il ne validera pas tous les paramètres spécifiques à l'application, mais c'est toujours une barrière de sécurité utile.

Après le redémarrage, vérifiez le journal pour les erreurs de démarrage :

sudo systemctl restart my-app.service
sudo journalctl -u my-app.service -n 100 --no-pager

Si vous devez confirmer qu'une variable a été chargée, interrogez-la avec précaution et caviardez la sortie avant de la partager. Pour les services sensibles, je préfère vérifier d'abord une variable non secrète comme APP_ENV ou LOG_LEVEL. Si celle-ci a été chargée à partir du même fichier, le chemin du fichier et la syntaxe de l'analyseur sont probablement corrects, et vous n'aurez peut-être pas besoin d'imprimer les valeurs contenant des secrets.

Un dernier point pratique : planifiez la rotation avant d'en avoir besoin. Si un mot de passe ou un jeton est stocké dans un fichier d'environnement, notez quel service doit redémarrer après le changement de valeur et si ce redémarrage entraîne des temps d'arrêt. Un identifiant facile à définir mais difficile à faire tourner deviendra éventuellement un incident. Pour les petits services, le manuel de rotation peut ne comporter que quatre lignes :

sudoedit /etc/my-app/my-app.env
sudo systemctl restart my-app.service
sudo systemctl status my-app.service
sudo journalctl -u my-app.service -n 50 --no-pager

Cela suffit si tout le monde connaît le rayon d'impact. Pour les systèmes plus grands, préférez les identifiants qui peuvent se chevaucher pendant la rotation, afin de pouvoir déployer la nouvelle valeur, la vérifier et supprimer l'ancienne valeur sans une fenêtre d'arrêt précipitée.