Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
A

app

> 编程语言
Open source

A GitHub App that acts like a Security Token Service (STS) for the Github API

382 stars0 likes0 views
WebsiteGitHub

About

A GitHub App that acts like a Security Token Service (STS) for the Github API

octo-sts: an STS for GitHub

This repository holds a GitHub App called octo-sts that acts like a Security Token Service (STS) for the GitHub API. Using this App, workloads running essentially anywhere that can produce OIDC tokens can federate with this App's STS API in order to produce short-lived tokens for interacting with GitHub.

The ultimate goal of this App is to wholly eliminate the need for GitHub Personal Access Tokens (aka PATs).

The original blog post and the page on Chainguard Academy.

Setting up workload trust

For the App to produce credentials that work with resources in your organization it must be installed into the organization and have access to any repositories that you will want workloads to be able to interact with. Unfortunately due to limitations with GitHub Apps, the App must ask for a superset of the permissions needed for federation, so the full set of permissions the App requests will be large, but with one exception (contents: read reading policy files) the App only creates tokens with these scopes based on the "trust policies" you have configured.

The Trust Policy

Trust policies are checked into .github/chainguard/{name}.sts.yaml, and consist of a few key parts:

  1. The claim matching criteria for federation,
  2. The permissions to grant the identity, and
  3. (for Org-level policies) The list of repositories to grant access.

Here is a simple example that allows the GitHub actions workflows in chainguard-dev/foo running on the main branch to read the repo contents and interact with issues:

# yaml-language-server: $schema=https://raw.githubusercontent.com/octo-sts/app/refs/heads/main/pkg/octosts/octosts.TrustPolicy.json

issuer: https://token.actions.githubusercontent.com
subject: repo:chainguard-dev/foo:ref:refs/heads/main

permissions:
  contents: read
  issues: write

The Trust Policy can also match the issuer, subject, and even custom claims with regular expressions. For example:

# yaml-language-server: $schema=https://raw.githubusercontent.com/octo-sts/app/refs/heads/main/pkg/octosts/octosts.TrustPolicy.json

issuer: https://accounts.google.com
subject_pattern: "[0-9]+"
claim_pattern:
  email: ".*@chainguard.dev"

permissions:
  contents: read

This policy will allow OIDC tokens from Google accounts of folks with a Chainguard email address to federate and read the repo contents.

Every pattern is matched against the whole value: subject_pattern: "[0-9]+" accepts 123 but not 123x, and refs/heads/main|refs/heads/develop accepts exactly those two refs. Do not add ^ or $ yourself.

Autocomplete

JSONSchemas are available to aid in IDE autocompletion:

  • TrustPolicy
  • OrgTrustPolicy
  • OrgTrustedIssuers
VSCode

We recommend using vscode-yaml. This will read the # yaml-language-server: $schema=... header and provide code completion.

Organization Trusted Token Issuers

An organization restricts which OIDC issuers can federate with its repositories. Add /.github/chainguard/trusted-token-issuers.yaml to the organization's policy repository (.github by default; see ORG_POLICY_REPO below). octo-sts rejects a token with an issuer that the file does not permit. It rejects the token before it reads the trust policy.

Configuration

Variable Default Description
ORG_POLICY_REPO .github Repository within the organization that holds the org-issuer allowlist. Must exist and be accessible to the App before enforcement takes effect.

Warning: migration hazard. If you set ORG_POLICY_REPO=my-policies while the allowlist is still in .github, octo-sts reads my-policies instead. That file does not exist yet, so the read returns 404. A 404 is treated as "no allowlist" — meaning all issuers are permitted for that organization, silently. Create or move the allowlist into the new repository and protect it before flipping ORG_POLICY_REPO.

Note: two-binary config skew. The exchange service and the webhook validator each read ORG_POLICY_REPO from their own deployment config. If you set ORG_POLICY_REPO on only one of the two, the webhook validates a different repository than the exchange enforces. Update both deployments together.

…

octo-sts matches an entry in issuers exactly. It matches an entry in issuer_patterns as an anchored regular expression against the whole issuer URL. An issuer passes if it matches either list. Matching is case-sensitive, so write the scheme and the host in lowercase.

octo-sts rejects a pattern that can match /. The / character separates the host from the path. A pattern that matches / can therefore reach past the host. Anchors at both ends do not prevent this, because a pattern can stay loose in the middle.

octo-sts parses each pattern with the same parser that Go's regexp package uses. It then applies two rules:

  • It rejects a character class or a . that can match /, at any position. This covers \S, [^\n], [[:ascii:]] and a range such as [.-9].
  • It rejects a literal / inside a repetition, an optional group, or an alternation. A group such as ([a-z0-9.-]+/)* repeats across separators.

A literal / at a fixed position is correct. Every issuer with a path has one.

