Compare commits
1 Commits
df5bfbbd3d
...
ce103a7353
| Author | SHA1 | Date | |
|---|---|---|---|
| ce103a7353 |
@@ -24,12 +24,39 @@ The transition is safe on `neogaia`: if activation fails the rebuild fails and t
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] An admin age identity exists in the operator's password manager; its private half is committed nowhere, in no form
|
||||
- [ ] A host age identity exists on `neogaia`'s encrypted root and was generated on the machine
|
||||
- [ ] The sops configuration in the repo names the admin recipient and the `neogaia` recipient
|
||||
- [ ] A shared secrets file, encrypted to admin plus `neogaia`, holds the primary user's password hash
|
||||
- [ ] The secrets flake input is added, following the base nixpkgs
|
||||
- [ ] The shared base config carries the machinery unconditionally — identity file location and default secrets file — with no enable flag
|
||||
- [ ] The password secret is declared beside the user declaration, consumed through `hashedPasswordFile`, and marked as needed for user creation
|
||||
- [ ] `nix flake check` builds the `neogaia` toplevel; a mistyped secret name or missing secrets file fails it
|
||||
- [ ] Manual confirmation: `neogaia` activates, and console login succeeds against the decrypted password hash
|
||||
- [x] An admin age identity exists in the operator's password manager; its private half is committed nowhere, in no form
|
||||
- [x] A host age identity exists on `neogaia`'s encrypted root and was generated on the machine
|
||||
- [x] The sops configuration in the repo names the admin recipient and the `neogaia` recipient
|
||||
- [x] A shared secrets file, encrypted to admin plus `neogaia`, holds the primary user's password hash
|
||||
- [x] The secrets flake input is added, following the base nixpkgs
|
||||
- [x] The shared base config carries the machinery unconditionally — identity file location and default secrets file — with no enable flag
|
||||
- [x] The password secret is declared beside the user declaration, consumed through `hashedPasswordFile`, and marked as needed for user creation
|
||||
- [x] `nix flake check` builds the `neogaia` toplevel; a mistyped secret name or missing secrets file fails it
|
||||
- [x] Manual confirmation: `neogaia` activates, and console login succeeds against the decrypted password hash
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**`users.mutableUsers = false` was required and is not in the plan.**
|
||||
NixOS applies a declared password hash to an account that already exists in `/etc/shadow` only when `mutableUsers` is false — `update-users-groups.pl` guards both assignments on it.
|
||||
At the default of true, `alexion` already existed, so `hashedPasswordFile` would have been ignored and the hand-set password kept, silently.
|
||||
The final acceptance criterion would then have passed while proving nothing, because the login being tested would still have been the old one.
|
||||
|
||||
Two consequences follow, neither sanctioned by the spec.
|
||||
`passwd` no longer works, so rotating the password means re-running `mkpasswd`, re-encrypting the shared file, and rebuilding.
|
||||
Root has no declared password and is therefore locked (`!`), which blocks direct root login and the systemd emergency shell's `sulogin` prompt.
|
||||
`sudo` from the wheel group is unaffected, and generation rollback or `init=/bin/sh` remains available for recovery.
|
||||
Leaving root locked was chosen over declaring a root password, on the grounds that the recovery paths that survive a locked root do not depend on `/etc/shadow` at all.
|
||||
This is worth folding back into the parent spec before the servers exist, where a locked root and no SSH key would be a harder corner.
|
||||
|
||||
**The negative half of the build criterion was exercised, not assumed.**
|
||||
A mistyped secret name fails with `the key 'alexion-passwrd' cannot be found`.
|
||||
A missing secrets file fails with `Path 'secrets/absent.yaml' does not exist in Git repository`.
|
||||
Both were tested by temporary edits that were reverted.
|
||||
|
||||
**Identity handling.**
|
||||
The admin identity was generated by the operator in a terminal outside this session, so no copy of its private half ever reached the agent or the repo.
|
||||
The host identity was generated on `neogaia` into `/var/lib/sops-nix/key.txt` (mode 0400, root) on the `@root` subvolume of the LUKS-encrypted `cryptroot`, and never transmitted.
|
||||
|
||||
**Follow-up worth flagging for 0011.**
|
||||
`services.openssh.enable` is true on `neogaia` with no declared `authorizedKeys`, so SSH is not a fallback route in if a future decryption failure locks the console.
|
||||
The task that makes the SSH host keys secrets is the natural place to settle that.
|
||||
|
||||
17
.sops.yaml
Normal file
17
.sops.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
# Recipients for the encrypted files under secrets/.
|
||||
keys:
|
||||
# A recipient of every file.
|
||||
# One readable only by machines becomes unrecoverable once they are wiped.
|
||||
# Adding a recipient requires decrypting first.
|
||||
# No private half here, only in the operator's password manager.
|
||||
- &admin age1m0pk94ysjlw3lmf6pyuv5l5pepvdjss8w0vxjv90dq6ndp02tdgsdwdvue
|
||||
# Generated on the machine it names.
|
||||
- &neogaia age14a04vphzjq74epfrz9a09wjw8lzchtru84awzuq2n45d8f42ychqjs89qe
|
||||
|
||||
creation_rules:
|
||||
# Material common to every machine, so it is stored once rather than per host.
|
||||
- path_regex: secrets/shared\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *neogaia
|
||||
@@ -15,6 +15,13 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
A file-top header is one concise purpose line, added only where the filename or path does not already say it — never a feature inventory of the code below.
|
||||
For a placeholder, say so plainly plus any actionable present-tense directive ("Placeholder: regenerate with nixos-generate-config on the target machine"), never "placeholder for <missing feature>".
|
||||
Option `description`/`mkEnableOption` strings are user-facing documentation rather than comments, so they may describe behaviour more fully — but the self-contained rule and the bans on glossary terms and agent-state references still apply.
|
||||
- Start each sentence of a comment on its own line, as with Markdown prose.
|
||||
A sentence needing more than one line is first a prompt to ask whether it should be two sentences.
|
||||
Only when it genuinely cannot be split does it wrap, and then it wraps normally at the right margin.
|
||||
Never break a line early at a comma or clause boundary to make it read as a unit.
|
||||
Never use a semicolon, in a comment or in authored prose.
|
||||
Recast as two sentences instead.
|
||||
Only reformat comments you are actually writing or changing.
|
||||
- Comments posted to Gitea (pull requests, issues, reviews) go out under the operator's account, so sign every one to make clear the author is the agent, not the operator.
|
||||
End the comment with a `— Claude` sign-off.
|
||||
(A dedicated bot account may replace this later; until then, the sign-off is the only marker.)
|
||||
|
||||
23
flake.lock
generated
23
flake.lock
generated
@@ -232,7 +232,28 @@
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"nixvim": "nixvim"
|
||||
"nixvim": "nixvim",
|
||||
"sops-nix": "sops-nix"
|
||||
}
|
||||
},
|
||||
"sops-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1783174389,
|
||||
"narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
|
||||
@@ -34,6 +34,12 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Decrypts committed secrets at activation, from an age identity on the host.
|
||||
sops-nix = {
|
||||
url = "github:Mic92/sops-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# CachyOS kernel and binary cache. Pins its own nixpkgs so its cache stays
|
||||
# usable and the kernel is fetched from it.
|
||||
chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable";
|
||||
|
||||
@@ -52,6 +52,7 @@ let
|
||||
inputs.home-manager.nixosModules.home-manager
|
||||
inputs.chaotic.nixosModules.default
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
(self + "/system")
|
||||
(self + "/hosts/${hostName}")
|
||||
{ networking.hostName = hostName; }
|
||||
|
||||
25
secrets/shared.yaml
Normal file
25
secrets/shared.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
alexion-password: ENC[AES256_GCM,data:EGTsJvW4/zA+FfhUtZD4E6HIsI0JQ3LLL511XjI9hDl3NaHNe4g+wWy7P5DQnW29G5W4TaPZIetUcV7OO+1btuRxTuwbWVOulA==,iv:Ihf1kz5DFtvy1bUnfQGAU0/BQrwUvqKwqnK1JbKnWHU=,tag:K0N/6H/IWesNMxzXhQQO/w==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVYzZuaERsRjMyaTAwL3Ri
|
||||
b2RhaGZ1aHNOSzVMamVxWkdKb3VKTk9QMmk4CmV0R3hYN3hkMU1tVDJLNkFlT08y
|
||||
SUdUeUZ4d2JwNmdyOWVJcmZNcEtCb1EKLS0tIHhDV1NZWWdDZUNMYjVqYUVlc0ty
|
||||
Y1owUFZPMXBHbDhjVWxTUjZGRk1IUzQK7VENq6TjuOFlon+CJqUxbIJZ9qka78C/
|
||||
LDsgaTD+7zCBPgASwPbF88pH6tdK7bvNLJnznlZdZBL12eOy25BmOQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1m0pk94ysjlw3lmf6pyuv5l5pepvdjss8w0vxjv90dq6ndp02tdgsdwdvue
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHY0ZXT3lRWS9DMFA1MHhl
|
||||
MDZiWHhEMy9INGtpd1ZOdzh0OFRoUlZDa0hZCkJwTUV4c01YWlE1QjNDd3pRN3F0
|
||||
SGJWWmFTT1NMQktNejVHY1RrRlZJNFEKLS0tIHlRZG9ZV3FrQktSN2tURVV1NmlW
|
||||
UTBZbFlqMmFGZ0VPSlA1dmNMU2Q3TFUKtL2V8t9+Qw5vjXursvCVRatflX8JKXJr
|
||||
VuA8oe0nKpk7wh4fCzcT7RoRKpJY0gPFjIzeTZGVfoAmZIUWMhzRuw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14a04vphzjq74epfrz9a09wjw8lzchtru84awzuq2n45d8f42ychqjs89qe
|
||||
lastmodified: "2026-07-20T03:22:00Z"
|
||||
mac: ENC[AES256_GCM,data:ei7PKVAIjJ6fGkxqJFc5wdYapq1gElel3fTJ+yKhvWHU+39aKcllG66T3d9FitRztgyt69phykHdKvxDHRUwYeyl1YBzyf1ZpPU5mXJb+hkLtVB1Am7StcP+m7jFqKSmqtYhIT9OxUrH0MJ8qeoU9216otwkhhpPz2hr1s7KYFk=,iv:Pp03KmlinjJiiTZezr0LzzkcHb1a5XWgDpu38jhl9Rk=,tag:HAqFitge+KTCtDE24t8/ig==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
@@ -10,6 +10,8 @@ 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;
|
||||
@@ -66,11 +68,32 @@ in
|
||||
# bare TTY and not only under a graphical session.
|
||||
console.useXkbConfig = true;
|
||||
|
||||
# Primary user, in the wheel group. No password set here.
|
||||
# Decryption machinery every host depends on.
|
||||
# The identity sits on the encrypted root, which is mounted early enough to
|
||||
# satisfy the secret below.
|
||||
# Clearing both `sshKeyPaths` defaults keeps the SSH host keys out of the
|
||||
# decryption path.
|
||||
sops.defaultSopsFile = ../secrets/shared.yaml;
|
||||
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
|
||||
sops.age.sshKeyPaths = [ ];
|
||||
sops.gnupg.sshKeyPaths = [ ];
|
||||
|
||||
# A password set by hand on a running machine otherwise takes precedence.
|
||||
# That leaves the declared `hashedPasswordFile` below silently inert.
|
||||
# Root has no declared password and is therefore locked.
|
||||
# `sudo` from the wheel group is the way in.
|
||||
users.mutableUsers = false;
|
||||
|
||||
# Decrypted in an earlier activation stage than ordinary secrets.
|
||||
# That is early enough to precede the account that reads it.
|
||||
sops.secrets.${passwordSecret}.neededForUsers = true;
|
||||
|
||||
# Primary user, in the wheel group.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user