Advanced

Configuring and Managing Kubernetes Security

Secure cluster access, application deployment and applications at runtime in Kubernetes.

Application: Wired Brain Coffee — used as a running example throughout the course Modules: Securing Cluster Access · Securing Application Deployment · Securing Applications at Runtime


Table of Contents


1. Securing Cluster Access

1.1 Authentication and RBAC — Core Concepts

Securing access to the Kubernetes cluster is the foundation of Defense in Depth. Without access control, every other security measure can be bypassed. Kubernetes uses Role-Based Access Control (RBAC) to manage permissions.

The four RBAC resources

ResourceScopeDescription
RoleNamespaceSet of rules (verbs + resources) within a single namespace
RoleBindingNamespaceBinds a Role to a principal (ServiceAccount, User, Group)
ClusterRoleCluster-widePermissions on cluster-wide resources (nodes, namespaces) or reusable template
ClusterRoleBindingCluster-wideBinds a ClusterRole to a principal for the entire cluster

Common Verbs

VerbDescription
getRead a specific resource
listList all resources
createCreate a new resource
updateModify an existing resource
patchPartial modification
deleteDelete a resource
watchObserve changes in real time

Important Predefined ClusterRoles

ClusterRolePermissions
viewRead-only access to most resources (not Secrets)
editRead/write, without RBAC management
adminFull read/write within a namespace, including RBAC management
cluster-adminFull access to the entire cluster — use with extreme caution

Danger: cluster-admin on the default ServiceAccount means every pod in the namespace has root access to the entire cluster.


1.2 Demo: RBAC for ServiceAccounts

Scenario: kube-explorer application without RBAC

The Wired Brain Coffee SRE team deploys kube-explorer, a web UI for managing Pods. Without explicit RBAC, the pod uses the default ServiceAccount, which inherits the following insecure ClusterRoleBinding:

# setup/rbac-insecure.yaml — dangerous configuration
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: default-cluster-admin
  labels:
    wiredbrain: debug
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin

Consequence: kube-explorer can view and delete pods in kube-system — the cluster’s critical components.

Fix: Dedicated Role + ServiceAccount (Least Privilege)

# update-1/rbac.yaml — fixed RBAC, least privilege
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: default-pod-admin
  namespace: default
  labels:
    wiredbrain: kube-explorer
