password & keyring cleanup: one password, one truth, no prompts
The Hub, GDM and sudo now share /etc/shadow as the single source of
truth. /var/lib/secrets/{free,root}-password become root-only display
copies, written only by system_password.py and verified against shadow
before being shown. The GNOME login keyring is decoupled from the login
password (blank master password, pam_gnome_keyring off), so nothing
needs re-keying or wiping when the password changes and the keyring
never pops an unlock prompt.
- add app/sovran_systemsos_web/system_password.py: the single module
that verifies / sets / displays the free + root passwords and drives
the legacy-migration onboarding step (same endpoints and UI)
- add modules/password-model.nix: PAM gnome-keyring off, blank
login.keyring seed with a one-time migration of old encrypted
keyrings, first-boot-only password application (no more boot-revert),
LIBCRYPT for the Hub
- server.py: login verifies against shadow and heals stale display
copies; change-password takes the current password; security reset
sets both passwords through the same writer (root is shown as a
password again, not a hash); all keyring wipes removed
- credentials.nix: drop root-password-setup, free-password-setup and
free-password-migration (replaced by password-model.nix) and the
keyring wipe in change-free-password
- configuration.nix: drop enableGnomeKeyring and the keyring tmpfiles
hack (replaced by password-model.nix)
- service-detail.js: "Current password" field; credentials changed
outside the Hub render with a hint instead of a stale value
Installed machines keep their exact passwords: the first-boot services
no-op once an account has a real hash, and the upgrade never writes
/etc/shadow. The legacy-migration onboarding step and "reset all
passwords" keep working.
This commit is contained in:
1 parent
5e56b07465
commit
4a25ef24d1
8 files changed
+430
-437
No files matched your search
@@ -33,9 +33,6 @@ let
|
||||
echo "$NEW_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Password for 'free' updated and saved."
|
||||
# Delete the old GNOME Keyring databases so a fresh one is created on next GDM login.
|
||||
rm -f /home/free/.local/share/keyrings/*.keyring
|
||||
echo "GNOME Keyring files cleared — a fresh keyring will be created on next login."
|
||||
'';
|
||||
in
|
||||
{
|
||||
@@ -76,172 +73,4 @@ in
|
||||
command passwd $argv
|
||||
end
|
||||
'';
|
||||
|
||||
# ── 1. Auto-Generate Root Password (Runs once) ─────────────
|
||||
systemd.services.root-password-setup = {
|
||||
description = "Generate and set a random root password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.shadow pkgs.coreutils ];
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
SECRET_FILE="/var/lib/secrets/root-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
# Generate a diceware-style passphrase: word-word-word-word-NN
|
||||
WORDS="apple barn brook cabin cedar cloud coral crane delta eagle ember \
|
||||
fern field flame flora flint frost grove haven hedge holly heron \
|
||||
jade juniper kelp larch lemon lilac linden loch lotus maple marsh \
|
||||
meadow mist mossy mount oak ocean olive petal pine pixel plum pond \
|
||||
prism quartz raven ridge river robin rocky rose rowan sage sand \
|
||||
sierra silver slate snow solar spark spruce stone storm summit \
|
||||
swift thorn tide timber torch trout vale vault vine walnut wave \
|
||||
willow wren amber aspen birch blaze bloom bluff coast copper crest \
|
||||
dune elder fjord forge glade glen glow gulf"
|
||||
WORD_ARRAY=($WORDS)
|
||||
COUNT=''${#WORD_ARRAY[@]}
|
||||
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W4=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
DIGIT=$(printf '%02d' $((RANDOM % 100)))
|
||||
ROOT_PASS="$W1-$W2-$W3-$W4-$DIGIT"
|
||||
echo "$ROOT_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
# If the file contains a scrypt hash (salt:hash), skip chpasswd — the
|
||||
# password was already set via the Hub security reset endpoint and this
|
||||
# service is only re-running as a manual recovery step.
|
||||
CONTENT="$(cat "$SECRET_FILE")"
|
||||
if echo "$CONTENT" | grep -qE '^[0-9a-f]{32}:[0-9a-f]{64,}$'; then
|
||||
echo "root-password-setup: stored value is already hashed — skipping chpasswd" >&2
|
||||
else
|
||||
echo "root:$CONTENT" | chpasswd
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── 1b. Generate random 'free' password on first boot ──────
|
||||
systemd.services.free-password-setup = {
|
||||
description = "Generate and set a random 'free' user password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "display-manager.service" ];
|
||||
after = [ "systemd-user-sessions.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.shadow pkgs.coreutils ];
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
PENDING_FILE="/var/lib/secrets/free-password-migration-pending"
|
||||
|
||||
if [ -f "$SECRET_FILE" ]; then
|
||||
echo "free:$(cat "$SECRET_FILE")" | chpasswd
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHADOW_HASH=""
|
||||
while IFS=: read -r user hash _; do
|
||||
if [ "$user" = "free" ]; then
|
||||
SHADOW_HASH="$hash"
|
||||
break
|
||||
fi
|
||||
done < /etc/shadow
|
||||
|
||||
HAS_REAL_HASH=0
|
||||
case "$SHADOW_HASH" in
|
||||
""|"!"|"*"|"!!"|"!"*|"*"*)
|
||||
HAS_REAL_HASH=0
|
||||
;;
|
||||
*)
|
||||
HAS_REAL_HASH=1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$HAS_REAL_HASH" -eq 1 ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
touch "$PENDING_FILE"
|
||||
chmod 600 "$PENDING_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p /var/lib/secrets
|
||||
# Generate a diceware-style passphrase: word-word-word-word-NN
|
||||
WORDS="apple barn brook cabin cedar cloud coral crane delta eagle ember \
|
||||
fern field flame flora flint frost grove haven hedge holly heron \
|
||||
jade juniper kelp larch lemon lilac linden loch lotus maple marsh \
|
||||
meadow mist mossy mount oak ocean olive petal pine pixel plum pond \
|
||||
prism quartz raven ridge river robin rocky rose rowan sage sand \
|
||||
sierra silver slate snow solar spark spruce stone storm summit \
|
||||
swift thorn tide timber torch trout vale vault vine walnut wave \
|
||||
willow wren amber aspen birch blaze bloom bluff coast copper crest \
|
||||
dune elder fjord forge glade glen glow gulf"
|
||||
WORD_ARRAY=($WORDS)
|
||||
COUNT=''${#WORD_ARRAY[@]}
|
||||
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W4=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
DIGIT=$(printf '%02d' $((RANDOM % 100)))
|
||||
FREE_PASS="$W1-$W2-$W3-$W4-$DIGIT"
|
||||
echo "$FREE_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "free:$FREE_PASS" | chpasswd
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.free-password-migration = {
|
||||
description = "Generate and set 'free' password for migrated machines";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "display-manager.service" ];
|
||||
after = [ "systemd-user-sessions.service" "free-password-setup.service" ];
|
||||
requires = [ "free-password-setup.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.shadow pkgs.coreutils ];
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
PENDING_FILE="/var/lib/secrets/free-password-migration-pending"
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
NEWPASS_FILE="/var/lib/secrets/free-password-migration-newpass"
|
||||
|
||||
[ -f "$PENDING_FILE" ] || exit 0
|
||||
|
||||
mkdir -p /var/lib/secrets
|
||||
|
||||
WORDS="apple barn brook cabin cedar cloud coral crane delta eagle ember \
|
||||
fern field flame flora flint frost grove haven hedge holly heron \
|
||||
jade juniper kelp larch lemon lilac linden loch lotus maple marsh \
|
||||
meadow mist mossy mount oak ocean olive petal pine pixel plum pond \
|
||||
prism quartz raven ridge river robin rocky rose rowan sage sand \
|
||||
sierra silver slate snow solar spark spruce stone storm summit \
|
||||
swift thorn tide timber torch trout vale vault vine walnut wave \
|
||||
willow wren amber aspen birch blaze bloom bluff coast copper crest \
|
||||
dune elder fjord forge glade glen glow gulf"
|
||||
WORD_ARRAY=($WORDS)
|
||||
COUNT=''${#WORD_ARRAY[@]}
|
||||
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
W4=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||
DIGIT=$(printf '%02d' $((RANDOM % 100)))
|
||||
FREE_PASS="$W1-$W2-$W3-$W4-$DIGIT"
|
||||
|
||||
printf '%s\n' "$FREE_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
|
||||
printf '%s\n' "$FREE_PASS" > "$NEWPASS_FILE"
|
||||
chmod 600 "$NEWPASS_FILE"
|
||||
rm -f "$PENDING_FILE"
|
||||
'';
|
||||
};
|
||||
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
# ── Always on (no flag) ───────────────────────────────────
|
||||
./php.nix
|
||||
./credentials.nix
|
||||
./password-model.nix
|
||||
|
||||
# ── Services (default ON — disable in custom.nix) ─────────
|
||||
./synapse.nix
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
# Sovran_SystemsOS -- password model cleanup (GNOME 50 / gnome-keyring 50)
|
||||
#
|
||||
# * /etc/shadow is the only source of truth for authentication (Hub login == GDM login).
|
||||
# * /var/lib/secrets/{free,root}-password are root-only DISPLAY copies for the Hub, written
|
||||
# by exactly one module (system_password.py) and verified against shadow before shown.
|
||||
# * The GNOME login keyring is decoupled from the login password: it is created with a
|
||||
# blank password and pam_gnome_keyring never hands the login password to the daemon.
|
||||
# => no "unlock your login keyring" prompts, ever (also on the autologin live ISO);
|
||||
# Brave's cookie/password key survives every password change; no *.keyring wiping.
|
||||
# * Legacy machines (password unknown to the Hub) are handled by the Hub's existing
|
||||
# onboarding "migration password" step -- not by a boot service any more.
|
||||
#
|
||||
# Import from configuration.nix and DELETE these lines there:
|
||||
# security.pam.services.gdm-password.enableGnomeKeyring = true;
|
||||
# security.pam.services.gdm-autologin.enableGnomeKeyring = true;
|
||||
# the four systemd.tmpfiles.rules for /home/free/.local/share/keyrings/{,default}
|
||||
# and in modules/credentials.nix delete root-password-setup, free-password-setup,
|
||||
# free-password-migration and the `rm -f /home/free/.local/share/keyrings/*.keyring` lines.
|
||||
|
||||
let
|
||||
user = "free";
|
||||
home = "/home/${user}";
|
||||
|
||||
# A login keyring with a blank password. gnome-keyring stores those in this textual
|
||||
# format and auto-unlocks them on demand without any prompt (verified on 48; the rule in
|
||||
# gkm-secret-collection.c that picks the textual format for a blank master is unchanged
|
||||
# in 50 and the 50.0 release notes contain no behavioural changes).
|
||||
blankLoginKeyring = pkgs.writeText "login.keyring" ''
|
||||
[keyring]
|
||||
display-name=login
|
||||
ctime=0
|
||||
mtime=0
|
||||
lock-on-idle=false
|
||||
lock-after=false
|
||||
'';
|
||||
|
||||
# Apply the installer's plaintext password ONCE: only while the account still has no
|
||||
# usable hash (nixos-install --no-root-password leaves both accounts locked). After that
|
||||
# the file is display-only and the Hub (system_password.py) is the only thing that
|
||||
# writes it. This replaces the old services, which re-applied the file on EVERY boot
|
||||
# and silently reverted any password changed elsewhere.
|
||||
# On already-installed machines the case below matches `*)` (real hash) -> no-op:
|
||||
# an upgrade can never change or generate a password.
|
||||
applyOnce = { account, fallback ? false }: ''
|
||||
set -euo pipefail
|
||||
f=/var/lib/secrets/${account}-password
|
||||
hash="$(getent shadow ${account} | cut -d: -f2 || true)"
|
||||
case "$hash" in
|
||||
""|"!"*|"*"*)
|
||||
if [ -s "$f" ] && ! grep -qE '^[0-9a-f]{32}:[0-9a-f]{64,}$' "$f"; then
|
||||
echo "${account}:$(head -n1 "$f")" | chpasswd
|
||||
${lib.optionalString fallback ''
|
||||
else
|
||||
# Installer left no usable file: generate one so GDM is always reachable
|
||||
# (the user reads it in the Hub). Never runs once the account has a password.
|
||||
W="apple barn brook cabin cedar cloud coral crane delta eagle ember fern field flame
|
||||
flora flint frost grove haven hedge holly heron jade juniper kelp larch lemon lilac
|
||||
linden loch lotus maple marsh meadow mist mossy mount oak ocean olive petal pine
|
||||
pixel plum pond prism quartz raven ridge river robin rocky rose rowan sage sand
|
||||
sierra silver slate snow solar spark spruce stone storm summit swift thorn tide
|
||||
timber torch trout vale vault vine walnut wave willow wren"
|
||||
pw=$(for i in 1 2 3 4; do set -- $W; eval echo \${$((RANDOM % $# + 1))}; done | paste -sd- -)-$(printf '%02d' $((RANDOM % 100)))
|
||||
umask 077; printf '%s\n' "$pw" > "$f"
|
||||
echo "${account}:$pw" | chpasswd
|
||||
''}
|
||||
fi ;;
|
||||
*) ;; # password already set: never touch it here again
|
||||
esac
|
||||
'';
|
||||
in
|
||||
{
|
||||
# ── 1. Keyring: never learns the login password ───────────────────────────
|
||||
# nixpkgs' gnome-keyring module turns this on for `login` (gdm-password includes it).
|
||||
# If the daemon ever receives the login password it re-encrypts the blank keyring with
|
||||
# it on the next save -- so it must be off everywhere.
|
||||
security.pam.services.login.enableGnomeKeyring = lib.mkForce false;
|
||||
security.pam.services.gdm-password.enableGnomeKeyring = lib.mkForce false;
|
||||
security.pam.services.gdm-autologin.enableGnomeKeyring = lib.mkForce false;
|
||||
security.pam.services.passwd.enableGnomeKeyring = lib.mkForce false;
|
||||
|
||||
# Seed the blank keyring. `C` copies only if the file does not exist, so an existing
|
||||
# keyring is never overwritten -- which is also what makes a reset's `rm *.keyring`
|
||||
# harmless: the next boot re-seeds a fresh blank keyring.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${home}/.local 0700 ${user} users -"
|
||||
"d ${home}/.local/share 0700 ${user} users -"
|
||||
"d ${home}/.local/share/keyrings 0700 ${user} users -"
|
||||
"C ${home}/.local/share/keyrings/login.keyring 0600 ${user} users - ${blankLoginKeyring}"
|
||||
];
|
||||
|
||||
# One-time migration for machines that already have a keyring encrypted with the
|
||||
# (old) login password: move it aside and seed the blank one. Runs before GDM; the old
|
||||
# file is kept. Same cost users pay today on every password change (Brave logs you out
|
||||
# of sites once) -- but only once, ever.
|
||||
system.activationScripts.sovranBlankKeyring = lib.stringAfter [ "users" ] ''
|
||||
d=${home}/.local/share/keyrings
|
||||
if [ -f "$d/login.keyring" ] && ! head -c 9 "$d/login.keyring" | grep -q '^\[keyring\]'; then
|
||||
mv "$d/login.keyring" "$d/login.keyring.encrypted-$(date +%Y%m%d)"
|
||||
install -o ${user} -g users -m 0600 ${blankLoginKeyring} "$d/login.keyring"
|
||||
fi
|
||||
'';
|
||||
|
||||
# ── 2. First-boot password hand-off (unit names kept: the Hub's "System Passwords"
|
||||
# service card is bound to root-password-setup.service) ───────────────────────
|
||||
systemd.services.free-password-setup = {
|
||||
description = "Apply the installer's 'free' password (first boot only)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "display-manager.service" ];
|
||||
serviceConfig = { Type = "oneshot"; RemainAfterExit = true; };
|
||||
path = [ pkgs.shadow pkgs.coreutils pkgs.gnugrep pkgs.getent ];
|
||||
script = applyOnce { account = user; fallback = true; };
|
||||
};
|
||||
systemd.services.root-password-setup = {
|
||||
description = "Apply the installer's root password (first boot only)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = { Type = "oneshot"; RemainAfterExit = true; };
|
||||
path = [ pkgs.shadow pkgs.coreutils pkgs.gnugrep pkgs.getent ];
|
||||
script = applyOnce { account = "root"; };
|
||||
};
|
||||
# Legacy detection now lives in the Hub (system_password.ensure_migration_pending).
|
||||
systemd.services.free-password-migration.enable = false;
|
||||
|
||||
# ── 3. The Hub verifies passwords against /etc/shadow via libxcrypt ───────
|
||||
systemd.services.sovran-hub-web.environment.LIBCRYPT = "${pkgs.libxcrypt}/lib/libcrypt.so.1";
|
||||
}
|
||||
Reference in new issue
Block a user