Helm — Packaging Security Into Your App Template
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
- Previous lesson
- Policy-as-Code with Kyverno