1.0.4 — contournement Cloudflare reellement applique
Terraform - plan / plan (push) Failing after 5s
Terraform - plan / plan (push) Failing after 5s
This commit is contained in:
1 parent
06f77d0e09
commit
b8b0d26f6f
11 files changed
+340
-115
No files matched your search
@@ -66,6 +66,27 @@ jobs:
|
||||
echo "SSH : $(terraform output -raw ssh_command)"
|
||||
echo "cloud-init tourne encore 3 à 6 minutes après la fin de ce job."
|
||||
|
||||
- name: Extraire le mot de passe root
|
||||
if: ${{ inputs.action == 'apply' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
terraform output -raw root_password > mot-de-passe-root.txt
|
||||
echo "Mot de passe root publie en artefact : a deposer dans Vaultwarden, puis supprimer l artefact."
|
||||
|
||||
- name: Publier le mot de passe root
|
||||
if: ${{ inputs.action == 'apply' }}
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mot-de-passe-root
|
||||
path: mot-de-passe-root.txt
|
||||
retention-days: 1
|
||||
|
||||
- name: Effacer la copie locale du mot de passe
|
||||
if: ${{ always() }}
|
||||
run: rm -f mot-de-passe-root.txt
|
||||
|
||||
- name: Publier l'inventaire Ansible
|
||||
if: ${{ inputs.action == 'apply' }}
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,124 +1,120 @@
|
||||
# CI/CD — `terraform-proxmox-serveur-debian-12`
|
||||
# CI/CD — dépôt `centreon-terraform`
|
||||
|
||||
Trois workflows Gitea Actions, un script de lecture des secrets, un backend S3.
|
||||
Aucun secret ne vit dans Gitea en dehors de quoi ouvrir OpenBao.
|
||||
La chaîne s'arrête à la création du serveur. Ansible n'est pas déclenché par la CI :
|
||||
il est joué depuis Semaphore, indépendamment.
|
||||
|
||||
| Workflow | Fichier | Déclencheur | Ce qu'il fait |
|
||||
|---|---|---|---|
|
||||
| plan | `.gitea/workflows/terraform-plan.yml` | pull request et push sur `main` | `fmt -check`, `validate`, `plan` — ne modifie rien |
|
||||
| deploy | `.gitea/workflows/deploy.yml` | manuel, `action` = `plan` ou `apply` | plan enregistré puis appliqué si demandé |
|
||||
| destroy | `.gitea/workflows/destroy.yml` | manuel, confirmation `DETRUIRE` | supprime la VM et son disque |
|
||||
## Répartition
|
||||
|
||||
Les trois partagent un `concurrency` commun : deux exécutions ne peuvent pas se
|
||||
disputer le verrou de l'état S3.
|
||||
| Étape | Outil | Déclencheur |
|
||||
|---|---|---|
|
||||
| `fmt`, `validate`, `plan` | Gitea Actions | pull request et push sur `main` |
|
||||
| `apply` | Gitea Actions | lancement manuel, `action=apply` |
|
||||
| `destroy` | Gitea Actions | lancement manuel, confirmation littérale `DETRUIRE` |
|
||||
| Installation Centreon | Semaphore | manuel, après la fin de cloud-init |
|
||||
|
||||
## 1. Préparer OpenBao
|
||||
## Préalables
|
||||
|
||||
Le dépôt du snippet cloud-init passe par SSH, il faut donc le mot de passe du nœud
|
||||
dans le secret Proxmox existant :
|
||||
### 1. OpenBao
|
||||
|
||||
Une clé est à **ajouter au secret existant** `secret/proxmox` : le dépôt du snippet
|
||||
cloud-init passe par SSH, le mot de passe du nœud est donc nécessaire.
|
||||
|
||||
```bash
|
||||
bao kv patch secret/proxmox ssh_password='<mot de passe root du noeud pve>'
|
||||
bao kv patch secret/proxmox ssh_password='<mot de passe root du nœud pve>'
|
||||
```
|
||||
|
||||
Le secret doit contenir au final `api_url`, `api_token`, `node_name`, `ssh_password`.
|
||||
L'adresse du nœud est déduite de `api_url`, il n'y a rien de plus à saisir.
|
||||
Le secret doit finir par contenir : `api_url`, `api_token`, `node_name`, `ssh_password`.
|
||||
L'adresse du nœud est déduite de `api_url`, il n'y a rien d'autre à saisir.
|
||||
|
||||
Politique et AppRole dédiés, en lecture seule sur les deux secrets utiles :
|
||||
Politique et AppRole dédiés au projet, en lecture seule sur les deux secrets utiles :
|
||||
|
||||
```bash
|
||||
bao policy write terraform-proxmox-debian-12 - <<'POLICY'
|
||||
bao policy write centreon-lab - <<'POLICY'
|
||||
path "secret/data/proxmox" { capabilities = ["read"] }
|
||||
path "secret/metadata/proxmox" { capabilities = ["read"] }
|
||||
path "secret/data/aws" { capabilities = ["read"] }
|
||||
path "secret/metadata/aws" { capabilities = ["read"] }
|
||||
POLICY
|
||||
|
||||
bao write auth/approle/role/terraform-proxmox-debian-12 \
|
||||
token_policies=terraform-proxmox-debian-12 token_ttl=1h token_max_ttl=4h
|
||||
bao write auth/approle/role/centreon-lab \
|
||||
token_policies=centreon-lab token_ttl=1h token_max_ttl=4h
|
||||
|
||||
bao read auth/approle/role/terraform-proxmox-debian-12/role-id
|
||||
bao write -f auth/approle/role/terraform-proxmox-debian-12/secret-id
|
||||
bao read auth/approle/role/centreon-lab/role-id
|
||||
bao write -f auth/approle/role/centreon-lab/secret-id
|
||||
```
|
||||
|
||||
## 2. Renseigner Gitea
|
||||
Le mot de passe admin de Centreon n'apparaît pas ici : il sert à Ansible, pas à
|
||||
Terraform. Le stocker à part, par exemple `secret/centreon-lab` avec la clé
|
||||
`centreon_admin_password`, et le récupérer côté Semaphore.
|
||||
|
||||
**Paramètres → Actions → Secrets** :
|
||||
### 2. Secrets et variables Gitea
|
||||
|
||||
Gitea ne détient que de quoi ouvrir OpenBao.
|
||||
|
||||
**Secrets du dépôt** (Paramètres → Actions → Secrets) :
|
||||
|
||||
| Nom | Valeur |
|
||||
|---|---|
|
||||
| `OPENBAO_ROLE_ID` | sortie de `role-id` |
|
||||
| `OPENBAO_SECRET_ID` | sortie de `secret-id` |
|
||||
|
||||
**Paramètres → Actions → Variables** :
|
||||
**Variables du dépôt** (Paramètres → Actions → Variables) :
|
||||
|
||||
| Nom | Valeur |
|
||||
| Nom | Exemple |
|
||||
|---|---|
|
||||
| `OPENBAO_ADDR` | `https://openbao.tips-of-mine.com` |
|
||||
| `TF_BACKEND_BUCKET` | le bucket déjà utilisé par `lab-ad` |
|
||||
| `TF_BACKEND_KEY` | `terraform-proxmox-serveur-debian-12/terraform.tfstate` |
|
||||
| `TF_BACKEND_REGION` | la région du bucket |
|
||||
| `TF_VERSION` | facultatif, `1.9.8` sinon |
|
||||
| `TF_BACKEND_BUCKET` | nom du bucket S3 déjà utilisé par `lab-ad` |
|
||||
| `TF_BACKEND_KEY` | `centreon-lab/terraform.tfstate` |
|
||||
| `TF_BACKEND_REGION` | région du bucket |
|
||||
| `TF_VERSION` | facultatif, `1.9.8` par défaut |
|
||||
|
||||
`TF_BACKEND_KEY` doit être différente de celle de `lab-ad` : deux projets sur la même
|
||||
clé écraseraient mutuellement leur état.
|
||||
`TF_BACKEND_KEY` doit être **différente** de celle de `lab-ad`, sans quoi les deux
|
||||
projets se partageraient le même état.
|
||||
|
||||
Si votre version de Gitea n'expose pas encore les variables de dépôt, déclarez les
|
||||
cinq entrées en secrets et remplacez `vars.` par `secrets.` dans les trois workflows —
|
||||
ce sont des valeurs de configuration, pas des secrets, mais ça fonctionne pareil.
|
||||
### 3. Bit d'exécution du script
|
||||
|
||||
## 3. Pousser le code
|
||||
Le piège déjà rencontré sur `proxmox-create-vm` : le transfert depuis Windows perd le
|
||||
bit d'exécution, et le job échoue sur un « Permission denied ».
|
||||
|
||||
```powershell
|
||||
cd D:\Users\thedj\git\terraform-proxmox-serveur-debian-12
|
||||
git add -A
|
||||
git update-index --chmod=+x scripts/openbao-env.sh
|
||||
git commit -m "CI/CD : plan, deploy, destroy"
|
||||
git push
|
||||
git commit -m "Rendre openbao-env.sh executable"
|
||||
```
|
||||
|
||||
Le `--chmod=+x` n'est pas optionnel : le bit d'exécution se perd au transfert depuis
|
||||
Windows et le job s'arrête sur un « Permission denied ». Le `.gitattributes` fige les
|
||||
fins de ligne en LF pour la même raison — un script en CRLF échoue avec un
|
||||
`bad interpreter` peu parlant.
|
||||
Le fichier `.gitattributes` livré fige les fins de ligne en LF, pour la même raison.
|
||||
|
||||
## 4. Tester, dans cet ordre
|
||||
### 4. Clé publique
|
||||
|
||||
**a. Le plan automatique.** Le push déclenche `Terraform - plan`. C'est le test le plus
|
||||
utile : il valide la chaîne complète — AppRole, lecture des secrets, backend S3,
|
||||
provider Proxmox — sans rien créer.
|
||||
`lab.tfvars` est versionné et contient la clé publique d'administration : y remplacer
|
||||
le `ssh-ed25519 AAAA...` d'exemple avant le premier `apply`. Sans quoi la VM sera
|
||||
créée avec une clé inutilisable et il faudra la recréer — cloud-init ne rejoue rien.
|
||||
|
||||
Le journal doit afficher `Secrets chargés depuis OpenBao : Proxmox (4 valeurs),
|
||||
backend S3 (2 valeurs)` puis un plan annonçant **4 ressources à ajouter** : l'image
|
||||
Debian téléchargée, le snippet cloud-init, la VM, le fichier d'inventaire.
|
||||
## Fonctionnement des workflows
|
||||
|
||||
**b. Le deploy en mode plan.** Lancer `Terraform - deployer la VM Debian` avec
|
||||
`action=plan`. Même résultat, plus l'étape « Résumé du plan ». Cela vérifie le
|
||||
déclenchement manuel et la construction du fichier `tfplan`.
|
||||
`scripts/openbao-env.sh` s'authentifie en AppRole, lit les deux secrets et pousse dans
|
||||
l'environnement du job : `TF_VAR_pve_endpoint`, `TF_VAR_pve_api_token`,
|
||||
`TF_VAR_pve_node`, `TF_VAR_pve_node_address`, `TF_VAR_pve_ssh_password`,
|
||||
`AWS_ACCESS_KEY_ID` et `AWS_SECRET_ACCESS_KEY`. Chaque valeur sensible est passée en
|
||||
`::add-mask::` avant d'être exportée : elle apparaît en `***` si un outil l'affiche.
|
||||
|
||||
**c. Le deploy en mode apply.** Relancer avec `action=apply`. Compter 2 à 4 minutes,
|
||||
dont le téléchargement de l'image Debian au premier passage. À la fin, le job affiche
|
||||
l'adresse et la commande SSH.
|
||||
Le script s'arrête avec un message explicite quand une clé manque dans OpenBao, plutôt
|
||||
que de laisser Terraform échouer sur une authentification vide.
|
||||
|
||||
Puis, depuis votre poste :
|
||||
`deploy.yml` calcule toujours un plan, et ne l'applique que si `action=apply` — c'est
|
||||
le plan enregistré qui est appliqué, pas un second calcul. À la fin, l'inventaire
|
||||
Ansible est publié en artefact (`inventaire-ansible`), utile surtout comme trace :
|
||||
l'adresse étant fixe, Semaphore peut se contenter d'un inventaire statique.
|
||||
|
||||
```bash
|
||||
ssh [email protected] "cloud-init status --wait && sudo nft list chain inet filter input"
|
||||
```
|
||||
## Côté Semaphore
|
||||
|
||||
La sortie ne doit montrer que `lo`, `established,related`, l'ICMP et le port 22.
|
||||
La configuration Semaphore est décrite dans le dépôt qu'elle concerne :
|
||||
`ansible/CICD.md`.
|
||||
|
||||
**d. Le destroy.** Lancer `Terraform - detruire la VM Debian` avec une confirmation
|
||||
erronée d'abord : le job doit s'arrêter immédiatement sans rien toucher. Recommencer
|
||||
avec `DETRUIRE`.
|
||||
## Enchaînement d'un test complet
|
||||
|
||||
## Les pannes les plus probables au premier essai
|
||||
|
||||
| Symptôme | Cause |
|
||||
|---|---|
|
||||
| `Secret manquant dans OpenBao : secret/proxmox → ssh_password` | l'étape 1 n'a pas été faite |
|
||||
| `permission denied` sur `./scripts/openbao-env.sh` | bit d'exécution perdu, voir étape 3 |
|
||||
| `403` du provider Proxmox au plan | rôle du jeton incomplet : il faut `SDN.Use`, et `Datastore.AllocateTemplate` pour le téléchargement de l'image |
|
||||
| `unable to create file ... snippets` | contenu `snippets` absent du stockage `local` : `pvesm set local --content images,rootdir,vztmpl,backup,iso,snippets` |
|
||||
| `Error acquiring the state lock` | un job précédent est mort en cours ; lever le verrou avec `terraform force-unlock <id>` |
|
||||
| La VM démarre mais SSH refuse la clé | `ssh_public_keys` de `lab.tfvars` non remplacé. cloud-init ne rejoue rien : détruire et recréer |
|
||||
1. `deploy.yml` avec `action=apply` — environ 3 minutes.
|
||||
2. Attendre la fin de cloud-init : `ssh [email protected] 'cloud-init status --wait'`.
|
||||
3. Lancer le modèle Semaphore `site.yml` — 10 à 20 minutes.
|
||||
4. Déposer les exports d'Enclume, relancer le modèle `--tags clapi`.
|
||||
5. Pour repartir propre : `destroy.yml` avec la confirmation `DETRUIRE`, puis reprendre
|
||||
à l'étape 1.
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
# Modèle de comptes
|
||||
|
||||
La machine n'a **pas** la commande `sudo` : elle est purgée par cloud-init et épinglée
|
||||
en priorité négative dans APT, pour qu'aucune dépendance ne la réinstalle en douce.
|
||||
L'escalade de privilèges passe donc par `su -`.
|
||||
|
||||
| Compte | Accès SSH | Privilèges | Usage |
|
||||
|---|---|---|---|
|
||||
| `hcornet` | clé | aucun | administration humaine, puis `su -` |
|
||||
| `invite` | clé | aucun | consultation |
|
||||
| `ansible` | clé | aucun | compte de service, sans droits |
|
||||
| `root` | **clé uniquement** | tous | Ansible s'y connecte directement |
|
||||
|
||||
`PermitRootLogin prohibit-password` : root est joignable en SSH par clé, jamais par mot
|
||||
de passe. Le mot de passe root ne sert donc qu'en local — `su -` depuis un compte
|
||||
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
|
||||
|
||||
Deux paires restent à créer, la clé `hcornet` étant déjà la vôtre.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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é.
|
||||
|
||||
## Le mot de passe root
|
||||
|
||||
Il est généré par Terraform à la création de la VM, en 24 caractères. Il n'est **jamais
|
||||
affiché dans les journaux** : le workflow `deploy` le publie en artefact
|
||||
`mot-de-passe-root`, avec une rétention d'un jour.
|
||||
|
||||
Marche à suivre après un `apply` :
|
||||
|
||||
1. télécharger l'artefact depuis la page du job ;
|
||||
2. déposer la valeur dans Vaultwarden ;
|
||||
3. supprimer l'artefact depuis Gitea.
|
||||
|
||||
À savoir : ce mot de passe est conservé en clair dans l'état Terraform, sur S3. C'est la
|
||||
contrepartie de la génération automatique. Si cela devient gênant, l'alternative est que
|
||||
vous le choisissiez vous-même et ne fournissiez que son empreinte — le code passerait
|
||||
alors d'un `random_password` à une variable `root_password_hash` produite par
|
||||
`openssl passwd -6`, et plus rien de sensible ne transiterait.
|
||||
|
||||
## Conséquence pour Ansible
|
||||
|
||||
Le dépôt `centreon-ansible` doit être repris : ses rôles utilisent `become: true` avec
|
||||
`sudo`, qui n'existe plus. La bascule consiste à se connecter en root et à retirer
|
||||
l'escalade — `remote_user: root` et `become: false` dans le playbook, l'inventaire
|
||||
généré par Terraform pointant déjà sur `root`.
|
||||
|
||||
## Vérifications après création
|
||||
|
||||
```bash
|
||||
ssh [email protected] "id && command -v sudo || echo 'sudo absent, conforme'"
|
||||
ssh -i ~/.ssh/id_ed25519_ansible [email protected] "hostname -f"
|
||||
```
|
||||
|
||||
La première commande doit montrer un compte sans groupe `sudo` et confirmer l'absence
|
||||
de la commande. La seconde valide la voie d'accès d'Ansible.
|
||||
+26
-7
@@ -1,5 +1,22 @@
|
||||
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]))
|
||||
}
|
||||
|
||||
# Mot de passe root : sert à « su - » et à la console Proxmox, jamais en SSH.
|
||||
# Il est généré une fois puis conservé dans l'état Terraform, à récupérer par
|
||||
# le workflow de déploiement pour être déposé dans Vaultwarden.
|
||||
resource "random_password" "root" {
|
||||
length = var.root_password_length
|
||||
special = true
|
||||
min_upper = 2
|
||||
min_lower = 2
|
||||
min_numeric = 2
|
||||
min_special = 2
|
||||
override_special = "!@#%^&*()-_=+[]{}:,.?"
|
||||
}
|
||||
|
||||
# Le fichier est déposé dans le contenu "snippets" du stockage, via SSH.
|
||||
@@ -14,13 +31,15 @@ resource "proxmox_virtual_environment_file" "user_data" {
|
||||
file_name = "${var.vm_hostname}-user-data.yaml"
|
||||
|
||||
data = templatefile("${path.module}/templates/user-data.yaml.tftpl", {
|
||||
hostname = var.vm_hostname
|
||||
fqdn = local.fqdn
|
||||
timezone = var.timezone
|
||||
locale = var.locale
|
||||
ansible_user = var.ansible_user
|
||||
ssh_keys = var.ssh_public_keys
|
||||
admin_cidrs = var.ssh_allowed_cidrs
|
||||
hostname = var.vm_hostname
|
||||
fqdn = local.fqdn
|
||||
timezone = var.timezone
|
||||
locale = var.locale
|
||||
comptes = var.local_accounts
|
||||
cles_root = var.root_ssh_public_keys
|
||||
allow_users = local.allow_users
|
||||
admin_cidrs = var.ssh_allowed_cidrs
|
||||
root_password_hash = random_password.root.bcrypt_hash
|
||||
})
|
||||
}
|
||||
}
|
||||
+33
-7
@@ -1,5 +1,5 @@
|
||||
# Valeurs non sensibles, versionnées : c'est ce fichier que la CI passe en -var-file.
|
||||
# Les secrets (jeton d'API, mot de passe SSH du nœud) arrivent par TF_VAR_*, depuis OpenBao.
|
||||
# Valeurs non sensibles, versionnées : c'est ce fichier que la CI passe en -var-file.
|
||||
# Les secrets (jeton d'API, mot de passe SSH du nœud) arrivent par TF_VAR_*, depuis OpenBao.
|
||||
|
||||
vm_id = 120
|
||||
vm_hostname = "centreon"
|
||||
@@ -14,11 +14,37 @@ vm_cores = 2
|
||||
vm_memory = 4096
|
||||
vm_disk_size = 40
|
||||
|
||||
ansible_user = "ansible"
|
||||
ssh_allowed_cidrs = ["10.0.4.0/24", "10.0.5.0/24"]
|
||||
|
||||
# Clé publique d'administration. Ce n'est pas un secret : elle reste ici plutôt que
|
||||
# dans OpenBao, sinon un -var-file l'emporterait sur la variable d'environnement.
|
||||
ssh_public_keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDIXkfS5dM3po9Jz7NKgd3t1H4vqh0/cVoxrAP42ivd thedj@ZEU",
|
||||
# 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.
|
||||
local_accounts = [
|
||||
{
|
||||
name = "hcornet"
|
||||
gecos = "Hubert Cornet - administration"
|
||||
ssh_keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDIXkfS5dM3po9Jz7NKgd3t1H4vqh0/cVoxrAP42ivd thedj@ZEU",
|
||||
]
|
||||
},
|
||||
{
|
||||
name = "invite"
|
||||
gecos = "Compte de consultation - aucun privilege"
|
||||
ssh_keys = [
|
||||
"REMPLACER par la cle publique du compte invite",
|
||||
]
|
||||
},
|
||||
{
|
||||
name = "ansible"
|
||||
gecos = "Compte de service - sans privilege"
|
||||
ssh_keys = [
|
||||
"REMPLACER par la cle publique d automatisation",
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
# 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",
|
||||
]
|
||||
@@ -112,7 +112,7 @@ resource "local_file" "ansible_inventory" {
|
||||
hosts = {
|
||||
(var.vm_hostname) = {
|
||||
ansible_host = var.vm_ipv4_address
|
||||
ansible_user = var.ansible_user
|
||||
ansible_user = var.ansible_remote_user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -13,9 +13,20 @@ output "vm_fqdn" {
|
||||
value = local.fqdn
|
||||
}
|
||||
|
||||
output "comptes" {
|
||||
description = "Comptes ouverts sur la machine, hors root."
|
||||
value = [for compte in var.local_accounts : compte.name]
|
||||
}
|
||||
|
||||
output "ssh_command" {
|
||||
description = "Commande de connexion une fois cloud-init terminé."
|
||||
value = "ssh ${var.ansible_user}@${var.vm_ipv4_address}"
|
||||
value = "ssh ${var.local_accounts[0].name}@${var.vm_ipv4_address}"
|
||||
}
|
||||
|
||||
output "root_password" {
|
||||
description = "Mot de passe root, à déposer dans Vaultwarden puis à ne plus consulter."
|
||||
value = random_password.root.result
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "ansible_inventory_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}"
|
||||
|
||||
@@ -9,25 +9,37 @@ manage_etc_hosts: true
|
||||
timezone: ${timezone}
|
||||
|
||||
# La clé "users" remplace la liste par défaut : le compte "debian" livré avec
|
||||
# l'image cloud n'est donc jamais créé. Un seul compte existe sur la machine,
|
||||
# sans mot de passe (lock_passwd), accessible uniquement par clé.
|
||||
# l'image cloud n'est donc jamais créé.
|
||||
#
|
||||
# Aucun compte n'appartient au groupe sudo, et la commande sudo est purgée du
|
||||
# système plus bas. Pour administrer : ouvrir une session avec son compte, puis
|
||||
# « su - » avec le mot de passe root.
|
||||
users:
|
||||
- name: ${ansible_user}
|
||||
gecos: Compte d administration (Ansible)
|
||||
groups: [sudo]
|
||||
%{ for compte in comptes ~}
|
||||
- name: ${compte.name}
|
||||
gecos: "${compte.gecos}"
|
||||
shell: /bin/bash
|
||||
lock_passwd: true
|
||||
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||
ssh_authorized_keys:
|
||||
%{ for key in ssh_keys ~}
|
||||
- "${key}"
|
||||
%{ for cle in compte.ssh_keys ~}
|
||||
- "${cle}"
|
||||
%{ endfor ~}
|
||||
%{ endfor ~}
|
||||
|
||||
disable_root: true
|
||||
# Root garde un mot de passe, utilisable par « su - » et sur la console Proxmox.
|
||||
# En SSH, seule la clé est acceptée (prohibit-password plus bas).
|
||||
disable_root: false
|
||||
ssh_pwauth: false
|
||||
ssh_deletekeys: true
|
||||
ssh_genkeytypes: [ed25519, rsa]
|
||||
|
||||
chpasswd:
|
||||
expire: false
|
||||
users:
|
||||
- name: root
|
||||
password: "${root_password_hash}"
|
||||
type: hash
|
||||
|
||||
package_update: true
|
||||
package_upgrade: true
|
||||
|
||||
@@ -48,18 +60,28 @@ packages:
|
||||
- apt-transport-https
|
||||
|
||||
write_files:
|
||||
- path: /root/.ssh/authorized_keys
|
||||
owner: root:root
|
||||
permissions: "0600"
|
||||
content: |
|
||||
%{ for cle in cles_root ~}
|
||||
${cle}
|
||||
%{ endfor ~}
|
||||
|
||||
- path: /etc/ssh/sshd_config.d/99-durcissement.conf
|
||||
owner: root:root
|
||||
permissions: "0600"
|
||||
content: |
|
||||
# Durcissement SSH — cloud-init
|
||||
PermitRootLogin no
|
||||
# root n'est joignable que par clé : le mot de passe root ne sert qu'en
|
||||
# local, par « su - » ou sur la console.
|
||||
PermitRootLogin prohibit-password
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PermitEmptyPasswords no
|
||||
PubkeyAuthentication yes
|
||||
AuthenticationMethods publickey
|
||||
AllowUsers ${ansible_user}
|
||||
AllowUsers ${allow_users}
|
||||
MaxAuthTries 3
|
||||
LoginGraceTime 30
|
||||
X11Forwarding no
|
||||
@@ -124,6 +146,17 @@ write_files:
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
|
||||
- path: /etc/apt/preferences.d/99-pas-de-sudo
|
||||
owner: root:root
|
||||
permissions: "0644"
|
||||
content: |
|
||||
# sudo est retiré volontairement de cette machine : l'escalade de
|
||||
# privilèges passe par « su - ». Cette épingle empêche une dépendance
|
||||
# de le réinstaller sans qu'on s'en aperçoive.
|
||||
Package: sudo
|
||||
Pin: release *
|
||||
Pin-Priority: -1
|
||||
|
||||
- path: /etc/sysctl.d/99-durcissement.conf
|
||||
owner: root:root
|
||||
permissions: "0644"
|
||||
@@ -141,6 +174,7 @@ write_files:
|
||||
fs.protected_symlinks = 1
|
||||
|
||||
runcmd:
|
||||
- chmod 700 /root/.ssh
|
||||
- sed -i "s|^# *${locale} UTF-8|${locale} UTF-8|" /etc/locale.gen
|
||||
- locale-gen
|
||||
- update-locale LANG=${locale}
|
||||
@@ -149,6 +183,7 @@ runcmd:
|
||||
- systemctl enable --now nftables
|
||||
- systemctl enable --now unattended-upgrades
|
||||
- systemctl restart ssh
|
||||
- apt-get -y purge sudo
|
||||
- apt-get -y autoremove --purge
|
||||
- apt-get -y clean
|
||||
|
||||
|
||||
+51
-10
@@ -190,20 +190,61 @@ variable "locale" {
|
||||
default = "fr_FR.UTF-8"
|
||||
}
|
||||
|
||||
variable "ansible_user" {
|
||||
description = "Compte unix créé par cloud-init, utilisé ensuite par Ansible."
|
||||
type = string
|
||||
default = "ansible"
|
||||
}
|
||||
variable "local_accounts" {
|
||||
description = <<-EOT
|
||||
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.
|
||||
EOT
|
||||
|
||||
variable "ssh_public_keys" {
|
||||
description = "Clés publiques autorisées pour le compte d'administration. Aucun mot de passe n'est défini."
|
||||
type = list(string)
|
||||
type = list(object({
|
||||
name = string
|
||||
gecos = optional(string, "")
|
||||
ssh_keys = list(string)
|
||||
}))
|
||||
|
||||
validation {
|
||||
condition = length(var.ssh_public_keys) > 0
|
||||
error_message = "Au moins une clé publique est nécessaire : l'authentification par mot de passe est désactivée."
|
||||
condition = length(var.local_accounts) > 0
|
||||
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."
|
||||
}
|
||||
}
|
||||
|
||||
variable "root_ssh_public_keys" {
|
||||
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.
|
||||
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."
|
||||
}
|
||||
}
|
||||
|
||||
variable "ansible_remote_user" {
|
||||
description = "Compte utilisé par Ansible dans l'inventaire généré."
|
||||
type = string
|
||||
default = "root"
|
||||
}
|
||||
|
||||
variable "root_password_length" {
|
||||
description = "Longueur du mot de passe root généré. Il sert à « su - » et à la console Proxmox."
|
||||
type = number
|
||||
default = 24
|
||||
}
|
||||
|
||||
variable "ssh_allowed_cidrs" {
|
||||
|
||||
@@ -6,6 +6,10 @@ terraform {
|
||||
source = "bpg/proxmox"
|
||||
version = ">= 0.69.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.6.0"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = ">= 2.4.0"
|
||||
|
||||
Reference in new issue
Block a user