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

# Kubernetes

> Call Reducto from Kubernetes pods with a projected service account token instead of a stored API key.

Kubernetes can mount a short-lived, signed service account token into a pod. Reducto verifies the token against your cluster's public keys and returns an access token. This guide covers self-managed clusters. For managed clusters, see the [EKS](https://docs.reducto.ai/security/workload-identity-federation/aws.md#amazon-eks), [AKS](https://docs.reducto.ai/security/workload-identity-federation/azure.md#azure-kubernetes-service-aks) and [GKE](https://docs.reducto.ai/security/workload-identity-federation/google-cloud.md#google-kubernetes-engine-gke) sections.

Use projected service account tokens. Legacy service account tokens stored in Secrets do not expire and have the wrong audience, so Reducto cannot use them.

## Set up the cluster

Create a service account for the workload:

```bash theme={null}
kubectl create serviceaccount reducto-client --namespace payments
```

Get the cluster's issuer and its public signing keys:

```bash theme={null}
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
kubectl get --raw /openid/v1/jwks > cluster-jwks.json
```

Many self-managed clusters use a private issuer such as `https://kubernetes.default.svc` or `https://kubernetes.default.svc.cluster.local`. Reducto cannot fetch discovery from a private issuer, so upload the JWKS for these clusters.

Mount a projected token with the Reducto audience. The kubelet refreshes the file before it expires.

```yaml theme={null}
apiVersion: v1
kind: Pod
metadata:
  name: invoice-parser
  namespace: payments
spec:
  serviceAccountName: reducto-client
  containers:
    - name: app
      image: ghcr.io/acme/invoice-parser:1.4.0
      env:
        - name: REDUCTO_IDENTITY_PROVIDER_ID
          value: wip_8f2k3m9q1r5t7v0x2z4b6d8f
      volumeMounts:
        - name: reducto-token
          mountPath: /var/run/secrets/reducto
          readOnly: true
  volumes:
    - name: reducto-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              audience: https://platform.reducto.ai
              expirationSeconds: 3600
```

## Inspect the token

```bash theme={null}
kubectl exec -n payments invoice-parser -- cat /var/run/secrets/reducto/token \
  | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
```

```json theme={null}
{
  "iss": "https://k8s.acme.internal",
  "aud": ["https://platform.reducto.ai"],
  "sub": "system:serviceaccount:payments:reducto-client",
  "iat": 1793322245,
  "nbf": 1793322245,
  "exp": 1793325845,
  "kubernetes.io": {
    "namespace": "payments",
    "pod": {
      "name": "invoice-parser",
      "uid": "6f1c2a7e-3b4d-4e5f-8a9b-0c1d2e3f4a5b"
    },
    "serviceaccount": {
      "name": "reducto-client",
      "uid": "2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a"
    }
  }
}
```

`sub` has the form `system:serviceaccount:<namespace>:<service account>`.

## Configure Reducto

In [Studio](https://studio.reducto.ai), open **Settings > Workload identity** and create a provider with the **Kubernetes** preset:

| Field | Value |
| - | - |
| **Name** | `k8s-prod` |
| **Issuer** | The issuer from `/.well-known/openid-configuration`, for example `https://k8s.acme.internal` |
| **Audience** | `https://platform.reducto.ai` |
| **Key source** | **Uploaded JWKS** with the contents of `cluster-jwks.json`. Use OIDC discovery only if the issuer serves discovery on the public internet over HTTPS. |

Add a mapping for the service account:

| Key | Value |
| - | - |
| `sub` | `system:serviceaccount:payments:reducto-client` |

To allow every service account in a namespace, use `sub` = `system:serviceaccount:payments:*`. To match on the nested namespace claim, add a transformation with `attribute` = `reducto.namespace` and `claim` = `["kubernetes.io", "namespace"]`, then match `reducto.namespace` = `payments`.

## Call Reducto from the pod

```python Python theme={null}
import os

from reducto import Reducto
from reducto.auth import k8s_service_account_token_provider

client = Reducto(
    workload_identity={
        "identity_provider_id": os.environ["REDUCTO_IDENTITY_PROVIDER_ID"],
        "provider": k8s_service_account_token_provider("/var/run/secrets/reducto/token"),
    }
)

result = client.parse.run(input="https://cdn.reducto.ai/samples/fidelity-example.pdf")
print(result.result.chunks[0].content[:500])
```

```javascript Node.js theme={null}
import Reducto from "reductoai";
import { k8sServiceAccountTokenProvider } from "reductoai/auth";

const client = new Reducto({
  workloadIdentity: {
    identityProviderId: process.env.REDUCTO_IDENTITY_PROVIDER_ID,
    provider: k8sServiceAccountTokenProvider("/var/run/secrets/reducto/token"),
  },
});

const result = await client.parse.run({
  input: "https://cdn.reducto.ai/samples/fidelity-example.pdf",
});
console.log(result.result.chunks[0].content.slice(0, 500));
```

```bash cURL theme={null}
ACCESS_TOKEN=$(jq -n \
  --arg subject_token "$(cat /var/run/secrets/reducto/token)" \
  --arg provider_id "$REDUCTO_IDENTITY_PROVIDER_ID" \
  '{
    grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
    subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
    subject_token: $subject_token,
    identity_provider_id: $provider_id
  }' \
  | curl -sS --fail-with-body -X POST "https://platform.reducto.ai/oauth/token" \
      -H "Content-Type: application/json" -d @- \
  | jq -r .access_token)

curl -X POST "https://platform.reducto.ai/parse" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input":"https://cdn.reducto.ai/samples/fidelity-example.pdf"}'
```

The token provider reads the file on every exchange, so it always sends the token the kubelet wrote last. Instead of code, you can set `REDUCTO_IDENTITY_TOKEN_FILE=/var/run/secrets/reducto/token` next to `REDUCTO_IDENTITY_PROVIDER_ID` and create the client with no arguments.

## Kubernetes best practices

* Use a dedicated service account for Reducto. Do not reuse one that has other cloud or cluster permissions.
* Match the full `sub`. Use a namespace wildcard only if everyone who can create service accounts in that namespace should have Reducto access.
* Keep the issuer stable across cluster upgrades. A changed issuer breaks every exchange.
* With an uploaded JWKS, update the provider before the cluster rotates its service account signing key. Upload both keys during the rotation.
* Protect the service account signing key. Anyone who has it can mint tokens that Reducto accepts.
