Skip to main content
OIDC federation

OIDC token federation

Each fremforge CI job can receive a short-lived OIDC token: an RS256-signed JWT issued by fremforge’s runner-OIDC issuer. Cloud providers (T Cloud Public, AWS, Azure, GCP) can be configured to trust this token and exchange it for scoped, time-limited credentials. No deploy keys stored as secrets, nothing to rotate on a schedule.

Claims reference

The fremforge runner JWT contains the following claims:

ClaimValue / formatExample
isshttps://frem.sh/_app/runner/oidc (single global issuer for all tenants)https://frem.sh/_app/runner/oidc
subrepo:<org>/<repo>:ref:refs/heads/<branch>repo:acme/api:ref:refs/heads/main
audConfigurable; default https://frem.shhttps://frem.sh
repository_ownerOrg slug — pin trust-policy conditions on this claim for per-tenant isolation under the shared issueracme
repository<org>/<repo>acme/api
refFull git refrefs/heads/main
shaCommit SHAa1b2c3d4...
environmentDeployment environment name (if job declares one)production
job_workflow_refWorkflow file path and refacme/api/.forgejo/workflows/deploy.yaml@refs/heads/main
deployer_group(T Cloud Public only) IAM group name to bind the federated user to, set per-workload by fremforge — see T Cloud Public agency trustacme-prod-deployers

The sub claim is the primary trust anchor in most provider trust policies. It uniquely identifies the org, repo, and branch combination. For per-tenant isolation in shared cloud accounts, add a condition pinning on repository_owner (your org slug) — same pattern as GitHub Actions trust on token.actions.githubusercontent.com. fremforge emits a single global issuer for all tenants; trust isolation is enforced at the claim layer, not the issuer-URL layer.

Getting the token in a workflow

Add id-token: write to the job’s permissions block. The runner then populates ACTIONS_ID_TOKEN_REQUEST_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL as environment variables, which you use to fetch the actual JWT:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: fremforge
    steps:
      - name: Get OIDC token
        run: |
          TOKEN=$(curl -sS \
            -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://frem.sh" \
            | jq -r .value)
          echo "TOKEN=$TOKEN" >> $GITHUB_ENV

Most provider-specific steps (AWS, Azure, GCP) handle the token fetch internally. You only write the manual fetch above if you need the raw JWT for a custom trust flow.

T Cloud Public agency trust

T Cloud Public’s IAM federation model is OpenStack-derived: you register the OIDC issuer as an identity provider, attach a mapping that translates ID-token claims to local IAM groups, then exchange the runner’s ID token at OTC’s OS-FEDERATION endpoint for a T Cloud Public Keystone token scoped to the groups the mapping bound.

fremforge uses claim-driven group binding (the same shape AWS Identity Center, GCP Workload Identity Federation, and Azure Federated Credentials use): the runner’s ID token carries a deployer_group claim per-workload, and your OTC mapping reads that claim verbatim. One mapping rule covers your entire fleet — adding a workload doesn’t touch your IAM at all.

Step 1: Create an OpenID Connect identity provider

In the T Cloud Public console, go to IAM → Identity Providers → Create Identity Provider.

  • Type: Virtual user
  • Protocol: OpenID Connect
  • Access type: Programmatic access (program)

After creation, configure the OIDC settings:

  • Identity provider URL (idp_url): https://frem.sh/_app/runner/oidc (single global issuer; matches the iss claim on every fremforge runner ID token).
  • Client ID (client_id): https://frem.sh, must equal the aud claim on the runner ID token.
  • Signing key (signing_key): paste the JWKS public-key JSON. Fetch it once from https://frem.sh/_app/runner/oidc/.well-known/openid-configurationjwks_uri and copy the JSON. T Cloud Public does not auto-fetch JWKS, so re-paste when fremforge announces a JWKS rotation (announced on the trust page; rotations are annual at most).

Step 2: Add the claim-driven mapping rule

One rule, scales for any number of workloads:

