Security and supply chain
fremforge ships a layered supply-chain security stack included in every seat plan. Controls run at four choke points: the push (secret scanning), the PR (dependency scanning, SAST), the artifact (container scanning, SBOM, SLSA provenance), and the commit (SSH-key or GPG signing).
For the contractual posture (BSI C5, ISO 27001/27017/27018, TISAX, GDPR, the Schrems II / CLOUD Act analysis) see frem.sh/trust.
Secret scanning (push protection)
Gitleaks runs as a pre-receive hook on every push, before the commit is accepted into the repository. Push protection is a platform floor: it cannot be disabled by any tenant or repo owner.
What gets blocked
Gitleaks matches 120+ high-confidence patterns including:
| Category | Example patterns |
|---|---|
| Cloud provider credentials | AWS access/secret keys, GCP service account JSON, Azure storage connection strings |
| Source control tokens | GitHub PATs, GitLab PATs, Bitbucket app passwords |
| Payment keys | Stripe secret keys, Stripe webhook signing secrets |
| Communication tokens | Slack bot tokens, Slack webhook URLs, Twilio auth tokens |
| Private keys | RSA/EC/Ed25519 private keys, PEM-encoded certificates |
| Generic secrets | High-entropy strings matching the generic-api-key pattern, JWT signing secrets |
| Database URLs | PostgreSQL, MySQL, MongoDB, Redis connection strings with embedded credentials |
When a push is blocked
The push is rejected at the remote with an error message that includes the matched pattern type and the file + line number. The commit is not accepted.
Do not just delete the file and re-commit. The secret is in git history and will be blocked again. The correct remediation:
- Rotate the secret immediately at the issuing service (AWS IAM, Stripe dashboard, etc.). Assume it is compromised.
- Rewrite history to remove the secret from every affected commit:
# Install git-filter-repo (https://github.com/newren/git-filter-repo)
pip install git-filter-repo
# Create a replacements file
echo '<secret-value>==>REDACTED' > replacements.txt
# Rewrite history
git filter-repo --replace-text replacements.txt
# Force-push the cleaned branch
git push --force-with-lease origin <branch>- Notify anyone who may have cloned the repo before the rewrite.
Override flow
If a blocked secret is intentional (internal tooling token, published test credential), an org owner can add a scoped override:
- Go to Org admin → Push protection → Active overrides → New override.
- Specify: Repository (or select “All repos”), secret pattern (from the Gitleaks rule ID), and justification (free text, logged in the audit trail).
- Save. The override takes effect on the next push.
All overrides are visible to other org owners and appear in the audit log with actor, timestamp, and justification. Overrides can be revoked at any time from the same page.
Overriding a push rejection
When a push is rejected by the secret scanning gate, the developer sees the rejection reason in the terminal output, the matched pattern type, the file path, and the line number.
An org owner can approve an override at Org admin → Push protection → Recent rejections. Each rejection entry shows: the repository, the committer, the timestamp, the secret type detected, and the affected file path.
Override scopes (select when approving):
- This commit only, approves the specific commit SHA. The same secret in a future commit will be rejected again.
- This file path, approves the secret at that path in any future commit. Use for test fixtures or deliberately non-sensitive placeholder values.
- This repository, approves any occurrence of this secret type in the repository. Use sparingly.
- Org-wide, approves the secret type across all repositories. Only for declared false positives (e.g., a custom token format that matches a rule pattern but is not a real secret).
Override reason is required (free text). It is written to the audit log.
The developer must re-push after an override is granted. The override does not retroactively accept the rejected push.
Custom allowlist
To silence known false positives in a specific repository (test fixtures, example keys in documentation, intentional public credentials), add a .gitleaks.toml at the repo root:
[extend]
useDefault = true
[[allowlists]]
description = "Test fixture AWS keys"
paths = ["tests/fixtures/.*", "docs/examples/.*"]
[[allowlists]]
description = "Example key in README"
regexes = ["AKIAIOSFODNN7EXAMPLE"]The allowlist is evaluated per-repo during the pre-receive hook. It does not override org-level push protection for patterns not in the allowlist.
Dependency scanning
Renovate (hosted, per-tenant bot user) raises PRs when dependencies have published CVEs above the configured severity threshold.
Enable and configure
Enable at Org admin → Dependency updates → Enable hosted Renovate. Once enabled, Renovate runs on a weekly security cadence (daily for CRITICAL-severity CVEs) and raises PRs against every repository in the org.
Per-repo configuration lives in renovate.json at the repo root. The full schema is at docs.renovatebot.com. fremforge runs upstream Renovate unmodified, so every config option applies.
CVE policy and severity thresholds
Set the org-wide threshold at Org admin → Dependency updates → CVE policy:
| Threshold | Behavior |
|---|---|
| LOW | PRs for all CVE-affected versions, including informational findings |
| MEDIUM | PRs for MEDIUM, HIGH, and CRITICAL CVEs |
| HIGH (default) | PRs for HIGH and CRITICAL CVEs only |
| CRITICAL | PRs for CRITICAL CVEs only |
Merge-block policy
By default, Renovate opens PRs for dependency updates but does not block merges on unpatched vulnerabilities.
To enable merge-blocking: Org admin → Code security → Dependency scanning → Merge-block policy → Enable.
Once enabled, any PR that introduces or retains a dependency with a CVSS score ≥ 7.0 (HIGH or CRITICAL) is blocked from merging until either: (a) the dependency is updated to a patched version, or (b) an org owner approves an exception at Org admin → Code security → Dependency scanning → Merge-block exceptions.
Exception approval requires a reason (written to the audit log). Exceptions expire after 30 days and must be renewed.
The merge-block appears as a required status check on the PR. The check links to the finding detail showing the CVE ID, affected package, fixed version, and CVSS score.
False positive path: if a finding is incorrect (e.g., the CVSS database has wrong version metadata), open a support ticket at support@frem.sh with the CVE ID and affected package. Confirmed false positives are suppressed at the platform level within one business day.
Supported manifests
| Ecosystem | Files |
|---|---|
| JavaScript / Node.js | package.json, package-lock.json, yarn.lock, pnpm-lock.yaml |
| Go | go.mod, go.sum |
| Rust | Cargo.toml, Cargo.lock |
| Python | requirements.txt, Pipfile, pyproject.toml, poetry.lock |
| Ruby | Gemfile, Gemfile.lock |
| Java | pom.xml, build.gradle, build.gradle.kts |
| Containers | Dockerfile, docker-compose.yml, docker-compose.yaml |
| CI workflows | .forgejo/workflows/*.yaml (action version pinning) |
Container image scanning
Trivy scans every OCI image pushed to the fremforge package registry. Results appear at Org admin → Code security → Container images.
Each finding includes:
| Field | Description |
|---|---|
| CVE ID | e.g. CVE-2024-12345 with link to NVD |
| Severity | CRITICAL / HIGH / MEDIUM / LOW / UNKNOWN |
| Affected package | Package name and installed version |
| Fix version | First version that resolves the CVE, if available |
| EPSS score | Exploit Prediction Scoring System probability |
CI pipeline scanning
Enable the Trivy CI workflow template at Org admin → Code security → Container images → Enable pipeline scanning. This provisions a image-scan-trivy.yaml workflow in your org’s .forgejo/workflows/ default template set:
name: Image scan
on:
push:
branches: [main]
pull_request:
jobs:
trivy:
runs-on: fremforge
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t ${{ github.repository }}:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ github.repository }}:${{ github.sha }}
format: table
exit-code: "1"
severity: HIGH,CRITICALAdjust severity to match your org CVE policy threshold.
SBOM generation
Syft generates CycloneDX 1.5 and SPDX 2.3 SBOMs for every container image pushed to the registry and for every release tag. SBOM generation is on by default for every org, there is no enable step.
Download
From the package registry UI: open the image → Attestations tab → Download SBOM.
Via API:
curl -H "Authorization: token <your-pat>" \
https://frem.sh/_app/api/v1/orgs/<org>/findings/attestations \
-o sbom.jsonThe SBOM is CycloneDX JSON and includes all transitive dependencies with their PURL identifiers, license expressions, and version ranges.
SAST
OpenGrep runs static analysis on every PR. SAST is on by default for every org, there is no enable step. Findings are managed at Org admin → Code security → SAST findings.
Supported languages
Python, JavaScript, TypeScript, Go, Java, Ruby, PHP, C, C++.
Findings
Findings appear as PR check annotations (inline on the diff) and are aggregated at Org admin → Code security → SAST findings for org-level review. The severity floor (block PR on ERROR vs. WARNING) is configurable per org.
To use a custom ruleset, set Custom rules URL under the SAST settings to any URL that serves a valid OpenGrep rules YAML file.
Signed commits
fremforge supports two commit-signing paths, both verified natively by Forgejo’s “Verified” badge in the web UI:
- SSH-key signing (recommended default), reuses the SSH key you already have registered for
git push. No new key material to manage, no third-party sub-processor, no transparency-log dependency, no US-jurisdiction transit. This is the canonical fremforge path. - GPG signing, classical OpenPGP key. Use this if your organisation already has a GPG key-management policy in place (smartcard, HSM, central keyserver).
Gitsign / Sigstore is not enabled at fremforge. The public Linux Foundation Sigstore instance routes signing requests through US-jurisdiction infrastructure (Fulcio CA + Rekor transparency log), which conflicts with fremforge’s no-US-sub-processor posture. A self-hosted Sigstore stack on T Cloud is on the Phase-2 customer-demand-gated roadmap. Customers who need keyless OIDC signing today should treat that as a roadmap item to coordinate with us, not a default-on feature.
SSH-key signing, configure
# Tell Git to use SSH for signing (Git ≥ 2.34)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# (Optional) point Git at your local allowedSignersFile for verify
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signersAdd the matching SSH key to your fremforge user profile under Settings → SSH and GPG keys → Add signing key (Forgejo accepts the same key for both push and signing, or a separate signing-only key).
On the next git commit, Git signs with your SSH key. Forgejo verifies the signature against the org’s allowedSignersFile (auto-derived from members’ registered signing keys) and renders the “Verified” badge in the web UI.
Verify
git log --show-signature HEAD~5..HEADEach commit shows a Good "git" signature for <user>@<org> line. The Forgejo web UI shows a green “Verified” badge next to the commit hash.
GPG signing, configure
If you prefer GPG, the standard git config --global commit.gpgsign true + user.signingkey <gpg-key-id> flow works unchanged; upload your public key under Settings → SSH and GPG keys → Add GPG key. Forgejo verifies and renders the “Verified” badge.
Require signed commits (org policy)
Enforce signed commits on protected branches via Org admin → Repo defaults → Branch protection → Require signed commits (applies to new repos), or per-repo at Repository → Settings → Branches → <branch> → Require signed commits. Unsigned commits are rejected at push time. Both SSH-key and GPG signatures satisfy the requirement.
SLSA provenance
Build provenance attestations are generated for every artifact built via a fremforge hosted runner using the slsa-provenance.yaml workflow template (installed on every tenant repo by default; opt out per repo via the admin UI). Each artifact gets an in-toto Statement wrapped in a DSSE envelope, signed by the fremforge platform builder key, persisted to your per-tenant attestation OBS bucket, and surfaced in the admin UI under Security → Code security → Attestations with a copy-paste verification command line.
EU-sovereign by design. The signing key, the trust root, and the storage all live inside the fremforge T Cloud account (eu-de). There is no Sigstore Fulcio cert chain, no Rekor transparency log, no tuf-repo-cdn.sigstore.dev lookup in the verification path. Customers point the standard slsa-verifier binary at fremforge’s published trust root.
Trust root: https://www.frem.sh/.well-known/slsa-trust-root.json, a small JSON file listing the active builder public key(s). Pin the file (or a copy of it) in your CI to lock-in trust over the verification window. Key rotations are announced 30 days in advance per the trust page §Sub-processors notification rule.
Attestation contents (predicate shape):
| Field | Value |
|---|---|
predicateType | https://slsa.dev/provenance/v1 |
buildDefinition.buildType | https://frem.sh/buildtypes/forgejo-actions/v1 |
buildDefinition.externalParameters.repo | <org>/<repo> |
buildDefinition.externalParameters.ref | The git ref (e.g. refs/tags/v1.2.3) |
buildDefinition.resolvedDependencies[0] | git+https://frem.sh/<org>/<repo>@<commit-sha> |
runDetails.builder.id | https://frem.sh/runner-controller/v1 (server-controlled, a compromised runner cannot forge this) |
runDetails.metadata.invocationId | Forgejo Actions workflow-run ID |
subject[].digest.sha256 | SHA-256 of the artifact being attested |
SLSA level: Build L2, hosted isolated build platform (CCI Kata micro-VM + Yangtse v2 per-namespace VPC ENI; see §Kernel isolation model), signed provenance, server-controlled builder identity. L3 (hermetic builds, reproducible artifacts, full external transparency log) is on the supply-chain roadmap, triggered by a regulated-industry RFP, not at launch.
Verify
Install slsa-verifier (Google-built static Go binary, no network calls at verify time; reproducible builds, release page):
# macOS
brew install slsa-verifier
# Linux x86_64
curl -sSfL -o slsa-verifier https://github.com/slsa-framework/slsa-verifier/releases/latest/download/slsa-verifier-linux-amd64
chmod +x slsa-verifier && sudo mv slsa-verifier /usr/local/bin/Pull the trust root once (pin the bytes in CI):
curl -sSfL https://www.frem.sh/.well-known/slsa-trust-root.json -o slsa-trust-root.jsonVerify an artifact + its .intoto.jsonl attestation (the workflow template writes it alongside the artifact in the build output):
slsa-verifier verify-artifact ./myapp \
--provenance-path ./myapp.intoto.jsonl \
--source-uri frem.sh/acme/myapp \
--source-tag v1.2.3 \
--builder-id https://frem.sh/runner-controller/v1 \
--trusted-root slsa-trust-root.jsonVerification fails (exit 1, no output) if:
- The DSSE signature doesn’t validate against the trust root’s public key (tampered attestation or wrong builder key)
- The artifact’s actual SHA-256 doesn’t match
subject[].digest.sha256(tampered artifact) --source-uridoesn’t matchbuildDefinition.externalParameters.repo(attestation from a different repo)--builder-iddoesn’t matchrunDetails.builder.id(attestation from a different platform)
Why not Sigstore at launch
Sigstore’s public Fulcio + Rekor + Trillian stack is operated by the OpenSSF (Linux Foundation), a US-incorporated body. Using it would send every build’s metadata to US-hosted infrastructure, incompatible with fremforge’s EU-only sub-processor commitment in DPA Annex B. The self-hosted Sigstore alternative (Fulcio + Rekor + Trillian + a TUF tree on T Cloud) is deferred to Phase 2, triggered by a regulated-industry customer requirement specifically for third-party-auditable transparency-log evidence. Until then, fremforge’s own audit-chain WORM anchor (3-year COMPLIANCE-locked OBS) provides equivalent tamper-evidence for the same scope, every attestation’s SHA-256 is also hash-chained into the platform’s audit log.
Kernel isolation model
Each CI job runs in its own CCI (Cloud Container Instance) pod on T Cloud in eu-de. The isolation properties:
| Property | Detail |
|---|---|
| Kernel sharing | None, each job has a dedicated pod; no shared kernel between concurrent jobs, same org or different orgs |
| Persistence | Pods are destroyed after job completion; no filesystem state survives between runs |
| Service account token | automountServiceAccountToken: false on all runner pods |
| Network | Egress-only NetworkPolicy; runners cannot receive inbound connections |
| T Cloud metadata | Blocked by SSRF outbound proxy; runner code cannot reach the instance metadata endpoint |
See CI runners for the full isolation specification and BYO runner registration.
Tenant isolation
Every fremforge tenant is a separately-keyed namespace in the platform. The isolation chain runs from the URL through middleware, the database query layer, the storage layer, and the audit chain.
| Layer | Mechanism | What stops cross-tenant access |
|---|---|---|
| URL routing | /<slug>/_admin/* middleware re-loads the tenant from the slug on every request | A user with a session for org A who pastes org B’s URL is rejected at the membership check, not at a stale cookie |
| Membership check | Forgejo Owners-team lookup against the slug, verified on every request | The api never trusts an inbound claim about which orgs the user belongs to |
| Database | Every tenant-keyed table has a tenant_id foreign key; every read AND every write filters on it | SELECT / UPDATE / DELETE without tenant_id is structurally impossible in the audit-flagged paths |
| Signed tokens | Every short-lived signed URL (billing magic link, undo cancellation, OIDC state) carries the tenant id in its payload; the verify-side cross-checks against the URL slug | A token minted for tenant A is rejected if replayed against tenant B’s URL |
| Storage (OBS) | SBOMs, attestations, audit-log objects all keyed <artifact>/<tenant_id>/...; signed-URL minting re-validates tenant.id === row.tenant_id before issue | A direct OBS-presigned URL bound to tenant A cannot be rewritten to read tenant B |
| Audit chain | Tamper-evident hash chain keyed per-tenant; one tenant’s appends never modify another’s chain head | A break in tenant A’s chain (e.g. an attempted retroactive delete) does not affect tenant B’s chain integrity |
| Operator access | Staff (fremverk) viewing customer data is logged to the customer’s audit chain as an operator action; tenant admins can see operator visits in their own audit log | No silent staff access to customer data |
Owners-team revocation lag
The api caches a customer’s Forgejo Owners-team membership for up to 5 minutes to keep page-loads under 50ms (the alternative is a Forgejo API round-trip on every request, which would be 100-200ms slower per page). The trade-off:
- When you add someone to Owners on the Forgejo side, they gain admin access within 5 minutes.
- When you remove someone from Owners on the Forgejo side, they lose admin access within 5 minutes, not instantly.
For most operations this lag is acceptable: a former owner cannot create new Forgejo resources (Forgejo’s own check is immediate), and any privileged action via the fremforge admin UI is recorded in the per-tenant audit log with the actor’s username, so post-revocation activity remains attributable.
If you need immediate revocation (e.g. as part of an incident response after a credential compromise), the operator on-call can pin the cache to zero TTL for your tenant on request, open a ticket at support@frem.sh with subject prefix [urgent-revocation] and the username + tenant slug. Standard SLA: under 15 minutes during business hours, under 60 minutes outside. The same effect is achieved by revoking the user’s Forgejo session (Forgejo Owner → User profile → Sign out), which invalidates the session cookie the api parses, no fremforge-side action needed.
Platform security commitments
| CVE severity | Patch SLA (from upstream fixed release) |
|---|---|
| Critical (CVSS ≥ 9.0) | 48 hours |
| High (7.0-8.9) | 72 hours |
| Medium (4.0-6.9) | 7 days |
| Low | Next scheduled maintenance window |
The patch SLA is published contractually and cited verbatim in the DPA security annex.
Audit-log integrity: tamper-evident hash chain anchored to T Cloud OBS WORM storage every 2 minutes; hourly FunctionGraph integrity check; chain breaks page on-call.
Troubleshooting
Push blocked but I cannot find the secret in the diff.
The secret may be in a file that was modified but whose full content is not visible in the standard diff. Run Gitleaks locally to identify the exact location:
pip install gitleaks # or brew install gitleaks
gitleaks detect --source . --verboseRenovate is not raising PRs for a known CVE.
Check the org CVE policy threshold first. The CVE severity may be below the configured floor. If the threshold is correct, check the repository has a supported manifest file and that Renovate has access (it requires read+write on the repo, granted automatically at enrolment). Trigger an out-of-cycle run at Org admin → Dependency updates → Run now.
SBOM download returns 404.
SBOMs are generated only for images pushed after SBOM generation was enabled. Images pushed before enabling do not have an SBOM. Push a new tag to generate one.
SSH-signing fails with ssh-keygen: gpg failed to sign the data.
Make sure git config gpg.format is set to ssh (not the default openpgp) and user.signingkey points at a public-key file. On macOS, also confirm ssh-keygen is on $PATH (Apple’s bundled ssh-keygen works; if you’ve installed Homebrew OpenSSH, ensure it shadows correctly).
Cross-references
- Push protection override in org admin, managing the active override list
- Dependency updates, Renovate configuration reference
- CI runners, runner isolation, CCI pods, BYO runner registration
- OIDC token federation, keyless cloud auth from CI jobs