Day 14 — Users & access

Updated

July 30, 2026

Day 14 — Users & access

Stage II · ~4h
Goal: Declare users and remote access properly—SSH keys as the default remote path, non-root admin via wheel, and no long-lived plaintext password habits in the flake.

Why this day exists

Most early NixOS grief is lockouts and sloppy auth: root password only, keys never deployed, secrets pasted into world-readable store paths. Fix access patterns before opening the host to a network.


Theory 1 — Declarative users

users.users.alice = {
  isNormalUser = true;
  extraGroups = [ "wheel" "networkmanager" ];
  openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAA… comment"
  ];
};
Field Role
isNormalUser Normal account defaults (home, shell group, …)
isSystemUser Service accounts
extraGroups Group membership
uid / group Pin when needed
shell e.g. pkgs.zsh
hashedPassword / initialHashedPassword Password auth
openssh.authorizedKeys.keys SSH public keys

Theory 2 — Root vs wheel admin

Pattern Notes
Root login over SSH Discourage; often disable
User in wheel + sudo Preferred admin path
Passwordless sudo Convenient lab; tighten later
security.sudo.wheelNeedsPassword = true; # or false for lab convenience
services.openssh.settings = {
  PermitRootLogin = "no";
  PasswordAuthentication = false;
};

Exact option paths can vary slightly by release—prefer services.openssh.settings.* on modern NixOS; verify with docs/man configuration.nix / nixos-option.


Theory 3 — Passwords and the store

Approach Store risk
initialPassword = "secret" Password may land in store/world-readable eval — lab only, rotate
hashedPassword = "…" Hash still visible in store; better than plaintext but not a secret manager
SSH keys only Public keys are public; private keys never in flake
sops-nix / agenix later Stage IV

House rule now: SSH public keys in git are fine; private keys and live passwords are not.

Generate hash on the machine if you need password console login:

mkpasswd -m sha-512
# or openssl passwd -6

Theory 4 — SSH service basics

services.openssh = {
  enable = true;
  ports = [ 22 ];
  settings = {
    PasswordAuthentication = false;
    KbdInteractiveAuthentication = false;
    PermitRootLogin = "no";
  };
};

Firewall must allow the port (Day 15)—if SSH dies after firewall day, check both.


Theory 5 — Host keys vs user keys

Key type Purpose
SSH host keys Identify the machine to clients
User authorized keys Authenticate users to the machine
Your laptop private key Stays on client

Host keys live under /etc/ssh and are part of machine identity—backup/restore stories later.


Theory 6 — Groups you will actually use

Group Common reason
wheel sudo
networkmanager NM control (if NM)
docker / podman Containers later—grant carefully
video / input Desktop labs

Least privilege: do not dump every group on the admin user “just in case.”


Theory 7 — Home directories and mutability

Declarative users create homes per policy. Files inside homes are state. Rolling back system generations does not reset ~/.ssh edits. Home Manager (Stage III) later manages dotfiles; today raw user access only.


Worked examples bank

Example A — Admin user with key

users.users.alice = {
  isNormalUser = true;
  extraGroups = [ "wheel" ];
  openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyMaterial alice@laptop"
  ];
};

Example B — Disable password SSH

services.openssh.enable = true;
services.openssh.settings.PasswordAuthentication = false;

Example C — Console password without storing plaintext

users.users.alice.hashedPassword = "$6$…"; # from mkpasswd

Lab 1 — Generate a lab keypair on the client

On your workstation (not necessarily the VM):

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_nixlab -C "nixlab"
cat ~/.ssh/id_ed25519_nixlab.pub

Copy the public key into the flake. Never commit the private key.


Lab 2 — Wire user + SSH into flake

Edit hosts/lab/configuration.nix:

  1. Admin user with wheel
  2. openssh.authorizedKeys.keys
  3. SSH enabled
  4. Prefer PasswordAuthentication = false once key login works
  5. PermitRootLogin = "no" when ready
cd ~/nixos-config
sudo nixos-rebuild switch --flake .#lab

From client:

ssh -i ~/.ssh/id_ed25519_nixlab alice@<vm-ip>

Lab 3 — Verify identity paths

On host:

whoami
groups
sudo -v
ss -ltn | grep -E ':22|:…'

On client: confirm you did not need a password for SSH (if disabled).


Lab 4 — Lockout drill (console required)

Snapshot first.

  1. Misconfigure authorized key (truncate key).
  2. Rebuild.
  3. Confirm SSH fails.
  4. Fix via console, rebuild.

Write the recovery path on your rollback card.


Lab 5 — Audit flake for secret smells

cd ~/nixos-config
rg -n "initialPassword|password\s*=" || true
rg -n "BEGIN OPENSSH PRIVATE KEY" || true

Remove plaintext habits. Document any temporary lab exceptions.



Theory 8 — mutableUsers and the declarative contract

users.mutableUsers = false; # strong: only flake defines users/passwords
# users.mutableUsers = true;  # default-ish: passwd changes may persist outside rebuild
Setting Effect
mutableUsers = true Imperative passwd / useradd can drift
mutableUsers = false Users/passwords must come from config (hashed)

For a lab that teaches flakes as SoT, consider false once SSH keys work—know that password changes then require a rebuild.


Theory 9 — Authorized keys file layout

users.users.alice.openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAA… alice@laptop" ];
# or
# users.users.alice.openssh.authorizedKeys.keyFiles = [ ./keys/alice.pub ];

Prefer one key per line, full public key material, no wrapping. A truncated key fails silently as “Permission denied (publickey).”

Client config hygiene

Host nixlab
  HostName 192.0.2.10
  User alice
  IdentityFile ~/.ssh/id_ed25519_nixlab
  IdentitiesOnly yes

IdentitiesOnly yes avoids offering the wrong key and hitting MaxAuthTries.


Worked example D — System user for a service (preview)

users.users.labbot = {
  isSystemUser = true;
  group = "labbot";
  description = "Lab automation user";
};
users.groups.labbot = {};

Do not put service bots in wheel. Day 17 will run units as dedicated users.


Lab 6 — users.mutableUsers experiment (optional, snapshot)

  1. Note current mutableUsers
  2. Set explicitly in flake
  3. Rebuild
  4. Document whether passwd changes survive the next rebuild

Revert if the policy is too strict for console-only recovery yet.


Lab 7 — SSH hard fail checklist

From client after a failed login:

ssh -vvv -i ~/.ssh/id_ed25519_nixlab alice@<vm-ip> 2>&1 | tail -40

On host:

sudo journalctl -u sshd -n 50 --no-pager
ls -la /home/alice/.ssh 2>/dev/null || true
# authorized_keys may be managed under /etc/ssh/authorized_keys.d on NixOS

Write the actual authorized_keys path you observed—NixOS often does not use only ~/.ssh/authorized_keys.

Common gotchas

Symptom Theory
Key in flake but SSH password still works PasswordAuthentication still on
Permission denied (publickey) Wrong user, wrong key, typo in key line
Locked out Console + rollback
Password in git history Rotate; rewrite history if needed; prefer never
Root SSH only Add wheel user before disabling root

Checkpoint

  • Declarative non-root admin user
  • SSH key login works
  • Password SSH disabled (or explicitly justified)
  • Root SSH disabled or justified
  • No private keys in repo
  • Lockout recovery tested on console

Commit

cd ~/nixos-config
git add .
git commit -m "day14: declarative users ssh keys only"

Tomorrow

Day 15 — Networking & firewall. Interface management (networkd vs NetworkManager), addressing, and opening only what you need.