[
  {
    "local": [
      { "user":  { "name": "fremforge-{0}-{1}" } },
      { "group": { "name": "{2}" } }
    ],
    "remote": [
      { "type": "repository_owner" },
      { "type": "repository" },
      { "type": "deployer_group" }
    ]
  }
]

The trust posture:

  • The federated user joins whatever IAM group name the ID token’s deployer_group claim asserts ({2} substitutes the claim value verbatim).
  • The claim is set by fremforge’s runner-OIDC issuer per the workload→group mapping you agree at onboarding. It’s only assertable for workloads we have a row for, and only for the ref the row pins (default: main branch).
  • Same JWT signature pinning, same iss + aud + repository_owner filtering — only the workload→group decision moved from “N OTC mapping rules” to “one claim emitted by fremforge.”

Step 3: Create the IAM groups your workloads will assume

Create one group per deployer profile you want to expose (e.g. acme-prod-deployers, acme-staging-deployers). Attach whatever project + EPS roles each profile needs — those are 100% yours; fremforge never touches your group configuration. Group names match the value fremforge will put in the deployer_group claim, agreed at onboarding.

Step 4: Tell fremforge which workload uses which group

Email or admin-API call: “Repo acme/api, main branch → group acme-prod-deployers.” fremforge inserts a row in its runner_deployer_groups table. From the next workflow run onward, ID tokens for that workload carry deployer_group=acme-prod-deployers and your mapping binds the federated user to that group.

Adding a new repo is just another email — your OTC IAM stays unchanged.

Step 3: Create an agency to assume

Create an agency (IAM → Agencies → Create Agency) with the following:

  • Agency type: Account (cloud service of another T Cloud Public account does not apply here).
  • Delegated party: select the OpenID Connect identity provider you created in Step 1.
  • Permissions: attach the policies the CI job needs (e.g. OBS Administrator scoped to the workload OBS bucket, or CCE FullAccess scoped to the workload project).

Note the agency name and the project where you scope it.

Step 5: Use the credentials in a workflow

The runner fetches an ID token, exchanges it at OTC’s OS-FEDERATION endpoint for an unscoped Keystone token, then swaps for a project-scoped token. Both T Cloud Public CLI (otc) and OpenTofu with the opentelekomcloud provider accept OS_AUTH_TOKEN from the environment with no further configuration.

# Forgejo-specific: opts the task into the OIDC envelope.
# Forgejo jobparser only reads this top-level key — it does NOT
# auto-derive from `permissions: id-token: write` like GitHub does.
enable-openid-connect: true

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: fremforge
    steps:
      - name: Exchange OIDC for T Cloud Public token
        run: |
          # 1. Fetch the runner OIDC JWT
          JWT=$(curl -sS --fail \
            -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://frem.sh" \
            | jq -r .value)

          # 2. Exchange at OTC's OS-FEDERATION endpoint (NOT /v3/auth/tokens
          #    with methods=["id_token"] — OTC's Keystone fork doesn't support
          #    that. Use the IdP-scoped URL with Bearer header instead).
          UNSCOPED=$(curl -sS -i \
            -X POST -H "Authorization: Bearer $JWT" \
            "https://iam.eu-de.otc.t-systems.com/v3/OS-FEDERATION/identity_providers/fremforge-runner-oidc/protocols/oidc/auth" \
            | awk 'BEGIN{IGNORECASE=1} /^x-subject-token:/ { sub(/^x-subject-token:[ \t]*/,"",$0); gsub(/\r/,"",$0); print }')

          # 3. Swap for project-scoped (the unscoped token gives you no
          #    permissions on its own; you need a project scope to do anything).
          SCOPED=$(curl -sS -i -X POST \
            -H "Content-Type: application/json" \
            -d "{\"auth\":{\"identity\":{\"methods\":[\"token\"],\"token\":{\"id\":\"$UNSCOPED\"}},\"scope\":{\"project\":{\"name\":\"eu-de\"}}}}" \
            "https://iam.eu-de.otc.t-systems.com/v3/auth/tokens" \
            | awk 'BEGIN{IGNORECASE=1} /^x-subject-token:/ { sub(/^x-subject-token:[ \t]*/,"",$0); gsub(/\r/,"",$0); print }')

          echo "::add-mask::$SCOPED"
          echo "OS_AUTH_TOKEN=$SCOPED" >> $GITHUB_ENV

      - name: Tofu plan
        run: tofu plan
        # opentelekomcloud provider picks up OS_AUTH_TOKEN automatically

