secrets-bao: inline configchanges, remove file, make configChanges attrs

This commit is contained in:
RingOfStorms (Joshua Bell) 2026-01-05 22:43:44 -06:00
parent c1f5677520
commit bd8cff90ed
4 changed files with 269 additions and 257 deletions

View file

@ -5,13 +5,32 @@
outputs = { ... }: outputs = { ... }:
{ {
lib = {
applyConfigChanges = secrets:
let
substitute = secretPath: value:
if builtins.isAttrs value then
builtins.mapAttrs (_: v: substitute secretPath v) value
else if builtins.isList value then
map (v: substitute secretPath v) value
else if builtins.isString value then
builtins.replaceStrings [ "$SECRET_PATH" ] [ secretPath ] value
else
value;
fragments = builtins.attrValues (builtins.mapAttrs (
name: s:
let
secretPath = s.path or ("/run/secrets/" + name);
in
substitute secretPath (s.configChanges or { })
) secrets);
in
builtins.foldl' (acc: v: acc // v) { } fragments;
};
nixosModules = { nixosModules = {
default = { default = import ./nixos-module.nix;
imports = [
(import ./nixos-module.nix)
(import ./nixos-configchanges.nix)
];
};
}; };
}; };
} }

View file

@ -1,10 +0,0 @@
{ config, lib, ... }:
let
cfg = config.ringofstorms.secretsBao;
secrets = cfg.secrets or { };
in
{
config = lib.mkIf cfg.enable (
lib.mkMerge (lib.mapAttrsToList (_: s: s.configChanges { path = s.path; }) secrets)
);
}

View file

