User management
The keystone.os.users option provides a unified interface for creating user accounts on Keystone systems. It handles NixOS user creation, ZFS-backed home directories with delegated permissions, SSH key authorization, and optional Home Manager integration for the terminal and desktop modules. Each configured user receives a fully provisioned account with consistent defaults and per-user module enablement.
Defining users
Users are defined as named attributes under keystone.os.users. Each entry creates a NixOS user account with the specified properties.
keystone.os.users = {
alice = {
fullName = "Alice Smith";
email = "alice@example.com";
extraGroups = [ "wheel" "networkmanager" ];
authorizedKeys = [ "ssh-ed25519 AAAAC3... alice@laptop" ];
hashedPassword = "$6$..."; # Generated with: mkpasswd -m sha-512
terminal.enable = true;
desktop.enable = true;
zfs.quota = "500G";
};
bob = {
fullName = "Bob Jones";
email = "bob@example.com";
extraGroups = [ "wheel" ];
initialPassword = "changeme";
terminal.enable = true;
};
};Every user must have either initialPassword or hashedPassword set. The initialPassword option stores the password in plaintext in the Nix store and should only be used for initial setup or testing. For production systems, generate a hashed password with mkpasswd -m sha-512 and use the hashedPassword option.
User options reference
The following options are available for each user under keystone.os.users.<name>:
| Option | Type | Default | Description |
| :----------------------------------- | :------------------- | :------------------- | :------------------------------------------------------ |
| uid | integer or null | null (auto-assign) | Explicit user ID |
| fullName | string | required | Full name (GECOS field and Git config) |
| email | string or null | null | Email address (used for Git config) |
| extraGroups | list of strings | [] | Additional group memberships |
| authorizedKeys | list of strings | [] | SSH public keys for authentication |
| hardwareKeys | list of strings | [] | Names of hardware keys from keystone.hardwareKey.keys |
| initialPassword | string or null | null | Plaintext initial password (insecure, for setup only) |
| hashedPassword | string or null | null | SHA-512 hashed password |
| terminal.enable | boolean | true | Enable the terminal development environment |
| desktop.enable | boolean | false | Enable the desktop environment |
| desktop.hyprland.modifierKey | "SUPER" or "ALT" | "SUPER" | Hyprland modifier key |
| desktop.hyprland.capslockAsControl | boolean | true | Remap Caps Lock to Control |
| zfs.quota | string or null | null | ZFS quota for the home dataset (e.g., "100G", "1T") |
| zfs.compression | enum | "lz4" | Compression algorithm for the home dataset |
| zfs.recordsize | string or null | null | ZFS block size (default 128K) |
| zfs.atime | "on" or "off" | "off" | Whether to update file access times on reads |
ZFS home directories
When the storage backend is ZFS, each user receives a dedicated ZFS dataset at rpool/crypt/home/<username>. This provides per-user isolation with independent snapshots, compression settings, and quota enforcement.
Dataset creation
A systemd service (zfs-user-datasets) runs at boot after the ZFS pool is imported. For each configured user it:
- Validates that the
rpoolpool andrpool/cryptencrypted root dataset are available. - Creates the
rpool/crypt/homeparent dataset if it does not exist. - Creates
rpool/crypt/home/<username>with the configured compression, quota, recordsize, and atime settings. - Delegates ZFS permissions to the user.
- Sets ownership and mode (
700) on the home directory.
Because the home directory is provided by the ZFS dataset, createHome is disabled in the NixOS user definition to avoid conflicts.
Delegated permissions
Each user is granted the following ZFS permissions on their home dataset:
- Snapshot operations:
snapshot,rollback,diff,hold,release,bookmark. - Send and receive:
send,receive(for backup and migration). - Property management:
compression,quota,refquota,recordsize,atime,readonly,userprop. - Dataset creation:
create,mount,mountpoint. - Descendant destroy:
destroyis allowed only on child datasets, not the home dataset itself.
Users are also added to the zfs group, which has read access to /dev/zfs via a udev rule.
Common ZFS operations
With delegated permissions, users can perform the following operations without root access:
# Create a snapshot
zfs snapshot rpool/crypt/home/alice@2026-02-28
# List snapshots
zfs list -t snapshot -r rpool/crypt/home/alice
# Roll back to a snapshot
zfs rollback rpool/crypt/home/alice@2026-02-28
# Send a snapshot for backup
zfs send rpool/crypt/home/alice@2026-02-28 | ssh backup zfs receive tank/backup/alice
# Set compression on the home dataset
zfs set compression=zstd rpool/crypt/home/aliceMount limitation
The Linux kernel restricts mounting filesystems to root. While users can create child datasets, mounting them requires sudo:
sudo zfs create rpool/crypt/home/alice/documentsIn practice, most users do not need to create child datasets manually. The primary use cases for delegated permissions are snapshots and backups.
ext4 fallback
When the storage backend is ext4 instead of ZFS, the users module creates standard home directories with appropriate ownership and permissions. ZFS-specific options (quota, compression, recordsize, atime) have no effect in ext4 mode.
SSH key authorization
SSH public keys can be specified in two ways:
Direct keys
Provide SSH public key strings directly in the authorizedKeys option:
keystone.os.users.alice = {
authorizedKeys = [
"ssh-ed25519 AAAAC3... alice@laptop"
"ssh-ed25519 AAAAC3... alice@desktop"
];
};Hardware keys
Reference named hardware keys defined in keystone.hardwareKey.keys. The SSH public key from each referenced hardware key is automatically added to the user's authorized keys:
keystone.hardwareKey.keys.yubi-black = {
sshPublicKey = "sk-ssh-ed25519@openssh.com AAAA... yubi-black";
};
keystone.os.users.alice = {
hardwareKeys = [ "yubi-black" ];
};This approach centralizes hardware key definitions so that the same physical key can be referenced by multiple users without duplicating the public key string.
When remote unlock is enabled (keystone.os.remoteUnlock.enable = true), user-configured SSH keys are also used for initrd SSH access if no explicit remoteUnlock.authorizedKeys are provided.
Home Manager integration
The users module automatically configures Home Manager for any user that has terminal.enable or desktop.enable set to true. This requires that home-manager.nixosModules.home-manager is imported in the system configuration.
System-wide mode
In system-wide mode, Home Manager runs as part of nixos-rebuild switch. The terminal and desktop modules are configured through the NixOS system configuration, and changes require root access.
keystone.os.users.alice = {
fullName = "Alice Smith";
email = "alice@example.com";
terminal.enable = true; # Configures home-manager with keystone.terminal
desktop.enable = true; # Configures home-manager with keystone.desktop
};When terminal.enable is true, the module sets:
keystone.terminal.enable = truein the user's Home Manager configuration.keystone.terminal.git.userNamefrom the user'sfullName.keystone.terminal.git.userEmailfrom the user'semail(Git is only enabled whenemailis not null).- The user's shell to Zsh (and enables Zsh system-wide).
When desktop.enable is true, the module additionally sets:
keystone.desktop.enable = truein the user's Home Manager configuration.keystone.desktop.hyprland.enable = true.keystone.desktop.hyprland.modifierKeyfrom the user's desktop configuration.keystone.desktop.hyprland.capslockAsControlfrom the user's desktop configuration.
Standalone mode
For environments where the Keystone NixOS modules are not available (macOS, non-NixOS Linux, GitHub Codespaces), the terminal module can be used independently via a standalone Home Manager configuration. In this mode, home-manager switch applies changes without requiring root access, and iteration is faster because it does not rebuild the full NixOS system.
# ~/.config/home-manager/flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
keystone.url = "github:ncrmro/keystone";
home-manager.url = "github:nix-community/home-manager/release-25.05";
};
outputs = { nixpkgs, keystone, home-manager, ... }: {
homeConfigurations."alice" = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
modules = [
keystone.homeModules.terminal
{
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "25.05";
keystone.terminal = {
enable = true;
git.userName = "Alice Smith";
git.userEmail = "alice@example.com";
};
}
];
};
};
}Apply with:
home-manager switch --flake ~/.config/home-manager#aliceThis approach replaces manual dotfile management (stow, symlink scripts, dotfile repositories) with a fully declarative configuration that produces identical results on any supported platform.
Group membership
Users configured through keystone.os.users are automatically added to the zfs group when the storage backend is ZFS. Additional groups can be specified via extraGroups. Common groups include:
| Group | Purpose |
| :--------------- | :-------------------------------------- |
| wheel | sudo access |
| networkmanager | Network configuration (desktop systems) |
| docker | Docker daemon access |
| libvirtd | Virtual machine management |
Validation
The users module enforces the following constraints at evaluation time:
- All user UIDs must be unique (when explicitly set).
- Every user must have either
initialPasswordorhashedPassworddefined. - Hardware key references must correspond to entries in
keystone.hardwareKey.keys. - ZFS must be enabled in the boot configuration when using ZFS storage.
See also
- Terminal development environment -- tools enabled by
terminal.enable - Desktop environment -- the Hyprland desktop enabled by
desktop.enable - Secrets management -- SSH keys and hardware key configuration
- Getting started -- user configuration during initial deployment