🛡️ 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

ModeMeaningK8s field value
UnconfinedNo restriction — all syscalls allowed (default without policy)Unconfined
RuntimeDefaultThe container runtime's built-in profile (Docker/containerd deny ~50 dangerous syscalls)RuntimeDefault
LocalhostA 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"
    }
  ]
}
💡 Use the Seccomp Operator for production The Security Profiles Operator (SIG-node) lets you distribute seccomp (and AppArmor) profiles via CRDs — 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.

Container syscalls Falco Agent eBPF / kernel module + K8s audit webhook Rules Engine match → alert Outputs stdout / syslog webhook / Slack / SIEM

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

RuleWhat it detectsPriority
Terminal shell in containerkubectl exec or any interactive shellWARNING
Write below etcAny write to /etc inside a containerERROR
Read sensitive file trusted after startupReading /etc/shadow, /etc/sudoers, SSH keysWARNING
Launch Privileged ContainerContainer started with privileged: trueINFO
K8s Serviceaccount CreatedAudit: SA creation (lateral movement indicator)WARNING
Outbound Connection to C2 ServerConnection 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
🔴 Falco is detective, not preventive Falco fires an alert — it does not block the action. Pair it with seccomp/AppArmor for prevention + Falco for detection. For automated response, use Falcosidekick to trigger a Lambda/Cloud Function that kills the pod or cordons the node.

🔒 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

ModeBehaviour
enforceViolations are blocked and logged
complainViolations are only logged — use to generate a profile
disableProfile 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,
}
⚠️ Node-local constraint AppArmor profiles must exist on every node where the Pod might schedule. Use the Security Profiles Operator or a DaemonSet to distribute profiles. If a profile is missing on the scheduled node, the Pod will fail to start.

🏷️ 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?

AppArmorSELinux
ModelPath-basedLabel-based
Default onUbuntu, DebianRHEL, CentOS, Amazon Linux
Profile syntaxHuman-readable textCompiled binary policy
Toolingaa-genprof, aa-logprofaudit2allow, semanage
K8s maturityGA (1.30)Beta (seLinuxOptions)
🔵 Practical advice Use whatever LSM your node OS provides — don't try to switch. On GKE/EKS Ubuntu nodes, use AppArmor. On RHEL-based nodes, use SELinux. Both achieve the same goal via different mechanisms.

🧠 Knowledge Check

Q1. What does seccompProfile.type: RuntimeDefault do?

A) Disables all syscall restrictions for the container
B) Applies the container runtime's built-in profile, blocking ~50 dangerous syscalls
C) Loads a custom JSON profile from /var/lib/kubelet/seccomp/
D) Requires SELinux to be enabled on the node

Q2. An AppArmor profile is in complain mode. What happens when the container violates it?

A) The container is immediately terminated
B) The syscall is denied and an error is returned to the process
C) The violation is only logged; the action is still allowed
D) AppArmor automatically generates a new profile

Q3. A Falco rule fires for "Terminal shell in container." What has happened and what should you do?

A) The container failed a liveness probe and restarted
B) An interactive shell was opened in a container — possible kubectl exec or attacker; investigate and isolate immediately
C) Falco automatically killed the container to prevent damage
D) The container image failed a signature check

Q4. Which LSM is default on RHEL-based Kubernetes nodes?

A) AppArmor
B) SELinux
C) seccomp
D) Falco