Chiffrer ses secrets avec sops dans un module customConfig

Tôt ou tard, votre configuration a besoin d'un secret : un jeton d'API, un mot de passe de partage réseau, une clé privée. Le réflexe est de l'écrire en clair dans un fichier .nix, et c'est précisément ce qu'il ne faut pas faire. Cet article explique pourquoi, puis détaille un module secret-mngt.nix complet qui met en place sops-nix et le trousseau de session.

:information_source: Remarque
Cet article fait partie d'une série sur les modules à importer dans customConfig. Si vous n'avez jamais édité ces fichiers, lisez d'abord Ajouter vos programmes et Configuration personnalisée.


Pourquoi pas en clair

Tout ce que NixOS évalue finit dans /nix/store. Or ce dossier est lisible par tous les utilisateurs de la machine, par construction : c'est ce qui permet le partage des paquets et le cache binaire. Un mot de passe écrit dans un fichier .nix se retrouve donc en clair dans un chemin que n'importe qui peut afficher, et il y reste tant que la génération correspondante n'a pas été supprimée par le ramasse-miettes.

sops-nix règle le problème proprement : vous versionnez un fichier chiffré, que le système déchiffre au moment de l'activation, dans un dossier protégé qui ne passe jamais par le store.


Ce que vous obtenez

  • un fichier de secrets chiffré, que vous pouvez sauvegarder, versionner ou publier sans risque ;
  • des secrets déposés dans /run/secrets/, lisibles seulement par qui vous décidez ;
  • la possibilité d'insérer un secret dans un fichier de configuration sans jamais l'écrire en clair ;
  • le trousseau de session, pour les secrets qui appartiennent à votre session graphique.

Le fichier

Créez /etc/nixos/customConfig/secret-mngt.nix :

{ config, lib, pkgs, ... }:

let
  sops-nix = builtins.fetchTarball {
    url = "https://github.com/Mic92/sops-nix/archive/9ed65852b6257fbeae4355bc24ecfea307ca759a.tar.gz";
    sha256 = "0jm1xzqp508avayvnx2q7qiw7wj1kqz6m2g1vr0i1vj03qvhmbqs";
  };
in
{
  imports = [ "${sops-nix}/modules/sops" ];

  sops.defaultSopsFile = ./secrets/secrets.yaml;
  sops.defaultSopsFormat = "yaml";
  sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];

  sops.secrets."github-token" = { };

  sops.templates."nix-access-tokens.conf".content = ''
    access-tokens = github.com=${config.sops.placeholder."github-token"}
  '';

  nix.extraOptions = ''
    !include ${config.sops.templates."nix-access-tokens.conf".path}
  '';

  services.gnome.gnome-keyring.enable = lib.mkDefault true;
  security.pam.services.gdm-password.enableGnomeKeyring = lib.mkDefault true;

  environment.systemPackages = with pkgs; [
    seahorse
    libsecret
    sops
    age
    ssh-to-age
  ];
}

Ce que fait chaque ligne

Le bloc let. sops-nix n'est pas dans nixpkgs, il faut aller le chercher. builtins.fetchTarball avec un commit figé et son empreinte garde tout dans ce seul fichier, sans toucher au flake.nix du système. La version est donc épinglée : elle ne bougera pas dans votre dos.

imports = [ "${sops-nix}/modules/sops" ]; charge le module téléchargé. Oui, un module peut en importer un autre, y compris un module venu de l'extérieur.

sops.defaultSopsFile désigne le fichier chiffré. sops.defaultSopsFormat dit comment le lire.

sops.age.sshKeyPaths est le choix le plus commode : plutôt que de gérer une clé de déchiffrement supplémentaire, on se sert de la clé SSH d'hôte, créée à l'installation et déjà présente sur la machine.

sops.secrets."github-token" = { }; déclare un secret. Les accolades vides veulent dire « avec les réglages par défaut », c'est-à-dire un fichier dans /run/secrets/github-token, lisible par root seul.

sops.templates est la partie la plus élégante. Un secret seul sert rarement : il faut souvent l'insérer dans un fichier de configuration. Vous écrivez donc le fichier avec un emplacement réservé, config.sops.placeholder."nom", et sops le remplace par la vraie valeur au moment de l'activation, hors du store.

nix.extraOptions inclut le fragment produit. Le point d'exclamation devant include rend l'inclusion silencieuse tant que le fichier n'existe pas, ce qui évite de bloquer le tout premier démarrage, avant que sops ait tourné une première fois.

La fin du module installe GNOME Keyring et Seahorse, l'autre famille de secrets. Voyez plus bas.


La mise en place, une fois pour toutes

1. Récupérer la clé de déchiffrement. Votre machine possède une clé SSH d'hôte. Convertissez sa partie publique en format age :

nix-shell -p ssh-to-age --run 'ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub'

