---
title: OIDC connectors | Runware Docs
url: https://runware.ai/docs/serverless/oidc-connectors
description: Let your workers call AWS, Google Cloud or your own services with short-lived tokens that Runware signs, instead of cloud keys you store and rotate.
relatedDocuments:
  - https://runware.ai/docs/serverless/api-oidc-connectors
  - https://runware.ai/docs/serverless/secrets
  - https://runware.ai/docs/serverless/environments
---
## Introduction

An OIDC connector lets **your workers reach AWS, Google Cloud or any service that trusts OpenID Connect without a stored key**. Runware signs a short-lived token for each worker, your cloud account trusts that token through federation, and your code uses the AWS or Google SDK unchanged.

```python
import boto3

s3 = boto3.client("s3")   # no access key anywhere
```

Compare that with a [secret](https://runware.ai/docs/serverless/secrets) that holds an access key. The key is valid until someone rotates it, it works from any machine that has a copy, and it is one more thing to keep out of logs. **A connector holds no credential at all.** The token proves which app in which environment is asking, it expires within the hour, and your cloud decides what that app may do.

## How it works

1. You create a connector in your organization. It says where the token goes: an IAM role, a Google workload identity provider, or an audience of your own.
2. You attach it to an app. Its workers start getting a token file and the environment variables the SDKs read.
3. You tell your cloud to trust the Runware issuer, and only for tokens whose subject names your app.
4. When the SDK needs credentials, it sends the token to the cloud's security token service and gets temporary credentials back.

The platform renews the token file while the worker runs, **with no restart and nothing for your code to do**.

## The values your cloud needs

Every trust rule you write is built from three claims of the token.

<table><tbody><tr><td>Claim</td><td>Value</td></tr><tr><td><code>iss</code></td><td><code>https://oidc.serverless.runware.ai</code>, also returned as <code>issuer</code> on every connector</td></tr><tr><td><code>aud</code></td><td>What your cloud checks the token against, one string that depends on the kind. Returned as <code>audience</code> on the connector</td></tr><tr><td><code>sub</code></td><td>Which app in which environment is asking, returned as <code>subject</code> on an attachment</td></tr></tbody></table>

An `aws` connector carries `runware:<organization uuid>` as its audience unless you set your own, and a `generic` one carries the value you set. **A `gcp` connector always carries the provider's full resource name**, the value Google expects.

```text
//iam.googleapis.com/projects/<project number>/locations/global/workloadIdentityPools/<pool id>/providers/<provider id>
```

The subject names the organization, the environment and the app, in that order.

```text
worker:org:<organization uuid>:env:<environment uuid>:app:<app id>
```

**The order of the `sub` segments and the `:` between them are stable**, so a pattern such as `worker:org:<organization uuid>:env:<environment uuid>:*` keeps matching every app of one environment. The `worker:` prefix means a rule written for workers matches no other kind of Runware token.

The environment segment is the environment's id, not its name, so **a staging copy of an app never matches a production trust rule**, and a new environment that reuses a deleted one's name gets none of its trust. The app segment is the app id, so an app you delete and create again with the same id in the same environment matches the same rule.

> [!WARNING]
> **The audience does not keep other Runware customers out.** You choose a `gcp` provider name and a `generic` audience, and anyone could choose the same one. Every trust rule you write must check `sub`, or `org_id` where your verifier reads claims. A rule that checks only `aud` can accept tokens from apps that are not yours.

### Finding your organization and environment ids

**Attach the connector to an app and list the app's connectors.** Each one carries the exact `subject` its tokens use, and a `subjectPattern` for every app of that environment. Copy them into your trust rule rather than building them by hand.

```bash
curl "https://api.serverless.runware.ai/v1/apps/my-app/oidc-connectors?environment=production" \
  -H "Authorization: Bearer $RUNWARE_API_KEY"
```

```json
{
  "data": [
    {
      "name": "s3-reader",
      "kind": "aws",
      "audience": "runware:0190a6a8-0000-7000-8000-00000000000a",
      "issuer": "https://oidc.serverless.runware.ai",
      "aws": { "roleArn": "arn:aws:iam::123456789012:role/runware-s3-reader" },
      "subject": "worker:org:0190a6a8-0000-7000-8000-00000000000a:env:0190a6a8-0000-7000-8000-0000000000e1:app:my-app",
      "subjectPattern": "worker:org:0190a6a8-0000-7000-8000-00000000000a:env:0190a6a8-0000-7000-8000-0000000000e1:*",
      "id": "...",
      "createdAt": "...",
      "updatedAt": "..."
    }
  ]
}
```

The ids are not shown anywhere else yet, so **the app has to exist before you can read its subject**. The cloud side can wait: the connector only names the role or provider, and the worker asks for credentials only when your code does.

## Connecting to AWS

### Create and attach the connector

Pick the name of the IAM role you will create, then create the connector with its ARN.

```bash
curl -X POST https://api.serverless.runware.ai/v1/oidc-connectors \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "s3-reader",
    "kind": "aws",
    "aws": { "roleArn": "arn:aws:iam::123456789012:role/runware-s3-reader" }
  }'
```

The response carries `issuer` and `audience`. Leave `audience` out, as here, and it is `runware:<organization uuid>`. Then attach the connector to the app, and add `?environment=<name>` for a copy outside `production`.

```bash
curl -X POST https://api.serverless.runware.ai/v1/apps/my-app/oidc-connectors \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "connectorName": "s3-reader" }'
```

**Attaching rolls the app's live deployment**, so new workers start with the token. Read the `subject` with `GET /v1/apps/my-app/oidc-connectors`, as shown above.

### Register the issuer in IAM

Create an IAM OIDC identity provider for the issuer, with the connector's `audience` as its client ID. **One provider serves every app of your organization**, because the default audience is the same for all of them.

```bash
aws iam create-open-id-connect-provider \
  --url https://oidc.serverless.runware.ai \
  --client-id-list runware:<organization uuid>
```

If you already have the provider and set a custom `audience` on a connector, add it with `aws iam add-client-id-to-open-id-connect-provider`.

### Create the role and its trust policy

The trust policy names the provider as a federated principal and pins both claims: **`StringEquals` on `aud`, and `StringEquals` or `StringLike` on `sub`**.

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.serverless.runware.ai"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.serverless.runware.ai:aud": "runware:<organization uuid>",
          "oidc.serverless.runware.ai:sub": "worker:org:<organization uuid>:env:<environment uuid>:app:my-app"
        }
      }
    }
  ]
}
```

The exact subject above admits one app in one environment. To admit every app of the environment, move `sub` into a `StringLike` block with the `subjectPattern` value, which ends in `:*`. Save the policy as `trust-policy.json`, create the role with the name the connector uses, and give it only the permissions the app needs.

```bash
aws iam create-role \
  --role-name runware-s3-reader \
  --assume-role-policy-document file://trust-policy.json

