MastertheMesh
Solo · platform install · kind · Keycloak · kagent · AgentRegistry · Istio ambient · agentgateway · OTel
Built · the full platform stood up on kind, product by product

Full Solo Enterprise Install deployment

By the end you have the whole Solo Enterprise agentic platform running on one kind cluster: Keycloak as the single OIDC issuer, Solo Enterprise for kagent, the OTel stack and the Enterprise UI, in-cluster AgentRegistry, and Istio ambient with enterprise agentgateway as the waypoint data plane, all reachable through one ingress at http://*.localtest.me with no port-forwards. You install it product by product, in dependency order, and for each one you get the real Helm command or YAML, why each flag is there, and the configuration notes that make it click into place.

Scope: this lab installs the foundation, it does not run anything on it. There are no agents, no MCP servers, and no skills here. The goal is the platform itself, the layer everything else runs on. To actually scaffold, publish, and deploy an agent across runtimes (Solo Enterprise for kagent and AWS Bedrock AgentCore) on top of this exact stack, go to the AgentCore lab. To put registry identity, AccessPolicies, and per-team catalog visibility on top, see the governance lab.

Before you begin

Prerequisites

  • docker, kind, kubectl, helm, jq, gcloud
  • arctl v2026.6.1, the enterprise CLI
  • gcloud auth login plus helm registry login to the Solo public registry (the charts are public, the OCI pull still needs a token)
  • Solo Enterprise license keys for kagent, Solo Istio, and enterprise agentgateway
  • ANTHROPIC_API_KEY (the kagent chart wires it as the default model provider)

What this installs, in order

  • kind cluster, a host OCI registry, and the Gateway API CRDs
  • Keycloak, the single OIDC issuer (realm, clients, personas)
  • Solo Enterprise for kagent
  • OTel telemetry and the Enterprise UI (Solo Enterprise management chart)
  • AgentRegistry, in-cluster
  • Istio ambient (Gloo Operator and a ServiceMeshController) and enterprise agentgateway
  • An ingress Gateway and the consoles at *.localtest.me

What the platform is

Seven products, each in its own namespace, wired together by one OIDC issuer and one ingress. Keycloak issues the tokens. Solo Enterprise for kagent is the agent runtime. The Solo Enterprise management chart brings the OTel collectors, ClickHouse, and the Enterprise UI (Dashboard, Agents, Tracing, Access Policies). AgentRegistry is the in-cluster catalog and control plane. Istio ambient plus enterprise agentgateway is the data plane that makes a kagent AccessPolicy enforceable at a waypoint. And the enterprise-agentgateway ingress Gateway puts the consoles on *.localtest.me with no port-forwards.

NamespaceWhat lives there
keycloakKeycloak (dev mode), the single OIDC issuer, realm agentregistry
kagentSolo Enterprise for kagent: the controller, bundled tool server, the OBO signing key
solo-enterpriseSolo Enterprise management chart: ClickHouse, OTel collectors, the Enterprise UI
agentregistry-systemAgentRegistry server plus its bundled Postgres, ClickHouse, and telemetry collector
agentgateway-systementerprise agentgateway controller and the ingress Gateway
gloo-systemGloo Operator and the AgentgatewayParameters for the waypoint GatewayClass
istio-systemSolo Istio in ambient mode: istiod, ztunnel, the CNI
KIND CLUSTER · agentcore-demo 1 · kind + host registry :5001 · Gateway API CRDs 2 · Keycloak single OIDC issuer realm agentregistry 3 · Solo for kagent controller · OBO key aud kagent-backend 4 · OTel + Enterprise UI ClickHouse · collectors Tracing · Agents · Policies 5 · AgentRegistry in-cluster · :12121 Postgres + ClickHouse 6 · Istio ambient + enterprise agentgateway Gloo Operator · ServiceMeshController · ztunnel the waypoint data plane (GatewayClasses) 7 · ingress Gateway · *.localtest.me NodePort 30080 ← host :80 keycloak · agentregistry · kagent CONSOLES (no port-forward) http://keycloak.localtest.me http://agentregistry.localtest.me http://kagent.localtest.me OIDC