Notes on T Cloud Public specifics

  • OTC uses /v3/OS-FEDERATION/identity_providers/{idp}/protocols/oidc/auth, NOT /v3/auth/tokens with methods=["id_token"]. The OpenStack-upstream auth-body method returns 400 "Unsupported method: id_token" on OTC’s Keystone fork. Verified empirically + per OTC API doc iam_13_0606.
  • OTC does not auto-rotate JWKS. You paste the signing key once at provider creation; re-paste when fremforge announces a rotation. AWS / Azure / GCP all auto-fetch from the discovery document — only T Cloud Public requires the explicit paste.
  • The exchanged token is in the X-Subject-Token response header, not the body — OpenStack-Keystone convention.
  • Workload scoping comes from the deployer_group claim (mapping binds the federated user to that group) AND the project roles on that group. The claim prevents the wrong workload from assuming the wrong group; the role limits what the group can do.
  • Optional: agencies for assumed-role patterns are also supported, but most use cases don’t need them — the group binding from step 2 is enough.

AWS IAM

Step 1: Create an OIDC identity provider

In the AWS console, go to IAM → Identity providers → Add provider.

  • Provider type: OpenID Connect
  • Provider URL: https://frem.sh/_app/runner/oidc (single global issuer; per-tenant filtering is done in the trust-policy condition, not at the provider URL)
  • Audience: https://frem.sh

AWS fetches the JWKS from the discovery document to verify tokens.

Step 2: Create an IAM role with a web identity trust policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<account-id>:oidc-provider/frem.sh/_app/runner/oidc"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "frem.sh/_app/runner/oidc:aud": "https://frem.sh",
          "frem.sh/_app/runner/oidc:repository_owner": "<your-org>",
          "frem.sh/_app/runner/oidc:sub": "repo:<your-org>/<your-repo>:ref:refs/heads/main"
        }
      }
    }
  ]
}

Attach the permissions the CI job needs to this role (e.g. AmazonS3FullAccess, AmazonECRPowerUser).

Step 3: Use in a workflow

Use the official aws-actions/configure-aws-credentials action:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: fremforge
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<account-id>:role/<role-name>
          aws-region: eu-central-1
          audience: https://frem.sh

      - name: Deploy
        run: aws s3 sync ./dist s3://my-bucket/

The action handles the token fetch, sts:AssumeRoleWithWebIdentity call, and credential injection automatically.

Azure (Entra Workload Identity)

Step 1: Create federated credentials on an app registration

  1. In the Azure portal, go to Entra ID → App registrations → <your app> → Certificates & secrets → Federated credentials.
  2. Click Add credential.
  3. Scenario: Other issuer.
  4. Fill in:
    • Issuer: https://frem.sh/_app/runner/oidc (single global issuer; tenant isolation is enforced by the Subject identifier below)
    • Subject identifier: repo:<your-org>/<your-repo>:ref:refs/heads/main
    • Audience: https://frem.sh
  5. Save.

Step 2: Use in a workflow

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: fremforge
    steps:
      - uses: actions/checkout@v4

      - name: Get OIDC token
        run: |
          TOKEN=$(curl -sS \
            -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://frem.sh" \
            | jq -r .value)
          echo "OIDC_TOKEN=$TOKEN" >> $GITHUB_ENV

      - name: Azure login via federated token
        run: |
          az login \
            --service-principal \
            --tenant <tenant-id> \
            --client-id <client-id> \
            --federated-token "$OIDC_TOKEN"

      - name: Deploy
        run: az webapp deploy --resource-group my-rg --name my-app --src-path ./dist

