Skip to main content

Push protection

Every push to a fremforge repo runs through a Gitleaks pre-receive hook on the Forgejo server. If the scanner finds a credential, the push is rejected and the developer sees the matched rule (no value, no commit SHA leaked back to a third party).

This is distinct from SAST findings and container-image scanning: push protection runs at git-push time, before the commits are stored anywhere fremforge has to defend. The other scanners run later on the stored result.

What gets matched

Gitleaks’ default rule set plus fremforge additions:

  • Cloud provider keys (AWS, GCP service account JSON, Azure SAS tokens, T Cloud Public AK/SK)
  • Common SaaS API keys (Stripe, Mollie, SendGrid, Twilio, Lettermint, OpenAI)
  • Generic high-entropy patterns (PEM private keys, JWT, SSH private keys)
  • fremforge-specific: personal access tokens (ffp_*), OAuth client IDs (ffc_*) + secrets (ffs_*), runner OIDC private keys, OBS bucket bootstrap creds, ack-signing HMAC secrets

The Gitleaks signature pack is pinned, reviewed when bumped, and shipped via the same SWR mirror as our other dev-tooling. We don’t pull from GitHub’s content registry at push time.

Your organisation can add its own patterns too. See Custom patterns. They are applied at push in the same way, and a rejection names the pattern’s rule id, custom-<name>.

What the developer sees

When a push hits a matched secret, the developer sees:

remote: Push rejected by fremforge push-protection.
remote:
remote: Matched rule: aws_access_key_id
remote: Path:         apps/api/src/services/uploader.ts:42
remote: Commit:       <commit-sha>
remote:
remote: If this is a false positive, ask an org owner to add an
remote: override at frem.sh/<your-org>/_admin/push-protection.
remote: Override IDs are auditable and time-boxed.

No credential value is logged or echoed. The developer has the source open already; they can find the false positive themselves.

Always on, and the one setting

Push protection cannot be turned off for an organisation or a repository. The one setting is the scanner failure mode, under Security › Policies › Push protection. It decides what happens to a push when the scanner itself is unavailable:

  • Block (the default): the push is rejected with a clear error, and you retry after the scanner recovers.
  • Allow: the push goes through and an allow-on-error event is recorded in the audit trail. Secrets pushed in that window are found later by the weekly full-history secret scan.

The change takes effect on the next push and is recorded in the audit log.

Overrides

Tenant admins can add overrides for cases the scanner gets wrong (test fixtures, sample SDK snippets, documentation that intentionally shows a key shape).

  1. Tenant admin → Security › Policies › Push protection. Expand Active overrides and click Add override. From a rejected push, Approve fills the form from that rejection.
  2. Pick what it covers: just this commit (a rule in one pushed commit), just this rule (every match of one rule), just this path (a path glob such as test/fixtures/**), or a custom combination. Every field you set must match; a field you leave empty matches anything. At least one of commit, rule and path is required.
  3. Optionally limit it to one repository; otherwise it applies to every repository in the organisation.
  4. Set the expiry date. Every override ends: the date defaults to 90 days from today and can be at most 1 year away. An expired or revoked override is no longer honoured, and you add a new one if it is still needed.
  5. Give a reason (required, audited).

How a push is judged. The pre-receive hook collects every secret it finds in the commits a push introduces (rule, path and line; never the value) and asks fremforge whether each one is covered by an active override. The push goes through only if every finding is covered; one uncovered finding rejects it as before. When an override lets a push through, the developer sees which override(s) applied, and push-protection.override.used is written to your organisation’s audit log. If fremforge cannot be asked (for example during an outage), the push is rejected: overrides never fail open. A commit override covers the pushed commit (the tip of the branch), which is the commit a rejection names. A secret inside an archive is matched at the archive’s path.

Overrides from before the limit. Overrides created before 2026-10-18 could be left without an expiry. They keep working as they are, and the console and the audit log show them as no expiry (created before the limit). Revoke and re-create one to give it an expiry.

Overrides apply to push protection only. A secret already in history that the weekly sweep lists is handled by dismissing the finding, not by an override.

Via REST API

Push-protection overrides, rejections, and failure-mode can be managed programmatically — useful when scripting tenant onboarding, integrating with a CI pipeline, or driving from an AI agent.

  • List overrides GET /_app/api/v1/orgs/:slug/push-protection/overrides
  • Create override POST /_app/api/v1/orgs/:slug/push-protection/overrides body: { "kind": "path", "path_pattern": "test/fixtures/**", "reason": "test fixtures", "expires_at": "2027-01-01T00:00:00Z" }
  • Revoke override DELETE /_app/api/v1/orgs/:slug/push-protection/overrides/:id
  • Set failure mode PUT /_app/api/v1/orgs/:slug/push-protection/failure-mode body: { "mode": "block" }

Without expires_at, a new override expires 90 days after it is created; the response carries the date. expires_at (an ISO 8601 date or timestamp) must be in the future and at most 1 year away. A later date is refused with 400 expires-at-too-far, not shortened. In the list, no_expiry_legacy: true marks an override created before the limit, the only kind with expires_at: null.

Creating or revoking an override needs a token with policy:write, because an override exempts pushes from secret scanning; a CI job’s runner token never carries it. Setting the failure mode needs policy:write too, because it can turn push protection off for the whole organisation. Listing needs findings:read, and dismissing a rejection needs findings:write.

curl -X POST \
  -H "Authorization: Bearer ${FREMFORGE_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"kind":"path","path_pattern":"test/fixtures/**","reason":"test fixtures","expires_at":"2027-01-01T00:00:00Z"}' \
  https://frem.sh/_app/api/v1/orgs/acme/push-protection/overrides

See the public REST API reference for the full OpenAPI specification.

Bypass for already-leaked credentials

A push-protection block is not the same as the credential being safe. By the time you hit the block, you have a credential sitting in a commit on your laptop. Rotate the credential before doing anything else. Once rotated, you can rebase the commit out of the branch or push with an override; either path works once the credential is dead.

What it doesn’t do

  • It does not scan history backwards. If a credential was committed before push protection was enabled, it stays committed. Use a history-rewrite tool (the migration guide walks through this).
  • It does not catch every possible credential shape. Custom rules per tenant cover the gaps that matter for your stack.
  • It does not run on git push --force of a branch that’s already known to fremforge (the pre-receive hook still runs, but the comparison is against the new tip; a force-push that overwrites a leaked commit with a clean one is allowed).