Pull Private Cross-Repo Modules in CI
Use Atmos Pro STS to mint short-lived, read-only GitHub tokens so CI can pull private Terraform modules and components from your other repos — no PATs, no second GitHub App, and no Terraform code changes.
When a stack vendors a component or references a Terraform module that lives in a different private repository, CI needs credentials to read that repo. The usual options — a Personal Access Token or a dedicated GitHub App — are long-lived and almost always scoped broader than "read these modules." That is exactly the credential a leak weaponizes: a PAT that surfaces in a build log, a compromised dependency, or a misconfigured runner keeps working indefinitely and reaches everything it was granted, which an attacker can use to pivot. Storing and rotating it is the visible cost; the standing blast radius is the real one.
Atmos Pro STS removes that entirely. Your CI run exchanges its GitHub Actions OIDC identity for a short-lived, read-only GitHub token scoped only to the source repositories that have explicitly opted in. The Atmos CLI injects that token as a git credential for the duration of the run, so
atmos vendor pull, source:-provisioned components, and terraform init of a private module all resolve with no changes to your Terraform code.A source repo only becomes readable when it opts in. STS is deny-by-default: each source repo grants access by committing a small trust policy (.atmos/pro/sts/default.yaml) that says which CI identities may mint a token for it. A repo with no policy is never mintable — even read access. This keeps consent where it belongs: in the source repo, as reviewable, branch-protectable code its own owners merged, rather than a switch someone flips elsewhere. Making access "effortless" is still one small file away (see the match-all example).
Requirements. This needs an Atmos CLI release that includes theatmos/proauth provider and thegithub/stsintegration; the Atmos Pro STS entitlement on your plan; the feature enabled on the workspace Security tab (Workspace → Settings → Security → Cross-Repo Token Broker); the Atmos Pro GitHub App installed on both the repo running CI and every source repo you want to read; and a committed trust policy in each source repo.
- 1Grant the consuming repo permission — In Atmos Pro, on the repository that runs CI, add the
ws:sts:createpermission (Repository Permissions page), just as you wouldws:commits:createfor autocommit. - 2Configure Atmos auth — Add an Atmos Pro provider and a
github/stsintegration to youratmos.yaml. - 3Grant the workflow an OIDC token — Add
id-token: writeto the workflow permissions. - 4Run Atmos as usual — Vendoring and Terraform module fetches now authenticate automatically.
The
atmos/pro provider authenticates the CLI to Atmos Pro (not to GitHub). The github/sts integration consumes that identity to broker GitHub tokens — the same pattern as the AWS provider's aws/ecr and aws/eks integrations.auth:
providers:
atmos-pro:
kind: atmos/pro # authenticate the CLI to Atmos Pro (via GitHub Actions OIDC)
base_url: https://atmos-pro.com
identities:
atmos-pro:
kind: atmos/pro
via:
provider: atmos-pro
integrations:
github-sts:
kind: github/sts # mint read-only git credentials for private sources
via:
identity: atmos-pro
spec:
# Optional narrowing allowlist. Omit to request every repo where Atmos
# Pro is installed; list repos to request only those. Either way, a repo
# is minted only if it has a matching trust policy committed (see below).
repos:
- acme/terraform-modules
- acme-labs/componentsBy default the minted token is read-only (
contents: read). The consumer never requests scopes itself — a source repo grants access (and any additional scopes) to matched consumers via its trust policy (see below), always bounded by what the Atmos Pro GitHub App holds. repos only narrows the request; it cannot grant access to a repo that hasn't opted in.The only thing your workflow declares is permission to mint an OIDC token. There is no GitHub Action to add — the Atmos CLI does the exchange, injection, and cleanup natively.
permissions:
id-token: write # required: lets the runner mint the OIDC token Atmos Pro verifies
contents: read
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cloudposse/github-action-setup-atmos@v2
- run: atmos vendor pull
- run: atmos terraform plan my-component -s my-stackThat's it. When
atmos vendor pull or atmos terraform runs, Atmos resolves the github/sts integration, mints the scoped token, and exports it as a git URL rewrite into the child process — so go-getter and Terraform's module installer both authenticate transparently.- 1The CLI mints a GitHub Actions OIDC token and exchanges it with Atmos Pro for a short-lived session.
- 2Atmos Pro verifies the OIDC token, confirms the calling repo belongs to your workspace, and checks the STS entitlement.
- 3For each requested source repo, Atmos Pro reads its required trust policy and checks the caller against it. Repos with no policy — or no matching rule — are excluded; the rest get a read-only (or policy-granted), repository-scoped GitHub App installation token (one per installation).
- 4The CLI injects the token as a per-owner git credential (
GIT_CONFIG_*URL rewrite) for the run, and revokes it when the command finishes.
GitHub installation tokens live at most ~1 hour; the CLI revokes them at the end of the run, so the credential effectively lives only for the lifecycle of the job.
STS is deny-by-default: a source repo is mintable only if it has committed a trust policy that the calling CI identity matches. No file → not mintable. The workspace toggle turns the feature on; it does not consent on a source repo's behalf — that authority lives only in the source repo's own
.atmos/pro/sts/*.yaml, so the people who own the code (and their CODEOWNERS reviewers) are the ones who grant access, in a reviewable, version-controlled file.Let Atmos Pro open the PR for you. If you use the Atmos Pro MCP (in the in-product chat or any connected client), just ask — e.g. "let acme/app read this repo via STS". The agent checks the source repo is imported, generates the policy below, and opens a pull request adding it (defaulting you as the reviewer). It will not touch a repo that isn't imported into your workspace.
A policy file lives at
.atmos/pro/sts/default.yaml in the repo being read. It is a versioned Atmos Pro manifest — apiVersion: atmos-pro.com/v1alpha1, kind: TrustPolicy, a metadata.name, and a spec.rules list. The caller need match any one rule. Within a rule, the caller must satisfy every entry in its match block, and the block must specify at least one claim (an empty match never matches). Start the file with the # yaml-language-server line so your editor gives you autocomplete and validates against the published schema:# yaml-language-server: $schema=https://atmos-pro.com/schemas/v1alpha1/trust-policy.json
apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: default # = the file name (.atmos/pro/sts/<name>.yaml)
spec:
rules:
- match:
# one or more claim → matcher entries (see below)
permissions:
# optional, owner-granted (see below)Each
match entry maps a GitHub OIDC claim to a matcher. A matcher is a glob string (the safe default — * matches any run of characters, ? matches one character, and every other character is literal, so . : / - are matched literally, not as wildcards) or an explicit { regex: "…" } opt-in (an anchored full-string regex, for when a glob can't express the intent). Prefer globs; reach for regex only when you must.The supported claims (a typo like
repositoryowner: fails validation rather than silently never matching):| Claim | Matches |
|---|---|
subject | The OIDC sub (e.g. repo:acme/app:ref:refs/heads/main) |
repository | owner/repo of the calling repo |
repository_owner | The owner (org or user) of the calling repo |
ref | The git ref (e.g. refs/heads/main) |
ref_type | branch or tag |
actor | The GitHub login that triggered the run |
workflow_ref | The calling workflow's ref |
environment | The GitHub Environment the job runs in |
sha | The commit SHA |
The OIDC issuer is always enforced to GitHub Actions (https://token.actions.githubusercontent.com) — you don't (and can't) match on it.
A rule's optional
permissions map grants scopes beyond the read-only default. Scopes are owner-granted — the consumer can never request them. Each key is a GitHub App permission name and each value is read, write, or admin. Omit permissions for the read-only default (contents: read + the always-present metadata: read). Grants are capped at what the Atmos Pro GitHub App actually holds (GitHub enforces that ceiling).The "effortless" opt-in — one committed file restores the zero-config feel, but the grant is now explicit and reviewable. Any caller Atmos Pro verifies gets a read-only token:
# .atmos/pro/sts/default.yaml — broad opt-in, read-only
# yaml-language-server: $schema=https://atmos-pro.com/schemas/v1alpha1/trust-policy.json
apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: default
spec:
rules:
- match:
subject: "*"# only callers in repos owned by `acme` may read this repo
apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: default
spec:
rules:
- match:
repository_owner: "acme"Prefer a narrow
subject over a broad one when you know the consumer. A glob with no wildcards is effectively an exact match:# only acme/infra's main-branch workflows may read this repo
apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: default
spec:
rules:
- match:
subject: "repo:acme/infra:ref:refs/heads/main" # exact; for any branch use "repo:acme/infra:*"# any acme repo, but only from the protected `production` environment
apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: default
spec:
rules:
- match:
repository_owner: "acme"
environment: "production"The caller need match any one rule:
apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: default
spec:
rules:
- match:
subject: "repo:acme/infra:ref:refs/heads/main"
- match:
repository: "acme/platform-*"
- match:
repository_owner: "acme-labs"Add a
permissions map to a rule to grant beyond the read-only default:apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: default
spec:
rules:
- match:
repository_owner: "acme"
permissions:
contents: read
pull_requests: write # e.g. let a matched consumer open PRsThe policy file name defaults to
default (→ .atmos/pro/sts/default.yaml), and metadata.name matches it. The github/sts integration can request a different name, selecting .atmos/pro/sts/<name>.yaml instead — useful for auditing distinct consumer classes ("federated as ci"):# .atmos/pro/sts/ci.yaml
apiVersion: atmos-pro.com/v1alpha1
kind: TrustPolicy
metadata:
name: ci
spec:
rules:
- match:
repository: "acme/*"The named (or default) file must exist, or the source is excluded with
no_trust_policy.Because the policy lives in the source repo, protect it. Branch protection on the default branch plus a CODEOWNERS rule for
/.atmos/pro/sts/ lets you require designated reviewers (e.g. org/security admins) to approve any change to who may mint tokens and what scopes they get:# CODEOWNERS — require admin review for trust-policy changes
/.atmos/pro/sts/ @your-org/security-admins
Prefer exact
subject matches over broad patterns, and grant the narrowest permissions that the consuming workflow needs.The minted token can only reach source repositories that have explicitly opted in with a matching trust policy, is read-only by default, and is gone within the hour (sooner, on run completion). It can never reach another organization's code, a repo that hasn't opted in, or anything beyond what the source repo's trust policy grants — capped by what the Atmos Pro GitHub App holds, so it can never exceed the App's own scopes. Every token issued is recorded in your audit log with the verified run identity — repository, ref, actor, the repos it was scoped to, and the permissions granted — while the token value itself is never logged.
That inverts the PAT risk above. If this token leaks from the runner, an attacker gets read access to a fixed set of opted-in repos for the few minutes until the job revokes it — not standing, broadly scoped access that lives until someone notices. The window and the reach are both small by construction.
If a source repo isn't resolving, check the response from the exchange: repos are reported as
excluded with a reason:no_trust_policy— the source repo has no.atmos/pro/sts/default.yaml(or the named file). Commit a trust policy that matches your CI identity (see the examples). This is the most common reason on first setup, since STS is deny-by-default.trust_policy_denied— the repo has a policy, but your workflow's verified identity matched no rule. Compare your OIDCsubject/claims against the policy's matchers.trust_policy_invalid— the policy file is present but doesn't parse: bad YAML, a wrongapiVersion, an unknownkind, or aspecthat violates the schema (e.g. a misspelled claim, or apermissionsvalue that isn'tread/write/admin). STS fails closed, so fix the file before access resumes.not_installed_in_workspace— the Atmos Pro GitHub App isn't installed on that repo for this workspace.
Confirm the Atmos Pro GitHub App is installed on the source repo, the STS entitlement and workspace toggle are enabled, and the source repo has a committed trust policy your
subject/claims match.