The app registration’s client ID and your Entra tenant ID are not secrets. They can be stored as plain workflow variables. Only the OIDC token is sensitive, and it expires after the job completes.

GCP Workload Identity

Step 1: Create a Workload Identity pool and OIDC provider

# Create the pool
gcloud iam workload-identity-pools create fremforge-pool \
  --location global \
  --display-name "fremforge CI"

# Create the OIDC provider inside the pool
gcloud iam workload-identity-pools providers create-oidc fremforge-provider \
  --location global \
  --workload-identity-pool fremforge-pool \
  --issuer-uri "https://frem.sh/_app/runner/oidc" \
  --allowed-audiences "https://frem.sh" \
  --attribute-mapping "google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner"

Step 2: Grant service account impersonation

# Allow the pool to impersonate a service account, scoped by sub claim
gcloud iam service-accounts add-iam-policy-binding <sa>@<project>.iam.gserviceaccount.com \
  --role roles/iam.workloadIdentityUser \
  --member "principalSet://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/fremforge-pool/attribute.sub/repo:<your-org>/<your-repo>:ref:refs/heads/main"

Step 3: Use in a workflow

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: fremforge
    steps:
      - uses: actions/checkout@v4

      - name: Authenticate to GCP via OIDC
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/fremforge-pool/providers/fremforge-provider
          service_account: <sa>@<project>.iam.gserviceaccount.com
          audience: https://frem.sh

      - name: Deploy
        run: gcloud run deploy my-service --image gcr.io/my-project/my-image

The google-github-actions/auth action works on fremforge runners. It uses the same ACTIONS_ID_TOKEN_REQUEST_TOKEN / ACTIONS_ID_TOKEN_REQUEST_URL mechanism.

Troubleshooting

Audience mismatch.

The aud claim in the token must match what the provider expects. If the trust policy or provider is configured for a different audience (e.g. sts.amazonaws.com instead of https://frem.sh), the exchange fails with an audience validation error. Set audience: https://frem.sh explicitly in the provider config and in the token request (use &audience=https://frem.sh in the curl URL).

sub claim does not match the trust policy condition.

The sub claim format is exact: repo:<org>/<repo>:ref:refs/heads/<branch>. Common mismatches:

  • Branch name typo (main vs master).
  • The trust policy uses a tag ref (refs/tags/v1.0) but the job runs on a branch.
  • The policy uses refs/heads/main but the job runs on a PR branch (refs/pull/42/head). Use StringLike with a wildcard if you need to allow PRs: "repo:acme/api:ref:refs/pull/*".

Token expired, jobs longer than 1 hour.

The OIDC token has a 1-hour TTL. Jobs that run for more than 1 hour receive a fresh token each time they call the token endpoint. Each step that calls ACTIONS_ID_TOKEN_REQUEST_URL gets a new token. If you cache the token across steps spanning more than 1 hour, it expires. Fetch a fresh token at each step that needs credentials.

id-token: write permission missing.

If permissions: is not set in the workflow or job, the runner uses the default read-only permission set, which does not include id-token: write. The ACTIONS_ID_TOKEN_REQUEST_TOKEN variable will be absent and the token request returns 403. Add id-token: write to the permissions: block at the job level (or workflow level if all jobs need it).

Per-org federation overrides

The tenant-admin page at https://frem.sh/<org>/_admin/runner-federation is the place to read your org’s federation settings and tighten them when needed.

For most setups you only need the issuer URL — copy it from this page into your cloud’s trust policy (the Provider/Issuer URL field for AWS, Azure, GCP, Scaleway, etc.). The page also shows the JWKS URL for providers that don’t auto-fetch the discovery document (T Cloud Public requires the explicit paste; AWS / Azure / GCP fetch it automatically).

Under Advanced overrides on the same page you can:

  • Pin allowed_audiences — a hard allowlist of aud values the issuer will sign. Use it when you want to mint tokens only for specific cloud trust policies and block any accidentally-broader audience.
  • Set a custom sub claim template — when your existing trust policy expects a different sub shape than the fremforge default.

Most customers leave the Advanced overrides at their defaults and use only the issuer URL. The same settings are also available programmatically — see below.

Per-tenant federation overrides

By default the issuer signs tokens with the platform claim shape (sub = repo:<org>/<repo>:ref:<ref>, no aud restriction). Two knobs let you pin tighter contracts on a per-tenant basis:

  • allowed_audiences, a hard allowlist of aud values the issuer will sign. Any mint request with aud outside the list is rejected before the JWT is created. Useful when you want to mint tokens for AWS and T Cloud Public trust policies but block any accidentally-broader audience claim. Empty list = no restriction (platform default).
  • sub_claim_template, a Mustache-shaped template rendered against four fixtures ({{tenant}}, {{repo}}, {{branch}}, {{ref_type}}) at token mint time. Useful when your existing GitHub Actions / Bitbucket Pipelines trust policy expects a different sub shape than the fremforge default.

Reading the current overrides

curl -H "Authorization: Bearer $FREMFORGE_PAT" \
  https://frem.sh/_app/api/v1/orgs/acme/runners/federation
# {
#   "issuer": "https://frem.sh/runner/oidc",
#   "jwks_uri": "https://frem.sh/runner/oidc/.well-known/jwks.json",
#   "tenant_org_slug": "acme",
#   "allowed_audiences": [],
#   "sub_claim_template": null
# }

Pinning allowed_audiences

curl -X PUT -H "Authorization: Bearer $FREMFORGE_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "allowed_audiences": [
      "sts.amazonaws.com",
      "sts.eu-west-1.amazonaws.com"
    ]
  }' \
  https://frem.sh/_app/api/v1/orgs/acme/runners/federation

