Template doré tmpl-rocky96-hardened

Rôle

Le dépôt /root/packer (indio/iac/packer) construit avec HashiCorp Packer (builder vsphere-iso) un template vCenter unique, tmpl-rocky96-hardened, à partir d'une image Rocky Linux 9.6 minimale, durci (SELinux enforcing, firewalld zone drop par défaut, politique de mots de passe et audit renforcés) et pré-intégré au dépôt interne Nexus.

Ce template est ensuite cloné par le dépôt Terraform /root/vmware pour produire chaque VM de la flotte — voir vcenter-esxi.md.

Architecture

packer/
├── rocky9.pkr.hcl          # source vsphere-iso + build (provisioners shell + manifest)
├── variables.pkr.hcl       # variables
├── example.pkrvars.hcl     # à copier en rocky96.pkrvars.hcl (secrets réels, jamais commité)
├── run-build.sh            # wrapper de build (dérive le hash du mot de passe, init+validate+build)
├── http/
│   └── ks.pkrtpl.hcl       # template Kickstart (rendu par templatefile(), hash injecté au build)
├── scripts/
│   ├── update.sh           # mise à jour système + outils VMware
│   ├── hardening.sh        # durcissement CIS-like
│   ├── internal-repo.sh    # bootstrap du dépôt interne + installation du RPM indio-baseline-config
│   └── cleanup.sh          # nettoyage avant conversion en template
└── manifest.json           # historique des builds (artifact_id, uuid, date)

Le kickstart est livré via un ISO secondaire généré par Packer (cd_content, label OEMDRV) plutôt que via un serveur HTTP : Anaconda détecte automatiquement ce device labellisé. Le placement vSphere (datacenter/cluster/datastore) réutilise les mêmes objets que le dépôt vmware (Datacenter/Cluster/vsanDatastore).

manifest.json montre plusieurs générations successives du template (tmpl-rocky96-hardened, -v2, -v3) au fil des correctifs apportés au RPM indio-baseline-config embarqué ; seul le nom canonique tmpl-rocky96-hardened doit être référencé par les dépôts consommateurs.

Schéma — pipeline de build

flowchart LR
    ISO["ISO Rocky 9.6 minimal<br/>(datastore vCenter)"] --> BUILD
    KS["http/ks.pkrtpl.hcl<br/>(templatefile, hash injecté)"] -->|ISO secondaire<br/>label OEMDRV| BUILD

    subgraph BUILD["packer build (source vsphere-iso)"]
        direction TB
        B1["Boot + Anaconda<br/>(kickstart automatique)"] --> B2["update.sh<br/>(maj système + open-vm-tools)"]
        B2 --> B3["hardening.sh<br/>(CIS-like : SSH, sysctl, auditd, firewalld...)"]
        B3 --> B4["internal-repo.sh<br/>(bootstrap Nexus + indio-baseline-config)"]
        B4 --> B5["cleanup.sh<br/>(purge logs/clés/machine-id, expect_disconnect)"]
    end

    BUILD --> CONV["convert_to_template = true<br/>remove_cdrom = true"]
    CONV --> MANIFEST["manifest.json<br/>(artifact_id, uuid, date)"]
    CONV --> TMPL[("tmpl-rocky96-hardened<br/>(vCenter)")]
    TMPL -->|cloné par| TF["Terraform /root/vmware<br/>(voir vcenter-esxi.md)"]

Le kickstart (http/ks.pkrtpl.hcl)

Livré via un ISO secondaire généré par Packer (label OEMDRV, détecté automatiquement par Anaconda) plutôt que par serveur HTTP. Contenu réel constaté :

  • Réseau de build : IP statique dédiée 10.15.100.59/26, gateway 10.15.100.62, hostname rocky96-template.infra.indio — propre à l'environnement de build, sans rapport avec l'IP finale posée par Terraform à chaque clonage (bloc customize, voir vcenter-esxi.md).
  • Comptes : rootpw --iscrypted et user --name=indio-adm --groups=wheel --iscrypted utilisent tous les deux le même hash ${ssh_password_hash}, injecté par templatefile() — dérivé automatiquement du mot de passe fourni à run-build.sh (voir plus bas), jamais renseigné à la main.
  • Partitionnement durci (CIS-like), disque unique sda (35 Go) : /boot et /boot/efi hors LVM (nodev,nosuid,noexec sur /boot), puis un unique VG vg_system avec des volumes logiques séparés pour /, swap, /home, /tmp, /var, /var/tmp, /var/log, /var/log/audit — chacun avec les options de montage durcies adaptées (nodev/nosuid/noexec selon le volume). ~1,5 Gio laissés libres dans le VG pour extension ultérieure.
  • Paquets (%packages) : @^minimal-environment + @standard + open-vm-tools, cloud-init, cloud-utils-growpart, lvm2, chrony, audit, aide, rsyslog, firewalld, plus retrait explicite de iprutils/iwl*firmware/biosdevname/plymouth.
  • %post : sudoers temporaire NOPASSWD pour indio-adm (retiré par cleanup.sh en fin de build), import de la clé GPG Rocky, et override datasource_list: [VMware, NoCloud, OVF, None] dans cloud-init — c'est ce qui permet à la fois la customization vSphere et le bloc customize Terraform de fonctionner correctement sur chaque clone.
  • bootloader --append : audit=1 audit_backlog_limit=8192 slub_debug=P page_poison=1 vsyscall=none crashkernel=no — durcissement noyau au boot, en complément de hardening.sh.