aws iam attach-role-policy \
  --role-name runware-s3-reader \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
```

### Use it from your app

The worker has `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE` set, which **every AWS SDK reads on its own**. Create the client once, in `load`, and keep it: it exchanges the token for credentials and does it again when they expire.

```python
import boto3

@serve
class Thumbnails:
    def load(self) -> None:
        self.s3 = boto3.client("s3", region_name="eu-west-1")

    @endpoint
    def list_inputs(self, payload: dict) -> dict:
        page = self.s3.list_objects_v2(Bucket="my-inputs", MaxKeys=10)
        return {"keys": [item["Key"] for item in page.get("Contents", [])]}
```

To check what the role sees, call `boto3.client("sts").get_caller_identity()`. It returns the assumed role's ARN.

## Connecting to Google Cloud

### Create and attach the connector

The connector names the workload identity provider by its full resource name. **The name is fixed by the ids you choose**, so you can create the connector before the pool exists. Use the project number, not the project id.

```bash
curl -X POST https://api.serverless.runware.ai/v1/oidc-connectors \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "gcs-reader",
    "kind": "gcp",
    "gcp": {
      "workloadIdentityProvider": "projects/123456789012/locations/global/workloadIdentityPools/runware/providers/runware-workers",
      "serviceAccountEmail": "gcs-reader@my-project.iam.gserviceaccount.com"
    }
  }'
```

`serviceAccountEmail` is optional. With it, the worker impersonates that service account after the exchange. Without it, **you grant roles to the federated identity directly**. The `audience` of a `gcp` connector is always the provider's name, the value Google expects, so leave it out.

Attach it to the app and read the `subject`, exactly as for AWS:

```bash
curl -X POST https://api.serverless.runware.ai/v1/apps/my-app/oidc-connectors \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "connectorName": "gcs-reader" }'
```

### Create the pool and the provider

Point the provider at the issuer URL, map the claims you want to write conditions on, and **restrict it with an attribute condition on the subject**.

```bash
gcloud iam workload-identity-pools create runware \
  --location=global \
  --display-name="Runware Serverless"

