Skip to content
Latest (currently 2026.7.0) has the newest features, bug fixes, and CVE patches of Solo Enterprise for agentregistry.

Keycloak

Page as Markdown

Deploy Keycloak as the OIDC identity provider for agentregistry and configure the required clients and realm settings.

Solo Enterprise for agentregistry is protected by an identity provider (IdP) so that only authorized users can access, publish, and deploy AI artifacts. This guide walks through deploying Keycloak as the IdP, creating the required OIDC clients, and configuring the registry server and CLI to use them.

Before you begin

These steps assume that you have a Kubernetes cluster, kubectl, and helm already set up. For quick testing, you can use Kind.

Step 1: Configure Keycloak

Solo Enterprise for agentregistry does not bundle its own identity provider. Instead, it relies on an external OIDC provider to authenticate users, issue access tokens, and enforce role-based access control.

Before you deploy Keycloak, configure it by creating a ConfigMap that defines the realm, clients, protocol mappers, groups, and users that Solo Enterprise for agentregistry requires. Keycloak reads this ConfigMap at startup and automatically creates these resources, so you do not have to configure them manually through the Keycloak UI.

The OIDC client configuration differs depending on the provider you connect to Solo Enterprise for agentregistry.

The following ConfigMap defines the resources for a setup where Solo Enterprise for agentregistry connects to AWS Bedrock AgentCore.

ResourceNameDescription
RealmagentregistryA dedicated Keycloak realm that isolates agentregistry’s identity configuration from other applications. The registry server uses the realm’s issuer URL to discover OIDC endpoints.
Clientar-backendA confidential client used by the registry server to validate access tokens issued by the CLI and UI clients.
Clientar-cli-interactiveA public client for the arctl CLI. Uses the device authorization grant (RFC 8628), which lets users log in from a terminal by opening a printed URL in a browser.
Clientar-cli-passwordA public client for scripted user authentication in CI/CD pipelines. Uses the password credentials grant with a username and password.
Clientar-uiA public client for the browser-based registry UI. Uses the authorization code flow with PKCE. The redirectUris and webOrigins are set to * for local testing. In production, replace these with the actual origin your UI uses.
Protocol mappergroupsAn oidc-group-membership-mapper that writes the user’s Keycloak group memberships into the Groups JWT claim. The registry reads this claim to determine RBAC roles. Added to access tokens issued for ar-cli-interactive, ar-cli-password, and ar-ui clients.
Protocol mapperar-backend-audienceAn oidc-audience-mapper that adds ar-backend to the aud claim of access tokens. Without the correct aud claim, the registry server rejects tokens with a 401 Unauthorized error. Added to access tokens issued for ar-cli-interactive, ar-cli-password, and ar-ui clients.
GroupadminsA group that grants full admin access in the registry. Assigned to admin-user so they have superuser rights on first login.
Useradmin-userA test admin user with the password password, assigned to the admins group.
  1. Create the Keycloak namespace.

    kubectl create namespace keycloak
  2. Create the ConfigMap with your Solo Enterprise for agentregistry OIDC configuration.

    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: keycloak-agentregistry-realm
      namespace: keycloak
    data:
      agentregistry-realm.json: |
        {
          "realm": "agentregistry",
          "enabled": true,
          "clients": [
            {
              "clientId": "ar-backend",
              "enabled": true,
              "publicClient": false,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": false
            },
            {
              "clientId": "ar-cli-interactive",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": false,
              "attributes": {
                "oauth2.device.authorization.grant.enabled": "true"
              },
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            },
            {
              "clientId": "ar-cli-password",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": true,
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            },
            {
              "clientId": "ar-ui",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": true,
              "directAccessGrantsEnabled": false,
              "redirectUris": ["*"],
              "webOrigins": ["*"],
              "attributes": {
                "pkce.code.challenge.method": "S256"
              },
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            }
          ],
          "groups": [
            {"name": "admins"}
          ],
          "users": [
            {
              "username": "admin-user",
              "email": "admin-user@example.com",
              "firstName": "Admin",
              "lastName": "User",
              "enabled": true,
              "credentials": [
                {"type": "password", "value": "password", "temporary": false}
              ],
              "groups": ["admins"]
            }
          ]
        }
    EOF

The following ConfigMap defines the resources for a setup where Solo Enterprise for agentregistry connects to Solo Enterprise for kagent. This configuration includes additional clients for the kagent management plane UI.

