Skip to content

Try it locally in 15 minutes

Does your company already have kubelatch?

You don't need this tutorial: go to Sign in and request access.

In this tutorial you run kubelatch on your machine with Docker Compose, next to a disposable kind cluster. You'll grant yourself a permission, use kubectl through kubelatch, see every request in the audit log, and revoke the credential. All with local passwords, no GitHub, and nothing to build: you use the published image and CLI.

It will take about 15 minutes, plus the first-time downloads (the kubelatch and Postgres images and the kind node image).

Public preview

kubelatch is in public preview. It works, but pieces are still missing (they're on the roadmap), and there's no stability commitment between versions yet; only the latest release is supported.

Before you start

Tool Version What for
Docker with Compose Docker 24 or later, Compose v2 kubelatch, Postgres and the kind cluster
kind 0.24 or later The test cluster. kind 0.20 creates Kubernetes 1.27, too old
kubectl 1.31 or later Talk to the cluster
curl and tar Any Download the Compose file and the CLI

Check the versions with docker compose version, kind version and kubectl version --client. The kubelatch CLI is built for Linux and macOS, on amd64 and arm64; on Windows, follow the tutorial inside WSL 2.

Work in a single terminal and in the same shell session from start to end: the steps reuse variables.

Your ~/.kube/config is not touched

If your ~/.kube/config points at real clusters, a plain kubectl copied from here would act on them. That's why every kubectl in this tutorial carries --kubeconfig with a file from the tutorial's own directory. Don't run kubectl without it, and don't run kubectl config use-context while following it.

1. Download the Compose file

mkdir kubelatch-tutorial && cd kubelatch-tutorial
curl -fsSLO https://raw.githubusercontent.com/picaportelabs/kubelatch/main/docker-compose.yaml

docker-compose.yaml starts Postgres, a one-off job that writes a self-signed certificate for localhost, kubelatch on https://localhost:8443, and a tiny container that holds kubelatch's network. Keycloak, for step 15, starts only when you ask for it. It carries test configuration: an all-zero encryption key, the database password kubelatch and Keycloak's test passwords. Never use it for a real installation; that is what the Helm chart is for.

2. Download the kubelatch CLI

export KUBELATCH_VERSION=$(curl -fsSL https://docs.kubelatch.com/releases/latest.json | sed 's/.*"version":"\([^"]*\)".*/\1/')
echo "KUBELATCH_VERSION=$KUBELATCH_VERSION" > .env
OS=$(uname -s | tr '[:upper:]' '[:lower:]'); ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
REL="https://github.com/picaportelabs/kubelatch/releases/download/v$KUBELATCH_VERSION"
curl -fsSLO "$REL/kubelatch_${KUBELATCH_VERSION}_${OS}_${ARCH}.tar.gz"
curl -fsSLO "$REL/kubelatch_${KUBELATCH_VERSION}_checksums.txt"
sha256sum --ignore-missing -c "kubelatch_${KUBELATCH_VERSION}_checksums.txt"
mkdir -p bin && tar -xzf "kubelatch_${KUBELATCH_VERSION}_${OS}_${ARCH}.tar.gz" -C bin kubelatch
export PATH="$PWD/bin:$PATH"
kubelatch version

KUBELATCH_VERSION is the latest release, and sha256sum checks the download against the release's checksums (… OK). On macOS, check it with shasum -a 256 --ignore-missing -c instead of sha256sum. The binary stays in bin/ inside the tutorial's directory, and PATH finds it only in this shell. The .env file keeps that version for Docker Compose, which reads it in step 4 and every time you start kubelatch again from this directory, so the server and the CLI are the same release.

3. Create the kind cluster

KIND_KC="$PWD/kind.kubeconfig"
kind create cluster --name kubelatch-tutorial --kubeconfig "$KIND_KC"
kubectl --kubeconfig "$KIND_KC" label namespace default pod-security.kubernetes.io/enforce=baseline
kubectl --kubeconfig "$KIND_KC" run web --image=busybox:1.36 -- sh -c 'mkdir -p /www && echo ok > /www/index.html && exec httpd -f -p 80 -h /www'
kubectl --kubeconfig "$KIND_KC" run ticker --image=busybox:1.36 -- sh -c 'while true; do date; sleep 1; done'
kubectl --kubeconfig "$KIND_KC" wait --for=condition=Ready pod/web pod/ticker --timeout=3m

kind creates the kubelatch-tutorial cluster in about half a minute and writes its admin kubeconfig to kind.kubeconfig, never to ~/.kube/config. The two test pods, web and ticker, end up condition met.

The label puts Pod Security Admission baseline on the default namespace. You need it because in step 8 you'll grant yourself the debugger tier, which kubelatch (like developer and admin) only grants in namespaces that enforce PSA baseline or restricted. System roles explains why.

The cluster comes before kubelatch because kind also creates the kind Docker network, and kubelatch joins it in the next step to reach the cluster.

4. Start kubelatch

docker compose up -d
docker compose cp kubelatch:/tls/server.crt kubelatch-ca.crt
curl --cacert kubelatch-ca.crt https://localhost:8443/readyz    # ok

up -d starts Postgres, writes the certificate and leaves kubelatch running in the background, listening on https://localhost:8443. HTTPS is required because kubectl only sends tokens over HTTPS. Postgres publishes no port: only kubelatch talks to it.

docker compose cp copies the certificate out to kubelatch-ca.crt: curl, the CLI and Claude Code trust kubelatch through it. kubelatch also embeds it as the CA of the kubeconfigs it issues. If curl can't connect yet, wait a few seconds and try again: kubelatch applies its database migrations when it starts.

5. Create the first administrator

The server's admin CLI (kubelatch-server user …) runs inside the kubelatch container, with the same configuration as the server:

docker compose exec kubelatch /kubelatch-server user create admin --admin --display-name "Admin"

It asks for the password twice, without echo (Password: and Repeat password:). It must be at least 12 characters. It ends with user admin created (id …, admin=true, break_glass=false).

6. Sign in to the interface

Open https://localhost:8443 in the browser. It will warn you that the certificate isn't trusted: it's the self-signed one from step 4, accept it for this test.

The interface follows your browser's language (English or Spanish); if it isn't in English, pick English in the selector in the top right corner of the page (Interface language).

Type admin in Login, your password in Password and click Sign in. You'll see Home and, on the left, the sidebar: Home and Agents first, then Permissions, Users, Groups, Roles and Credentials under Access, Audit under Record, and Clusters and CI under Platform. Beside the name at the top is the edition (Free): it leads to Edition. At the bottom of the sidebar is your own name: click it to open the menu with My account, Edition, Appearance, Language and Sign out.

Use localhost as is, not 127.0.0.1: kubelatch only accepts interface requests from its own URL.

7. Register the cluster in kubelatch

kubelatch needs two ServiceAccounts in each cluster: one to forward your requests and another to maintain RBAC. Registering the cluster creates them and passes their tokens to kubelatch.

  1. Go to Clusters and click Register a cluster: the form opens in a panel on the right. Type kind in Identifier (slug, goes in the proxy URL) and kind local in Name, and click Register. You land on the page of the cluster kind, with the status Not enrolled and its Enroll card open at three steps: Where, Command and Waiting.

  2. Leave kubectl context (optional) empty. Open Advanced options and type https://kubelatch-tutorial-control-plane:6443 in API server URL for kubelatch. Your kubeconfig reaches the cluster at an address on 127.0.0.1, which inside the kubelatch container is the container itself; on the kind network, kubelatch reaches the API server by the name of kind's node container instead.

  3. Click Generate command. The Command step shows the Enrollment command: a curl … | sh that works once and for an hour. Click Copy and run it like this, pasting it in place of <enrollment-command>:

    ( export KUBECONFIG="$KIND_KC" CURL_CA_BUNDLE="$PWD/kubelatch-ca.crt"; <enrollment-command> )
    

    CURL_CA_BUNDLE makes curl trust the self-signed certificate from step 4: the script downloads with one curl and sends the tokens back with another, so a -k typed on the command line would cover the first only. With a real certificate, leave it out.

    The command uses kubectl's current context. The export inside the parentheses sets it to the kind cluster only for that subshell, without touching your session or ~/.kube/config.

  4. The script prints the context it will enroll and asks Continue? [y/N]. Check that it names kind-kubelatch-tutorial and answer y. It applies the manifest (the kubelatch-system namespace and the two ServiceAccounts, with minimal permissions), reads their tokens and sends them to kubelatch.

kubelatch validates the tokens against the cluster, stores them encrypted and runs the first reconciliation. The card says Enrolled. First reconciliation under way. and the cluster moves to Ready. The header says first reconciliation under way until that first pass finishes: reload the page and it gives the time (reconciled …). Details shows the Kubernetes version in Kubernetes version, and the Namespaces card lists default with PSA baseline.

The card stays on the page after enrolling. If you left before running the command, open kind in Clusters again: the card shows the pending link, without its command (only the tab that generated it shows it), and Generate another command gives you a new one. If your terminal can't download the command, Do it by hand, at the foot of the card, has the manual steps.

8. Grant yourself a permission

Being a kubelatch administrator doesn't grant cluster access: you also need a permission.

  1. Go to Permissions and click Grant permissions: the form opens in a panel on the right.
  2. Pick admin in Accounts, kind in Cluster, and tick default (PSA baseline) under Scopes. Under What, leave viewer selected; under Until when, accept the default 30 days. Click Grant 1 permission.
  3. Click Grant permissions again and repeat with debugger under What.

The map shows your row with a cell on kind: debugger in default, with "+1" for viewer; List has both under Active. viewer lets you read almost everything in default except secrets; debugger adds exec, attach and port-forward. Within a second or two, kubelatch creates the corresponding RoleBindings in the cluster.

9. Sign in with the kubelatch CLI

  1. Go to Home and scroll down to My clusters: the kind cluster is there, open, with your two permissions, one line each. Click a role to see what it allows and the Kubernetes group the cluster will recognize you by: kubelatch:ns:default:viewer and kubelatch:ns:default:debugger.
  2. Sign in with the CLI from step 2. KUBELATCH_CONFIG_DIR keeps its session inside the tutorial's directory and -o writes the kubeconfig there too, so nothing of yours under ~/.config or ~/.kube changes:
export KUBELATCH_CONFIG_DIR="$PWD/.kubelatch"
KL_KC="$PWD/tutorial.kubeconfig"
kubelatch login https://localhost:8443 --ca-file kubelatch-ca.crt -o "$KL_KC"
  1. The browser opens the question Approve the sign-in from ?. Under Who asks it lists the computer (Computer (as the CLI names it)), From (the IP, and whether it is on the same network as this browser) and Asked; under What it gets · until when, A credential for all your clusters and how long it lasts. Click Approve. The tab says you can close it, and the terminal finishes:
Approve this login in your browser:
  https://localhost:8443/cli/approve/…?state=…
Signed in to https://localhost:8443 as admin; the session expires ….
Wrote 1 context(s) to …/kubelatch-tutorial/tutorial.kubeconfig. Use it with:
…

The kubeconfig has one context, kubelatch-kind, and no token: kubectl asks kubelatch exec-credential for it on every run, which is why kubelatch has to be in the PATH of the terminal where you use kubectl. The CLI trusts the self-signed certificate you gave it with --ca-file and puts it in the kubeconfig. The CLI explains all this. If you'd rather not use it, Issue a credential on Home issues a kubeconfig with the token inside, as Get a credential shows.

10. Use kubectl through kubelatch

kubectl --kubeconfig "$KL_KC" auth whoami
kubectl --kubeconfig "$KL_KC" get pods
kubectl --kubeconfig "$KL_KC" exec web -- sh -c 'echo hello'
ATTRIBUTE   VALUE
Username    user:admin
UID         35b57553-…
Groups      [kubelatch:ns:default:debugger kubelatch:ns:default:viewer system:authenticated]
NAME     READY   STATUS    RESTARTS   AGE
ticker   1/1     Running   0          3m
web      1/1     Running   0          3m
hello

Every request reaches kubelatch with the token the CLI hands kubectl. kubelatch checks that the credential is valid, logs it and forwards it to the cluster with impersonation: it acts as user:admin with the groups from your permissions. The cluster decides with its own RBAC. The journey of a request walks through it step by step.

11. Try something you can't do

kubectl --kubeconfig "$KL_KC" get secrets
Error from server (Forbidden): secrets is forbidden: User "user:admin" cannot list resource "secrets" in API group "" in the namespace "default"

viewer doesn't include secrets, and neither does debugger. It's the cluster itself that answers 403: kubelatch only tells it who you are.

12. Look at the audit log

  • On Home, Recent activity lists your last requests, day by day. See all my activity opens My activity, which lists every request made with your credentials: create of selfsubjectreviews (the whoami), list of pods (default) with 200, exec of pods/exec web (default) with 101, and list of secrets (default) with 403.
  • In My activity, click a row's date to open the Request detail in a panel on the right: the Audit-ID (the same one the cluster gets), the Impersonated groups, the Source IP and the User-Agent.
  • On Audit, the Requests to clusters tab shows the same for every account, with filters. The Control plane tab shows what you did in the interface, one sentence per event (Cluster kind registered, Tokens of cluster kind saved … by its enrollment command, CLI sign-in approved on ……); opening one shows its action code: user.create, cluster.create, cluster.enrollment.create, cluster.tokens, grant.create, cli.approve, cli.login…

13. Revoke the credential

  1. On Home, under My credentials, click Revoke on the CLI session on row.
  2. A dialog asks for a reason (optional). Type end of tutorial and click Revoke. A notice says the credential is revoked and the row leaves the list, which shows the active credentials. Check Show expired and revoked and it comes back with Revoked on … and your reason under it.

Try again:

kubectl --kubeconfig "$KL_KC" get pods
error: You must be logged in to the server (credential revoked)

That's a 401: the credential stopped being valid instantly. An exec, logs -f or port-forward that was open with it would have been cut off in under 10 seconds. kubelatch logout does the same from the terminal.

The attempt also shows up in Audit. Open More filters, check Include discovery and click Apply: a row with no account appears, with the token's prefix (klt_…), the status 401 and, under it, credential revoked.

14. Connect an AI agent (optional, 5 minutes)

If you use Claude Code, let it work on the cluster as you, through kubelatch. First give yourself a role that can change things, to see that the agent can't use it without you:

  1. In Permissions, click Grant permissions and grant admin the role developer on kind, default (PSA baseline), for 30 days.
  2. In the terminal, add kubelatch to Claude Code for this directory, and start it trusting the tutorial's certificate:

    claude mcp add --transport http kubelatch-tutorial https://localhost:8443/mcp
    NODE_EXTRA_CA_CERTS="$PWD/kubelatch-ca.crt" claude
    
  3. Ask it: Using kubelatch, who am I and which pods run in default on kind? Claude Code says kubelatch needs you to sign in; run /mcp, choose kubelatch-tutorial and sign in.

  4. The browser opens the question Let Claude Code act as you?, with This agent runs on your computer. Authorize it only if you just connected it yourself. above the buttons. Under What it gets · until when your three permissions on default are listed, under kind. Leave debugger on Leave out, as it comes, set viewer to Leave out (developer includes it) and developer to Same role (developer): the agent gets what developer allows, and, with Writes need my approval on as it comes, each of its writes waits for you. Keep the duration and click Authorize.
  5. Back in Claude Code, the agent calls whoami and list_resources. whoami answers (the ids and dates vary):

    identity: user:admin
    session:
      id: …
      mode: delegated
      expires_at: …
    on_behalf_of: user:admin
    credential_expires_at: …
    writes_need_approval: true
    permissions:
      - cluster: kind
        role: developer
        scope: default
        includes: Read plus deploying workloads in the namespace (no Secrets or exec; no writes to ServiceAccounts, roles or quotas)
        expires_at: …
    scope "*" is the whole cluster; any other scope is one namespace.
    

    and it lists ticker and web.

  6. Ask it: Delete the pod ticker in default. It calls delete_resource. kubelatch checks with the cluster that the deletion would go through, then holds it instead of running it. Claude Code shows Approve in kubelatch: delete_resource pods/ticker in default (kind) and asks to open the approval page in your browser: accept. If you decline, or your Claude Code is older than 2.1.274, the agent gets agent.approval_pending with the link instead: open it yourself, or go to Agents, the Waiting for you card.

    The page's title says what is asked in one sentence (… wants to delete pods/ticker in default (kind)) with the status Pending, and Changes says This object is deleted. with every line of the pod marked −. Click Deny and confirm with Deny in the dialog. Back in Claude Code, the call that was waiting for your decision ends with agent.approval_denied: a person denied this write: do not try it another way. ticker is still running: nothing changes until you say so. Had you clicked Approve, that call would have run the deletion once, exactly as you saw it.

  7. In the interface, open Agents: Agents at work lists your Claude Code, delegated, with its last call. Open it; its page shows the agent on behalf of admin, mode Delegated: Ceiling lists what you gave it, and Timeline every call, the held deletion included (its read of the pod and its dry run, both under delete_resource). Click Cut and confirm. The agent's next call fails, and Claude Code asks you to sign in again.

Leave Claude Code with /exit. AI agents tells the rest: approvals, temporary access and the Claude Code plugin.

15. Sign in with your company's SSO (optional, 5 minutes)

kubelatch can sign people in with the OpenID Connect provider they already use (Google Workspace, Microsoft Entra ID, Okta, Keycloak…), and links each one to their account by e-mail. The Compose file brings a Keycloak to try it with, behind the sso profile. It knows three people: alice@acme.test; bob@acme.test, whose address Keycloak hasn't verified; and carol@other.test, outside the domain you'll allow. Each one's password is their name followed by -password.

  1. With OIDC login on, only break-glass accounts sign in with a password. Make your admin one first, so it can still get in:

    docker compose exec kubelatch /kubelatch-server user set-break-glass admin
    

    It answers break_glass=true for admin.

  2. Point kubelatch at Keycloak and start it:

    cat >> .env <<'EOF'
    OIDC_ISSUER=http://127.0.0.1:8081/realms/kubelatch
    OIDC_CLIENT_ID=kubelatch
    OIDC_CLIENT_SECRET=kubelatch-tutorial-secret
    OIDC_ALLOWED_DOMAINS=acme.test
    EOF
    docker compose --profile sso up -d --wait
    

    The first time, it downloads the Keycloak image. kubelatch restarts with the variables once Keycloak is ready, in under a minute. Keycloak is at 127.0.0.1 rather than localhost on purpose: kubelatch, on https://localhost:8443, tells the browser to use only https on localhost, and this Keycloak speaks plain http.

  3. In the interface, go to Users, click Create an account, type alice in Login and alice@acme.test in Email, and click Create and invite. Close the dialog with the link twice: alice won't need it. kubelatch never creates people: the account has to exist, with the address the provider has for that person.

  4. Open https://localhost:8443 in a private window: the login screen now offers Sign in with Keycloak. Click it and sign in at Keycloak as alice, with alice-password. You land in Home as Alice Acme, the name Keycloak has for her: her first sign-in linked the account by its e-mail. Back in your window, Audit → Control plane shows "alice" was linked to the OIDC identity alice@acme.test by e-mail, 1 pending link revoked (the invitation of the previous step) and "alice" signed in. After a reload, Notices, the bell in the top bar, says 1 person signed in with Keycloak for the first time this week and was linked by e-mail.
  5. Close every private window, which also ends alice's session at Keycloak (Sign out in kubelatch ends only kubelatch's), open a new one and try carol: kubelatch answers That Keycloak account isn't in a domain this kubelatch accepts. Did you pick a personal account?. With bob, it answers Keycloak doesn't vouch for your e-mail address, so kubelatch can't match it to an account.. Each attempt leaves a failed sign-in in Control plane, with its reason.

kubelatch login works the same way for alice: the browser goes through Keycloak on its way to the approval. Keycloak's console is at http://127.0.0.1:8081, as admin with admin. OIDC login (SSO) explains how to set it up with your own provider.

16. Clean up

claude mcp remove kubelatch-tutorial   # if you did step 14
docker compose --profile sso down -v   # stops kubelatch, Postgres and Keycloak and deletes their data
kind delete cluster --name kubelatch-tutorial --kubeconfig "$KIND_KC"
cd .. && rm -r kubelatch-tutorial

To come back later instead, stop it with docker compose --profile sso down, without -v: the data stays in Docker volumes, and docker compose up -d brings kubelatch back with your admin (docker compose --profile sso up -d --wait if you did step 15), so skip step 5.

If something goes wrong

Symptom What to do
docker compose up answers network kind declared as external, but could not be found The kind cluster doesn't exist yet. Do step 3 first, then run docker compose up -d again.
docker compose up answers required variable KUBELATCH_VERSION is missing a value There's no .env where you ran it. Go into kubelatch-tutorial, or run the first two lines of step 2 there.
docker compose up fails because port 8443 is taken In docker-compose.yaml, change the host port in 127.0.0.1:8443:8443 and in KUBELATCH_BASE_URL, and use that port in every URL of the tutorial.
docker compose --profile sso up fails because port 8081 is taken Stop whatever uses it. The port is also in OIDC_ISSUER and in Keycloak's settings in docker-compose.yaml, so changing it means changing every one of them.
Sign in with Keycloak answers kubelatch can't reach Keycloak right now… Keycloak isn't running: you started the services without --profile sso. Run docker compose --profile sso up -d --wait.
curl … /readyz fails to connect after a minute kubelatch didn't start. docker compose ps shows its state and docker compose logs kubelatch why.
The interface answers cross-origin request rejected You opened https://127.0.0.1:8443. Use https://localhost:8443.
user create answers that login already exists The admin is from a previous run. Sign in with it, or set another password with docker compose exec kubelatch /kubelatch-server user set-password admin.
Grant answers role debugger requires namespace default to have the label … The PSA label from step 3 is missing. Apply it and grant again.
kind creates Kubernetes 1.27 and the enrollment command ends with no matches for kind "ValidatingAdmissionPolicy" in version "admissionregistration.k8s.io/v1" Your kind is old. Install kind 0.24 or later, delete the cluster with kind delete cluster --name kubelatch-tutorial --kubeconfig "$KIND_KC" and go back to step 3.
The enrollment command ends with kubelatch refused the tokens and if kubelatch cannot reach https://127.0.0.1:… API server URL for kubelatch was left empty. Run the same command again (the link stays valid), with -s -- --server https://kubelatch-tutorial-control-plane:6443 added after its final sh.
The enrollment command prints kubelatch: enrolling cluster "kind" with kubectl context "…" with a context other than kind-kubelatch-tutorial KIND_KC is empty in that terminal, so kubectl reads ~/.kube/config. Press Ctrl-C, set KIND_KC="$PWD/kind.kubeconfig" and run it again.
kubectl answers x509: certificate signed by unknown authority The kubelatch login from step 9 lacked --ca-file kubelatch-ca.crt; run it again with it.
kubectl answers certificate has expired The certificate lasts 30 days. Run docker compose down, docker volume rm kubelatch-tutorial_tls and docker compose up -d, copy the new certificate as in step 4 and repeat the kubelatch login from step 9.

For everyday errors with a credential, see If something fails. When you're done, Next steps tells you what to read depending on what you're going to do.