gcloud iam workload-identity-pools providers create-oidc runware-workers \
  --location=global \
  --workload-identity-pool=runware \
  --issuer-uri=https://oidc.serverless.runware.ai \
  --attribute-mapping="google.subject=assertion.sub,attribute.org_id=assertion.org_id,attribute.environment_id=assertion.environment_id,attribute.app_id=assertion.app_id" \
  --attribute-condition="assertion.sub.startsWith('worker:org:<organization uuid>:env:<environment uuid>:')"
```

Leave `--allowed-audiences` out. Google then accepts the provider's own name, which is what the token carries.

> [!WARNING]
> **Do not upload a copy of the Runware keys to the provider.** Google lets a provider hold an uploaded JWKS instead of reading it from the issuer, but Runware rotates its signing keys, and an uploaded set stops working at the next rotation. Use `--issuer-uri` as above, so Google fetches the current keys itself.

### Grant access

With a service account on the connector, let the app's identity impersonate it:

```bash
gcloud iam service-accounts add-iam-policy-binding \
  gcs-reader@my-project.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member="principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/runware/subject/worker:org:<organization uuid>:env:<environment uuid>:app:my-app"
```

Without one, grant the role on the resource to the same member:

```bash
gcloud storage buckets add-iam-policy-binding gs://my-inputs \
  --role=roles/storage.objectViewer \
  --member="principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/runware/subject/worker:org:<organization uuid>:env:<environment uuid>:app:my-app"
```

To grant every app of the environment at once, use a member on a mapped attribute, for example `principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/runware/attribute.environment_id/<environment uuid>`, and keep the attribute condition that pins your organization.

### Use it from your app

The worker has `GOOGLE_APPLICATION_CREDENTIALS` pointing at an external account credential file, which **google-auth and every Google client library read as Application Default Credentials**. Create the client once and keep it.

```python
from google.cloud import storage

@serve
class Thumbnails:
    def load(self) -> None:
        self.gcs = storage.Client(project="my-project")

    @endpoint
    def list_inputs(self, payload: dict) -> dict:
        blobs = self.gcs.list_blobs("my-inputs", max_results=10)
        return {"names": [blob.name for blob in blobs]}
```

Pass `project` explicitly: a federated credential does not always carry one. For lower level use, `google.auth.default()` returns the same credentials.

## Connecting to your own service

The `generic` kind is for anything else that can verify an OpenID Connect token: your own API, a partner's service, or a third-party product with an OIDC trust setting. **You choose the audience, and the worker gets the raw token file.**

```bash
curl -X POST https://api.serverless.runware.ai/v1/oidc-connectors \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "billing-api",
    "kind": "generic",
    "audience": "https://billing.example.com"
  }'
```

An audience that starts with `runware:` is reserved, except your own organization's `runware:<organization uuid>`. Attach the connector to the app as above.

### Sending the token

`RUNWARE_OIDC_TOKEN_FILE` names the file. **Read it on every request instead of once at startup**, because the platform replaces it before it expires.

```python
import os
import requests

