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

# Configure API key authentication

> Configure API key authentication, authorization, and token revocation for a StreamNative Private Cloud Pulsar cluster.

API key authentication lets you issue and revoke credentials without distributing a token-signing key to applications. The API key service issues each credential as a signed JSON Web Token (JWT), and Pulsar validates the token and its revocation status when a client connects.

For broker and proxy authentication, this procedure uses the token `jti` claim as the Pulsar role. The API key service returns the same value as `token_id`. Grant namespace permissions to this value, not to the token name or its `sub` claim. Console uses `sub` separately to identify its administrator, as described in [Configure Console for API key management](#configure-console-for-api-key-management).

## Before you begin

* Install StreamNative Operator `v0.20.0` or later. If your cluster uses an earlier Operator version, upgrade it before enabling API key authentication. Version `v0.20.0` introduces the `ApiKeys` custom resource and the `PulsarCoordinator.spec.authentication.apiKey` field required by this procedure.
* Deploy a Pulsar cluster with `PulsarCoordinator`, `PulsarBroker`, and `PulsarProxy` resources in the same namespace.
* Use StreamNative Console `v2.3.32` or later for this procedure. Upgrade an earlier Console version before configuring API key management.
* Install `kubectl`, `jq`, and `pulsarctl`.
* Make sure that you can update the cluster custom resources and read Secrets in the cluster namespace.

The examples use the following values:

| Resource | Example value |
| - | - |
| Kubernetes namespace | `pulsar` |
| Pulsar cluster and coordinator name | `private-cloud` |

Replace these values to match your environment. The `ApiKeys` resource and `PulsarCoordinator` resource must have the same name and namespace.

## Deploy the API key service

Create an `ApiKeys` resource:

```yaml theme={null}
apiVersion: k8s.streamnative.io/v1alpha1
kind: ApiKeys
metadata:
  name: private-cloud
  namespace: pulsar
  labels:
    k8s.streamnative.io/coordinator-name: private-cloud
spec:
  replicas: 1
  image: streamnative/sn-api-keys-svc:v0.11.4
  hostname: http://private-cloud-apikeys.pulsar.svc.cluster.local:8081
  issuerPathPrefix: /
  brokerServiceUrl: pulsar://private-cloud-broker.pulsar.svc.cluster.local:6650
  config:
    server:
      audience: urn:sn:pulsar:pulsar:private-cloud
      currentKid: private-cloud
      allowCustomClaims: true
    backend:
      pulsarTopic: api_keys_api.example
    authenticator:
      enabled: true
      issuers:
        - http://private-cloud-apikeys.pulsar.svc.cluster.local:8081/
      acceptedAudience: urn:sn:pulsar:pulsar:private-cloud
```

Use the same value for `spec.config.server.audience` and `spec.config.authenticator.acceptedAudience`. The audience format used by the coordinator is `urn:sn:pulsar:<namespace>:<coordinator-name>`.

`allowCustomClaims` lets an authenticated administrator set claims such as `scope` and `sub` when creating an API key. You can enable this option only when `spec.config.server.multiTenant.enable` is `false` or omitted.

### Prepare the API key backend topic

The API key service stores its state in the topic configured by `spec.config.backend.pulsarTopic`. The short topic name in this example resolves to:

```text theme={null}
persistent://public/default/api_keys_api.example
```

Before you start the API key service, use one of the following options to make this topic available.

#### Enable automatic topic creation

Add the following settings to the `PulsarBroker` resource:

```yaml theme={null}
spec:
  config:
    custom:
      PULSAR_PREFIX_allowAutoTopicCreation: "true"
      PULSAR_PREFIX_allowAutoTopicCreationType: "non-partitioned"
      PULSAR_PREFIX_defaultNumPartitions: "1"
```

With this configuration, the API key service creates `api_keys_api.example` as a non-partitioned topic when it first accesses the topic.

#### Create the topic manually

If automatic topic creation is off for your cluster, create the non-partitioned topic before you start the API key service or enable API key authentication. Use an existing administrator credential:

```bash theme={null}
kubectl exec -n pulsar private-cloud-toolset-0 -- /pulsar/bin/pulsarctl \
  --admin-service-url http://private-cloud-proxy:8080 \
  --token-file "" \
  --token "<existing-admin-token>" \
  topics create \
  persistent://public/default/api_keys_api.example \
  0
```

The final `0` creates a non-partitioned topic. The administrator credential must have permission to manage the `public/default` namespace. If the API key service credential isn't an administrator, grant it `produce` and `consume` permissions on this topic.

<Warning>
  For a first-time deployment when automatic topic creation is off, create the backend topic before switching all brokers to API-key-dependent authentication. Otherwise, the brokers can wait for authentication metadata from the API key service while the API key service waits for its Pulsar backend topic.
</Warning>

<Warning>
  Don't expose the API key service directly to public networks. In this procedure, administrators manage API keys through Console.
</Warning>

Save the resource as `apikeys.yaml`. Apply it together with the cluster configuration in the next section so that the API key service and Pulsar authentication can become ready together.

## Enable API key authentication on the cluster

Add the following configuration to the `PulsarCoordinator` resource:

```yaml theme={null}
spec:
  authentication:
    apiKey:
      enabled: true
      subjectClaim: jti
```

The operator configures the broker and proxy to discover the API key service signing keys and revocation list. It also creates internal API keys for cluster components.

For broker and proxy authentication, `subjectClaim: jti` makes each API key's unique token ID its Pulsar role. Use this role when you grant permissions to a non-administrator API key.

Configure authorization on the `PulsarBroker` resource:

```yaml theme={null}
spec:
  config:
    authorization:
      superUserRoles: "broker-admin,proxy-admin,admin"
```

With API key authentication enabled on the coordinator, the operator enables broker authorization and selects the API key authorization provider. Use `spec.config.authorization` for broker roles and preserve any existing superuser roles when updating `superUserRoles`.

Configure the `PulsarProxy` resource to forward the original client credentials to the broker:

```yaml theme={null}
spec:
  config:
    custom:
      PULSAR_PREFIX_forwardAuthorizationCredentials: "true"
```

The operator enables `authenticateOriginalAuthData` when you configure proxy authentication.

Merge these fields with the existing broker and proxy configuration. Preserve unrelated settings. Remove any `spec.config.custom` overrides for `authorizationEnabled`, `authorizationProvider`, `superUserRoles`, or `proxyRoles`, including their `PULSAR_PREFIX_` forms, so they don't override the typed authorization configuration. Apply `apikeys.yaml` and the updated coordinator, broker, and proxy resources.

Wait for the API key service to become ready:

```bash theme={null}
kubectl wait -n pulsar \
  --for=condition=Ready \
  pod -l cloud.streamnative.io/component=apikeys \
  --timeout=180s
```

If your deployment uses different labels, run `kubectl get pods -n pulsar` to identify the API key service Pod.

### Apply the proxy role workaround

<Note>
  StreamNative Operator `v0.20.16` has a known issue that prevents it from automatically adding the proxy API key ID to the broker's superuser and proxy roles. Use the following temporary workaround on affected versions. Once you upgrade to a release that fixes automatic proxy role synchronization, the manual patch is no longer required.
</Note>

The operator creates an internal proxy credential in the `<coordinator-name>-apikeys-proxy-key` Secret. Read only its `jti` value:

```bash theme={null}
PROXY_JTI=$(kubectl get secret private-cloud-apikeys-proxy-key -n pulsar \
  -o json | jq -r '.data.jti | @base64d')
```

Add that value to `spec.config.authorization.superUserRoles` and `spec.config.authorization.proxyRoles` on the broker. The following command preserves existing roles and avoids duplicate entries:

```bash theme={null}
kubectl patch pulsarbroker private-cloud -n pulsar --type=merge \
  -p "$(kubectl get pulsarbroker private-cloud -n pulsar -o json | \
    jq -c --arg role "$PROXY_JTI" '
      def add_role: split(",") + [$role] | map(select(length > 0)) | unique | join(",");
      {spec:{config:{authorization:{
        superUserRoles: ((.spec.config.authorization.superUserRoles // "") | add_role),
        proxyRoles: ((.spec.config.authorization.proxyRoles // "") | add_role)
      }}}}')"
```

Keep these role settings in your deployment manifests so that subsequent deployments preserve the workaround.

<Warning>
  On affected Operator versions, repeat this workaround if the operator regenerates the proxy API key, and remove the previous `jti` from both role settings. Otherwise, non-administrator clients fail with an error that indicates the proxy doesn't have a valid proxy role.
</Warning>

Wait for the broker and proxy Pods to become ready after the configuration rollout:

```bash theme={null}
kubectl wait -n pulsar \
  --for=condition=Ready \
  pod -l cloud.streamnative.io/component=broker \
  --timeout=300s

kubectl wait -n pulsar \
  --for=condition=Ready \
  pod -l cloud.streamnative.io/component=proxy \
  --timeout=300s
```

## Configure Console for API key management

Add the following configuration to the `Console` resource. Keep your existing `spec.image` value if it meets the Console version prerequisite:

```yaml theme={null}
spec:
  config:
    authentication:
      apiKey:
        serviceUrl: http://private-cloud-apikeys.pulsar.svc.cluster.local:8081/
        secretName: private-cloud-apikeys-console-key
        superRole: admin
        subjectClaim: sub
```

The `subjectClaim` values in the coordinator and Console configurations intentionally differ:

| Setting | Applies to | Purpose |
| - | - | - |
| `PulsarCoordinator.spec.authentication.apiKey.subjectClaim: jti` | Broker and proxy authentication | Uses the unique API key ID as the Pulsar authorization role. |
| `Console.spec.config.authentication.apiKey.subjectClaim: sub` | Console login and authorization | Uses the token subject as the Console user identity and compares it with `superRole`. |

Keep `subjectClaim: sub` in the Console configuration. The generated Console administrator token has `sub: admin`, which matches `superRole: admin`. Its `jti` is a unique API key ID and doesn't match the configured superuser role. If you set the Console `subjectClaim` to `jti`, token validation can succeed during sign-in, but Console rejects subsequent operations because the `jti` doesn't match `superRole`.

The Console setting doesn't change how brokers and proxies derive the Pulsar role from `jti`.

The operator stores the Console administrator credential in the configured Secret. Treat this Secret as a superuser credential. Apply the updated `Console` resource and wait for its Pod to become ready.

## Create API keys in Console

1. Retrieve the Console administrator token from the configured Secret:

   ```bash theme={null}
   kubectl get secret -n pulsar private-cloud-apikeys-console-key \
     -o json | jq -r '.data.token | @base64d'
   ```

   Copy the token from the command output. Don't save it in a file or shell history.

2. Open Console, paste the administrator token into the **JWT Token** field, and then click **Sign in**.

   <Frame caption="Sign in to Console with the administrator token.">
     <img src="https://mintcdn.com/streamnative/3sy-tlpB-ZvAzoui/media/private-cloud/api-key-auth/console-login.png?fit=max&auto=format&n=3sy-tlpB-ZvAzoui&q=85&s=1f33dd323c33f438908db227a7d2d0de" alt="StreamNative Console sign-in page with the JWT Token field" width="3442" height="1972" data-path="media/private-cloud/api-key-auth/console-login.png" />
   </Frame>

3. In the left navigation menu, select **API Keys**.

### Choose an expiration time

When you create an API key, select one of the following values from **Expiration date**:

* **30 days**
* **90 days**
* **1 year**
* **Custom expiration date**
* **No expiration date**

Use a finite expiration time for app credentials. Use **No expiration date** only when your security policy permits long-lived credentials, and rotate the credential regularly.

### Create an administrator API key

An administrator API key contains the `admin access` scope. It can perform cluster-wide administrative operations without explicit namespace permissions.

1. Click **New API Key**.

2. Enter a descriptive name.

3. Select an expiration time.

4. Turn on **Is Admin**.

   <Frame caption="Create an administrator API key with Is Admin turned on.">
     <img src="https://mintcdn.com/streamnative/3sy-tlpB-ZvAzoui/media/private-cloud/api-key-auth/console-create-admin-api-key.png?fit=max&auto=format&n=3sy-tlpB-ZvAzoui&q=85&s=0102173075d54bf775edabf0fc15973b" alt="New API Key dialog configured to create an administrator API key" width="3428" height="1924" data-path="media/private-cloud/api-key-auth/console-create-admin-api-key.png" />
   </Frame>

5. Click **Confirm**.

6. Copy the API key and store it in a secrets manager. Console displays the API key only once.

7. Click **Copy and Close**.

Limit administrator API keys to administrative workflows. For apps that only produce or consume messages, create a non-administrator API key and grant only the required permissions.

### Create a non-administrator API key

A non-administrator API key contains the `access` scope. It can't access Pulsar resources until you grant permissions to its API key ID.

1. Click **New API Key**.

2. Enter a descriptive name.

3. Select an expiration time.

4. Turn off **Is Admin**.

   <Frame caption="Create a non-administrator API key with Is Admin turned off.">
     <img src="https://mintcdn.com/streamnative/3sy-tlpB-ZvAzoui/media/private-cloud/api-key-auth/console-create-non-admin-api-key.png?fit=max&auto=format&n=3sy-tlpB-ZvAzoui&q=85&s=961f7865a56fb9b20fa2636c78c2cea1" alt="New API Key dialog configured to create a non-administrator API key with a 90-day expiration" width="3434" height="1916" data-path="media/private-cloud/api-key-auth/console-create-non-admin-api-key.png" />
   </Frame>

5. Click **Confirm**.

6. Copy the API key and store it in a secrets manager. Console displays the API key only once.

7. Click **Copy and Close**.

8. In the API key list, record the value in the **ID** column. The ID is the token `jti` and the Pulsar role that you use to grant permissions.

<Warning>
  Never commit an API key to source control. Use a finite expiration time and the least privileges required by the app.
</Warning>

## Grant permissions to a non-administrator API key

Use an administrator API key to grant permissions. Set `--role` to the non-administrator API key's **ID**, not its name or API key value:

```bash theme={null}
pulsarctl --admin-service-url http://<Your PulsarProxy Endpoint>:8080 \
  --token "<admin-api-key>" \
  namespaces grant-permission \
  --role <non-admin-api-key-id> \
  --actions produce \
  --actions consume \
  public/default
```

Expected output:

```text theme={null}
Grant permissions [produce consume] to the client role <non-admin-api-key-id> to access the namespace public/default successfully
```

## Connect a Pulsar client

The API key service issues API keys as JWT tokens. Configure Pulsar clients to use an API key in the same way that you configure a client for [JWT authentication](/private-cloud/v2/configure-private-cloud/security/authentication/private-cloud-jwt-auth#connect-clients-to-pulsar). Use the `AuthenticationToken` plugin and pass the API key as the token value. You don't need an API-key-specific client plugin.

Produce messages with the non-administrator API key:

```bash theme={null}
bin/pulsar-client \
  --url pulsar://<Your PulsarProxy Endpoint>:6650 \
  --auth-plugin org.apache.pulsar.client.impl.auth.AuthenticationToken \
  --auth-params "token:<non-admin-api-key>" \
  produce public/default/test -m "test" -n 10
```

Consume messages with the same API key:

```bash theme={null}
bin/pulsar-client \
  --url pulsar://<Your PulsarProxy Endpoint>:6650 \
  --auth-plugin org.apache.pulsar.client.impl.auth.AuthenticationToken \
  --auth-params "token:<non-admin-api-key>" \
  consume public/default/test -s my-subscription -n 10
```

Both commands complete successfully when the API key role has `produce` and `consume` permissions on `public/default`.

<Note>
  Use the API key value that Console displays after creation for `--auth-params`. Use the API key **ID** only as the Pulsar role when granting permissions.
</Note>

## Revoke an API key

1. In Console, select **API Keys**.

2. Open the actions menu for the API key that you want to revoke, and then select **Revoke**.

   <Frame caption="Open an API key's actions menu and select Revoke.">
     <img src="https://mintcdn.com/streamnative/3sy-tlpB-ZvAzoui/media/private-cloud/api-key-auth/console-revoke-api-key.png?fit=max&auto=format&n=3sy-tlpB-ZvAzoui&q=85&s=e5c30855a56473be8d7d9734f09b9f5f" alt="API Keys page with the Revoke action selected for an API key" width="3422" height="1746" data-path="media/private-cloud/api-key-auth/console-revoke-api-key.png" />
   </Frame>

3. Confirm the revocation.

Brokers and proxies cache the revocation list, so they might continue to accept the credential until the next refresh. Allow at least 65 seconds before verifying that Pulsar rejects the revoked credential with `401 Unauthorized`.

## Troubleshoot API key authentication

### The broker rejects the original principal

If a non-administrator client fails with an error similar to `cannot specify originalPrincipal when connecting without valid proxy role`, compare the proxy credential `jti` with the broker configuration:

```bash theme={null}
kubectl get secret private-cloud-apikeys-proxy-key -n pulsar \
  -o json | jq -r '.data.jti | @base64d'

kubectl get pulsarbroker private-cloud -n pulsar \
  -o jsonpath='{.spec.config.authorization.proxyRoles}'
```

If the proxy `jti` is missing from the broker's proxy roles, follow [Apply the proxy role workaround](#apply-the-proxy-role-workaround), and wait for the brokers to become ready.

### A revoked API key still works

Wait for the revocation list to refresh, and then open a new client connection. An existing authenticated connection might remain active until it reconnects.

### Console can't create an API key

Confirm that `spec.config.server.allowCustomClaims` is `true` and `spec.config.server.multiTenant.enable` is `false` or omitted. Console sets the API key `scope` and `sub` claims when it creates the credential.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.