Isoler les secrets des services et vérifier leur rotation avec systemd
Lorsqu’un même serveur dédié héberge un service d’API, des tâches de synchronisation et des agents de collecte pour la supervision, les jetons d’accès se retrouvent souvent dispersés dans les scripts de démarrage, les fichiers d’environnement et les traces de déploiement. L’inspection des arguments des processus, les journaux d’erreur ou un simple archivage de configuration peuvent alors élargir leur exposition. Cet article utilise LoadCredential de systemd sous Debian 12 pour conserver les fichiers sources contenant les secrets dans un répertoire contrôlé par l’administrateur, ne fournir une copie à l’exécution qu’au service désigné et vérifier la procédure de rotation au moyen de contrôles reproductibles.
1. Définir les limites de l’isolation des secrets
L’objectif est de déterminer comment un service accède à un secret, et non de traiter l’émission des clés, leur conservation centralisée ou les protocoles d’authentification à distance. Sur les serveurs dédiés CloudDDR, l’utilisateur administre le système et les données ; après le paiement, il consulte les identifiants de connexion dans la console, puis accède au système par SSH. Les opérations suivantes nécessitent des droits d’administration. Effectuez-les d’abord sur un service de test, sans modifier directement le démarrage des services en production.
Les variables d’environnement ne devraient pas être le support par défaut des secrets : elles peuvent se retrouver dans les sorties de débogage, l’environnement des processus enfants ou les archives de diagnostic. Les arguments de démarrage peuvent eux aussi être enregistrés par des outils d’inspection des processus. Avec LoadCredential, le gestionnaire de services système lit le fichier source et fournit un répertoire de secrets au démarrage du service. Le programme utilise CREDENTIALS_DIRECTORY pour y retrouver le fichier.
L’isolation des fichiers contenant des secrets ne protège pas contre un processus disposant déjà de droits d’administration et n’empêche pas l’application d’écrire des secrets dans les journaux. Le périmètre de protection doit donc couvrir à la fois les permissions des fichiers, le compte de service et les sorties de l’application.
Dans cet exemple, le fichier source reste en clair : l’utilisation de LoadCredential ne le chiffre pas automatiquement au repos. Les sauvegardes du serveur, les accès administrateur et les enregistrements liés à l’émission des clés doivent également être traités comme des données sensibles.
2. Créer un compte dédié et un fichier source à accès restreint
Préparer un environnement de vérification distinct
Vérifiez la version avec systemctl --version. Cet exemple utilise systemd fourni avec Debian 12 et nécessite l’installation de Python 3. Assurez-vous que le nom du compte n’est pas déjà utilisé par un service existant ; la commande de création ci-dessous ne doit être exécutée que si le compte n’existe pas encore.
sudo apt-get update
sudo apt-get install python3
id credential-check
sudo useradd --system --no-create-home --shell /usr/sbin/nologin credential-check
sudo install -d -o root -g root -m 0700 /etc/service-secrets
sudo sh -c 'umask 077; printf "%s" "fixture-token-alpha" > /etc/service-secrets/api-token'
sudo stat -c '%U %G %a %n' /etc/service-secrets /etc/service-secrets/api-token
N’exécutez useradd qu’après avoir constaté avec id que le compte n’existe pas. S’il existe déjà, vérifiez son usage au lieu de réutiliser un compte dont vous ignorez la fonction. Les permissions attendues sont 700 pour le répertoire et 600 pour le fichier, avec root comme propriétaire et comme groupe. La chaîne utilisée dans l’exemple est une donnée de test qui ne confère aucun accès ; elle peut donc apparaître dans l’historique des commandes. Ne saisissez jamais un véritable jeton directement dans une commande de cette manière.
La valeur réelle doit être reçue par un transfert de fichier contrôlé, puis installée à l’emplacement prévu avec les droits d’administration. Ne faites pas transiter les secrets par des fichiers temporaires d’éditeur, par un affichage dans le terminal ou par un Shell dont le traçage est activé. Le compte de service ne doit pas pouvoir lire le fichier source :
sudo -u credential-check test -r /etc/service-secrets/api-token
printf '%s\n' "$?"
Le code de sortie attendu est 1. S’il vaut 0, vérifiez d’abord les permissions du répertoire, les ACL du fichier et les groupes auxquels appartient le compte. Ne poursuivez pas le déploiement.
3. Limiter le programme à la lecture du secret fourni à l’exécution
Enregistrez le programme suivant dans /usr/local/libexec/credential-check.py, après avoir créé son répertoire parent, et attribuez-lui root comme propriétaire avec les permissions 755. Ni le répertoire ni le programme ne doivent être modifiables par le compte de service. Le programme ne renvoie que des messages d’erreur fixes : il n’affiche ni le jeton, ni sa longueur, ni son empreinte, afin que la vérification ne crée pas elle-même un canal de fuite.
import os
from pathlib import Path
directory = os.environ.get("CREDENTIALS_DIRECTORY")
if not directory:
raise SystemExit("Credential directory missing")
try:
token = (Path(directory) / "api-token").read_bytes()
except OSError:
raise SystemExit("Credential unavailable") from None
if not token or b"\n" in token or b"\r" in token:
raise SystemExit("Credential format rejected")
Cet exemple exige un jeton non vide et sans retour à la ligne ; ce n’est pas une règle valable pour tous les formats de secrets. Une application réelle doit valider le format conformément au protocole du service distant. Si celui-ci accepte, par exemple, du contenu PEM sur plusieurs lignes, ce contrôle ne peut pas être repris tel quel.
Créez /etc/systemd/system/credential-check.service :
[Unit]
Description=Service credential validation
[Service]
Type=oneshot
User=credential-check
Group=credential-check
LoadCredential=api-token:/etc/service-secrets/api-token
ExecStart=/usr/bin/python3 /usr/local/libexec/credential-check.py
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
UMask=0077
Cette unité effectue une vérification ponctuelle : il est normal qu’elle passe à l’état inactive après une exécution réussie. Il n’est pas nécessaire de l’activer au démarrage du système, ni de lui appliquer, pour ce test, la politique de redémarrage d’un service de production. Une application réelle peut lire le fichier de la même façon, mais les restrictions du système de fichiers doivent être vérifiées en tenant compte des répertoires dans lesquels elle doit écrire. Ne recopiez pas ces réglages sans les adapter.
4. Vérifier séparément l’accès au secret et l’authentification
sudo systemctl daemon-reload
sudo systemctl start credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
sudo journalctl -u credential-check.service -n 20 --no-pager
Les résultats attendus sont Result=success et ExecMainStatus=0. Ils indiquent que le compte de service peut lire le secret depuis le répertoire fourni à l’exécution, alors que le contrôle précédent a établi qu’il ne pouvait pas lire directement le fichier source. Ne cherchez pas à le démontrer avec cat, un affichage de toutes les variables d’environnement ou un vidage de la mémoire du processus.
Ce contrôle ne prouve pas que le jeton est valide. Une fois le mécanisme intégré au service réel, effectuez aussi une requête d’authentification qui ne modifie aucune donnée métier, puis vérifiez l’état de la réponse et les autorisations attendues. En cas d’échec d’authentification, contrôlez séparément le format du secret, l’activation du nouveau jeton par le service distant, le redémarrage effectif de l’application et l’éventuelle utilisation d’une ancienne connexion ou d’un cache par la requête.
Le compte rendu de validation doit conserver uniquement la version de l’unité, l’ordre des opérations, les codes de sortie et le résultat de l’authentification, jamais la valeur du jeton. Pour rattacher ces informations à une rotation précise, utilisez un numéro de changement interne plutôt qu’une empreinte du jeton. Lors de l’examen des journaux, vérifiez également que les chemins d’erreur n’affichent ni les en-têtes de requête ni la configuration complète.
5. Effectuer la rotation et le retour arrière par remplacement atomique
La mise à jour du fichier source ne modifie pas la copie du secret utilisée par un service en cours d’exécution. La rotation doit suivre cet ordre : obtenir la nouvelle valeur, valider son format, remplacer le fichier de manière atomique, redémarrer le service, vérifier l’authentification, puis révoquer l’ancienne valeur conformément aux règles du service distant. La seule exécution de daemon-reload ne renouvelle pas les secrets d’un service déjà en cours d’exécution.
Les commandes suivantes ne remplacent que les données de test. Le fichier temporaire et le fichier cible se trouvent dans le même répertoire afin que le renommage ait lieu sur le même système de fichiers.
sudo sh -c 'umask 077; printf "%s" "fixture-token-beta" > /etc/service-secrets/api-token.next'
sudo chown root:root /etc/service-secrets/api-token.next
sudo chmod 0600 /etc/service-secrets/api-token.next
sudo mv -f /etc/service-secrets/api-token.next /etc/service-secrets/api-token
sudo systemctl restart credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
Simulez ensuite un échec : remplacez le fichier source de test par un fichier vide, puis démarrez l’unité de vérification. Le résultat attendu est un code de sortie non nul accompagné du message fixe d’erreur de format. Rétablissez une valeur de test non vide et relancez l’unité ; la vérification doit alors réussir. Ce test permet de confirmer que l’application ne continue pas silencieusement à fonctionner en l’absence de secret. N’effectuez pas de simulation destructive sur un jeton de production.
Avant une rotation réelle, vérifiez si le service distant peut accepter simultanément l’ancien et le nouveau jeton. Si ce n’est pas le cas, prévoyez une bascule coordonnée. Si l’ancienne valeur a déjà été révoquée, restaurer l’ancien fichier ne rétablira pas l’authentification. Ne conservez l’ancienne valeur que pendant une période de retour arrière contrôlée, avec les mêmes protections d’accès ; ne laissez pas de copie durable dans les répertoires de l’application.
Une fois la validation terminée, désactivez et supprimez l’unité de test, le script et les secrets de test, exécutez daemon-reload, puis vérifiez qu’aucun fichier temporaire ne subsiste. Le résultat final doit être une procédure vérifiable : un compte ordinaire ne peut pas lire le fichier source, le service désigné peut lire le secret fourni à l’exécution, une valeur incorrecte provoque un échec explicite, l’authentification réelle réussit après la rotation et les journaux ne contiennent aucune valeur sensible.
Questions fréquentes
LoadCredential protège-t-il les secrets contre les administrateurs ?
Non. Les administrateurs et les processus suffisamment privilégiés peuvent encore lire le fichier source ou la mémoire du service. Ce mécanisme réduit l’exposition dans les arguments, les variables d’environnement et les comptes ordinaires ; il ne protège pas contre un administrateur.
Un service actif reçoit-il automatiquement le secret après remplacement du fichier ?
Non. LoadCredential charge les données au démarrage du service. Il faut remplacer atomiquement le fichier source, redémarrer le service et vérifier l’authentification avec le nouveau secret. daemon-reload seul ne renouvelle pas les données actives.
Serveur dédié AMD EPYC 7543P à partir de $447/mois.