CKS — Secret Access Control

11 minute read

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 get with resourceNames instead of list or watch. 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 RequestResponse on Secrets logs the actual secret data in the audit log. Use Metadata level 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 create on 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 create on serviceaccounts/token subresource
  • Restrict create on Secrets of type kubernetes.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:

  1. Restrict Pod creation to trusted identities
  2. Use a validating admission webhook (OPA Gatekeeper, Kyverno) to restrict which Secrets a Pod can reference
  3. Separate sensitive workloads into different namespaces
  4. 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

Updated: