Config, Secrets & Security

Inject configuration safely, control who can do what, and harden Pods against compromise.

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
Hot reload: ConfigMap updates do not automatically restart Pods. Use a sidecar (Reloader, Stakater Reloader) or roll out a new Deployment revision to pick up changes.

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

TypeUse
OpaqueArbitrary user-defined data
kubernetes.io/tlsTLS cert + key (tls.crt, tls.key)
kubernetes.io/dockerconfigjsonDocker registry credentials
kubernetes.io/service-account-tokenLegacy 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 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: Every namespace has a 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:

LevelDescription
privilegedUnrestricted - system workloads only
baselinePrevents known privilege escalations
restrictedHeavily 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.

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

# 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

TaskCommand
Create ConfigMap from filekubectl create configmap app-config --from-file=config.yaml
Create Secretkubectl create secret generic db --from-literal=password=secret
View Secret (decoded)kubectl get secret db -o jsonpath='{.data.password}' | base64 -d
Check permissionskubectl auth can-i create deployments -n production
List RBACkubectl get role,rolebinding -n production
PSA namespace labelkubectl label ns production pod-security.kubernetes.io/enforce=restricted

Full cheat sheet: kubectl Cheat Sheet