🛡️ Defence in Depth at the Kernel Layer
Container namespaces and cgroups isolate resources, but they don't restrict what system calls a process can make. A compromised container can still call ptrace, mount, or bpf unless the kernel is explicitly told to deny them. Runtime security adds mandatory access control (MAC) and syscall filtering as a second line of defence after your application is already running.
seccomp
Syscall filter via BPF program. Restricts which kernel system calls a process may invoke. Supported natively by the Linux kernel and all major container runtimes.
AppArmor
Path-based MAC enforced by the kernel LSM. Profiles restrict file access, network, capabilities. Default on Ubuntu/Debian nodes.
SELinux
Label-based MAC. Every process and file has a security context. Policy rules define allowed interactions. Default on RHEL/CentOS/Fedora nodes.
Falco
Runtime anomaly detection via eBPF/kernel module. Watches syscalls and K8s audit events in real time and fires alerts on suspicious behaviour.
🔬 seccomp Profiles
Linux has ~350 syscalls. A typical container application uses fewer than 50. seccomp (secure computing mode) uses a BPF program to allowlist or blocklist syscalls before they reach the kernel.
Three seccomp Modes
| Mode | Meaning | K8s field value |
|---|---|---|
| Unconfined | No restriction — all syscalls allowed (default without policy) | Unconfined |
| RuntimeDefault | The container runtime's built-in profile (Docker/containerd deny ~50 dangerous syscalls) | RuntimeDefault |
| Localhost | A custom JSON profile file on the node (in /var/lib/kubelet/seccomp/) | Localhost |
Applying seccomp in a Pod Spec
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # pod-level default for all containers
containers:
- name: app
image: myapp:v1
securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/myapp-strict.json # relative to /var/lib/kubelet/seccomp/
Writing a Custom seccomp Profile
A profile is a JSON file with a default action and an optional list of syscall overrides. Approach: start with SCMP_ACT_LOG to discover which syscalls your app uses, then switch to SCMP_ACT_ERRNO.
{
"defaultAction": "SCMP_ACT_ERRNO", // deny all by default
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"read", "write", "open", "close", "stat", "fstat",
"mmap", "mprotect", "munmap", "brk", "rt_sigaction",
"rt_sigprocmask", "ioctl", "access", "execve",
"openat", "newfstatat", "exit_group", "futex",
"getdents64", "clone", "wait4", "socket", "connect",
"sendto", "recvfrom", "bind", "listen", "accept4"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
SeccompProfile and AppArmorProfile. It copies them to the right node path automatically. No manual file distribution needed.
seccomp + Restricted Pod Security Standard
The Restricted PSS (Pod Security Standard) requires seccompProfile.type to be RuntimeDefault or Localhost. This is the main reason many teams switch from Unconfined. Enforce it at the namespace level:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest
🦅 Falco — Runtime Anomaly Detection
seccomp and AppArmor prevent bad syscalls before they happen. Falco (CNCF graduated) detects suspicious behaviour after it occurs — a complementary, detective control. It watches kernel syscalls and Kubernetes audit events and fires alerts when a rule matches.
Installing Falco (Helm)
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco \
--namespace falco --create-namespace \
--set driver.kind=ebpf \
--set falcosidekick.enabled=true \
--set falcosidekick.webui.enabled=true
Anatomy of a Falco Rule
Rules are written in YAML and use a condition DSL that queries syscall fields:
# Built-in rule (simplified)
- rule: Terminal shell in container
desc: A shell was opened interactively inside a running container
condition: >
spawned_process
and container
and shell_procs
and proc.tty != 0
and container_entrypoint
output: >
Shell opened in container (user=%user.name container=%container.name
image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name
shell=%proc.name cmdline=%proc.cmdline)
priority: WARNING
tags: [container, shell, mitre_execution]
Essential Built-in Rules to Know
| Rule | What it detects | Priority |
|---|---|---|
| Terminal shell in container | kubectl exec or any interactive shell | WARNING |
| Write below etc | Any write to /etc inside a container | ERROR |
| Read sensitive file trusted after startup | Reading /etc/shadow, /etc/sudoers, SSH keys | WARNING |
| Launch Privileged Container | Container started with privileged: true | INFO |
| K8s Serviceaccount Created | Audit: SA creation (lateral movement indicator) | WARNING |
| Outbound Connection to C2 Server | Connection to known malicious IPs (threat intel list) | CRITICAL |
Writing a Custom Falco Rule
# Custom rule: alert on unexpected outbound port from specific namespace
- macro: payment_service_pod
condition: k8s.ns.name = "payments" and k8s.pod.name startswith "checkout-"
- rule: Unexpected egress from payments namespace
desc: checkout pod made outbound connection on non-443/5432 port
condition: >
outbound
and payment_service_pod
and not fd.sport in (443, 5432)
output: >
Unexpected egress (pod=%k8s.pod.name dst=%fd.rip:%fd.rport
proto=%fd.l4proto user=%user.name)
priority: CRITICAL
tags: [network, payments, custom]
Falco + Kubernetes Audit Events
Falco can also consume Kubernetes audit log events (via a webhook backend) to detect control-plane level threats — not just node-level syscalls:
# kube-apiserver audit webhook → Falco
# Add to kube-apiserver flags:
--audit-webhook-config-file=/etc/kubernetes/falco-webhook.yaml
--audit-policy-file=/etc/kubernetes/audit-policy.yaml
# falco-webhook.yaml points to Falco's k8s audit webhook endpoint
apiVersion: v1
kind: Config
clusters:
- cluster:
server: http://falco.falco.svc:8765/k8s-audit
name: falco
🔒 AppArmor Profiles
AppArmor is the default Linux Security Module (LSM) on Ubuntu and Debian — which means most managed Kubernetes nodes (GKE, EKS with Ubuntu AMIs, AKS) have it available. It uses path-based rules to restrict what files and capabilities a container process can access.
Profile Modes
| Mode | Behaviour |
|---|---|
| enforce | Violations are blocked and logged |
| complain | Violations are only logged — use to generate a profile |
| disable | Profile inactive |
Loading a Profile on the Node
# Copy profile to node (or use Security Profiles Operator)
cat /etc/apparmor.d/myapp-profile
# Then load it:
apparmor_parser -r -W /etc/apparmor.d/myapp-profile
# Verify it loaded:
aa-status | grep myapp
Attaching an AppArmor Profile to a Container
As of Kubernetes 1.30, AppArmor is configured via securityContext.appArmorProfile (promoted from annotation). The annotation form still works on older clusters:
# Kubernetes 1.30+ native field
spec:
containers:
- name: app
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: myapp-profile
# Legacy annotation (pre-1.30 / still works)
metadata:
annotations:
container.apparmor.security.beta.kubernetes.io/app: localhost/myapp-profile
Example AppArmor Profile
#include <tunables/global>
profile myapp-profile flags=(attach_disconnected) {
#include <abstractions/base>
## allow read-only access to OS libraries
/usr/lib/** r,
/lib/** r,
## allow the app binary to execute
/app/server mrix,
## allow writes only to /tmp and /var/log/myapp/
/tmp/** rw,
/var/log/myapp/** rw,
## deny everything else implicitly (AppArmor default)
deny /etc/shadow r,
deny /proc/sys/** w,
}
🏷️ SELinux Context
SELinux is label-based: every process gets a type label and every file/socket gets a type label. The policy says which process types may access which resource types. It is the default LSM on RHEL, CentOS, Fedora, and Amazon Linux 2 nodes.
Setting SELinux Options in Kubernetes
apiVersion: v1
kind: Pod
spec:
securityContext:
seLinuxOptions:
level: "s0:c123,c456" # MCS label — isolates volumes between Pods
containers:
- name: app
securityContext:
seLinuxOptions:
type: "container_t" # standard container type
level: "s0:c123,c456"
SELinux vs AppArmor — Which One?
| AppArmor | SELinux | |
|---|---|---|
| Model | Path-based | Label-based |
| Default on | Ubuntu, Debian | RHEL, CentOS, Amazon Linux |
| Profile syntax | Human-readable text | Compiled binary policy |
| Tooling | aa-genprof, aa-logprof | audit2allow, semanage |
| K8s maturity | GA (1.30) | Beta (seLinuxOptions) |