> ## Documentation Index
> Fetch the complete documentation index at: https://docs.knoxcall.com/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub Actions

> Authenticate from GitHub Actions to KnoxCall with zero stored secrets via OIDC workload identity federation

# GitHub Actions Workload Identity

GitHub Actions can exchange its workflow OIDC token for a KnoxCall access token via RFC 8693 — no secrets stored in the repo, organization, or environment.

## 1. Add the workflow permission

In your `.github/workflows/*.yml`:

```yaml theme={"dark"}
permissions:
  id-token: write   # required to mint the GHA OIDC token
  contents: read    # standard
```

## 2. Configure the trust binding in KnoxCall

In the dashboard, open **Settings → API → Workload Identity → Connect workload** — the wizard checks your issuer, builds the rule, and lets you test a real token before your first run — or call the admin API directly. The `/admin/*` routes are served on the admin host (`admin.knoxcall.com` / any `knoxcall.com` host) and are authenticated by your logged-in admin/owner session — a session JWT plus the `X-Tenant-ID` header — not an API key against `api.knoxcall.com`.

<Note>
  Creating a binding requires a **recent step-up verification** (passkey, TOTP or
  emailed code) within the last 5 minutes — a binding is a trust that lets an
  external workload mint tenant tokens, so it carries the same bar as creating an
  OAuth client. A `curl` carrying only a session JWT answers
  `403 {"requires_step_up": true}`: verify in the Dashboard (any action that
  prompts for your passkey or TOTP), then replay the request inside the 5-minute
  window. Each verification is single-use, so a retried request needs a fresh one.
  Listing and revoking bindings need no verification — containment must never be
  gated.
</Note>

```bash theme={"dark"}
curl -X POST https://admin.knoxcall.com/admin/oauth/workload-bindings \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "X-Tenant-ID: $TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "oauth_client_id": "<your_oauth_client_id>",
    "name": "gha-acme-api-deploy",
    "issuer": "https://token.actions.githubusercontent.com",
    "audience": "knoxcall:api",
    "attribute_conditions": {
      "repository": "acme/api",
      "ref": "refs/heads/main",
      "workflow": "deploy"
    },
    "allowed_scopes": ["routes:write", "secrets:read"],
    "access_token_ttl_seconds": 3600
  }'
```

**Attribute conditions are required.** They prevent any caller on `token.actions.githubusercontent.com` (i.e., any GitHub repository in the world) from minting tokens for your tenant. Always lock down at minimum `repository`; lock to `workflow` + `ref` for production paths.

**`allowed_scopes` is required and may not be empty.** A binding with no scopes is not a binding that grants nothing — a token with an empty scope is treated as *unnarrowed*, so it would reach every endpoint your tenant has. Creating one is refused with `400 invalid_allowed_scopes`, and a binding that somehow holds an empty list refuses to exchange. List exactly the scopes the workflow needs.

## 3. Call KnoxCall from the workflow

```yaml theme={"dark"}
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install @knoxcall/sdk
      - run: node deploy.js
        env:
          KNOXCALL_TENANT: acme
```

In `deploy.js`:

```javascript theme={"dark"}
import { KnoxCall } from "@knoxcall/sdk";
const client = new KnoxCall({ tenant: process.env.KNOXCALL_TENANT });
const routes = await client.routes.list();
```

The SDK auto-detects `ACTIONS_ID_TOKEN_REQUEST_URL` + `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, fetches the OIDC token, exchanges it for a 1-hour KnoxCall access token, and uses it for all API calls. Zero stored secrets anywhere in your repo or organization.

## What's in the OIDC token

GitHub puts these claims in every workflow OIDC token; you can match on any of them in `attribute_conditions`:

| Claim              | Example                                                 | Use for                                        |
| ------------------ | ------------------------------------------------------- | ---------------------------------------------- |
| `repository`       | `acme/api`                                              | Always include — locks the binding to one repo |
| `repository_owner` | `acme`                                                  | Permit any repo under an org                   |
| `ref`              | `refs/heads/main`                                       | Permit production branch only                  |
| `ref_type`         | `branch`                                                | Block tag pushes                               |
| `event_name`       | `push` / `pull_request` / `workflow_dispatch`           | Block PRs from forks                           |
| `workflow`         | `deploy`                                                | Permit one workflow only                       |
| `job_workflow_ref` | `acme/api/.github/workflows/deploy.yml@refs/heads/main` | Strictest match                                |
| `environment`      | `production`                                            | Permit only protected environments             |
| `sub`              | `repo:acme/api:ref:refs/heads/main`                     | Composite — useful when you want exact match   |

## Recommendations

* For production paths: require `environment` + `ref` + `repository`. GitHub Environments add a manual-approval gate you should leverage.
* For PR / CI paths: require `event_name != pull_request` to avoid third-party fork PRs minting tokens.
* Use one binding per logical workload, not one per repo. Granular bindings make `last_used_at` data actually useful.

## Troubleshooting

* **`invalid_grant: no binding matched`** — JWT attributes don't match. Fetch and decode the OIDC token to inspect its claims and compare them to your binding:

  ```bash theme={"dark"}
  curl -s -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
    "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=knoxcall:api" \
    | python3 -c 'import sys,json,base64; t=json.load(sys.stdin)["value"].split("."); print(base64.urlsafe_b64decode(t[1]+"=="*(-len(t[1])%4)).decode())'
  ```
* **`invalid_request: subject_token signature invalid`** — Audience mismatch. The SDK appends `&audience=knoxcall:api` to the OIDC request — make sure your binding's `audience` field is exactly `knoxcall:api`.
* **`invalid_request: subject_token expired`** — Clock skew. Ensure your runner's clock is sane (NTP).