ResourceNameDescription
RealmagentregistryA dedicated Keycloak realm that isolates agentregistry’s identity configuration from other applications. The registry server uses the realm’s issuer URL to discover OIDC endpoints.
Clientar-backendA confidential client used by the registry server to validate access tokens issued by the CLI and UI clients.
Clientar-cli-interactiveA public client for the arctl CLI. Uses the device authorization grant (RFC 8628), which lets users log in from a terminal by opening a printed URL in a browser.
Clientar-cli-passwordA public client for scripted user authentication in CI/CD pipelines. Uses the password credentials grant with a username and password.
Clientar-uiA public client for the browser-based registry UI. Uses the authorization code flow with PKCE. The redirectUris and webOrigins are set to * for local testing. In production, replace these with the actual origin your UI uses.
Clientkagent-backendA confidential client used by the kagent-enterprise controller and the solo-enterprise-ui backend to validate access tokens issued to kagent-ui. Tokens that include ar-backend in their aud claim are not accepted — only tokens with kagent-backend in the audience are.
Clientkagent-uiA public client for the solo-enterprise-ui browser frontend. Uses the authorization code flow with PKCE.
Protocol mappergroupsAn oidc-group-membership-mapper that writes the user’s Keycloak group memberships into the Groups JWT claim. Added to access tokens issued for ar-cli-interactive, ar-cli-password, ar-ui, and kagent-ui clients.
Protocol mapperar-backend-audienceAn oidc-audience-mapper that adds ar-backend to the aud claim of access tokens issued for ar-cli-interactive, ar-cli-password, and ar-ui clients. Without the correct aud claim, the registry server rejects tokens with a 401 Unauthorized error.
Protocol mapperkagent-backend-audienceAn oidc-audience-mapper that adds kagent-backend to the aud claim of access tokens issued for the kagent-ui client. Without this, kagent-enterprise rejects tokens from the UI with a 401 Unauthorized error.
GroupadminsA group that grants full admin access in the registry. Assigned to admin-user so they have superuser rights on first login.
Useradmin-userA test admin user with the password password, assigned to the admins group.
  1. Create the Keycloak namespace.

    kubectl create namespace keycloak
  2. Create the ConfigMap with your Solo Enterprise for agentregistry OIDC configuration.

    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: keycloak-agentregistry-realm
      namespace: keycloak
    data:
      agentregistry-realm.json: |
        {
          "realm": "agentregistry",
          "enabled": true,
          "clients": [
            {
              "clientId": "ar-backend",
              "enabled": true,
              "publicClient": false,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": false
            },
            {
              "clientId": "ar-cli-interactive",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": false,
              "attributes": {
                "oauth2.device.authorization.grant.enabled": "true"
              },
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            },
            {
              "clientId": "ar-cli-password",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": true,
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            },
            {
              "clientId": "ar-ui",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": true,
              "directAccessGrantsEnabled": false,
              "redirectUris": ["*"],
              "webOrigins": ["*"],
              "attributes": {
                "pkce.code.challenge.method": "S256"
              },
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            },
            {
              "clientId": "kagent-backend",
              "enabled": true,
              "publicClient": false,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": false
            },
            {
              "clientId": "kagent-ui",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": true,
              "directAccessGrantsEnabled": false,
              "redirectUris": ["*"],
              "webOrigins": ["*"],
              "attributes": {
                "pkce.code.challenge.method": "S256"
              },
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "kagent-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "kagent-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            }
          ],
          "groups": [
            {"name": "admins"}
          ],
          "users": [
            {
              "username": "admin-user",
              "email": "admin-user@example.com",
              "firstName": "Admin",
              "lastName": "User",
              "enabled": true,
              "credentials": [
                {"type": "password", "value": "password", "temporary": false}
              ],
              "groups": ["admins"]
            }
          ]
        }
    EOF

Step 2: Deploy Keycloak