rules:
- apiGroups: [""]   # core API group
  resources: ["pods"]
  verbs: ["get", "list", "delete"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kube-explorer
  labels:
    wiredbrain: kube-explorer
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: kube-explorer-default
  namespace: default
  labels:
    wiredbrain: kube-explorer
subjects:
- kind: ServiceAccount
  name: kube-explorer
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: default-pod-admin

To allow read-only pod access in kube-system:

# update-2/rbac.yaml — read-only access in kube-system
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: system-pod-reader
  namespace: kube-system
  labels:
    wiredbrain: kube-explorer
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: kube-explorer-system
  namespace: kube-system
  labels:
    wiredbrain: kube-explorer
subjects:
- kind: ServiceAccount
  name: kube-explorer
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: system-pod-reader

Disabling token mounting for pods with no API access needs

Most application pods do not need to call the Kubernetes API. Best practice is to disable automatic token mounting:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app-sa
  namespace: my-namespace
automountServiceAccountToken: false   # no token = no API access

Or at the Pod spec level:

spec:
  automountServiceAccountToken: false
  containers:
  - name: app
    image: my-image:v1

1.3 End-User Authentication

Kubernetes does not manage users internally — there is no User object to create. Instead, Kubernetes delegates authentication to external systems.

Authentication Methods

MethodAdvantagesDisadvantages
X.509 CertificatesWorks everywhere, simpleNo individual revocation, no MFA, long validity
Static tokensSimple to configureEquivalent to a plaintext password file
OpenID Connect (OIDC)MFA, short-lived tokens, IdP integrationRequires additional configuration
Azure Entra ID (AKS)Enforced MFA, temporary tokens, centralized managementAzure-specific

The kubeconfig file: a security risk

The ~/.kube/config file contains cluster credentials (certificates, tokens). If this file is compromised:

  • An attacker gains access identical to the legitimate user’s
  • Embedded certificates may be valid for years

The solution: use OIDC with short-lived tokens — a stolen token expires quickly.

Least-privilege access for users

# rbac/least-privilege.yaml — user with ClusterRole "view" in a namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: least-privilege-binding
  namespace: wiredbrain
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view              # read-only, not Secrets
subjects:
- kind: User
  name: "least-privilege@wiredbrain.com"
  apiGroup: rbac.authorization.k8s.io

1.4 Demo: MFA and RBAC for End-Users (AKS + Azure Entra ID)

Initial problem: static credentials in kubeconfig

# Retrieving default credentials — token embedded in the file
az aks get-credentials --resource-group rg-wiredbrain --name aks-wiredbrain
kubectl config view   # shows the encoded token/certificate (but not encrypted)

Solution: Enable Azure Entra ID on AKS

# Identify the Azure AD group for cluster-admins
az ad group list --display-name "cluster-admins" --query "[].id" -o tsv

# Enable Entra ID integration + disable local accounts
az aks update \
  --resource-group rg-wiredbrain \
  --name aks-wiredbrain \
  --enable-aad \
  --aad-admin-group-object-ids <GROUP_OBJECT_ID> \
  --disable-local-accounts

After the update:

  • Tokens and certificates in the existing kubeconfig no longer work
  • Each kubectl call triggers an OAuth2 flow via kubelogin
  • Azure Entra ID enforces MFA based on the organization’s Conditional Access policies
  • Tokens have a short lifespan (typically 1h)
# Installing kubelogin (tool for OAuth/OIDC authentication)
# Download from https://github.com/Azure/kubelogin/releases
export PATH=$PATH:~/kubelogin

# First command after the update — triggers the MFA flow
kubectl get nodes
# → Opens browser for Azure Entra MFA authentication

1.5 Defense in Depth — Overview

┌─────────────────────────────────────────────────────────────┐
│                      CLUSTER ACCESS                          │
│                                                              │
│  Authentication (OIDC/Entra)  →  RBAC (Authorization)       │
│                                                              │
│  • MFA enforced by the IdP                                   │
│  • Short-lived tokens (no static certificates)               │
│  • --disable-local-accounts on AKS                          │
│  • Roles/ClusterRoles with least privilege                   │
│  • automountServiceAccountToken: false by default            │
└─────────────────────────────────────────────────────────────┘

Key principle: Compromising a pod or a credential should not allow pivoting to other resources. Each RBAC boundary forces an attacker to work harder, generating detectable noise.


2. Securing Application Deployment

2.1 Supply Chain Complexity

Every container image is a complete software supply chain you do not fully control:

Your Python application
    └── Framework (Django, Flask, FastAPI)
        └── Python dependencies (requirements.txt)
            └── Python base image
                └── Debian/Alpine Linux
                    └── Hundreds of system packages

The CVE problem over time

MomentSituation
Initial deploymentImage scanned → clean, 0 critical CVEs
Day +30New CVE discovered in OpenSSL → image vulnerable
Day +45OpenSSL updated → incompatibility with the framework
Day +60Framework updated → incompatibility with another dependency

Solution: continuous scanning (not only at deployment) with Trivy Operator (CNCF project).


2.2 Demo: Image Scanning with Trivy Operator

Installing Trivy Operator via Helm

# Add the Aqua Security Helm repository
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update

# Install Trivy Operator
helm install trivy-operator aqua/trivy-operator \
  --namespace trivy-system \
  --create-namespace \
  --set trivy.ignoreUnfixed=true \
  --set resources.requests.memory=200Mi \
  --set trivy.arch=arm64    # For Apple Silicon

Once deployed, Trivy Operator begins scanning all images of all pods across all namespaces and produces Kubernetes-native VulnerabilityReport objects.

Deploy a vulnerable image (example)

# demo1/initial-scan/vulnerable-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: products-api
  namespace: wb-scan
spec:
  replicas: 1
  selector:
    matchLabels:
      app: products-api
  template:
    metadata:
      labels:
        app: products-api
    spec:
      containers:
      - name: api
        # Old image with known CVEs
        image: sixeyed/wiredbrain-products-api:k8s-security-m2-old

Viewing vulnerability reports

# Wait for the report to be generated
kubectl get vulnerabilityreport -n wb-scan

# View the YAML summary
kubectl get vulnerabilityreport -n wb-scan \
  -o jsonpath='{.items[0].report.summary}'

# Typical result:
# {"criticalCount":4,"highCount":12,"lowCount":45,"mediumCount":28,"noneCount":2}

Fixed image after update

# demo1/update-1/secure-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: products-api
  namespace: wb-secure
spec:
  replicas: 1
  selector:
    matchLabels:
      app: products-api
      version: secure
  template:
    metadata:
      labels:
        app: products-api
        version: secure
    spec:
      containers:
      - name: api
        # Updated image — no critical CVEs
        image: sixeyed/wiredbrain-products-api:k8s-security-m2
        command: ["sleep", "infinity"]

2.3 Vulnerability Reports and Kyverno Policies

Trivy Operator Architecture

┌──────────────────────────────────────────────────────┐
│                   Kubernetes Cluster                  │
│                                                       │
│   Trivy Operator Pod                                  │
│   ├── Scans all images in all Pods                   │
│   ├── Regularly updates the CVE database             │
│   └── Produces VulnerabilityReport CRDs              │
│                                                       │
│   VulnerabilityReport (CRD)                          │
│   ├── report.summary.criticalCount: 4                │
│   ├── report.summary.highCount: 12                   │
│   └── report.artifact.tag: "v1.2.3"                 │
└──────────────────────────────────────────────────────┘

Kyverno: Kubernetes-native Policy Engine

Kyverno acts as an admission controller — it intercepts requests to the Kubernetes API before they are persisted in etcd, and can block or audit resources that do not comply with policies.

# Installing Kyverno via Helm
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace

2.4 Demo: Policy Enforcement with Kyverno

Policy 1: Security Restrictions (Enforce)

# admission-control/security-policies.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: container-security-policies
  annotations:
    policies.kyverno.io/title: Container Security Policies
    policies.kyverno.io/category: Security
    policies.kyverno.io/severity: high
    policies.kyverno.io/description: >-
      Enforces container security best practices including registry restrictions
      and running as non-root.
spec:
  validationFailureAction: Enforce   # BLOCKS non-compliant deployments
  background: false
  rules:

  # Rule 1: Only the approved registry is allowed
  - name: restrict-image-registries
    match:
      any:
      - resources:
          kinds:
          - Pod
          - Deployment
          - StatefulSet
          - DaemonSet
          - Job
          - CronJob
    validate:
      message: >-
        Images must be pulled from the approved registry: ghcr.io/wiredbrain/
      deny:
        conditions:
          any:
          - key: "{{ request.object.spec.containers[].image || \
                     request.object.spec.template.spec.containers[].image }}"
            operator: AnyNotIn
            value:
            - "ghcr.io/wiredbrain/*"

  # Rule 2: Prohibit running as root
  - name: require-non-root
    match:
      any:
      - resources:
          kinds:
          - Pod
          - Deployment
          - StatefulSet
          - DaemonSet
          - Job
          - CronJob
    validate:
      message: >-
        Containers must run as non-root user.
        Set securityContext.runAsNonRoot: true in the pod spec.
      deny:
        conditions:
          any:
          - key: "{{ request.object.spec.securityContext.runAsNonRoot || \
                     request.object.spec.template.spec.securityContext.runAsNonRoot \
                     || `false` }}"
            operator: NotEquals
            value: true

Policy 2: Trivy CVE Audit (Audit only)

# admission-control/vulnerability-report-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: vulnerability-report-policy
  annotations:
    policies.kyverno.io/title: Vulnerability Report Policy
    policies.kyverno.io/category: Security
    policies.kyverno.io/severity: high
spec:
  validationFailureAction: Audit   # Logs without blocking (to avoid impacting production)
  background: false
  rules:
  - name: check-vulnerability-reports
    match:
      any:
      - resources:
          kinds: [Pod, Deployment, StatefulSet, DaemonSet, Job, CronJob]
    preconditions:
      all:
      - key: "{{ request.operation || 'CREATE' }}"
        operator: AnyIn
        value: [CREATE, UPDATE]
    context:
    - name: allReports
      apiCall:
        # Queries Trivy VulnerabilityReports via the Kubernetes API
        urlPath: "/apis/aquasecurity.github.io/v1alpha1/namespaces/\
                  {{ request.namespace || 'default' }}/vulnerabilityreports"
        jmesPath: "items[]"
    validate:
      message: >-
        Image contains CRITICAL severity CVEs. Please use a patched version.
      deny:
        conditions:
          any:
          - key: "{{ allReports[...].report.summary.criticalCount | [0] || `0` }}"
            operator: GreaterThan
            value: 0

Why Audit for CVEs? Using Enforce for CVEs would block auto-scaling and updates as long as a vulnerable container is still running — which could make remediation impossible. Best practice is to use CVE reports for continuous detection and notification, not for blocking.

Compliance tests

# Test 1: image from Docker Hub (should be blocked)
kubectl apply -f test-deployments/01-wrong-registry.yaml
# → Error: Images must be pulled from the approved registry: ghcr.io/wiredbrain/

# Test 2: image running as root (should be blocked)
kubectl apply -f test-deployments/02-runs-as-root.yaml
# → Error: Containers must run as non-root user.

# Test 3: image with critical CVEs (audit only — passes but logged)
kubectl apply -f test-deployments/03-has-critical-cves.yaml
# → Deployed, but violation logged in audit logs

# Test 4: compliant image (should pass)
kubectl apply -f test-deployments/04-compliant.yaml
# → deployment.apps/compliant-app created

2.5 Defense in Depth — Deployment Layer

ToolRoleMode
Trivy OperatorContinuous scanning of running imagesDetection / Audit
Kyverno (Enforce)Blocks non-compliant configs (registry, non-root)Prevention
Kyverno (Audit)Logs CVE violations without blockingDetection
Falco (CNCF)Behavioral detection at runtime (suspicious syscalls)Runtime detection
Kubernetes Audit LogsFull traceability of all API calls (who did what, when)Forensics

3. Securing Applications at Runtime

3.1 NetworkPolicy as a Firewall

By default, the Kubernetes network is completely open: any pod can communicate with any other pod, in any namespace. A compromised container becomes a jump box for an attacker.

NetworkPolicy Model

┌─────────────────────────────────────────────────────────────┐
│  BEFORE: open network (default)                             │
│                                                              │
│  web-app ←──→ products-api ←──→ stock-api ←──→ database    │
│     ↑                                            ↑           │
│     └──────────────── direct access ─────────────┘           │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  AFTER: zero-trust network with NetworkPolicies             │
│                                                              │
│  web-app ──→ products-api ──→ database                      │
│      └────→ stock-api ────→ database                        │
│                                                              │
│  Any direct web-app → database access is BLOCKED            │
└─────────────────────────────────────────────────────────────┘

How it works: NetworkPolicies use label selectors to identify the pods they apply to. A pod not covered by a policy is blocked if a default-deny is in place.

Prerequisite: The cluster’s CNI plugin must support NetworkPolicies (Calico, Cilium, Weave). Docker Desktop does not support NetworkPolicies — use k3d, kind with Calico, or a cloud cluster.


3.2 Demo: NetworkPolicies for Wired Brain Coffee

Application architecture

Namespace: wiredbrain
├── web-app          (component: web-app)
├── products-api     (component: products-api)
├── stock-api        (component: stock-api)
└── database         (component: database, port: 5432)

Step 1: Default Deny — total block

# default-deny/default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: wiredbrain
spec:
  # empty podSelector = applies to ALL pods in the namespace
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  # No rules defined = all traffic is blocked
kubectl apply -f default-deny/default-deny.yaml

# Verification: DNS is also blocked!
kubectl exec -n wiredbrain deploy/web-app -- \
  wget -qO- http://products-api
# → wget: bad address 'products-api'  (DNS blocked)

Step 2: Allow DNS (infrastructure)

# allow-policies/allow-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: wiredbrain
spec:
  podSelector: {}     # All pods
  policyTypes:
  - Egress
  egress:
  - ports:
    - port: 53
      protocol: UDP
    - port: 53
      protocol: TCP

Step 3: Granular application rules

# allow-policies/app/allow-web-to-api.yaml
# Allow web-app → products-api (port 80)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-products-api
  namespace: wiredbrain
spec:
  podSelector:
    matchLabels:
      component: products-api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          component: web-app
    ports:
    - protocol: TCP
      port: 80
---
# Allow web-app → stock-api (port 8080)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-stock-api
  namespace: wiredbrain
spec:
  podSelector:
    matchLabels:
      component: stock-api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          component: web-app
    ports:
    - protocol: TCP
      port: 8080
# allow-policies/app/allow-api-to-db.yaml
# Allow products-api and stock-api → database (port 5432)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: wiredbrain
spec:
  podSelector:
    matchLabels:
      component: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          component: products-api
    ports:
    - protocol: TCP
      port: 5432
  - from:
    - podSelector:
        matchLabels:
          component: stock-api
    ports:
    - protocol: TCP
      port: 5432
# allow-policies/app/allow-web-egress.yaml
# Allow web-app to send outbound traffic to the APIs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-egress
  namespace: wiredbrain
spec:
  podSelector:
    matchLabels:
      component: web-app
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          component: products-api
  - to:
    - podSelector:
        matchLabels:
          component: stock-api
# allow-policies/app/allow-api-egress.yaml
# Allow APIs to contact the database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-egress
  namespace: wiredbrain
spec:
  podSelector:
    matchLabels:
      component: products-api
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          component: database
    ports:
    - protocol: TCP
      port: 5432

3.3 Secret Store CSI Driver

Native Kubernetes Secrets are not secure for sensitive data:

ProblemDetail
Encoding ≠ encryptionBase64 is immediately reversible (base64 -d)
etcd storageOften not encrypted at-rest without explicit configuration
Developer accessAny developer with kubectl get secret -o yaml can read credentials
Environment variableskubectl exec pod -- printenv reveals secrets in plaintext
Source controlHelm values.yaml files with passwords are often committed

Solution: External Secrets with Secret Store CSI Driver

┌─────────────────────────────────────────────────────────────┐
│  Kubernetes Pod                                              │
│  ├── Volume mounted via CSI Driver                          │
│  │   └── /mnt/secrets/postgres-password  (text file)       │
│  └── NO Kubernetes secret in etcd                           │
└─────────────────────┬───────────────────────────────────────┘
                       │ Auth via Workload Identity
                       ↓
┌─────────────────────────────────────────────────────────────┐
│  Azure Key Vault / AWS Secrets Manager / HashiCorp Vault    │
│  ├── postgres-password: "s3cur3P@ss!"  (encrypted)         │
│  ├── application.properties  (encrypted)                    │
│  └── db-connection-string  (encrypted)                      │
└─────────────────────────────────────────────────────────────┘

Benefits:

  • Secrets never transit through etcd
  • Authentication to the vault is managed by the cloud provider (Workload Identity) — no credentials in YAML
  • Each pod can only access the secrets it needs

3.4 Demo: Secure Secrets with Azure Key Vault

Problem demonstrated: vulnerable native secrets

# View the base64-encoded secret
kubectl get secret db-credentials -n wiredbrain -o yaml

# Decode immediately
kubectl get secret db-credentials -n wiredbrain \
  -o go-template='{{.data.POSTGRES_PASSWORD | base64decode}}'
# → my_secret_password123   (in plaintext!)

# View in the pod via printenv
kubectl exec -n wiredbrain deploy/database -- printenv POSTGRES_PASSWORD
# → my_secret_password123   (in plaintext!)

ServiceAccount with Azure Workload Identity

# charts/wiredbrain-secure/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: workload-identity-sa
  namespace: wiredbrain
  labels:
    app.kubernetes.io/managed-by: Helm
  annotations:
    # Links the Kubernetes SA to an Azure Managed Identity
    # No Azure credentials in the YAML!
    azure.workload.identity/client-id: "{{ .Values.azure.clientId }}"

SecretProviderClass — abstraction over the external vault

# charts/wiredbrain-secure/templates/secret-provider.yaml

# Credentials for the database
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: database-credentials
  namespace: wiredbrain
  labels:
    component: database
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    clientID: "{{ .Values.azure.clientId }}"
    keyvaultName: "{{ .Values.azure.keyVaultName }}"
    tenantId: "{{ .Values.azure.tenantId }}"
    objects: |
      array:
        - |
          objectName: postgres-password       # Name in Key Vault
          objectType: secret
          objectVersion: ""
          objectAlias: postgres-password      # Mounted file name
---
# Credentials for products-api
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: products-api-credentials
  namespace: wiredbrain
  labels:
    component: products-api
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    clientID: "{{ .Values.azure.clientId }}"
    keyvaultName: "{{ .Values.azure.keyVaultName }}"
    tenantId: "{{ .Values.azure.tenantId }}"
    objects: |
      array:
        - |
          objectName: application-properties
          objectType: secret
          objectAlias: application.properties
---
# Credentials for stock-api
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: stock-api-credentials
  namespace: wiredbrain
  labels:
    component: stock-api
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    clientID: "{{ .Values.azure.clientId }}"
    keyvaultName: "{{ .Values.azure.keyVaultName }}"
    tenantId: "{{ .Values.azure.tenantId }}"
    objects: |
      array:
        - |
          objectName: db-connection-string
          objectType: secret
          objectAlias: db-connection-string

Using the CSI volume in a Deployment

# Example: Deployment with CSI Driver for secrets
apiVersion: apps/v1
kind: Deployment
metadata:
  name: database
  namespace: wiredbrain
spec:
  template:
    spec:
      serviceAccountName: workload-identity-sa   # SA with Workload Identity
      containers:
      - name: postgres
        image: ghcr.io/wiredbrain/postgres:latest
        env:
        - name: POSTGRES_PASSWORD_FILE
          value: /mnt/secrets/postgres-password   # Read from the file
        volumeMounts:
        - name: secrets-store
          mountPath: /mnt/secrets
          readOnly: true
      volumes:
      - name: secrets-store
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: database-credentials   # References the SecretProviderClass

3.5 Completing the Security Model

Additional hardening at the Pod/Container level

These settings require testing as they may break the application:

spec:
  securityContext:
    # seccomp profile to restrict dangerous Linux syscalls
    seccompProfile:
      type: RuntimeDefault
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  containers:
  - name: app
    image: ghcr.io/wiredbrain/app:v1
    securityContext:
      allowPrivilegeEscalation: false    # Prevents sudo/setuid
      readOnlyRootFilesystem: true       # Read-only filesystem
      capabilities:
        drop:
        - ALL                            # Drop all Linux capabilities
        add:
        - NET_BIND_SERVICE               # Re-add only what is needed

Summary table of Wired Brain Coffee security layers

LayerTool / MechanismProtection
Cluster AccessAzure Entra ID + OIDCMFA, short-lived tokens
Cluster AccessRBAC (Roles, ClusterRoles)Least privilege per namespace
Cluster AccessautomountServiceAccountToken: falseNo API access by default
DeploymentTrivy OperatorContinuous CVE scanning
DeploymentKyverno (Enforce)Blocks unapproved registry + running as root
DeploymentKyverno (Audit)Logs critical CVEs
RuntimeNetworkPolicy (default-deny + allow rules)Zero-trust networking
RuntimeSecret Store CSI DriverNo secrets in etcd
RuntimeWorkload IdentityVault auth without credentials in YAML
RuntimesecurityContext (seccomp, readOnly, no-privesc)Reduced attack surface

4. Reference Tables

Comparison: Native Kubernetes Secrets vs CSI Driver

CriterionNative Kubernetes SecretsSecret Store CSI Driver
Storageetcd (base64)Azure Key Vault / AWS / Vault
Encryption at-restOptional, often disabledAlways encrypted
RevocationManual (delete the secret)Immediate in the vault
Audit trailKubernetes audit logsKey Vault/Vault logs (detailed)
RotationRedeployment requiredAutomatic rotation possible
Developer accesskubectl get secret = credentials in plaintextSeparate IAM access

Comparison: end-user authentication methods

MethodMFAToken lifetimeRevocableRecommended
X.509 CertificateNoYearsNo (unless CA rotation)❌ No
Static bearer tokenNoInfiniteYes (API server restart)❌ No
OIDC (OpenID Connect)Yes (via IdP)Minutes–hoursYes (IdP)✅ Yes
Azure Entra ID (AKS)Yes~1hYes✅ Yes

Kyverno: validationFailureAction — when to use which

ActionBehaviorUse case
EnforceBlocks creation/updateApproved registry, non-root, forbidden capabilities
AuditAllows but logs the violationCVEs, policies not yet ready to enforce

NetworkPolicy: checklist per namespace

# Check if a namespace has a default-deny
kubectl get networkpolicy -n <namespace>

# Test connectivity from a pod
kubectl exec -n <namespace> deploy/<name> -- \
  wget -qO- --timeout=2 http://<target-service>

# Test DNS
kubectl exec -n <namespace> deploy/<name> -- \
  nslookup kubernetes.default.svc.cluster.local

5. Architecture Diagrams

Diagram 1: Full RBAC Architecture

graph TD
    subgraph "External Identity"
        AEntra["Azure Entra ID\n(MFA enforced)"]
        DevUser["Developer\n👤"]
        CI["CI/CD Pipeline\n🔧"]
    end

    subgraph "Kubernetes API Server"
        AuthN["Authentication\n(OIDC / Certificates)"]
        AuthZ["Authorization\n(RBAC)"]
        AC["Admission Control\n(Kyverno)"]
    end

    subgraph "Namespace: wiredbrain"
        SA1["ServiceAccount\nworkload-identity-sa\nautomountToken: false"]
        SA2["ServiceAccount\nkube-explorer"]
        Role1["Role\ndefault-pod-admin\nverbs: get,list,delete\nresources: pods"]
        RB1["RoleBinding\nkube-explorer-default"]
        CRB["ClusterRoleBinding\nview (read-only)"]
    end

    DevUser --> AEntra
    AEntra -->|"OIDC token (1h)"| AuthN
    CI -->|"ServiceAccount token"| AuthN
    AuthN --> AuthZ
    AuthZ --> AC
    SA2 --> RB1
    RB1 --> Role1
    DevUser -.->|"RoleBinding"| CRB

    style AEntra fill:#0078d4,color:#fff
    style AuthZ fill:#107c41,color:#fff
    style AC fill:#d83b01,color:#fff

Diagram 2: NetworkPolicy — zero-trust Wired Brain Coffee

graph LR
    subgraph "Namespace: wiredbrain"
        WEB["web-app\ncomponent: web-app"]
        PAPI["products-api\ncomponent: products-api\nport: 80"]
        SAPI["stock-api\ncomponent: stock-api\nport: 8080"]
        DB["database\ncomponent: database\nport: 5432"]
        DNS["kube-dns\nport: 53"]
    end

    WEB -->|"allow-web-to-products-api\ningress port 80"| PAPI
    WEB -->|"allow-web-to-stock-api\ningress port 8080"| SAPI
    PAPI -->|"allow-api-to-db\ningress port 5432"| DB
    SAPI -->|"allow-api-to-db\ningress port 5432"| DB
    WEB -.->|"BLOCKED\ndefault-deny"| DB

    WEB -->|"allow-dns\negress port 53"| DNS
    PAPI -->|"allow-dns\negress port 53"| DNS
    SAPI -->|"allow-dns\negress port 53"| DNS
    DB -->|"allow-dns\negress port 53"| DNS

    style DB fill:#d83b01,color:#fff
    style WEB fill:#107c41,color:#fff

Diagram 3: Supply Chain Security — Trivy + Kyverno

graph TD
    Dev["Developer\npush image"] --> Registry["Container Registry\nghcr.io/wiredbrain/"]
    Registry --> CI["CI/CD Pipeline\nTrivy scan integrated"]
    CI -->|"Clean image"| Deploy["kubectl apply\n(Deployment)"]
    Deploy --> Kyverno{"Kyverno\nAdmission Control"}

    Kyverno -->|"✅ Approved registry\n✅ runAsNonRoot: true"| Running["Pod Running"]
    Kyverno -->|"❌ Docker Hub\n❌ Runs as root"| Blocked["BLOCKED\n403 Forbidden"]

    Running --> TrivyOp["Trivy Operator\n(continuous scanning)"]
    TrivyOp --> Report["VulnerabilityReport\ncriticalCount: 0"]
    TrivyOp -->|"New CVE detected"| Alert["🚨 Alert\nKyverno Audit Log\nPrometheus metrics"]

    style Kyverno fill:#d83b01,color:#fff
    style Blocked fill:#a4262c,color:#fff
    style Running fill:#107c41,color:#fff
    style Alert fill:#ff8c00,color:#fff

Diagram 4: Secret Store CSI Driver — authentication flow

sequenceDiagram
    participant Pod
    participant CSI as CSI Driver
    participant KV as Azure Key Vault
    participant MI as Managed Identity (Azure)

    Pod->>CSI: Volume mount request\n(SecretProviderClass: database-credentials)
    CSI->>MI: Workload Identity token\n(from ServiceAccount annotation)
    MI->>KV: Auth via Managed Identity\n(no credentials in YAML)
    KV-->>CSI: Returns the encrypted secret
    CSI-->>Pod: Mounts the secret\nas /mnt/secrets/postgres-password
    Note over Pod: Reads the secret from\nthe filesystem\nNEVER stored in etcd

Diagram 5: Defense in Depth — global view

graph TB
    subgraph "Layer 1: Cluster Access"
        MFA["MFA\n(Azure Entra ID)"]
        RBAC_U["RBAC Users\n(least privilege)"]
        RBAC_SA["RBAC ServiceAccounts\n(automount: false)"]
    end

    subgraph "Layer 2: Deployment"
        Registry["Registry Policy\n(Kyverno Enforce)"]
        NonRoot["Non-Root Policy\n(Kyverno Enforce)"]
        CVEScan["CVE Scanning\n(Trivy Operator)"]
        CVEPolicy["CVE Policy\n(Kyverno Audit)"]
    end

    subgraph "Layer 3: Runtime"
        NetPol["NetworkPolicy\n(default-deny + rules)"]
        CSIDrv["CSI Driver\n(External Secrets)"]
        SecCtx["SecurityContext\n(seccomp, readOnly, no-privesc)"]
        Falco["Falco\n(Behavioral Detection)"]
    end

    subgraph "Cross-cutting"
        AuditLog["Kubernetes Audit Logs"]
        Prometheus["Prometheus Metrics\n(Trivy)"]
    end

    MFA --> Registry
    RBAC_U --> NonRoot
    RBAC_SA --> CVEScan
    Registry --> NetPol
    NonRoot --> CSIDrv
    CVEScan --> SecCtx
    CVEPolicy --> Falco

    NetPol --> AuditLog
    CSIDrv --> AuditLog
    Falco --> Prometheus

    style MFA fill:#0078d4,color:#fff
    style Registry fill:#107c41,color:#fff
    style NetPol fill:#d83b01,color:#fff
    style AuditLog fill:#5c2d91,color:#fff

Summary: Kubernetes security is not a checkbox — it is a continuous process. New vulnerabilities emerge, attack techniques evolve. The foundations covered in this course — RBAC with least privilege, continuous scanning, admission policies, zero-trust networking, and external secrets — form the operational Defense in Depth model for a modern organization.


Search Terms

configuring · managing · kubernetes · security · containers · rbac · authentication · csi · diagram · kyverno · trivy · application · architecture · azure · driver · networkpolicy · policy · secrets · access · brain · coffee · defense · deployment · depth

Interested in this course?

Contact us to book it or get a custom training plan for your team.