test
Terraform - plan / plan (push) Failing after 6s

This commit is contained in:
hcornet committed 2026-09-13 17:54:01 +02:00
1 parent d3828b601d
commit 8f279465bc
11 files changed
+186 -64

No files matched your search

+3 -3
View File
@@ -6,7 +6,7 @@ on:
push:
branches: [main]
# Un seul job Terraform à la fois : l'état est partagé, autant ne pas se battre
# Un seul job Terraform à la fois : l'état est partagé, autant ne pas se battre
# avec le verrou du backend.
concurrency:
group: terraform-proxmox-debian-12
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Récupérer le dépôt
- name: Récupérer le dépôt
uses: actions/checkout@v4
- name: Installer les outils
@@ -42,7 +42,7 @@ jobs:
- name: Charger les secrets depuis OpenBao
run: ./scripts/openbao-env.sh
- name: Vérifier le formatage
- name: Vérifier le formatage
run: terraform fmt -check -recursive -diff
- name: Initialiser le backend S3
+5
View File
@@ -9,3 +9,8 @@ override.tf
override.tf.json
backend.hcl
tfplan
# Seules les cles publiques sont versionnees
cles/*
!cles/*.pub
!cles/README.md
+27 -14
View File
@@ -18,25 +18,38 @@ humain, ou la console Proxmox quand le réseau est cassé.
`AllowUsers` est généré à partir de la liste des comptes : un compte absent de
`local_accounts` ne peut pas ouvrir de session, même s'il existe sur le système.
## Les clés à produire
## Où vivent les clés
Deux paires restent à créer, la clé `hcornet` étant déjà la vôtre.
Un fichier `.pub` par identité dans `cles/`, référencé par `lab.tfvars`. Rien n'est
recopié dans le `.tfvars` lui-même.
```powershell
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\id_ed25519_invite -C "invite@centreon"
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\id_ed25519_ansible -C "ansible@automatisation"
Get-Content $env:USERPROFILE\.ssh\id_ed25519_invite.pub
Get-Content $env:USERPROFILE\.ssh\id_ed25519_ansible.pub
```
cles/
├── hcornet.pub livré, c'est votre clé
├── invite.pub à déposer pour activer le compte invite
└── ansible.pub à déposer pour l'automatisation
```
Recopier ces deux lignes dans `lab.tfvars`, à la place des `REMPLACER par...`. La clé
d'automatisation apparaît **deux fois** : sur le compte `ansible` et dans
`root_ssh_public_keys`. C'est voulu — le compte `ansible` permet de tester la liaison
sans privilège, tandis que la connexion root sert aux configurations.
Un compte dont `ssh_key_files` est vide **n'est pas créé**. `invite` et `ansible` sont
donc déclarés dans `lab.tfvars` mais inactifs : déposer leur `.pub` et compléter la
liste suffit à les activer au prochain déploiement.
La clé privée d'automatisation ira dans le magasin de clés de Semaphore. Elle ne doit
pas rester sur votre poste une fois Semaphore configuré.
Deux garde-fous avant création, faute de quoi une VM inaccessible serait produite et
cloud-init ne rejoue rien :
- un fichier référencé mais absent fait échouer le plan à la lecture ;
- une valeur qui ne ressemble pas à une clé publique est refusée par une précondition.
C'est ce second contrôle qui manquait : la version précédente vérifiait qu'une clé
était présente, pas qu'il s'agissait bien d'une clé.
### Votre clé donne aussi l'accès root
`root_ssh_key_files` contient `cles/hcornet.pub` : `ssh [email protected]` fonctionne
directement, sans mot de passe. Votre clé privée est donc un identifiant root à part
entière — ce qui rend sa phrase de passe et sa protection d'autant plus importantes.
Le jour où Ansible entre en jeu, ajouter `cles/ansible.pub` à cette même liste.
## Le mot de passe root
+24
View File
@@ -0,0 +1,24 @@
# Clés publiques
Un fichier `.pub` par identité, repris par `lab.tfvars`. Seules des clés **publiques**
ont leur place ici : elles sont versionnées, ce qui est leur usage normal.
| Fichier | Utilisé par |
|---|---|
| `hcornet.pub` | compte `hcornet` et accès root |
| `invite.pub` | compte `invite`, à déposer pour activer le compte |
| `ansible.pub` | compte `ansible` et accès root d'Ansible, à déposer |
Un compte dont la liste `ssh_key_files` est vide n'est pas créé du tout : c'est ainsi
que `invite` et `ansible` restent en attente tant que leur clé n'existe pas.
Pour ajouter une identité : déposer son `.pub` ici, puis la déclarer dans `lab.tfvars`.
Un fichier référencé mais absent fait échouer le plan, et une clé mal formée est
refusée avant la création de la VM.
Générer une paire, si besoin :
```powershell
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\id_ed25519_ansible -C "ansible@automatisation"
Get-Content $env:USERPROFILE\.ssh\id_ed25519_ansible.pub | Set-Content -NoNewline cles\ansible.pub
```
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDIXkfS5dM3po9Jz7NKgd3t1H4vqh0/cVoxrAP42ivd thedj@ZEU
+39 -5
View File
@@ -1,9 +1,26 @@
locals {
fqdn = "${var.vm_hostname}.${var.dns_domain}"
# root d'abord, puis les comptes humains et de service : c'est la liste
# exhaustive des comptes autorisés à ouvrir une session SSH.
allow_users = join(" ", concat(["root"], [for compte in var.local_accounts : compte.name]))
# Les clés sont lues dans les fichiers du répertoire cles/. Un fichier
# référencé mais absent fait échouer le plan, ce qui est le comportement
# voulu : mieux vaut un plan rouge qu'une VM inaccessible.
# Un compte sans aucun fichier de clé n'est pas créé.
comptes = [
for compte in var.local_accounts : {
name = compte.name
gecos = compte.gecos
ssh_keys = [for fichier in compte.ssh_key_files : trimspace(file("${path.module}/${fichier}"))]
}
if length(compte.ssh_key_files) > 0
]
cles_root = [for fichier in var.root_ssh_key_files : trimspace(file("${path.module}/${fichier}"))]
toutes_les_cles = concat(local.cles_root, flatten([for compte in local.comptes : compte.ssh_keys]))
# root d'abord, puis les comptes réellement créés : c'est la liste exhaustive
# des comptes autorisés à ouvrir une session SSH.
allow_users = join(" ", concat(["root"], [for compte in local.comptes : compte.name]))
}
# Mot de passe root : sert à « su - » et à la console Proxmox, jamais en SSH.
@@ -35,11 +52,28 @@ resource "proxmox_virtual_environment_file" "user_data" {
fqdn = local.fqdn
timezone = var.timezone
locale = var.locale
comptes = var.local_accounts
cles_root = var.root_ssh_public_keys
comptes = local.comptes
cles_root = local.cles_root
allow_users = local.allow_users
admin_cidrs = var.ssh_allowed_cidrs
root_password_hash = random_password.root.bcrypt_hash
})
}
lifecycle {
# Garde-fou : une valeur qui n'est pas une clé publique produirait une VM
# sur laquelle personne ne pourrait se connecter, et cloud-init ne rejoue rien.
precondition {
condition = alltrue([
for cle in local.toutes_les_cles :
can(regex("^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp[0-9]+|sk-ssh-ed25519) AAAA", cle))
])
error_message = "Un des fichiers de cles/ ne contient pas une clé publique valide."
}
precondition {
condition = length(local.comptes) > 0
error_message = "Aucun compte n'a de clé : la machine serait inaccessible."
}
}
}
+14 -21
View File
@@ -18,33 +18,26 @@ ssh_allowed_cidrs = ["10.0.4.0/24", "10.0.5.0/24"]
# Comptes de la machine. Aucun n'a de privilège : sudo est absent du système.
# Pour administrer, ouvrir une session avec son compte puis « su - ».
# Une clé publique n'est pas un secret, sa place est bien ici.
# Les clés vivent dans cles/, un fichier .pub par identité.
# Une liste ssh_key_files vide signifie : compte déclaré mais non créé.
local_accounts = [
{
name = "hcornet"
gecos = "Hubert Cornet - administration"
ssh_keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDIXkfS5dM3po9Jz7NKgd3t1H4vqh0/cVoxrAP42ivd thedj@ZEU",
]
name = "hcornet"
gecos = "Hubert Cornet - administration"
ssh_key_files = ["cles/hcornet.pub"]
},
{
name = "invite"
gecos = "Compte de consultation - aucun privilege"
ssh_keys = [
"REMPLACER par la cle publique du compte invite",
]
name = "invite"
gecos = "Compte de consultation - aucun privilege"
ssh_key_files = []
},
{
name = "ansible"
gecos = "Compte de service - sans privilege"
ssh_keys = [
"REMPLACER par la cle publique d automatisation",
]
name = "ansible"
gecos = "Compte de service - sans privilege"
ssh_key_files = []
},
]
# Clés autorisées à ouvrir une session directement en root : c'est par là
# qu'Ansible obtient ses droits, sudo n'existant pas. Rien d'humain ici.
root_ssh_public_keys = [
"REMPLACER par la cle publique d automatisation",
]
# Clés autorisées à ouvrir une session directement en root.
# Y ajouter cles/ansible.pub le jour où l'automatisation entre en jeu.
root_ssh_key_files = ["cles/hcornet.pub"]
+2 -2
View File
@@ -15,12 +15,12 @@ output "vm_fqdn" {
output "comptes" {
description = "Comptes ouverts sur la machine, hors root."
value = [for compte in var.local_accounts : compte.name]
value = [for compte in local.comptes : compte.name]
}
output "ssh_command" {
description = "Commande de connexion une fois cloud-init terminé."
value = "ssh ${var.local_accounts[0].name}@${var.vm_ipv4_address}"
value = "ssh ${local.comptes[0].name}@${var.vm_ipv4_address}"
}
output "root_password" {
+1 -3
View File
@@ -11,9 +11,7 @@
#
set -euo pipefail
# Adresse d'OpenBao : valeur par défaut du homelab, surchargeable par
# une variable d'environnement si besoin.
OPENBAO_ADDR="${OPENBAO_ADDR:-https://openbao.tips-of-mine.com}"
: "${OPENBAO_ADDR:?variable OPENBAO_ADDR absente}"
: "${OPENBAO_ROLE_ID:?variable OPENBAO_ROLE_ID absente}"
: "${OPENBAO_SECRET_ID:?variable OPENBAO_SECRET_ID absente}"
: "${GITHUB_ENV:?ce script est prévu pour un job Gitea Actions}"
+55
View File
@@ -0,0 +1,55 @@
name: Terraform - plan
on:
pull_request:
branches: [main]
push:
branches: [main]
# Un seul job Terraform à la fois : l'état est partagé, autant ne pas se battre
# avec le verrou du backend.
concurrency:
group: terraform-proxmox-debian-12
cancel-in-progress: false
env:
TF_VERSION: "1.9.8"
TF_IN_AUTOMATION: "true"
TF_INPUT: "false"
OPENBAO_ROLE_ID: ${{ secrets.OPENBAO_ROLE_ID }}
OPENBAO_SECRET_ID: ${{ secrets.OPENBAO_SECRET_ID }}
jobs:
plan:
runs-on: ubuntu-latest
steps:
- name: Récupérer le dépôt
uses: actions/checkout@v4
- name: Installer les outils
run: |
set -euo pipefail
if ! command -v jq >/dev/null || ! command -v unzip >/dev/null; then
apt-get update -qq
apt-get install -y -qq curl jq unzip
fi
curl -fsSL -o /tmp/terraform.zip \
"https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip"
unzip -oq /tmp/terraform.zip -d /usr/local/bin
terraform version
- name: Charger les secrets depuis OpenBao
run: ./scripts/openbao-env.sh
- name: Vérifier le formatage
run: terraform fmt -check -recursive -diff
- name: Initialiser le backend S3
run: terraform init
- name: Valider la configuration
run: terraform validate
- name: Calculer le plan
run: terraform plan -var-file=lab.tfvars -lock-timeout=120s
+15 -16
View File
@@ -195,13 +195,16 @@ variable "local_accounts" {
Comptes unix créés par cloud-init. Aucun n'a de privilège : la commande sudo
est retirée du système. Pour administrer, on ouvre une session avec son
compte puis on bascule par « su - » avec le mot de passe root.
Le champ ssh_keys ne doit contenir que des clés publiques.
ssh_key_files référence des fichiers du répertoire cles/, relatifs à la racine
du dépôt. Un compte sans aucun fichier de clé n'est pas créé : c'est le moyen
de déclarer un compte à l'avance sans l'activer.
EOT
type = list(object({
name = string
gecos = optional(string, "")
ssh_keys = list(string)
name = string
gecos = optional(string, "")
ssh_key_files = list(string)
}))
validation {
@@ -209,29 +212,25 @@ variable "local_accounts" {
error_message = "Au moins un compte est nécessaire pour accéder à la machine."
}
validation {
condition = alltrue([for c in var.local_accounts : length(c.ssh_keys) > 0])
error_message = "Chaque compte doit avoir au moins une clé publique : l'authentification par mot de passe est désactivée en SSH."
}
validation {
condition = !contains([for c in var.local_accounts : c.name], "root")
error_message = "Le compte root se configure par root_ssh_public_keys, pas par local_accounts."
error_message = "Le compte root se configure par root_ssh_key_files, pas par local_accounts."
}
}
variable "root_ssh_public_keys" {
variable "root_ssh_key_files" {
description = <<-EOT
Clés autorisées à ouvrir une session SSH directement en root. C'est la voie
d'accès d'Ansible, sudo n'existant pas sur la machine. À réserver à
l'automatisation : une clé humaine n'a rien à faire ici.
Fichiers de clés publiques autorisées à ouvrir une session SSH directement en
root, sudo n'existant pas sur la machine. C'est par là qu'Ansible obtient ses
droits, et c'est aussi la voie d'accès root d'un administrateur qui y dépose
sa clé.
EOT
type = list(string)
validation {
condition = length(var.root_ssh_public_keys) > 0
error_message = "Sans clé root, aucune automatisation ne pourra configurer la machine."
condition = length(var.root_ssh_key_files) > 0
error_message = "Sans clé root, aucune configuration ne serait possible sur la machine."
}
}