From a2e36eee780a48efa60e8830b871d7765247a697 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Fri, 26 Jun 2026 10:28:40 -0400 Subject: [PATCH] feat(secrets): drive the backup-repo PAT through SOPS + bridge it to Hermes' env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only plaintext secret left in the stack was GITHUB_PAT in `.env` (and embedded in the ordo-hermes-backup remote URL), used to push to the backup repo. It was a different token from the SOPS `github_pat` (fine-grained) and the fine-grained one has no access to the backup repo, so the backup push genuinely needs its own token. Bring it under SOPS like everything else, and surface it to Hermes the way Hermes expects — as an env var — via the established Docker-secret → entrypoint bridge: - secrets/github_backup_pat.sops — encrypted classic PAT (age recipient). - scripts/secrets/decrypt.sh — decrypt it to runtime/secrets/github_backup_pat. - docker-compose.yml — mount it on hermes-gateway (/run/secrets/github_backup_pat) + GITHUB_BACKUP_PAT_FILE env. - hermes/entrypoint.sh — bridge GITHUB_BACKUP_PAT_FILE -> GITHUB_BACKUP_PAT env var (same pattern as DISCORD_BOT_TOKEN), so Hermes finds the token in its environment instead of concluding "no access" when it's only in SOPS. - docs (secrets runbook, secrets/README, Hermes SOUL): all secrets are SOPS-driven and reach apps as bridged env vars; never plaintext in .env, never in a URL. The backup remote is now a clean URL authenticated by a credential helper that reads the SOPS-decrypted token (works on host and in-container). The plaintext GITHUB_PAT is removed from `.env`. No secret values are committed (only the encrypted .sops blob and path references). Validated: host `git push` to the backup works via the SOPS helper; the bridge is present in the rebuilt hermes image with all preconditions met (secret mounted, non-empty, _FILE env wired) — identical to the proven discord bridge. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker-compose.yml | 6 ++++++ docs/runbooks/secrets.md | 7 ++++++- hermes/entrypoint.sh | 8 ++++++++ scripts/secrets/decrypt.sh | 1 + secrets/README.md | 4 ++++ secrets/github_backup_pat.sops | 20 ++++++++++++++++++++ 6 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 secrets/github_backup_pat.sops diff --git a/docker-compose.yml b/docker-compose.yml index 5867597..c3cb240 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1068,6 +1068,9 @@ services: # discord.py expects. Legacy DISCORD_TOKEN inline alias is dropped — # use SOPS at secrets/discord_token.sops. - DISCORD_BOT_TOKEN_FILE=/run/secrets/discord_token + # Backup-repo PAT (git push to ordo-hermes-backup). SOPS-managed; the + # entrypoint bridges GITHUB_BACKUP_PAT_FILE -> GITHUB_BACKUP_PAT env var. + - GITHUB_BACKUP_PAT_FILE=/run/secrets/github_backup_pat - DISCORD_ALLOWED_USERS=${DISCORD_ALLOWED_USERS:-} - DISCORD_ALLOWED_CHANNELS=${DISCORD_ALLOWED_CHANNELS:-} - DISCORD_ALLOWED_ROLES=${DISCORD_ALLOWED_ROLES:-} @@ -1097,6 +1100,7 @@ services: - ${BASE_PATH:-.}/..:${HERMES_HOST_DEV_MOUNT:-/projects}:rw secrets: - discord_token + - github_backup_pat healthcheck: # gateway_state.json is written by `hermes gateway` on startup (Docker-mode # doesn't create a gateway.pid — that's only for systemd/launchd installs). @@ -1223,6 +1227,8 @@ secrets: file: ${HOME}/.ai-toolkit/runtime/secrets/discord_token github_pat: file: ${HOME}/.ai-toolkit/runtime/secrets/github_pat + github_backup_pat: + file: ${HOME}/.ai-toolkit/runtime/secrets/github_backup_pat hf_token: file: ${HOME}/.ai-toolkit/runtime/secrets/hf_token civitai_token: diff --git a/docs/runbooks/secrets.md b/docs/runbooks/secrets.md index 684102a..f4c903d 100644 --- a/docs/runbooks/secrets.md +++ b/docs/runbooks/secrets.md @@ -27,7 +27,12 @@ Two delivery paths, both fed from `~/.ai-toolkit/runtime/` (produced by `SEARXNG_SECRET`, `N8N_OWNER_*`, …). `make up` always passes both. - **File-form** (`secrets/.sops` → `runtime/secrets/`): mounted as Docker secrets at `/run/secrets/`, so they never appear in - `docker inspect`. + `docker inspect`. Where an app SDK expects a plain env var, the consumer's + entrypoint **bridges** `_FILE` → a `` env var (e.g. hermes-gateway: + `DISCORD_BOT_TOKEN`, `GITHUB_BACKUP_PAT`). So agents read the token from their + environment — they never see, need, or look for a plaintext secret in `.env`. + The `ordo-hermes-backup` git remote authenticates the same way: a credential + helper reads the SOPS-decrypted token, so no token is ever embedded in a URL. ### ops-controller recreates with real secrets diff --git a/hermes/entrypoint.sh b/hermes/entrypoint.sh index e152269..df66a3e 100644 --- a/hermes/entrypoint.sh +++ b/hermes/entrypoint.sh @@ -46,6 +46,14 @@ if [ -n "${DISCORD_BOT_TOKEN_FILE:-}" ] && [ -f "$DISCORD_BOT_TOKEN_FILE" ]; the export DISCORD_BOT_TOKEN fi +# Same bridge for the backup-repo PAT: SOPS Docker secret at +# /run/secrets/github_backup_pat -> GITHUB_BACKUP_PAT env var that Hermes and +# git expect. Secrets live in SOPS, never in .env; this is how they reach the env. +if [ -n "${GITHUB_BACKUP_PAT_FILE:-}" ] && [ -f "$GITHUB_BACKUP_PAT_FILE" ]; then + GITHUB_BACKUP_PAT="$(cat "$GITHUB_BACKUP_PAT_FILE")" + export GITHUB_BACKUP_PAT +fi + HERMES_BIN=/opt/hermes-agent/.venv/bin/hermes # Seed model + MCP endpoints to Docker-network DNS. hermes config set is idempotent diff --git a/scripts/secrets/decrypt.sh b/scripts/secrets/decrypt.sh index 2b25335..03fb1dc 100644 --- a/scripts/secrets/decrypt.sh +++ b/scripts/secrets/decrypt.sh @@ -36,6 +36,7 @@ echo "==> ${RUNTIME_DIR}/.env (env-form internal tokens)" # File-form: decrypt each high-value token to its own file. for src in secrets/discord_token.sops \ secrets/github_pat.sops \ + secrets/github_backup_pat.sops \ secrets/hf_token.sops \ secrets/civitai_token.sops \ secrets/n8n_api_key.sops; do diff --git a/secrets/README.md b/secrets/README.md index fee62d7..92794c2 100644 --- a/secrets/README.md +++ b/secrets/README.md @@ -16,6 +16,10 @@ with the age private key at `~/.config/sops/age/keys.txt`. - `github_pat.sops` — GitHub fine-grained PAT. Mounted on `mcp-gateway` and `comfyui` (the latter as `GITHUB_TOKEN_FILE` for ComfyUI-Manager). +- `github_backup_pat.sops` — classic GitHub PAT for `git push` to the + `ordo-hermes-backup` private repo. Mounted on `hermes-gateway`; the + entrypoint bridges it to the `GITHUB_BACKUP_PAT` env var, and the backup + repo's credential helper reads it. Not used by the stack services themselves. - `hf_token.sops` — HuggingFace token (gated model downloads). Mounted on `ops-controller`, `dashboard`, `gguf-puller`, and the comfyui model puller. diff --git a/secrets/github_backup_pat.sops b/secrets/github_backup_pat.sops new file mode 100644 index 0000000..03c4095 --- /dev/null +++ b/secrets/github_backup_pat.sops @@ -0,0 +1,20 @@ +{ + "data": "ENC[AES256_GCM,data:whjXnID5PQh/mlkpS7/5XDuYgxRQyGM5ry7+sQcxFcrlLCDXk0hV3Q==,iv:9VOjPZQB5+rZO82bc5QBgWF3gLqVY+AahQHN4U0vF7g=,tag:ForGDI3m4LsCCjC7azZlDw==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age1egt7028wtwpf3g9fqe5xvf80ptmhze25tltw0ff6nwv5hf6v6grqpl2qar", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyZlpneGVUZ2V0a200NVE2\neTRrNGFqOEtWdlFkcWQ0VE1keEt0YjJ1OWlvCnplTm1MYWVDQWlMM0pISFRhWHlt\nQ3hxeStBL1BXQTYxRVdoOVNjcTNBZkkKLS0tIHBxdG1iWHgvdytkdEpZeC83Q2tM\nQmRkaXZCNDRmTldpU0JVTU9RTkg0VHMKErCgr6lhx5ICdemS3dEzkFFDvAHjMObO\nLvLtTexE3rSuX62x/XEHT5mPHV+K5RG/pvBYI5V78ghS3CCHKy7P7w==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-26T14:16:58Z", + "mac": "ENC[AES256_GCM,data:2N50+k13OFK0rhvei/vfVib5Psad5iB9jhSiTItePnTUfRaw+ViHUB7n2FMrSl1i6oAphfwkU4Un/3hp6oANVHpg8pLBn2EaYm1QY841R2Q81kL3zYmQANMIIW7YHm/9HihMBRKTD5jNU7k4RuhBem5PTJjCt8AMyYfcJKJZNF0=,iv:rzndofwrUJI2QAHW4vyfy6CoshO5+qa5FMDDtYqKbI4=,tag:n0hR1nMZaI+KM/zYDZ2hNQ==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.7.3" + } +} \ No newline at end of file