Keystone SystemsKS Systems

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:

  1. Validates that the rpool pool and rpool/crypt encrypted root dataset are available.
  2. Creates the rpool/crypt/home parent dataset if it does not exist.
  3. Creates rpool/crypt/home/<username> with the configured compression, quota, recordsize, and atime settings.
  4. Delegates ZFS permissions to the user.
  5. 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: destroy is 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/alice

Mount 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/documents

In 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 = true in the user's Home Manager configuration.
  • keystone.terminal.git.userName from the user's fullName.
  • keystone.terminal.git.userEmail from the user's email (Git is only enabled when email is 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 = true in the user's Home Manager configuration.
  • keystone.desktop.hyprland.enable = true.
  • keystone.desktop.hyprland.modifierKey from the user's desktop configuration.
  • keystone.desktop.hyprland.capslockAsControl from 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#alice

This 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 initialPassword or hashedPassword defined.
  • 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