Aller au contenu

Images NixOS pour Outscale

Mise à jour :

logo 3ds outscale

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-cli ou 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.follows assure que tout utilise la même version de nixpkgs
  • format = "qcow" génère une image QCOW2 compatible Outscale
  • virtualisation.diskSize dé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

Terminal window
# Installer git-agecrypt
nix profile install github:vlaci/git-agecrypt
# Générer une clé age (si vous n'en avez pas)
age-keygen -o ~/.config/age/keys.txt

La commande age-keygen affiche votre clé publique :

Public key: age1stq2rxg545r3m5stdv7ch60e3t30qepqz4h0tg4wsqh5whl4443sx582sg

Conservez 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 :

Terminal window
cat > git-agecrypt.toml << 'EOF'
[config]
"secrets/password-hash.txt" = ["age1stq2rxg545r3m5stdv7ch60e3t30qepqz4h0tg4wsqh5whl4443sx582sg"]
EOF

Remplacez la clé publique par la vôtre.

Je crée le fichier .gitattributes pour activer le filtre :

Terminal window
cat > .gitattributes << 'EOF'
secrets/password-hash.txt filter=git-agecrypt diff=git-agecrypt
EOF

J’initialise git-agecrypt dans le dépôt :

Terminal window
git-agecrypt init

Créer le secret

Je génère un hash de mot de passe et le stocke :

Terminal window
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.txt

Je vérifie que le chiffrement fonctionne :

Terminal window
# Le fichier local est en clair
cat secrets/password-hash.txt
# Affiche : $6$uY4uFIM7VeAHL6qh$3uiwDtRY0Z...
# Mais dans Git, il sera chiffré
git add secrets/password-hash.txt
git 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 :

Terminal window
cd /mnt/c/Users/votre-user/Nixos-WSL
# Versionner les changements
git add .
git commit -m "feat: Ajout génération image Outscale"
# Générer l'image QCOW2
nix build .#outscale-qcow -o ./result-outscale

La génération prend plusieurs minutes. Le résultat est un lien symbolique vers l’image :

Terminal window
ls -la result-outscale/
# nixos.qcow2

Étape 5 : Tester localement avec QEMU

Avant de déployer sur Outscale, je teste l’image localement :

Terminal window
# Copier l'image pour ne pas modifier l'original
cp result-outscale/nixos.qcow2 /tmp/nixos-test.qcow2
# Lancer QEMU
qemu-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

Une fois la VM démarrée, je peux me connecter en SSH :

Terminal window
# 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 :

Terminal window
# Installer osc-sdk
pipx install osc-sdk
# Créer le répertoire de configuration
mkdir -p ~/.osc
chmod 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"
}
}
EOF
chmod 600 ~/.osc/config.json

Configuration de s3cmd pour OOS

Je configure s3cmd pour accéder à Outscale Object Storage :

Terminal window
# Créer le fichier de configuration (remplacez par vos credentials)
cat > ~/.s3cfg << EOF
[default]
access_key = $OSC_ACCESS_KEY
secret_key = $OSC_SECRET_KEY
host_base = oos.eu-west-2.outscale.com
host_bucket = %(bucket)s.oos.eu-west-2.outscale.com
use_https = True
signature_v2 = False
EOF
chmod 600 ~/.s3cfg

Upload de l’image vers OOS

Terminal window
# Créer le bucket
s3cmd mb s3://nixos-images
# Uploader l'image QCOW2
s3cmd put ./result-outscale/nixos.qcow2 s3://nixos-images/ --progress

Création d’une URL pré-signée

L’API Outscale nécessite une URL accessible pour créer le snapshot :

Terminal window
# 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 :

Terminal window
# La taille virtuelle (pas la taille fichier) est nécessaire pour le snapshot
VIRTUAL_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 :

Terminal window
# Créer le snapshot depuis l'image
SNAPSHOT_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êt
while true; do
STATE=$(osc-cli api ReadSnapshots \
--Filters "{\"SnapshotIds\": [\"$SNAPSHOT_ID\"]}" \
| jq -r '.Snapshots[0].State')
echo "État: $STATE"
[ "$STATE" = "completed" ] && break
sleep 10
done

La 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 :

Terminal window
# 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 :

Terminal window
# Créer une VM de test
osc-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:

Terminal window
osc-cli api ReadVms --Filters '{"ImageIds": ["'"$OMI_ID"'"]}' | jq -r '.Vms[0].PublicIp'
<IP-PUBLIQUE-DE-LA-VM>

Connexion :

Terminal window
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 :

Terminal window
# Scanner le dépôt avec gitleaks
gitleaks detect --source .
# Scanner avec trufflehog
trufflehog git file://.

Workflow complet

Pour résumer, voici le workflow de mise à jour de l’image :

Terminal window
# 1. Modifier la configuration
vim hosts/outscale.nix
# 2. Versionner
git add .
git commit -m "feat: Ma modification"
# 3. Reconstruire l'image
nix build .#outscale-qcow -o ./result-outscale
# 4. Tester localement avec QEMU
cp result-outscale/nixos.qcow2 /tmp/nixos-test.qcow2
qemu-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 OOS
s3cmd 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 instance
osc-cli api CreateVms --ImageId $OMI_ID --VmType tinav7.c4r4p1 \
--KeypairName ma-keypair

Conclusion

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-omi et osc-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. 🎉

motd NixOS Outscale

Pour aller plus loin

Après avoir maîtrisé la génération d’images OMI NixOS, complétez votre expertise :

Ressources externes

RessourceDescription
nixos-generatorsGénération d’images multi-formats
git-agecryptChiffrement transparent dans Git
Outscale DocumentationDocumentation cloud Outscale
NixOS Cloud WikiWiki NixOS pour le déploiement cloud