Prerequisites

Tooling first: docker, kind, kubectl, helm, jq, openssl, plus gcloud for the Solo public Helm charts and arctl for the registry CLI. The Solo charts live in a public Google Artifact Registry, but a Helm OCI pull still needs a token, so authenticate gcloud once and log Helm in to the registry. arctl is pinned to v2026.6.1, the in-cluster registry release (the old local Docker daemon is gone).

# gcloud + helm OCI login for the Solo public charts
gcloud auth login
gcloud auth print-access-token \
  | helm registry login -u oauth2accesstoken --password-stdin us-docker.pkg.dev

# arctl pinned to the in-cluster release
curl -sSL https://storage.googleapis.com/agentregistry-enterprise/install.sh \
  | ARCTL_VERSION=v2026.6.1 sh
export PATH="$HOME/.arctl/bin:$PATH"

Three licenses are needed, all enterprise: Solo Enterprise for kagent, Solo Istio, and enterprise agentgateway. In this lab they can all be one Solo license key. You also need an Anthropic API key for the kagent default model provider. AgentRegistry itself needs no license.

Why gcloud. The charts under us-docker.pkg.dev/solo-public are public, but Helm's OCI client still presents a bearer token on pull, so the helm registry login step is not optional. The Solo Istio container images are a separate problem handled in step 6: kind nodes have no registry credentials, so those images are pre-pulled on the host and loaded into the cluster.

Running it

Every step below is automated by the scripts that ship with the AgentCore lab. ./scripts/setup.sh runs the whole chain in order, or run each step on its own:

StepScript
Prerequisites./scripts/00-prereqs.sh
Step 1 · cluster + registry + Gateway API./scripts/01-cluster.sh
Step 2 · Keycloak./scripts/02-keycloak.sh
Step 3 · Solo Enterprise for kagent./scripts/03-kagent.sh
Step 4 · OTel + Enterprise UI./scripts/03b-telemetry.sh
Step 5 · AgentRegistry./scripts/04-agentregistry.sh
Step 6 · Istio ambient + agentgateway./scripts/05-waypoint.sh
Step 7 · ingress Gateway./scripts/06-gateway.sh
Connect arctl./scripts/connect.sh

Step 1: kind cluster, host OCI registry, Gateway API CRDs

Run: ./scripts/00-prereqs.sh then ./scripts/01-cluster.sh

The kind config does two things that matter for the whole platform. It maps host ports 80 and 443 onto NodePorts 30080 and 30443, so once the ingress Gateway is up the consoles answer at http://*.localtest.me with nothing forwarded. And it points containerd at /etc/containerd/certs.d so the nodes can pull localhost:5001/... images from the host-side registry.

yamlkind/cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: agentcore-demo
nodes:
  - role: control-plane
    # Map host :80/:443 to the agentgateway ingress (a NodePort Service on
    # 30080/30443), so the consoles are reachable at http://*.localtest.me with
    # no kubectl port-forward. *.localtest.me resolves to 127.0.0.1 via public DNS.
    extraPortMappings:
      - containerPort: 30080
        hostPort: 80
        protocol: TCP
      - containerPort: 30443
        hostPort: 443
        protocol: TCP
  - role: worker
# Read per-registry config from /etc/containerd/certs.d so the nodes can pull
# localhost:5001/... from the host-side kind-registry container.
containerdConfigPatches:
  - |-
    [plugins."io.containerd.grpc.v1.cri".registry]
      config_path = "/etc/containerd/certs.d"
networking:
  apiServerAddress: "127.0.0.1"

The host registry follows the canonical kind local-registry recipe: a registry:2 container on 127.0.0.1:5001, a per-node hosts.toml that resolves localhost:5001 to kind-registry:5000, a local-registry-hosting ConfigMap that advertises it to the cluster, and joining the registry to the kind docker network so the nodes can reach it by name. Then the standard Gateway API CRDs.

