Pre-launch: verify forge runner settings for fork PRs (approval gate, no docker socket, egress isolation) #415

Open
opened 2026-08-17 15:48:30 +00:00 by hexajon · 3 comments
Owner

Both CI-audit passes flagged that the workflows themselves are well designed: plain pull_request (never pull_request_target/workflow_run), anonymous checkout with no token, and no third-party uses: actions. So the residual CI risk once public lives in runner and forge configuration that is NOT in repo source, and it must be confirmed before accepting fork PRs. This is a tracked checklist, not a code change.

Confirm, at the forge and the runner:

  1. Fork-PR approval gate - the org runner requires maintainer approval before it runs workflows from non-collaborator fork PRs (recent Forgejo defaults do this; it is version- and config-dependent). This is the single most important control, because it neutralizes the running-stranger-code risk behind the other findings. Confirm it is enabled.
  2. No host docker socket in job containers - the runner does NOT bind-mount /var/run/docker.sock into job containers, and RUN_FORGEJO_SMOKE is unset anywhere a fork PR can reach. (Ties to #266 and the closed #265: the code-side fix there is to drop pull_request from the socket-privileged smoke job, and this item is the config-side confirmation.)
  3. Egress isolation re-confirmed - the runner's outbound isolation is intact, verified with the isolation smoke check. The CI docs note this must be re-confirmed after any runner or VM rebuild. Make it a launch gate.

Acceptance: all three confirmed and noted. A comment here recording the verification is enough. This should be done before the repo starts accepting fork PRs.

Both CI-audit passes flagged that the workflows themselves are well designed: plain `pull_request` (never `pull_request_target`/`workflow_run`), anonymous checkout with no token, and no third-party `uses:` actions. So the residual CI risk once public lives in runner and forge configuration that is NOT in repo source, and it must be confirmed before accepting fork PRs. This is a tracked checklist, not a code change. Confirm, at the forge and the runner: 1. **Fork-PR approval gate** - the org runner requires maintainer approval before it runs workflows from non-collaborator fork PRs (recent Forgejo defaults do this; it is version- and config-dependent). This is the single most important control, because it neutralizes the running-stranger-code risk behind the other findings. Confirm it is enabled. 2. **No host docker socket in job containers** - the runner does NOT bind-mount `/var/run/docker.sock` into job containers, and `RUN_FORGEJO_SMOKE` is unset anywhere a fork PR can reach. (Ties to #266 and the closed #265: the code-side fix there is to drop `pull_request` from the socket-privileged smoke job, and this item is the config-side confirmation.) 3. **Egress isolation re-confirmed** - the runner's outbound isolation is intact, verified with the isolation smoke check. The CI docs note this must be re-confirmed after any runner or VM rebuild. Make it a launch gate. Acceptance: all three confirmed and noted. A comment here recording the verification is enough. This should be done before the repo starts accepting fork PRs.
Author
Owner

Runner-side verification done (2026-07-20) for items 2 and 3. Item 1, the fork-PR approval gate in the org Actions settings, is still open. It is a forge-UI setting.

2. No docker socket, and no smoke env reachable by fork PRs - CONFIRMED. From the runner's live job-container configuration:

  • docker_host: "-" - no docker host is mounted into job containers, neither automount nor a URL. Job containers get no /var/run/docker.sock.
  • privileged: false - task containers never run privileged.
  • valid_volumes is a closed allowlist of exactly the two named Go-cache volumes the workflows declare (madtea-gomod, madtea-gobuild). A workflow cannot mount arbitrary host paths or a socket even if it asks.
  • Runner-level job env is empty (envs: {}), there is no environment file, and RUN_FORGEJO_SMOKE appears nowhere in the runner or its service configuration. Forge-side, the repo and org Actions variable lists are both empty (verified via the API), so nothing injects it into fork-PR jobs there either.

3. Egress isolation - RE-CONFIRMED, config and behavior. The runner's network interface carries a default-reject egress ACL. Its allow rules are DNS and DHCP to the gateway, plus an explicit public-internet CIDR allowlist. Private ranges are excluded by construction: no LAN, no overlay networks. Behavioral probe from inside the runner: the public forge answers (HTTP 200), and connections toward private-network destinations time out. The known failure mode this re-check exists for, an interface rebuild silently resetting the ACL to allow, has not occurred.

Leaving the issue open on item 1 only.

Runner-side verification done (2026-07-20) for items 2 and 3. Item 1, the fork-PR approval gate in the org Actions settings, is still open. It is a forge-UI setting. **2. No docker socket, and no smoke env reachable by fork PRs - CONFIRMED.** From the runner's live job-container configuration: - `docker_host: "-"` - no docker host is mounted into job containers, neither automount nor a URL. Job containers get no `/var/run/docker.sock`. - `privileged: false` - task containers never run privileged. - `valid_volumes` is a closed allowlist of exactly the two named Go-cache volumes the workflows declare (`madtea-gomod`, `madtea-gobuild`). A workflow cannot mount arbitrary host paths or a socket even if it asks. - Runner-level job env is empty (`envs: {}`), there is no environment file, and `RUN_FORGEJO_SMOKE` appears nowhere in the runner or its service configuration. Forge-side, the repo and org Actions variable lists are both empty (verified via the API), so nothing injects it into fork-PR jobs there either. **3. Egress isolation - RE-CONFIRMED, config and behavior.** The runner's network interface carries a default-reject egress ACL. Its allow rules are DNS and DHCP to the gateway, plus an explicit public-internet CIDR allowlist. Private ranges are excluded by construction: no LAN, no overlay networks. Behavioral probe from inside the runner: the public forge answers (HTTP 200), and connections toward private-network destinations time out. The known failure mode this re-check exists for, an interface rebuild silently resetting the ACL to allow, has not occurred. Leaving the issue open on item 1 only.
Author
Owner

Re-checked against current main now the repo is public.

Code side (in-repo) - confirmed safe:

  • Both workflows trigger on plain pull_request. There is no pull_request_target and no workflow_run, so fork code never runs in a privileged context.
  • No third-party actions at all: uses: appears nowhere. Checkout is an in-container git init plus a shallow anonymous fetch of the PR ref. The repo is public, so it needs no token.
  • The socket-privileged smoke job is gated if: vars.RUN_FORGEJO_SMOKE == 'true' && github.event_name != 'pull_request', so it never queues on a fork PR. The build cache volume-mounts and the smoke job's admin-token provisioning (against a throwaway in-job Forgejo container, not the real forge) are gated off pull_request the same way.

Runner side: the workflows target the shared docker runner. So two of the three original items are now the shared-runner platform's posture rather than repo config:

  1. No host docker socket in job containers - platform-owned, and irrelevant to fork PRs regardless, because the only socket-touching job is gated off pull_request.
  2. Egress isolation - platform-owned on the shared runner.

Remaining item, the one control that is ours:
3. Fork-PR approval gate - confirm the repo's Actions setting requires approval before workflows run from outside or first-time contributors. This is the single most important control, and the last thing to tick before accepting fork PRs.

Everything in the repo source is verified. This stays open on item 3 only.

Re-checked against current `main` now the repo is public. **Code side (in-repo) - confirmed safe:** - Both workflows trigger on plain `pull_request`. There is no `pull_request_target` and no `workflow_run`, so fork code never runs in a privileged context. - No third-party actions at all: `uses:` appears nowhere. Checkout is an in-container `git init` plus a shallow anonymous fetch of the PR ref. The repo is public, so it needs no token. - The socket-privileged smoke job is gated `if: vars.RUN_FORGEJO_SMOKE == 'true' && github.event_name != 'pull_request'`, so it never queues on a fork PR. The build cache volume-mounts and the smoke job's admin-token provisioning (against a throwaway in-job Forgejo container, not the real forge) are gated off `pull_request` the same way. **Runner side:** the workflows target the shared `docker` runner. So two of the three original items are now the shared-runner platform's posture rather than repo config: 1. **No host docker socket in job containers** - platform-owned, and irrelevant to fork PRs regardless, because the only socket-touching job is gated off `pull_request`. 2. **Egress isolation** - platform-owned on the shared runner. **Remaining item, the one control that is ours:** 3. **Fork-PR approval gate** - confirm the repo's Actions setting requires approval before workflows run from outside or first-time contributors. This is the single most important control, and the last thing to tick before accepting fork PRs. Everything in the repo source is verified. This stays open on item 3 only.
hexajon self-assigned this 2026-08-17 17:45:48 +00:00
Author
Owner

Decided: the fork-PR approval gate is not needed. Access is already gated at the account level.

The repository is public to read, but the forge requires authentication for anything beyond read: forking, opening a pull request, and running a workflow all need an account, and I control who has one. There is no anonymous contributor who can trigger a workflow run, so the fork / first-time-contributor approval gate that item 1 asked about is redundant with the access model.

Items 2 and 3 stay as defense-in-depth and are already confirmed: the runner exposes no docker socket, and egress is isolated. In-repo workflow code was verified safe: plain pull_request triggers, no pull_request_target, no third-party uses:.

Posture: account-level access control is the gate; runner isolation contains whatever a trusted contributor's PR runs. No approval-gate toggle required. Clearing needs-decision.

Decided: the fork-PR approval gate is not needed. Access is already gated at the account level. The repository is public to read, but the forge requires authentication for anything beyond read: forking, opening a pull request, and running a workflow all need an account, and I control who has one. There is no anonymous contributor who can trigger a workflow run, so the fork / first-time-contributor approval gate that item 1 asked about is redundant with the access model. Items 2 and 3 stay as defense-in-depth and are already confirmed: the runner exposes no docker socket, and egress is isolated. In-repo workflow code was verified safe: plain `pull_request` triggers, no `pull_request_target`, no third-party `uses:`. Posture: account-level access control is the gate; runner isolation contains whatever a trusted contributor's PR runs. No approval-gate toggle required. Clearing needs-decision.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea#415
No description provided.