Securing Kubernetes Pods: A Complete Guide to Pod-Level Security Configuration


Securing Kubernetes Pods: A Complete Guide to Pod-Level Security Configuration
When deploying applications to Kubernetes, security should be your top priority, not an afterthought. While Kubernetes provides numerous security features at the cluster and network level, many critical security configurations happen right at the pod level—in your YAML manifests. A single misconfigured pod can become the entry point for attackers or cause significant security vulnerabilities.
The Tesla Cryptojacking Attack
In 2018, Tesla discovered that attackers had compromised their Kubernetes infrastructure and were using it to mine cryptocurrency. The breach happened because their Kubernetes console was exposed to the internet without password protection. Once inside, the attackers deployed cryptomining containers that silently consumed Tesla's cloud resources.
What makes this particularly relevant to pod security? The attackers specifically:
- Deployed pods without resource limits, allowing them to consume as much CPU as possible
- Configured the mining software to run at low utilization to avoid detection
- Used the compromised environment to access AWS credentials stored insecurely
This attack could have been prevented or detected early with proper pod-level security configurations: resource limits would have constrained the mining pods, proper secret management would have protected the AWS credentials, and security contexts would have limited what the containers could access.
The lesson is clear: Pod security isn't just about best practices—it's about preventing real-world attacks that can cost organizations millions in cloud bills and reputational damage.
This comprehensive guide covers every security configuration you should implement in your pod specifications to ensure your workloads run securely by default.
Security Context: The Foundation of Pod Security
The securityContext is your primary tool for implementing security controls at the pod and container level. It allows you to configure various security-related settings that govern how your containers run.
Running as Non-Root User
Never run containers as root unless absolutely necessary. Most applications don't need root privileges and running as root violates the principle of least privilege.
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: app
image: myapp:v1.2.3
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
Key configurations:
runAsNonRoot: true- Ensures the container cannot run as rootrunAsUser: 1000- Specifies the user ID to run the containerrunAsGroup: 1000- Specifies the primary group IDfsGroup: 1000- Sets the group for volume ownership
Preventing Privilege Escalation
Privilege escalation occurs when a process gains higher privileges than it was originally granted, often exploiting vulnerabilities to gain root or administrative access. In containers, this can happen when a process uses setuid binaries, capabilities, or other mechanisms to gain elevated permissions. Preventing privilege escalation is crucial because it stops attackers from gaining root access even if they compromise your application.
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Only if you need to bind to ports < 1024
allowPrivilegeEscalation: false- Prevents processes from gaining more privileges than their parentcapabilities.drop: [ALL]- Removes all Linux capabilities- Only add specific capabilities your application needs
Read-Only Root Filesystem
Making the root filesystem read-only prevents malicious code from modifying system files:
securityContext:
readOnlyRootFilesystem: true
When using read-only filesystems, mount writable volumes for directories that need write access:
volumeMounts:
- name: tmp-volume
mountPath: /tmp
- name: var-log
mountPath: /var/log
volumes:
- name: tmp-volume
emptyDir: {}
- name: var-log
emptyDir: {}
Linux Capabilities
Linux capabilities provide fine-grained privilege control. Always follow the principle of least privilege:
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Bind to ports < 1024
- CHOWN # Change file ownership (if needed)
# Only add capabilities your application actually needs
Common capabilities and their use cases:
NET_BIND_SERVICE- Bind to privileged ports (< 1024)NET_ADMIN- Network administration (usually not needed)SYS_TIME- Set system time (usually not needed)CHOWN- Change file ownershipDAC_OVERRIDE- Bypass file permission checks (dangerous)
Using Secrets Instead of Plain Text
Never store sensitive information in plain text in your pod specifications. Always use Kubernetes Secrets for passwords, API keys, certificates, and other sensitive data.
Bad Practice (DON'T DO THIS):
# ❌ NEVER DO THIS
containers:
- name: app
image: myapp:v1.2.3
env:
- name: DATABASE_PASSWORD
value: "my-super-secret-password"
- name: API_KEY
value: "sk-1234567890abcdef"
Better Practice:
First, create a Secret:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
database-password: bXktc3VwZXItc2VjcmV0LXBhc3N3b3Jk # base64 encoded
api-key: c2stMTIzNDU2Nzg5MGFiY2RlZg== # base64 encoded
Then reference it in your pod:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
containers:
- name: app
image: myapp:v1.2.3
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: database-password
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secrets
key: api-key
Using Secrets as Volumes
For configuration files or certificates, mount secrets as volumes:
containers:
- name: app
image: myapp:v1.2.3
volumeMounts:
- name: app-secrets
mountPath: /etc/secrets
readOnly: true
volumes:
- name: app-secrets
secret:
secretName: app-secrets
defaultMode: 0400 # Read-only for owner only
Important: Kubernetes Secrets Are Not Really "Secret"
A critical caveat: While Kubernetes Secrets are better than plain text environment variables, they're not truly secure by default. Here's why:
- Base64 is encoding, not encryption - Secrets are only base64 encoded, which anyone can easily decode
- Stored in etcd - By default, secrets are stored unencrypted in etcd (though encryption at rest can be enabled)
- Accessible via API - Anyone with API access can retrieve secrets
- Visible in pod specs - Secrets mounted as environment variables can be seen by anyone who can describe the pod
What we've shown here is simply a better alternative to plain text - it prevents secrets from being accidentally committed to Git or visible in logs, but it's not a complete security solution.
For production environments, you need proper secret management solutions like:
- HashiCorp Vault - Centralized secret management with encryption and access control
- External Secrets Operator - Sync secrets from external systems into Kubernetes
- Cloud provider secret managers - AWS Secrets Manager, Azure Key Vault, GCP Secret Manager
- Sealed Secrets - Encrypt secrets so they can be safely stored in Git
Since secret management is a comprehensive topic that deserves its own deep dive, we'll cover enterprise-grade secret management strategies, encryption at rest, secret rotation, and integration with external secret stores in a dedicated article. For now, just remember: Kubernetes Secrets are better than plain text, but they're not the final answer for production security.
Container Image Security
Your container images are a critical attack surface. Implement these practices to ensure image security.
Never Use Latest Tags
Always pin specific image versions instead of using latest tags:
# ❌ DON'T DO THIS
containers:
- name: app
image: myapp:latest
# ✅ DO THIS
containers:
- name: app
image: myapp:v1.2.3
# Or use digest for immutability
# image: myapp@sha256:abc123...
Why avoid latest?
- Unpredictable deployments
- Security vulnerabilities in newer versions
- Difficult to reproduce issues
- No rollback strategy
Use Minimal Base Images
Choose secure, minimal base images:
# ✅ Good choices FROM gcr.io/distroless/java17-debian11:latest FROM alpine:3.18 FROM scratch # For static binaries # ❌ Avoid if possible FROM ubuntu:latest FROM centos:latest
Image Pull Policies
Control when images are pulled:
containers:
- name: app
image: myapp:v1.2.3
imagePullPolicy: Always # Always pull (for security updates)
# or
imagePullPolicy: IfNotPresent # Only pull if not present locally
Using Private Registries
For sensitive applications, use private registries:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
imagePullSecrets:
- name: private-registry-secret
containers:
- name: app
image: private-registry.company.com/myapp:v1.2.3
Create the image pull secret:
kubectl create secret docker-registry private-registry-secret \ --docker-server=private-registry.company.com \ --docker-username=username \ --docker-password=password \ --docker-email=email@company.com
Resource Limits and Security
Resource limits are a security feature that prevents resource exhaustion attacks and ensures fair resource sharing.
CPU and Memory Limits
Always set resource requests and limits:
containers:
- name: app
image: myapp:v1.2.3
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Ephemeral Storage Limits
Limit temporary storage to prevent disk space attacks:
resources:
requests:
ephemeral-storage: "1Gi"
limits:
ephemeral-storage: "2Gi"
Why Resource Limits Matter for Security
- Prevent DoS attacks: Malicious code can't consume all resources
- Limit blast radius: Compromised containers can't affect other workloads
- Predictable behavior: Applications behave consistently across environments
- Cost control: Prevent unexpected resource usage costs
Linux Security Modules Integration
Kubernetes integrates with Linux security modules like SELinux, AppArmor, and seccomp to provide additional security layers.
SELinux Integration
What is SELinux? Security-Enhanced Linux (SELinux) is a Linux kernel security module that provides mandatory access control (MAC). Unlike traditional discretionary access control (DAC) where users control access to their own files, SELinux enforces system-wide security policies that even root users must follow. Think of it as an additional layer of protection where every process and file has a security label, and the kernel enforces rules about what can access what.
Why use SELinux with Kubernetes? Even if a container is compromised, SELinux restricts what the attacker can do by enforcing strict policies about file access, network operations, and inter-process communication. It's like having a security guard that checks permissions even after someone has the keys.
apiVersion: v1
kind: Pod
metadata:
name: selinux-secured-app
spec:
securityContext:
seLinuxOptions:
level: "s0:c123,c456" # Multi-Level Security (MLS) level
role: "object_r" # SELinux role
type: "container_t" # SELinux type (most important)
user: "system_u" # SELinux user
containers:
- name: app
image: myapp:v1.2.3
Understanding SELinux Options:
-
type(most important): Defines what the process can access.container_tis the default type for containers, which has restricted access to system resources. This is the primary security boundary. -
level: Used for Multi-Level Security (MLS) to implement sensitivity levels (like "top secret" vs "public"). The format issensitivity:category. For example,s0:c123,c456means sensitivity level 0 with categories 123 and 456. Processes can only access resources at their level or below. -
role: Defines what types a user can transition to.object_ris the standard role for files and objects. Roles are less commonly customized in containers. -
user: SELinux user identity (not the same as Linux user).system_uis the system user for system processes.
Practical Example:
# A pod that can only access specific resources
securityContext:
seLinuxOptions:
type: "container_t" # Standard container type
level: "s0:c100" # Can only access resources at s0:c100
Note: SELinux must be enabled on your nodes for these settings to take effect. Many Kubernetes distributions (like OpenShift) enable SELinux by default, while others (like many managed Kubernetes services) may not. You can check if SELinux is enabled on your nodes with:
kubectl debug node/your-node -it --image=busybox -- chroot /host sestatus
If SELinux is not enabled on your nodes, these options will be ignored (they won't cause errors, but they won't provide protection either).
AppArmor Profiles
What is AppArmor? AppArmor (Application Armor) is a Linux security module that restricts programs' capabilities using per-program profiles. Similar to SELinux, it provides mandatory access control but with a simpler, path-based approach. AppArmor profiles define what files a program can access, which capabilities it can use, and what network access it has, creating an additional security layer that contains damage if a container is compromised.
apiVersion: v1
kind: Pod
metadata:
name: apparmor-secured-app
spec:
securityContext:
appArmorProfile:
type: RuntimeDefault
# or use a custom profile:
# type: Localhost
# localhostProfile: custom-profile-name
containers:
- name: app
image: myapp:v1.2.3
AppArmor profile types:
RuntimeDefault- Use the container runtime's default AppArmor profileLocalhost- Use a custom profile loaded on the nodeUnconfined- No AppArmor restrictions (not recommended)
Seccomp Profiles
What is Seccomp? Secure Computing Mode (seccomp) is a Linux kernel feature that restricts which system calls (syscalls) a process can make. Since most applications only use a small subset of available system calls, seccomp acts as a filter that blocks dangerous or unnecessary syscalls that attackers might exploit. By limiting the attack surface at the kernel level, seccomp prevents compromised containers from performing low-level operations like creating new processes, accessing raw sockets, or manipulating kernel modules—common techniques used in container breakout attacks.
apiVersion: v1
kind: Pod
metadata:
name: seccomp-secured-app
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
# or use a custom profile:
# type: Localhost
# localhostProfile: profiles/default.json
containers:
- name: app
image: myapp:v1.2.3
Seccomp profile types:
RuntimeDefault- Use the container runtime's default profileUnconfined- No seccomp restrictions (not recommended)Localhost- Use a custom profile from the node
Custom Seccomp Profile Example
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "open", "close", "mmap"],
"action": "SCMP_ACT_ALLOW"
}
]
}
Service Account and RBAC Integration
Service accounts provide identity for pods within the Kubernetes cluster, allowing them to authenticate with the API server and access cluster resources. Every pod runs with a service account (using the "default" account if none is specified), which comes with associated permissions. Properly configuring service accounts and their Role-Based Access Control (RBAC) permissions is critical—overly permissive service accounts are a common attack vector, as compromised pods can use them to escalate privileges and access resources they shouldn't.
Use Dedicated Service Accounts
Don't use the default service account for applications:
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-service-account
namespace: default
automountServiceAccountToken: false # Disable if not needed
---
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
serviceAccountName: myapp-service-account
automountServiceAccountToken: false # Disable API access if not needed
Disable Service Account Token Mounting
By default, Kubernetes automatically mounts a service account token into every pod, giving it credentials to communicate with the Kubernetes API server. If your application doesn't need to interact with the Kubernetes API (which is the case for most applications), this token becomes an unnecessary security risk—if an attacker compromises your container, they can use this token to query the API, potentially discovering sensitive information about your cluster or escalating their access. Disabling token mounting removes this attack vector entirely.
spec:
automountServiceAccountToken: false
RBAC for Service Accounts
Even with a dedicated service account, you need to control what permissions it has through Role-Based Access Control (RBAC). Following the principle of least privilege, grant only the minimum permissions your application needs to function. For example, if your app only reads configuration data, grant it read-only access to ConfigMaps, not full cluster access. This ensures that even if your pod is compromised, attackers can't use the service account to access or modify other cluster resources.
Create minimal RBAC permissions:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: myapp-role
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-rolebinding
subjects:
- kind: ServiceAccount
name: myapp-service-account
roleRef:
kind: Role
name: myapp-role
apiGroup: rbac.authorization.k8s.io
Network Security at Pod Level
While comprehensive network security involves NetworkPolicies (covered in a future article), there are pod-level configurations that enhance network security.
DNS Policy Configuration
Control DNS resolution behavior:
spec:
dnsPolicy: ClusterFirst
# or for more control:
dnsPolicy: None
dnsConfig:
nameservers:
- 8.8.8.8
searches:
- my.domain.com
options:
- name: ndots
value: "2"
Host Network and Ports
Sharing the host's network namespace (hostNetwork: true) gives your container direct access to the node's network interfaces, which significantly increases security risks. When enabled, a compromised container can sniff network traffic, bind to any port on the host (including privileged ports), and potentially access services that should be isolated. Similarly, hostPID and hostIPC expose the host's process and inter-process communication namespaces, allowing containers to see and potentially manipulate processes running on the node itself. Unless you have a specific need (like network monitoring tools), always keep these disabled.
# ❌ DON'T DO THIS unless absolutely necessary
spec:
hostNetwork: true
hostPID: true
hostIPC: true
# ✅ DO THIS (default behavior)
spec:
hostNetwork: false # default
Complete Secure Pod Example
Here's a comprehensive example that implements all the security best practices:
apiVersion: v1
kind: Pod
metadata:
name: ultra-secure-app
annotations:
container.apparmor.security.beta.kubernetes.io/app: "runtime/default"
spec:
serviceAccountName: myapp-service-account
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
appArmorProfile:
type: RuntimeDefault
seccompProfile:
type: RuntimeDefault
seLinuxOptions:
level: "s0:c123,c456"
imagePullSecrets:
- name: private-registry-secret
containers:
- name: app
image: private-registry.company.com/myapp:v1.2.3
imagePullPolicy: Always
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: database-password
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secrets
key: api-key
envFrom:
- configMapRef:
name: app-config
resources:
requests:
memory: "256Mi"
cpu: "250m"
ephemeral-storage: "1Gi"
limits:
memory: "512Mi"
cpu: "500m"
ephemeral-storage: "2Gi"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: tmp-volume
mountPath: /tmp
- name: app-secrets
mountPath: /etc/secrets
readOnly: true
ports:
- containerPort: 8080
protocol: TCP
volumes:
- name: tmp-volume
emptyDir: {}
- name: app-secrets
secret:
secretName: app-secrets
defaultMode: 0400
restartPolicy: Always
dnsPolicy: ClusterFirst
---
# Supporting resources
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-service-account
automountServiceAccountToken: false
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
database-password: bXktc3VwZXItc2VjcmV0LXBhc3N3b3Jk
api-key: c2stMTIzNDU2Nzg5MGFiY2RlZg==
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
ENVIRONMENT: "production"
MAX_CONNECTIONS: "100"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: myapp-role
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-rolebinding
subjects:
- kind: ServiceAccount
name: myapp-service-account
roleRef:
kind: Role
name: myapp-role
apiGroup: rbac.authorization.k8s.io
Beyond Pod Configuration
While this article focuses on pod-level security configurations, comprehensive Kubernetes security requires additional layers of defense. Important security topics that work alongside pod configuration include Network Policies for controlling traffic between pods, Pod Security Admission for enforcing cluster-wide security standards, Admission Controllers for validating pod specifications, RBAC and Authentication for access management, and Runtime Security tools like Falco for detecting anomalous behavior. We'll cover each of these critical security components in detail in upcoming articles to provide you with a complete Kubernetes security strategy.
Conclusion
Securing Kubernetes pods requires a layered approach that starts with your YAML manifests. By implementing the security practices outlined in this guide—from security contexts and secrets management to image security and resource limits—you create a strong foundation for your application security.
Remember that security is not a one-time configuration but an ongoing process. Regularly update your security practices, scan for vulnerabilities, monitor for threats, and stay informed about new security features and best practices in the Kubernetes ecosystem.
The security configurations you implement at the pod level are just the beginning. In future articles, we'll explore NetworkPolicies, Pod Security Admission, cluster hardening, and operational security practices that complete your Kubernetes security strategy.
Start with these pod-level security practices today, and you'll be well on your way to running secure, production-ready Kubernetes workloads.




