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

Deploy secrets: OIDC federation

fremforge doesn’t want to hold your production secrets. Long-lived deploy creds (AWS_ACCESS_KEY_ID, AZURE_CLIENT_SECRET, GCP_SA_KEY, …) stored in CI secret backends are a perpetual rotation chore and a perpetual breach blast-radius. The modern shape is OIDC federation: your workflow asks the CI server for a short-lived identity token, your cloud’s IAM verifies the token and hands back a short-lived credential. fremforge ships this end-to-end.

How it works

Every CI job that runs on fremforge gets a freshly-minted JSON Web Token signed by fremforge’s runner OIDC issuer at https://frem.sh/_app/runner/oidc. The token carries claims describing the job context, repository, ref, workflow, optionally environment, and your cloud IAM trust policy decides whether to grant credentials based on those claims.

  sequenceDiagram
    autonumber
    actor User
    participant FF as fremforge<br/>runner-OIDC issuer
    participant Pod as runner Pod
    participant Cloud as Your cloud's<br/>IAM • STS / WIF / …

    User->>FF: git push
    FF->>Pod: dispatch job — mint OIDC JWT
    Pod->>Cloud: "Here's my JWT, give me credentials"
    Cloud->>Cloud: Verify JWT against fremforge JWKS<br/>+ check trust policy claims
    Cloud->>Pod: 1h credentials
    Pod->>Cloud: deploy / read / write

Trust-policy claims your cloud can match on:

ClaimExampleWhat it identifies
isshttps://frem.sh/_app/runner/oidcfremforge itself
audsts.amazonaws.comwhat cloud you’re authenticating to
subrepo:my-org/my-repo:environment:production (when environment: is declared)
repo:my-org/my-repo:ref:refs/heads/main (when no env)
which workflow run
repositorymy-org/my-reporepo name
repository_ownermy-orgorg slug
refrefs/heads/maingit ref
environmentproductionworkflow-declared env name
actoraliceForgejo username that triggered the run

These mirror GitHub Actions’ claim shape, so existing trust policies and the actions/configure-aws-credentials / azure/login / google-github-actions/auth actions work without modification.

Discovery URL

Point your cloud at:

  • Issuer URL: https://frem.sh/_app/runner/oidc
  • JWKS URL: https://frem.sh/_app/runner/oidc/.well-known/jwks.json
  • Discovery doc: https://frem.sh/_app/runner/oidc/.well-known/openid-configuration

All three are anonymous-readable.

Workflow patterns

Declaring an environment

jobs:
  deploy:
    runs-on: fremforge-runner
    environment: production
    permissions:
      id-token: write    # required to mint the OIDC JWT
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/fremforge-deploy-prod
          aws-region: eu-central-1
      - run: aws s3 sync ./build s3://my-prod-bucket/

The environment: production reshapes the OIDC sub claim to repo:my-org/my-repo:environment:production, so your trust policy can grant access only to production-declared jobs.

Without an environment

When you don’t declare environment:, the sub falls back to repo:<owner>/<repo>:ref:<ref>. Trust policies that match on ref work the same way as on GitHub. The fremforge-side environments gate doesn’t apply (it only gates env-declared workflows), so the cloud-side trust policy is your only line.

Trust policy examples

AWS

IAM trust policy for an OIDC-federated role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789012:oidc-provider/frem.sh/_app/runner/oidc"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "frem.sh/_app/runner/oidc:aud": "sts.amazonaws.com"
      },
      "StringLike": {
        "frem.sh/_app/runner/oidc:sub": "repo:my-org/my-repo:environment:production"
      }
    }
  }]
}

One-time setup: register fremforge as an OIDC provider:

aws iam create-open-id-connect-provider \
  --url https://frem.sh/_app/runner/oidc \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list $(curl -s https://frem.sh/_app/runner/oidc/.well-known/jwks.json | jq -r '.keys[0].x5t')

Azure (Workload Identity Federation)

Register the federated credential on an Entra ID App Registration:

az ad app federated-credential create \
  --id $APP_OBJECT_ID \
  --parameters '{
    "name": "fremforge-prod-deploy",
    "issuer": "https://frem.sh/_app/runner/oidc",
    "subject": "repo:my-org/my-repo:environment:production",
    "audiences": ["api://AzureADTokenExchange"]
  }'

Workflow:

- uses: azure/login@v2
  with:
    client-id: ${{ vars.AZURE_CLIENT_ID }}
    tenant-id: ${{ vars.AZURE_TENANT_ID }}
    subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

GCP (Workload Identity Federation)

gcloud iam workload-identity-pools providers create-oidc fremforge-provider \
  --workload-identity-pool=fremforge-pool \
  --issuer-uri="https://frem.sh/_app/runner/oidc" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.environment=assertion.environment" \
  --attribute-condition="attribute.environment == 'production'"

Workflow:

- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/fremforge-pool/providers/fremforge-provider
    service_account: deploy@my-project.iam.gserviceaccount.com

T Cloud (Open Telekom Cloud / OTC IAM Identity Provider)

T Cloud uses IAM Identity Providers + token-exchange:

  1. Identity Provider in OTC Console → IAM → Identity Providers → Create:
    • Name: fremforge-runner-oidc
    • Protocol: OpenID Connect
    • Issuer URL: https://frem.sh/_app/runner/oidc
    • JWKS URL: https://frem.sh/_app/runner/oidc/.well-known/jwks.json
    • Client ID: iam.eu-de.otc.t-systems.com (or your project’s STS audience)
  2. Mapping rules, translate claims into a federated user. Example mapping a repo:my-org/my-repo:environment:production subject to the OTC group deploy-prod:
[
  {
    "local": [
      { "user": { "name": "{0}" } },
      { "group": { "name": "deploy-prod" } }
    ],
    "remote": [
      { "type": "sub" },
      { "type": "environment", "any_one_of": ["production"] }
    ]
  }
]
  1. Workflow, exchange the JWT for an OTC token via the IAM token-exchange endpoint, then use the returned X-Subject-Token for any OTC API call.

Gating which refs can deploy where

OIDC federation removes the need to STORE long-lived secrets, but you still need to decide which branches can deploy to which environment. A workflow on a feature branch CAN mint a JWT claiming environment: production; the gate is what stops the JWT from being honoured.

fremforge offers two gates, and you should use both as defense in depth.

Gate 1: cloud-side trust policy (always required)

Your cloud IAM trust policy is the final authority. Pin the sub claim to the repo+environment shape AND pin the ref claim to the branch(es) that env is allowed to deploy from. Example AWS:

{
  "Condition": {
    "StringEquals": {
      "frem.sh/_app/runner/oidc:sub": "repo:my-org/my-repo:environment:production"
    },
    "StringLike": {
      "frem.sh/_app/runner/oidc:ref": "refs/heads/main"
    }
  }
}

A feature branch CAN mint the JWT, but AWS refuses to issue credentials because ref is refs/heads/feature/foo instead of refs/heads/main. This is the GitHub-recommended baseline pattern and works on day one.

Gate 2: fremforge-side per-environment deployment-branch patterns (recommended)

At Settings → Environments, configure each environment GitHub-style. Three sections per environment:

Deployment branches and tags, glob patterns matched against the triggering git ref. Examples (auto-prefixed with refs/heads/ for branch shorthand):

