The journey of a request¶
Follow a kubectl request from when it leaves your machine until the response comes back. This shows you what kubelatch checks, what the cluster sees, and what ends up in the audit log.
At a glance¶
sequenceDiagram
participant K as kubectl
participant P as kubelatch proxy
participant DB as Postgres
participant A as API server
K->>P: GET /clusters/prod/api/v1/namespaces/apps/pods<br/>Authorization: Bearer klt_…
P->>P: Route and Impersonate-* headers
P->>DB: Looks up the token hash
P->>DB: Subject, cluster and active permissions
P->>DB: Inserts the audit row (start)
P->>A: Same request with Impersonate-User,<br/>Impersonate-Group, Impersonate-Uid, Audit-ID
A-->>P: Response (cluster's native RBAC)
P-->>K: Response + Audit-ID
P->>DB: Completes the row (status, duration)
1. The request reaches the proxy¶
Your kubeconfig points to https://<kubelatch>/clusters/<id> and carries a klt_… token as Bearer. Everything under /clusters/ goes to the proxy before any other route.
Every proxy response carries an Audit-ID header, errors included. It's what you look up in the logs or in the audit log to locate a specific request.
2. First filter: route and headers¶
Before looking at the token, the proxy rejects with 400:
- Routes with empty segments,
.or.., escaped characters or a%. - A cluster identifier that can't exist.
- Any
Impersonate-*header coming from the client. This is whykubectl --asdoesn't work through kubelatch: the proxy already uses impersonation, and letting a nested impersonation through would let someone ask to be another person.
These rejections leave no audit row: the request never gets far enough to identify anyone.
3. Who are you? The token¶
The proxy only accepts Authorization: Bearer klt_… matching the exact format of a kubelatch token. It ignores cookies. Then:
- It computes the token's SHA-256 and looks up the credential by that hash. There's no cache: every request queries Postgres, so a revocation takes effect on the very next request.
- It checks the credential isn't revoked or expired. If either check fails, or the token doesn't exist, it responds
401. - It loads the subject that owns the credential. If the account is disabled, it responds
403. - If the credential is restricted to a cluster and this isn't it, it responds
403.
4. What can you do here? Cluster and permissions¶
The proxy loads the cluster. If it doesn't exist, it responds 404; if it doesn't have tokens configured yet, 503.
Then it looks up the subject's active permissions on that cluster: not revoked and not expired at this instant. If there are none, it responds 403 with a clear message. Each permission translates into a group:
| Permission scope | Group being impersonated |
|---|---|
| A namespace | kubelatch:ns:<namespace>:<tier> |
The whole cluster (*) |
kubelatch:cluster:<tier> |
An expiry doesn't need anyone to do anything: as soon as a permission expires, the proxy stops sending its group.
5. The audit row, before forwarding¶
With the identity and groups resolved, the proxy classifies the request the way the API server would: verb, API group, resource, subresource, namespace and name. It inserts the audit row before forwarding. If it can't write it, it responds 503 and forwards nothing: no request goes through without its record.
It also updates the credential's Último uso (Last used), at most once per minute.
6. What the API server receives¶
The proxy rewrites the request before forwarding it:
| Header | What the proxy does |
|---|---|
Authorization |
Replaces it with that cluster's kubelatch-proxy ServiceAccount token. Your token never reaches the cluster. |
Impersonate-User |
user:<login> for people, bot:<name> for bots. |
Impersonate-Uid |
The subject's internal identifier. |
Impersonate-Group |
One per group from step 4. |
Audit-ID |
A UUIDv7 generated by kubelatch. It never honors the client's own. |
X-Forwarded-For |
The client's real IP (see trusted proxies). |
Cookie, X-Real-IP, Forwarded, X-Forwarded-* |
Removed. |
The API server authenticates the proxy's ServiceAccount, checks that it can impersonate that user and those groups, and applies its native RBAC to the impersonated identity. kubelatch doesn't add system:authenticated: the API server itself does that.
The API server uses the Audit-ID header as the auditID of its own record. This way a kubelatch row and the cluster's native audit event correlate through the same identifier. In the native audit log, user is the proxy's ServiceAccount and impersonatedUser is user:<login>.
7. The response comes back¶
The proxy returns the API server's response as-is, with its own Audit-ID. If it can't reach the API server, it responds 502 with a summary of the problem (timeout, certificate mismatch, unreachable API server), without leaking internal addresses.
When it finishes, it completes the audit row with the status code, the duration, the end time and the error if there was one. A row can only be completed once.
Long-lived streams: exec, port-forward, watch and logs¶
kubectl exec, attach, port-forward and cp open a connection that upgrades to WebSocket or SPDY. The proxy sends these over a dedicated HTTP/1.1 transport, because the API server doesn't perform that upgrade over HTTP/2. All other requests go over HTTP/2. watch and logs -f are forwarded unbuffered, byte by byte.
A stream's audit row is opened when it starts (101 for upgraded connections) and completed when it closes, with its real duration.
Revoking cuts what's already open¶
Revoking a credential rejects the next request with 401. But an open exec or watch doesn't make new requests. That's why each replica keeps a record of its in-flight requests and revalidates each one every 10 s:
- Is the credential still unrevoked and unexpired?
- Is the account still enabled?
- Are the groups still exactly the same? Adding or removing a permission on that cluster also cuts the stream, and the client reconnects with the new groups.
Whatever's no longer valid is cut within 10 s at most. If the cut happens before the API server responds, the client gets a 403; if the stream was already open, the connection is closed. The audit row ends up with the error stream cortado: la credencial ya no es válida (stream cut: the credential is no longer valid).
If Postgres fails during that revalidation, open streams stay open. New requests do fail closed.
What gets logged when something fails¶
| Rejection | Leaves an audit row? |
|---|---|
400 (route, invalid cluster, Impersonate-*) |
No |
401 (missing, unknown, revoked or expired token) |
Yes, with no subject, with the token's visible prefix |
403 (disabled account, another cluster, no permissions), 404 (unknown cluster), 503 (cluster without tokens) |
Yes, with credential and subject |
503 because kubelatch can't reach Postgres |
No: there's nowhere to write it |
Rejection rows are limited to one per IP every 10 s, so no one can fill the table by probing tokens. The response to the client doesn't change because of that limit. The full list of codes is in Errors.