With the realm configuration in place, deploy Keycloak. This step creates the Keycloak pod and exposes it through a load balancer service so that both the registry server and the arctl CLI can reach it. The --import-realm flag instructs Keycloak to load the ConfigMap from Step 1 on startup to apply all the realm, client, and user configuration automatically.

  1. Create the Keycloak deployment and service.

    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: Service
    metadata:
      name: keycloak
      namespace: keycloak
      labels:
        app: keycloak
    spec:
      ports:
      - name: http
        port: 8080
        targetPort: 8080
      selector:
        app: keycloak
      type: LoadBalancer
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: keycloak
      namespace: keycloak
      labels:
        app: keycloak
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: keycloak
      template:
        metadata:
          labels:
            app: keycloak
        spec:
          containers:
          - name: keycloak
            image: quay.io/keycloak/keycloak:26.1.3
            args: ["start-dev", "--import-realm"]
            env:
            - name: KEYCLOAK_ADMIN
              value: "admin"
            - name: KEYCLOAK_ADMIN_PASSWORD
              value: "admin"
            - name: PROXY_ADDRESS_FORWARDING
              value: "true"
            - name: KC_PROXY
              value: "edge"
            ports:
            - name: http
              containerPort: 8080
            readinessProbe:
              httpGet:
                path: /realms/master
                port: 8080
            volumeMounts:
            - name: realm-config
              mountPath: /opt/keycloak/data/import
          volumes:
          - name: realm-config
            configMap:
              name: keycloak-agentregistry-realm
    EOF
  2. Wait for the Keycloak rollout to finish.

    kubectl -n keycloak rollout status deploy/keycloak
  3. Set the Keycloak endpoint details from the load balancer service. These environment variables are used throughout the rest of this guide. The KEYCLOAK_ISSUER is the OIDC discovery URL that the registry uses to find the token and JWKS endpoints. If you are running locally in Kind and need a local IP address for the load balancer service, consider using cloud-provider-kind.

    export ENDPOINT_KEYCLOAK=$(kubectl -n keycloak get service keycloak -o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}'):8080
    export HOST_KEYCLOAK=$(echo ${ENDPOINT_KEYCLOAK} | cut -d: -f1)
    export PORT_KEYCLOAK=$(echo ${ENDPOINT_KEYCLOAK} | cut -d: -f2)
    export KEYCLOAK_URL=http://${ENDPOINT_KEYCLOAK}
    export KEYCLOAK_REALM=agentregistry
    export KEYCLOAK_ISSUER=$KEYCLOAK_URL/realms/${KEYCLOAK_REALM}
    echo $KEYCLOAK_ISSUER
  4. Get the client secrets that Keycloak automatically generated. You pass these secrets to the respective Helm charts in the next steps.

    Get the ar-backend client secret. The registry server uses this secret to verify access tokens.

    export KEYCLOAK_TOKEN=$(curl -s \
      -d "client_id=admin-cli" -d "username=admin" -d "password=admin" \
      -d "grant_type=password" \
      "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
      | jq -r .access_token)
    
    export AR_BACKEND_ID=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
      "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=ar-backend" \
      | jq -r '.[0].id')
    
    export AR_BACKEND_SECRET=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
      "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${AR_BACKEND_ID}/client-secret" \
      | jq -r .value)
    echo "AR_BACKEND_SECRET: $AR_BACKEND_SECRET"

    Get the ar-backend and kagent-backend client secrets. The registry server uses the ar-backend secret to verify access tokens. The kagent-enterprise controller uses the kagent-backend secret to verify tokens that are issued to the kagent-ui client.

    export KEYCLOAK_TOKEN=$(curl -s \
      -d "client_id=admin-cli" -d "username=admin" -d "password=admin" \
      -d "grant_type=password" \
      "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
      | jq -r .access_token)
    
    export AR_BACKEND_ID=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
      "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=ar-backend" \
      | jq -r '.[0].id')
    export AR_BACKEND_SECRET=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
      "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${AR_BACKEND_ID}/client-secret" \
      | jq -r .value)
    echo "AR_BACKEND_SECRET: $AR_BACKEND_SECRET"
    
    export KAGENT_BACKEND_ID=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
      "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=kagent-backend" \
      | jq -r '.[0].id')
    export KAGENT_BACKEND_SECRET=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
      "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${KAGENT_BACKEND_ID}/client-secret" \
      | jq -r .value)
    echo "KAGENT_BACKEND_SECRET: $KAGENT_BACKEND_SECRET"

Step 3: Install Solo Enterprise for kagent

