Images NixOS pour Outscale
Mise à jour :

Dans ce guide, je vous explique comment produire des Outscale Machine Images (OMI) pour NixOS. Je génère une image QCOW2 sécurisée, prête à être déployée sur le cloud Outscale, en réutilisant ma configuration NixOS-WSL comme base de travail.
Aucune connaissance préalable d’Outscale n’est requise. Si vous n’avez pas encore d’environnement NixOS-WSL fonctionnel, je vous recommande de suivre d’abord mon guide d’installation NixOS dans WSL.
Pourquoi NixOS pour les images cloud ?
Les images cloud traditionnelles posent plusieurs problèmes :
- Dérive de configuration : les instances s’éloignent de leur état initial au fil du temps
- Difficultés de mise à jour : patcher une AMI/OMI existante est fastidieux
- Manque de reproductibilité : recréer exactement la même image des mois plus tard est incertain
- Surface d’attaque accrue : les images génériques contiennent souvent des services inutiles
NixOS résout ces problèmes :
- Déclaratif : l’image entière se définit dans des fichiers de configuration versionnés
- Reproductible : même configuration = même image, bit pour bit
- Atomique : les mises à jour sont tout-ou-rien, pas de système dans un état intermédiaire
- Auditabilité : chaque changement est tracé dans Git
Prérequis
- Un environnement NixOS-WSL fonctionnel (voir le guide précédent)
- Un compte Outscale avec accès à l’API
- Les outils
osc-cliou l’accès à la console Cockpit - Une clé age pour le chiffrement des secrets (je vais la créer)
Architecture du projet
Le projet s’organise ainsi :
nixos-config/├── flake.nix # Point d'entrée avec génération QCOW2├── hosts/│ ├── wsl.nix # Configuration WSL (existante)│ └── outscale.nix # Configuration pour l'image cloud├── modules/│ ├── docker.nix # Module Docker│ ├── tools.nix # Outils CLI│ ├── zsh.nix # Configuration Zsh│ └── vscode.nix # Compatibilité VS Code├── secrets/│ └── password-hash.txt # Hash du mot de passe (chiffré dans Git)├── git-agecrypt.toml # Configuration git-agecrypt└── .gitattributes # Filtres Git pour le chiffrementÉtape 1 : Installer nixos-generators
nixos-generators est l’outil qui permet de générer des images dans différents
formats (QCOW2, ISO, AWS AMI, etc.) à partir d’une configuration NixOS.
Je modifie mon flake.nix pour ajouter cette dépendance :
{ description = "Ma configuration NixOS-WSL";
inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; nixos-wsl.url = "github:nix-community/NixOS-WSL"; nixos-wsl.inputs.nixpkgs.follows = "nixpkgs"; sops-nix.url = "github:Mic92/sops-nix"; sops-nix.inputs.nixpkgs.follows = "nixpkgs"; # Ajout de nixos-generators pour la génération d'images nixos-generators.url = "github:nix-community/nixos-generators"; nixos-generators.inputs.nixpkgs.follows = "nixpkgs"; };
outputs = { self, nixpkgs, nixos-wsl, sops-nix, nixos-generators, ... }: { # Configuration WSL (existante) nixosConfigurations.wsl = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ nixos-wsl.nixosModules.wsl sops-nix.nixosModules.sops ./hosts/wsl.nix ]; };
# Configuration Outscale (pour tests) nixosConfigurations.outscale = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ ./hosts/outscale.nix ]; };
# Image QCOW2 pour Outscale packages.x86_64-linux.outscale-qcow = nixos-generators.nixosGenerate { system = "x86_64-linux"; format = "qcow"; modules = [ ./hosts/outscale.nix ({ config, pkgs, ... }: { # Taille du disque QCOW2 : 10 Go virtualisation.diskSize = 10 * 1024; # en Mo }) ]; }; };}Points importants :
nixos-generators.inputs.nixpkgs.followsassure que tout utilise la même version de nixpkgsformat = "qcow"génère une image QCOW2 compatible Outscalevirtualisation.diskSizedéfinit la taille du disque (ici 10 Go)
Étape 2 : Gérer les secrets avec git-agecrypt
Le problème des secrets au build
Un problème se pose : comment inclure un mot de passe dans l’image sans le commiter en clair dans Git ?
J’ai d’abord essayé sops-nix, mais il déchiffre les secrets au démarrage
du système, pas au build. Pour une image cloud, j’ai besoin que le hash du
mot de passe soit disponible pendant la génération de l’image.
La solution : git-agecrypt. Cet outil chiffre les fichiers dans Git, mais les
déchiffre automatiquement dans le répertoire de travail. Ainsi :
- Dans Git (et sur GitHub) : le fichier est chiffré
- Localement : le fichier est en clair, utilisable pendant le build
Installer git-agecrypt
# Installer git-agecryptnix profile install github:vlaci/git-agecrypt
# Générer une clé age (si vous n'en avez pas)age-keygen -o ~/.config/age/keys.txtLa commande age-keygen affiche votre clé publique :
Public key: age1stq2rxg545r3m5stdv7ch60e3t30qepqz4h0tg4wsqh5whl4443sx582sgConservez précieusement le fichier ~/.config/age/keys.txt — c’est votre clé
privée.
Configurer git-agecrypt dans le projet
Je crée le fichier de configuration git-agecrypt.toml :
cat > git-agecrypt.toml << 'EOF'[config]"secrets/password-hash.txt" = ["age1stq2rxg545r3m5stdv7ch60e3t30qepqz4h0tg4wsqh5whl4443sx582sg"]EOFRemplacez la clé publique par la vôtre.
Je crée le fichier .gitattributes pour activer le filtre :
cat > .gitattributes << 'EOF'secrets/password-hash.txt filter=git-agecrypt diff=git-agecryptEOFJ’initialise git-agecrypt dans le dépôt :
git-agecrypt initCréer le secret
Je génère un hash de mot de passe et le stocke :
mkdir -p secrets
# Générer un hash pour le mot de passe "changeme"# (à remplacer par un vrai mot de passe en production !)mkpasswd -m sha-512 "votremotdepasse" > secrets/password-hash.txtJe vérifie que le chiffrement fonctionne :
# Le fichier local est en claircat secrets/password-hash.txt# Affiche : $6$uY4uFIM7VeAHL6qh$3uiwDtRY0Z...
# Mais dans Git, il sera chiffrégit add secrets/password-hash.txtgit show :secrets/password-hash.txt | head -1# Affiche : -----BEGIN AGE ENCRYPTED FILE-----Note importante : Toute personne qui clone le dépôt sans la clé privée age verra le fichier chiffré. Seuls ceux qui possèdent la clé peuvent le déchiffrer.
Étape 3 : Créer la configuration Outscale
Je crée le fichier hosts/outscale.nix avec la configuration complète de
l’image :
{ config, pkgs, lib, modulesPath, ... }:
let # Hash chiffré avec git-agecrypt (déchiffré automatiquement dans le working tree) passwordHash = lib.strings.trim (builtins.readFile ../secrets/password-hash.txt);in{ ############################################################################# # IMPORTS #############################################################################
imports = [ "${modulesPath}/profiles/qemu-guest.nix" ];
############################################################################# # SYSTEM #############################################################################
system.stateVersion = "25.11";
############################################################################# # BOOT #############################################################################
boot = { loader = { grub.device = lib.mkDefault "/dev/vda"; timeout = lib.mkForce 0; };
# Console série pour QEMU/cloud avec debug verbeux kernelParams = [ "console=ttyS0,115200n8" "console=tty0" "systemd.show_status=true" "loglevel=7" ];
# Afficher les logs cloud-init sur la console série kernel.sysctl."kernel.printk" = "7 4 1 7"; };
############################################################################# # FILESYSTEM #############################################################################
fileSystems."/" = lib.mkDefault { device = "/dev/vda1"; fsType = "ext4"; };
############################################################################# # NETWORKING #############################################################################
networking = { hostName = "nixos-outscale"; useNetworkd = true;
firewall = { enable = true; allowedTCPPorts = [ 22 ]; rejectPackets = true; logRefusedConnections = true; }; };
############################################################################# # USERS & GROUPS #############################################################################
users = { mutableUsers = false;
groups.outscale = { };
users.outscale = { isNormalUser = true; description = "Outscale admin user"; home = "/home/outscale"; createHome = true; group = "outscale"; uid = 1000; extraGroups = [ "wheel" "network" "docker" ]; shell = pkgs.zsh; hashedPassword = passwordHash; }; };
############################################################################# # SECURITY #############################################################################
security = { sudo.wheelNeedsPassword = false; # a adapter en production
apparmor = { enable = true; killUnconfinedConfinables = false; packages = [ pkgs.apparmor-profiles ]; }; };
############################################################################# # NIX #############################################################################
nix = { settings.experimental-features = [ "nix-command" "flakes" ];
gc = { automatic = true; dates = "weekly"; options = "--delete-older-than 30d"; }; };
############################################################################# # PROGRAMS #############################################################################
programs.zsh.enable = true;
############################################################################# # ENVIRONMENT #############################################################################
environment.systemPackages = with pkgs; [ # Éditeurs et outils de base vim git htop curl wget # Firewall iptables # AppArmor apparmor-utils apparmor-profiles ];
############################################################################# # SERVICES #############################################################################
services = { # SSH openssh = { enable = true; ports = [ 22 ]; settings = { PermitRootLogin = "no"; PasswordAuthentication = false; KbdInteractiveAuthentication = false; PubkeyAuthentication = true; AllowUsers = [ "outscale" ]; ChallengeResponseAuthentication = false; MaxAuthTries = 3; LoginGraceTime = "20s"; }; };
# Cloud-init pour récupérer les clés SSH depuis les métadonnées Outscale cloud-init = { enable = true; network.enable = true; settings = { chpasswd.expire = false; users = [ "default" ]; system_info.default_user = { name = "outscale"; lock_passwd = false; gecos = "Outscale admin user"; groups = [ "wheel" "docker" ]; sudo = [ "ALL=(ALL) NOPASSWD:ALL" ]; shell = "/run/current-system/sw/bin/zsh"; }; ssh_pwauth = true; disable_root = true; preserve_hostname = true; # Debug: logs verbeux output.all = "| tee -a /dev/ttyS0"; }; }; };
############################################################################# # SYSTEMD #############################################################################
systemd.services = { # Rediriger les logs cloud-init vers la console série cloud-init.serviceConfig = { StandardOutput = "journal+console"; StandardError = "journal+console"; }; cloud-init-local.serviceConfig = { StandardOutput = "journal+console"; StandardError = "journal+console"; }; cloud-final.serviceConfig = { StandardOutput = "journal+console"; StandardError = "journal+console"; }; };}Points importants de cette configuration :
- SSH sur port 22 : port standard, cloud-init injecte les clés SSH
- Root interdit en SSH :
PermitRootLogin = "no" - Firewall restrictif : seul le port 22 est ouvert
- AppArmor activé : confinement des applications
- cloud-init configuré : récupère les clés SSH depuis les métadonnées Outscale
- Logs debug : sortie sur console série pour faciliter le diagnostic
Étape 4 : Générer l’image QCOW2
Je commite les changements et génère l’image :
cd /mnt/c/Users/votre-user/Nixos-WSL
# Versionner les changementsgit add .git commit -m "feat: Ajout génération image Outscale"
# Générer l'image QCOW2nix build .#outscale-qcow -o ./result-outscaleLa génération prend plusieurs minutes. Le résultat est un lien symbolique vers l’image :
ls -la result-outscale/# nixos.qcow2Étape 5 : Tester localement avec QEMU
Avant de déployer sur Outscale, je teste l’image localement :
# Copier l'image pour ne pas modifier l'originalcp result-outscale/nixos.qcow2 /tmp/nixos-test.qcow2
# Lancer QEMUqemu-system-x86_64 \ -m 2048 \ -cpu qemu64 \ -drive file=/tmp/nixos-test.qcow2,format=qcow2,if=virtio \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0 \ -nographicUne fois la VM démarrée, je peux me connecter en SSH :
# Depuis un autre terminal (port local 2222 redirigé vers port 22 de la VM)ssh -p 2222 outscale@localhostÉtape 6 : Déployer sur Outscale
Installation et configuration de osc-cli
J’installe le SDK Outscale via pipx :
# Installer osc-sdkpipx install osc-sdk
# Créer le répertoire de configurationmkdir -p ~/.oscchmod 700 ~/.osc
# Créer le fichier de configuration (remplacez par vos credentials)cat > ~/.osc/config.json << EOF{ "default": { "access_key": "$OSC_ACCESS_KEY", "secret_key": "$OSC_SECRET_KEY", "host": "outscale.com", "https": true, "method": "POST", "region_name": "eu-west-2" }}EOFchmod 600 ~/.osc/config.jsonConfiguration de s3cmd pour OOS
Je configure s3cmd pour accéder à Outscale Object Storage :
# Créer le fichier de configuration (remplacez par vos credentials)cat > ~/.s3cfg << EOF[default]access_key = $OSC_ACCESS_KEYsecret_key = $OSC_SECRET_KEYhost_base = oos.eu-west-2.outscale.comhost_bucket = %(bucket)s.oos.eu-west-2.outscale.comuse_https = Truesignature_v2 = FalseEOFchmod 600 ~/.s3cfgUpload de l’image vers OOS
# Créer le buckets3cmd mb s3://nixos-images
# Uploader l'image QCOW2s3cmd put ./result-outscale/nixos.qcow2 s3://nixos-images/ --progressCréation d’une URL pré-signée
L’API Outscale nécessite une URL accessible pour créer le snapshot :
# URL valide 1 semaine (604800 secondes)EXPIRY=$(($(date +%s) + 604800))PRESIGNED_URL=$(s3cmd signurl s3://nixos-images/nixos.qcow2 $EXPIRY)echo "URL: $PRESIGNED_URL"Calcul de la taille virtuelle de l’image
Lors de la création du snapshot, Outscale demande la taille virtuelle de l’image en bytes :
# La taille virtuelle (pas la taille fichier) est nécessaire pour le snapshotVIRTUAL_SIZE=$(qemu-img info --output=json ./result-outscale/nixos.qcow2 \ | jq -r '."virtual-size"')VOLUME_SIZE_GIB=$(( (VIRTUAL_SIZE + 1073741823) / 1073741824 ))echo "Taille virtuelle: $VIRTUAL_SIZE bytes ($VOLUME_SIZE_GIB GiB)"Et en GiB (arrondi supérieur) pour la création de l’OMI.
Création du snapshot
Maintenant, je crée le snapshot depuis l’image uploadée :
# Créer le snapshot depuis l'imageSNAPSHOT_RESULT=$(osc-cli api CreateSnapshot \ --FileLocation "$PRESIGNED_URL" \ --SnapshotSize "$VIRTUAL_SIZE" \ --Description "Snapshot NixOS 25.11")
SNAPSHOT_ID=$(echo "$SNAPSHOT_RESULT" | jq -r '.Snapshot.SnapshotId')echo "Snapshot créé: $SNAPSHOT_ID"
# Attendre que le snapshot soit prêtwhile true; do STATE=$(osc-cli api ReadSnapshots \ --Filters "{\"SnapshotIds\": [\"$SNAPSHOT_ID\"]}" \ | jq -r '.Snapshots[0].State') echo "État: $STATE" [ "$STATE" = "completed" ] && break sleep 10doneLa boucle attend que le snapshot soit complètement créé avant de continuer.
Création de l’OMI
On peut enfin créer l’OMI en mappant le snapshot au bon device :
# Créer l'OMI avec le bon mapping de disque (Adapter la taille si besoin)OMI_RESULT=$(osc-cli api CreateImage \ --ImageName "NixOS-25.11" \ --RootDeviceName "/dev/sda1" \ --Architecture "x86_64" \ --Description "NixOS 25.11 image" \ --BlockDeviceMappings "[{ \"DeviceName\": \"/dev/sda1\", \"Bsu\": { \"SnapshotId\": \"$SNAPSHOT_ID\", \"VolumeSize\": $VOLUME_SIZE_GIB, \"VolumeType\": \"gp2\", \"DeleteOnVmDeletion\": true } }]")
OMI_ID=$(echo "$OMI_RESULT" | jq -r '.Image.ImageId')echo "OMI créée: $OMI_ID"Lancer une instance
On peut tester l’OMI en lançant une VM de test :
# Créer une VM de testosc-cli api CreateVms \ --ImageId "$OMI_ID" \ --VmType tinav7.c4r4p1 \ --KeypairName ma-keypair \ --SecurityGroupIds '["sg-xxxxxxxx"]'On récupère l’IP publique de la VM depuis la console Cockpit ou avec `osc-cli api ReadVms:
osc-cli api ReadVms --Filters '{"ImageIds": ["'"$OMI_ID"'"]}' | jq -r '.Vms[0].PublicIp'
<IP-PUBLIQUE-DE-LA-VM>Connexion :
ssh -i ~/.ssh/ma-cle.pem outscale@<IP-PUBLIQUE-DE-LA-VM>Détection de secrets dans le code
Pour éviter de commiter accidentellement des secrets en clair, j’ajoute des
outils de détection dans modules/tools.nix :
{ config, pkgs, ... }:
{ environment.systemPackages = with pkgs; [ # ... autres outils ...
# Détection de secrets gitleaks # Scan Git pour les secrets trufflehog # Détection avancée de secrets ];}Utilisation :
# Scanner le dépôt avec gitleaksgitleaks detect --source .
# Scanner avec trufflehogtrufflehog git file://.Workflow complet
Pour résumer, voici le workflow de mise à jour de l’image :
# 1. Modifier la configurationvim hosts/outscale.nix
# 2. Versionnergit add .git commit -m "feat: Ma modification"
# 3. Reconstruire l'imagenix build .#outscale-qcow -o ./result-outscale
# 4. Tester localement avec QEMUcp result-outscale/nixos.qcow2 /tmp/nixos-test.qcow2qemu-system-x86_64 -m 2048 -cpu qemu64 \ -drive file=/tmp/nixos-test.qcow2,format=qcow2,if=virtio \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0 -nographic
# 5. Uploader vers OOSs3cmd put ./result-outscale/nixos.qcow2 s3://nixos-images/
# 6. Créer snapshot + OMI (voir étape 6 pour les détails)# ...
# 7. Déployer une nouvelle instanceosc-cli api CreateVms --ImageId $OMI_ID --VmType tinav7.c4r4p1 \ --KeypairName ma-keypairConclusion
Ce guide met en place un pipeline complet de génération d’images cloud NixOS :
- Images reproductibles : même configuration = même image
- Secrets protégés : git-agecrypt chiffre les données sensibles dans Git
- Sécurité renforcée : SSH durci, firewall, AppArmor
- Tests locaux : validation avec QEMU avant déploiement
- Déploiement automatisé : scripts
create-omietosc-setup
L’avantage majeur de cette approche est l’auditabilité : chaque changement dans l’image est tracé dans Git, et il est possible de reconstruire exactement la même image des mois plus tard.
Pour fêter la réussite de ce projet, j’ai écris un motd dynamique pour accueillir les utilisateurs lors de la connexion SSH. 🎉

Pour aller plus loin
Après avoir maîtrisé la génération d’images OMI NixOS, complétez votre expertise :
- Durcissement système : Appliquez les recommandations ANSSI-BP-028 ou auditez avec Lynis
- NixOS : Approfondissez avec mon guide NixOS et le langage Nix
- Infrastructure as Code : Déployez vos instances avec Terraform
- Automatisation : Configurez vos images avec Ansible ou Packer
Ressources externes
| Ressource | Description |
|---|---|
| nixos-generators ↗ | Génération d’images multi-formats |
| git-agecrypt ↗ | Chiffrement transparent dans Git |
| Outscale Documentation ↗ | Documentation cloud Outscale |
| NixOS Cloud Wiki ↗ | Wiki NixOS pour le déploiement cloud |