refactor: jq for tea-login, build-time avatar png, shared leaf-sign script
This commit is contained in:
parent
935e967718
commit
e0cfed7fe8
2 changed files with 59 additions and 70 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue