docs: correct Gitea Actions guarantees
CI / verify (pull_request) Successful in 29s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Failing after 39s

The security document claimed the CI workflow "is least privilege" because it
declares `permissions: contents: read`. That is not true on the server this
repository actually uses.

The inventoried server is Gitea 1.24.2, and its official comparison page lists
`permissions` / `jobs.<job_id>.permissions`, `concurrency`, and
`jobs.<job_id>.timeout-minutes` under unsupported workflow syntax — verified
against https://docs.gitea.com/1.24/usage/actions/comparison rather than assumed.
On this server those keys are inert, so the branch cannot claim the in-job
GITEA_TOKEN is read-only, and cannot claim YAML-enforced timeout or concurrency
behavior. Job-token permission controls, repository/owner defaults, and
server-side maximum-permission clamping arrive in the 1.26 feature line
(https://docs.gitea.com/1.26/usage/actions/token-permissions).

The document now separates what is actually true of this repository — full-SHA
action pins, frozen-lockfile install, no pull_request_target, no referenced
secret, `persist-credentials: false` on every checkout, and the declared
`permissions` block — from what the server enforces. It also states precisely
what `persist-credentials: false` proves: checkout leaves no credential in
`.git/config` for a later step, which is not the same as removing or reducing
the job token. The CI blockers are recorded: upgrade to a reviewed supported
release, configure repository/owner defaults and a maximum permission, prove
against the real server what the token cannot do, move timeout/cancellation/
teardown into the trusted provisioner and reaper, and finish the disposable-guest
proof before opening the real pull request.

The workflow keeps every declaration — they are already correct for a supported
Gitea release and for GitHub Actions — and gains a comment saying plainly that
this server ignores them, so no future reader mistakes them for enforcement.

Nothing about runners, proxies, firewalls, or deployment is asserted here.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-07-28 20:10:20 -05:00
co-authored by Claude Opus 5
parent cda841f8c7
commit a2db774b8c
2 changed files with 81 additions and 11 deletions
+64 -11
View File
@@ -140,15 +140,68 @@ Ekklesia does not ship one. Do not describe the current limiter as distributed t
## Continuous integration boundary
CI runs on self-hosted Gitea Actions runners. The workflow itself is least privilege: top-level
`permissions: contents: read`, both actions pinned by full commit SHA, `pnpm install
--frozen-lockfile`, no `pull_request_target`, no secrets referenced, and checkout configured with
`persist-credentials: false` so the job token is not left in `.git/config` for a later step to read.
CI runs on self-hosted Gitea Actions runners. The inventoried server is **Gitea 1.24.2**, and that
version determines which parts of the workflow are real guarantees and which are not.
**The runner topology itself is not verified in this repository and is not documented here as
though it were.** A pull request can edit `runs-on` and select any runner label its repository can
reach, so a workflow file cannot be the boundary. Whether untrusted pull-request jobs execute on a
disposable guest, and whether any runner reachable from this repository holds deployment authority
or a host Docker socket, must be established against the actual Gitea instance and runner hosts.
Until that evidence exists, treat pull-request CI as executing with whatever authority the runner
host actually grants.
### What is true of the repository today
These are properties of files in this repository, enforced by the action or the package manager,
independent of server version:
- every action reference is pinned by a full immutable commit SHA;
- dependencies install with `pnpm install --frozen-lockfile`;
- there is no `pull_request_target` workflow;
- no repository, organization, or instance secret is referenced anywhere in the workflow;
- every checkout step sets `persist-credentials: false`;
- the workflow declares `permissions: contents: read`.
### What `persist-credentials: false` actually proves
It proves exactly one thing: `actions/checkout` does not write its credential into `.git/config`,
so a later step in the same job cannot read or reuse it from there.
It does **not** remove the short-lived `GITEA_TOKEN` from the job environment, and it does **not**
reduce that token's server-side authority. A step that wants the token can still read it.
### What Gitea 1.24 ignores — so the branch cannot claim it
Per the official comparison for this version
(<https://docs.gitea.com/1.24/usage/actions/comparison>), Gitea 1.24's unsupported workflow syntax
includes `permissions` and `jobs.<job_id>.permissions`, `concurrency`, and
`jobs.<job_id>.timeout-minutes`. On this server those declarations are inert. Therefore:
- **The workflow's `permissions: contents: read` does not restrict the job token.** This branch
cannot claim the `GITEA_TOKEN` is read-only, because nothing in workflow YAML makes it so on
1.24. Job-token permission controls, repository/owner defaults, and server-side
maximum-permission clamping arrive in the 1.26 feature line
(<https://docs.gitea.com/1.26/usage/actions/token-permissions>).
- **`concurrency` and `timeout-minutes` are not bounded-execution guarantees here.** Cancellation,
timeout, and teardown must be enforced by a trusted provisioner and reaper outside the workflow.
The declarations are kept deliberately: they are already correct for a supported Gitea release and
for GitHub Actions, and keeping them means the upgrade does not require re-deriving the intent. They
are documented as ignored, not removed.
### Runner topology is not verified
**The runner topology is not verified in this repository and is not documented here as though it
were.** A pull request can edit `runs-on` and select any runner label its repository can reach, so a
workflow file cannot be the boundary. Whether untrusted pull-request jobs execute on a disposable
guest, and whether any runner reachable from this repository holds deployment authority or a host
Docker socket, must be established against the actual Gitea instance and runner hosts. Until that
evidence exists, treat pull-request CI as executing with whatever authority the runner host actually
grants.
### CI blockers before the real pull request is opened
1. Upgrade Gitea to a reviewed supported release with job-token permission controls — at least the
1.26 feature line. Select the actual release during infrastructure work, after backup and
compatibility review; do not pin a version from this document.
2. Configure repository and owner defaults, and the server-side maximum permission, so workflow
code cannot request more than read-only repository access.
3. Prove against the real server that the job's `GITEA_TOKEN` cannot push code, create or update
releases, packages, issues, or pull requests, change Actions configuration, or reach another
private repository.
4. Enforce job timeout, cancellation, concurrency, and teardown in the trusted
provisioner/reaper rather than relying on the ignored workflow keys above.
5. Complete the disposable-guest runner isolation proof before opening the real pull request.