Users and access
Users and access
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 chapter matters
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.
Ops motivation: access policy is production policy. A lab that practices key-only SSH and console recovery trains the same habits you need on real hosts.
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 |
description |
GECOS / human label |
System users (preview)
users.users.labbot = {
isSystemUser = true;
group = "labbot";
};
users.groups.labbot = {};Service bots do not belong in wheel.
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 / search.nixos.org 26.05.
Order of hardening
- Key login works for wheel user
- Disable password SSH
- Disable root SSH
- Consider
mutableUsers = false
Never reverse the order on a remote-only host without console.
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 | Services and security part |
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 -6Why the store leaks matter
NixOS evaluation materializes values into the store. Anything in the flake that is a secret string is a secret management failure—even if file mode looks fine in git.
Theory 4 — SSH service basics
services.openssh = {
enable = true;
ports = [ 22 ];
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "no";
};
};Firewall must allow the port (networking chapter)—if SSH dies after firewall work, check both.
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 (filesystems chapter).
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 (later part) manages dotfiles; this chapter is raw user access only.
mutableUsers
users.mutableUsers = false; # strong: only flake defines users/passwords
# users.mutableUsers = true; # imperative passwd may persist| Setting | Effect |
|---|---|
true |
Imperative drift possible |
false |
Users/passwords 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 8 — Sudo and elevation patterns
| Pattern | Lab use |
|---|---|
wheelNeedsPassword = true |
Safer habit |
| passwordless sudo | Faster drills; note risk |
sudo -u service users |
Later service chapters |
Prefer proving sudo -v works before disabling other access methods.
Theory 9 — Debugging SSH failures
From client:
ssh -vvv -i ~/.ssh/id_ed25519_nixlab alice@<vm-ip> 2>&1 | tail -40On host:
sudo journalctl -u sshd -n 50 --no-pager
ss -ltn | grep 22| Symptom | Checks |
|---|---|
| Connection refused | sshd down / wrong IP / firewall |
| Permission denied (publickey) | key material, user, identities |
| Auth succeeds, sudo fails | wheel membership |
Theory 10 — Audit checklist for flakes
cd ~/nixos-config
rg -n "initialPassword|password\s*=" || true
rg -n "BEGIN OPENSSH PRIVATE KEY" || true
rg -n "PRIVATE KEY" || trueRotate anything that ever landed in git history.
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 mkpasswdExample D — System user for a service (preview)
users.users.labbot = {
isSystemUser = true;
group = "labbot";
description = "Lab automation user";
};
users.groups.labbot = {};Example E — Key file in repo
users.users.alice.openssh.authorizedKeys.keyFiles = [
./keys/alice.pub
];Ensure ./keys/alice.pub is git-tracked (public only).
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.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.
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
Run the verbose client + journalctl flow from Theory 9. Write the actual authorized_keys path you observed.
Exercises
- Add a second non-admin user with a different key.
- Move public keys to
keyFilesand rebuild.
- Toggle
wheelNeedsPasswordand document UX impact.
- Attempt root SSH after
PermitRootLogin = "no"; confirm failure.
- Create a system user; prove it has no login shell access as intended.
- Document host key fingerprints for your SSH
known_hostsprocess.
- Practice recovery: break sudo group membership; fix on console.
- Write an access policy paragraph for this lab (who, how, exceptions).
- Scan git history for accidental secrets if you ever committed them.
- Align firewall chapter: ensure port 22 allowed after hardening.
- Compare password hash approaches; choose one documented method.
- Update
docs/rollback.mdwith SSH lockout recovery steps.
- Explain store visibility of
hashedPasswordto a peer.
- Prepare for Home Manager later: list dotfiles you will not put in the system flake.
Common pitfalls
| 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 |
| Private key committed | Emergency rotation |
| Wrong authorized_keys path assumptions | NixOS managed paths |
mutableUsers surprise |
Policy mismatch with passwd |
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
- Secret audit clean
Further depth (this book)
| Topic | Chapter / part |
|---|---|
| Firewall for SSH | Networking and firewall |
| Host keys as state | Filesystems and state |
| Secrets management | Services and security: sops/agenix |
| User dotfiles | Home Manager chapters |