NamePatternsEffect
productionmainmain branch only
stagingmain
develop
release/*
main + develop + release/* branches
pr-preview*any branch (cloud-side gate is the real authority)
releaserefs/tags/v[0-9]+.*version tags only (explicit refs/tags/ prefix)

Workflows that declare environment: production are refused at dispatch when the triggering ref doesn’t match. Workflows WITHOUT an environment: declaration run unrestricted, basic CI on feature branches keeps working.

Environment secrets, encrypted, env-scoped, injected as workflow ${{ secrets.NAME }} AND plain env vars when the workflow’s environment: matches. Covers the non-cloud-IAM secret cases (third-party API keys, signing keys, webhook secrets) that OIDC federation doesn’t solve.

Environment variables, non-secret env-scoped values. Same injection model.

What about approval gates?

GitHub Actions Environments has a “Required reviewers” feature: a job pauses post-merge until N people click Approve in the Actions UI. fremforge intentionally doesn’t ship this because Forgejo already has a stronger gate at the same point in the timeline, branch protection on main with CODEOWNERS on .forgejo/workflows/* requires PR review before any deploy-capable workflow lands. For the 99% case (1 PR = 1 deploy), Forgejo’s PR review IS the deploy approval. Customers needing the GitHub case (1 PR can produce N independent deploys with separate approvers per deploy), file an issue and we’ll prioritise Phase 2.5 deploy-time approvers based on actual demand.

The fremforge-side gate stops the JWT from being minted at all. Cloud-side gate stops a minted JWT from being honoured. Combine both: even if a future fremforge bug let a refused dispatch through, the cloud’s IAM still refuses to issue credentials. Even if a customer’s cloud trust policy has a typo and accepts a broader ref pattern than intended, fremforge still refused to mint the JWT.

Lazy onboarding

When you declare environment: x on a workflow but haven’t yet configured an environments row for x, fremforge allows the dispatch rather than refusing it. This is deliberate so a fresh customer’s workflows don’t break the moment they declare an environment, your cloud-side trust policy is the safety net until you add the fremforge-side row. Once a row exists, the gate is enforced.

Lock-down mode: actions_only_protected_branches

For high-security environments where you want to forbid CI execution on unprotected refs ENTIRELY (not just gate deploy creds), enable Settings → Security & policies → Actions deploy gate:

Only run workflows on protected branches

When enabled, the runner-controller refuses to dispatch ANY workflow whose triggering branch is NOT in the repo’s protected-branch list (configured via Forgejo’s native Settings → Branches → Add protection rule). This includes basic CI (tests, lint), nothing runs on unprotected branches. Useful for regulated environments where you want zero code execution outside reviewed refs; not recommended for normal dev flow because it breaks feature-branch testing.

If you enable this, you’ll typically protect main, develop, release/* patterns in Forgejo to keep your standard dev branches CI-eligible.

Recommended posture

For typical SaaS / product teams:

  1. Don’t enable actions_only_protected_branches, let basic CI run on feature branches.
  2. Do configure environments at Settings → Environments with appropriate allowed_refs_regex for each (production, staging, etc.).
  3. Do configure cloud trust policies with both sub (env-pinned) AND ref (branch-pinned) conditions.
  4. Branch-protect main in Forgejo with CODEOWNERS coverage on .forgejo/workflows/* so workflow changes need review.

For regulated / lock-down environments (banking, defence, healthcare):

  1. Do enable actions_only_protected_branches.
  2. Do configure environments for granular per-env gating on top.
  3. Same cloud-side trust policy posture as above.

This combination matches the deploy-gate model GitHub Enterprise + AWS recommend (require-environments-on-deploy + branch-protected-only) plus the optional lock-down switch for regulated tenants.

Comparison: what fremforge stores vs your cloud

Long-lived secretsOIDC federation (this page)
Where deploy creds liveSettings → Secrets in fremforgeYour cloud’s IAM. fremforge never sees them.
Credential lifetimeUntil you rotate1 hour (configurable per-trust-policy)
If fremforge is breachedAttacker holds your AWS keysAttacker can mint OIDC JWTs but cloud-side trust policy bounds what those JWTs can do (per-env, per-repo, per-ref)
Rotation choreQuarterly per secret per cloudNone, keys rotate on each job
Audit trailManual: who exported the secret whenEvery OIDC mint is in audit_events + your cloud’s CloudTrail/Activity Log

Limitations

  • Object-form environment: (with name: + url:) is supported by name; url: is parsed but not currently surfaced as a claim.
  • Matrix-expanded environment names (environment: ${{ matrix.target }}) are not resolved at OIDC mint time and result in no environment claim being emitted. Use a hardcoded environment: per matrix job if you need env-claim-based trust policies.
  • Per-environment required-approver gates (à la GitHub Environments’ “wait for approval” feature) are tracked as Phase 2 follow-up; today’s enforcement is binary (protected-branch yes/no).

See also