Dans la partie 5, j’ai posé toute l’architecture sur le papier : Proxmox en cluster, tout-en-code, secrets avec SOPS, Kubernetes avec Talos, observabilité… Beaucoup de théorie, zéro commande. Je vous avais promis que la suite serait 100 % pratique. Nous y voilà.
On attaque la phase 0 de la roadmap. Et contre toute attente, cette première phase pratique ne touche pas au matériel. Pas encore. Avant de brancher le moindre serveur, il faut préparer le poste de travail et poser le socle “tout en code” : l’outillage, la racine de confiance qui chiffrera tous mes secrets, le squelette du dépôt versionné, et les garde-fous qui m’empêcheront de committer une bêtise.
Rappel du contexte matériel de cette phase : tout se passe sur ma workstation, un MacBook Pro (macOS avec une puce Apple Silicon), et le dépôt vit sur GitLab.com dans un projet privé que j’appellerai kentrowlab tout au long de la série.
L’objectif de la phase tient en une phrase : poser tout ce qui doit exister avant de toucher au matériel. À la fin, le socle est prêt à recevoir les phases suivantes. Ça se déroule en six étapes.
Étape 1 : installer la toolchain#
Tout passe par Homebrew, le gestionnaire de paquets de macOS. En une poignée de commandes, j’installe tout l’outillage IaC, secrets et qualité de code dont j’aurai besoin dès les premières phases :
# Cœur IaC + secrets + qualité (nécessaire dès les phases 0-3)
brew install opentofu ansible sops age pre-commit gitleaks ansible-lint git jq yq
# Packer (tap HashiCorp - licence BSL, cf. note plus bas)
brew install hashicorp/tap/packer
# tflint (tap dédié du projet terraform-linters)
brew tap terraform-linters/tap
brew install terraform-linters/tap/tflint
# CLI Kubernetes (utiles en phases 6-7, mais posées dès maintenant)
brew install kubectl helm argocd
brew install siderolabs/tap/talosctlUn petit détail qui a son importance : j’installe Packer via le tap HashiCorp et pas par le dépôt central. La raison est la même que celle qui m’a fait choisir OpenTofu plutôt que Terraform dans la partie 5 : Packer est passé en licence BSL en 2023.
Pour vérifier que tout est bien en place, un petit bloc de validation qui liste chaque binaire :
for t in tofu ansible sops age packer pre-commit gitleaks tflint ansible-lint \
kubectl helm argocd talosctl; do
printf "%-13s %s\n" "$t:" "$(command -v $t || echo 'ABSENT')"
doneÉtape 2 : la clé age, ma racine de confiance#
C’est l’étape la plus importante de toute la phase, et sans doute de tout le projet. Cette clé va déchiffrer tout ce que SOPS protégera par la suite. La perdre sans copie, c’est perdre l’accès à l’intégralité de mon contenu chiffré. Autant dire qu’il faut en prendre le plus grand soin.
Je génère la paire de clés age à l’emplacement que SOPS lit par défaut sur macOS :
mkdir -p "$HOME/Library/Application Support/sops/age"
chmod 700 "$HOME/Library/Application Support/sops/age"
age-keygen -o "$HOME/Library/Application Support/sops/age/keys.txt"
chmod 600 "$HOME/Library/Application Support/sops/age/keys.txt"La commande produit deux choses : une clé privée (AGE-SECRET-KEY-1...) qui ne quittera jamais ma workstation, et une clé publique (age1...) qu’on peut partager sans aucun risque et qui ira dans le dépôt à l’étape 4.
L’escrow, volontairement manuel#
Une clé aussi critique mérite une copie de secours. Je copie l’intégralité du fichier keys.txt dans une note sécurisée Bitwarden (mon gestionnaire de mots de passe). Et je le fais à la main, exprès : je ne veux surtout pas que cette clé privée transite par l’historique de mon shell ou un presse-papier scriptable.
Bitwarden n’est pas dans le chemin d’amorçage automatique du projet. C’est un coffre de récupération manuelle, pas une dépendance du pipeline. La copie destinée à la CI/CD, elle, ira en variable protégée, mais seulement en phase 3. Ici, on pose juste la racine de confiance et sa sauvegarde.
C’est le seul secret que je dois protéger manuellement. Tout le reste en découle.
Étape 3 : le squelette du dépôt#
Je crée un projet privé kentrowlab sur GitLab, sans README d’initialisation (je pousse mon propre squelette), et je le clone en local. Puis je monte une arborescence par couches, un dossier par outil de la chaîne :
mkdir -p \
answer-files \
ansible/inventory ansible/group_vars ansible/roles ansible/playbooks \
packer \
tofu/modules tofu/stacks \
argocd \
docsChaque couche de l’architecture a son dossier : answer-files/ pour l’install Proxmox, ansible/ pour la config des hôtes, tofu/ pour le provisionnement, packer/ pour les images, argocd/ pour le GitOps. C’est cloisonné, donc facile à faire évoluer (voire à séparer en plusieurs repos) plus tard.
Le .gitignore, première brique de sécurité#
C’est ici que se joue une bonne partie de l’hygiène du projet. Le principe est simple mais capital : je versionne les fichiers chiffrés (*.sops.*), et j’ignore absolument tout ce qui ne doit jamais entrer dans git, à savoir les clés privées, les fichiers déchiffrés, le state OpenTofu, les kubeconfig et talosconfig.
# --- Secrets & clés (JAMAIS dans git) ---
*.agekey
keys.txt
*.dec
*.decrypted
# NB : les fichiers chiffrés *.sops.yaml / *.sops.json SONT versionnés
# --- OpenTofu / Terraform ---
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
# On VERSIONNE le lockfile : ne pas ignorer .terraform.lock.hcl
# --- Kubernetes / Talos ---
kubeconfig
*.kubeconfig
talosconfig
*.talosconfigLe piège des dossiers fantômes#
Petit détail qui peut coûter cher : git ne versionne pas les dossiers vides. Mon arborescence toute neuve disparaîtrait donc au prochain clone, ce qui casserait net le principe “tout reconstructible depuis git”. La parade classique : déposer un fichier .gitkeep vide dans chaque dossier. Un petit find de contrôle permet de vérifier qu’aucun dossier vide ne traîne :
find . -path ./.git -prune -o -type d -empty -print # ne doit rien renvoyerÉtape 4 : câbler SOPS et prouver la chaîne#
Il reste à relier SOPS à ma clé age. Ça se fait via un fichier .sops.yaml à la racine, qui déclare que tout fichier de secrets sera chiffré pour ma clé publique :
# .sops.yaml
creation_rules:
- path_regex: .*\.sops\.(ya?ml|json|env)$
age: age1.........................................XCe fichier se committe sans problème : il ne contient que la clé publique et des règles, rien de secret.
Et maintenant, la validation : prouver que la chaîne tourne de bout en bout. Je crée un faux secret, je le chiffre, je le déchiffre, je nettoie :
cat > test.sops.yaml << 'EOF'
demo_secret: "hello-kentrowlab"
EOF
sops -e -i test.sops.yaml # chiffrement en place
cat test.sops.yaml # -> valeurs ENC[...], structure lisible
sops -d test.sops.yaml # -> réaffiche le clair
rm test.sops.yaml # ménageLe résultat est exactement celui attendu : la valeur devient ENC[AES256_GCM,...], mais la clé demo_secret reste lisible, et sops -d restitue bien le clair, le tout sans que j’aie eu à préciser où se trouve ma clé (SOPS la trouve tout seul dans ~/Library).
Ce comportement illustre un point essentiel de SOPS : il chiffre les valeurs, pas la structure. Concrètement, ça veut dire que mes diffs git restent exploitables : je verrai quelle clé a changé, sans jamais exposer sa valeur. C’est tout l’intérêt de pouvoir committer des secrets sans committer de secret.
Étape 5 : les garde-fous locaux (pre-commit)#
Avoir la mécanique de chiffrement, c’est bien. S’assurer de ne jamais l’oublier, c’est mieux. J’installe donc pre-commit avec un fichier .pre-commit-config.yaml qui empile plusieurs hooks : de l’hygiène de fichiers, gitleaks (détection de secrets), les linters terraform_fmt/tflint et ansible-lint, et surtout un hook maison qui refuse tout fichier *.sops.* qui ne serait pas chiffré.
# Garde-fous exécutés avant chaque commit. Voir https://pre-commit.com
minimum_pre_commit_version: "3.0.0"
repos:
# Hygiène de base des fichiers
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-added-large-files
args: ["--maxkb=2048"]
- id: check-yaml
exclude: '\.sops\.ya?ml$'
# Détection de secrets en clair
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
# Vérifie qu'aucun fichier de secrets SOPS n'est commité EN CLAIR
- repo: local
hooks:
- id: forbid-unencrypted-sops
name: "Refuse les fichiers .sops non chiffrés"
entry: >-
bash -c 'for f in "$@"; do
if [ -f "$f" ] && ! grep -q "ENC\[" "$f"; then
echo "ERREUR : $f n'\''est pas chiffré par SOPS"; exit 1;
fi; done' --
language: system
# On matche les fichiers de SECRETS (*.sops.*) mais PAS la config .sops.yaml
files: '\.sops\.(ya?ml|json|env)$'
exclude: '^\.sops\.yaml$'
# Lint OpenTofu / Terraform (format + règles)
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.108.0
hooks:
- id: terraform_fmt
- id: terraform_tflint
# Lint Ansible
- repo: https://github.com/ansible/ansible-lint
rev: v26.6.0
hooks:
- id: ansible-lintUne fois le fichier en place, on installe le hook git et on lance une première passe sur tout le dépôt :
pre-commit install
pre-commit run --all-filesDeux subtilités à noter. D’abord, terraform_fmt et tflint marchent parfaitement sur du code OpenTofu : le format .tf est identique, les hooks gardent juste le nom “terraform” pour des raisons historiques. Ensuite, pre-commit run --all-files n’agit que sur les fichiers connus de git. Tant que rien n’est git add, la plupart des hooks affichent “no files to check → Skipped”. Ils s’activeront vraiment au moment du commit.
Étape 6 : premier commit, CI, et clôture de la phase#
Dernière étape : rejouer les mêmes garde-fous côté serveur, parce qu’un hook local, ça se contourne (volontairement ou non). Un .gitlab-ci.yml minimal fait tourner la Secret Detection de GitLab (basée gitleaks elle aussi) plus un job maison qui vérifie qu’aucun fichier *.sops.* n’est en clair. Double barrière : local et serveur.
Puis vient le grand moment, le commit initial :
git add -A
git commit -m "chore: bootstrap phase 0 - squelette repo, SOPS+age, garde-fous"
git push -u origin mainL’accroc que je n’avais pas vu venir#
Et évidemment, ça n’est pas passé du premier coup. Mon propre garde-fou anti-secret s’est déclenché… sur sa propre configuration :
Refuse les fichiers .sops non chiffrés...................................Failed
ERREUR : .sops.yaml n'est pas chiffré par SOPSLe coupable : mon motif \.sops\.yaml$ matchait aussi le fichier .sops.yaml lui-même. Sauf que ce fichier, c’est la configuration de SOPS (clé publique + règles), il est public par nature et il ne doit surtout pas être chiffré, sinon SOPS ne peut plus lire ses propres règles. Le fix : exclure explicitement .sops.yaml du hook (exclude: '^\.sops\.yaml$') et appliquer le même écart côté CI.
C’est un faux positif qu’on ne voit qu’à l’exécution, et c’est exactement pour ça que je trouve précieux de tester ses garde-fous à vide, avant d’avoir de vrais secrets à protéger. Le jour où un vrai secret sera en jeu, le filet devra déjà être fiable.
Autre petit piège du premier commit : les hooks end-of-file-fixer et trailing-whitespace peuvent corriger un fichier au vol et faire échouer le commit. C’est voulu. On ré-indexe (git add -A) et on recommite, cette fois ça passe.
Après ce petit round d’ajustements, le pipeline GitLab passe au vert. La phase 0 est terminée.
L’état des lieux après la phase 0#
Si je fais le point, voilà ce qui est en place :
Toolchain complète sur la workstation : OpenTofu, Ansible, Packer, SOPS, age, les linters et les CLI Kubernetes
Racine de confiance : clé age générée et sauvegardée hors-ligne dans Bitwarden
Dépôt structuré par couches, prêt à accueillir chaque outil de la chaîne
Chaîne SOPS câblée et prouvée par un round-trip complet
Garde-fous en double barrière : pre-commit en local, CI côté serveur
Le dépôt versionné ne contient à ce stade que l’ossature : .gitignore, .gitlab-ci.yml, .pre-commit-config.yaml, .sops.yaml, un README.md, et tous les .gitkeep. Aucun keys.txt, aucun fichier déchiffré, aucun state. Exactement ce qu’on veut.
Ça peut sembler beaucoup de cérémonie pour n’avoir “rien déployé”. Mais c’est tout l’inverse : cette phase 0, c’est le socle qui rend toutes les suivantes rejouables et sûres. Le jour où je committe mon premier vrai secret, tout le filet est déjà en place et testé.
Et maintenant ?#
Le poste de travail est prêt, la racine de confiance est posée, le dépôt attend ses premières vraies briques. Dans la partie 7, on passe enfin au matériel : l’installation physique des quatre nœuds, avec les answer files de Proxmox et le JetKVM pour piloter tout ça à distance. On commence pour de bon à transformer le plan en machines qui tournent.
À bientôt !