bashhost registry + Gateway API CRDs (from 01-cluster.sh)
# 1. registry container on the host
docker run -d --restart=always -p 127.0.0.1:5001:5000 --name kind-registry registry:2

# 2. per-node hosts.toml: localhost:5001 -> kind-registry:5000
for node in $(kind get nodes --name agentcore-demo); do
  docker exec "$node" mkdir -p /etc/containerd/certs.d/localhost:5001
  cat <<EOF | docker exec -i "$node" cp /dev/stdin /etc/containerd/certs.d/localhost:5001/hosts.toml
[host."http://kind-registry:5000"]
EOF
done

# 3. join the registry to the kind docker network
docker network connect kind kind-registry

# 4. advertise it to the cluster
kubectl apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata: { name: local-registry-hosting, namespace: kube-public }
data:
  localRegistryHosting.v1: |
    host: "localhost:5001"
    help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF

# 5. Gateway API standard CRDs
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml

Step 2: Keycloak, the single OIDC issuer

Run: ./scripts/02-keycloak.sh

Keycloak is the one issuer for the whole platform, both AgentRegistry and kagent validate against it. It runs in dev mode as a StatefulSet with --import-realm, with bootstrap admin admin/admin. The key setting is KC_HOSTNAME=http://keycloak.localtest.me: that stamps the token iss as the gateway hostname so the same issuer string works in the browser, on the host, and (via the hostAlias in step 5) inside the cluster.

yamlyaml/keycloak/keycloak.yaml (the env that matters)
containers:
  - name: keycloak
    image: quay.io/keycloak/keycloak:26.3
    args: [start-dev, --import-realm]
    env:
      - { name: KC_BOOTSTRAP_ADMIN_USERNAME, value: admin }
      - { name: KC_BOOTSTRAP_ADMIN_PASSWORD, value: admin }
      - { name: KC_HTTP_RELATIVE_PATH, value: / }
      # Issuer = http://keycloak.localtest.me (port 80). The SAME iss works in the
      # browser (via the gateway) and in-cluster (via a hostAlias to this ClusterIP).
      - { name: KC_HOSTNAME, value: "http://keycloak.localtest.me" }
      - { name: KC_HOSTNAME_STRICT, value: "false" }
      - { name: KC_HOSTNAME_BACKCHANNEL_DYNAMIC, value: "true" }
      - { name: KC_PROXY_HEADERS, value: "xforwarded" }
      - { name: KC_HEALTH_ENABLED, value: "true" }
    volumeMounts:
      - { name: realm-import, mountPath: /opt/keycloak/data/import, readOnly: true }

The realm is agentregistry. It carries the clients both products need, one group, a group-membership mapper, and the per-client audience mappers. The clients:

ClientRole
ar-backendAgentRegistry confidential validator (no flows enabled, validation only)
ar-uiAgentRegistry UI, public, authcode + PKCE
ar-cli-passwordscripted arctl login, public, password grant (audience ar-backend)
kagent-backendkagent confidential validator, service accounts enabled (audience kagent-backend)
kagent-uiEnterprise UI frontend, public, authcode + PKCE (audience kagent-backend)

Two realm details run through everything that follows. First, every client carries a group-membership mapper that emits the user's groups as a claim named Groups, capital G. Every role-mapper in the platform reads claims.Groups, so the realm emits the claim with a capital G to match. Second, the audience mappers are split on purpose: the ar-* clients stamp aud: ar-backend, while kagent-ui and kagent-cli-password stamp aud: kagent-backend, so each product validates tokens against its own audience.

jsonthe two mapper shapes on each client (from agentregistry-realm.json)
{
  "name": "groups",
  "protocol": "openid-connect",
  "protocolMapper": "oidc-group-membership-mapper",
  "config": {
    "claim.name": "Groups",          // capital G, every role mapper reads claims.Groups
    "full.path": "false",
    "id.token.claim": "true",
    "access.token.claim": "true",
    "userinfo.token.claim": "true"
  }
},
{
  "name": "kagent-backend-audience", // ar-* clients use included.client.audience: ar-backend
  "protocol": "openid-connect",
  "protocolMapper": "oidc-audience-mapper",
  "config": {
    "included.client.audience": "kagent-backend",
    "id.token.claim": "false",
    "access.token.claim": "true"
  }
}

