Introduction
A single misconfigured Kubernetes cluster can expose your entire infrastructure in minutes. In 2025 alone, over 60% of organizations reported at least one Kubernetes security incident — and the majority traced back to preventable misconfigurations, not zero-day exploits.
Kubernetes ships with minimal security defaults. Everything is open. Pods can talk to each other freely. Service accounts carry cluster-admin privileges by default. Secrets sit in etcd unencrypted. If you deploy a vanilla cluster and walk away, you are effectively running with the doors unlocked.
This guide walks through 10 hardened security layers you must implement in 2026. Each section includes real, copy-paste-ready YAML manifests and CLI commands. Whether you run EKS, GKE, AKS, or bare-metal kubeadm clusters, these practices apply universally.
Who this is for: DevOps engineers, SREs, platform engineers, and anyone responsible for production Kubernetes clusters. You should be comfortable with kubectl and basic YAML. No prior security specialization required.
What you will implement by the end of this guide:
- Fine-grained RBAC with least-privilege principles
- Pod Security Admission replacing deprecated PSPs
- Zero-trust network policies with default-deny
- Image vulnerability scanning with Trivy in CI/CD
- Encrypted secrets management with Sealed Secrets and External Secrets Operator
- Runtime threat detection with Falco
- Supply chain integrity with Sigstore and signed images
Let's lock it down.
1. RBAC: Least Privilege from Day One
Role-Based Access Control (RBAC) is your first and most important line of defense. The principle is simple: every user, service account, and application should have exactly the permissions it needs — nothing more.
The Problem with Default RBAC
Out of the box, Kubernetes grants the system:masters group (used by kubeadm init) cluster-admin. Every default service account in the kube-system namespace gets elevated privileges. The default service account in every namespace exists automatically and — unless you explicitly bind it — has no permissions. But many teams accidentally grant it broad access during development and forget to revoke it.
Here is the most common anti-pattern we see in production audits:
# BAD: cluster-admin bound to default service account
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dangerous-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: default
namespace: default
This gives every pod in the default namespace unrestricted control over the entire cluster. If any pod gets compromised, the attacker owns everything.
RBAC Best Practices
1. Use namespace-scoped Roles, not ClusterRoles, whenever possible.
A Role is bound to a single namespace. A ClusterRole is cluster-wide. Most applications only need access to resources in their own namespace.
# GOOD: namespace-scoped Role for a web application
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: webapp-role
namespace: production
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update"]
2. Create a dedicated ServiceAccount for every application.
Never use the default service account. Always create a named ServiceAccount and bind it to a specific Role.
apiVersion: v1
kind: ServiceAccount
metadata:
name: webapp-sa
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: webapp-binding
namespace: production
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: webapp-role
subjects:
- kind: ServiceAccount
name: webapp-sa
namespace: production
Then reference it in your Deployment:
spec:
serviceAccountName: webapp-sa
automountServiceAccountToken: true
3. Avoid wildcard verbs and resources.
Every * in your RBAC rules is a potential escalation path. Be specific:
# BAD
verbs: ["*"]
resources: ["*"]
# GOOD: explicit
verbs: ["get", "list", "watch"]
resources: ["pods", "services"]
4. Use kubectl auth can-i to verify permissions.
Before deploying, test what a service account can actually do:
kubectl auth can-i create deployments \
--as=system:serviceaccount:production:webapp-sa \
--namespace=production
5. Separate human users from machine accounts.
Use OIDC (OpenID Connect) for human authentication. Service accounts are for pods and CI/CD pipelines. Map OIDC groups to Kubernetes roles — never give individual users cluster-admin. Tools like Dex, Keycloak, or cloud-provider IAM (aws-iam-authenticator for EKS, GCP IAM for GKE) handle this cleanly.
6. Audit your RBAC regularly.
Run this one-liner to find overly permissive bindings:
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'
You will be surprised how many cluster-admin bindings accumulate over time. RBAC auditing tools like kubescape, kube-bench, or popeye can automate this check.
RBAC for CI/CD Pipelines
CI/CD systems (GitHub Actions, GitLab CI, ArgoCD) need API access to deploy. The pattern: create a ServiceAccount with the minimum permissions required for deployments, extract its token, and inject it into your pipeline secrets.
kubectl create serviceaccount cicd-deployer -n production
kubectl create rolebinding cicd-deployer-binding \
--role=webapp-role \
--serviceaccount=production:cicd-deployer \
-n production
# For Kubernetes 1.24+, create a long-lived token:
kubectl create token cicd-deployer -n production --duration=8760h
Store that token in your CI secrets manager — never in source code.
2. Pod Security Standards & Pod Security Admission
Pod Security Policies (PSPs) were deprecated in Kubernetes 1.21 and removed in 1.25. The replacement is Pod Security Admission (PSA) — a built-in admission controller that enforces Pod Security Standards at the namespace level.
The Three Pod Security Standards
| Standard | Description | Key Restrictions | |----------|-------------|------------------| | Privileged | Unrestricted. Equivalent to no policy. | None. Use only for system namespaces. | | Baseline | Prevents known privilege escalations. Minimum for production. | No hostNetwork, hostPID, hostIPC, hostPorts, privileged containers, or hostPath volumes. | | Restricted | Hardened following Pod hardening best practices. | Everything Baseline restricts, plus: must run as non-root, seccomp profile required, capabilities dropped to NET_BIND_SERVICE only, read-only root filesystem. |
Enforcing PSA with Namespace Labels
PSA uses namespace labels — no separate CRD needed. Apply a label to any namespace:
# Enforce the restricted policy on a namespace
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted
# Also set audit and warn modes for visibility
kubectl label namespace production \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
Three enforcement modes exist for each label:
- enforce: reject pods that violate the policy
- audit: allow but log violations to the audit log
- warn: allow but show a warning to the user
Gradual rollout strategy: Start with warn and audit modes for a week. Fix all warnings. Then switch to enforce. Never jump straight to enforce=restricted on existing production namespaces — you will break running workloads.
Writing a Restricted-Compliant Pod
Here is a pod that passes the restricted Pod Security Standard:
apiVersion: v1
kind: Pod
metadata:
name: secure-nginx
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginx:1.25-alpine
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: nginx-cache
mountPath: /var/cache/nginx
volumes:
- name: tmp
emptyDir: {}
- name: nginx-cache
emptyDir: {}
Key points:
runAsNonRoot: true— the container must not run as UID 0seccompProfile: RuntimeDefault— blocks dangerous syscalls by defaultcapabilities.drop: ["ALL"]— strip all Linux capabilitiesreadOnlyRootFilesystem: true— attackers cannot write to the filesystemallowPrivilegeEscalation: false— no setuid binaries
Exemptions (When You Need Them)
Some system workloads genuinely need privileged access — CNI plugins, CSI drivers, monitoring agents. Use namespace exemptions:
# In kube-apiserver.yaml
apiVersion: v1
kind: Pod
spec:
containers:
- command:
- kube-apiserver
- --admission-control-config-file=/etc/kubernetes/admission.yaml
And in the admission configuration:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: "restricted"
exemptions:
namespaces: ["kube-system", "cert-manager", "ingress-nginx"]
usernames: ["system:serviceaccount:kube-system:calico-node"]
3. Network Policies: Zero-Trust Inside the Cluster
Kubernetes networking is flat by default. Every pod can reach every other pod in the cluster, across all namespaces — with zero built-in filtering. If one pod gets compromised, it can scan your entire internal network, hit internal APIs, and pivot to databases.
Network Policies are your internal firewall. They are Kubernetes-native resources that control traffic flow at Layers 3 and 4 (IP and port level). Think of them as security group rules for pods.
Default-Deny: Lock Everything First
Start by denying all ingress and egress traffic. Then selectively open only what each application needs:
# Default deny all ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # selects all pods
policyTypes:
- Ingress
# Default deny all egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
With these two policies in place, no pod can receive or initiate traffic. Then layer on allow rules for specific flows:
# Allow frontend → backend on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Real-World Zero-Trust Policy Patterns
Pattern 1: Allow only from the ingress controller
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
- podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
Pattern 2: Allow DNS egress only (block everything else)
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
- podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
Pattern 3: Allow egress only to specific external IPs
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/8 # internal VPC only
except:
- 10.0.0.0/28 # except management subnet
Cilium: Beyond Basic Network Policies
If your CNI is Cilium (increasingly common in 2026 for eBPF-based networking), you get Layer 7 policies — filtering by HTTP method, path, or DNS name:
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: l7-policy
spec:
endpointSelector:
matchLabels:
app: api
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: "GET"
path: "/api/v1/.*"
Layer 7 policies let you declare: only GET requests to /api/v1/ endpoints are allowed — no POST, no DELETE, no /admin. A compromised frontend pod cannot abuse backend endpoints it should not access.
Testing Network Policies
Use netshoot (a network debugging container) to verify connectivity:
kubectl run netshoot --rm -it --image nicolaka/netshoot -- /bin/bash
# From inside, test connectivity:
curl backend-service.production.svc.cluster.local:8080
# Test DNS:
nslookup kubernetes.default
Always test both positive (traffic that should flow) and negative (traffic that should be blocked) cases. Network policies are easy to misconfigure — a single missing label selector can leave a hole wide open.
4. Image Security: Scan Every Container with Trivy
Running containers from untrusted or unverified images is the most common entry point for supply-chain attacks. In 2024, a compromised xz-utils backdoor nearly made it into production containers worldwide. In 2025, multiple malicious NPM and PyPI packages were found embedded in popular Docker images.
The rule: every image that enters your cluster must be scanned. Trivy, by Aqua Security, is the de-facto open-source scanner — fast, comprehensive, and CI/CD-friendly. It scans OS packages, language dependencies, and misconfigurations in a single pass.
Scanning Images
trivy image nginx:1.25-alpine
# Filter by severity — only flag HIGH and CRITICAL
trivy image --severity HIGH,CRITICAL nginx:1.25-alpine
# Output as JSON for pipeline integration
trivy image --format json --output trivy-report.json myapp:latest
# Scan filesystem for IaC misconfigurations
trivy config ./kubernetes/
Integrating Trivy into CI/CD
The most effective pattern: scan in your pipeline and block deployments for HIGH or CRITICAL findings.
GitHub Actions — Trivy scan step:
- name: Scan container image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: 1
GitLab CI — Trivy scan job:
trivy-scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false
Trivy Operator: Continuous Cluster Scanning
For runtime scanning of images already deployed in your cluster, install the Trivy Operator:
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm install trivy-operator aqua/trivy-operator \
--namespace trivy-system \
--create-namespace \
--set trivy.ignoreUnfixed=true
Query vulnerability reports across your namespaces:
kubectl get vulnerabilityreports -n production
kubectl describe vulnerabilityreport replicaset-myapp-7d4f8b9c6d-nginx
Image Pinning and Digest-Based References
Never use floating tags like :latest in production. Pin to a content-addressable digest:
# BAD: floating tag, can change underneath you
image: nginx:latest
# GOOD: digest pinning, immutable reference
image: nginx@sha256:aed492c4d72c4a4e2f4d7d5e1f3b6c8a9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4
Extract the digest of any image:
docker inspect nginx:1.25-alpine | jq -r '.[0].RepoDigests[0]'
Combine digest pinning with automated PRs from Renovate or Dependabot to keep digests updated without manual toil. For an even tighter image supply chain, read our guide on Docker Multi-Stage Builds to minimize your attack surface by shrinking images by up to 90%.
5. Secrets Management: Never Store Secrets in Plaintext
Kubernetes Secrets are base64-encoded, not encrypted. By default, they sit in etcd unencrypted. Anyone with kubectl get secrets -o yaml can decode them in one command:
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
This is not security — it is obfuscation. Here are the production-grade alternatives.
Encrypting Secrets at Rest
Enable encryption at rest in your kube-apiserver configuration. This encrypts all Secrets before they are written to etcd:
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # fallback for reading old unencrypted secrets
Then point your kube-apiserver to it:
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
Restart the apiserver and rewrite all existing secrets:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Sealed Secrets: GitOps-Friendly Encryption
Sealed Secrets by Bitnami solves a critical problem: storing encrypted secrets in Git. The controller runs in your cluster and holds the private key. You encrypt secrets client-side with kubeseal, generating a SealedSecret CRD that is safe to commit:
# Install the controller
helm install sealed-secrets sealed-secrets/sealed-secrets --namespace kube-system
# Create a standard secret
kubectl create secret generic db-creds --from-literal=username=admin --from-literal=password=s3cr3t --dry-run=client -o yaml | kubeseal --format yaml > sealed-db-creds.yaml
# sealed-db-creds.yaml is now safe to commit to Git
The SealedSecret can only be decrypted by the controller in your specific cluster. Even if someone steals your Git repo, they cannot decrypt the secrets without access to the cluster's private key.
External Secrets Operator: Sync from Vaults
For organizations using external secret managers (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault), the External Secrets Operator (ESO) syncs secrets into Kubernetes automatically:
helm install external-secrets external-secrets/external-secrets --namespace external-secrets-system --create-namespace
Define a SecretStore pointing to your provider:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-sa
Then an ExternalSecret that maps a cloud secret to a Kubernetes Secret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: prod/database
property: password
ESO keeps the Kubernetes Secret in sync — if you rotate credentials in AWS, the pod sees the new value within one hour (or your configured refreshInterval).
6. Runtime Security: Detect Threats with Falco
Preventive controls (RBAC, network policies, image scanning) are your first line of defense. But you also need detection — something watching for anomalies at runtime. Falco, the CNCF-graduated runtime security tool, fills this gap.
Falco hooks into the Linux kernel via eBPF (or a kernel module) and monitors every syscall. It ships with 80+ built-in rules that detect:
- Shell spawned inside a container
- Unexpected outbound network connections
- Reading sensitive files (
/etc/shadow, private keys) - Privileged container creation
- Writing to unexpected binary directories
Installing Falco
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco --namespace falco --create-namespace --set falcosidekick.enabled=true --set falcosidekick.webui.enabled=true --set driver.kind=ebpf
Custom Falco Rules
Define custom rules for your specific application behavior:
- rule: Unauthorized Database Access
desc: Detect connections to database from non-application pods
condition: >
evt.type=connect and
fd.sport=5432 and
container.id != "" and
not k8s.pod.labels.app in (allowed_apps)
output: >
Pod %k8s.pod.name in namespace %k8s.ns.name
connected to PostgreSQL (user=%proc.name fd=%fd.name)
priority: CRITICAL
tags: [database, network]
Falco Output Channels
Falco can send alerts to: stdout (default), syslog, Slack, PagerDuty, Opsgenie, Prometheus Alertmanager, webhooks, AWS SNS, and more — configured through Falcosidekick. Integrate Falco alerts into your existing incident response pipeline so that a shell spawned in a production container triggers the same pager as a service outage.
Kubernetes Audit Logging
Complement Falco with Kubernetes audit logging. The audit log records every API server request:
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
verbs: ["create", "update", "patch", "delete"]
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: RequestResponse
verbs: ["create", "update", "patch"]
resources:
- group: "rbac.authorization.k8s.io"
Ship audit logs to a centralized SIEM or log aggregator (Loki, Elasticsearch, Splunk). Many compliance frameworks (SOC 2, PCI DSS, HIPAA) explicitly require Kubernetes audit logging.
7. Supply Chain Security: Sign Your Images with Sigstore
A scanned image without a verified signature is like a vaccine passport without an identity check — you know what is in it, but you do not know who built it or whether it has been tampered with since scanning.
Sigstore (the project behind cosign) provides cryptographic signing and verification of container images — without requiring a central key server. It uses short-lived, certificate-based signatures tied to OIDC identities.
Sign Images with Cosign
# Sign an image using GitHub Actions OIDC identity
cosign sign --oidc-issuer https://token.actions.githubusercontent.com --identity https://github.com/myorg/myrepo/.github/workflows/ci.yml@refs/heads/main myregistry.io/myapp:v1.2.3
This produces a signature stored in the same OCI registry, next to your image. No long-lived signing keys to manage or rotate.
Verify Signatures Before Deployment
Add signature verification to your admission pipeline. With Kyverno (a policy engine for Kubernetes), you can block unsigned images at admission time:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
rules:
- name: check-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.io/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*"
issuer: "https://token.actions.githubusercontent.com"
Any pod referencing an unsigned image from your registry is rejected before scheduling.
SBOM: Know What Is Inside
Pair signing with Software Bill of Materials (SBOM). Sigstore integrates with Syft to generate and attach SBOMs:
# Generate SBOM
syft myregistry.io/myapp:v1.2.3 -o spdx-json > sbom.json
# Attach to image
cosign attach sbom --sbom sbom.json myregistry.io/myapp:v1.2.3
Now you have a cryptographically signed, verifiable manifest of every package in your image — essential for vulnerability triage when CVEs like Log4Shell drop.
8. Kubernetes Security Checklist
| Layer | Control | Tool / Mechanism | Priority |
|-------|---------|------------------|----------|
| Authentication | OIDC for humans, ServiceAccounts for machines | Dex, Keycloak, cloud IAM | HIGH |
| RBAC | Least-privilege Roles, no wildcards, per-app ServiceAccounts | Native RBAC | CRITICAL |
| Pod Security | Enforce restricted PSA on all non-system namespaces | Pod Security Admission | CRITICAL |
| Network | Default-deny NetworkPolicies, allow-list specific flows | Calico/Cilium NetworkPolicy | HIGH |
| Image Security | Scan all images, block HIGH+CRITICAL, pin digests | Trivy + Trivy Operator | CRITICAL |
| Secrets | Encrypt at rest, use Sealed Secrets or ESO | EncryptionConfiguration, Sealed Secrets, ESO | HIGH |
| Runtime | Detect anomalies with Falco, enable audit logging | Falco + Falcosidekick | HIGH |
| Supply Chain | Sign images, verify at admission, generate SBOMs | Cosign, Kyverno, Syft | MEDIUM |
| Node Security | CIS benchmarks, minimal host OS, regular patching | kube-bench, Talos, Bottlerocket | HIGH |
| Policy as Code | Codify and enforce all security policies | Kyverno or OPA/Gatekeeper | MEDIUM |
| Backup & DR | Encrypted etcd backups, tested restore procedures | Velero + etcd snapshots | HIGH |
| Monitoring | Centralize logs, metrics, and audit trails | Loki, Prometheus, Grafana | MEDIUM |
Start from the top. Each CRITICAL item is a single configuration mistake away from a breach. HIGH items significantly reduce your blast radius if something does get through.
9. Conclusion
Kubernetes security is not a feature you bolt on at the end. It is the foundation. A cluster without RBAC, network policies, and image scanning is not "insecure by default" — it is wide open.
The good news: every control in this guide is open-source, battle-tested, and deployable today. You do not need a vendor contract or a dedicated security team. You need discipline:
- Start with RBAC. Audit cluster-admin bindings first.
- Lock down Pod Security. Enforce
restrictedwith a gradual rollout. - Default-deny network. Then allow what your apps actually need.
- Scan every image. Block HIGH and CRITICAL in CI/CD.
- Encrypt secrets. Stop storing passwords in base64.
- Deploy Falco. Know the moment something anomalous happens.
- Sign your images. Verify provenance, not just content.
Security is iterative. Pick one control, implement it across your clusters, verify it works, move to the next. A partially secured cluster is infinitely better than an open one.
Further Reading
- Docker Multi-Stage Builds: Slash Your Image Size by 90% — Reduce attack surface by shipping smaller images
- Error Budgets: Stop Wasting Your SRE Team's Time — Balance security velocity with reliability targets
- Kubernetes Pod Autoscaling: HPA, VPA, and KEDA Explained — Scale securely without over-provisioning
- Kubernetes Mistakes That Cost Companies Millions — Real-world incidents and how security controls prevent them
- Kubernetes Monitoring with Prometheus and Grafana — The monitoring stack that feeds your security dashboards