GET on /api/v1/namespaces/prod/secrets/db-root is the whole operation, and the Accept header says whether the caller wants a table of names or the object itself. An http lane in front of kube-apiserver reads those three things, path, query and headers, and applies rules, masking and analysis to them.
This page walks the configuration the kubernetes overlay of the compose stack runs. The same file works against your own cluster with the upstream and TLS paths changed.
config.yaml
The free tier allows one guardrail and one mask rule per process. The file above needs a license for its second rule of each kind; drop
no-kubectl-delete and credentials-in-responses to run it unlicensed.Two TLS shapes
The lane holds the credential. kubectl connects to loopback without TLS and without a token. The lane opens TLS to the apiserver withupstream_tls.cert_file and key_file, so every request reaches the cluster as that certificate’s identity. Use this on a workstation, or wherever one Sidecar serves one principal. Nothing in the request names a user, so the audit trail records anonymous unless you set identity_header.
A proxy holds the credential. In the compose stack, Envoy terminates kubectl’s TLS, OPA resolves the bearer token to a user and writes it back as x-hoop-user, and the lane verifies the apiserver with upstream_tls.ca_file alone. Set identity_header: X-Hoop-User so the audit row names the principal. Authorization cannot be allowlisted, so the token never reaches the audit trail, OPA or a model.
Offer no ALPN on the upstream hop. The apiserver then answers in HTTP/1.1, the dialect the codec reads.
Read the request through its headers
kubectl stamps two headers the lane can act on:no-secret-contents scopes an http_header rule to GET on the Secret collections and names, with headers_not, the one safe Accept: the table view. Every other request on a secret is refused: kubectl get secret db-root -o yaml (Accept: application/json), a client adding ;q=1, one asking for application/vnd.kubernetes.protobuf, and a curl that sends no Accept at all. kubectl get secrets lists names. The resources patterns cover both spellings kubectl uses: /api/v1/secrets for -A, and /api/v1/namespaces/<ns>/secrets with or without a name for one namespace. A * segment before a trailing /** stays a wildcard.
Write a rule that protects something with headers_not, naming what is safe. The other map, headers, names what is unsafe and matches when every named header is present with a listed value; no-kubectl-delete uses it, because there the header is the evidence. A request without the header cannot match headers, which is what makes it the wrong form for a guardrail on secrets.
Values in both maps compare case-insensitively. A bare * matches any run of characters, which is what kubectl delete* needs to cover kubectl delete pod x, kubectl delete -f and --all, and what application/json;as=Table;* needs to cover the version and group parameters that follow. Write \* for a literal star. Several header names in one rule must all be satisfied; several values under one name are alternatives; an empty list means present (headers) or absent (headers_not). Only a request matches: a response carries the server’s headers.
The rule reads only headers the lane captures. Name one outside http.headers, in either map, and startup refuses the config with the header to add.
curl -X DELETE against the lane carries no Kubectl-Command and passes no-kubectl-delete. The rule matches kubectl’s statement of intent; use http_resource with methods: [DELETE] to match the verb itself.
Mask by JSON key
The codec walks a JSON response value by value and hands each string and number to the masker under its dotted key path:data.ca.crt, items.data.password, metadata.name. A columns rule names a key the way it names a result-set column. columns: [data] masks every value under a data object, in a ConfigMap, a Secret, one object or a list of them, whatever the value holds. data.password masks that one key; password masks that key at any depth. Where two rules match, the longer path wins, then the one nearest the leaf.
Transfer-Encoding: chunked. The codec re-chunks what it rewrote, and a chunked stream goes out as it arrives: each complete top-level JSON value (one watch event) or each chunk of text (one log line) is masked and forwarded when it is whole. A kubectl get -w or kubectl logs -f is not held to its end.
Content-Encoding: gzip, on bodies over 128 KiB when kubectl offers it) are inflated around the masker and compressed again. A response body above MaxMessageBytes (16 MiB) goes out unmasked and the next response is masked again.
credentials-in-responses runs beside the key rule. It redacts by class wherever the detector finds a match: an annotation, a pod’s environment, a log line. The two rules do not collide; one claims a key path, the other claims entities.
Judge a request that has no body
Ananalyzer block on the lane classifies bodiless requests. The model receives the request line, the normalized resource where it differs, and the allowlisted headers in name order:
config.yaml
Kubectl-Session out of http.headers: kubectl mints a fresh UUID per invocation, and allowlisting it makes every request a model call.
Guardrails run first and cost nothing. kubectl get secret db-root -o yaml is refused by no-secret-contents before the analyzer sees it; what reaches the model is what the guardrails let through.
kubectl exec over WebSocket
kubectl 1.30 and later opensexec, attach and port-forward as a WebSocket: GET /api/v1/namespaces/prod/pods/web-0/exec?command=sh&... with Upgrade: websocket, answered 101 Switching Protocols with subprotocol v5.channel.k8s.io.
The lane records the GET, the 101 and then one ws_message row per frame, each keyed on the exec path, and a ws_close row when the session ends:
Upgrade, Sec-WebSocket-Protocol and X-Stream-Protocol-Version so the handshake rows show the subprotocol selected.
Behind Envoy, the listener needs upgrade_configs: [{upgrade_type: websocket}] or Envoy answers the upgrade with a plain 200 and kubectl reports “unable to upgrade connection”. Remove the route timeout on that listener; an exec or watch stays open as long as the user does.
What the audit trail holds
Every request row carries the allowlisted headers. The refused GET above records as:masked event naming the entity and the count, K8S_DATA and 3 for the ConfigMap above. No masked value appears in the trail.
Next
HTTP
The normalized resource, the
http block, identity and denials.Guardrail Rules
http_header beside http_resource and http_status.Risk Analysis
Triggers, actions, the cache and what a lane’s analyzer costs.
Data Masking
Entity rules, column rules and the strategies.