These rules catch a common mistake: a pattern that spans into another domain. https://.*.example.com reads as "any subdomain of example.com". It also permits https://evil.attacker-example.com, because the unescaped . before example matches -. It also permits https://totally-evil.com/x.example.com, where an attacker controls the whole host.

Write a literal dot as \.. Give an explicit character class for the part that varies. https://[a-z0-9-]+\.example\.com expresses "subdomains of example.com", and octo-sts accepts it. The error message names the pattern that failed.

Write a path one segment at a time, with literal separators. Use https://example\.com/realms/[a-z0-9-]+. Do not use https://example\.com/[a-z0-9/-]+. octo-sts deliberately gives you no way to match a path of variable depth.

This is a best-effort guard, not a guarantee. It catches accidents. It does not stop a determined author. Write your patterns as narrowly as you can. An organization owner writes this file, and the control serves that owner. An over-broad pattern is therefore an unwise choice, not an attack.

No file means no restriction. An organization that adds no file sees no change.

Rolling it out

  1. Commit the file with mode: audit. octo-sts rejects nothing in audit mode.
  2. Read the logs and the exchange events. octo-sts records each token that the allowlist does not permit.
  3. Check that your list is complete.
  4. Remove the mode line to enforce.

Audit mode does not protect you against a broken file or a failed lookup.

Requirement: the App must be able to read the policy repository

octo-sts reads the allowlist with a short-lived contents: read token. The token is scoped to the ORG_POLICY_REPO repository (.github by default). Enforcement silently does not apply if octo-sts cannot read that repository. The most common cause is an App that is installed on selected repositories only.

octo-sts cannot report which cause it hit. GitHub answers a token request for an inaccessible repository with one 422 status. That status does not separate "the repository does not exist" from "you do not have access".

Grant the App access to the policy repository before you rely on this control.

When the lookup fails

Situation Result
File absent octo-sts permits all issuers
No App can read ORG_POLICY_REPO octo-sts permits all issuers and logs a warning. It first re-reads the installation list from GitHub, without its local cache. See effect 4 below
Rate limited, or GitHub unavailable octo-sts uses the last known good allowlist. If there is none, it rejects the exchange
File present but invalid octo-sts uses the last known good allowlist. If there is none, it rejects every exchange in the organization

This caching creates four timing effects.

  1. After you narrow the allowlist, octo-sts can still serve the previous, broader list for up to one hour. This happens whenever GitHub returns an error.
  2. After you fix an invalid file, rejections can continue for up to five minutes. The cached result must expire first.
  3. When you enable the allowlist, octo-sts can permit all issuers for up to one hour.
  4. After you install the App, enforcement can take a few minutes to start.

Plan for effect 3. "No file" is itself a valid last known good state. An organization that adds its first allowlist during a GitHub incident can therefore keep permitting all issuers for up to one hour. This behavior is deliberate. It keeps the organizations that do not use this feature working through the incident. One successful read replaces the state. Check that enforcement started. Do not assume it started.

Effect 4 has a different cause. Before octo-sts concludes that no installation can read the policy repository, it re-reads the installation list from GitHub without its local cache. GitHub does not list a new installation at once. octo-sts cannot see an installation that GitHub has not yet propagated.

Hardening

This control is only as strong as write access to the policy repository.

  • Create the policy repository before you need it. GitHub does not reserve repository names. In an organization without this repository, any member who can create a repository becomes the sole author of this control. Restrict who can create repositories.

  • Protect the default branch of the policy repository. Make the Trust Policy Validation check a required status check. The check validates this file on every pull request. It only reports. It blocks nothing until you make it required.

  • Add a CODEOWNERS entry for the file.

  • Scope organization-level trust policies with repositories:. An organization-level policy that grants contents: write without a repositories: restriction covers every repository the installation can see. That includes the policy repository. A federated identity can then rewrite the allowlist that constrains it. Any permission that writes, renames, or deletes the allowlist file defeats this control. Deletion of the file fails open.

  • .github-private is not consulted. The file must live in ORG_POLICY_REPO.

Federating a token

The GitHub App implements the Chainguard SecurityTokenService GRPC service definition here.

If a ${TOKEN} suitable for federation is sent like so:

curl -H "Authorization: Bearer ${TOKEN}" \
  "https://octo-sts.dev/sts/exchange?scope=${REPO}&identity=${NAME}"

The App will attempt to load the trust policy from .github/chainguard/${NAME}.sts.yaml from ${REPO} and if the provided ${TOKEN} satisfies those rules, it will return a token with the permissions in the trust policy.

Release cadence

Our release cadence at this moment is set to when is needed, meaning if we have a bug fix or a new feature we will might make a new release.

Container images

For self-hosting, container images are published to ghcr.io/octo-sts/app and ghcr.io/octo-sts/webhook. A release tagged vX.Y.Z is pushed as X.Y.Z and latest.

Images are signed keylessly with [cosign](https://github.com/

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Go

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言