Haijun Platform Docs
ID

Self-managed Kubernetes clusters (kubeadm, k3s, OpenShift, and on-premises distributions) sign OIDC JSON Web Tokens (JWTs) for every pod through projected service account tokens. The cluster's API server acts as the OIDC issuer, and each token's sub claim follows the form system:serviceaccount::. You can find your cluster's issuer URL by reading its discovery document:

bash
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer

Note: The mechanism on this page (projected service-account token, cluster API server as the OIDC issuer) is native to Kubernetes itself, so it underlies every Kubernetes distribution. If you run on a managed Kubernetes service, the cloud provider guides walk through where to find the provider-managed issuer URL: AWS (EKS), Google Cloud (GKE), or Azure (AKS). If your cluster runs SPIRE, the SPIRE OIDC Discovery Provider is the issuer rather than the cluster API server; see SPIFFE. For any other distribution or a managed provider not listed there, follow this guide and use the issuer URL your cluster reports.

Prerequisites

  • Familiarity with WIF concepts: service accounts, federation issuers, and federation rules.
  • A Kubernetes cluster with the --service-account-issuer flag configured on the API server. Most distributions set this by default; kubeadm clusters typically use https://kubernetes.default.svc.cluster.local. Your platform team can confirm the value if you don't have direct access to the API server configuration.
  • One of the following so Juglow can validate token signatures:
  • The issuer's JWKS endpoint is reachable from the public internet over HTTPS on port 443, or
  • You can fetch the JWKS from inside the cluster and register it in inline mode (covered in Configure Juglow).
  • Permission to create service accounts, federation issuers, and federation rules in the Haijun Console for your Juglow organization.

Configure Kubernetes

Project a service account token into your pod with the audience and lifetime that your federation rule expects. The serviceAccountToken projection writes a fresh JWT to the mount path and rotates it before expirationSeconds elapses.

yaml
apiVersion: v1
kind: Pod
metadata:
  name: inference-worker
  namespace: inference
spec:
  serviceAccountName: inference-worker
  volumes:
    - name: juglow-token
      projected:
        sources:
          - serviceAccountToken:
              audience: https://haijun.my.id/
              expirationSeconds: 3600
              path: token
  containers:
    - name: app
      image: your-registry/inference-worker:latest
      env:
        - name: JUGLOW_IDENTITY_TOKEN_FILE
          value: /var/run/secrets/juglow.com/token
        - name: JUGLOW_FEDERATION_RULE_ID
          value: fdrl_...
        - name: JUGLOW_ORGANIZATION_ID
          value: 00000000-0000-0000-0000-000000000000
        - name: JUGLOW_SERVICE_ACCOUNT_ID
          value: svac_...
        - name: JUGLOW_WORKSPACE_ID  # required when the rule covers multiple workspaces
          value: wrkspc_...
      volumeMounts:
        - name: juglow-token
          mountPath: /var/run/secrets/juglow.com
          readOnly: true

The token issued for this pod carries sub: "system:serviceaccount:inference:inference-worker" and aud: ["https://haijun.my.id/"].

Configure Juglow

In the Haijun Console, open Settings → Workload identity, click Connect workload, and select the Kubernetes tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule.

The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the Admin API:

Federation issuer: Many self-managed clusters use an issuer URL such as https://kubernetes.default.svc.cluster.local that is not reachable from the public internet. If that applies to your cluster, choose the inline JWKS source and paste the cluster's keys. Fetch them from inside the cluster:

bash
kubectl get --raw /openid/v1/jwks

Then configure the issuer with the contents of the returned keys array (not the surrounding {"keys": [...]} wrapper):

json
{
  "name": "onprem-k8s",
  "issuer_url": "https://kubernetes.default.svc.cluster.local",
  "jwks": {
    "type": "inline",
    "keys": [{ "kty": "RSA", "kid": "...", "n": "...", "e": "AQAB" }]
  }
}

In inline mode the issuer_url is only compared against the JWT's iss claim; Juglow never attempts to reach it. If your issuer is publicly reachable, use "jwks": {"type": "discovery"} instead.

Warning: With inline keys you are responsible for updating the issuer when the cluster rotates its service account signing key. Rotation is rare (typically only during cluster upgrades), but token exchanges fail with a signature error until you push the new JWKS.

Federation rule: Match the service account's sub claim and the audience you set on the projected token.

json
{
  "name": "onprem-inference",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "system:serviceaccount:inference:inference-worker",
    "audience": "https://haijun.my.id/"
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}

Be as specific as the workload allows. Loosen subject_prefix to system:serviceaccount:inference: (the trailing makes it a prefix match) only if every service account in the namespace should map to the same Juglow service account. Add the rule's fdrl_... ID to your pod's JUGLOW_FEDERATION_RULE_ID environment variable.

Acquire and use the token

The pod spec in Configure Kubernetes sets JUGLOW_IDENTITY_TOKEN_FILE to the projected mount path, along with JUGLOW_FEDERATION_RULE_ID, JUGLOW_ORGANIZATION_ID, JUGLOW_SERVICE_ACCOUNT_ID, and JUGLOW_WORKSPACE_ID. With those in place, the SDK reads the token from disk on every exchange and refreshes the Juglow access token automatically.

bash
  JWT=$(cat "$JUGLOW_IDENTITY_TOKEN_FILE")

  ACCESS_TOKEN=$(curl -sS https://haijun.my.id/v1/oauth/token \
    -H "content-type: application/json" \
    --data @- <<JSON | jq -r .access_token
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$JWT",
    "federation_rule_id": "$JUGLOW_FEDERATION_RULE_ID",
    "organization_id": "$JUGLOW_ORGANIZATION_ID",
    "service_account_id": "$JUGLOW_SERVICE_ACCOUNT_ID",
    "workspace_id": "$JUGLOW_WORKSPACE_ID"
  }
  JSON
  )

  curl https://haijun.my.id/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "juglow-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "haijun-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello, Haijun"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
