Lorsqu’un Mac cloud assure à lui seul la récupération du code, la compilation, les tests et la publication, la frontière la plus facile à négliger n’est pas celle des répertoires, mais celle de l’environnement des processus. Dès que le pipeline place un jeton dans une variable d’environnement, xcodebuild, les scripts, les hôtes de test et les sous-processus en arrière-plan en héritent par défaut. Même après la fin de la tâche principale, un processus auxiliaire encore actif peut conserver l’ensemble des variables. La bonne approche ne consiste pas à interdire les variables d’environnement, mais à réduire au strict minimum la durée pendant laquelle les secrets restent visibles et le nombre de processus qui peuvent y accéder.
Cartographier la propagation des variables entre les processus
Chaque processus lancé par le runner doit être classé dans l’une des quatre catégories suivantes : compilation, test, publication ou outil auxiliaire. La compilation et les tests n’ont généralement pas besoin des jetons de publication, et les outils auxiliaires ne doivent pas non plus en hériter. Consignez le nom de chaque variable, l’étape à laquelle elle est injectée, la commande qui l’utilise et le point de suppression prévu, sans jamais enregistrer sa valeur réelle.
| Élément contrôlé | Risque courant | Méthode de validation |
|---|---|---|
| Processus parent du runner | Reçoit tous les secrets au démarrage | Vérifier uniquement si les noms des variables existent |
| Sous-processus de compilation | Hérite sans condition de l’environnement parent | Tester l’héritage avec un marqueur aléatoire |
| Domaine launchd | Conserve les valeurs entre les tâches après setenv |
Les contrôler et les supprimer avant et après chaque tâche |
| Fichiers temporaires | Droits trop larges ou fichiers résiduels après un arrêt anormal | Définir les droits sur 600 et les supprimer avec un trap |
| Journaux | Le traçage du shell affiche les arguments des commandes | Désactiver set -x pendant la manipulation des secrets |
Un script d’audit doit uniquement déterminer si un marqueur donné est visible. N’enregistrez pas de captures de l’environnement comme artefacts de compilation. La sortie complète de
env,exportoups ewwpeut elle-même devenir une nouvelle source de fuite.
Tester l’héritage avec un marqueur aléatoire
N’utilisez jamais un véritable jeton pour effectuer ce test. Générez un marqueur à usage unique, lancez un sous-processus de courte durée, puis vérifiez silencieusement s’il peut voir ce marqueur. Le contrôle ci-dessous n’affiche pas le contenu du marqueur dans les journaux, mais son code de sortie indique au pipeline si l’héritage a eu lieu.
set -euo pipefail
marker="$(/usr/bin/uuidgen)"
export CI_SECRET_PROBE="$marker"
/bin/sleep 20 &
child_pid=$!
if /bin/ps eww -p "$child_pid" -o command= | /usr/bin/grep -q "$marker"; then
result=1
else
result=0
fi
kill "$child_pid" 2>/dev/null || true
wait "$child_pid" 2>/dev/null || true
unset CI_SECRET_PROBE
test "$result" -eq 1
Ce test confirme que l’héritage par défaut existe. Il ne signifie pas qu’il faille analyser régulièrement tous les processus dans les journaux de production. Pour une validation formelle, utilisez un nom de variable fixe associé à une valeur aléatoire et consignez uniquement « réussi » ou « échec ».
Rechercher les résidus dans launchd
N’injectez pas les secrets d’une tâche avec launchctl setenv, car leur durée de vie peut dépasser celle d’une session shell. Si d’anciens scripts ont utilisé cette méthode, contrôlez d’abord l’état sans afficher la valeur, puis exécutez :
launchctl unsetenv CI_RELEASE_TOKEN
test -z "$(launchctl getenv CI_RELEASE_TOKEN)"
Effectuez ce contrôle une première fois au début de la tâche, puis une seconde fois pendant le nettoyage. Si le runner s’exécute sous un utilisateur dédié, lancez la vérification dans le domaine launchd correspondant à cet utilisateur, au lieu de contrôler uniquement la session distante en cours.
Créer un environnement minimal pour la compilation
env -i permet de lancer une commande avec un environnement vide, mais il ne faut pas tout supprimer sans discernement. La chaîne d’outils Xcode a généralement besoin de HOME, PATH, TMPDIR, des paramètres régionaux et d’un répertoire de développement explicite. Validez d’abord l’environnement avec une commande affichant la version, puis lancez la compilation complète.
set -euo pipefail
job_tmp="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/mac-ci.XXXXXX")"
trap '/bin/rm -rf "$job_tmp"' EXIT HUP INT TERM
/usr/bin/env -i \
HOME="$HOME" \
PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
TMPDIR="$job_tmp/" \
LANG="en_US.UTF-8" \
DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" \
/usr/bin/xcrun xcodebuild -version
Si le projet dépend de Ruby, de Node.js ou d’un gestionnaire de paquets, ajoutez individuellement à PATH les répertoires contenant les exécutables nécessaires, sans restaurer l’intégralité de l’environnement du shell de connexion. Pour chaque variable ajoutée, précisez quelle commande en a besoin.
Séparer la compilation et la publication en deux domaines d’autorisation
L’étape de compilation doit uniquement produire les archives et les fichiers de contrôle, sans accéder aux identifiants de publication. L’étape de publication doit lire les artefacts déjà validés et injecter uniquement les variables requises par la commande d’envoi. N’exécutez pas export CI_RELEASE_TOKEN au début de la tâche, car tous les scripts suivants en hériteraient.
set +x
CI_RELEASE_TOKEN="$release_token" \
/usr/bin/env \
RELEASE_ARTIFACT="$artifact_path" \
./ci/publish.sh
unset release_token
set -x
Une solution plus sûre consiste à faire en sorte que publish.sh ne lance qu’un seul processus d’envoi, attende qu’il se termine, puis s’arrête immédiatement. Si l’outil exige un fichier d’identifiants, utilisez un répertoire temporaire propre à la tâche, définissez umask 077 avant de créer le fichier, puis vérifiez que ses droits sont bien réglés sur 600. Ne placez jamais ce fichier dans le dépôt, sur le bureau de l’utilisateur ou dans un répertoire de cache persistant.
Limiter les gestionnaires d’échec
Les hooks exécutés en cas d’échec collectent souvent des informations sur l’environnement, les processus et l’espace de travail. Ils ne doivent pas exécuter env sans filtrage ni archiver les lignes de commande complètes. Ils peuvent recueillir les codes de sortie, les versions des outils, les chemins des artefacts, l’espace disque disponible et les noms de variables sélectionnés au moyen d’une liste blanche. Lorsqu’il est nécessaire de rechercher un secret, comparez uniquement des empreintes ou vérifiez la présence d’une sonde aléatoire, sans conserver la valeur d’origine.
Faire du nettoyage une condition de validation de la tâche
La réussite d’une tâche ne garantit pas celle du nettoyage. L’étape finale doit au minimum vérifier que les variables de publication ont été supprimées avec unset, que les noms correspondants sont absents de launchd, que les répertoires temporaires n’existent plus, que les sous-processus de test et d’envoi se sont arrêtés et que le traçage des commandes n’est pas activé dans les journaux. Si l’un de ces contrôles échoue, marquez le runner concerné comme nécessitant une inspection et empêchez-le d’accepter d’autres tâches de publication contenant des secrets.
Créez un TMPDIR distinct pour chaque tâche et utilisez le groupe de processus comme unité de nettoyage. Arrêter uniquement le script principal peut laisser actifs des processus auxiliaires de simulateur, des outils d’envoi ou des démons personnalisés. Après le nettoyage, exécutez une nouvelle sonde aléatoire ne contenant aucune valeur réelle et confirmez que les nouveaux sous-processus ne peuvent pas observer le marqueur de l’étape précédente.
L’objectif n’est pas d’éliminer totalement les variables d’environnement du pipeline, mais d’établir des frontières claires : l’étape de compilation ne possède aucun secret de publication, un seul processus indispensable peut voir le secret pendant l’étape de publication et, une fois la tâche terminée, ni les processus, ni launchd, ni le disque ne le conservent. En intégrant ces contrôles aux scripts d’initialisation et de finalisation du runner, les projets suivants ne dépendront plus de la mémoire de l’opérateur.
Questions fréquentes
Un secret CI stocké uniquement dans une variable d’environnement est-il protégé ?
Non. La variable est héritée par les sous-processus et peut apparaître dans un diagnostic, une sortie de débogage ou un service persistant. Injectez-la uniquement dans la commande qui en a besoin.
Comment vérifier qu’aucune variable secrète ne subsiste après une tâche ?
Contrôlez le processus parent du runner, le domaine launchd et les sous-processus survivants par nom de variable, puis vérifiez la suppression du répertoire temporaire sans afficher la valeur réelle.
env -i peut-il empêcher xcodebuild de fonctionner ?
Oui, car l’environnement par défaut est supprimé. Fournissez explicitement HOME, PATH, TMPDIR, LANG et DEVELOPER_DIR, puis validez la liste avec une petite compilation avant de la généraliser.
Placez votre prochaine compilation dans le cloud sur un Mac mini
Choisissez la configuration, la durée de facturation et la région du nœud pour exécuter vos tâches de développement, de compilation et d’automatisation sur une machine physique dédiée.