sts-cat 0.2.0

Yet another GitHub STS, aims to be easy-self-hosting and cloud-agnostic
Documentation
# sts-cat

OIDC-to-GitHub-token exchange service. A Rust reimplementation of [octo-sts](https://github.com/octo-sts/app) designed for easy self-hosting on AWS (Lambda, ECS) or any environment with a TCP socket.

sts-cat accepts OIDC JWT tokens from any identity provider, validates them against trust policies stored in GitHub repositories, and returns scoped GitHub installation access tokens.

## Setup

### Prerequisites

- A [GitHub App]https://docs.github.com/en/apps/creating-github-apps with the desired permissions
- The GitHub App's private key (PEM file) or an AWS KMS asymmetric signing key

### Configuration

All configuration is via environment variables:

| Variable | Required | Description |
|---|---|---|
| `STS_CAT_GITHUB_APP_ID` | Yes | GitHub App ID |
| `STS_CAT_IDENTIFIER` | Yes | Identifier used as default audience (e.g. `https://sts.example.com`) |
| `STS_CAT_GITHUB_API_URL` | No | GitHub API base URL (default: `https://api.github.com`) |
| `HOST` | No | Listen host (default: `0.0.0.0`). Ignored in Lambda mode. |
| `PORT` | No | Listen port (default: `8080`). Ignored in Lambda mode. |
| `STS_CAT_LOG_JSON` | No | Enable JSON-formatted logging |
| `STS_CAT_KEY_SOURCE` | Yes | Signing key source: `file`, `env`, or `aws-kms` |
| `STS_CAT_KEY_FILE` | When `file` | Path to the GitHub App PEM private key |
| `STS_CAT_KEY_ENV` | When `env` | Name of env var containing the PEM private key |
| `STS_CAT_AWS_KMS_KEY_ARN` | When `aws-kms` | ARN of the AWS KMS asymmetric signing key |
| `STS_CAT_POLICY_PATH_PREFIX` | No | Path prefix within repos for trust policy files (default: `.github/sts-cat`) |
| `STS_CAT_POLICY_FILE_EXTENSION` | No | File extension for trust policy files (default: `.sts.toml`) |
| `STS_CAT_ALLOWED_ISSUER_URLS` | No | Comma-separated list of allowed OIDC issuer URLs |
| `STS_CAT_ORG_REPO` | No | Comma-separated `org/repo` pairs to override org-level policy repository |

### Running

```bash
# HTTP server mode
STS_CAT_GITHUB_APP_ID=12345 \
STS_CAT_IDENTIFIER=https://sts.example.com \
STS_CAT_KEY_SOURCE=file \
STS_CAT_KEY_FILE=/path/to/private-key.pem \
sts-cat-http

# Docker
docker run \
  -e STS_CAT_GITHUB_APP_ID=12345 \
  -e STS_CAT_IDENTIFIER=https://sts.example.com \
  -e STS_CAT_KEY_SOURCE=file \
  -e STS_CAT_KEY_FILE=/app/private-key.pem \
  -v /path/to/private-key.pem:/app/private-key.pem:ro \
  sts-cat
```

### Lambda Deployment

Build with the `aws-lambda` feature:

```bash
cargo lambda build --release
```

Deploy the `sts-cat-lambda` binary as a Lambda function with Function URL (`AuthType=NONE`).

## API

### Token Exchange

```
POST /token HTTP/1.1
Authorization: Bearer <oidc-jwt>
Content-Type: application/json

{"scope": "org/repo", "identity": "my-policy"}
```

Response:

```json
{"token": "ghs_xxx..."}
```

- `scope`: `"org/repo"` for repository-level, or `"org"` for organization-level policies
- `identity`: Name of the trust policy file (without extension)

### Health Check

```
GET /healthz
```

Returns `{"ok": true}` with HTTP 200.

### Usage from GitHub Actions

```yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - name: Get token from sts-cat
        uses: actions/github-script@v7
        id: sts-cat
        with:
          script: |
            const idToken = await core.getIDToken('https://sts.example.com');
            const resp = await fetch('https://sts.example.com/token', {
              method: 'POST',
              headers: {
                'Authorization': `Bearer ${idToken}`,
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                scope: '${{ github.repository }}',
                identity: 'deploy',
              }),
            });
            if (!resp.ok) {
              throw new Error(`sts-cat returned ${resp.status}: ${await resp.text()}`);
            }
            const { token } = await resp.json();
            core.setSecret(token);
            core.setOutput('github_token', token);
          result-encoding: string

      - name: Use the token
        env:
          GITHUB_TOKEN: ${{ steps.sts-cat.outputs.github_token }}
        run: |
          gh api repos/myorg/myrepo/pulls
```

The audience passed to `core.getIDToken()` must match the `STS_CAT_IDENTIFIER` value (or the `audience` field in the trust policy).

## Trust Policies

Trust policies are TOML files stored at `{policy_path_prefix}/{identity}{policy_file_extension}` in the target repository (or the `.github` repo for org-level policies).

### Repository-level example

`.github/sts-cat/deploy.sts.toml`:

```toml
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:myorg/myrepo:ref:refs/heads/main"

[permissions]
contents = "read"
pull_requests = "write"
```

### Organization-level example

In the `.github` repo, `.github/sts-cat/ci.sts.toml`:

```toml
issuer = "https://token.actions.githubusercontent.com"
subject_pattern = "repo:myorg/.*:ref:refs/heads/main"
repositories = ["repo-a", "repo-b"]

[permissions]
contents = "read"
```

### Trust Policy Fields

| Field | Type | Required | Description |
|---|---|---|---|
| `issuer` | string | One of `issuer`/`issuer_pattern` | Exact match for the OIDC token issuer |
| `issuer_pattern` | string | | Regex pattern for issuer |
| `subject` | string | One of `subject`/`subject_pattern` | Exact match for the OIDC token subject |
| `subject_pattern` | string | | Regex pattern for subject |
| `audience` | string | Optional | Exact match for at least one token audience |
| `audience_pattern` | string | | Regex pattern for audience |
| `claim_pattern` | table | Optional | Map of claim names to regex patterns |
| `permissions` | table | Required | GitHub permission keys and access levels |
| `repositories` | array | Org-level only | Scoped list of repositories |

All regex patterns use Rust `regex` crate syntax and are automatically anchored with `^...$`.

## Building

```bash
# Default (includes both aws-kms and aws-lambda)
cargo build --release

# HTTP server only (no AWS KMS or Lambda support)
cargo build --release --no-default-features

# Lambda binary via cargo-lambda
cargo lambda build --release
```

## Feature Flags

| Feature | Default | Description |
|---|---|---|
| `aws-kms` | On | Enables AWS KMS signer |
| `aws-lambda` | On | Enables `sts-cat-lambda` binary |

## License

(c) Sorah Fukumori https://sorah.jp/

Apache 2.0 License unless otherwise noted.

- Code snippets in this README are licensed under CC0-1.0 universal. https://spdx.org/licenses/CC0-1.0.html