Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

#106

P2: Commission an external review of runner isolation and the browser sign-in flow

closed · opened by keenan 10d ago

keenan opened this 10d ago

Why

#14 ends with “external review focused on CI runner isolation and the WebAuthn/session flow”, and it is the one item on that list we cannot close with a change request. It is an engagement with someone outside the project, and what it produces is findings, not a branch.

Two boundaries in Vole are worth an outside pair of eyes precisely because the project cannot test its own assumptions:

  • The CI runner. A job container executes arbitrary repository code. What it can reach depends on how the runner process was started, which the server cannot see, and the trust story in docs/architecture.md and docs/runner.md rests on choices rather than on an isolated test.
  • The browser sign-in flow. Passkeys, sessions, invite tokens, and the ceremony map are the only anonymous endpoints that mutate state, and the recovery paths (lost device, revoked session, restored users.db) are conditions that are hard to exercise honestly from inside.

Scope

This is not a change request. It is:

  • Choose a reviewer with container-escape and WebAuthn experience, and agree the scope in writing: the runner boundary, the sign-in flow, and the documents that describe them.
  • Give the reviewer a way to run the instance: the packaging, a docs/operations.md deployment, and a runner. No production data, no maintainer keys, and no other account’s material.
  • Take the report, and file each finding as its own issue with a severity and a link back here, before writing any fix. Responsible disclosure applies: do not paste exploit detail for an unfixed finding into a public issue.
  • Record what was covered and what was not on this issue, so a later review can start from the edge of this one.

Acceptance

  • A report exists covering both boundaries, or a written statement that one of them was out of scope and why.
  • Every finding is either an issue or an explicit “accepted risk” with the reason recorded.
  • Nothing from the report that could be used against a running instance is in this issue, a comment, a commit, or a release note.
  • This issue closes on triage, not on the fixes: the fixes are their own issues.

Part of #14.

Comments (1)

keenan 2d ago

Internal review completed on 2026-11-06 by the maintainer. This was a code and design review from inside the project, not the external engagement this issue asks for. It did not include container-escape work or testing against real authenticators.

Scope reviewed

  • Runner boundary: the container argument list and mount set, capabilities, user namespaces, network defaults, /tmp, service containers, secret delivery and log masking, the job-tree/cache/saved-checkout split, contributor-tip approval and event-run branch selection, the runner allowlist, run credentials, lease and finish-record validation, artifact and site publication, and the cleanup paths for containers, networks, and caches.
  • Browser sign-in: WebAuthn RP ID and origin, user verification, credential and sign-counter handling, invite token generation/storage/consumption, ceremony storage and keys, session creation/lookup/revocation and cookie flags, CSRF and same-origin checks, security headers, rate limits, open registration, bot accounts, and the loopback dev-login guard.

Controls confirmed

  • Only the job tree and the per-job cache are host mounts; no Docker socket, SSH agent, host environment, or runner key reaches the container. Capabilities are dropped, no-new-privileges is set, the network defaults to none, and the job runs as the runner’s unprivileged uid.
  • Image definitions are pinned by digest and built from a Dockerfile-only context; job fields, paths, image references, service names, environment names, and artifact patterns are revalidated at the runner.
  • Caches and saved checkouts are namespaced by repository and contributor account; the trusted checkout is never mounted, and the job gets a disposable clone.
  • Secrets are written into the job tree as 0600, sourced by each step, never passed in a runtime argument, and masked in the stored log. Services get none.
  • Contributor CI is gated on a maintainer allowing the exact tip; event runs use a trusted branch. Run credentials are bot-only, repo/run-scoped, expire, and are checked by the same role rules as the account’s own key. Finish records are checked against the lease and stored pointers.
  • WebAuthn uses a fixed RP ID and exact origin from public_url, requires user verification, and refuses a sign counter that does not advance. Invite tokens are 256-bit, hashed at rest, single-use, consumed by a conditional update, and carried in the URL fragment rather than the request line.
  • Sessions are 256-bit tokens stored as hashes, in __Host- Secure HttpOnly cookies with SameSite=Lax; CSRF tokens are derived from the session and compared in constant time; writes also check Origin/Referer when present. Revoking a passkey cascades to its sessions, and retirement is checked on every lookup. Open registration cannot grant admin and bots cannot sign in.

Findings filed

  • #209 — sign-in names, passkey labels, and proxy-derived rate-limit keys are stored before being bounded; the limiter and ceremony tables cap entries, not entry size. Availability.
  • #210 — private repository artifacts, archives, and run artifacts are served with a public cache directive, and dynamic pages have no private-cache default, so a shared cache in front of the instance can serve private content to an anonymous reader. Information disclosure when a caching proxy or CDN is used.
  • #211 — the documented runner configuration leaves container memory, pids, and the tmpfs/job tree unbounded, so an approved job or compromised dependency can exhaust the runner host. Availability.

Residual risks recorded, not filed

  • The OCI runtime’s image/build cache is shared across repositories on one runner. Built image tags are digest-scoped, but build layers are not. A separate builder or separate runners is the conservative answer for untrusted repositories.
  • The anonymous JSON endpoints do not require a JSON content type or check Origin. SameSite=Lax, __Host- ceremony/session cookies, the ceremony secret, and WebAuthn user verification carry the protection; a content-type and Origin check would be cheap defense in depth.
  • The ceremony map’s 4,096-entry cap and five-minute TTL bound entries, not bytes. #209 covers the attacker-controlled size; a many-address flood can still deny sign-in for a few minutes.
  • No automated test covers a real authenticator, a hardware token, a kernel or container escape, or a hostile OCI runtime. That is the external review’s job.

Conclusion

No P0 was found. Three findings are filed: two P1 and one P2. I am leaving this issue open because it asks for an external review, which has not happened, and that review remains the right step before accepting untrusted contributor CI on a public runner. If the project accepts that risk for the first deployment, close this on triage and point at #209–#211.