Merge pull request #127 from naturallaw777/copilot/update-api-password-check

Use /etc/shadow as authoritative source for factory default password detection
This commit is contained in:
Sovran_Systems
2026-04-07 13:55:53 -05:00
committed by GitHub
2 changed files with 54 additions and 11 deletions

View File

@@ -2963,15 +2963,42 @@ async def api_security_status():
return {"status": status, "warning": warning} return {"status": status, "warning": warning}
def _is_free_password_default() -> bool:
"""Check /etc/shadow directly to see if 'free' still has a factory default password.
Hashes each known factory default against the current shadow hash so that
password changes made via GNOME, passwd, or any method other than the Hub
are detected correctly.
"""
import crypt # available in Python stdlib (deprecated in 3.13 but present on NixOS)
FACTORY_DEFAULTS = ["free", "gosovransystems"]
try:
with open("/etc/shadow", "r") as f:
for line in f:
parts = line.strip().split(":")
if parts[0] == "free" and len(parts) > 1:
current_hash = parts[1]
if not current_hash or current_hash in ("!", "*", "!!"):
return True # locked/no password — treat as default
for default_pw in FACTORY_DEFAULTS:
if crypt.crypt(default_pw, current_hash) == current_hash:
return True
return False
except (FileNotFoundError, PermissionError):
pass
return True # if /etc/shadow is unreadable, assume default for safety
@app.get("/api/security/password-is-default") @app.get("/api/security/password-is-default")
async def api_password_is_default(): async def api_password_is_default():
"""Check if the free account password is still the factory default.""" """Check if the free account password is still the factory default.
try:
with open("/var/lib/secrets/free-password", "r") as f: Uses /etc/shadow as the authoritative source so that password changes made
current = f.read().strip() via GNOME Settings, the passwd command, or any other method are detected
return {"is_default": current == "free"} correctly — not just changes made through the Hub or change-free-password.
except FileNotFoundError: """
return {"is_default": True} return {"is_default": _is_free_password_default()}
# ── System password change ──────────────────────────────────────── # ── System password change ────────────────────────────────────────

View File

@@ -98,7 +98,7 @@ in
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
}; };
path = [ pkgs.coreutils ]; path = [ pkgs.coreutils pkgs.python3 ];
script = '' script = ''
# If sealed AND onboarded fully clean, nothing to do # If sealed AND onboarded fully clean, nothing to do
[ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0 [ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0
@@ -119,9 +119,25 @@ EOF
# If the user completed Hub onboarding, they've addressed security # If the user completed Hub onboarding, they've addressed security
[ -f /var/lib/sovran/onboarding-complete ] && exit 0 [ -f /var/lib/sovran/onboarding-complete ] && exit 0
# If the free password has been changed from the factory default, no warning needed # If the free password has been changed from ALL known factory defaults, no warning needed
if [ -f /var/lib/secrets/free-password ]; then if [ -f /etc/shadow ]; then
[ "$(cat /var/lib/secrets/free-password)" != "free" ] && exit 0 FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then
STILL_DEFAULT=false
for DEFAULT_PW in "free" "gosovransystems"; do
EXPECTED=$(DEFAULT_PW="$DEFAULT_PW" FREE_HASH="$FREE_HASH" python3 -c \
"import crypt, os; print(crypt.crypt(os.environ['DEFAULT_PW'], os.environ['FREE_HASH']))")
if [ "$EXPECTED" = "$FREE_HASH" ]; then
STILL_DEFAULT=true
break
fi
done
if [ "$STILL_DEFAULT" = "false" ]; then
# Password was changed clear any legacy warning and exit
rm -f /var/lib/sovran/security-status /var/lib/sovran/security-warning
exit 0
fi
fi
fi fi
# No flags at all + secrets exist = legacy (pre-seal era) machine # No flags at all + secrets exist = legacy (pre-seal era) machine