1 parent
d3828b601d
commit
8f279465bc
11 files changed
+186
-64
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDIXkfS5dM3po9Jz7NKgd3t1H4vqh0/cVoxrAP42ivd thedj@ZEU
|
||||
+39
-5
@@ -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
@@ -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
@@ -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" {
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user