Skip to content

Instantly share code, notes, and snippets.

@sini
Last active May 12, 2026 20:27
Show Gist options
  • Select an option

  • Save sini/61fdbb6bcb873cedc3b5e032d36aaa25 to your computer and use it in GitHub Desktop.

Select an option

Save sini/61fdbb6bcb873cedc3b5e032d36aaa25 to your computer and use it in GitHub Desktop.
Fleet Demo: User Registry & Policy-Driven Access - Design Spec

Fleet Demo: User Registry & Policy-Driven Access

Date: 2026-05-12 Status: Draft

Summary

Replace the fleet demo's inline users.deploy = { } host definitions with a standalone user registry (den.users.registry) and fleet-level access mappings (fleet.user-access). Users are defined once with extended schema (email, groups, ssh-keys), then policies resolve them onto hosts based on environment or host-level group membership.

Motivation

The current fleet demo shows a single deploy user hardcoded on every host. This doesn't demonstrate:

  • Centralized user management with rich metadata
  • Policy-driven user assignment by environment or host
  • Group-based selection (admin vs deploy roles)
  • SSH key provisioning scoped to granted hosts only

Design

User Registry

A standalone top-level option den.users.registry — not namespaced under fleet. Each entry carries identity metadata:

den.users.registry = {
  alice = {
    email = "alice@example.com";
    groups = [ "admin" ];
    ssh-keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG... alice@workstation" ];
  };
  bob = {
    email = "bob@example.com";
    groups = [ "deploy" ];
    ssh-keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG... bob@laptop" ];
  };
};

The registry type is a lib.types.submodule that imports den.schema.user, mirroring how environmentType imports den.schema.environment in environments.nix. This ensures registry entries are proper user entities with userName, classes, aspect, etc. The extended options (email, groups, ssh-keys) are added via the schema extension below.

User Schema Extension

Users are promoted to real entities (den.schema.user.isEntity = true), same as environments. This means they appear in the scope tree as named entities and policies use resolve.to "user" rather than resolve.shared.

The user schema is extended via den.schema.user.imports, same pattern as the host schema extension in environments.nix:

den.schema.user.isEntity = true;

extendUserSchema = { ... }: {
  options.email = lib.mkOption {
    type = lib.types.str;
    default = "";
    description = "User email address";
  };
  options.groups = lib.mkOption {
    type = lib.types.listOf lib.types.str;
    default = [];
    description = "Group memberships for access policy selection";
  };
  options.ssh-keys = lib.mkOption {
    type = lib.types.listOf lib.types.str;
    default = [];
    description = "SSH public keys for authorized_keys";
  };
};

den.schema.user.imports = [ extendUserSchema ];

The policies filter the registry by group membership and resolve matching entries directly:

# matchedNames is the filtered list of usernames from the registry
map (name: resolve.to "user" { user = config.den.users.registry.${name}; }) matchedNames

Aspects like ssh-keys can then read user.ssh-keys from the user entity in scope.

Fleet Access Mappings

A fleet.user-access option with two sub-attrs:

fleet.user-access = {
  by-environment = {
    staging = { groups = [ "admin" "deploy" ]; };
    prod = { groups = [ "admin" ]; };
  };
  by-host = {
    # Available but unused in demo.
    # lb-prod = { groups = [ "admin" ]; };
  };
};

Type: lib.types.attrsOf (lib.types.submodule { options.groups = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; }; }) for both by-environment and by-host.

Access Policies

Two new policies in modules/policies/fleet.nix replace the old inline user definitions. These bypass the default host-to-users core policy — the demo explicitly shows policy-driven user resolution.

env-users — fires at host scope via den.schema.host.includes. Looks up config.fleet.user-access.by-environment.${host.environment} to get the granted groups, then filters config.den.users.registry for entries whose groups intersect. For each matching registry entry, emits resolve.to "user" { user = registryEntry; } — the registry entry is the user entity, carrying name, email, groups, and ssh-keys directly from the registry.

host-users — fires at host scope via den.schema.host.includes. Looks up config.fleet.user-access.by-host.${host.name}, same registry filter + resolve.

Both policies require { host, config, ... }: destructuring.

SSH Keys Aspect

A new ssh-keys aspect replaces the old deploy aspect:

den.aspects.ssh-keys = {
  nixos = { user, ... }: {
    users.users.${user.userName}.openssh.authorizedKeys.keys = user.ssh-keys;
  };
};

Included via den.default.includes so it fires for every user scope. Since users only exist on hosts where policies granted access, keys are provisioned only where appropriate.

Demo Narrative

User Groups Access via Hosts
alice admin by-environment.{prod,staging} all hosts (admin on both envs)
bob deploy by-environment.staging web-staging

Files

File Action
modules/users.nix New — registry option, registry type, user schema extension, access mapping option
modules/aspects/users/ssh-keys.nix New — SSH authorized keys aspect (replaces deploy.nix)
modules/policies/fleet.nix Edit — add env-users and host-users policies, append to den.schema.host.includes, exclude den.policies.host-to-users from host schema
modules/den.nix Edit — remove users.deploy = { } from all hosts, add ssh-keys aspect to den.default.includes
modules/aspects/users/deploy.nix Delete

Scope Tree (after)

flake
+-- fleet
    +-- environment:prod
    |   +-- host:lb-prod
    |   |   +-- user:alice  (via prod/admin)
    |   +-- host:web-prod-1
    |   |   +-- user:alice  (via prod/admin)
    |   +-- host:web-prod-2
    |       +-- user:alice  (via prod/admin)
    +-- environment:staging
        +-- host:web-staging
            +-- user:alice  (via staging/admin)
            +-- user:bob    (via staging/deploy)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment