Category: Kubernetes · Cloud Security · Lateral Movement
Level: Advanced
Kubernetes RBAC is one of those controls that organizations implement and then consider done. Roles are created, service accounts are bound, and the cluster moves on. What rarely happens is a periodic review of what each service account can actually do — and more importantly, what combinations of permissions create unintended paths.
A compromised pod in a low-criticality namespace. No root, no shell, no tools. Just /bin/sh and the standard service account token mounted at:
/var/run/secrets/kubernetes.io/serviceaccount/token
With that token and the cluster API endpoint (available via environment variables), it is possible to query the Kubernetes API directly without kubectl:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
https://${KUBERNETES_SERVICE_HOST}/api/v1/namespaces/
The first step after confirming API access is understanding what the current service account can do. The SelfSubjectRulesReview API returns exactly that:
curl -s ... -X POST \
-H "Content-Type: application/json" \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"app-dev"}}' \
https://.../apis/authorization.k8s.io/v1/selfsubjectrulesreviews
In the exercise, the current SA could:
The dangerous part was not the create Jobs permission by itself. It was the combination:
serviceAccountName — including other service accounts in the namespacebatch-runner) had permission to list and read Secretsbatch-runner also had access to a ConfigMap in the production namespaceBy creating a Job that ran under batch-runner's identity, the batch-runner token was accessible in that Job's pod — and with it, all permissions that batch-runner had.
{
"spec": {
"template": {
"spec": {
"serviceAccountName": "batch-runner",
"containers": [{"name": "r", "image": "busybox",
"command": ["sh", "-c", "cat /var/run/secrets/kubernetes.io/serviceaccount/token"]}]
}
}
}
}
From there: list secrets → get registry credentials → pull private image → find embedded kubeconfig in image layer → access production namespace.
| Permission | Seemed harmless | Was actually |
|---|---|---|
| create Jobs | Just batch processing | Allows impersonating other service accounts |
| batch-runner reads secrets | Needed for app config | Exposed registry credentials |
| Registry access | Standard image pull | Exposed image history with embedded kubeconfig |
| kubeconfig in image layer | Already "deleted" | Still present in layer history |
Restrict Job creation to specific service accounts:
Use an admission webhook or OPA/Kyverno policy to prevent Jobs from specifying arbitrary serviceAccountName values:
# Kyverno policy example
spec:
rules:
- name: restrict-serviceaccount-in-jobs
match:
resources:
kinds: ["Job"]
validate:
message: "Jobs must use the default service account"
pattern:
spec:
template:
spec:
serviceAccountName: "default"
Audit service account permissions regularly:
kubectl auth can-i --list \
--as=system:serviceaccount:app-dev:batch-runner \
-n app-dev
Run this for every service account in every namespace. Automate it. Make the output part of your security review cadence.
Scan images for embedded credentials:
Layer history survives even when files are "deleted" in subsequent layers. Make image scanning with secret detection (trufflehog, trivy) a hard gate in CI.
In Kubernetes, the risk is not in individual permissions — it is in graphs. A read permission here, a create permission there, and a service account that was provisioned too broadly last quarter combine into a path from a low-criticality pod to production secrets.
The question to ask about every service account is not "what does it do?" but "what could someone do with this token if they had it?"
Related: Case 04 — Kubernetes RBAC and Lateral Movement Between Namespaces
Categoria: Kubernetes · Cloud Security · Movimentação Lateral
Nível: Avançado
O RBAC do Kubernetes é um daqueles controles que as organizações implementam e então consideram resolvido. Roles são criadas, service accounts são vinculadas, e o cluster segue em frente. O que raramente acontece é uma revisão periódica do que cada service account pode realmente fazer — e mais importante, que combinações de permissões criam caminhos não intencionais.
Um pod comprometido em um namespace de baixa criticidade. Sem root, sem shell, sem ferramentas. Apenas /bin/sh e o token padrão da service account montado em:
/var/run/secrets/kubernetes.io/serviceaccount/token
Com esse token e o endpoint da API do cluster (disponível via variáveis de ambiente), é possível consultar a API do Kubernetes diretamente sem kubectl:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
https://${KUBERNETES_SERVICE_HOST}/api/v1/namespaces/
O primeiro passo após confirmar acesso à API é entender o que a service account atual pode fazer. A API SelfSubjectRulesReview retorna exatamente isso:
curl -s ... -X POST \
-H "Content-Type: application/json" \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"app-dev"}}' \
https://.../apis/authorization.k8s.io/v1/selfsubjectrulesreviews
No exercício, a SA atual conseguia:
A parte perigosa não era a permissão de create Jobs por si só. Era a combinação:
serviceAccountName — incluindo outras service accounts no namespacebatch-runner) tinha permissão para listar e ler Secretsbatch-runner também tinha acesso a um ConfigMap no namespace de produçãoAo criar um Job que rodava sob a identidade de batch-runner, o token de batch-runner ficava acessível no pod desse Job — e com ele, todas as permissões que batch-runner tinha.
{
"spec": {
"template": {
"spec": {
"serviceAccountName": "batch-runner",
"containers": [{"name": "r", "image": "busybox",
"command": ["sh", "-c", "cat /var/run/secrets/kubernetes.io/serviceaccount/token"]}]
}
}
}
}
A partir daí: listar secrets → obter credenciais de registry → fazer pull de imagem privada → encontrar kubeconfig embutido na camada de imagem → acessar namespace de produção.
| Permissão | Parecia inofensiva | Na verdade era |
|---|---|---|
| create Jobs | Apenas processamento batch | Permite impersonar outras service accounts |
| batch-runner lê secrets | Necessário para config da app | Expôs credenciais de registry |
| Acesso ao registry | Pull de imagem padrão | Expôs histórico com kubeconfig embutido |
| kubeconfig na camada | Já "deletado" | Ainda presente no histórico da camada |
Restringir criação de Job a service accounts específicas:
# Exemplo de policy Kyverno
spec:
rules:
- name: restrict-serviceaccount-in-jobs
match:
resources:
kinds: ["Job"]
validate:
message: "Jobs devem usar a service account padrão"
pattern:
spec:
template:
spec:
serviceAccountName: "default"
Audite permissões de service accounts regularmente:
kubectl auth can-i --list \
--as=system:serviceaccount:app-dev:batch-runner \
-n app-dev
Execute isso para cada service account em cada namespace. Automatize. Inclua o output no seu ciclo de revisão de segurança.
Escaneie imagens em busca de credenciais embutidas:
O histórico de camadas sobrevive mesmo quando arquivos são "deletados" em camadas subsequentes. Faça o escaneamento com detecção de segredos (trufflehog, trivy) um gate obrigatório no CI.
No Kubernetes, o risco não está em permissões individuais — está em grafos. Uma permissão de leitura aqui, uma permissão de criação lá, e uma service account provisionada de forma muito ampla no trimestre passado se combinam em um caminho de um pod de baixa criticidade até segredos de produção.
A pergunta a fazer sobre cada service account não é "o que ela faz?" mas "o que alguém poderia fazer com esse token se o tivesse?"
Relacionado: Case 04 — Kubernetes RBAC and Lateral Movement Between Namespaces