Personas. The realm ships one built-in superuser: admin-user / password, in group admins. Group admins maps to the kagent global.Admin role and to the AgentRegistry superuser. To model real teams you add more realm users, say alice and bob, and place them in groups like readers and writers; the role-mapper then turns the Groups claim into product roles per user. The governance lab does exactly this, with per-team catalog lanes built on these same groups.

Keycloak generates the two confidential client secrets (ar-backend and kagent-backend) at realm import. The next charts consume them, so scrape them from the Keycloak admin API once Keycloak is up.

bashscrape a confidential client secret (from lib.sh)
# admin token on the master realm, then the client secret on agentregistry
kubectl -n keycloak port-forward svc/keycloak 18099:8080 &
ADM=$(curl -s -X POST http://localhost:18099/realms/master/protocol/openid-connect/token \
  -d 'grant_type=password&client_id=admin-cli&username=admin&password=admin' | jq -r .access_token)
CID=$(curl -s -H "Authorization: Bearer $ADM" \
  "http://localhost:18099/admin/realms/agentregistry/clients?clientId=kagent-backend" | jq -r '.[0].id')
curl -s -H "Authorization: Bearer $ADM" \
  "http://localhost:18099/admin/realms/agentregistry/clients/$CID/client-secret" | jq -r .value

The two DNS facts

Two name-resolution facts recur in every step, so it is worth stating them once.

1. *.localtest.me resolves to 127.0.0.1 via public DNS. No /etc/hosts entry is needed. A browser request to http://keycloak.localtest.me hits host port 80, the kind extraPortMapping forwards it to NodePort 30080, the ingress Gateway picks it up, an HTTPRoute matches the hostname, and it lands on the backing Service.

2. In-cluster pods cannot resolve keycloak.localtest.me. Public DNS sends it to 127.0.0.1, which inside a pod is the pod itself. Every pod that does OIDC discovery (the AgentRegistry server, the kagent controller, the Enterprise UI) therefore gets a hostAlias that maps keycloak.localtest.me to Keycloak's ClusterIP. The bridge_keycloak_hostalias helper patches it onto each deployment, then the pod rolls and OIDC discovery resolves.

bashthe hostAlias bridge (bridge_keycloak_hostalias in lib.sh)
# map keycloak.localtest.me -> Keycloak's ClusterIP on a deployment's pods
ip=$(kubectl -n keycloak get svc keycloak -o jsonpath='{.spec.clusterIP}')
kubectl -n "$ns" patch deploy "$dep" --type=json -p \
  "[{\"op\":\"replace\",\"path\":\"/spec/template/spec/hostAliases\",\
     \"value\":[{\"ip\":\"$ip\",\"hostnames\":[\"keycloak.localtest.me\"]}]}]"

Step 3: Solo Enterprise for kagent

Run: ./scripts/03-kagent.sh

kagent is installed as two charts, CRDs then controller, both at 0.4.3 from oci://us-docker.pkg.dev/solo-public/kagent-enterprise-helm/charts. The provider is Anthropic. The controller runs an OIDC access-token interceptor and refuses to start without a discoverable issuer, so oidc.issuer points at the Keycloak issuer, oidc.clientId is kagent-backend, and its secret comes from a Secret reference. oidc.skipOBO=false turns on on-behalf-of token exchange, which needs an RSA signing key in a Secret named jwt in the kagent namespace.

bashOBO signing key + OIDC secret (from 03-kagent.sh)
# RSA signing key the controller uses to mint OBO tokens; Secret MUST be named jwt
openssl genpkey -algorithm RSA -out jwt.pem -pkeyopt rsa_keygen_bits:2048
kubectl -n kagent create secret generic jwt --from-file=jwt=jwt.pem

