feat: Forgejo admin broker for the admin token and webhook secret #56
No reviewers
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck!56
Loading…
Reference in a new issue
No description provided.
Delete branch "55-forgejo-admin-broker-isolate-admin-token-and-webhook-secret-from-the-agent"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds
apps/broker, a separate process that holds the Forgejo site-admin token and the webhook secret under its own UID. It answers two read-only requests on a unix socket:verify-webhookandpermission. The deck checks webhook signatures through it and no longer stores or exposes the webhook secret (API route, UI generator and DB row removed). The dispatcher checks every trigger author's permission through the broker instead of the payload, andreviewerCanWriteis gone. The Docker image now starts as root, and its entrypoint runs the broker as UID 999 and the deck asOMP_DECK_UID:OMP_DECK_GIDwithHOME=/data.Decisions
verifyForgejoSignature's tests, but none existed; the broker has new signature tests instead (comment).not_configuredoutcome (detailwebhook broker unavailable: …), so migration 012's CHECK constraint stays unchanged.runrecipe change is local only and not in this PR (comment).docker-compose.ymlnow bind-mounts/dataand passesOMP_DECK_UID/OMP_DECK_GID; its root-owned named volume would have been unwritable by the deck..dockerignoreand.gitignoreexclude/data(same comment).OMP_BROKER_SOCKETinstead of hard-coding/run/omp-broker.Verification
bun run typecheck: all workspaces pass.apps/broker:bun test15 pass.apps/server: fullbun test859 pass, including the converted dispatcher tests and the webhook route tests against a stub broker socket.must have mode 0600 (is 0644); the socket is 0666;permissionfor a repo off the allowlist → 403repository not allowed;verify-webhookwith a correct HMAC →{"valid":true}; the socket is removed on SIGTERM. The admin-token permission lookup against a real Forgejo was not exercised (no site-admin token here).docker/entrypoint.sh:sh -npasses; run as non-root it exits 1 with the root-required message. The Docker build and in-container checks were not run (no Docker in this environment).Closes #55
Container step decisions and deviations from the plan:
.gitignoreignoresJustfile("kept out of the repo as a personal convenience file"), so therunrecipe change exists only in the local working tree and can't be committed:--userand-e HOME=/dataare removed,-e OMP_DECK_UID=$(id -u) -e OMP_DECK_GID=$(id -g)are added, and the comment explains the broker config setup./data/omp-agentwould have been unwritable by the deck UID. It now bind-mounts${OMP_DECK_DATA:-./data}as/data(the deck's HOME, so.omp/agentlives there), passesOMP_DECK_UID/OMP_DECK_GID, and dropsOMP_AGENT_DIR..dockerignoreadded with/data, and/dataadded to.gitignore. Compose's default data dir sits in the build context, and itsbroker/subdirectory is readable only by UID 999, which would make sending the context fail.dirname $OMP_BROKER_SOCKETinstead of the hard-coded/run/omp-broker, so overriding the socket path keeps working.sh -nand the non-root guard were exercised.Right now, the user has to manually create the config file, right? Can't we give the broker a route to set its settings, but it only works on init?
That would alleviate the issue that an agent could overwrite the tokens, because no one can call it a second time. And it would still make the happy path easier because most people would likely just configure things once and rarely need to edit afterwards.
omp-deck failed to handle this event: an internal error occurred.
Details are in omp-deck under webhook delivery
1104699d-7f89-4cd6-83c4-72116bf5da2f.Please answer the previous question
This repository isn't connected to an omp-deck workspace, so I can't answer here. Register a checkout of it as a workspace in omp-deck.
Please answer the previous question
Sorry for the missed reply. Yes, right now the operator has to create
$OMP_DECK_DATA_DIR/broker/config.jsonby hand, owned by 999 with mode 600. If it's missing,entrypoint.shrefuses to start, andloadConfigwould exit anyway.An init-only route would stop the config from being overwritten, but it opens a race for the first write. The socket is
0666, andserver.tssays the broker can't tell the deck from the agent because they run as the same UID. If the broker starts unconfigured and exposes/init, any agent session that runs before the user finishes setup can claim it. That attacker could then:webhookSecretthey know and forge webhook deliveries that start sessions, orforgejoUrlat a server they control, so/permissionreturns whatever they want.After that, the "only once" rule protects the attacker's config. On a fresh install the window may be short. But the deck is usable before Forgejo is set up, so agents can run before setup happens, and the window stays open until the user configures it.
Three ways to keep the easy setup without that race:
/initrequires that code. The deck/agent UID can't read the broker's stdout or/procentries, so only someone with access to the container logs can finish setup. After a successful init the code is discarded and the route is gone. The Settings panel could offer a form: "paste the code fromdocker logs".docker exec -it omp-deck omp-broker initasks for the URL, token and repos, generates the webhook secret, and writes the file with the right owner and mode. There's no network surface and no race, and editing later is the same command. The trade-off is that setup happens in the terminal, not the UI.webhookSecretalready filled in, so the user only adds the token, URL and repos. This is the smallest change but still needs manual editing.Either way, the broker needs a "running but unconfigured" state:
/verify-webhookreturnsvalid: falseand/permissionreturns 503 until it's configured, and the entrypoint no longer dies when the config is missing.I'd go with (1), with (2) as the way to fix mistakes, because "can only be called once" means a typo otherwise requires deleting the file by hand. Should I implement (1), (2), or both in this PR?
Let's go with #3 then
Option 3 it is. I can't push from this session, so here's the change I'll make. One decision below needs your input first.
docker/entrypoint.sh$BROKER_CONFIGis missing, write a template as the broker UID (umask 077, so 999:999 with mode 600): An existing file is never touched, and the ownership and mode checks stay as they are.forgejoUrl,adminTokenandrepos, then put the generatedwebhookSecretinto the Forgejo webhook. The secret itself is never printed.Docs:
docs/deployment.mddrops the manualmkdir/chown/chmodsteps. They're replaced by "start once, then edit the generated file as root" (docker exec -u 0 -it omp-deck vi …orsudoon the host) and "restart after editing".Decision needed: what happens while the template is still unfilled?
/verify-webhookwithvalid: falseand/permissionwith 503. The deck runs normally and only the Forgejo integration stays off until the next restart with a filled-in config. This is more code (a new state inconfig.ts/index.tsplus tests), but it fits a deck where Forgejo is optional.I'd go with (b), since Forgejo is opt-in everywhere else in the deck. Which do you want?
You should now have write access.
Thanks. Before I implement option 3, I need your answer on the open question in my last comment: while the generated template is still unfilled, should the deck
/verify-webhookreturnsvalid: false,/permissionreturns 503, and the deck otherwise runs normally)?I recommend (b). Reply with (a) or (b) and I'll implement it, run the checks and push.
b) please
Implemented option 3(b) in 692bdc4.
docker/entrypoint.sh: ifbroker/config.jsonis missing, the entrypoint writes a template as UID 999 withumask 077, so the file is 999:999 with mode 600. The template has emptyforgejoUrl/adminToken,repos: [], and a 64-hex-charwebhookSecretfrom/dev/urandom. The entrypoint then prints the path and next steps to stderr; the secret isn't printed. An existing file is never touched, and the owner/mode checks still apply.loadConfigreturnsnullfor the exact unfilled template. In that case the broker logs a warning and runs unconfigured:/verify-webhookreturns{valid:false}and/permissionreturns 503broker not configuredwithout contacting Forgejo. A partially filled template still fails validation, so the container exits. I made that choice on purpose: a typo should be loud, not leave Forgejo silently off.deployment.mdreplaces the manualinstallsteps with "start once, edit as root (docker exec -u 0 -it omp-deck vi …), restart".configuration.mdis updated to match.loadConfigtest checks that the template is treated as unconfigured and that a partially filled one is invalid. A handler test checks the unconfigured responses.Verification:
bun test apps/broker: 17 pass.sh -n docker/entrypoint.shpasses.as_brokerreplaced by plainsh, since this shell isn't root) and got mode 600 and a 64-char secret.bun run typecheckcouldn't run becausetscisn't installed in this checkout.Separately, Forgejo now reports the PR as not mergeable against
main. Do you want me to rebase it ontomain?Can you also fix the file conflicts?
692bdc4d17toc45d72769dI rebased the branch onto
mainand force-pushed it (head is nowc45d727). Two files had conflicts, both frommain's newreadPull/ own-PR handling:dispatcher.ts: I kept the broker permission check for every trigger author.reviewerCanWriteand the payloadpermissionscheck are gone, as before. The own-PR variable is nowagentPr, matchingmain, somain's newreviewRequest && agentPrnotice still works.dispatcher.test.ts: I keptmain'spullandpullReadsfake fields and removedcanWriteandreviewChecks.All checks below pass:
bun run typecheck: all workspaces pass. I had to runbun install --frozen-lockfilefirst.apps/server:bun testpasses, 866 tests.apps/broker:bun testpasses, 17 tests.The migration numbering is still sequential:
mainends at 015, and this PR adds 016.