def call_billing(body: dict) -> dict:
    with open(os.environ["RUNWARE_OIDC_TOKEN_FILE"]) as f:
        token = f.read().strip()

    response = requests.post(
        "https://billing.example.com/usage",
        json=body,
        headers={"Authorization": f"Bearer {token}"},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()
```

### Verifying the token

Your service reads the issuer's discovery document at `https://oidc.serverless.runware.ai/.well-known/openid-configuration`, which names the JWKS URL. **Fetch the keys from there and cache them by `kid`**, and fetch again when a token arrives with a `kid` you have not seen, because a rotation adds new keys. Then check:

- the signature, with `RS256` only,
- `iss` equals `https://oidc.serverless.runware.ai`,
- `aud` equals the connector's audience,
- `exp` is in the future and `nbf` is not, with a small allowance for clock skew,
- `sub` equals the subject you trust or starts with your environment's prefix, or `org_id` equals your organization's id.

```python
import jwt
import requests

ISSUER = "https://oidc.serverless.runware.ai"
AUDIENCE = "https://billing.example.com"
TRUSTED_PREFIX = "worker:org:<organization uuid>:env:<environment uuid>:"

discovery = requests.get(f"{ISSUER}/.well-known/openid-configuration", timeout=5).json()
keys = jwt.PyJWKClient(discovery["jwks_uri"])

def verify(token: str) -> dict:
    signing_key = keys.get_signing_key_from_jwt(token)
    claims = jwt.decode(
        token,
        signing_key.key,
        algorithms=["RS256"],
        audience=AUDIENCE,
        issuer=ISSUER,
        leeway=60,
        options={"require": ["iss", "sub", "aud", "exp", "iat"]},
    )
    if not claims["sub"].startswith(TRUSTED_PREFIX):
        raise PermissionError("token is not from a trusted app")
    return claims
```

The example uses PyJWT with its crypto extra, `pip install "pyjwt[crypto]" requests`. Any OIDC library that reads a JWKS URL does the same checks.

The `jti` claim is unique per token, so a service that must refuse a replay can record the ones it has seen until they expire.

## What the worker gets

The platform mounts a read-only directory at `/var/run/secrets/runware/oidc` and sets the variables for each attached kind. **The file variables hold a path inside that directory**, at the name below. The first two come with an `aws` connector, the third with a `gcp` one and the fourth with a `generic` one.

<table><tbody><tr><td>Variable</td><td>Value</td></tr><tr><td><code>AWS_ROLE_ARN</code></td><td>The role on the <code>aws</code> connector</td></tr><tr><td><code>AWS_WEB_IDENTITY_TOKEN_FILE</code></td><td><code>aws-token</code></td></tr><tr><td><code>GOOGLE_APPLICATION_CREDENTIALS</code></td><td><code>gcp-credential-configuration.json</code></td></tr><tr><td><code>RUNWARE_OIDC_TOKEN_FILE</code></td><td><code>generic-token</code></td></tr></tbody></table>

**`AWS_ROLE_ARN`, `AWS_WEB_IDENTITY_TOKEN_FILE` and `RUNWARE_OIDC_TOKEN_FILE` belong to the connector only while one of that kind is attached.** Attaching to an app that already sets one of them as an environment variable or a secret returns `422`, and so does setting one later. Whatever else sets one of them, such as an older version the app is pinned to, the worker gets the connector's value. An app with no connector of that kind can set them as before.

**`GOOGLE_APPLICATION_CREDENTIALS` and `RUNWARE_OIDC_CONFIG_DIGEST` are reserved on every app**, with or without a connector, and setting either returns `422`. The platform sets `RUNWARE_OIDC_CONFIG_DIGEST` on workers that have a connector, and your code has no use for it. The token directory is also reserved for volumes: a mount path on it, under it or over it, such as `/var`, returns `422`.

## The token

A token is a JWT signed with `RS256`, and its header carries the `kid` of the signing key.

<table><tbody><tr><td>Claim</td><td>Value</td></tr><tr><td><code>iss</code>, <code>aud</code>, <code>sub</code></td><td>As described <a href="#the-values-your-cloud-needs">above</a></td></tr><tr><td><code>org_id</code></td><td>Your organization's id</td></tr><tr><td><code>environment_id</code></td><td>The id of the environment the app runs in</td></tr><tr><td><code>app_id</code></td><td>The app id</td></tr><tr><td><code>deployment_id</code></td><td>The id of the deployment the worker belongs to</td></tr><tr><td><code>worker</code></td><td>The worker's name</td></tr><tr><td><code>connector</code></td><td>The connector's name</td></tr><tr><td><code>jti</code></td><td>Unique per token</td></tr><tr><td><code>iat</code>, <code>nbf</code>, <code>exp</code></td><td>When it was issued, when it becomes valid, and when it expires</td></tr></tbody></table>

AWS trust policies can match only `aud` and `sub`. A Google attribute condition or your own verifier can match any claim, for example `deployment_id` to trust one deployment only.

**A token lives at most one hour. Do not depend on an exact lifetime**, because it may get shorter. The platform replaces the token file at half its lifetime, including on a worker that was resumed after scaling to zero, and the AWS and Google SDKs read the new file when their credentials run out.

**Key rotation needs nothing from you.** New keys are published at the issuer before they sign anything, and old keys stay until every token they signed has expired. If a signing key is ever compromised, Runware removes it and tells every affected organization when the exposure started and ended, which subjects were used, and what to revoke.

## Limits

- **One connector of each kind per app.** Attaching a second `aws` connector, or the same one twice, returns `409`.
- **A connector belongs to the organization**, not to an environment. Any app in any environment can attach it, and the environment shows up only in `sub`.
- **Changes reach workers when they roll.** Attaching, updating and detaching roll the app's live deployment. An app that is not live, or one with a rollout already in progress, picks the change up on its next rollout.
- **Detaching does not revoke access at once.** A worker that still runs keeps getting tokens until it rolls, a token stays valid until it expires, and the cloud credentials it was exchanged for stay valid for their own lifetime. The worst case is one hour of token plus the credential lifetime: one hour on Google, and on AWS the role's maximum session duration, one hour by default and up to twelve. To cut access at once, remove the trust or the grant on your cloud's side, and on AWS revoke the role's active sessions.
- **A connector cannot be deleted while any app attaches it**, a stopped or failed one included. Detach it first, or delete the app.
- **The audience is one string**, never a list, of at most 256 characters with no whitespace.
- **A name is 2 to 63 characters**: lowercase letters, digits and hyphens, starting with a letter and ending with a letter or a digit. The name and the kind are fixed once created.
- **An update replaces the whole configuration**, so send every field you want to keep. An `aws` update without `audience` goes back to the default.

## Troubleshooting

### On AWS

### [InvalidIdentityToken: No OpenIDConnect provider found in your account](#error-invalididentitytoken-no-openidconnect-provider-found-in-your-account)

The IAM OIDC provider is missing, or its URL is not exactly `https://oidc.serverless.runware.ai`, with no trailing slash.

### [InvalidIdentityToken: Incorrect token audience](#error-invalididentitytoken-incorrect-token-audience)

The provider's client ID list does not contain the connector's `audience`.

### [AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity](#error-accessdenied-not-authorized-to-perform-stsassumerolewithwebidentity)

The trust policy does not match the token. Compare its `sub` with the `subject` the API returns, check the environment, and check that `aud` matches. Also check that the connector's `roleArn` names this role.

### [InvalidIdentityToken: Couldn't retrieve verification key from your identity provider](#error-invalididentitytoken-couldnt-retrieve-verification-key-from-your-identity-provider)

retryable

AWS could not reach the issuer for a moment.

### [NoCredentialsError](#error-nocredentialserror)

boto3 raises it when the worker has no connector. Check that the connector is attached in the app's environment, and that the deployment has rolled since.

### On Google Cloud

### [invalid\_target](#error-invalid_target)

The pool or the provider does not exist, is disabled, or the connector's `workloadIdentityProvider` names another one. Use the project number, not the project id.

### [invalid\_grant](#error-invalid_grant)

Naming the audience, the provider has `--allowed-audiences` set to something else. Clear it, so Google accepts its own name. About the signature or the key, after it used to work, the provider holds an uploaded JWKS. Switch it to the issuer URL.

### [unauthorized\_client: The given credential is rejected by the attribute condition](#error-unauthorized_client-the-given-credential-is-rejected-by-the-attribute-condition)

The condition does not match the token. Compare it with the `subject` the API returns.

### [Permission 'iam.serviceAccounts.getAccessToken' denied](#error-permission-iamserviceaccountsgetaccesstoken-denied)

The app's identity has no `roles/iam.workloadIdentityUser` on the service account, or the member's subject does not match.

### [403](#error-403)

The exchange worked and Google refused the call itself. The service account, or the federated identity without one, lacks a role on the resource.

### From the Serverless API

<table><tbody><tr><td>Status</td><td>Cause</td></tr><tr><td><code>409</code> on create</td><td>Your organization already has a connector of that name</td></tr><tr><td><code>409</code> on attach</td><td>The app already has a connector of that kind, or this one</td></tr><tr><td><code>409</code> on delete</td><td>An app still attaches the connector</td></tr><tr><td><code>409</code> on deploying an older version</td><td>That version sets one of the connector's variables. Remove it, or detach the connector first</td></tr><tr><td><code>422</code> on attach, or on setting a variable or a secret</td><td>A name the connector sets is already set on the app, or you are setting one while the connector is attached. <code>GOOGLE_APPLICATION_CREDENTIALS</code> and <code>RUNWARE_OIDC_CONFIG_DIGEST</code> return it on every app</td></tr><tr><td><code>422</code> on create or update</td><td>A field breaks its rule, for example a custom <code>audience</code> on a <code>gcp</code> connector, or another organization's <code>runware:</code> audience</td></tr><tr><td><code>403</code></td><td>Your organization is not active, so its connectors cannot change</td></tr></tbody></table>