This step is only required if you are setting up Solo Enterprise for kagent as a provider. If you are setting up Agentcore (AWS), skip to Step 4: Install Solo Enterprise for agentregistry.
  1. Save your Solo Enterprise for kagent license key in an environment variable. To obtain the key, contact an account representative. If you prefer to specify the license key in a secret instead, see Licensing.

    export KAGENT_LICENSE_KEY="<key>"
  2. Set the Solo Enterprise for kagent patch version. You can find available versions in the reference documentation.

    export KAGENT_ENT_VERSION=0.3.17
  3. Install the kagent-mgmt chart for Solo Enterprise for kagent.

    helm upgrade -i kagent-mgmt \
      oci://us-docker.pkg.dev/solo-public/solo-enterprise-helm/charts/management \
      -n kagent --create-namespace \
      --version "$KAGENT_ENT_VERSION" \
      --set cluster=mgmt-cluster \
      --set products.kagent.enabled=true \
      --set products.agentregistry.enabled=true \
      --set-string licensing.licenseKey="$KAGENT_LICENSE_KEY" \
      --set-string oidc.issuer="$KEYCLOAK_ISSUER" \
      --set-string ui.backend.oidc.clientId=kagent-backend \
      --set-string ui.backend.oidc.secret="$KAGENT_BACKEND_SECRET" \
      --set-string ui.frontend.oidc.clientId=kagent-ui
  4. Verify that the management component pods have a status of Running.

    kubectl get po -n kagent

    Example output:

    NAME                                    READY   STATUS    RESTARTS   AGE
    kagent-mgmt-clickhouse-shard0-0         1/1     Running   0          4s
    solo-enterprise-telemetry-collector-0   1/1     Running   0          4s
    solo-enterprise-ui-76c85c7d6b-xftcj     5/5     Running   0          4s
    
  5. Install the kagent-enterprise CRDs chart.

    helm upgrade --install kagent-crds \
      oci://us-docker.pkg.dev/solo-public/kagent-enterprise-helm/charts/kagent-enterprise-crds \
      -n kagent \
      --version "$KAGENT_ENT_VERSION"
  6. Install the kagent-enterprise chart.

    helm upgrade -i kagent \
      oci://us-docker.pkg.dev/solo-public/kagent-enterprise-helm/charts/kagent-enterprise \
      -n kagent \
      --version "$KAGENT_ENT_VERSION" \
      --set-string licensing.licenseKey="$KAGENT_LICENSE_KEY" \
      --set agents.k8s-agent.enabled=true \
      --set kagent-tools.enabled=true \
      --set kmcp.licensing.createSecret=false \
      --set-string oidc.issuer="$KEYCLOAK_ISSUER" \
      --set oidc.clientId=kagent-backend \
      --set-string oidc.secret="$KAGENT_BACKEND_SECRET"
  7. Verify that all pods are running.

    kubectl get po -n kagent

    Example output:

    kagent-controller-dfc4c966c-zzds5                    1/1     Running   0          3m
    kagent-mgmt-clickhouse-shard0-0                      1/1     Running   0          5m48s
    kagent-postgresql-57c4f5597f-x4mjg                   1/1     Running   0          3m
    kagent-tools-69bdb559df-cb9bw                        1/1     Running   0          3m
    kmcp-enterprise-controller-manager-dbd59bcbf-j5hjt   1/1     Running   0          3m
    solo-enterprise-telemetry-collector-0                1/1     Running   0          5m48s
    solo-enterprise-ui-66cbb6687c-6czzk                  4/4     Running   0          5m48s
    

Step 4: Install Solo Enterprise for agentregistry

Install Solo Enterprise for agentregistry and connect it to the Keycloak instance that you set up. The OIDC Helm values point the registry server at your Keycloak realm so that it knows what endpoint to use to validate access tokens, which client credentials to use, and which JWT claims carry the role information used for RBAC decisions.

  1. Save your Solo Enterprise for agentregistry license key in an environment variable, then install Solo Enterprise for agentregistry. To obtain the key, contact an account representative. For more options, such as providing the key in a secret, see Licensing.

    export AGENTREGISTRY_LICENSE_KEY=<license_key>
    helm upgrade --install agentregistry \
      oci://us-docker.pkg.dev/solo-public/agentregistry-enterprise/helm/agentregistry-enterprise \
      --version 2026.7.0 \
      --namespace agentregistry-system \
      --create-namespace \
      --set licensing.createSecret=true \
      --set licensing.licenseKey=$AGENTREGISTRY_LICENSE_KEY \
      --set oidc.issuer=$KEYCLOAK_ISSUER \
      --set oidc.clientId=ar-backend \
      --set oidc.clientSecret=$AR_BACKEND_SECRET \
      --set oidc.publicClientId=ar-ui \
      --set oidc.roleClaim=Groups \
      --set oidc.superuserRole=admins
    Helm valueDescription
    licensing.createSecretSet to true so that the chart creates the license secret from the licensing.licenseKey value.
    licensing.licenseKeyYour Solo Enterprise for agentregistry license key.
    oidc.issuerThe issuer URL of your Keycloak realm. The registry uses this URL to discover the OIDC provider’s endpoints.
    oidc.clientIdThe confidential backend client ID (ar-backend).
    oidc.clientSecretThe client secret for the ar-backend client.
    oidc.publicClientIdThe public client ID for the browser-based UI (ar-ui). The server uses this client to log in to the UI.
    oidc.roleClaimThe JWT claim that contains role or group names. Must match the Token Claim Name in the group mapper (Groups).
    oidc.superuserRoleThe group name that grants full admin access (admins).
  2. Verify that the Solo Enterprise for agentregistry pods are up and running.

    kubectl get pods -n agentregistry-system

    Example output:

    NAME                                                            READY   STATUS    RESTARTS   AGE
    agentregistry-clickhouse-shard0-0                               1/1     Running   0          40s
    agentregistry-enterprise-postgresql-764898cbff-hwb4r            1/1     Running   0          40s
    agentregistry-enterprise-server-79c4cdcdf8-dp6h4                1/1     Running   0          40s
    agentregistry-enterprise-telemetry-collector-777798c5b4-rctfs   1/1     Running   0          40s
    
  3. Port-forward the Solo Enterprise for agentregistry controller on port 12121. The registry service is not exposed externally, so port-forwarding is required to reach it from your local machine. The arctl CLI and the UI both default to http://localhost:12121 as the registry endpoint.

    kubectl -n agentregistry-system port-forward svc/agentregistry-enterprise-server 12121:12121