# the kagent-backend confidential client secret scraped in step 2
kubectl -n kagent create secret generic kagent-enterprise-oidc-secret \
  --from-literal=clientSecret="$KAGENT_BACKEND_SECRET"
bashinstall the controller (from 03-kagent.sh)
helm upgrade --install kagent-crds \
  oci://us-docker.pkg.dev/solo-public/kagent-enterprise-helm/charts/kagent-enterprise-crds \
  --namespace kagent --create-namespace --version 0.4.3 --wait

helm upgrade --install kagent \
  oci://us-docker.pkg.dev/solo-public/kagent-enterprise-helm/charts/kagent-enterprise \
  --namespace kagent --version 0.4.3 \
  --set global.licensing.licenseKey="$SOLO_LICENSE_KEY" \
  --set providers.default=anthropic \
  --set providers.anthropic.apiKey="$ANTHROPIC_API_KEY" \
  --set oidc.issuer="http://keycloak.localtest.me/realms/agentregistry" \
  --set oidc.clientId=kagent-backend \
  --set oidc.secretRef=kagent-enterprise-oidc-secret \
  --set oidc.secretKey=clientSecret \
  --set oidc.skipOBO=false \
  --set-json 'controller.envFrom=[{"configMapRef":{"name":"kagent-enterprise-config"}}]' \
  --set kagent-tools.enabled=true \
  --set ui.enabled=false \
  --set otel.tracing.enabled=true \
  --set otel.tracing.exporter.otlp.endpoint="http://solo-enterprise-telemetry-collector.solo-enterprise.svc.cluster.local:4317" \
  --set-json 'rbac.roleMapping={"roleMapper":"claims.Groups.transformList(i, v, v in rolesMap, rolesMap[v])","roleMappings":{"admins":"global.Admin","readers":"global.Reader","writers":"global.Writer"}}'

The role-mapper CEL reads claims.Groups and maps group admins to global.Admin. The install does not pass --wait: the controller does OIDC discovery at startup against keycloak.localtest.me, which a pod cannot resolve until the hostAlias is added, so it would never go Ready under --wait. The script applies the chart, adds the hostAlias to the controller, then waits. The OTel tracing settings also make the controller stamp the collector endpoint into every agent it later deploys, so spans export once an agent runs.

Step 4: OTel telemetry and the Enterprise UI

Run: ./scripts/03b-telemetry.sh

