/secrets is the authoritative location for every secret, incl. the ssh host keys

Operator rule: secrets live in /secrets and consumers reach them from there.
Three subdirectories with the ownership each consumer needs — files/ (loops),
host/ (root: ssh host keys + sops age identity), nginx/ (root:nginx: the
opencode UI htpasswd) — under a 0711 /secrets so nginx can traverse to its own
without the directory being listable.

sshd's hostKeys and sops-nix's sshKeyPaths/keyFile are pointed at /secrets
DIRECTLY rather than through symlinks: the ed25519 host key is load-bearing
beyond ssh, since its age identity (age1tmvg…) is a recipient of cc-ci-secrets,
and a dangling symlink would let sshd write a NEW key and silently make every
cc-ci secret undecryptable. The /etc/ssh symlinks are added for discoverability
only, so nothing depends on activation ordering.

nginx's htpasswd path becomes an option (opencodeUiHtpasswdFile) defaulting
under /secrets, rather than a hard-coded /etc/nginx path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqkQq3CDmFWcQ7u1LzoyRz
This commit is contained in:
2026-09-08 17:23:28 +00:00
co-authored by Claude Opus 5
parent 360999d623
commit e7aa055784
2 changed files with 50 additions and 1 deletions
+13 -1
View File
@@ -62,6 +62,18 @@ in
description = "nginx server_name for the opencode web UI (TLS + basic auth).";
};
opencodeUiHtpasswdFile = lib.mkOption {
type = lib.types.str;
default = "/secrets/nginx/oc-htpasswd";
description = ''
htpasswd file for the opencode UI (`oc:<bcrypt>`), created out of band a store path
would be world-readable. Default is under /secrets, the authoritative location for this
host's secrets; it must be readable by the `nginx` user (root:nginx 0640 in a directory
nginx can traverse). **nginx refuses to start if it is missing**, and its config check
runs as the nginx user, so a root-only file fails the check even though the path exists.
'';
};
};
config = {
@@ -204,7 +216,7 @@ SSHCFG
recommendedProxySettings = true;
virtualHosts.${cfg.opencodeUiHost} = {
listen = [ { addr = "0.0.0.0"; port = cfg.opencodeUiBackendPort; } ];
basicAuthFile = "/etc/nginx/oc-htpasswd";
basicAuthFile = cfg.opencodeUiHtpasswdFile;
extraConfig = ''
# traefik sits on the docker networks (ingress 10.0.0.0/24, gwbridge 172.18.0.0/16)
set_real_ip_from 172.16.0.0/12;