After this, any attempt to mint a token with aud not in the list is rejected and the job fails closed with an audience-rejected error. PAT scope required: runners:write.

Customising sub_claim_template

curl -X PUT -H "Authorization: Bearer $FREMFORGE_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "sub_claim_template": "tenant:{{tenant}}:repo:{{repo}}:branch:{{branch}}"
  }' \
  https://frem.sh/_app/api/v1/orgs/acme/runners/federation

The four supported substitutions are:

TokenResolves toExample
{{tenant}}The org slugacme
{{repo}}<org>/<repo>acme/api
{{branch}}Branch name (only when ref_type=branch; empty for tags)main
{{ref_type}}branch or tagbranch

Unknown placeholders are left literal in the output, for example, {{nope}} stays as {{nope}} in the rendered sub. This is intentional: a misconfigured template fails loudly at the trust-policy match step rather than silently emitting an empty string.

Resetting to defaults

Send an empty allowed_audiences and null sub_claim_template:

curl -X PUT -H "Authorization: Bearer $FREMFORGE_PAT" \
  -H "Content-Type: application/json" \
  -d '{ "allowed_audiences": [], "sub_claim_template": null }' \
  https://frem.sh/_app/api/v1/orgs/acme/runners/federation

All updates are recorded in the tenant audit log under runner_federation.updated. The body snapshot omits the full sub_claim_template but records whether one was set.

Scaleway IAM External App

Scaleway is a French-headquartered EU-sovereign cloud with data residency in FR / NL / PL. Their IAM External Apps primitive (GA October 2024) accepts an upstream OIDC IdP and lets you bind workload principals via JWT claims, the same federation shape AWS / GCP / Azure offer.

Step 1: Create an IAM External App