The Solo Enterprise management chart (0.4.3 from oci://us-docker.pkg.dev/solo-public/solo-enterprise-helm/charts/management) bundles ClickHouse, the OTel collectors, and the Enterprise UI served at kagent.localtest.me with its Dashboard, Agents, Tracing, and Access Policies views. The trace path is: an agent exports OTLP to the telemetry collector, the collector writes to ClickHouse, and the Tracing tab reads it back.

bashinstall the management chart (from 03b-telemetry.sh)
# the Enterprise UI backend validates with the kagent-backend client secret
kubectl -n solo-enterprise create secret generic ui-backend-oidc-secret \
  --from-literal=clientSecret="$KAGENT_BACKEND_SECRET"

helm upgrade --install solo-mgmt \
  oci://us-docker.pkg.dev/solo-public/solo-enterprise-helm/charts/management \
  --namespace solo-enterprise --create-namespace --version 0.4.3 \
  --set cluster=agentcore-demo \
  --set products.kagent.enabled=true \
  --set products.kagent.namespace=kagent \
  --set products.agentgateway.namespace=agentgateway-system \
  --set licensing.licenseKey="$SOLO_LICENSE_KEY" \
  --set clickhouse.persistentVolume.enabled=false \
  --set oidc.issuer="http://keycloak.localtest.me/realms/agentregistry" \
  --set ui.frontend.oidc.clientId=kagent-ui \
  --set ui.backend.oidc.clientId=kagent-backend \
  --set-json 'rbac.roleMapping={"roleMapper":"claims.Groups.transformList(i, v, v in rolesMap, rolesMap[v])","roleMappings":{"admins":"global.Admin","readers":"global.Reader","writers":"global.Writer"}}'

The two flags that make the UI manage the local cluster are products.kagent.enabled=true and products.kagent.namespace=kagent; without the namespace pointing at the local kagent install the UI has nothing to manage. ui.frontend.oidc.clientId=kagent-ui is the public PKCE client for the browser; ui.backend.oidc.clientId=kagent-backend is the confidential client whose secret is the Secret above. ClickHouse persistence is off for the demo. As with kagent, there is no --wait: the UI backend does OIDC discovery, so the script installs, adds the hostAlias to solo-enterprise-ui, then waits for readiness.

Step 5: AgentRegistry, in-cluster

Run: ./scripts/04-agentregistry.sh

AgentRegistry at 2026.6.1 from oci://us-docker.pkg.dev/solo-public/agentregistry-enterprise/helm/agentregistry-enterprise is the in-cluster server with a bundled Postgres, ClickHouse, and telemetry collector. No license is required. OIDC is wired to the same realm: oidc.clientId=ar-backend with its scraped secret as the validator, oidc.publicClientId=ar-ui for the browser, oidc.roleClaim=Groups, and oidc.superuserRole=admins. Postgres is bundled. The server Service listens on :12121 and stays ClusterIP; the ingress in step 7 exposes it.

bashinstall AgentRegistry (from 04-agentregistry.sh)
helm upgrade --install agentregistry \
  oci://us-docker.pkg.dev/solo-public/agentregistry-enterprise/helm/agentregistry-enterprise \
  --namespace agentregistry-system --create-namespace --version 2026.6.1 \
  --set oidc.issuer="http://keycloak.localtest.me/realms/agentregistry" \
  --set oidc.clientId=ar-backend \
  --set oidc.clientSecret="$AR_BACKEND_SECRET" \
  --set oidc.publicClientId=ar-ui \
  --set oidc.roleClaim=Groups \
  --set oidc.superuserRole=admins \
  --set kagent.outboundAuth.oidc.clientId=kagent-backend \
  --set kagent.outboundAuth.oidc.clientSecret="$KAGENT_BACKEND_SECRET" \
  --set database.postgres.type=bundled
Configuration note: point kagent.outboundAuth at kagent-backend. When the registry later deploys an agent to the kagent runtime it mints a client-credentials token to call the kagent controller, and that token needs the kagent audience. Set kagent.outboundAuth.oidc to kagent-backend, the service-account client whose token carries aud: kagent-backend and Groups: [admins], so the cross-product call is authorized end to end. The server gets the same hostAlias treatment, then the script waits for it to come up.

Step 6: Istio ambient and enterprise agentgateway, the waypoint data plane

Run: ./scripts/05-waypoint.sh

A kagent AccessPolicy is inert on its own. Enforcement happens at an agentgateway waypoint sitting in front of the workload, and a waypoint needs Istio in ambient mode. This step stands up that data plane: ambient Istio via the Gloo Operator and a ServiceMeshController, then enterprise agentgateway, which provides the enterprise-agentgateway and enterprise-agentgateway-waypoint GatewayClasses. There is no workload here; this is the data plane the AgentCore lab later enforces policy on.

First the Gateway API experimental CRDs, which ambient waypoints need. The standard install from step 1 (Gateway API at 1.5 and up) ships a safe-upgrades.gateway.networking.k8s.io ValidatingAdmissionPolicy and Binding that forbid layering the experimental channel on top, so delete both first, then apply the experimental CRDs. The deletion is eventually consistent, so retry the apply with backoff, re-dropping the policy on each denial.

bashexperimental Gateway API CRDs (from 05-waypoint.sh)
# drop the safe-upgrades policy that blocks the experimental channel
kubectl delete validatingadmissionpolicybinding safe-upgrades.gateway.networking.k8s.io --ignore-not-found
kubectl delete validatingadmissionpolicy        safe-upgrades.gateway.networking.k8s.io --ignore-not-found

# then apply experimental (retry with backoff; re-drop on each denial)
kubectl apply --server-side --force-conflicts \
  -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml

Then the Gloo Operator (0.5.2 from oci://us-docker.pkg.dev/solo-public/gloo-operator-helm/gloo-operator) and a ServiceMeshController in ambient mode. The SMC version field is the stripped form 1.29.2-patch0; the operator appends -solo for the Standard distribution. Cluster and network are set so the mesh identity is consistent.

yamlServiceMeshController (ambient), from 05-waypoint.sh
apiVersion: operator.gloo.solo.io/v1
kind: ServiceMeshController
metadata:
  name: managed-istio
spec:
  cluster: agentcore-demo
  network: agentcore-demo
  trustDomain: cluster.local
  version: "1.29.2-patch0"      # operator appends -solo for distribution: Standard
  dataplaneMode: Ambient
  distribution: Standard
  installNamespace: istio-system
  scalingProfile: Demo
  trafficCaptureMode: Auto
  onConflict: Force
  image:
    registry: us-docker.pkg.dev
    repository: soloio-img/istio
Configuration note: load the Solo Istio images into kind. kind nodes pull from the host, so fetch the four Solo Istio images (pilot, proxyv2, install-cni, ztunnel) and kind load them into the cluster before the ServiceMeshController reconciles. The operator deploys the -solo distribution tags (pilot:1.29.2-patch0-solo), so pull those exact tags.
bashpre-pull + kind load the Solo Istio images (from 05-waypoint.sh)
gcloud auth configure-docker us-docker.pkg.dev --quiet
for img in pilot proxyv2 install-cni ztunnel; do
  docker pull "us-docker.pkg.dev/soloio-img/istio/$img:1.29.2-patch0-solo"
  kind load docker-image "us-docker.pkg.dev/soloio-img/istio/$img:1.29.2-patch0-solo" --name agentcore-demo
done

Then enterprise agentgateway at v2026.5.1 (CRDs then controller, from oci://us-docker.pkg.dev/solo-public/enterprise-agentgateway/charts), which registers the two GatewayClasses. The last piece is the waypoint identity.

bashenterprise agentgateway (from 05-waypoint.sh)
helm upgrade --install agentgateway-crds \
  oci://us-docker.pkg.dev/solo-public/enterprise-agentgateway/charts/enterprise-agentgateway-crds \
  --namespace agentgateway-system --create-namespace --version v2026.5.1 --wait

helm upgrade --install agentgateway \
  oci://us-docker.pkg.dev/solo-public/enterprise-agentgateway/charts/enterprise-agentgateway \
  --namespace agentgateway-system --version v2026.5.1 \
  --set licensing.licenseKey="$AGENTGATEWAY_LICENSE_KEY" --wait
Configuration note: set CLUSTER_ID and NETWORK on the waypoint GatewayClass. Every waypoint the kagent translator later creates takes its identity from CLUSTER_ID and NETWORK, matched to istiod, so it fetches its cert and ztunnel can classify the service. Set them once via an AgentgatewayParameters referenced by the waypoint GatewayClass, and every waypoint inherits them, no per-deployment patching.
yamlwaypoint identity params + GatewayClass wiring (from 05-waypoint.sh)
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayParameters
metadata:
  name: waypoint-params
  namespace: gloo-system
spec:
  env:
    - { name: CLUSTER_ID, value: "agentcore-demo" }
    - { name: NETWORK,    value: "agentcore-demo" }
---
# then point the waypoint GatewayClass at it
kubectl patch gatewayclass enterprise-agentgateway-waypoint --type=merge -p \
  '{"spec":{"parametersRef":{"group":"agentgateway.dev","kind":"AgentgatewayParameters","name":"waypoint-params","namespace":"gloo-system"}}}'

Finally, enroll the kagent namespace in the ambient mesh so its workloads can sit behind a waypoint:

kubectl label ns kagent istio.io/dataplane-mode=ambient --overwrite
kubectl label ns kagent topology.istio.io/network=agentcore-demo --overwrite

Step 7: the ingress Gateway and the *.localtest.me consoles

Run: ./scripts/06-gateway.sh

The last install is the ingress that ties the host port mapping from step 1 to the services. A Gateway on the enterprise-agentgateway GatewayClass with an HTTP :80 listener that accepts routes from all namespaces, plus three HTTPRoutes that map each console hostname to its Service: Keycloak, AgentRegistry on :12121, and the Enterprise UI.

yamlthe ingress Gateway + three HTTPRoutes (from 06-gateway.sh)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: agentgateway-proxy
  namespace: agentgateway-system
spec:
  gatewayClassName: enterprise-agentgateway
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces: { from: All }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: keycloak, namespace: keycloak }
spec:
  parentRefs: [{ name: agentgateway-proxy, namespace: agentgateway-system }]
  hostnames: ["keycloak.localtest.me"]
  rules:
    - matches: [{ path: { type: PathPrefix, value: / } }]
      backendRefs: [{ name: keycloak, port: 80 }]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: agentregistry, namespace: agentregistry-system }
spec:
  parentRefs: [{ name: agentgateway-proxy, namespace: agentgateway-system }]
  hostnames: ["agentregistry.localtest.me"]
  rules:
    - matches: [{ path: { type: PathPrefix, value: / } }]
      backendRefs: [{ name: agentregistry-enterprise-server, port: 12121 }]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: kagent-ui, namespace: solo-enterprise }
