feat: introduce guests as nested-container definitions
Add a third auto-loaded kind beside the host and the module: the guest, a reusable definition under guests/ that a host enables like a module and that realizes its interior as a systemd-nspawn nested container. Split the shared base config so a guest can stand on it. base.nix now holds the substrate both bases share — the primary user, home-manager, and the unstable/stable overlays. system.nix keeps the host-only machinery, and a new guest.nix is the slim guest-base: it imports the full modules tree, pins the interior release, and auto-enables the toolkit bundle and SSH so any guest is workable on sight. Give modules.ssh a guest flavor. A host restores its host keys from secrets as before, while a guest sets hostKeys.restore = false, names no sops files, and self-generates a host key, so it holds no age key of its own. The lib grows a guest helper that declares the guests.<path> namespace with an enable and a backend field. Only the container backend is built; microvm is a reserved value that trips a clear build-time assertion rather than silently building nothing. A sample guest exercises the whole path, and neogaia enables it, so the guest interior builds through the existing nix flake check seam.
This commit was merged in pull request #28.
This commit is contained in:
54
.claude/tasks/0002-guest-walking-skeleton.md
Normal file
54
.claude/tasks/0002-guest-walking-skeleton.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
spec: guests
|
||||
blocked-by: 0001-toolkit-bundle
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
The tracer bullet for the Guest concept: the thinnest complete path from discovery to a running nested container.
|
||||
The Auto-loader gains a third kind, discovering every Guest under `guests/` the way it already discovers Modules and Hosts.
|
||||
The shared base config splits into a host base (`system.nix`, carrying host-only machinery — bootloader, hardware profile, host identity, boot and garbage-collection timers) and a slim guest-base that every nested Guest stands on.
|
||||
Both keep the primary user, home-manager, and the shared overlays; the guest-base additionally auto-enables the `toolkit` bundle and `modules.ssh`, and imports the full `modules/` tree so any Module is available inside a Guest.
|
||||
A minimal sample Guest is Module-shaped: it declares its own `guests.<path>` namespace with an `enable` and a `backend` field (default `container`, `microvm` reserved but not built), and guards its body per the Enable convention.
|
||||
Its body's payload — the one difference from a Module — realizes a nested container running the Guest's interior on the guest-base.
|
||||
A Host enables the sample Guest exactly as it enables a Module, and the whole thing builds through the existing `nix flake check` seam.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] The Auto-loader discovers and wires every Guest under `guests/` as a third kind, with no manual `imports` edits, and a Guest's option path mirrors its `guests/` location per the Namespace convention (including the index-node and file-or-folder rules).
|
||||
- [x] The shared base config is split into a host base and a slim guest-base; the existing Host still builds via `nix flake check` with its host-only machinery intact.
|
||||
- [x] The guest-base includes the primary user, home-manager, and the shared overlays, auto-enables `toolkit` and `modules.ssh`, and imports the full `modules/` tree.
|
||||
- [x] A sample Guest declares `guests.<path>.enable` plus a `backend` field defaulting to `container`, guards its body on `enable`, and realizes a nested container running its interior when a Host enables it.
|
||||
- [x] The `microvm` backend value is accepted as reserved but unimplemented, failing clearly rather than silently building nothing.
|
||||
- [x] A Host enabling the sample Guest builds via `nix flake check`, the guest is reachable from its Host by `machinectl` with no per-Guest configuration, and the baseline toolset and SSH access are present inside it.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The base split is realized as three files, not two.
|
||||
`base.nix` is the shared substrate both bases build on — the primary user, home-manager, the `unstable`/`stable` overlays, and flakes.
|
||||
`system.nix` (the host base) imports it and adds the host-only machinery (bootloader limit, sops decryption and the password, the maintenance timers, the chaotic cache, console keymap).
|
||||
`guest.nix` (the guest-base) imports it and adds the slim guest layer.
|
||||
Factoring the common substrate out keeps "both include the primary user, home-manager, and the shared overlays" a single fact rather than a duplicated one.
|
||||
|
||||
- The guest-base imports `sops-nix` and `stylix` alongside the full `modules/` tree.
|
||||
This is load-bearing, not incidental: the module system pushes an `mkIf` down to the leaves it guards, so an option path a module names must be *declared* even where its `enable` is off.
|
||||
`modules/ssh.nix` names `sops.*` and the desktop modules name `stylix.*`, so those option namespaces have to exist for the tree to evaluate inside a guest that leaves them disabled.
|
||||
|
||||
- `modules/ssh.nix` gained a guest flavor.
|
||||
`hostKeys.restore` (default on) gates restoring host keys from secrets, and both `hostKeys.sopsFile` and `userKey.sopsFile` are now nullable.
|
||||
A guest sets `restore = false` and names no sops files, so its daemon self-generates a host key and it carries no age key — verified: the interior's `sops.secrets` is empty and `services.openssh.hostKeys` falls back to the generated defaults, while `neogaia` still restores its committed host keys with `openssh.hostKeys = [ ]`.
|
||||
|
||||
- The namespace mirroring is honored by author discipline, exactly as a Module's is: `my.guest { name = "sample"; }` names the option path, and `guests/sample.nix` places it.
|
||||
The Auto-loader change is the same recursive `collectNixFiles`, so the index-node and file-or-folder rules a folder-shaped guest would use come for free from the loader that already serves Modules — no folder-shaped guest exists yet to exercise them.
|
||||
|
||||
- The guest gets `privateNetwork = true` by default, so its interior sshd never contends with the host's on the shared namespace.
|
||||
It is `mkDefault`, so the networking foundation can later attach the guest to a VLAN bridge.
|
||||
|
||||
- `backend` is an `enum [ "container" "microvm" ]` defaulting to `container`.
|
||||
`microvm` is built by nothing; choosing it trips a build-time assertion with a clear message rather than silently producing no container, per the acceptance criterion and ADR 0006's reserved-value decision.
|
||||
|
||||
- `neogaia` enables `guests.sample` to demonstrate the path end to end, the way it demonstrated `modules.toolkit`.
|
||||
This means a rebuild starts a live nspawn container on the laptop; it is a minimal smoke test and can be turned off with one line.
|
||||
|
||||
- `nix flake check` passes and builds the nested `nixos-system-sample` interior in full.
|
||||
A pre-existing nixvim warning about its own `nixpkgs.follows` now also prints for the guest's reused Neovim config; it is upstream noise, not a defect in this change.
|
||||
82
base.nix
Normal file
82
base.nix
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
# The shared foundation both the host base and the guest-base build on: the
|
||||
# primary user, home-manager, and the fresher/pinned package overlays.
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
user = config.user;
|
||||
|
||||
# Args to instantiate an extra nixpkgs source on the base platform.
|
||||
pinArgs = prev: {
|
||||
inherit (prev.stdenv.hostPlatform) system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [ inputs.home-manager.nixosModules.home-manager ];
|
||||
|
||||
options.user = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "alexion";
|
||||
description = ''
|
||||
The primary interactive user this system is built for. Drives both the
|
||||
system account and the home-manager user in lockstep.
|
||||
'';
|
||||
};
|
||||
description = mkOption {
|
||||
type = types.str;
|
||||
default = "Alexion";
|
||||
description = "Human-readable description (GECOS field) for the primary user.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
# Reach fresher packages with `unstable.<name>` or pin with `stable.<name>`.
|
||||
nixpkgs.overlays = [
|
||||
(_final: prev: {
|
||||
unstable = import inputs.nixpkgs-unstable (pinArgs prev);
|
||||
stable = import inputs.nixpkgs-stable (pinArgs prev);
|
||||
})
|
||||
];
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
# Flakes, so `nixos-rebuild switch` works from the console and a direnv
|
||||
# `use flake` resolves inside a guest.
|
||||
nix.settings.experimental-features = [
|
||||
"nix-command"
|
||||
"flakes"
|
||||
];
|
||||
|
||||
# Primary user.
|
||||
# The wheel group is the way in, since root is locked.
|
||||
# No password is set here, since that is host-only.
|
||||
# A guest therefore has none and is reached by SSH key or `machinectl`.
|
||||
users.users.${user.name} = {
|
||||
isNormalUser = true;
|
||||
description = user.description;
|
||||
extraGroups = [ "wheel" ];
|
||||
};
|
||||
|
||||
# home-manager as a NixOS module: one build produces the system and user
|
||||
# environment together, sharing the system's pkgs and installing user
|
||||
# packages into the system profile.
|
||||
home-manager = {
|
||||
useGlobalPkgs = true;
|
||||
useUserPackages = true;
|
||||
extraSpecialArgs = {
|
||||
inherit inputs;
|
||||
my = inputs.self.lib;
|
||||
};
|
||||
users.${user.name} = {
|
||||
home.username = user.name;
|
||||
home.homeDirectory = "/home/${user.name}";
|
||||
home.stateVersion = "26.05";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
31
guest.nix
Normal file
31
guest.nix
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
my,
|
||||
inputs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
# The guest-base: the slim foundation every nested guest's interior stands on.
|
||||
# It imports the full modules tree so any module is available to enable inside a
|
||||
# guest, and stands on the same shared base a host does.
|
||||
{
|
||||
imports = my.collectNixFiles (inputs.self + "/modules") ++ [
|
||||
(inputs.self + "/base.nix")
|
||||
|
||||
# The modules tree reaches for these option namespaces, so they must be
|
||||
# declared for the tree to evaluate even where a guest leaves them off.
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
];
|
||||
|
||||
# A nested container has no per-host `default.nix` to pin its release.
|
||||
system.stateVersion = "26.05";
|
||||
|
||||
# The baseline toolset and SSH access, so any guest shelled into is a workable
|
||||
# environment without per-guest wiring.
|
||||
modules.toolkit.enable = lib.mkDefault true;
|
||||
modules.ssh.enable = lib.mkDefault true;
|
||||
|
||||
# A guest carries no host identity, so it presents a self-generated host key
|
||||
# rather than restoring one from secrets.
|
||||
modules.ssh.hostKeys.restore = lib.mkDefault false;
|
||||
}
|
||||
5
guests/sample.nix
Normal file
5
guests/sample.nix
Normal file
@@ -0,0 +1,5 @@
|
||||
args@{ my, ... }:
|
||||
# The tracer-bullet guest: the thinnest complete path from discovery to a
|
||||
# running nested container. Its interior is just the guest-base — the baseline
|
||||
# toolset and SSH access — so it proves the concept without carrying a service.
|
||||
my.guest { name = "sample"; } args
|
||||
@@ -44,6 +44,10 @@
|
||||
|
||||
modules.toolkit.enable = true;
|
||||
|
||||
# The walking-skeleton guest, enabled like any module: proves the guest path
|
||||
# end to end through this host's `nix flake check`.
|
||||
guests.sample.enable = true;
|
||||
|
||||
modules.agents.claude-code.enable = true;
|
||||
modules.agents.tools.gitea-axi.enable = true;
|
||||
modules.agents.pi.enable = true;
|
||||
|
||||
87
lib.nix
87
lib.nix
@@ -32,22 +32,27 @@ let
|
||||
) (builtins.readDir dir)
|
||||
);
|
||||
|
||||
# Build one host: every module is imported unconditionally (inert until its
|
||||
# `enable` flag is set), alongside home-manager, chaotic, the shared base, and
|
||||
# the host's own directory.
|
||||
# The special arguments every configuration is evaluated with, host and guest
|
||||
# interior alike.
|
||||
specialArgs = {
|
||||
inherit inputs;
|
||||
my = self.lib;
|
||||
};
|
||||
|
||||
# Build one host: every module and every guest is imported unconditionally
|
||||
# (inert until its `enable` flag is set), alongside chaotic, the host base,
|
||||
# and the host's own directory.
|
||||
mkHost =
|
||||
{
|
||||
hostName,
|
||||
system ? "x86_64-linux",
|
||||
}:
|
||||
inputs.nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = {
|
||||
inherit inputs;
|
||||
my = self.lib;
|
||||
};
|
||||
modules = (collectNixFiles (self + "/modules")) ++ [
|
||||
inputs.home-manager.nixosModules.home-manager
|
||||
inherit system specialArgs;
|
||||
modules =
|
||||
(collectNixFiles (self + "/modules"))
|
||||
++ (collectNixFiles (self + "/guests"))
|
||||
++ [
|
||||
inputs.chaotic.nixosModules.default
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
@@ -58,6 +63,67 @@ let
|
||||
];
|
||||
};
|
||||
|
||||
# Build a guest: a module-shaped definition whose body realizes its interior
|
||||
# as a nested container standing on the guest-base, keyed by its namespace path.
|
||||
# `name` is the dotted namespace under `guests.` and `interior` is an extra
|
||||
# module merged into the container alongside the guest-base.
|
||||
guest =
|
||||
{
|
||||
name,
|
||||
interior ? { },
|
||||
}:
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
optionPath = [ "guests" ] ++ lib.splitString "." name;
|
||||
cfg = lib.getAttrFromPath optionPath config;
|
||||
machineName = lib.replaceStrings [ "." ] [ "-" ] name;
|
||||
in
|
||||
{
|
||||
options = lib.setAttrByPath optionPath {
|
||||
enable = lib.mkEnableOption "the ${name} guest, run in its own nested container";
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"container"
|
||||
"microvm"
|
||||
];
|
||||
default = "container";
|
||||
description = ''
|
||||
How the guest is realized. `container` runs the guest as a
|
||||
systemd-nspawn nested container. `microvm` is reserved for a future
|
||||
hard-isolation backend and is not built yet.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.backend == "container";
|
||||
message = ''
|
||||
guests.${name}.backend = "${cfg.backend}" is not implemented. Only the "container" backend is built; "microvm" is reserved for future work.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
containers.${machineName} = lib.mkIf (cfg.backend == "container") {
|
||||
autoStart = lib.mkDefault true;
|
||||
|
||||
# The guest gets its own network namespace, so its services — its own
|
||||
# sshd included — never contend with the host's.
|
||||
privateNetwork = lib.mkDefault true;
|
||||
|
||||
inherit specialArgs;
|
||||
|
||||
config = {
|
||||
imports = [
|
||||
(self + "/guest.nix")
|
||||
interior
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Discover every host (a subdirectory of `hostsDir`) and build each one.
|
||||
mkHosts =
|
||||
hostsDir:
|
||||
@@ -71,5 +137,6 @@ in
|
||||
collectNixFiles
|
||||
mkHost
|
||||
mkHosts
|
||||
guest
|
||||
;
|
||||
}
|
||||
|
||||
@@ -53,11 +53,25 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
hostKeys.restore = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Restore the host keys from secrets rather than letting the daemon
|
||||
generate its own.
|
||||
|
||||
A machine with its own identity keeps its fingerprint across a reimage
|
||||
by restoring committed keys. A guest carries no host identity, so it
|
||||
turns this off and presents a self-generated key instead.
|
||||
'';
|
||||
};
|
||||
|
||||
hostKeys.sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Encrypted file holding this host's SSH host private keys, one entry per
|
||||
key type, named `ssh-host-<type>-key`.
|
||||
key type, named `ssh-host-<type>-key`. Required when `restore` is on.
|
||||
|
||||
These are the keys the daemon presents to identify itself to connecting
|
||||
clients, not keys used to authenticate anyone to a remote server.
|
||||
@@ -81,10 +95,12 @@ in
|
||||
};
|
||||
|
||||
userKey.sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Encrypted file holding this machine's SSH client private key, under the
|
||||
entry `ssh-user-ed25519-key`.
|
||||
entry `ssh-user-ed25519-key`. Left unset on a machine that authenticates
|
||||
to no remote server, such as a guest.
|
||||
|
||||
This is the key the primary user offers to authenticate to a remote
|
||||
server, not a key the daemon presents to identify this machine.
|
||||
@@ -94,36 +110,50 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
config = lib.mkIf cfg.enable (
|
||||
lib.mkMerge [
|
||||
{
|
||||
services.openssh.enable = true;
|
||||
|
||||
sops.secrets =
|
||||
# The primary user is the only account reachable over SSH.
|
||||
users.users.${user}.openssh.authorizedKeys.keys = cfg.authorizedKeys;
|
||||
}
|
||||
|
||||
# A machine with its own identity restores its host keys from secrets.
|
||||
(lib.mkIf cfg.hostKeys.restore {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.hostKeys.sopsFile != null;
|
||||
message = "modules.ssh.hostKeys.restore requires modules.ssh.hostKeys.sopsFile to name the encrypted host keys.";
|
||||
}
|
||||
];
|
||||
|
||||
# The daemon reads its host keys once at startup, so a re-key has to
|
||||
# restart it to take effect.
|
||||
lib.genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
||||
sops.secrets = lib.genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
||||
inherit (cfg.hostKeys) sopsFile;
|
||||
mode = "0400";
|
||||
restartUnits = [ "sshd.service" ];
|
||||
})
|
||||
// {
|
||||
# The primary user is the only account that authenticates with this key,
|
||||
# and the mode admits no other.
|
||||
# The client rereads it per connection, so no unit restarts on a re-key.
|
||||
${userKeySecret} = {
|
||||
inherit (cfg.userKey) sopsFile;
|
||||
mode = "0400";
|
||||
owner = user;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
# An empty list is what stops the daemon generating keys of its own.
|
||||
services.openssh.hostKeys = [ ];
|
||||
services.openssh.extraConfig = lib.concatMapStrings (
|
||||
type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n"
|
||||
) cfg.hostKeys.types;
|
||||
})
|
||||
|
||||
# The primary user is the only account reachable over SSH.
|
||||
users.users.${user}.openssh.authorizedKeys.keys = cfg.authorizedKeys;
|
||||
# The client key the primary user offers to remote servers, present only on
|
||||
# a machine that has one.
|
||||
(lib.mkIf (cfg.userKey.sopsFile != null) {
|
||||
# The primary user is the only account that authenticates with this key,
|
||||
# and the mode admits no other.
|
||||
# The client rereads it per connection, so no unit restarts on a re-key.
|
||||
sops.secrets.${userKeySecret} = {
|
||||
inherit (cfg.userKey) sopsFile;
|
||||
mode = "0400";
|
||||
owner = user;
|
||||
};
|
||||
|
||||
# The client reads the decrypted key where it is written, so no copy of it
|
||||
# lives in the user's home to drift from the secret.
|
||||
@@ -134,5 +164,7 @@ in
|
||||
enableDefaultConfig = false;
|
||||
settings."*".IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
||||
};
|
||||
};
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
71
system.nix
71
system.nix
@@ -2,55 +2,17 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
# Shared base config carried by every host.
|
||||
# The host base: the host-only machinery a physical machine needs on top of the
|
||||
# shared base — bootloader, secret decryption, and the maintenance timers.
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
user = config.user;
|
||||
|
||||
passwordSecret = "${user.name}-password";
|
||||
|
||||
# Args to instantiate an extra nixpkgs source on the base platform.
|
||||
pinArgs = prev: {
|
||||
inherit (prev.stdenv.hostPlatform) system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.user = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "alexion";
|
||||
description = ''
|
||||
The primary interactive user this host is built for. Drives both the
|
||||
system account and the home-manager user in lockstep.
|
||||
'';
|
||||
};
|
||||
description = mkOption {
|
||||
type = types.str;
|
||||
default = "Alexion";
|
||||
description = "Human-readable description (GECOS field) for the primary user.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
# Reach fresher packages with `unstable.<name>` or pin with `stable.<name>`.
|
||||
# chaotic's overlay is added by its own module, not here.
|
||||
nixpkgs.overlays = [
|
||||
(_final: prev: {
|
||||
unstable = import inputs.nixpkgs-unstable (pinArgs prev);
|
||||
stable = import inputs.nixpkgs-stable (pinArgs prev);
|
||||
})
|
||||
];
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
# Flakes, so `nixos-rebuild switch` works from the console.
|
||||
nix.settings.experimental-features = [
|
||||
"nix-command"
|
||||
"flakes"
|
||||
];
|
||||
imports = [ ./base.nix ];
|
||||
|
||||
# chaotic's binary cache, so the CachyOS kernel is fetched rather than compiled.
|
||||
# The `extra-` prefix keeps cache.nixos.org alongside it.
|
||||
@@ -109,30 +71,5 @@ in
|
||||
# That is early enough to precede the account that reads it.
|
||||
sops.secrets.${passwordSecret}.neededForUsers = true;
|
||||
|
||||
# Primary user.
|
||||
# The wheel group is the way in, since root is locked.
|
||||
users.users.${user.name} = {
|
||||
isNormalUser = true;
|
||||
description = user.description;
|
||||
extraGroups = [ "wheel" ];
|
||||
hashedPasswordFile = config.sops.secrets.${passwordSecret}.path;
|
||||
};
|
||||
|
||||
# home-manager as a NixOS module: one `nixos-rebuild switch` builds the
|
||||
# system and user environment together, sharing the system's pkgs and
|
||||
# installing user packages into the system profile.
|
||||
home-manager = {
|
||||
useGlobalPkgs = true;
|
||||
useUserPackages = true;
|
||||
extraSpecialArgs = {
|
||||
inherit inputs;
|
||||
my = inputs.self.lib;
|
||||
};
|
||||
users.${user.name} = {
|
||||
home.username = user.name;
|
||||
home.homeDirectory = "/home/${user.name}";
|
||||
home.stateVersion = "26.05";
|
||||
};
|
||||
};
|
||||
};
|
||||
users.users.${user.name}.hashedPasswordFile = config.sops.secrets.${passwordSecret}.path;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user