Vous obtenez une ligne qui commence par age1.

2. Dire à sops pour qui chiffrer. Écrivez /etc/nixos/.sops.yaml :

keys:
  - &host age1votreclepubliqueici
creation_rules:
  - path_regex: secrets/[^/]+\.yaml$
    key_groups:
      - age:
          - *host

Ce fichier n'est lu que par la commande sops au moment où vous éditez un secret. Il ne contient aucune information sensible : une clé publique est faite pour être connue.

3. Écrire le secret.

sudo mkdir -p /etc/nixos/customConfig/secrets
sudo -E EDITOR=nano sops /etc/nixos/customConfig/secrets/secrets.yaml

La commande ouvre votre éditeur sur un contenu déchiffré et rechiffre à l'enregistrement. Le -E conserve vos variables d'environnement et EDITOR choisit l'éditeur : mettez vim, micro ou ce que vous préférez.

Le contenu est un simple YAML :

github-token: ghp_votrejetonici

4. Appliquer. Après glf-update, votre secret est déposé dans /run/secrets/github-token. Vérifiez :

sudo ls -l /run/secrets/

Le fichier chiffré, lui, peut être sauvegardé, versionné ou publié : sans la clé, il ne dit rien.

:stop_sign: Prudence
Si vous réinstallez la machine, la clé d'hôte change et vos secrets deviennent illisibles. Sauvegardez /etc/ssh/ssh_host_ed25519_key, ou ajoutez une seconde clé dans .sops.yaml (une clé age personnelle rangée sur une clé USB, par exemple) pour garder une porte d'entrée. Après avoir ajouté une clé, lancez sops updatekeys secrets.yaml pour rechiffrer le fichier à destination de tout le monde.


Deux niveaux, deux usages

La fin du module installe GNOME Keyring et Seahorse. C'est l'autre famille de secrets, ceux de votre session : mots de passe d'applications, jetons que vos scripts lisent quand vous êtes connecté. Le trousseau se déverrouille avec votre mot de passe de session et se lit en ligne de commande :

secret-tool store --label='Ma clé' service monservice key macle
secret-tool lookup service monservice key macle

Retenez la répartition :

  • sops pour ce dont le système a besoin, avant même que vous ayez ouvert une session : jetons de construction, mots de passe de services, clés de montage réseau ;
  • le trousseau pour ce qui appartient à votre session : comptes d'applications, jetons que vous utilisez à la main.

Activer le module

Dans /etc/nixos/customConfig/default.nix :

{
  imports = [ ./secret-mngt.nix ];

  environment.systemPackages = [
  # Add your stable apps here (exemple: pkgs.btop)

Puis :

glf-update

Les options que vous pouvez changer

  • Récupérer sops-nix autrement. Le fetchTarball garde tout dans un fichier, sans toucher au flake du système. Si vous préférez, déclarez sops-nix en entrée de flake : sa mise à jour suivra alors celle des autres entrées.
  • Une clé age dédiée, si vous ne voulez pas dépendre de la clé d'hôte :
    sops.age.keyFile = "/var/lib/sops-nix/key.txt";
    
  • Donner un secret à un service précis. owner, group et mode règlent les droits du fichier déchiffré, path le range ailleurs que dans /run/secrets :
    sops.secrets."jeton-de-service" = {
      owner = "monservice";
      group = "monservice";
      mode = "0400";
    };
    
  • Redémarrer un service quand son secret change :
    sops.secrets."jeton-de-service".restartUnits = [ "monservice.service" ];
    
  • Plusieurs fichiers de secrets : sops.secrets."x".sopsFile = ./secrets/autre.yaml; sort du fichier par défaut.
  • Chiffrer un fichier entier plutôt qu'une liste de clés, par exemple une configuration WireGuard :
    sops.secrets."proton-wg" = {
      format = "binary";
      sopsFile = ./secrets/proton.conf;
      path = "/etc/wireguard/proton.conf";
    };
    

En cas de problème

  • no key could be found à l'édition : .sops.yaml n'est pas au bon endroit, ou son path_regex ne correspond pas au chemin de votre fichier. Le chemin est évalué relativement au dossier où se trouve .sops.yaml.
  • L'activation échoue sur un secret : la clé d'hôte ne fait pas partie des destinataires du fichier. Ajoutez-la dans .sops.yaml, puis sops updatekeys.
  • Le fichier n'apparaît pas dans /run/secrets : le secret est déclaré mais absent du fichier chiffré, ou le nom ne correspond pas exactement. Les deux doivent s'écrire pareil, majuscules comprises.
  • Vous avez commis un secret en clair par erreur. Considérez-le comme compromis : changez-le à la source, ne vous contentez pas de l'effacer. Un secret qui a existé en clair dans le store ou dans un dépôt a pu être lu.

Voir aussi