diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9d8f73..03c8a9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,22 @@ name: CI +# IMPORTANT — the currently inventoried server is Gitea 1.24.2, which *ignores* several keys used +# below. Per https://docs.gitea.com/1.24/usage/actions/comparison, its unsupported workflow syntax +# includes `permissions` / `jobs..permissions`, `concurrency`, and +# `jobs..timeout-minutes`. They are declared here deliberately, so the workflow is already +# correct on a supported release and on GitHub Actions, but on this server they are inert: +# +# * `permissions:` does NOT restrict the in-job `GITEA_TOKEN`. 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). Until the server +# is upgraded and configured, the token's authority is whatever the server grants — not what +# this file requests. +# * `concurrency:` and `timeout-minutes:` are NOT bounded-execution guarantees here. Cancellation, +# timeout, and teardown must be enforced by the trusted runner provisioner/reaper. +# +# `persist-credentials: false` below is unaffected by any of this and is enforced by the action +# itself. See docs/development/security.md. + on: pull_request: push: diff --git a/docs/development/security.md b/docs/development/security.md index 46e9c3d..ec18074 100644 --- a/docs/development/security.md +++ b/docs/development/security.md @@ -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 +(), Gitea 1.24's unsupported workflow syntax +includes `permissions` and `jobs..permissions`, `concurrency`, and +`jobs..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 + (). +- **`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.