Securing Kubernetes Using AppArmor

Table of contents

When deploying applications at scale in Kubernetes, security becomes a non-negotiable factor. In modern production setups, simply using RBAC and NetworkPolicies isnโt enoughโespecially when you're running critical workloads that handle sensitive data. Thatโs where AppArmor comes in.
This post walks you through how a fintech company secures their payment microservice using AppArmor. Youโll see not only how to write and apply profiles but also how to integrate them into your CI/CD pipeline and monitor their effectiveness using real logs and examples.
๐ What is App-armor, Really?
AppArmor acts like a dedicated bodyguard for Linux processes. Think of it as creating a strict "process agreement" โ defining exactly what files the application can touch, what network access it's allowed to use, and which system capabilities are permitted or blocked.
Instead of trusting containers implicitly, AppArmor wraps them in a carefully crafted set of permissions:
Which specific directories/files should be accessible?
What kind of filesystem access is needed (read-only, read-write)?
Can the application interact with network sockets?
Are there any system capabilities it canโt use?
Unlike its more complex counterpart SELinux, AppArmor uses straightforward file paths for defining rulesโmaking it easier to grasp but still incredibly effective.
๐ผ Real Production Use Case: Fintech Payment API
๐งพ Scenario
Imagine a fintech company running a Kubernetes cluster that hosts a critical Node.js-based payment processing API. This service needs to interact with both an internal PostgreSQL database and external payment gateway APIs (like Razorpay or Stripe).
๐ฏ Goals
1๏ธโฃ Strict Network Controls: Prevent the container from freely accessing the internet.
2๏ธโฃ Minimal Filesystem Access: Restrict file access to just whatโs necessary, including its own configuration files and logs. Block any other filesystem activity.
3๏ธโฃ Block Unauthorized Executions: Ensure no forbidden binaries or scripts (like .sh
files) can be executed by the container.
4๏ธโฃ Prevent Privilege Escalation: Stop containers from trying to gain host-level privileges โ even if they somehow escalate within themselves.
โ๏ธ AppArmor Profile: payment-api
Let's create a sample profile. This defines access rules for our payment API application and should be saved on each Kubernetes node (typically in /etc/apparmor.d/payment-api
):
profile payment-api flags=(attach_disconnected) {
# Include global tunables if needed
/usr/local/bin/node {
/app/** r, # App can read files in /app/
/app/logs/** rw, # Logs directory is writable for the app
/etc/payment-api/config.json r, # Read-only access to config file
/lib/** mr, # Map (read) system libraries but don't execute them directly from here
/usr/lib/** mr, # Map user-space libraries โ read only
deny /**.sh x, # Prevent execution of shell scripts (.sh files)
network inet, # Allow standard internet socket connections (e.g., TCP/UDP)
deny network raw, # Deny raw IP packet operations (not needed for most apps)
deny network packet, # Block advanced packet-level networking
capability net_bind_service, # Allow binding to network ports (like 443 or 8443)
deny capability sys_admin, # Prevent gaining system administration-like capabilities on the host
}
}
๐ง Why This Profile Works
File Access Control (
r
/rw
): The app can only read files in/app/
, write to its logs in/app/logs/
, and has read-only access to/etc/payment-api/config.json
. All other file paths are explicitly denied.Network Restrictions: By allowing
network inet
(standard sockets) but denying raw and packet operations, we ensure the app can communicate over HTTP/HTTPS or standard TCP connections, while blocking potentially dangerous low-level network activities.Capability Management: The container is given permission to bind to ports (
net_bind_service
) โ required for its operation. But capabilities likesys_admin
are blocked entirely, preventing privilege escalation.
This profile essentially creates a sandboxed environment at the operating system level, ensuring the payment API can only do what it needs and nothing more.
๐ฆ Kubernetes Pod Definition
To enforce this security policy in Kubernetes, we need to attach the AppArmor profile to our container. Hereโs how you might define your pod:
apiVersion: v1
kind: Pod
metadata:
name: payment-api-pod
spec:
containers:
- name: payment-api-container
image: registry.company.com/payment-api:prod # Your specific Docker image URL
ports:
- containerPort: 8080
protocol: TCP
securityContext:
# If you have a user ID defined for your app, specify it here (runAsUser)
runAsUser: 1001
# Prevent the root filesystem from being writable โ makes the host more secure
readOnlyRootFilesystem: true
# Disallow any attempt to raise privileges beyond what was initially granted
allowPrivilegeEscalation: false
# This is crucial for AppArmor integration:
apparmorProfile: "payment-api" # Name must match your profile file name (without .profile extension)
๐ก Note: The
apparmorProfile
field tells Kubernetes which pre-loaded profile to apply. Ensure you have loaded the profile onto each node beforehand.
๐ Automating in CI/CD with GitOps
To make AppArmor profiles manageable and repeatable, integrate them into your infrastructure-as-code approach:
๐ Step 1: Store Profiles Securely in Git (Infrastructure Version Control)
Store your security profiles alongside other Kubernetes manifests. Example directory structure (infrastructure-repo
):
infrastructure-repo/
โโโ namespaces/
โ โโโ payment-api-ns.yaml
โโโ apparmor/
โโโ profiles/ # Actual profile definitions (if you're distributing them)
โ โโโ payment-api.apparmor
โโโ manifests/ # Optional: Place your full profile files here if nodes expect a specific location?
โโโ payment-api.profile
โ๏ธ Step 2: Load Profiles onto Nodes Using a DaemonSet
Create a dedicated Kubernetes DaemonSet
that runs once on each node. This daemon can load the AppArmor profiles from shared storage:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: apparmor-daemonset # Name for your DaemonSet
spec:
selector:
matchLabels:
app.kubernetes.io/name: apparmor-loader
template:
metadata:
labels:
app.kubernetes.io/name: apparmor-loader
spec:
containers:
- name: loader-container
image: alpine:latest # Use a lightweight base image for the DaemonSet
command: ["/bin/sh", "-c", "apparmor_parser -r /etc/apparmor.d/payment-api.profile"]
volumeMounts:
- mountPath: /etc/apparmor.d
name: profiles-volume
volumes:
- name: profiles-volume
hostPath:
path: "/etc/apparmor.d"
๐ Step 3: Use GitOps Tools for Profile Updates
Tools like ArgoCD can monitor your Git repository (infrastructure-repo in the example above). When you push an updated profile (payment-api.apparmor
), these tools will:
Trigger a new deployment of the
apparmor-daemonset
.The DaemonSet pod(s) will run and parse the new profile, applying it to the node.
๐ Observability: Monitoring AppArmor in Production
Once your application is running with AppArmor profiles, you'll need visibility into how they're performing:
๐ชต Log Violations (Check System Journaled Logs)
Kubernetes nodes run Linux systems โ and apparmor_parser
writes enforcement events to the system logs. You can check for violations directly on a node or use tools that aggregate these logs:
# On one of your nodes:
sudo journalctl -u apparmor | grep "payment-api" # Filter by profile name
# Or, more generally (might be noisy):
journalctl -k | grep "denied" # Shows only AppArmor events with a 'DENIED' action
๐ก Best Practice: Pipe logs like journalctl -u apparmor
into your monitoring pipeline.
๐งช Complain Mode for Testing and Debugging
During development or testing phases (especially when you're refining your profile rules), switch profiles to complain mode:
sudo aa-complain /etc/apparmor.d/payment-api.profile # This tells AppArmor to ignore violations but still log them
This allows your application to run more freely, helping you identify necessary permissions or find unintended restrictions. Use aa-logprof
periodically on a test node with complain mode enabled:
sudo aa-logprof -t /etc/apparmor.d/payment-api.profile # Analyzes logs and suggests policy updates (add missing perms)
๐ Summary: Why AppArmor Matters in Kubernetes
Incorporating AppArmor into your Kubernetes security posture brings tangible benefits:
๐ก๏ธ Stronger Isolation: You can enforce strict file access rules, preventing apps from reading or writing unauthorized files.
๐ Reduced Attack Surface: Blocking capabilities like raw sockets and
sys_admin
makes it harder for malicious code to exploit the host environment.๐ Repeatable Hardening: Using GitOps allows you to version control security policies alongside your application code. Profiles can be consistently applied across all nodes โ making hardening verifiable.
And crucially, AppArmor achieves this without modifying your application's source code or its container image itself. Security is enforced at the node level via profile definitions attached during pod creation.
Subscribe to my newsletter
Read articles from Sanmarg Paranjpe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
