{ config, lib, pkgs, ... }: let cfg = config.ringofstorms.secretsBao; mkJwtMintScript = pkgs.writeShellScript "zitadel-mint-jwt" '' #!/usr/bin/env bash set -euo pipefail key_json="${cfg.zitadelKeyPath}" kid="$(${pkgs.jq}/bin/jq -r .keyId "$key_json")" sub="$(${pkgs.jq}/bin/jq -r .userId "$key_json")" pem_file="$(${pkgs.coreutils}/bin/mktemp)" trap '${pkgs.coreutils}/bin/rm -f "$pem_file"' EXIT ${pkgs.jq}/bin/jq -r .key "$key_json" >"$pem_file" ${pkgs.coreutils}/bin/chmod 600 "$pem_file" now="$(${pkgs.coreutils}/bin/date +%s)" exp="$(( now + ${toString cfg.jwtLifetimeSeconds} ))" jti="$(${pkgs.openssl}/bin/openssl rand -hex 16)" header="$(${pkgs.jq}/bin/jq -cn --arg kid "$kid" '{alg:"RS256",typ:"JWT",kid:$kid}')" payload="$(${pkgs.jq}/bin/jq -cn \ --arg iss "$sub" \ --arg sub "$sub" \ --arg aud "${cfg.zitadelTokenEndpoint}" \ --arg jti "$jti" \ --argjson iat "$now" \ --argjson exp "$exp" \ '{iss:$iss,sub:$sub,aud:$aud,iat:$iat,exp:$exp,jti:$jti}' )" b64url() { ${pkgs.openssl}/bin/openssl base64 -A | ${pkgs.gnused}/bin/sed -e 's/+/-/g' -e 's/\//_/g' -e 's/=*$//' } h64="$(${pkgs.coreutils}/bin/printf '%s' "$header" | b64url)" p64="$(${pkgs.coreutils}/bin/printf '%s' "$payload" | b64url)" sig="$(${pkgs.coreutils}/bin/printf '%s' "$h64.$p64" | ${pkgs.openssl}/bin/openssl dgst -sha256 -sign "$pem_file" | b64url)" assertion="$h64.$p64.$sig" resp="" if ! resp="$(${pkgs.curl}/bin/curl -sS --fail-with-body -X POST "${cfg.zitadelTokenEndpoint}" \ -H 'content-type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \ --data-urlencode "assertion=$assertion" \ --data-urlencode "scope=${cfg.zitadelScopes}" \ )"; then echo "Zitadel token endpoint returned error; response:" >&2 echo "$resp" >&2 exit 1 fi token="$(${pkgs.jq}/bin/jq -r '.access_token // empty' <<<"$resp" 2>/dev/null || true)" if [ -z "$token" ] || [ "$token" = "null" ]; then echo "Zitadel token mint did not return access_token; response:" >&2 echo "$resp" >&2 exit 1 fi # Quick sanity check: JWT should have 2 dots. if ! ${pkgs.gnugrep}/bin/grep -q '\\.' <<<"$token"; then echo "Zitadel access_token does not look like a JWT; response:" >&2 echo "$resp" >&2 exit 1 fi ${pkgs.coreutils}/bin/printf '%s' "$token" ''; zitadelHost = let noProto = lib.strings.removePrefix "https://" (lib.strings.removePrefix "http://" cfg.zitadelTokenEndpoint); in builtins.head (lib.strings.splitString "/" noProto); mkAgentConfig = pkgs.writeText "vault-agent.hcl" '' vault { address = "${cfg.openBaoAddr}" } auto_auth { method "jwt" { mount_path = "${cfg.jwtAuthMountPath}" config = { role = "${cfg.openBaoRole}" jwt_file = "${cfg.zitadelJwtPath}" } } sink "file" { config = { path = "${cfg.vaultAgentTokenPath}" mode = 0400 } } } ${lib.concatStringsSep "\n\n" ( lib.mapAttrsToList ( name: secret: let renderedTemplate = if secret.template != null then secret.template else ''{{- with secret "${secret.kvPath}" -}}{{- .Data.data.${secret.field} -}}{{- end -}}''; in '' template { destination = "${secret.path}" perms = "${secret.mode}" contents = <&2 exit 1 fi echo "zitadel-mint-jwt: starting (host=${zitadelHost})" >&2 jwt_is_valid() { local token="$1" local payload_b64 payload_json exp now payload_b64="$(${pkgs.coreutils}/bin/printf '%s' "$token" | ${pkgs.coreutils}/bin/cut -d. -f2)" 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 *1) payload_b64="$payload_b64=" ;; *2) payload_b64="$payload_b64==" ;; *3) : ;; *0) : ;; esac payload_json="$(${pkgs.coreutils}/bin/printf '%s' "$payload_b64" | ${pkgs.coreutils}/bin/base64 -d 2>/dev/null || true)" exp="$(${pkgs.jq}/bin/jq -r '.exp // empty' <<<"$payload_json" 2>/dev/null || true)" if [ -z "$exp" ]; then return 1 fi now="$(${pkgs.coreutils}/bin/date +%s)" if [ "$exp" -gt $(( now + 60 )) ]; then return 0 fi return 1 } if [ -s "${cfg.zitadelJwtPath}" ] && jwt_is_valid "$(cat "${cfg.zitadelJwtPath}")"; then echo "zitadel-mint-jwt: existing token still valid; skipping" >&2 exit 0 fi dns_ok() { ${pkgs.systemd}/bin/resolvectl query ${zitadelHost} >/dev/null 2>&1 && return 0 ${pkgs.glibc}/bin/getent hosts ${zitadelHost} >/dev/null 2>&1 && return 0 return 1 } # Wait for DNS to be usable. for i in {1..180}; do if dns_ok; then break fi sleep 1 done if ! dns_ok; then echo "DNS still not ready for ${zitadelHost}" >&2 ${pkgs.systemd}/bin/resolvectl status >&2 || true exit 1 fi # Mint token (retry a bit for transient network issues). jwt="" for i in {1..10}; do if jwt="$(${mkJwtMintScript})"; then break fi sleep 2 done if [ -z "$jwt" ] || [ "$jwt" = "null" ]; then echo "Failed to mint Zitadel access token" >&2 exit 1 fi tmp="$(${pkgs.coreutils}/bin/mktemp)" trap '${pkgs.coreutils}/bin/rm -f "$tmp"' EXIT ${pkgs.coreutils}/bin/printf '%s' "$jwt" > "$tmp" ${pkgs.coreutils}/bin/mv -f "$tmp" "${cfg.zitadelJwtPath}" ''; }; }; 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 = "30s"; UMask = "0077"; ExecStartPre = pkgs.writeShellScript "openbao-wait-jwt" '' #!/usr/bin/env bash set -euo pipefail for i in {1..180}; do if [ -s "${cfg.zitadelJwtPath}" ]; then jwt="$(cat "${cfg.zitadelJwtPath}")" # very cheap sanity check: JWT has at least 2 dots if ${pkgs.gnugrep}/bin/grep -q '\\..*\\.' <<<"$jwt"; then exit 0 fi fi sleep 1 done echo "Missing or invalid Zitadel JWT at ${cfg.zitadelJwtPath}" >&2 exit 1 ''; ExecStart = "${pkgs.openbao}/bin/bao agent -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" ]; 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 exit 0 fi sleep 1 done echo "Secret file not rendered: $p" >&2 exit 1 ''; }; } ) cfg.secrets) ]; age.secrets = lib.mapAttrs' ( name: secret: lib.nameValuePair name { file = null; path = secret.path; } ) cfg.secrets; }; }