@ -371,9 +371,9 @@ in
}; };
configChanges = lib.mkOption { configChanges = lib.mkOption {
type = lib.types.functionTo lib.types.attrs; type = lib.types.attrs;
default = { path, ... }: { }; default = { };
description = "Function that returns extra config given { path = secret.path; }."; description = "Extra config applied when enabled; supports '$SECRET_PATH' string substitution.";
}; };
template = lib.mkOption { template = lib.mkOption {
@ -389,210 +389,214 @@ in
}; };
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [ config = lib.mkIf cfg.enable (
{ lib.mkMerge [
assertions = lib.mapAttrsToList (name: s: {
assertion = (s.template != null) || (s.kvPath != null);
message = "ringofstorms.secretsBao.secrets.${name} must set either template or kvPath";
}) cfg.secrets;
environment.systemPackages = [
pkgs.jq
pkgs.curl
pkgs.openssl
pkgs.openbao
zitadelMintJwt
];
systemd.tmpfiles.rules = [
"d /run/openbao 0700 root root - -"
"f /run/openbao/zitadel.jwt 0400 root root - -"
"d /run/secrets 0711 root root - -"
];
systemd.services = lib.mkMerge [
{ {
zitadel-mint-jwt = { assertions = lib.mapAttrsToList (name: s: {
description = "Mint Zitadel access token (JWT) for OpenBao"; assertion = (s.template != null) || (s.kvPath != null);
wantedBy = [ "multi-user.target" ]; message = "ringofstorms.secretsBao.secrets.${name} must set either template or kvPath";
after = [ }) cfg.secrets;
"network-online.target"
"nss-lookup.target"
"NetworkManager-wait-online.service"
"systemd-resolved.service"
"time-sync.target"
];
wants = [
"network-online.target"
"NetworkManager-wait-online.service"
"systemd-resolved.service"
];
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
Restart = "on-failure";
RestartSec = "30s";
TimeoutStartSec = "2min";
UMask = "0077";
ExecStart = pkgs.writeShellScript "zitadel-mint-jwt-service" '' environment.systemPackages = [
#!/usr/bin/env bash pkgs.jq
set -euo pipefail pkgs.curl
pkgs.openssl
pkgs.openbao
zitadelMintJwt
];
if [ ! -d "/run/openbao" ]; then systemd.tmpfiles.rules = [
${pkgs.coreutils}/bin/mkdir -p /run/openbao "d /run/openbao 0700 root root - -"
${pkgs.coreutils}/bin/chmod 0700 /run/openbao "f /run/openbao/zitadel.jwt 0400 root root - -"
fi "d /run/secrets 0711 root root - -"
];
if [ ! -f "${cfg.zitadelKeyPath}" ]; then systemd.services = lib.mkMerge [
echo "Missing Zitadel key JSON at ${cfg.zitadelKeyPath}" >&2 {
exit 1 zitadel-mint-jwt = {
fi description = "Mint Zitadel access token (JWT) for OpenBao";
wantedBy = [ "multi-user.target" ];
after = [
"network-online.target"
"nss-lookup.target"
"NetworkManager-wait-online.service"
"systemd-resolved.service"
"time-sync.target"
];
wants = [
"network-online.target"
"NetworkManager-wait-online.service"
"systemd-resolved.service"
];
echo "zitadel-mint-jwt: starting (host=${zitadelHost})" >&2 serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
Restart = "on-failure";
RestartSec = "30s";
TimeoutStartSec = "2min";
UMask = "0077";
# Best-effort: wait briefly for time sync + DNS. ExecStart = pkgs.writeShellScript "zitadel-mint-jwt-service" ''
for i in {1..10}; do #!/usr/bin/env bash
if ${pkgs.systemd}/bin/timedatectl show -p NTPSynchronized --value 2>/dev/null | ${pkgs.gnugrep}/bin/grep -qi true; then set -euo pipefail
break
fi
sleep 1
done
for i in {1..10}; do if [ ! -d "/run/openbao" ]; then
if ${pkgs.systemd}/bin/resolvectl query ${zitadelHost} >/dev/null 2>&1; then ${pkgs.coreutils}/bin/mkdir -p /run/openbao
break ${pkgs.coreutils}/bin/chmod 0700 /run/openbao
fi fi
sleep 1
done
jwt_is_valid() { if [ ! -f "${cfg.zitadelKeyPath}" ]; then
local token="$1" echo "Missing Zitadel key JSON at ${cfg.zitadelKeyPath}" >&2
local payload_b64 payload_json exp now exit 1
fi
payload_b64="$(${pkgs.coreutils}/bin/printf '%s' "$token" | ${pkgs.coreutils}/bin/cut -d. -f2)" echo "zitadel-mint-jwt: starting (host=${zitadelHost})" >&2
payload_b64="$(${pkgs.coreutils}/bin/printf '%s' "$payload_b64" | ${pkgs.gnused}/bin/sed -e 's/-/+/g' -e 's/_/\//g')"
case $((${pkgs.coreutils}/bin/printf '%s' "$payload_b64" | ${pkgs.coreutils}/bin/wc -c)) in # Best-effort: wait briefly for time sync + DNS.
*1) payload_b64="$payload_b64=" ;; for i in {1..10}; do
*2) payload_b64="$payload_b64==" ;; if ${pkgs.systemd}/bin/timedatectl show -p NTPSynchronized --value 2>/dev/null | ${pkgs.gnugrep}/bin/grep -qi true; then
*3) : ;; break
*0) : ;; fi
esac sleep 1
done
payload_json="$(${pkgs.coreutils}/bin/printf '%s' "$payload_b64" | ${pkgs.coreutils}/bin/base64 -d 2>/dev/null || true)" for i in {1..10}; do
exp="$(${pkgs.jq}/bin/jq -r '.exp // empty' <<<"$payload_json" 2>/dev/null || true)" if ${pkgs.systemd}/bin/resolvectl query ${zitadelHost} >/dev/null 2>&1; then
if [ -z "$exp" ]; then break
return 1 fi
fi sleep 1
done
now="$(${pkgs.coreutils}/bin/date +%s)" jwt_is_valid() {
if [ "$exp" -gt $(( now + 60 )) ]; then local token="$1"
return 0 local payload_b64 payload_json exp now
fi
return 1
}
if [ -s "${cfg.zitadelJwtPath}" ] && jwt_is_valid "$(cat "${cfg.zitadelJwtPath}")"; then payload_b64="$(${pkgs.coreutils}/bin/printf '%s' "$token" | ${pkgs.coreutils}/bin/cut -d. -f2)"
echo "zitadel-mint-jwt: existing token still valid; skipping" >&2 payload_b64="$(${pkgs.coreutils}/bin/printf '%s' "$payload_b64" | ${pkgs.gnused}/bin/sed -e 's/-/+/g' -e 's/_/\//g')"
exit 0
fi
jwt="$(${zitadelMintJwt}/bin/zitadel-mint-jwt)" case $((${pkgs.coreutils}/bin/printf '%s' "$payload_b64" | ${pkgs.coreutils}/bin/wc -c)) in
*1) payload_b64="$payload_b64=" ;;
*2) payload_b64="$payload_b64==" ;;
*3) : ;;
*0) : ;;
esac
if [ -z "$jwt" ] || [ "$jwt" = "null" ]; then payload_json="$(${pkgs.coreutils}/bin/printf '%s' "$payload_b64" | ${pkgs.coreutils}/bin/base64 -d 2>/dev/null || true)"
echo "Failed to mint Zitadel access token" >&2 exp="$(${pkgs.jq}/bin/jq -r '.exp // empty' <<<"$payload_json" 2>/dev/null || true)"
exit 1 if [ -z "$exp" ]; then
fi return 1
fi
tmp="$(${pkgs.coreutils}/bin/mktemp)" now="$(${pkgs.coreutils}/bin/date +%s)"
trap '${pkgs.coreutils}/bin/rm -f "$tmp"' EXIT if [ "$exp" -gt $(( now + 60 )) ]; then
${pkgs.coreutils}/bin/printf '%s' "$jwt" > "$tmp" return 0
fi
return 1
}
# In-place update so the agent's file watcher sees changes. if [ -s "${cfg.zitadelJwtPath}" ] && jwt_is_valid "$(cat "${cfg.zitadelJwtPath}")"; then
${pkgs.coreutils}/bin/cat "$tmp" > "${cfg.zitadelJwtPath}" echo "zitadel-mint-jwt: existing token still valid; skipping" >&2
${pkgs.coreutils}/bin/chmod 0400 "${cfg.zitadelJwtPath}" || true exit 0
''; fi
};
};
vault-agent = { jwt="$(${zitadelMintJwt}/bin/zitadel-mint-jwt)"
description = "OpenBao agent for rendering secrets";
wantedBy = [ "multi-user.target" ];
after = [
"network-online.target"
"zitadel-mint-jwt.service"
];
wants = [
"network-online.target"
"zitadel-mint-jwt.service"
];
serviceConfig = { if [ -z "$jwt" ] || [ "$jwt" = "null" ]; then
Type = "simple"; echo "Failed to mint Zitadel access token" >&2
User = "root"; exit 1
Group = "root"; fi
Restart = "on-failure";
RestartSec = "10s"; tmp="$(${pkgs.coreutils}/bin/mktemp)"
TimeoutStartSec = "30s"; trap '${pkgs.coreutils}/bin/rm -f "$tmp"' EXIT
UMask = "0077"; ${pkgs.coreutils}/bin/printf '%s' "$jwt" > "$tmp"
ExecStart = "${pkgs.openbao}/bin/bao agent -log-level=debug -config=${mkAgentConfig}";
}; # In-place update so the agent's file watcher sees changes.
}; ${pkgs.coreutils}/bin/cat "$tmp" > "${cfg.zitadelJwtPath}"
${pkgs.coreutils}/bin/chmod 0400 "${cfg.zitadelJwtPath}" || true
'';
};
};
vault-agent = {
description = "OpenBao agent for rendering secrets";
wantedBy = [ "multi-user.target" ];
after = [
"network-online.target"
"zitadel-mint-jwt.service"
];
wants = [
"network-online.target"
"zitadel-mint-jwt.service"
];
serviceConfig = {
Type = "simple";
User = "root";
Group = "root";
Restart = "on-failure";
RestartSec = "10s";
TimeoutStartSec = "30s";
UMask = "0077";
ExecStart = "${pkgs.openbao}/bin/bao agent -log-level=debug -config=${mkAgentConfig}";
};
};
}
(lib.mapAttrs' (
name: secret:
lib.nameValuePair "openbao-secret-${name}" {
description = "Wait for OpenBao secret ${name}";
after = [ "vault-agent.service" ];
requires = [ "vault-agent.service" ];
wantedBy = map (svc: "${svc}.service") secret.dependencies;
startLimitIntervalSec = 300;
startLimitBurst = 3;
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
UMask = "0077";
ExecStart = pkgs.writeShellScript "openbao-wait-secret-${name}" ''
#!/usr/bin/env bash
set -euo pipefail
p=${lib.escapeShellArg secret.path}
for i in {1..60}; do
if [ -s "$p" ]; then
break
fi
sleep 1
done
if [ ! -s "$p" ]; then
echo "Secret file not rendered: $p" >&2
exit 1
fi
${lib.concatStringsSep "\n" (map (svc: ''
echo "Restarting ${svc} due to secret ${name}" >&2
systemctl try-restart ${lib.escapeShellArg (svc + ".service")} || true
'') secret.dependencies)}
'';
};
}
) cfg.secrets)
];
age.secrets = lib.mapAttrs' (
name: secret:
lib.nameValuePair name {
file = null;
path = secret.path;
}
) cfg.secrets;
} }
(lib.mapAttrs' ( ]
name: secret: );
lib.nameValuePair "openbao-secret-${name}" {
description = "Wait for OpenBao secret ${name}";
after = [ "vault-agent.service" ];
requires = [ "vault-agent.service" ];
wantedBy = map (svc: "${svc}.service") secret.dependencies;
startLimitIntervalSec = 300;
startLimitBurst = 3;
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
UMask = "0077";
ExecStart = pkgs.writeShellScript "openbao-wait-secret-${name}" ''
#!/usr/bin/env bash
set -euo pipefail
p=${lib.escapeShellArg secret.path}
for i in {1..60}; do
if [ -s "$p" ]; then
break
fi
sleep 1
done
if [ ! -s "$p" ]; then
echo "Secret file not rendered: $p" >&2
exit 1
fi
${lib.concatStringsSep "\n" (map (svc: ''
echo "Restarting ${svc} due to secret ${name}" >&2
systemctl try-restart ${lib.escapeShellArg (svc + ".service")} || true
'') secret.dependencies)}
'';
};
}
) cfg.secrets)
];
age.secrets = lib.mapAttrs' (
name: secret:
lib.nameValuePair name {
file = null;
path = secret.path;
}
) cfg.secrets;
}
]);
} }

View file

@ -43,8 +43,9 @@
{ {
nixosConfigurations = { nixosConfigurations = {
"${configuration_name}" = ( "${configuration_name}" = (
lib.nixosSystem { lib.nixosSystem {
modules = [ specialArgs = { inherit inputs; };
modules = [
inputs.nixos-hardware.nixosModules.framework-12-13th-gen-intel inputs.nixos-hardware.nixosModules.framework-12-13th-gen-intel
inputs.impermanence.nixosModules.impermanence inputs.impermanence.nixosModules.impermanence
({ ({
@ -68,7 +69,7 @@
}) })
inputs.common.nixosModules.jetbrains_font inputs.common.nixosModules.jetbrains_font
inputs.secrets-bao.nixosModules.default inputs.secrets-bao.nixosModules.default
inputs.ros_neovim.nixosModules.default inputs.ros_neovim.nixosModules.default
({ ({
ringofstorms-nvim.includeAllRuntimeDependencies = true; ringofstorms-nvim.includeAllRuntimeDependencies = true;
@ -89,57 +90,55 @@
inputs.common.nixosModules.tailnet inputs.common.nixosModules.tailnet
inputs.common.nixosModules.remote_lio_builds inputs.common.nixosModules.remote_lio_builds
( (
{ config, ... }: { inputs, lib, ... }:
{ let
secrets = {
ringofstorms.secretsBao = { headscale_auth = {
enable = true; kvPath = "kv/data/machines/home_roaming/headscale_auth";
zitadelKeyPath = "/machine-key.json"; dependencies = [ "tailscaled" ];
openBaoAddr = "https://sec.joshuabell.xyz"; configChanges = {
jwtAuthMountPath = "auth/zitadel-jwt"; services.tailscale.authKeyFile = "$SECRET_PATH";
openBaoRole = "machines"; };
zitadelIssuer = "https://sso.joshuabell.xyz"; };
zitadelProjectId = "344379162166820867"; nix2github = {
debugMint = true; owner = "josh";
secrets = { group = "users";
headscale_auth = { kvPath = "kv/data/machines/home_roaming/nix2github";
kvPath = "kv/data/machines/home_roaming/headscale_auth"; };
dependencies = [ "tailscaled" ]; nix2bitbucket = {
configChanges = { path, ... }: { owner = "josh";
services.tailscale.authKeyFile = path; group = "users";
}; kvPath = "kv/data/machines/home_roaming/nix2bitbucket";
}; };
nix2gitforgejo = {
nix2github = { owner = "josh";
owner = "josh"; group = "users";
group = "users"; kvPath = "kv/data/machines/home_roaming/nix2gitforgejo";
kvPath = "kv/data/machines/home_roaming/nix2github"; };
}; nix2lio = {
nix2bitbucket = { owner = "josh";
owner = "josh"; group = "users";
group = "users"; kvPath = "kv/data/machines/home_roaming/nix2lio";
kvPath = "kv/data/machines/home_roaming/nix2bitbucket"; };
}; };
nix2gitforgejo = { in
owner = "josh"; lib.mkMerge [
group = "users"; {
kvPath = "kv/data/machines/home_roaming/nix2gitforgejo"; ringofstorms.secretsBao = {
}; enable = true;
nix2lio = { zitadelKeyPath = "/machine-key.json";
owner = "josh"; openBaoAddr = "https://sec.joshuabell.xyz";
group = "users"; jwtAuthMountPath = "auth/zitadel-jwt";
kvPath = "kv/data/machines/home_roaming/nix2lio"; openBaoRole = "machines";
}; zitadelIssuer = "https://sso.joshuabell.xyz";
}; zitadelProjectId = "344379162166820867";
}; inherit secrets;
};
systemd.services.tailscaled = { }
after = [ "openbao-secret-headscale_auth.service" ]; (inputs.secrets-bao.lib.applyConfigChanges secrets)
requires = [ "openbao-secret-headscale_auth.service" ]; ]
}; )
}
)
# inputs.beszel.nixosModules.agent # inputs.beszel.nixosModules.agent
# ({ # ({