Lesson  in  DevSecOps in Practice: Container & Kubernetes Security

Helm — Packaging Security Into Your App Template

Helm charts are the standard packaging format for Kubernetes applications. A well-designed chart bakes security defaults into its templates: non-root securityContext, resource limits, and NetworkPolicy are set correctly by default — making the insecure path require explicit opt-out. This lab creates a chart from scratch, codes security settings as required defaults, and uses helm template to dry-run before deploying.

Creating a Helm Chart with Security Defaults

Why Helm for Security?

Every deployment so far has been a raw YAML file. That works for one app. For ten apps across five environments, raw YAML leads to:

  • Copy-paste errors (insecure settings from App A get copied to App B)
  • Configuration drift (prod and staging diverge silently)
  • No security baseline (each team uses different securityContext settings)

A Helm chart solves all three: security settings live in values.yaml with secure defaults. Any team that deploys your chart gets security for free — removing a control requires explicitly overriding it.

Scaffold the Chart

cd ~
helm create secure-app
ls secure-app/

helm create generates a working chart with deployment, service, and helper templates already wired up. No need to write those — we just replace values.yaml with security-hardened defaults.

Bake Security into values.yaml

This is the key file. Every security setting lives here as a default:

cat > ~/secure-app/values.yaml << 'EOF'
replicaCount: 1

image:
  repository: public.ecr.aws/nginx/nginx
  tag: "1.25-alpine"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

resources:
  requests:
    cpu: "100m"
    memory: "64Mi"
  limits:
    cpu: "200m"
    memory: "128Mi"

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 101
  runAsGroup: 101
  seccompProfile:
    type: RuntimeDefault

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]
EOF

The generated deployment template already references .Values.podSecurityContext, .Values.securityContext, and .Values.resources — so these defaults flow into every pod automatically.

Dry Run with helm template

Render the chart locally without deploying — useful for reviewing what will be applied:

helm template secure-app ~/secure-app

Check that the security context values from values.yaml appear in the rendered output:

helm template secure-app ~/secure-app | grep -A5 "securityContext"

Lint the Chart

helm lint ~/secure-app

Deploying, Upgrading & Overriding Chart Defaults

Deploy the Chart

kubectl create namespace helm-demo

helm install secure-app ~/secure-app \
  --namespace helm-demo \
  --wait
# Check the release
helm list -n helm-demo
helm status secure-app -n helm-demo

# Check the deployed pods
kubectl get pods -n helm-demo
kubectl get deployment -n helm-demo

Verify Security Defaults Are Enforced

POD=$(kubectl get pods -n helm-demo -o jsonpath='{.items[0].metadata.name}')

# Confirm non-root
kubectl exec -n helm-demo "$POD" -- id

# Confirm read-only filesystem
kubectl exec -n helm-demo "$POD" -- sh -c "echo test > /etc/test.txt" 2>&1

# Confirm resource limits are set
kubectl get pod "$POD" -n helm-demo \
  -o jsonpath='{.spec.containers[0].resources}' | python3 -m json.tool

Expected:

uid=1000 gid=1000 groups=1000
sh: can't create /etc/test.txt: Read-only file system
{
    "limits": {"cpu": "200m", "memory": "128Mi"},
    "requests": {"cpu": "100m", "memory": "64Mi"}
}

Override Values at Deploy Time

The chart's values.yaml defines secure defaults — but they can be overridden per environment. Overriding should require justification:

# Override replica count for production
helm upgrade secure-app ~/secure-app \
  --namespace helm-demo \
  --set replicaCount=3 \
  --wait

kubectl get deployment -n helm-demo
# Override with a values file (for environment-specific config)
cat > ~/prod-values.yaml << 'EOF'
replicaCount: 3
resources:
  requests:
    cpu: "200m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "256Mi"
EOF
helm upgrade secure-app ~/secure-app \
  --namespace helm-demo \
  --values ~/prod-values.yaml \
  --wait

Helm Rollback

If an upgrade breaks something, roll back to any previous revision:

# See all revisions
helm history secure-app -n helm-demo

# Rollback to revision 1
helm rollback secure-app 1 -n helm-demo --wait

helm history secure-app -n helm-demo

Rollback is a new revision (not a destructive operation) — the history is preserved.

The Security-as-Code Pattern

Chart values.yaml
  securityContext.runAsNonRoot: true     ← secure by default
  containerSecurityContext.caps.drop: ALL ← secure by default
  resources.limits.cpu: "200m"           ← secure by default

To remove a security control, a developer must:
  1. Explicitly set the value to false/empty in their values.yaml
  2. Commit the change
  3. Get it reviewed
  4. Merge it
  5. Deploy it

The insecure path requires MORE work than the secure path.

This is the design principle: make the secure choice the default, make insecurity require deliberate effort.

Cleanup

helm uninstall secure-app -n helm-demo
kubectl delete namespace helm-demo