The relay was forwarding X-Gitea-Event and re-signing the body into the
deprecated generic-V1 X-Webhook-Signature header. Neither is something
Hermes acts on, which left the PR's core premise — "Hermes owns event
selection" — impossible to reach:
- Hermes reads the event name only from X-GitHub-Event/X-GitLab-Event,
then payload event_type/type, then falls back to the literal string
"unknown" (gateway/platforms/webhook.py). Gitea sends X-Gitea-Event and
no such payload key, so every delivery arrived as "unknown" and
`hermes webhook subscribe --events ...` could never select anything.
- Gitea's addDefaultHeaders() already signs every webhook type with
X-Hub-Signature-256 in GitHub's exact format, and Hermes accepts that
header on any route with no per-route provider gating. Re-signing into
V1 was both redundant and on a deprecated path.
So the relay now verifies the signature (accepting either X-Hub-Signature-256
or X-Gitea-Signature), forwards body and signature byte-for-byte, and copies
the one header Hermes actually needs. Authentication alone never justified
this service; that header copy does, and the module comment now says so.
Also fixed:
- gitea-hermes-webhook-provision had no API readiness wait, unlike both
sibling units in the same file. After=gitea.service does not mean gitea
is serving HTTP, so under `set -e` a Type=oneshot with no Restart= would
fail on first boot and stay failed, leaving the webhook unregistered.
- podman-hermes-agent added to the secret's restartUnits. The secret
reaches the container only via sops.templates, whose rendered path never
changes, so systemd would not restart the container when the secret was
first added — hermes-agent-webhook-route then read an empty value back
out of it and subscribed with an empty secret.
- Webhook provisioning passes the request body to curl on stdin rather
than in argv, keeping the shared secret out of /proc/<pid>/cmdline.
- Missing Content-Length now returns 411 rather than 413; dropped the
unreachable non-2xx branch (urlopen raises on non-2xx); env-var secret
fallback is stripped to match the credential-file path.
Adds gitea-hermes-webhook-relay-test.py, which drives the real relay over
real HTTP against a stub Hermes and covers the header copy as a regression
test. Both nixosConfigurations still evaluate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa
75 lines
3.7 KiB
Nix
75 lines
3.7 KiB
Nix
{ config, ... }:
|
|
|
|
# sops-nix wiring for mars. Encrypted values in ../../secrets/mars.yaml,
|
|
# decrypted with mars's own SSH host key (recipient in ../../.sops.yaml).
|
|
# The host key is pre-generated on the laptop and shipped at install
|
|
# (nixos-anywhere --extra-files -> /etc/ssh/ssh_host_ed25519_key).
|
|
{
|
|
sops.defaultSopsFile = ../../secrets/mars.yaml;
|
|
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
|
|
|
sops.secrets.darman_password.neededForUsers = true;
|
|
users.users.darman.hashedPasswordFile = config.sops.secrets.darman_password.path;
|
|
|
|
sops.secrets.tailscale_authkey = { };
|
|
|
|
# Credentials file for the //jupiter/data cifs mount (see configuration.nix).
|
|
# Same value as jupiter's own samba_password (services/network/samba.nix) —
|
|
# mars authenticates as the same smb user, mirroring terra's setup.
|
|
sops.secrets.samba_password = { };
|
|
sops.templates."jupiter-smb.credentials".content = ''
|
|
username=darman
|
|
password=${config.sops.placeholder.samba_password}
|
|
'';
|
|
|
|
# Hermes Agent (hermes-agent.nix) — moved here from jupiter (see that
|
|
# host's git history); same Telegram bot token, opencode key, and
|
|
# Authentik OIDC client secret, so no new bot/app to provision.
|
|
sops.secrets.opencode_go_api_key = { };
|
|
sops.secrets.telegram_bot_token = { };
|
|
sops.secrets.hermes_dashboard_oidc_client_secret = { };
|
|
# Add the same value to secrets/mars.yaml before deploying Mars, and store
|
|
# it WITHOUT a trailing newline: it reaches Hermes through the env template
|
|
# below, where a newline would both corrupt the env file and change the key
|
|
# the HMAC is computed with. `scripts/edit_secrets` writes a bare value.
|
|
#
|
|
# podman-hermes-agent is in restartUnits for a reason that is easy to miss:
|
|
# the secret reaches the container only through sops.templates, whose
|
|
# rendered PATH never changes, so the container unit's definition is
|
|
# identical before and after the secret is added and systemd will NOT
|
|
# restart it on its own. Without this line the very first deploy leaves the
|
|
# container holding an empty GITEA_HERMES_WEBHOOK_SECRET, and
|
|
# hermes-agent-webhook-route (which reads it back out of the running
|
|
# container) subscribes with an empty secret — every relayed delivery then
|
|
# fails signature validation inside Hermes with no obvious cause.
|
|
sops.secrets.gitea_hermes_webhook_secret = {
|
|
restartUnits = [
|
|
"gitea-hermes-webhook-relay.service"
|
|
"podman-hermes-agent.service"
|
|
"hermes-agent-webhook-route.service"
|
|
];
|
|
};
|
|
sops.templates."hermes-agent.env".content = ''
|
|
OPENCODE_GO_API_KEY=${config.sops.placeholder.opencode_go_api_key}
|
|
TELEGRAM_BOT_TOKEN=${config.sops.placeholder.telegram_bot_token}
|
|
TELEGRAM_HOME_CHANNEL=15151223
|
|
TELEGRAM_ALLOWED_USERS=15151223
|
|
WEBHOOK_ENABLED=true
|
|
WEBHOOK_PORT=8644
|
|
GITEA_HERMES_WEBHOOK_SECRET=${config.sops.placeholder.gitea_hermes_webhook_secret}
|
|
HERMES_DASHBOARD_OIDC_CLIENT_SECRET=${config.sops.placeholder.hermes_dashboard_oidc_client_secret}
|
|
'';
|
|
|
|
# luna's own gitea push token (services/dev/gitea.nix provisions the
|
|
# account + PR-tier repo access on jupiter; this is the per-user token
|
|
# generated once via `gitea admin user generate-access-token --username
|
|
# luna --scopes write:repository,read:user` on jupiter — read:user is
|
|
# required, `tea logins add` fails without it). Read directly by
|
|
# hermes-agent.nix's prepare-dirs oneshot (default root:root owner is
|
|
# fine — that oneshot already runs as root) to set up a git
|
|
# credential-store file and a `tea` login, both written into hermesHome
|
|
# so they're visible inside the container at /opt/data/....
|
|
# restartUnits re-provisions both on rotation, without a full mars deploy.
|
|
sops.secrets.gitea_luna_token.restartUnits = [ "hermes-agent-prepare-dirs.service" ];
|
|
}
|