refactor: jq for tea-login, build-time avatar png, shared leaf-sign script

This commit is contained in:
müde 2026-07-13 22:14:25 +02:00
commit e0cfed7fe8
2 changed files with 59 additions and 70 deletions

View file

@ -10,6 +10,13 @@
let
userName = config.hyperhive.user.name;
homeDir = "/home/${userName}";
# Same 512×512 rasterization of the agent icon the matrix avatar
# sync uses (./matrix.nix — identical derivation, same store path).
# Only forced when an icon is configured (the avatar-sync unit below
# is gated on `hyperhive.icon != null`).
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
'';
in
{
options.hyperhive.forge.url = lib.mkOption {
@ -65,7 +72,7 @@ in
};
path = [
pkgs.curl
pkgs.python3
pkgs.jq
pkgs.coreutils
];
environment.HOME_DIR = homeDir;
@ -84,8 +91,7 @@ in
USER=$(curl -sf --max-time 5 \
-H "Authorization: token $TOKEN" \
"$FORGE_URL/api/v1/user" \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("login",""))' \
2>/dev/null || true)
| jq -r '.login // empty' 2>/dev/null || true)
if [ -z "$USER" ]; then
echo "tea-login: could not resolve username from forge API; skipping"
exit 0
@ -123,7 +129,7 @@ in
# Without this path unit, RemainAfterExit=true would prevent systemd
# from ever re-running the service. See
# docs/persistence.md::forge-avatar-sync.
systemd.paths.forge-avatar-sync = {
systemd.paths.forge-avatar-sync = lib.mkIf (config.hyperhive.icon != null) {
description = "trigger forge-avatar-sync when forge-token appears";
wantedBy = [ "multi-user.target" ];
pathConfig.PathExistsGlob = "/agents/*/state/forge-token";
@ -132,8 +138,12 @@ in
# One-shot: hyperhive.icon → Forgejo profile avatar. Shape contract:
# docs/conventions.md::Best-effort oneshot services.
# RemainAfterExit = false so the .path trigger above can re-fire
# this unit when the forge-token arrives after boot.
systemd.services.forge-avatar-sync = {
# this unit when the forge-token arrives after boot. The PNG is
# rasterized at build time (`iconPng`, shared shape with the matrix
# avatar sync), so the unit only exists when an icon is configured
# and needs no librsvg at runtime — Forgejo's Go image library
# can't decode SVG, hence PNG.
systemd.services.forge-avatar-sync = lib.mkIf (config.hyperhive.icon != null) {
description = "sync agent icon to Forgejo user avatar (best-effort)";
wantedBy = [ "multi-user.target" ];
after = [ "tea-login.service" ];
@ -147,14 +157,8 @@ in
pkgs.curl
pkgs.coreutils
pkgs.jq
pkgs.librsvg
];
script = ''
ICON=/etc/hyperhive/icon.svg
if [ ! -f "$ICON" ]; then
echo "forge-avatar-sync: no icon configured; skipping"
exit 0
fi
FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url}
# $HYPERHIVE_STATE_DIR is set system-wide by the meta flake
# (systemd.globalEnvironment) to `/agents/<name>/state`.
@ -164,15 +168,7 @@ in
exit 0
fi
TOKEN=$(cat "$TOKEN_FILE")
# Rasterize SVG → PNG (Forgejo's Go image library can't decode SVG).
PNG=$(mktemp --suffix=.png)
if ! rsvg-convert -f png -w 512 -h 512 "$ICON" -o "$PNG" 2>/dev/null; then
echo "forge-avatar-sync: rsvg-convert failed; skipping"
rm -f "$PNG"
exit 0
fi
IMAGE=$(base64 -w 0 < "$PNG")
rm -f "$PNG"
IMAGE=$(base64 -w 0 < ${iconPng})
# Forgejo POST /user/avatar expects {"image":"<base64>"} — just the
# raw base64 string, NOT a data URI (data:image/png;base64,...).
# Use jq to build the payload so the large base64 value is safely quoted.

View file

@ -18,6 +18,46 @@ let
# module's single source of truth (`gateway.useSelfSigned`): true when
# neither an operator cert (`tls.certDir`) nor ACME is set.
active = hyperhiveCfg.enable && gatewayCfg.useSelfSigned;
# The leaf-signing action shared by the boot-time `hive-tls-ca`
# generation and the weekly `hive-tls-resign` renewal: fresh key +
# CSR, SAN ext-file, sign under the (stable) CA, tighten modes.
# Takes the TLS state dir as `$1`; each caller keeps its own
# when-to-sign condition. The leaf covers the bare hive domain plus
# `forge.`, `matrix.` and `*.<domain>` so all sub-domains validate
# under the same cert + the hive CA.
signLeafScript = pkgs.writeShellScript "hive-tls-sign-leaf" ''
set -euo pipefail
d="$1"
ca="$d/ca.pem"
cak="$d/ca-key.pem"
leaf="$d/gateway.pem"
leafk="$d/gateway-key.pem"
csr="$(mktemp "$d/gateway.csr.XXXXXX")"
ext="$(mktemp "$d/leaf.ext.XXXXXX")"
trap 'rm -f "$csr" "$ext"' EXIT
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout "$leafk" -out "$csr" \
-subj "/CN=${domain}"
# printf (not a heredoc) so the ext-file lines carry no leading
# whitespace once nix has stripped the indented-string indent.
{
printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \
${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \
${lib.escapeShellArg domain} ${lib.escapeShellArg domain}
printf 'basicConstraints=critical,CA:FALSE\n'
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
printf 'extendedKeyUsage=serverAuth\n'
} > "$ext"
openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \
-CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \
-extfile "$ext" -out "$leaf"
chmod 0600 "$leafk"
chmod 0644 "$leaf"
'';
in
{
# Host-side TLS trust root for the self-signed gateway mode.
@ -132,30 +172,7 @@ in
if [ ! -s "$leaf" ] || [ ! -s "$leafk" ] \
|| ! openssl x509 -in "$leaf" -noout -checkend 2592000 >/dev/null 2>&1; then
echo "signing fresh gateway leaf at $leaf"
csr="$(mktemp "$d/gateway.csr.XXXXXX")"
ext="$(mktemp "$d/leaf.ext.XXXXXX")"
trap 'rm -f "$csr" "$ext"' EXIT
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout "$leafk" -out "$csr" \
-subj "/CN=${domain}"
# printf (not a heredoc) so the ext-file lines carry no leading
# whitespace once nix has stripped the indented-string indent.
{
printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \
${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \
${lib.escapeShellArg domain} ${lib.escapeShellArg domain}
printf 'basicConstraints=critical,CA:FALSE\n'
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
printf 'extendedKeyUsage=serverAuth\n'
} > "$ext"
openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \
-CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \
-extfile "$ext" -out "$leaf"
chmod 0600 "$leafk"
chmod 0644 "$leaf"
${signLeafScript} "$d"
fi
'';
};
@ -200,10 +217,7 @@ in
script = ''
set -euo pipefail
d=${lib.escapeShellArg cfg.stateDir}
ca="$d/ca.pem"
cak="$d/ca-key.pem"
leaf="$d/gateway.pem"
leafk="$d/gateway-key.pem"
# Re-sign only when the leaf is within half its validity of expiry.
# The weekly cadence catches this window well before the leaf lapses.
@ -217,28 +231,7 @@ in
echo "gateway leaf missing or near expiry re-signing under current CA"
before="$(sha256sum "$leaf" 2>/dev/null || true)"
csr="$(mktemp "$d/gateway.csr.XXXXXX")"
ext="$(mktemp "$d/leaf.ext.XXXXXX")"
trap 'rm -f "$csr" "$ext"' EXIT
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout "$leafk" -out "$csr" \
-subj "/CN=${domain}"
{
printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \
${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \
${lib.escapeShellArg domain} ${lib.escapeShellArg domain}
printf 'basicConstraints=critical,CA:FALSE\n'
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
printf 'extendedKeyUsage=serverAuth\n'
} > "$ext"
openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \
-CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \
-extfile "$ext" -out "$leaf"
chmod 0600 "$leafk"
chmod 0644 "$leaf"
${signLeafScript} "$d"
after="$(sha256sum "$leaf" 2>/dev/null || true)"
if [ "$before" != "$after" ]; then