In the Scaleway console: IAM → External Apps → Add an External App. Configure:

  • Issuer URL, the fremforge issuer URL from the wizard (e.g. https://frem.sh/_app/runner/oidc).
  • JWKS URI, the fremforge JWKS URI.
  • Subject mapping, bind on sub. Constrain to one repo if possible (repo:<your-slug>/<repo>:*).
  • Audience, your Scaleway organisation URN (scw:org:<org-id>).

Step 2: Attach a Policy

Attach a Scaleway Policy to the External App granting the actions your CI needs. Scope to a single Project for least-privilege.

Step 3: Use in a workflow

- name: Get Scaleway STS token
  id: scw-token
  uses: frem.sh/mirrors/actions-oidc@v1
  with:
    audience: scw:org:<org-id>
- name: Deploy
  env:
    SCW_ACCESS_KEY: ${{ steps.scw-token.outputs.access_key }}
    SCW_SECRET_KEY: ${{ steps.scw-token.outputs.secret_key }}
  run: ./deploy.sh

The token-exchange endpoint shape is documented in the Scaleway IAM docs, the External App primitive emits short-lived STS-style keys keyed on the OIDC subject.

Generic OIDC broker (Authentik, Keycloak)

For setups where the cloud doesn’t speak workload OIDC directly (legacy SAML-only enterprise targets, on-prem deployers, multi-cloud trust maps maintained centrally), use a broker: Authentik, Keycloak, or any OIDC-RP-with-downstream-RP setup.

When to use a broker

  • You already run Authentik / Keycloak for human SSO and want CI workloads under the same federation surface.
  • You want one trust-policy registration to manage, not N per-cloud. Adding a new cloud is then “add a new Authentik provider,” not “redo the trust policy in each cloud.”
  • Your target is SAML-only (some on-prem regulated systems). The broker bridges OIDC → SAML.
  • You’re targeting a cloud without first-class workload-OIDC (Stackit, OVH, Hetzner), federate via the broker rather than wait for first-class support.

Step 1: Add fremforge as an upstream source

In Authentik: Directory → Federation → OAuth/OIDC Source. Set:

  • OIDC Provider, issuer URL from the wizard.
  • Audience, leave empty or set per your downstream provider.
  • Scopes, openid is sufficient; fremforge tokens carry no profile/email scopes.

The Keycloak equivalent is Identity Providers → OpenID Connect v1.0 with Use JWKS URL enabled.

Step 2: Map claims to local groups / roles

In Authentik: Customisation → Property Mappings → OIDC. Map the runner JWT’s sub (shape repo:<slug>/<repo>:ref:refs/heads/<branch>) onto local groups using a small Python expression:

# Example: anyone deploying main → 'prod-deploy' group
import re
if re.match(r'^repo:[^/]+/[^:]+:ref:refs/heads/main$', token.get('sub', '')):
    return ['prod-deploy']
return []

Step 3: Use the broker as the downstream-cloud issuer

Your cloud’s trust policy then matches on a token signed by Authentik, not by fremforge directly. Audience = the Authentik Provider’s client_id. Exchange the fremforge JWT for an Authentik-signed JWT via Authentik’s /application/o/token/ endpoint:

- name: Get fremforge runner OIDC token
  id: ff-token
  uses: frem.sh/mirrors/actions-oidc@v1
  with: { audience: <authentik-provider-client-id> }
- name: Exchange for Authentik token
  id: ak-token
  run: |
    curl -fsS -X POST https://idp.acme.com/application/o/token/ \
      -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
      -d subject_token=${{ steps.ff-token.outputs.token }} \
      -d subject_token_type=urn:ietf:params:oauth:token-type:jwt \
      -d audience=<downstream-rp> \
      | jq -r .access_token > /tmp/ak-token
- name: Deploy
  env:
    BROKER_TOKEN: $(cat /tmp/ak-token)
  run: ./deploy.sh

Trade-off

The broker is one extra hop in the runner→cloud path (more latency, one more place to debug). It pays for itself if you have ≥3 clouds, regulated SAML targets, or already run a broker for human SSO. For single-cloud setups, federate directly to the cloud (tabs above).

Cross-references

  • CI runners, hosted and BYO runner reference
  • Secrets, storing and scoping long-lived credentials
  • Security, runner isolation model and supply-chain security