Day 14 — Users & access
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 convenienceservices.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 -6Theory 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 mkpasswdLab 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.pubCopy the public key into the flake. Never commit the private key.
Lab 2 — Wire user + SSH into flake
Edit hosts/lab/configuration.nix:
- Admin user with
wheel
openssh.authorizedKeys.keys
- SSH enabled
- Prefer
PasswordAuthentication = falseonce key login works
PermitRootLogin = "no"when ready
cd ~/nixos-config
sudo nixos-rebuild switch --flake .#labFrom 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.
- Misconfigure authorized key (truncate key).
- Rebuild.
- Confirm SSH fails.
- 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" || trueRemove 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.
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)
- Note current
mutableUsers
- Set explicitly in flake
- Rebuild
- Document whether
passwdchanges 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 -40On 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 NixOSWrite 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.