Users and access

Updated

July 30, 2026

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 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 / search.nixos.org 26.05.

Order of hardening

  1. Key login works for wheel user
  2. Disable password SSH
  3. Disable root SSH
  4. 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 -6

Why 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 5 — Authorized keys on NixOS

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 as “Permission denied (publickey).”

NixOS often manages authorized keys under /etc/ssh/authorized_keys.d rather than only ~/.ssh/authorized_keys—verify on your host.

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.


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 -40

On 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" || true

Rotate 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 mkpasswd

Example 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.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.


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

Run the verbose client + journalctl flow from Theory 9. Write the actual authorized_keys path you observed.


Exercises

  1. Add a second non-admin user with a different key.
  2. Move public keys to keyFiles and rebuild.
  3. Toggle wheelNeedsPassword and document UX impact.
  4. Attempt root SSH after PermitRootLogin = "no"; confirm failure.
  5. Create a system user; prove it has no login shell access as intended.
  6. Document host key fingerprints for your SSH known_hosts process.
  7. Practice recovery: break sudo group membership; fix on console.
  8. Write an access policy paragraph for this lab (who, how, exceptions).
  9. Scan git history for accidental secrets if you ever committed them.
  10. Align firewall chapter: ensure port 22 allowed after hardening.
  11. Compare password hash approaches; choose one documented method.
  12. Update docs/rollback.md with SSH lockout recovery steps.
  13. Explain store visibility of hashedPassword to a peer.
  14. 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