Step 5: Log in with the CLI

  1. Download and install the arctl CLI.

    curl -sSL https://storage.googleapis.com/agentregistry-enterprise/install.sh | ARCTL_VERSION=v2026.7.0 sh
    export PATH=$HOME/.arctl/bin:$PATH
  2. If you previously used the registry in demo auth mode, unset the ARCTL_API_TOKEN environment variable. This step is required to remove any previously obtained access tokens so that requests are not rejected with a 401 HTTP response.

    unset ARCTL_API_TOKEN
  3. Log in to the registry. Choose between the interactive device authorization and the static username and password credential flow.

    Use this flow for interactive logins from a terminal. The CLI uses the ar-cli-interactive public client and starts a device authorization grant flow that prints a URL and a one-time code for you to approve in a browser.

    1. Configure the CLI to use the Keycloak issuer and ar-cli-interactive client. Then, log in to Solo Enterprise for agentregistry.

      export OIDC_ISSUER=$KEYCLOAK_ISSUER
      export OIDC_CLIENT_ID=ar-cli-interactive
      arctl user login

      Example output:

      To complete the login process, please:
       1. Open: http://172.18.0.13:8080/realms/agentregistry/device
       2. Enter the code: SYSQ-SNSP
       - or go to: http://172.18.0.13:8080/realms/agentregistry/device?user_code=SYSQ-SNSP
      
    2. Open the URL in a web browser and log into Keycloak with the admin-user username and password password. Confirming the code in the browser tells Keycloak to issue an access token for the CLI session. After authorization is complete, the CLI stores the token in your system keychain so you do not have to log in again until it expires.

    Use this flow for scripted logins where a specific user identity is required in CI/CD pipelines. The CLI uses the ar-cli-password public client and exchanges a username and password directly for an access token. The password can be supplied with the --oidc-password flag or the OIDC_CLIENT_PASSWORD environment variable. If both are set, the CLI flag takes precedence.

    1. Log in to Solo Enterprise for agentregistry with the admin-user/password credentials.

      export OIDC_ISSUER=$KEYCLOAK_ISSUER
      arctl user login \
        --oidc-flow=password-credentials \
        --oidc-client-id=ar-cli-password \
        --oidc-username=admin-user \
        --oidc-password=password
    2. Save the access token in the ARCTL_API_TOKEN environment variable.

      export ARCTL_API_TOKEN=$(arctl user info --show-tokens | jq -r .access_token)

  4. Verify that you can run arctl commands.

    arctl user whoami

    Example output:

    FIELD       VALUE
    Subject     37aae242-96df-4261-81c6-2b8fd8c4e8a8
    Email       admin-user@example.com
    Issuer      http://172.18.0.17:8080/realms/agentregistry
    Superuser   true (overrides role-based permission checks)
    
    ROLE     STATUS
    admins   no explicit permissions configured
    
If you see 401 errors in the CLI, you might have previously stored a token in the ARCTL_API_TOKEN environment variable. This variable takes precedence over the token obtained via the CLI login flow. Run unset ARCTL_API_TOKEN and repeat the login flow.

Cleanup

kubectl delete namespace keycloak