Provisioning Packer

rocky9.pkr.hcl :

  • source "vsphere-iso" "rocky96" : firmware efi-secure, contrôleur disque pvscsi (thin provisionné), carte réseau vmxnet3, ISO Rocky 9.6 minimal fourni via iso_paths. En fin de build : convert_to_template = true, remove_cdrom = true, tools_upgrade_policy = true.
  • build "rocky96-hardened" : deux provisioners shell exécutés dans l'ordre :
  • update.sh, hardening.sh, internal-repo.sh (via sudo -S, mot de passe transmis en pipe depuis var.ssh_password) ;
  • cleanup.sh (expect_disconnect = true, la VM s'éteignant à la fin du script).
  • post-processor "manifest" : trace chaque build dans manifest.json.

Variables clés (variables.pkr.hcl) : connexion vCenter (vsphere_password marquée sensitive), placement, définition de la VM template (vm_disk_size par défaut 40960 Mo, nécessaire au partitionnement LVM du kickstart), et le couple ssh_username/ssh_password/ssh_password_hash — ce dernier est le hash sha512-crypt injecté dans le kickstart (rootpw/user du compte indio-adm), à ne jamais renseigner à la main (voir procédure ci-dessous).

Étapes de durcissement (scripts/)

Ordre d'exécution réel et contenu vérifié dans scripts/ :

  1. update.shdnf upgrade --refresh, installation de open-vm-tools, cloud-init, cloud-utils-growpart, lvm2, chrony, audit, aide, firewalld ; activation de vmtoolsd.

  2. hardening.sh — durcissement CIS-like :

  3. SSH : drop-in /etc/ssh/sshd_config.d/00-hardening.conf (préfixe 00- pour gagner le first-match face à un éventuel 50-cloud-init.conf généré ensuite) : PermitRootLogin no, PasswordAuthentication yes, PermitEmptyPasswords no, X11Forwarding no, MaxAuthTries 4, chiffrement restreint (Ciphers/MACs/KexAlgorithms modernes), bannière légale /etc/issue.net, validation sshd -t avant de continuer.
  4. Politique de mots de passe : pwquality (longueur minimale 14, 4 classes de caractères, maxrepeat 3, appliquée aussi à root) et vieillissement via login.defs (PASS_MAX_DAYS 90, UMASK 027).
  5. Modules noyau désactivés : cramfs, freevxfs, jffs2, hfs(+), squashfs, udf, usb-storage, dccp, sctp, rds, tipc.
  6. sysctl réseau/noyau durci : IP forwarding et redirects désactivés, rp_filter/ syncookies/log_martians activés, dmesg_restrict/kptr_restrict/ptrace_scope restreints, protections hardlinks/symlinks.
  7. /dev/shm remonté avec nodev,nosuid,noexec.
  8. auditd activé avec des règles de base (surveillance de passwd/shadow/group/ sudoers/sshd_config, mode immuable -e 2).
  9. firewalld : service SSH autorisé dans la zone drop avant de définir cette zone comme zone par défaut (sinon la session SSH du build serait coupée) — c'est l'origine de la zone drop par défaut du template.
  10. Désactivation de kdump.service.
  11. Initialisation d'une base d'intégrité AIDE.
  12. Permissions durcies sur /root (0700) et grub.cfg (0600).
  13. FIPS : aucun appel à fips-mode-setup trouvé dans les scripts — le mode FIPS reste dans son état par défaut Rocky (désactivé), cohérent avec le "FIPS off" attendu du template.

  14. internal-repo.sh — bootstrap du dépôt interne : résout repo.infra.infra vers la VIP HAProxy (10.100.2.130) dans /etc/hosts, crée un dépôt bootstrap minimal indio-custom, installe le RPM indio-baseline-config (dépôts Nexus/EPEL/CRB + NTP + proxy + CA Vault, voir baseline-config.md) puis retire le dépôt bootstrap (le indio.repo définitif à 6 sections est déposé par le RPM lui-même).

  15. cleanup.sh — nettoyage avant conversion en template : sudoers de build retiré, cache et logs dnf purgés, logs applicatifs tronqués, clés SSH host supprimées (régénérées au premier boot de chaque clone), machine-id remis à zéro, connexions NetworkManager supprimées, cloud-init clean, historique bash purgé, remplissage de zéros pour améliorer la compression du disque thin-provisionné.

Configuration Ansible

Non applicable. Le provisioning post-boot du build (durcissement, dépôt interne) est fait par des provisioners shell Packer natifs (scripts/*.sh, détaillés ci-dessus), pas par Ansible. La configuration Ansible de la flotte intervient en aval, après clonage du template — voir baseline-config.md.

Procédure de déploiement (build)

  1. Déposer l'ISO Rocky-9.6-x86_64-minimal.iso sur un datastore accessible.
  2. cp example.pkrvars.hcl rocky96.pkrvars.hcl et adapter vCenter/placement/ISO (jamais le mot de passe dans ce fichier).
  3. Lancer via le wrapper (méthode recommandée) :
PKR_VAR_ssh_password='...' ./run-build.sh rocky96.pkrvars.hcl
# ou saisie interactive masquée si la variable n'est pas fournie
VALIDATE_ONLY=1 ./run-build.sh rocky96.pkrvars.hcl   # s'arrêter après validate
  1. run-build.sh dérive automatiquement ssh_password_hash (openssl passwd -6) à partir du mot de passe fourni : le hash injecté dans le kickstart correspond ainsi toujours au mot de passe utilisé pour la connexion SSH du build (source de vérité unique, évite toute désynchronisation).
  2. packer initpacker validatepacker build.
  3. Le template produit (vm_name, par défaut tmpl-rocky96-hardened) est ensuite consommé par /root/vmware via la variable template_name.
  4. Bascule du template canonique (manuelle, après un nouveau build réussi sous un nom temporaire type -v3) : renommage vCenter pour que tmpl-rocky96-hardened pointe sur le nouveau build — procédure détaillée dans vcenter-esxi.md.

Contrôle de santé / Vérification

# Historique des builds (dates, uuid Packer)
cat manifest.json

# Confirmer le template présent côté vCenter (nom canonique, powerState off attendu)
govc vm.info tmpl-rocky96-hardened

Vérifié en direct le 2026-07-19 : tmpl-rocky96-hardened présent (poweredOff, hébergé sur esxi-node01), cohérent avec manifest.json (dernier artifact_id = tmpl-rocky96-hardened-v3, renommé depuis en nom canonique — voir Points d'attention sur les générations successives).

Points d'attention

  • rocky96.pkrvars.hcl (et tout *.pkrvars.hcl hors example) ainsi que les logs de build (*.log) sont gitignorés : ils peuvent contenir des identifiants vCenter réels ou des sorties de commande sensibles — ne jamais les committer ni les citer dans une documentation.
  • Le mot de passe des comptes root/indio-adm n'est jamais stocké dans un fichier versionné : il transite par variable d'environnement ou saisie interactive, et le hash sha512-crypt est régénéré à chaque build — la valeur elle-même est gérée séparément (Vault / non versionnée).
  • Ordre critique dans hardening.sh : autoriser SSH dans la zone drop de firewalld avant de la définir comme zone par défaut, sous peine de couper la session SSH du build en cours.
  • Le kickstart fixe une IP de build statique dédiée au réseau de build, distincte de l'IP finale posée par Terraform à chaque clonage (bloc customize) — normal, propre à l'environnement de build.
  • Plusieurs générations de template ont coexisté (tmpl-rocky96-hardened, -v2, -v3) au fil des correctifs du RPM indio-baseline-config (dépôts stock Rocky d'abord désactivés puis supprimés) ; seul le nom canonique tmpl-rocky96-hardened doit être utilisé par les projets consommateurs.
  • PasswordAuthentication yes est activé sur le template : cohérent avec les provisioners Terraform du dépôt vmware qui s'appuient sur une authentification SSH par mot de passe, à réévaluer si une politique clé-uniquement est souhaitée à terme.