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
- 2. Securing Application Deployment
- 3. Securing Applications at Runtime
- 4. Reference Tables
- 5. Architecture Diagrams
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
| Resource | Scope | Description |
|---|---|---|
Role | Namespace | Set of rules (verbs + resources) within a single namespace |
RoleBinding | Namespace | Binds a Role to a principal (ServiceAccount, User, Group) |
ClusterRole | Cluster-wide | Permissions on cluster-wide resources (nodes, namespaces) or reusable template |
ClusterRoleBinding | Cluster-wide | Binds a ClusterRole to a principal for the entire cluster |
Common Verbs
| Verb | Description |
|---|---|
get | Read a specific resource |
list | List all resources |
create | Create a new resource |
update | Modify an existing resource |
patch | Partial modification |
delete | Delete a resource |
watch | Observe changes in real time |
Important Predefined ClusterRoles
| ClusterRole | Permissions |
|---|---|
view | Read-only access to most resources (not Secrets) |
edit | Read/write, without RBAC management |
admin | Full read/write within a namespace, including RBAC management |
cluster-admin | Full access to the entire cluster — use with extreme caution |
Danger:
cluster-adminon thedefaultServiceAccountmeans 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
| Method | Advantages | Disadvantages |
|---|---|---|
| X.509 Certificates | Works everywhere, simple | No individual revocation, no MFA, long validity |
| Static tokens | Simple to configure | Equivalent to a plaintext password file |
| OpenID Connect (OIDC) | MFA, short-lived tokens, IdP integration | Requires additional configuration |
| Azure Entra ID (AKS) | Enforced MFA, temporary tokens, centralized management | Azure-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
kubectlcall triggers an OAuth2 flow viakubelogin - 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
| Moment | Situation |
|---|---|
| Initial deployment | Image scanned → clean, 0 critical CVEs |
| Day +30 | New CVE discovered in OpenSSL → image vulnerable |
| Day +45 | OpenSSL updated → incompatibility with the framework |
| Day +60 | Framework 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
Enforcefor 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
| Tool | Role | Mode |
|---|---|---|
| Trivy Operator | Continuous scanning of running images | Detection / Audit |
| Kyverno (Enforce) | Blocks non-compliant configs (registry, non-root) | Prevention |
| Kyverno (Audit) | Logs CVE violations without blocking | Detection |
| Falco (CNCF) | Behavioral detection at runtime (suspicious syscalls) | Runtime detection |
| Kubernetes Audit Logs | Full 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:
| Problem | Detail |
|---|---|
| Encoding ≠ encryption | Base64 is immediately reversible (base64 -d) |
| etcd storage | Often not encrypted at-rest without explicit configuration |
| Developer access | Any developer with kubectl get secret -o yaml can read credentials |
| Environment variables | kubectl exec pod -- printenv reveals secrets in plaintext |
| Source control | Helm 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
| Layer | Tool / Mechanism | Protection |
|---|---|---|
| Cluster Access | Azure Entra ID + OIDC | MFA, short-lived tokens |
| Cluster Access | RBAC (Roles, ClusterRoles) | Least privilege per namespace |
| Cluster Access | automountServiceAccountToken: false | No API access by default |
| Deployment | Trivy Operator | Continuous CVE scanning |
| Deployment | Kyverno (Enforce) | Blocks unapproved registry + running as root |
| Deployment | Kyverno (Audit) | Logs critical CVEs |
| Runtime | NetworkPolicy (default-deny + allow rules) | Zero-trust networking |
| Runtime | Secret Store CSI Driver | No secrets in etcd |
| Runtime | Workload Identity | Vault auth without credentials in YAML |
| Runtime | securityContext (seccomp, readOnly, no-privesc) | Reduced attack surface |
4. Reference Tables
Comparison: Native Kubernetes Secrets vs CSI Driver
| Criterion | Native Kubernetes Secrets | Secret Store CSI Driver |
|---|---|---|
| Storage | etcd (base64) | Azure Key Vault / AWS / Vault |
| Encryption at-rest | Optional, often disabled | Always encrypted |
| Revocation | Manual (delete the secret) | Immediate in the vault |
| Audit trail | Kubernetes audit logs | Key Vault/Vault logs (detailed) |
| Rotation | Redeployment required | Automatic rotation possible |
| Developer access | kubectl get secret = credentials in plaintext | Separate IAM access |
Comparison: end-user authentication methods
| Method | MFA | Token lifetime | Revocable | Recommended |
|---|---|---|---|---|
| X.509 Certificate | No | Years | No (unless CA rotation) | ❌ No |
| Static bearer token | No | Infinite | Yes (API server restart) | ❌ No |
| OIDC (OpenID Connect) | Yes (via IdP) | Minutes–hours | Yes (IdP) | ✅ Yes |
| Azure Entra ID (AKS) | Yes | ~1h | Yes | ✅ Yes |
Kyverno: validationFailureAction — when to use which
| Action | Behavior | Use case |
|---|---|---|
Enforce | Blocks creation/update | Approved registry, non-root, forbidden capabilities |
Audit | Allows but logs the violation | CVEs, 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