> ## 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.

# Kubernetes (self-managed, EKS, GKE, AKS)

> Authenticate from a Kubernetes pod to KnoxCall using its projected service-account token — including clusters whose issuer is unreachable from the internet

# Kubernetes Workload Identity

Every pod can be given a **projected service-account token**: a short-lived OIDC
token, signed by the cluster, that names the service account it was issued to. KnoxCall
accepts it via RFC 8693 token exchange, so a pod authenticates with no stored
KnoxCall credential at all.

The part that makes Kubernetes different from GitHub Actions or GCP is **where your
cluster publishes its signing keys**.

## Which setup do you have?

<Note>
  Most self-managed clusters land in the second row. If you are not sure, run
  `kubectl get --raw /.well-known/openid-configuration` and look at the `jwks_uri` it
  returns — then ask whether that URL is reachable from the public internet.
</Note>

| Your cluster                                                                                                                     | JWKS source to choose                                                                       |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| A managed cluster with a **public** OIDC issuer (EKS with an IAM OIDC provider, GKE Workload Identity, AKS with a public issuer) | **Discovery** — the default. Nothing extra to configure.                                    |
| A **self-managed** cluster, or any cluster whose issuer is only reachable inside the network (`https://kubernetes.default.svc`)  | **Inline** — paste the cluster's public keys. KnoxCall never makes a network call for them. |
| An issuer whose keys are published at a public URL, but whose discovery document is missing or wrong                             | **Explicit URL**                                                                            |

`https://kubernetes.default.svc` is the default issuer string for *every* cluster, so it
is not unique to you. KnoxCall stores your keys against your tenant and your issuer
together, and an assertion is only ever verified against **your** cluster's keys — a
different customer registering the same issuer string cannot mint into your tenant, and
you cannot mint into theirs.

## 1. Get your cluster's issuer and keys

```bash theme={"dark"}
# The issuer string your tokens will carry in `iss`
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer

# The public keys, as a JWKS document
kubectl get --raw /openid/v1/jwks
```

The second command prints something of the form `{"keys":[{...}]}`. That whole document
is what you paste for **Inline**. It contains **public** keys only — there is no private
key material in it.

## 2. Configure the issuer's JWKS source

For discovery clusters, skip this step.

<Note>
  Changing an issuer's JWKS source requires a **recent step-up verification** (passkey,
  TOTP or emailed code) within the last 5 minutes. Pasting keys is telling KnoxCall which
  signer to believe for your tenant, so it carries the same bar as creating a binding. A
  request carrying only a session JWT answers `403 {"requires_step_up": true}`.
</Note>

```bash theme={"dark"}
curl -X POST https://admin.knoxcall.com/admin/federation/issuers/jwks-source \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "X-Tenant-ID: $TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "issuer": "https://kubernetes.default.svc",
    "mode": "inline",
    "jwks": { "keys": [ { "kty": "RSA", "kid": "…", "n": "…", "e": "AQAB" } ] }
  }'
```

The request is **rejected** if the document cannot produce a usable key — KnoxCall parses
it before saving rather than letting you discover the problem on your first exchange.
For `inline` this check makes no network call at all.

<Warning>
  **Inline keys do not refresh.** When your cluster rotates its service-account signing
  keys, exchanges stop verifying until you paste the new document. KnoxCall names that
  cause specifically rather than reporting a generic signature failure, and the binding's
  status in the Dashboard shows it — but nothing rotates them for you. If your cluster
  rotates on a schedule, put this on the same schedule.
</Warning>

## 3. Create the workload binding

Bind the service accounts you want to trust. A Kubernetes `sub` looks like:

```
system:serviceaccount:<namespace>:<serviceaccount>
```

```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 '{
    "issuer": "https://kubernetes.default.svc",
    "name": "prod deployer",
    "attribute_conditions": { "sub": "system:serviceaccount:prod:deployer" },
    "allowed_scopes": ["secrets:read"]
  }'
```

You can cover a whole namespace with a trailing `*`:

```json theme={"dark"}
{ "sub": "system:serviceaccount:prod:*" }
```

On an **inline** issuer a broad prefix is accepted, because the issuer is already the
boundary — only your cluster's signing key produces a token that verifies at all, so
`system:serviceaccount:*` grants nothing your cluster could not grant itself. On a shared
public issuer the same prefix is refused, because there it would match other people's
workloads. A bare `"*"` is always refused: it names no workload.

## 4. Project the token into your pod

```yaml theme={"dark"}
apiVersion: v1
kind: Pod
spec:
  serviceAccountName: deployer
  containers:
    - name: app
      image: your/image
      env:
        - name: KNOXCALL_TENANT
          value: acme
      volumeMounts:
        - name: knoxcall-token
          mountPath: /var/run/secrets/knoxcall
          readOnly: true
  volumes:
    - name: knoxcall-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              # The audience MUST match the binding's audience.
              audience: knoxcall:api
              # Short is good: the minted token can never outlive the assertion.
              expirationSeconds: 600
```

<Warning>
  Set `audience:` explicitly. A projected token with the cluster's default audience will not
  match your binding, and the exchange refuses it — correctly, but the message reads like a
  credential problem rather than a manifest one.
</Warning>

## 5. Exchange it

The token file is re-read **before every exchange**, never once at startup: KnoxCall
assertions are single-use, so replaying the same bytes is refused. The SDKs'
`WorkloadCredentialProvider` does this for you, including refreshing before expiry.

```javascript theme={"dark"}
import { WorkloadCredentialProvider } from '@knoxcall/sdk';
import { readFile } from 'node:fs/promises';

const provider = new WorkloadCredentialProvider({
  tenant: process.env.KNOXCALL_TENANT,
  // Re-read on EVERY call — kubelet rewrites this file as it rotates the token.
  assertion: () => readFile('/var/run/secrets/knoxcall/token', 'utf8'),
});

const token = await provider.getAccessToken();
```

Doing it by hand is one request:

```bash theme={"dark"}
curl -X POST https://api.knoxcall.com/oauth/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
  -d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
  -d audience=knoxcall:api \
  --data-urlencode subject_token@/var/run/secrets/knoxcall/token
```

## Troubleshooting

| What you see                                            | What it means                                                                                                                                  |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `subject_token has no kid`                              | The JWS header names no key. Every mainstream cluster sets it; if yours does not, it is a signing configuration option.                        |
| `no binding matched the subject_token's claims`         | The `sub` does not match any binding — check the namespace and service-account name, and that `audience:` in the manifest matches the binding. |
| `the issuer publishes no signing keys KnoxCall can use` | For inline, the pasted document had no importable key. Re-run `kubectl get --raw /openid/v1/jwks`.                                             |
| Exchanges that used to work now fail to verify          | On an **inline** issuer, your cluster has rotated its keys. Paste the current JWKS.                                                            |
| `subject_token has already been exchanged`              | The same token bytes were sent twice. Re-read the file before each exchange rather than caching its contents.                                  |
