Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →

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:

CategoryExample patterns
Cloud provider credentialsAWS access/secret keys, GCP service account JSON, Azure storage connection strings
Source control tokensGitHub PATs, GitLab PATs, Bitbucket app passwords
Payment keysStripe secret keys, Stripe webhook signing secrets
Communication tokensSlack bot tokens, Slack webhook URLs, Twilio auth tokens
Private keysRSA/EC/Ed25519 private keys, PEM-encoded certificates
Generic secretsHigh-entropy strings matching the generic-api-key pattern, JWT signing secrets
Database URLsPostgreSQL, 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:

  1. Rotate the secret immediately at the issuing service (AWS IAM, Stripe dashboard, etc.). Assume it is compromised.
  2. 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>
  1. 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:

  1. Go to Org admin → Push protection → Active overrides → New override.
  2. Specify: Repository (or select “All repos”), secret pattern (from the Gitleaks rule ID), and justification (free text, logged in the audit trail).
  3. 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:

ThresholdBehavior
LOWPRs for all CVE-affected versions, including informational findings
MEDIUMPRs for MEDIUM, HIGH, and CRITICAL CVEs
HIGH (default)PRs for HIGH and CRITICAL CVEs only
CRITICALPRs 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

EcosystemFiles
JavaScript / Node.jspackage.json, package-lock.json, yarn.lock, pnpm-lock.yaml
Gogo.mod, go.sum
RustCargo.toml, Cargo.lock
Pythonrequirements.txt, Pipfile, pyproject.toml, poetry.lock
RubyGemfile, Gemfile.lock
Javapom.xml, build.gradle, build.gradle.kts
ContainersDockerfile, 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:

FieldDescription
CVE IDe.g. CVE-2024-12345 with link to NVD
SeverityCRITICAL / HIGH / MEDIUM / LOW / UNKNOWN
Affected packagePackage name and installed version
Fix versionFirst version that resolves the CVE, if available
EPSS scoreExploit 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,CRITICAL

Adjust 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.json

The 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_signers

Add 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..HEAD

Each 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):

FieldValue
predicateTypehttps://slsa.dev/provenance/v1
buildDefinition.buildTypehttps://frem.sh/buildtypes/forgejo-actions/v1
buildDefinition.externalParameters.repo<org>/<repo>
buildDefinition.externalParameters.refThe git ref (e.g. refs/tags/v1.2.3)
buildDefinition.resolvedDependencies[0]git+https://frem.sh/<org>/<repo>@<commit-sha>
runDetails.builder.idhttps://frem.sh/runner-controller/v1 (server-controlled, a compromised runner cannot forge this)
runDetails.metadata.invocationIdForgejo Actions workflow-run ID
subject[].digest.sha256SHA-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.json

Verify 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.json

Verification 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-uri doesn’t match buildDefinition.externalParameters.repo (attestation from a different repo)
  • --builder-id doesn’t match runDetails.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:

PropertyDetail
Kernel sharingNone, each job has a dedicated pod; no shared kernel between concurrent jobs, same org or different orgs
PersistencePods are destroyed after job completion; no filesystem state survives between runs
Service account tokenautomountServiceAccountToken: false on all runner pods
NetworkEgress-only NetworkPolicy; runners cannot receive inbound connections
T Cloud metadataBlocked 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.

LayerMechanismWhat stops cross-tenant access
URL routing/<slug>/_admin/* middleware re-loads the tenant from the slug on every requestA 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 checkForgejo Owners-team lookup against the slug, verified on every requestThe api never trusts an inbound claim about which orgs the user belongs to
DatabaseEvery tenant-keyed table has a tenant_id foreign key; every read AND every write filters on itSELECT / UPDATE / DELETE without tenant_id is structurally impossible in the audit-flagged paths
Signed tokensEvery 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 slugA 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 issueA direct OBS-presigned URL bound to tenant A cannot be rewritten to read tenant B
Audit chainTamper-evident hash chain keyed per-tenant; one tenant’s appends never modify another’s chain headA break in tenant A’s chain (e.g. an attempted retroactive delete) does not affect tenant B’s chain integrity
Operator accessStaff (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 logNo 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 severityPatch 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
LowNext 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 . --verbose

Renovate 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