Operations
Repositories
| Repo | Owner | Notes |
|---|---|---|
osfi-gate |
Platform security | This code. App repos run it at a pinned commit. |
osfi-policy |
Second-line risk | The contents of policy/. main is protected with a required CODEOWNERS review from risk (templates/policy-repo/CODEOWNERS). |
| App repos | Engineering | Workflow, waivers/, model-inventory.yaml, CODEOWNERS (templates/app-repo/) |
Onboarding a GitHub repository
- Copy
ci/examples/github-caller.ymlto.github/workflows/osfi.ymland pinuses:andgate-refto one osfi-gate commit. - Add a branch ruleset on
main: - Require a PR with 2 approvals. - Dismiss stale approvals. - Require signed commits. - Block force pushes. - Require theosfi / gatestatus check. - Add a ruleset on
osfi-evidence: only GitHub Actions may push, and deletion and force-push are blocked. - Choose signing:
-
sign: keylessuses the OIDC identity and is logged in Rekor (see the privacy note in audit-trail.md). -sign: keywithcosign-key-ref: awskms://...also needs AWS credentials in the job. Add an OIDC role step before publishing. - Add
OSFI_POLICY_TOKEN, a read-only token for the policy repo. - Code-scanning upload requires GitHub Advanced Security on private repos. Without it, the step is skipped and the summary still appears in the job.
Onboarding a Gitea repository
- Copy
ci/gitea/osfi-gate.ymlto.gitea/workflows/osfi-gate.yml. - Set these variables:
OSFI_GATE_REPO,OSFI_GATE_REF(a commit SHA),OSFI_POLICY_REPOandOSFI_POLICY_REF. Optionally setOSFI_SIGNandOSFI_COSIGN_KEY_REF. - Set these secrets:
| Secret | Purpose |
|---|---|
OSFI_POLICY_TOKEN |
Reads the gate and policy repos |
OSFI_API_TOKEN |
Reads repos and writes issue comments. Repo admin rights let it read the full branch-protection rule; without them the gate reads the branch summary. |
COSIGN_PRIVATE_KEY, COSIGN_PASSWORD |
Signing, or use a KMS URI in OSFI_COSIGN_KEY_REF |
- Protect
main: - Required approvals: 2. - Dismiss stale approvals, require signed commits, disable push. - Add the gate as a required status check. - If signed commits are required, configure
[repository.signing]on the instance. Otherwise Gitea refuses merges with "wont sign: nokey". - UseMERGES = pubkey, commitssignedunless you enforce 2FA. - Fast-forward merges keep the author's signed commits, but see the schedule caveat below: delete the source branch on merge. - Protect
osfi-evidence: allow push only for the Actions user, and disable force-push. - Runner requirements:
- The runner needs Docker and outbound access to the image registries, github.com (actions, conftest, cosign) and PyPI.
- Gitea older than 1.25 rejects
upload-artifact@v4, which is why the workflow pins v3.2.1-node20. - Gitea has no SARIF viewer. The gate summary is posted as a single PR comment and updated in place.
Waivers
See templates/app-repo/waivers/README.md. The workflow for a waiver:
- Engineer: copies the
fp=value from the gate output and opens a PR addingwaivers/<id>.yaml. - Risk: a member reviews and approves the PR. CODEOWNERS enforces this on the platform, and WV-01 enforces it in the gate.
- Expiry: the waiver applies until 00:00 UTC on its expiry date. The gate warns 14 days before, and after that the finding blocks again.
Changing policy
- Merging: policy changes merge only in the policy repo, with risk approval. App repos pick them up through
OSFI_POLICY_REF/policy-ref. Use a SHA for strict change control ormainfor immediate effect. - Record: each bundle records the policy commit and the content digest, and replay uses whichever policy produced the decision.
- Before merging: run
conftest verify --policy regoandosfi-gate catalog.
Refreshing Semgrep registry rules
scripts/vendor-semgrep-rules.sh policy # or: policy p/default p/owasp-top-ten
git -C policy diff --stat # review, then open a policy PR
Run the fixtures before merging. A rule that was added or changed upstream can produce new findings, or change an existing finding's fingerprint, which would invalidate its waiver.
Scheduled rescans
Both workflows rescan the default branch daily at 06:17 UTC. A scheduled run:
- Checks: re-evaluates every finding against current advisories, scans the default branch's full history for secrets, and checks branch protection (CM-03).
- Unmerged branches: these aren't rescanned. Their commits were checked when the PR was opened. Delete the branch of any closed PR that leaked a secret, and rotate the secret regardless.
- Skips: the change controls, since there is no change to approve.
- Records: writes a bundle like any other run.
Gitea caveat: a scheduled run uses the workflow file from the commit that registered the schedule. Gitea 1.24.7 re-registers schedules on a push to the default branch, but only if git name-rev resolves the pushed commit to that branch (services/actions/notifier_helper.go, handleSchedules).
- When it goes wrong: after a fast-forward merge where the feature branch still exists,
name-revcan return the feature branch. Gitea then silently keeps the old schedule, so scheduled runs use an outdated workflow, or no schedule is registered at all. - How to avoid it: merge workflow changes with a merge commit, or delete the source branch on merge.
- How to check: the SHA of the next scheduled run should be the new commit.
A failing scheduled run is the signal that something on main has fallen out of SLA. Route it to the owning team, for example with a notification on workflow failure.
Turning on AI usage controls
- Discover first. Run the gate as-is. Undeclared AI usage that already exists is reported as a warning and recorded in the evidence bundle, so the first run tells you what is out there without blocking anyone.
- Register what you keep. Add an
ai_usage:entry per call site to each repository'smodel-inventory.yaml(see docs/controls.md), with the model-risk ID and validation record. - Fix or waive the rest. A provider outside the approved hosting classes fails MR-03. Either move the call to an approved provider or have risk approve a waiver with an expiry, like any other finding.
- Tighten. Once a repository is clean, set
gate.ai.enforce: allin the policy repo so pre-existing usage blocks too. - Keep the lists current.
gate.ai.providers,approved_hosting,floating_model_patternsanddependency_packagesare risk-owned. Review them when a provider publishes new aliases or regions.
Upgrading scanner or action pins
- Pull the new image and read its digest:
docker inspect --format '{{index .RepoDigests 0}}'. - Update
osfi_gate/tools.pyand both workflow files. Then runosfi-gate check-pins .github/workflows/*.yml ci/gitea/osfi-gate.yml. - Re-run the fixtures (
osfi-gate run ...) and refreshtests/data/raw-*if the output shape changed. Watch for changed rule IDs, since they change fingerprints and therefore waivers. - To change Python dependencies, regenerate
ci/requirements.lockwithpip-compile --generate-hashesunder Python 3.14, the version CI uses.
When the gate fails
- Where to look: the step summary (GitHub) or PR comment (Gitea) lists each violation with its control, tool, rule, location and fingerprint.
- EV-01: a scanner or the API failed. Re-run. If it persists, check the scanner step log. The gate never passes on missing evidence.
- "Trivy vulnerability DB is N days old": the runner is using a cached or mirrored DB that stopped updating.
- History scan failed: the checkout needs
fetch-depth: 0. - SC-01 from commit history: removing the secret in a later commit isn't enough. 1. Rotate the credential. 2. Then either rewrite the branch history before merging, or add a waiver that references the rotation ticket.
- CM-01 after approvals: approvals on an earlier commit are stale. Ask for a re-review of the head commit. On GitHub, the
pull_request_reviewtrigger re-runs the gate automatically. - Evidence publish failed: the job fails, but the bundle is still uploaded as a CI artifact. Re-run the job to record it in the ledger.
Enrolment across the estate
Every other view in this system starts from the enrolment list, so none of them can see a
repository nobody enrolled. osfi-console enrolment asks the forge instead:
osfi-console --config console.yaml enrolment # exits 1 when something is unenrolled
osfi-console --config console.yaml enrolment --deep # also: does it even have the workflow?
Configure the organisations to enumerate:
organisations:
- platform: github
api_url: https://git.internal/api/v3
name: northwind
token: ${FORGE_READ_TOKEN}
identity: svc-osfi@northwind # whose visibility produced the answer
The count is bounded by what the token can see, and the boundary travels with it into
coverage.not_enrolled, the health page and the regulator pack's README. A repository in an
organisation nobody listed, or one this token cannot see, is exactly as invisible as it was
before — and an organisation that could not be read is excluded from the count entirely rather
than being treated as complete, because shrinking the denominator would make coverage look
better than it is.
On GitHub, org rulesets or required workflows can enforce enrolment directly and this becomes a cross-check. Gitea has no equivalent, which is why a scheduled job is the answer there.
The policy a decision was made under
osfi-gate bundle refuses to write a bundle when the policy directory has uncommitted
changes. Verification finds the policy for replay either in the working tree or at the commit
the bundle recorded; an uncommitted policy is in neither once the runner is destroyed, so such a
bundle is permanently unverifiable — and until now the only thing that said so was a line on
stderr in a CI log.
--allow-dirty-policy exists for evidence that is genuinely not meant to be verified, and takes
saying so.
This is also what makes splitting the policy into its own repository worth doing: application repositories pin a commit, that commit is recorded in every bundle, and a decision made under anything else does not produce evidence at all.
Running the gate from an image
CI checks out osfi-gate's source and pip-installs it from a hash-locked requirements file. That works, and it means every run resolves an artefact rather than fetching one that was built, reviewed and signed.
ci/build-image.sh registry.internal/osfi-gate --push --sign # COSIGN_KEY=file-or-kms-uri
It prints the digest and refuses to build from an uncommitted tree — the same rule osfi-gate
bundle enforces, for the same reason: an artefact stamped with a commit that does not describe
it is worse than one stamped with nothing.
The tag is a convenience; the digest is the identity. A tag can be moved, and one whose
contents change between builds is worse than no tag at all. Pass the digest as gate-image and
every bundle the run produces records it, so an examiner can tell which build decided a run
rather than inferring it from a version string. The image bakes its own source commit for the
same reason: a container has no .git, and the provenance a checkout gives for free would
otherwise become null the moment CI moved to the image.
Switching the job to execute from the image — rather than merely recording it — means running the gate steps in a container that also needs the Docker socket for the scanners. That is a change to your runner topology, so it is yours to make; the image, its identity and the pinning check are here.
osfi-gate check-pins now fails on any image a workflow references that is not pinned by
digest, not only the scanners whose digests it knows. It cannot know the digest of an image you
publish, but "whatever you reference is pinned" is enforceable without it.
Access review
Both halves are commands, not a checklist — see privacy.md:
FORGE_TOKEN=... osfi-gate access-review --platform github --api-url ... \
--repo owner/name --expect osfi-ci # exit 1 finding, 2 could-not-see
curl -s .../api/access-review # who can read bundles, and who has
The onboarding step that says "protect osfi-evidence" is now checkable, which is the point:
it was a sentence somebody followed or did not, with no artefact recording which.
Controlled egress and internal mirrors
osfi-gate egress # or --json
The list is generated from the pins in osfi_gate/tools.py, so it cannot drift from them the
way a hand-written allowlist does the first time somebody bumps a scanner.
| Setting | Redirects |
|---|---|
OSFI_REGISTRY_MIRROR=reg.internal/proxy |
Every scanner image. The digest is preserved exactly — a mirror changes where the bytes come from, never which bytes they are, so the evidence records the same images and a mirror serving different content fails to pull rather than substituting quietly. |
OSFI_TRIVY_DB_REPOSITORY |
Trivy's vulnerability database, which it fetches at run time and which is therefore not one of the pinned images. OSFI_TRIVY_JAVA_DB_REPOSITORY for the Java database. |
Two things no setting here reaches, and they are the honest limits of this:
- Workflow actions are pinned by commit SHA but are fetched from the forge. Redirecting them is a forge-level concern.
- Dedicated ephemeral runners are a property of your runner fleet. What this repository can do is make the gate's own dependencies enumerable and pinned, which is the above.