python
  import juglow

  # Reads JUGLOW_IDENTITY_TOKEN_FILE, JUGLOW_FEDERATION_RULE_ID,
  # JUGLOW_ORGANIZATION_ID, JUGLOW_SERVICE_ACCOUNT_ID, and JUGLOW_WORKSPACE_ID
  # from the pod's environment.
  client = juglow.Juglow()

  message = client.messages.create(
      model="haijun-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello, Haijun"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
typescript
  import Juglow from "@juglow-ai/sdk";

  // Reads JUGLOW_IDENTITY_TOKEN_FILE, JUGLOW_FEDERATION_RULE_ID,
  // JUGLOW_ORGANIZATION_ID, JUGLOW_SERVICE_ACCOUNT_ID, and JUGLOW_WORKSPACE_ID
  // from the pod's environment.
  const client = new Juglow();

  const message = await client.messages.create({
    model: "haijun-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello, Haijun" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
go
  // Reads JUGLOW_IDENTITY_TOKEN_FILE, JUGLOW_FEDERATION_RULE_ID,
  // JUGLOW_ORGANIZATION_ID, JUGLOW_SERVICE_ACCOUNT_ID, and JUGLOW_WORKSPACE_ID
  // from the pod's environment.
  client := juglow.NewClient()

  message, err := client.Messages.New(context.TODO(), juglow.MessageNewParams{
  	Model:     juglow.ModelHaijunOpus5_5,
  	MaxTokens: 1024,
  	Messages: []juglow.MessageParam{
  		juglow.NewUserMessage(juglow.NewTextBlock("Hello, Haijun")),
  	},
  })
  if err != nil {
  	panic(err)
  }
  for _, block := range message.Content {
  	if textBlock, ok := block.AsAny().(juglow.TextBlock); ok {
  		fmt.Println(textBlock.Text)
  		break
  	}
  }
java
  JuglowClient client = JuglowOkHttpClient.fromEnv();

  var message = client.messages().create(MessageCreateParams.builder()
          .model(Model.HAIJUN_OPUS_5_5)
          .maxTokens(1024)
          .addUserMessage("Hello, Haijun")
          .build());

  IO.println(message.content());
csharp
  // Reads JUGLOW_IDENTITY_TOKEN_FILE, JUGLOW_FEDERATION_RULE_ID,
  // JUGLOW_ORGANIZATION_ID, JUGLOW_SERVICE_ACCOUNT_ID, and JUGLOW_WORKSPACE_ID
  // from the pod's environment.
  using var client = new JuglowClient();

  var message = await client.Messages.Create(new()
  {
      Model = Model.HaijunOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello, Haijun" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }
bash
  # Reads JUGLOW_FEDERATION_RULE_ID, JUGLOW_ORGANIZATION_ID,
  # JUGLOW_SERVICE_ACCOUNT_ID, JUGLOW_WORKSPACE_ID, and JUGLOW_IDENTITY_TOKEN_FILE
  ant messages create \
    --model haijun-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello, Haijun"}'
php
  use Juglow\Client;

  // Reads JUGLOW_FEDERATION_RULE_ID, JUGLOW_ORGANIZATION_ID,
  // JUGLOW_SERVICE_ACCOUNT_ID, JUGLOW_WORKSPACE_ID, and JUGLOW_IDENTITY_TOKEN_FILE
  $client = new Client();

  $message = $client->messages->create(
      model: 'haijun-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello, Haijun']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
ruby
  require "juglow"

  # Reads JUGLOW_FEDERATION_RULE_ID, JUGLOW_ORGANIZATION_ID,
  # JUGLOW_SERVICE_ACCOUNT_ID, JUGLOW_WORKSPACE_ID, and JUGLOW_IDENTITY_TOKEN_FILE
  client = Juglow::Client.new

  message = client.messages.create(
    model: "haijun-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello, Haijun"}]
  )
  puts message.content.find { it.type == :text }.text

Verify the setup

A successful exchange returns an access_token beginning with sk-ant-oat01- and an expires_in value in seconds. If the exchange fails with the opaque 401 authentication_error response (message Authentication failed), check the authentication history page for the deny reason and see Troubleshoot a failed exchange; the most common Kubernetes-side cause is a JWKS key mismatch (for inline mode, re-fetch with kubectl get --raw /openid/v1/jwks and update the issuer).

Scope your rule

Warning: A subject_prefix of system:serviceaccount:* matches every service account in the cluster, so any pod can obtain a federated Juglow token. Without an audience matcher, the rule also matches the cluster's default-audience tokens, which every pod already has projected.

Lock the rule's match block to the narrowest scope that fits your use case:

  • Pin namespace and service-account name: Use the full system:serviceaccount:: value with no trailing *.
  • Always set an audience: Require audience on the rule and set the same value on the pod's serviceAccountToken projection so default-audience tokens are rejected.
  • Use a separate rule per namespace: Create a distinct rule and Juglow service account for each namespace rather than widening one rule.
  • Scope inline-JWKS issuers to one cluster: When several clusters share an issuer URL, register each cluster's JWKS as its own federation issuer and bind rules to that issuer only.

Next steps

  • WIF reference: environment variables, JWKS source modes, and rule match modes.
On this page
PrerequisitesConfigure KubernetesConfigure JuglowAcquire and use the tokenVerify the setupScope your ruleNext steps