CKS — Secret Access Control
Secret Access Control
CKS Exam Domain: Minimize Microservice Vulnerabilities (20%)
Secrets are only as safe as the RBAC policies that guard them. If any ServiceAccount, user, or group can list Secrets in a namespace, every credential in that namespace is exposed. This topic covers how to lock down Secret access using RBAC, audit logging, network isolation, and operational best practices.
1. RBAC for Secrets
Available Verbs
| Verb | Description | Risk Level |
|---|---|---|
get |
Read a single Secret by name | Medium — but can be scoped to resourceNames |
list |
List all Secrets in a namespace | HIGH — exposes ALL secrets |
watch |
Watch for Secret changes in real time | HIGH — exposes ALL secrets |
create |
Create new Secrets | Medium |
update |
Modify existing Secrets | High |
patch |
Partially modify Secrets | High |
delete |
Delete Secrets | High |
Why list and watch Are Dangerous
list and watch return all Secrets in the target scope. Unlike get, they cannot be restricted with resourceNames. Granting list on Secrets is equivalent to granting read access to every secret in the namespace.
# DANGEROUS — grants access to ALL secrets in the namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-lister
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["list", "watch"] # ← Reads EVERY secret
# SAFE — grants access to ONLY the specified secret:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-db-creds"] # ← Only this one secret
verbs: ["get"]
CKS Exam Tip: If a question asks you to “restrict secret access,” always use
getwithresourceNamesinstead oflistorwatch. This is the most granular control available.
2. Complete RBAC Example — Restricting Secret Access
Scenario
The backend ServiceAccount in the production namespace should only be able to read the db-creds Secret. No other secret access should be permitted.
Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: backend-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-creds"]
verbs: ["get"]
RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: backend-read-db-creds
namespace: production
subjects:
- kind: ServiceAccount
name: backend
namespace: production
roleRef:
kind: Role
name: backend-secret-reader
apiGroup: rbac.authorization.k8s.io
Verification
# Check if the SA can get the specific secret:
kubectl auth can-i get secrets/db-creds \
--as=system:serviceaccount:production:backend \
-n production
# yes
# Check if the SA can list all secrets:
kubectl auth can-i list secrets \
--as=system:serviceaccount:production:backend \
-n production
# no
# Check if the SA can get a different secret:
kubectl auth can-i get secrets/other-creds \
--as=system:serviceaccount:production:backend \
-n production
# no
3. Namespace-Scoped Secret Isolation
Never Use ClusterRole for Secrets
A ClusterRoleBinding with secret access grants cross-namespace secret reading:
# DANGEROUS — DO NOT DO THIS:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secret-reader-all
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dev-team-secrets
subjects:
- kind: Group
name: dev-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: secret-reader-all
apiGroup: rbac.authorization.k8s.io
# ← dev-team can now read secrets in EVERY namespace, including kube-system!
Correct Approach — Per-Namespace Roles
# Create a Role in each namespace that needs it:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dev-secret-reader
namespace: dev # ← Scoped to dev namespace only
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-config"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-team-secrets
namespace: dev
subjects:
- kind: Group
name: dev-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: dev-secret-reader
apiGroup: rbac.authorization.k8s.io
Finding Dangerous Cluster-Wide Secret Bindings
# Find all ClusterRoleBindings that reference ClusterRoles with secret access:
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
select(.roleRef.kind == "ClusterRole") |
.metadata.name as $binding |
.roleRef.name as $role |
.subjects[]? |
"\($binding) → \($role) → \(.kind)/\(.name)"
' | while read line; do
role=$(echo "$line" | awk -F' → ' '{print $2}')
# Check if the role has secret access
kubectl get clusterrole "$role" -o json 2>/dev/null | \
jq -e '.rules[]? | select(.resources[]? == "secrets" or .resources[]? == "*")' > /dev/null 2>&1 && \
echo "SECRET ACCESS: $line"
done
4. Auditing Secret Access
API Audit Policy for Secrets
Configure Kubernetes audit logging to capture all secret access events:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all secret access at RequestResponse level (captures request AND response bodies)
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
# Omit response body for list/watch to avoid logging all secret data:
omitStages:
- "RequestReceived"
# Alternative: Metadata level (logs who accessed what, but NOT the secret data)
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
verbs: ["get", "list", "watch"]
# Log secret creation/modification at Request level
- level: Request
resources:
- group: ""
resources: ["secrets"]
verbs: ["create", "update", "patch", "delete"]
# Default: don't log everything else at high verbosity
- level: Metadata
omitStages:
- "RequestReceived"
Warning: Using
RequestResponseon Secrets logs the actual secret data in the audit log. UseMetadatalevel if you only need to know who accessed which Secret.
Adding Audit Configuration to API Server
# In /etc/kubernetes/manifests/kube-apiserver.yaml:
spec:
containers:
- command:
- kube-apiserver
- --audit-policy-file=/etc/kubernetes/audit/policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
# ... other flags
Reading Audit Logs
# Find who accessed secrets in a specific namespace:
cat /var/log/kubernetes/audit.log | jq -r '
select(.objectRef.resource == "secrets") |
select(.objectRef.namespace == "production") |
[.requestReceivedTimestamp, .user.username, .verb, .objectRef.name] | @tsv
'
# Find unauthorized secret access attempts:
cat /var/log/kubernetes/audit.log | jq -r '
select(.objectRef.resource == "secrets") |
select(.responseStatus.code >= 403) |
[.requestReceivedTimestamp, .user.username, .verb, .objectRef.namespace + "/" + .objectRef.name, .responseStatus.code] | @tsv
'
5. Auditing Who Can Access Secrets
Use kubectl auth can-i to audit secret access permissions:
Check a Specific ServiceAccount
# Can this SA get secrets in its namespace?
kubectl auth can-i get secrets \
--as=system:serviceaccount:production:backend \
-n production
# yes / no
# Can this SA list all secrets?
kubectl auth can-i list secrets \
--as=system:serviceaccount:production:backend \
-n production
# yes / no
# Can this SA create secrets?
kubectl auth can-i create secrets \
--as=system:serviceaccount:production:backend \
-n production
# yes / no
Check All Permissions for a SA
# Full permission audit:
kubectl auth can-i --list \
--as=system:serviceaccount:production:backend \
-n production | grep -i secret
Find All Identities With Secret Access
# Manual audit: check common ServiceAccounts
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
for sa in $(kubectl get serviceaccounts -n "$ns" -o jsonpath='{.items[*].metadata.name}'); do
can=$(kubectl auth can-i get secrets \
--as="system:serviceaccount:${ns}:${sa}" \
-n "$ns" 2>/dev/null)
if [ "$can" = "yes" ]; then
echo "WARNING: ${ns}/${sa} can get secrets in ${ns}"
fi
done
done
6. NetworkPolicy to Restrict Secret-Consuming Pods
While RBAC controls who can read Secrets through the API, NetworkPolicy controls which Pods can communicate with secret-consuming services.
Isolate Secret-Consuming Pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: restrict-db-access
namespace: production
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: backend # Only backend Pods can reach the database
ports:
- protocol: TCP
port: 5432
Prevent Exfiltration
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: restrict-egress
namespace: production
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to: # Allow DNS
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
# Backend Pods can ONLY talk to the database and DNS
# Cannot exfiltrate secrets to external endpoints
7. Preventing Secret Exposure in Logs and Environment
Avoid Environment Variable Injection
# AVOID — secrets in env vars leak in logs, crash dumps, describe:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
# PREFER — volume mounts:
volumeMounts:
- name: db-creds
mountPath: /etc/secrets
readOnly: true
volumes:
- name: db-creds
secret:
secretName: db-creds
Application-Level Safeguards
# NEVER log environment variables:
env # ← DON'T
printenv # ← DON'T
set # ← DON'T
# NEVER use secrets in command-line arguments (visible in /proc):
myapp --password=secret123 # ← Visible in `ps aux`
# PREFER reading from files:
myapp --password-file=/etc/secrets/password
Prevent kubectl describe from Revealing Secret References
Note: kubectl describe pod always shows secretKeyRef references (though not values). This is metadata exposure — an attacker knows which secrets to target:
Environment:
DB_PASSWORD: <set to the key 'password' in secret 'db-creds'>
Volume mounts only show the mount path:
Mounts:
/etc/secrets from db-creds (ro)
8. Preventing Escalation via Secrets
The Pod-Creation Escalation Path
If an identity can create Pods in a namespace, it can mount any Secret in that namespace:
# An attacker who can create Pods can steal any secret:
apiVersion: v1
kind: Pod
metadata:
name: steal-secret
namespace: production
spec:
containers:
- name: thief
image: busybox
command: ["cat", "/stolen/password"]
volumeMounts:
- name: stolen
mountPath: /stolen
volumes:
- name: stolen
secret:
secretName: db-admin-creds # ← Any secret in the namespace
Mitigation:
- Restrict
createon Pods (and Deployments, DaemonSets, etc.) to trusted identities only - Use OPA Gatekeeper or Kyverno to enforce which secrets a Pod can reference
- Use separate namespaces for different trust levels
The SA Token Escalation Path
If an identity can create ServiceAccount token Secrets, it can impersonate that SA:
# Create a long-lived token for a privileged SA:
kubectl create token admin-sa -n kube-system --duration=99999h
Mitigation:
- Restrict
createonserviceaccounts/tokensubresource - Restrict
createon Secrets of typekubernetes.io/service-account-token
Summary
| Control | Action | Scope |
|---|---|---|
| RBAC Verb Restriction | Use get with resourceNames, avoid list/watch |
Namespace |
| Namespace Isolation | Use Roles, not ClusterRoles, for secret access | Per-namespace |
| Audit Logging | Configure audit policy to log secret access | Cluster-wide |
| Permission Audit | kubectl auth can-i for all SAs |
Per-namespace |
| NetworkPolicy | Restrict which Pods reach secret-consuming services | Namespace |
| No Env Vars | Use volume mounts instead of environment variables | Pod spec |
| Escalation Prevention | Restrict Pod creation, SA token creation | Namespace/Cluster |
Practice Questions
Question 1
Why is granting list permission on Secrets more dangerous than granting get?
Show Answer
list returns all Secrets in the target scope (namespace or cluster). There is no way to restrict list to specific Secret names using resourceNames — the RBAC API ignores resourceNames for list and watch verbs.
# This does NOT work — resourceNames is ignored for list:
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["my-secret"] # ← IGNORED for list verb
verbs: ["list"]
# Result: can list ALL secrets in the namespace
get CAN be scoped to specific resourceNames:
# This WORKS — only allows reading the specific secret:
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["my-secret"]
verbs: ["get"]
# Result: can only read "my-secret", nothing else
Rule of thumb: Never grant list or watch on Secrets unless the identity genuinely needs to enumerate all secrets (e.g., a backup controller).
Question 2
Write a Role and RoleBinding that allows the deployer ServiceAccount in the staging namespace to only read the registry-creds Secret.
Show Answer
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer-registry-reader
namespace: staging
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["registry-creds"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deployer-read-registry-creds
namespace: staging
subjects:
- kind: ServiceAccount
name: deployer
namespace: staging
roleRef:
kind: Role
name: deployer-registry-reader
apiGroup: rbac.authorization.k8s.io
Verify:
kubectl auth can-i get secrets/registry-creds \
--as=system:serviceaccount:staging:deployer -n staging
# yes
kubectl auth can-i list secrets \
--as=system:serviceaccount:staging:deployer -n staging
# no
kubectl auth can-i get secrets/other-secret \
--as=system:serviceaccount:staging:deployer -n staging
# no
Question 3
How would you audit which ServiceAccounts in the production namespace can access Secrets?
Show Answer
Use kubectl auth can-i to check each ServiceAccount:
# Iterate over all SAs in the namespace:
for sa in $(kubectl get sa -n production -o jsonpath='{.items[*].metadata.name}'); do
for verb in get list watch create update delete; do
result=$(kubectl auth can-i $verb secrets \
--as=system:serviceaccount:production:$sa \
-n production 2>/dev/null)
if [ "$result" = "yes" ]; then
echo "production/$sa can $verb secrets"
fi
done
done
Example output:
production/default can get secrets
production/backend can get secrets
production/admin-sa can get secrets
production/admin-sa can list secrets ← INVESTIGATE
production/admin-sa can create secrets ← INVESTIGATE
For a broader view, check the actual Role/ClusterRole rules:
# Find all Roles in the namespace that reference secrets:
kubectl get roles -n production -o json | jq -r '
.items[] |
select(.rules[]?.resources[]? == "secrets" or .rules[]?.resources[]? == "*") |
.metadata.name
'
Question 4
A developer has create permission for Pods in the production namespace. Explain how this allows them to read any Secret in that namespace, even without explicit Secret RBAC.
Show Answer
If a user can create Pods, they can mount any Secret in the namespace as a volume:
apiVersion: v1
kind: Pod
metadata:
name: secret-reader
namespace: production
spec:
containers:
- name: reader
image: busybox
command: ["cat", "/secrets/password"]
volumeMounts:
- name: stolen-secret
mountPath: /secrets
volumes:
- name: stolen-secret
secret:
secretName: any-secret-in-namespace
The kubelet (not the user) mounts the Secret, so RBAC on Secrets does not apply. The user just needs Pod creation permission.
Mitigations:
- Restrict Pod creation to trusted identities
- Use a validating admission webhook (OPA Gatekeeper, Kyverno) to restrict which Secrets a Pod can reference
- Separate sensitive workloads into different namespaces
- Use PodSecurity admission to restrict capabilities
Question 5
Write a Kubernetes audit policy rule that logs all get, list, and watch operations on Secrets at the Metadata level.
Show Answer
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log read operations on secrets at Metadata level
# (captures who/what/when but NOT the secret data)
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
verbs: ["get", "list", "watch"]
# Log write operations at Request level (captures the request body)
- level: Request
resources:
- group: ""
resources: ["secrets"]
verbs: ["create", "update", "patch", "delete"]
# Default catch-all
- level: Metadata
omitStages:
- "RequestReceived"
Why Metadata and not RequestResponse for reads?
Using RequestResponse on Secret get/list would log the actual secret data in the audit log, creating a new secret exposure surface. Metadata level logs the user, verb, resource, namespace, and timestamp — enough for auditing without leaking secret values.
Apply by adding to the API server:
--audit-policy-file=/etc/kubernetes/audit/policy.yaml
--audit-log-path=/var/log/kubernetes/audit.log
| Previous | Up | Next |
|---|---|---|
| Secret Rotation | Secrets Management | Secrets Management Lab |