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.
-
Go to Clusters and click Register a cluster: the form opens in a panel on the right. Type
kindin Identifier (slug, goes in the proxy URL) andkind localin Name, and click Register. You land on the page of the clusterkind, with the status Not enrolled and its Enroll card open at three steps: Where, Command and Waiting. -
Leave kubectl context (optional) empty. Open Advanced options and type
https://kubelatch-tutorial-control-plane:6443in API server URL for kubelatch. Your kubeconfig reaches the cluster at an address on127.0.0.1, which inside the kubelatch container is the container itself; on thekindnetwork, kubelatch reaches the API server by the name of kind's node container instead. -
Click Generate command. The Command step shows the Enrollment command: a
curl … | shthat 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_BUNDLEmakescurltrust the self-signed certificate from step 4: the script downloads with onecurland sends the tokens back with another, so a-ktyped on the command line would cover the first only. With a real certificate, leave it out.The command uses kubectl's current context. The
exportinside the parentheses sets it to the kind cluster only for that subshell, without touching your session or~/.kube/config. -
The script prints the context it will enroll and asks
Continue? [y/N]. Check that it nameskind-kubelatch-tutorialand answery. It applies the manifest (thekubelatch-systemnamespace 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.
- Go to Permissions and click Grant permissions: the form opens in a panel on the right.
- Pick
adminin Accounts,kindin Cluster, and tickdefault (PSA baseline)under Scopes. Under What, leaveviewerselected; under Until when, accept the default30 days. Click Grant 1 permission. - Click Grant permissions again and repeat with
debuggerunder 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¶
- Go to Home and scroll down to My clusters: the
kindcluster 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:viewerandkubelatch:ns:default:debugger. - Sign in with the CLI from step 2.
KUBELATCH_CONFIG_DIRkeeps its session inside the tutorial's directory and-owrites the kubeconfig there too, so nothing of yours under~/.configor~/.kubechanges:
export KUBELATCH_CONFIG_DIR="$PWD/.kubelatch"
KL_KC="$PWD/tutorial.kubeconfig"
kubelatch login https://localhost:8443 --ca-file kubelatch-ca.crt -o "$KL_KC"
- 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:
createofselfsubjectreviews(thewhoami),listofpods (default)with200,execofpods/exec web (default)with101, andlistofsecrets (default)with403. - 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¶
- On Home, under My credentials, click Revoke on the CLI session on
row. - A dialog asks for a reason (optional). Type
end of tutorialand 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:
- In Permissions, click Grant permissions and grant
adminthe roledeveloperonkind,default (PSA baseline), for30 days. -
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 -
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, choosekubelatch-tutorialand sign in. - 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
defaultare listed, underkind. Leavedebuggeron Leave out, as it comes, setviewerto Leave out (developerincludes it) anddeveloperto Same role (developer): the agent gets whatdeveloperallows, and, with Writes need my approval on as it comes, each of its writes waits for you. Keep the duration and click Authorize. -
Back in Claude Code, the agent calls
whoamiandlist_resources.whoamianswers (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
tickerandweb. -
Ask it:
Delete the pod ticker in default.It callsdelete_resource. kubelatch checks with the cluster that the deletion would go through, then holds it instead of running it. Claude Code showsApprove 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 getsagent.approval_pendingwith 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 withagent.approval_denied: a person denied this write: do not try it another way.tickeris 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. -
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 underdelete_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.
-
With OIDC login on, only break-glass accounts sign in with a password. Make your
adminone first, so it can still get in:docker compose exec kubelatch /kubelatch-server user set-break-glass adminIt answers
break_glass=true for admin. -
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 --waitThe 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.1rather thanlocalhoston purpose: kubelatch, onhttps://localhost:8443, tells the browser to use only https onlocalhost, and this Keycloak speaks plain http. -
In the interface, go to Users, click Create an account, type
alicein Login andalice@acme.testin 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. - 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, withalice-password. You land in Home asAlice 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, says1 person signed in with Keycloak for the first time this week and was linked by e-mail. - 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 answersThat Keycloak account isn't in a domain this kubelatch accepts. Did you pick a personal account?. Withbob, it answersKeycloak 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.