diff --git a/modules/security.nix b/modules/security.nix index 90b1c87..ae78033 100644 --- a/modules/security.nix +++ b/modules/security.nix @@ -1,6 +1,10 @@ { ... }: { + # Module NixOS officiel : installe YubiKey Manager et les règles udev + # nécessaires pour que ykman puisse accéder à la clé en session utilisateur. + programs.yubikey-manager.enable = true; + # Déploiement du mapping utilisateur <-> YubiKey environment.etc."u2f-mappings".source = ../u2f-mappings; diff --git a/modules/users.nix b/modules/users.nix index c09a8c8..c221246 100644 --- a/modules/users.nix +++ b/modules/users.nix @@ -34,6 +34,7 @@ # Avec users.mutableUsers = true, ce hash sert uniquement lors de la # création initiale du compte. Après changement via passwd, le nouveau # mot de passe local est conservé lors des activations suivantes. - initialHashedPassword = "$y$j9T$mra1r8ta998VNiAAoS89a.$cqQKliEqw4k9WvnDeXA.5BqBGGEKVsr5zQ6fwXRid32"; + initialHashedPassword = + "$6$VwAqDD0hXRojZr2T$5EVBlLOVnOvyJxde9uAGj.ehkiQaRis5SwTrSLuVAhiXXvG4DH9DNC.88pWfiL2O28wanR52.23INxuyEtNSQ/"; }; } diff --git a/modules/workstation-setup.nix b/modules/workstation-setup.nix index 7c2927b..4a5799d 100644 --- a/modules/workstation-setup.nix +++ b/modules/workstation-setup.nix @@ -10,76 +10,99 @@ let #!${pkgs.expect}/bin/expect -f # Les secrets arrivent uniquement par stdin depuis l'application. - # Ils ne sont jamais passés dans argv et aucun transcript n'est affiché. + # Ils ne sont jamais placés dans argv et le transcript enfant n'est pas affiché. log_user 0 exp_internal 0 set timeout 30 + proc fail {token code} { + puts stderr $token + exit $code + } + if {[gets stdin current] < 0 || [gets stdin newpass] < 0 || [gets stdin confirm] < 0} { - puts stderr "INPUT_ERROR" - exit 20 + fail "INPUT_ERROR" 20 } if {$newpass ne $confirm} { - puts stderr "CONFIRM_MISMATCH" - exit 21 + fail "CONFIRM_MISMATCH" 21 } spawn -noecho ${pkgs.coreutils}/bin/env LC_ALL=C LANG=C /run/wrappers/bin/passwd + # Étape 1 : passwd doit demander le mot de passe actuel. expect { - -re {(?i)(current.*password|unix password|password.*current).*:} { + -re {(?i)(current|old|unix).*password.*:} { send -- "$current\r" } + -re {(?i)new.*password.*:} { + fail "CURRENT_PROMPT_MISSING" 22 + } + -re {(?i)(authentication failure|incorrect password|password unchanged)} { + fail "CURRENT_REJECTED" 23 + } eof { - puts stderr "CURRENT_REJECTED" - exit 22 + fail "CURRENT_REJECTED" 24 } timeout { - puts stderr "TIMEOUT_CURRENT" - exit 124 + fail "TIMEOUT_CURRENT" 124 } } - # Efface la copie du mot de passe actuel dès qu'elle n'est plus utile. set current "" + # Étape 2 : le nouveau mot de passe n'est envoyé qu'après validation + # du mot de passe actuel par passwd/PAM. expect { -re {(?i)new.*password.*:} { send -- "$newpass\r" } + # Certains passwd redemandent immédiatement le mot de passe courant + # après une erreur. On s'arrête sans envoyer une seconde tentative. + -re {(?i)(current|old|unix).*password.*:} { + fail "CURRENT_REJECTED" 25 + } + -re {(?i)(authentication failure|incorrect password|password unchanged)} { + fail "CURRENT_REJECTED" 25 + } eof { - puts stderr "CURRENT_REJECTED" - exit 23 + fail "CURRENT_REJECTED" 26 } timeout { - puts stderr "TIMEOUT_NEW" - exit 124 + fail "TIMEOUT_NEW" 124 } } + # Étape 3 : confirmation. Un nouveau prompt "New password" à ce stade + # signifie que PAM/passwd a refusé la valeur proposée. expect { -re {(?i)(retype|repeat|confirm).*password.*:} { send -- "$confirm\r" } -re {(?i)new.*password.*:} { - puts stderr "NEW_REJECTED" - exit 24 + fail "NEW_REJECTED" 27 } eof { - puts stderr "NEW_REJECTED" - exit 25 + fail "NEW_REJECTED" 28 } timeout { - puts stderr "TIMEOUT_CONFIRM" - exit 124 + fail "TIMEOUT_CONFIRM" 124 } } set newpass "" set confirm "" - expect eof + expect { + eof {} + -re {(?i)new.*password.*:} { + fail "NEW_REJECTED" 29 + } + timeout { + fail "TIMEOUT_FINISH" 124 + } + } + set waitResult [wait] set exitCode [lindex $waitResult 3] @@ -88,8 +111,7 @@ let exit 0 } - puts stderr "PASSWD_FAILED" - exit $exitCode + fail "PASSWD_FAILED" $exitCode ''; }; @@ -99,64 +121,69 @@ let text = '' #!${pkgs.expect}/bin/expect -f - # Même principe que pour passwd : aucune valeur secrète dans argv. + # Une seule invocation de ykman par clic : aucune tentative automatique + # supplémentaire n'est faite en cas de mauvais PIN. log_user 0 exp_internal 0 set timeout 30 + proc fail {token code} { + puts stderr $token + exit $code + } + if {[gets stdin current] < 0 || [gets stdin newpin] < 0 || [gets stdin confirm] < 0} { - puts stderr "INPUT_ERROR" - exit 30 + fail "INPUT_ERROR" 30 } if {$newpin ne $confirm} { - puts stderr "CONFIRM_MISMATCH" - exit 31 + fail "CONFIRM_MISMATCH" 31 } spawn -noecho ${pkgs.coreutils}/bin/env LC_ALL=C LANG=C ${pkgs.yubikey-manager}/bin/ykman fido access change-pin set transcript "" + # Une clé déjà provisionnée doit demander le PIN actuel. Si ykman passe + # directement au nouveau PIN, la clé n'a pas le pré-provisionnement attendu. expect { - -re {Enter your current PIN.*:} { + -re {(?i)enter.*current.*pin.*:} { append transcript $expect_out(buffer) send -- "$current\r" } - -re {No YubiKey|Failed to connect|No FIDO} { - puts stderr "NO_YUBIKEY" - exit 32 + -re {(?i)enter.*new.*pin.*:} { + append transcript $expect_out(buffer) + fail "PIN_NOT_CONFIGURED" 32 + } + -re {(?i)(no yubikey|multiple yubikey|failed to connect|no fido|device.*not found|permission denied|access denied)} { + append transcript $expect_out(buffer) + fail "NO_YUBIKEY" 33 } eof { append transcript $expect_out(buffer) - if {[regexp -nocase {No YubiKey|Failed to connect|No FIDO} $transcript]} { - puts stderr "NO_YUBIKEY" - } else { - puts stderr "PIN_FAILED" + if {[regexp -nocase {no yubikey|multiple yubikey|failed to connect|no fido|device.*not found|permission denied|access denied} $transcript]} { + fail "NO_YUBIKEY" 34 } - exit 33 + fail "PIN_FAILED" 35 } timeout { - puts stderr "TIMEOUT_CURRENT_PIN" - exit 124 + fail "TIMEOUT_CURRENT_PIN" 124 } } set current "" expect { - -re {Enter your new PIN.*:} { + -re {(?i)enter.*new.*pin.*:} { append transcript $expect_out(buffer) send -- "$newpin\r" } eof { append transcript $expect_out(buffer) - puts stderr "PIN_FAILED" - exit 34 + fail "PIN_FAILED" 36 } timeout { - puts stderr "TIMEOUT_NEW_PIN" - exit 124 + fail "TIMEOUT_NEW_PIN" 124 } } @@ -167,12 +194,10 @@ let } eof { append transcript $expect_out(buffer) - puts stderr "PIN_FAILED" - exit 35 + fail "PIN_FAILED" 37 } timeout { - puts stderr "TIMEOUT_CONFIRM_PIN" - exit 124 + fail "TIMEOUT_CONFIRM_PIN" 124 } } @@ -189,27 +214,25 @@ let exit 0 } - if {[regexp -nocase {Wrong PIN|PIN_INVALID} $transcript]} { - puts stderr "WRONG_PIN" - } elseif {[regexp -nocase {authentication is currently blocked|PIN_AUTH_BLOCKED} $transcript]} { - puts stderr "PIN_AUTH_BLOCKED" - } elseif {[regexp -nocase {PIN is blocked|PIN_BLOCKED} $transcript]} { - puts stderr "PIN_BLOCKED" - } elseif {[regexp -nocase {No YubiKey|Failed to connect|No FIDO} $transcript]} { - puts stderr "NO_YUBIKEY" - } elseif {[regexp -nocase {complexity|policy|at least|at most} $transcript]} { - puts stderr "PIN_POLICY" - } else { - puts stderr "PIN_FAILED" + if {[regexp -nocase {pin[_ ]auth[_ ]blocked|ctaperr_pin_auth_blocked|temporarily blocked} $transcript]} { + fail "PIN_AUTH_BLOCKED" $exitCode + } elseif {[regexp -nocase {pin[_ ]blocked|ctaperr_pin_blocked|pin is blocked} $transcript]} { + fail "PIN_BLOCKED" $exitCode + } elseif {[regexp -nocase {wrong pin|pin_invalid|pin auth invalid|pin verification failed|ctaperr_pin_invalid} $transcript]} { + fail "WRONG_PIN" $exitCode + } elseif {[regexp -nocase {no yubikey|multiple yubikey|failed to connect|no fido|device.*not found|permission denied|access denied} $transcript]} { + fail "NO_YUBIKEY" $exitCode + } elseif {[regexp -nocase {complexity|policy|minimum pin length|must be at least|too short} $transcript]} { + fail "PIN_POLICY" $exitCode } - exit $exitCode + fail "PIN_FAILED" $exitCode ''; }; workstationSetup = pkgs.stdenv.mkDerivation { pname = "nixos-workstations-setup"; - version = "1.0.0"; + version = "1.1.0"; src = ../workstation-setup; @@ -230,14 +253,17 @@ let cmakeFlags = [ "-DPASSWORD_HELPER_PATH=${passwordHelper}" "-DPIN_HELPER_PATH=${pinHelper}" - "-DTARGET_USER=${cfg.user}" ]; }; launcher = pkgs.writeShellScript "nixos-workstations-setup-launcher" '' set -eu - if [ "$(${pkgs.coreutils}/bin/id -un)" != ${lib.escapeShellArg cfg.user} ]; then + current_user="$(${pkgs.coreutils}/bin/id -un)" + target_user=${lib.escapeShellArg cfg.user} + + # L'autostart ne doit concerner que l'utilisateur configuré. + if [ "$current_user" != "$target_user" ]; then exit 0 fi @@ -245,6 +271,7 @@ let password_marker="$state_dir/password-initialized" pin_marker="$state_dir/yubikey-pin-initialized" + # Une session déjà finalisée n'affiche plus l'assistant. if [ -e "$password_marker" ] && [ -e "$pin_marker" ]; then exit 0 fi @@ -252,11 +279,11 @@ let # Ne jamais générer de core dump contenant potentiellement un secret. ulimit -c 0 - # Laisse Plasma finir son démarrage sans créer de logique de supervision - # susceptible de tuer ou manipuler la session utilisateur. + # Laisse Plasma terminer son démarrage. ${pkgs.coreutils}/bin/sleep 3 - exec ${workstationSetup}/bin/nixos-workstations-setup + exec ${workstationSetup}/bin/nixos-workstations-setup \ + --target-user "$target_user" ''; in @@ -274,7 +301,6 @@ in config = lib.mkIf cfg.enable { environment.systemPackages = [ workstationSetup - pkgs.yubikey-manager ]; environment.etc."xdg/autostart/nixos-workstations-setup.desktop".text = ''