1. ConfigMaps
ConfigMaps store non-sensitive configuration as key-value pairs. They can be mounted as files or injected as environment variables.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
APP_ENV: production
LOG_LEVEL: info
config.yaml: |
server:
port: 8080
timeout: 30s
database:
pool_size: 20
Mount as environment variables
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
envFrom:
- configMapRef:
name: app-config # all keys become env vars
Mount as files
volumeMounts:
- name: config
mountPath: /etc/app/config.yaml
subPath: config.yaml
readOnly: true
volumes:
- name: config
configMap:
name: app-config
2. Secrets
Secrets store sensitive data (passwords, tokens, TLS certs). They are base64-encoded in etcd - not encrypted by default. Enable encryption at rest for production.
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData: # preferred - Kubernetes encodes to base64 for you
DB_USER: appuser
DB_PASSWORD: s3cr3tP@ss
DB_HOST: postgres.production.svc.cluster.local
Secret types
| Type | Use |
|---|---|
Opaque | Arbitrary user-defined data |
kubernetes.io/tls | TLS cert + key (tls.crt, tls.key) |
kubernetes.io/dockerconfigjson | Docker registry credentials |
kubernetes.io/service-account-token | Legacy SA token (avoid; use TokenRequest API) |
Encryption at rest
# /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 unencrypted secrets
External secret management
For production, prefer external secret stores integrated via:
- External Secrets Operator - syncs AWS Secrets Manager, GCP Secret Manager, Vault into K8s Secrets
- Sealed Secrets - encrypt Secrets for safe storage in Git
- HashiCorp Vault - inject secrets via Vault Agent sidecar or CSI driver
# External Secrets Operator example
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-credentials
data:
- secretKey: DB_PASSWORD
remoteRef:
key: production/db
property: password
3. ServiceAccounts
Every Pod runs as a ServiceAccount. It provides an identity for in-cluster API access and (optionally) cloud IAM via workload identity.
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-sa
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/api-pod-role
spec:
serviceAccountName: api-sa
automountServiceAccountToken: false # disable if Pod doesn't need K8s API access
default ServiceAccount. Never run production workloads with it - create dedicated ServiceAccounts with minimal permissions.
4. RBAC
Role-Based Access Control restricts who (subjects) can perform what actions (verbs) on which resources.
┌──────────────┐ bound by ┌──────────────┐ grants ┌──────────────┐
│ Subject │ ─────────▶ │ RoleBinding │ ───────▶ │ Role │
│ (User/Group/ │ │ │ │ (permissions)│
│ ServiceAcct)│ └──────────────┘ └──────────────┘
└──────────────┘
# ClusterRole + ClusterRoleBinding = cluster-wide
# Role + RoleBinding = namespace-scoped
Role (namespace-scoped)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list"]
RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production
subjects:
- kind: ServiceAccount
name: api-sa
namespace: production
- kind: Group
name: dev-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
ClusterRole for cluster-wide access
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-viewer
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
Common verbs
get, list, watch, create, update, patch, delete, deletecollection
# Check permissions
kubectl auth can-i create pods --namespace=production
kubectl auth can-i delete secrets --as=system:serviceaccount:production:api-sa
# Who can do what?
kubectl auth can-i --list --as=system:serviceaccount:production:api-sa
5. Pod Security Standards
Pod Security Admission (built-in since K8s 1.25) enforces three policy levels on namespaces:
| Level | Description |
|---|---|
privileged | Unrestricted - system workloads only |
baseline | Prevents known privilege escalations |
restricted | Heavily hardened - production apps |
# Label namespace for enforcement
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
Restricted-compliant Pod spec
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
capabilities:
drop: ["ALL"]
6. Admission Controllers & OPA
Admission controllers intercept API requests after authentication/authorization but before persistence. They can mutate or reject resources.
- Mutating: modify objects (inject sidecars, set defaults)
- Validating: reject non-compliant objects
OPA Gatekeeper
Gatekeeper enforces custom policies using Rego. Common use cases: require labels, block :latest tags, enforce resource limits.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
labels: ["app", "team", "env"]
Kyverno (alternative)
Kyverno uses Kubernetes-style YAML policies instead of Rego - easier for teams without OPA expertise.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-non-root
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Containers must run as non-root"
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
7. Pod Hardening Checklist
- Run as non-root user with
runAsNonRoot: trueand explicitrunAsUser - Drop all capabilities:
capabilities: { drop: ["ALL"] } - Disable privilege escalation:
allowPrivilegeEscalation: false - Read-only root filesystem with explicit writable
emptyDirmounts - Enable seccomp:
seccompProfile: { type: RuntimeDefault } - Set resource requests and limits on every container
- Use dedicated ServiceAccounts with minimal RBAC
- Disable automount of SA token if not needed
- Never store secrets in images or ConfigMaps
- Enable etcd encryption at rest and audit logging
- Apply NetworkPolicies to restrict east-west traffic
- Scan images for CVEs in CI (Trivy, Grype, Snyk)
- Pin image digests, not just tags
# Audit who accessed secrets (enable audit logging)
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
8. Cheat Sheet
| Task | Command |
|---|---|
| Create ConfigMap from file | kubectl create configmap app-config --from-file=config.yaml |
| Create Secret | kubectl create secret generic db --from-literal=password=secret |
| View Secret (decoded) | kubectl get secret db -o jsonpath='{.data.password}' | base64 -d |
| Check permissions | kubectl auth can-i create deployments -n production |
| List RBAC | kubectl get role,rolebinding -n production |
| PSA namespace label | kubectl label ns production pod-security.kubernetes.io/enforce=restricted |
Full cheat sheet: kubectl Cheat Sheet