spec:
  parentRefs: [{ name: agentgateway-proxy, namespace: agentgateway-system }]
  hostnames: ["kagent.localtest.me"]
  rules:
    - matches: [{ path: { type: PathPrefix, value: / } }]
      backendRefs: [{ name: solo-enterprise-ui, port: 80 }]

The gateway controller provisions a Service for the Gateway. Patch it to type: NodePort with nodePort: 30080 so kind's host :80 mapping reaches it, then wait for the Gateway to report Programmed.

bashpin the gateway Service to NodePort 30080 (from 06-gateway.sh)
GW_SVC=$(kubectl -n agentgateway-system get svc \
  -l gateway.networking.k8s.io/gateway-name=agentgateway-proxy -o name | head -1)
kubectl -n agentgateway-system patch "$GW_SVC" --type=json -p "[
  {\"op\":\"replace\",\"path\":\"/spec/type\",\"value\":\"NodePort\"},
  {\"op\":\"replace\",\"path\":\"/spec/ports/0/nodePort\",\"value\":30080}
]"
kubectl -n agentgateway-system wait --for=condition=Programmed gateway/agentgateway-proxy --timeout=120s

With the gateway Programmed the platform is reachable at three URLs, no port-forwards: http://keycloak.localtest.me, http://agentregistry.localtest.me, and http://kagent.localtest.me (log in as admin-user / password).

Connect arctl

Run: ./scripts/connect.sh

The platform is up, so the registry CLI can log in. arctl user login uses the password-credentials flow against the ar-cli-password client, against the keycloak.localtest.me issuer. This needs the gateway up first, since that is what makes the issuer resolve from the host. The token lands in the OS keychain, so subsequent arctl commands need no per-call token.

arctl user login \
  --oidc-flow password-credentials \
  --oidc-issuer-url http://keycloak.localtest.me/realms/agentregistry \
  --oidc-client-id ar-cli-password \
  --oidc-username admin-user --oidc-password password

The configuration notes that matter

Install order is a dependency chain. Keycloak must be up and its two confidential client secrets scraped before the kagent, UI, and registry charts that consume them. Ambient Istio must exist before the waypoint GatewayClass means anything. The ingress Gateway must be Programmed before arctl can log in, because that is what makes the issuer resolve from the host. Run them out of order and the symptom is rarely the real cause.

Where to take it next

Versions

Built and verified on:

Enterprise
arctlv2026.6.1
Solo Enterprise for kagent0.4.3
AgentRegistry (Solo Enterprise)2026.6.1
Solo Enterprise management0.4.3
Gloo Operator0.5.2
Solo Istio1.29.2-patch0
enterprise agentgatewayv2026.5.1
Gateway APIv1.4.0
Keycloak26.3
kindlatest