Indledning: Container-sikkerhed i en verden af konstante trusler
Container-teknologi har fundamentalt ændret den måde, vi bygger, distribuerer og kører software på. Docker, Kubernetes og alt det drumherum er blevet rygraden i moderne infrastruktur — fra små startups til Fortune 500-virksomheder. Men med den massive udbredelse følger en tilsvarende massiv angrebsflade.
I 2025-2026 står vi over for en virkelighed, hvor 87 procent af alle container-images indeholder mindst én kendt sårbarhed. Lad det lige synke ind et øjeblik. Angribere udnytter aktivt svagheder i container-runtimes til at bryde ud af den isolation, som containere ellers forventes at levere.
De seneste runC-sårbarheder, der blev offentliggjort i 2025, har ærligt talt sendt rystelser gennem DevOps- og sikkerhedsmiljøerne. RunC er den underliggende container-runtime, som Docker, containerd og CRI-O alle afhænger af. Når der dukker kritiske fejl op i runC, påvirker det bogstaveligt talt millioner af kørende containere verden over. CVE-2025-31133, CVE-2025-52565 og CVE-2025-52881 viste alle, at en ondsindet container under visse omstændigheder kan bryde ud af sin isolation og kompromittere hele værtssystemet.
Denne guide dækker hele spektret — fra grundlæggende forståelse af Linux-containerisolering, over de nyeste runC-sårbarheder, til praktisk image-hærdning, runtime-beskyttelse med seccomp og AppArmor, overvågning med Falco, og alternative runtimes som gVisor og Kata Containers. Målet er at give dig en komplet, handlingsorienteret forståelse af container-sikkerhed i 2026.
Forståelse af container-isolation: Namespaces, cgroups og seccomp
For at forstå, hvorfor container-sikkerhed er så kompleks, er vi nødt til at forstå de underliggende Linux-mekanismer. I modsætning til virtuelle maskiner (VMs), som kører deres eget operativsystem med en separat kerne, deler containere værts-kernen. Denne deling er både containerens største styrke (effektivitet, hastighed) og dens største svaghed (potentiel isolation-svigt).
Linux Namespaces
Namespaces er den primære isoleringsmekanisme for containere. De partitionerer kernel-ressourcer, så én gruppe processer ser ét sæt ressourcer, mens en anden gruppe ser noget helt andet. Linux understøtter flere typer:
- PID namespace — Isolerer proces-ID-rummet. Processer i en container kan ikke se processer i andre containere eller på værten.
- Network namespace — Giver hver container sin egen netværksstak med egne interfaces, routing-tabeller og firewall-regler.
- Mount namespace — Isolerer filsystem-mount-punkter, så containeren har sit eget filsystem-hierarki.
- UTS namespace — Tillader containere at have deres eget hostname og domænenavn.
- IPC namespace — Isolerer inter-process communication ressourcer (delt hukommelse, semaforer osv.).
- User namespace — Kortlægger bruger- og gruppe-ID'er inde i containeren til andre ID'er på værten, hvilket muliggør "rootless" containere.
- Cgroup namespace — Isolerer visningen af cgroup-hierarkiet.
Du kan inspicere en containers namespaces med denne kommando:
# Vis namespaces for en kørende container
docker inspect --format '{{.State.Pid}}' min-container
# Brug derefter resultatet (f.eks. PID 12345) til at se namespaces
ls -la /proc/12345/ns/
# Output eksempel:
# lrwxrwxrwx 1 root root 0 jan 15 10:00 cgroup -> 'cgroup:[4026532587]'
# lrwxrwxrwx 1 root root 0 jan 15 10:00 ipc -> 'ipc:[4026532511]'
# lrwxrwxrwx 1 root root 0 jan 15 10:00 mnt -> 'mnt:[4026532509]'
# lrwxrwxrwx 1 root root 0 jan 15 10:00 net -> 'net:[4026532514]'
# lrwxrwxrwx 1 root root 0 jan 15 10:00 pid -> 'pid:[4026532512]'
# lrwxrwxrwx 1 root root 0 jan 15 10:00 user -> 'user:[4026531837]'
# lrwxrwxrwx 1 root root 0 jan 15 10:00 uts -> 'uts:[4026532510]'
Control Groups (cgroups)
Cgroups kontrollerer og begrænser de ressourcer, en container kan bruge — CPU, hukommelse, disk I/O og netværksbåndbredde. Uden cgroups kunne en ondsindet container monopolisere værtens ressourcer og forårsage denial-of-service for alt andet, der kører på maskinen.
# Kør en container med ressource-begrænsninger
docker run -d \
--name sikker-app \
--memory="512m" \
--memory-swap="512m" \
--cpus="1.5" \
--pids-limit=100 \
--read-only \
min-app:latest
# Verificer cgroup-begrænsninger
cat /sys/fs/cgroup/docker/$(docker inspect --format '{{.Id}}' sikker-app)/memory.max
Seccomp (Secure Computing Mode)
Seccomp filtrerer de systemkald, en container kan foretage. Linux-kernen har over 300 systemkald, men en typisk container bruger kun en brøkdel af dem. Docker anvender som standard en seccomp-profil, der blokerer ca. 44 potentielt farlige systemkald — herunder mount, reboot, kexec_load og ptrace.
# Se Dockers standard seccomp-profil
docker run --rm -it alpine cat /proc/self/status | grep Seccomp
# Output: Seccomp: 2 (2 = filter mode aktiv)
# Kør en container UDEN seccomp (farligt — kun til test!)
docker run --rm -it --security-opt seccomp=unconfined alpine sh
Hvorfor containere ikke er VMs
Det her er virkelig vigtigt at forstå: containerens isolation er software-baseret, ikke hardware-baseret. En VM kører med sin egen kerne og er isoleret af hypervisoren, der udnytter hardware-virtualiseringsudvidelser (Intel VT-x, AMD-V). En container deler derimod kernen med værten.
Det betyder i praksis, at:
- En kernel-sårbarhed kan potentielt kompromittere alle containere på værten
- En fejl i container-runtimen (som runC) kan tillade container escape
- Forkert konfigurerede containere (f.eks. med
--privilegedflaget) kan have næsten fuld adgang til værten - Den delte kerneoverflade giver en større angrebsflade sammenlignet med VMs
Denne fundamentale arkitektoniske forskel er netop grunden til, at container-sikkerhed kræver en lagdelt tilgang — defense in depth — hvor flere sikkerhedsmekanismer kompenserer for hinanden.
RunC-sårbarhederne i 2025: En dybdegående analyse
RunC er referenceimplementeringen af OCI (Open Container Initiative) runtime-specifikationen og ligger under Docker, containerd, CRI-O og Podman. I 2025 blev tre alvorlige sårbarheder offentliggjort, som alle handlede om, hvordan runC håndterer filsystem-operationer under container-opsætning.
Lad os gennemgå dem én for én.
CVE-2025-31133: Masked Path Abuse — /dev/null-symlink-angrebet
Denne sårbarhed udnyttede den måde, runC implementerede "masked paths" — stier i containeren, der skulle gøres utilgængelige ved at blive overskrevet med /dev/null. OCI-specifikationen kræver, at visse følsomme stier (som /proc/kcore eller /proc/sched_debug) maskeres for at forhindre informationslækage.
Problemet? En angriber inde i containeren kunne erstatte filen på den maskerede sti med et symlink, inden runC nåede at montere /dev/null ovenpå. Da runC fulgte symlinket og monterede /dev/null på symlinkets destination, kunne resultatet være, at vigtige filer på værtssystemet blev overskrevet. Temmelig ubehageligt.
# Konceptuelt angreb (forenklet illustration)
# 1. Angriber i container erstatter en masked path med et symlink:
# /proc/sched_debug -> /host-path/etc/crontab
#
# 2. RunC forsøger at maskere stien ved at montere /dev/null:
# mount --bind /dev/null /proc/sched_debug
#
# 3. Da /proc/sched_debug nu er et symlink, følger mount operationen
# symlinket og monterer /dev/null over målfilen på værten
#
# Resultat: Kritiske værts-filer bliver utilgængelige eller overskrevet
Angrebskrav: Angriberen skulle kunne oprette filer og symlinks inde i containeren og have timing til at placere symlinket i det korte vindue mellem containerens opstart og runC's maskeringsproces.
CVE-2025-52565: /dev/console Mount Exploitation — Race condition med symlinks
Denne sårbarhed involverede en race condition (TOCTOU — Time of Check, Time of Use) i den måde, runC monterede /dev/console inde i containeren. Under container-opstart opretter runC en pseudo-terminal (PTY) og monterer den som /dev/console.
Angrebet udnyttede det tidsmæssige vindue mellem, hvornår runC verificerede destinationsstien, og hvornår den faktisk udførte monteringen. I det korte vindue kunne en koordineret proces inde i containeren erstatte /dev/console med et symlink, der pegede på en vilkårlig fil på værtens filsystem.
# Race condition illustration:
#
# Tidspunkt T1: runC verificerer /dev/console → det er en normal fil ✓
# Tidspunkt T2: Angriber erstatter /dev/console med symlink → /host/etc/shadow
# Tidspunkt T3: runC monterer PTY over /dev/console (følger symlink!)
#
# Resultat: PTY-enheden monteres over /etc/shadow på værten
#
# For at udnytte dette kræves præcis timing, typisk via:
# - Høj-hastighed symlink-udskiftningsløkke inde i containeren
# - Brug af inotify til at detektere runC's filsystem-operationer
Konsekvens: Denne sårbarhed kunne føre til vilkårlig filskrivning på værtssystemet — noget der potentielt kunne bruges til privilege escalation eller fuld kompromittering af værten.
CVE-2025-52881: Procfs Write Redirection — Omdirigering af skrivninger til følsomme filer
Den tredje sårbarhed handlede om, hvordan runC håndterede skrivninger til procfs (process filesystem). Under visse omstændigheder kunne en angriber omdirigere skrivninger, der var beregnet til procfs-filer inde i containeren, til vilkårlige filer på værtens filsystem.
Procfs (/proc) indeholder en masse filer, der bruges til at konfigurere processer og kerneparametre. RunC skriver til visse procfs-filer under container-opsætning (f.eks. for at konfigurere cgroups eller oom_score_adj). Ved at manipulere filsystem-opstillingen kunne en angriber narre runC til at følge et symlink og skrive til en fil på værten i stedet.
# Eksempel på procfs-filer, som runC interagerer med:
# /proc/[pid]/oom_score_adj — Justerer OOM killer prioritet
# /proc/[pid]/attr/current — SELinux sikkerhedskontekst
#
# Angrebsvektor (konceptuelt):
# 1. Angriber opretter symlink: /proc/self/oom_score_adj -> /etc/passwd
# 2. RunC skriver den konfigurerede OOM-score til filen
# 3. Skrivningen rammer /etc/passwd i stedet for procfs
#
# Selvom indholdet der skrives er begrænset, kan selv
# delvis korrumpering af systemfiler have alvorlige konsekvenser
Patchede versioner og hvad du bør gøre
Alle tre sårbarheder blev rettet i følgende runC-versioner:
| RunC-version | Status | Anbefaling |
|---|---|---|
| 1.2.8 | Patched (stabil 1.2.x-gren) | Opdater hvis du kører 1.2.x |
| 1.3.3 | Patched (stabil 1.3.x-gren) | Anbefalet for de fleste produktionsmiljøer |
| 1.4.0-rc.3 | Patched (release candidate) | Til early adopters og test-miljøer |
For Docker- og Kubernetes-brugere er det vigtigt at forstå afhængighedskæden:
# Tjek din runC-version
runc --version
# Forventet output: runc version 1.3.3 (eller nyere)
# For Docker-brugere:
docker info | grep -i runc
# Se efter: runc version 1.3.3+
# For Kubernetes-brugere (med containerd):
crictl info | jq '.config.containerd.runtimes.runc'
# Opdater runC på Debian/Ubuntu:
sudo apt update && sudo apt install runc
# Opdater Docker Engine (som inkluderer ny runC):
sudo apt update && sudo apt install docker-ce docker-ce-cli containerd.io
Vigtig pointe: Disse sårbarheder kræver alle, at angriberen allerede har kodeeksekvering inde i en container. Det er ikke remote exploits. Men i en multi-tenant Kubernetes-klynge, hvor du kører upålidelig kode, eller i CI/CD-pipelines, hvor build-jobs kører i containere — ja, så er denne angrebsvektor yderst relevant.
Hærdning af container-images
Den mest grundlæggende container-sikkerhedsforanstaltning er at minimere angrebsfladen i dine container-images. Simpelt sagt: jo færre pakker, filer og kendte sårbarheder, jo færre muligheder har en angriber.
Minimale base-images
Valget af base-image er afgørende — og det er ærligt talt et af de steder, hvor mange teams kan gøre den største forskel med mindst mulig indsats. Her er en sammenligning:
| Base Image | Størrelse | Pakker | Typiske CVE'er |
|---|---|---|---|
| ubuntu:24.04 | ~78 MB | ~90+ | Mange |
| debian:bookworm-slim | ~52 MB | ~50+ | Moderate |
| alpine:3.21 | ~7 MB | ~15 | Få |
| gcr.io/distroless/static | ~2 MB | Ingen pakkemanager | Minimale |
| scratch | 0 MB | Ingen | Ingen |
Multi-stage builds
Multi-stage builds er en af de mest effektive teknikker til at minimere image-størrelse og angrebsflade. Idéen er enkel: brug ét image til at bygge din applikation og et andet, minimalt image til at køre den.
# Eksempel: Multi-stage Dockerfile for en Go-applikation
# Stage 1: Build
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
# Stage 2: Runtime (minimalt image)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
# Eksempel: Multi-stage Dockerfile for en Python-applikation
# Stage 1: Build med alle udviklerværktøjer
FROM python:3.13-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.13-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /opt/venv /opt/venv
COPY . /app
WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"
USER appuser
CMD ["python", "app.py"]
Scanning med Trivy og Grype
Automatiseret sårbarhedsscanning bør være en integreret del af din CI/CD-pipeline. Ingen diskussion. To fremragende open source-værktøjer er Trivy (fra Aqua Security) og Grype (fra Anchore).
# Installation af Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan et container-image for sårbarheder
trivy image --severity HIGH,CRITICAL min-app:latest
# Scan med output i tabelformat og ignorer unfixed
trivy image --severity HIGH,CRITICAL --ignore-unfixed min-app:latest
# Scan en Dockerfile for fejlkonfigurationer
trivy config --severity HIGH,CRITICAL Dockerfile
# Scan et lokalt filsystem (f.eks. i CI/CD)
trivy fs --severity HIGH,CRITICAL --scanners vuln,misconfig .
# Installation af Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Scan med Grype
grype min-app:latest --only-fixed --fail-on high
Det smarte er at integrere scanning i din CI/CD-pipeline, så buildet fejler automatisk, hvis der dukker kritiske sårbarheder op:
# Eksempel: GitHub Actions workflow
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Byg Docker image
run: docker build -t min-app:${{ github.sha }} .
- name: Kør Trivy sårbarhedsscanning
uses: aquasecurity/trivy-action@master
with:
image-ref: 'min-app:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload scanningsresultater
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
Supply chain-sikkerhed og signerede images
I kølvandet på SolarWinds og lignende supply chain-angreb er det blevet kritisk vigtigt at verificere, at dine container-images faktisk er autentiske og umodificerede. Cosign (fra Sigstore-projektet) er det førende værktøj til opgaven:
# Installer Cosign
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Generer et nøglepar
cosign generate-key-pair
# Signer et image
cosign sign --key cosign.key min-registry.dk/min-app:v1.2.3
# Verificer et signeret image
cosign verify --key cosign.pub min-registry.dk/min-app:v1.2.3
# Keyless signing med OIDC (anbefalet for CI/CD)
cosign sign min-registry.dk/min-app:v1.2.3
# Dette bruger din identitetsudbyder (GitHub, Google, osv.) automatisk
Runtime-sikkerhed med seccomp og AppArmor
Selv med hærdede images har du brug for runtime-beskyttelse. Seccomp og AppArmor er to komplementære mekanismer, der giver dig præcis kontrol over, hvad en container kan gøre, mens den kører. Tænk på det som endnu et lag i din forsvarsmodel.
Tilpassede seccomp-profiler
Dockers standard seccomp-profil er en fin start, men en tilpasset profil, der er skræddersyet til netop din applikation, er langt mere sikker. Princippet er simpelt nok: tillad kun de systemkald, din applikation faktisk har brug for, og bloker alt andet.
// Eksempel: Tilpasset seccomp-profil (custom-seccomp.json)
// Denne profil tillader kun de nødvendige syscalls for en webserver
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": [
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
]
}
],
"syscalls": [
{
"names": [
"accept4", "bind", "clone", "close", "connect",
"epoll_create1", "epoll_ctl", "epoll_wait",
"exit", "exit_group", "fstat", "futex",
"getpid", "getsockopt", "listen", "mmap",
"mprotect", "munmap", "openat", "read",
"recvfrom", "rt_sigaction", "rt_sigprocmask",
"sendto", "setsockopt", "socket", "write",
"writev", "brk", "fcntl", "getrandom",
"nanosleep", "clock_gettime", "sigaltstack",
"set_tid_address", "set_robust_list",
"prlimit64", "sched_getaffinity",
"arch_prctl", "access", "execve",
"readlinkat", "newfstatat", "getdents64",
"lseek", "pipe2", "dup3"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Anvend den tilpassede seccomp-profil
docker run -d \
--name webserver \
--security-opt seccomp=custom-seccomp.json \
min-webserver:latest
# Tip: Brug strace til at identificere nødvendige syscalls
# Kør først uden seccomp og observer:
strace -c -f -p $(docker inspect --format '{{.State.Pid}}' webserver)
# Eller brug Dockers --security-opt seccomp=unconfined med audit log:
# (kræver Linux audit framework)
docker run --security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
min-app:latest
AppArmor container-profiler
AppArmor er et Mandatory Access Control (MAC) system, der begrænser programmers adgang til filer, netværk og systemressourcer. Docker bruger som standard en AppArmor-profil kaldet docker-default, men du kan (og bør ofte) oprette mere restriktive profiler:
# Fil: /etc/apparmor.d/docker-webserver
#include
profile docker-webserver flags=(attach_disconnected,mediate_deleted) {
#include
#include
# Netværksadgang
network inet tcp,
network inet udp,
network inet6 tcp,
network inet6 udp,
# Tillad læsning af applikationsfiler
/app/** r,
/app/server ix,
# Tillad skrivning til log-mappe
/var/log/app/ rw,
/var/log/app/** rw,
# Tillad adgang til tmp
/tmp/ rw,
/tmp/** rw,
# Bloker adgang til følsomme stier
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/*/mem rw,
deny /sys/firmware/** rwx,
deny /proc/sysrq-trigger rw,
deny /proc/kcore r,
# Bloker mount operationer
deny mount,
deny umount,
# Bloker raw socket adgang
deny network raw,
deny network packet,
# Tillad nødvendige proc-filer
/proc/self/fd/ r,
/proc/self/fd/** rw,
/proc/sys/net/** r,
}
# Indlæs AppArmor-profilen
sudo apparmor_parser -r /etc/apparmor.d/docker-webserver
# Kør containeren med den tilpassede profil
docker run -d \
--name webserver \
--security-opt apparmor=docker-webserver \
min-webserver:latest
# Verificer at profilen er aktiv
docker inspect --format '{{.HostConfig.SecurityOpt}}' webserver
# Output: [apparmor=docker-webserver]
# Se AppArmor-status
sudo aa-status | grep docker
Kombineret sikkerhedskonfiguration
For maksimal runtime-sikkerhed bør du kombinere flere mekanismer. Her er et eksempel, der samler det hele — det kan virke som mange flag, men hvert enkelt har sin berettigelse:
# Den mest sikre måde at køre en container på
docker run -d \
--name sikker-webserver \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--security-opt seccomp=custom-seccomp.json \
--security-opt apparmor=docker-webserver \
--security-opt no-new-privileges:true \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--memory="256m" \
--memory-swap="256m" \
--cpus="0.5" \
--pids-limit=64 \
--user 1000:1000 \
--network min-isolerede-netværk \
-p 8080:8080 \
min-webserver:latest
Lad os gennemgå, hvad hver parameter gør:
--read-only— Gør containerens filsystem skrivebeskyttet--tmpfs /tmp— Opretter en midlertidig skrivbar tmpfs (uden exec-rettigheder)--security-opt no-new-privileges:true— Forhindrer privilege escalation via setuid/setgid--cap-drop ALL— Fjerner alle Linux capabilities--cap-add NET_BIND_SERVICE— Tilføjer kun den specifikke capability, der er nødvendig--pids-limit=64— Begrænser antallet af processer (forhindrer fork bombs)--user 1000:1000— Kører som ikke-root bruger
Runtime-overvågning med Falco
Mens seccomp og AppArmor forhindrer uønskede handlinger, giver Falco (fra CNCF) dig mulighed for at detektere og alarmere om mistænkelig container-aktivitet i realtid. Det er lidt som at have et alarmsystem i dit hus — selvom du har låst alle døre og vinduer, vil du stadig gerne vide, hvis nogen prøver at bryde ind.
Falco bruger enten en kernel-modul eller en eBPF-driver til at observere systemkald, og det er overraskende nemt at komme i gang med.
Installation af Falco med eBPF
# Installation via Helm (anbefalet til Kubernetes)
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco \
--namespace falco \
--create-namespace \
--set driver.kind=modern_ebpf \
--set falcosidekick.enabled=true \
--set falcosidekick.config.slack.webhookurl="https://hooks.slack.com/services/DIN/WEBHOOK/URL"
# Installation direkte på en Linux-vært (Debian/Ubuntu)
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | \
sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" | \
sudo tee /etc/apt/sources.list.d/falcosecurity.list
sudo apt update
sudo apt install -y falco
# Aktiver modern eBPF-driver (anbefalet, kræver kernel >= 5.8)
sudo falcoctl driver install --type modern_ebpf
# Start Falco
sudo systemctl enable --now falco-modern-bpf.service
Tilpassede Falco-regler til container-sikkerhed
Falcos styrke ligger i dets regelbaserede system. Du kan definere præcis, hvad der udgør "mistænkelig aktivitet" i dit specifikke miljø. Her er et sæt regler, som vi har haft god erfaring med i praksis:
# Fil: /etc/falco/rules.d/custom-container-rules.yaml
# Regel 1: Detekter shell-eksekvering i containere
- rule: Shell spawned in container
desc: >
En shell-proces (bash, sh, zsh osv.) er blevet startet i en container.
Dette kan indikere et angreb eller uautoriseret debugging.
condition: >
spawned_process
and container
and proc.name in (bash, sh, zsh, dash, ash, csh, ksh, fish)
and not container.image.repository in (
"tilladt-debug-image",
"min-ci-runner"
)
output: >
Shell startet i container
(bruger=%user.name kommando=%proc.cmdline container=%container.name
image=%container.image.repository:%container.image.tag
pod=%k8s.pod.name namespace=%k8s.ns.name)
priority: WARNING
tags: [container, shell, mitre_execution]
# Regel 2: Detekter forsøg på at læse følsomme filer
- rule: Sensitive file read in container
desc: >
En proces i en container forsøger at læse følsomme filer
som /etc/shadow, private nøgler eller cloud credentials.
condition: >
open_read
and container
and (
fd.name startswith /etc/shadow or
fd.name startswith /etc/gshadow or
fd.name contains "/.ssh/id_" or
fd.name contains "/.aws/credentials" or
fd.name contains "/.kube/config" or
fd.name endswith ".pem" or
fd.name endswith ".key"
)
output: >
Følsom fil læst i container
(fil=%fd.name bruger=%user.name proces=%proc.name
container=%container.name image=%container.image.repository)
priority: CRITICAL
tags: [container, credential_access, mitre_credential_access]
# Regel 3: Detekter container escape-forsøg
- rule: Container escape attempt via mount
desc: >
En proces i en container forsøger at montere et filsystem,
hvilket kan indikere et container escape-forsøg.
condition: >
evt.type in (mount, umount2)
and container
and not proc.name in (mount, umount)
output: >
Mount-forsøg detekteret i container
(kommando=%proc.cmdline container=%container.name
image=%container.image.repository bruger=%user.name)
priority: CRITICAL
tags: [container, escape, mitre_privilege_escalation]
# Regel 4: Detekter kryptomining i containere
- rule: Crypto mining activity in container
desc: >
Detekterer forbindelser til kendte mining-pools
eller processer med mining-relaterede navne.
condition: >
container
and (
(spawned_process and proc.name in (xmrig, minerd, minergate, cpuminer))
or
(evt.type = connect and fd.sip.name contains "mining" or
fd.sport in (3333, 4444, 5555, 8333, 9999, 14444))
)
output: >
Mulig kryptomining detekteret i container
(proces=%proc.name container=%container.name
image=%container.image.repository forbindelse=%fd.name)
priority: CRITICAL
tags: [container, cryptomining, mitre_resource_hijacking]
# Regel 5: Detekter ændringer i /etc inde i containere
- rule: Modification of etc directory in container
desc: >
Ændringer i /etc kan indikere persistence-forsøg
eller konfigurationsmanipulation.
condition: >
open_write
and container
and fd.name startswith /etc/
and not proc.name in (sed, tee)
and not container.image.repository in ("tilladt-config-image")
output: >
Fil i /etc ændret i container
(fil=%fd.name proces=%proc.name container=%container.name
image=%container.image.repository)
priority: WARNING
tags: [container, persistence, mitre_persistence]
# Genindlæs Falco-regler uden genstart
sudo kill -SIGHUP $(pidof falco)
# Test reglerne ved at starte en shell i en container
docker exec -it min-container bash
# → Falco genererer en alarm
# Se Falco-logfiler
sudo journalctl -u falco-modern-bpf.service -f
# Kør Falco i forgrunden med verbose output (debugging)
sudo falco -r /etc/falco/falco_rules.yaml \
-r /etc/falco/rules.d/custom-container-rules.yaml \
--verbose
Integration med Falcosidekick
Falcosidekick er en companion-tjeneste, der videresender Falco-alarmer til mere end 50 forskellige destinationer — Slack, PagerDuty, Elasticsearch, Prometheus, AWS SNS og mange flere. Det gør det muligt at integrere container-sikkerhedsovervågning direkte i dine eksisterende incident response-workflows, hvilket virkelig er der, magien sker:
# Falcosidekick konfiguration (values.yaml for Helm)
falcosidekick:
enabled: true
config:
slack:
webhookurl: "https://hooks.slack.com/services/DIN/WEBHOOK"
minimumpriority: "warning"
messageformat: "Falco Alert: {{ .Output }}"
prometheus:
enabled: true
elasticsearch:
hostport: "https://elasticsearch.intern.dk:9200"
index: "falco-alerts"
minimumpriority: "notice"
Alternative container-runtimes: gVisor, Kata Containers og rootless
Når standard container-isolation simpelthen ikke er nok — f.eks. ved kørsel af upålidelig kode, i multi-tenant-miljøer eller under strenge sikkerhedskrav — kan alternative container-runtimes give et ekstra lag af beskyttelse.
gVisor: En bruger-rum kerne
gVisor (udviklet af Google) implementerer en bruger-rum kerne kaldet Sentry, der opfanger og håndterer systemkald fra containeren. I stedet for at containerens processer taler direkte med værtens kerne, går alle systemkald gennem Sentry, som implementerer en begrænset delmængde af Linux-kernens systemkald i et sandboxed miljø.
Det er en ret elegant løsning, skal det siges.
# Installation af gVisor (runsc)
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | \
sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt update && sudo apt install -y runsc
# Konfigurer Docker til at bruge gVisor
# Tilføj til /etc/docker/daemon.json:
{
"runtimes": {
"runsc": {
"path": "/usr/bin/runsc"
}
}
}
# Genstart Docker
sudo systemctl restart docker
# Kør en container med gVisor
docker run --runtime=runsc -d --name sikker-sandbox min-app:latest
# Verificer at gVisor er aktiv
docker exec sikker-sandbox dmesg | head -5
# Output: [ 0.000000] Starting gVisor...
Fordele ved gVisor:
- Dramatisk reduceret kernel-angrebsflade — kun ~200 af ~300+ systemkald er implementeret
- Skrevet i Go med memory safety (ingen buffer overflows)
- Nem integration med Docker og Kubernetes
- Ingen speciel hardware kræves
Ulemper:
- Ydelses-overhead, især for I/O-intensive workloads (10-30% langsommere)
- Ikke alle applikationer er kompatible (specialiserede syscalls mangler)
- Debugging kan være mere udfordrende
Kata Containers: Lette virtuelle maskiner
Kata Containers kombinerer fordelene ved containere (hastighed, OCI-kompatibilitet) med sikkerheden fra virtuelle maskiner. Hver container — eller pod — kører i sin egen letvægts-VM med en dedikeret kerne, isoleret af hardware-virtualisering. Det er på en måde det bedste fra begge verdener.
# Installation af Kata Containers
sudo apt install -y kata-containers
# Konfigurer Docker til at bruge Kata
# Tilføj til /etc/docker/daemon.json:
{
"runtimes": {
"kata": {
"path": "/usr/bin/kata-runtime"
}
}
}
sudo systemctl restart docker
# Kør en container med Kata
docker run --runtime=kata -d --name vm-isoleret min-app:latest
# Verificer at Kata kører (du kan se den separate kerne)
docker exec vm-isoleret uname -r
# Output viser Kata's gæste-kerne, ikke værtens kerne
# For Kubernetes: Brug RuntimeClass
# Opret RuntimeClass:
cat <<'YAML' | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
YAML
# Brug den i en Pod:
cat <<'YAML' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: sikker-pod
spec:
runtimeClassName: kata
containers:
- name: app
image: min-app:latest
YAML
Fordele ved Kata Containers:
- Hardware-baseret isolation — ægte VM-sikkerhed
- Separat kerne pr. container — kernel-sårbarheder påvirker kun den ene container
- OCI-kompatibel — fungerer med Docker og Kubernetes
- Ideel til multi-tenant-miljøer
Ulemper:
- Højere ressourceforbrug pr. container (separat kerne og VM-overhead)
- Kræver hardware-virtualiseringsunderstøttelse
- Langsamere opstartstid end standard containere
- Mere kompleks netværkskonfiguration
Rootless Docker og Podman
Rootless containere kører hele container-runtimen — inklusive dæmonen — som en uprivilegeret bruger. Det eliminerer en hel kategori af angreb, da selv en succesfuld container escape kun giver adgang til den uprivilegerede brugers rettigheder. Det er nok den nemmeste sikkerhedsopgradering, du kan lave.
# Opsæt rootless Docker
dockerd-rootless-setuptool.sh install
# Sæt miljøvariabler (tilføj til ~/.bashrc)
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
# Kør containere som rootless
docker run -d --name rootless-app min-app:latest
# Alternativ: Brug Podman (rootless som standard)
# Installation af Podman
sudo apt install -y podman
# Podman kører rootless som standard - ingen dæmon nødvendig
podman run -d --name sikker-app min-app:latest
# Podman understøtter også Docker Compose-formatet
podman compose up -d
# Verificer at containeren kører rootless
podman top sikker-app user huser
# Output viser bruger-mapping mellem container og vært
Hvornår skal du bruge hvad?
Det er et spørgsmål, vi hører ret ofte. Her er en oversigt, der forhåbentlig gør valget lidt nemmere:
| Scenarie | Anbefalet Runtime | Begrundelse |
|---|---|---|
| Standard webapplikationer | Rootless Docker/Podman + seccomp | God balance mellem sikkerhed og ydeevne |
| CI/CD build-jobs | gVisor | Kører vilkårlig kode med reduceret angrebsflade |
| Multi-tenant SaaS | Kata Containers | Hardware-isolation mellem kunder |
| Kørsel af upålidelig kode | gVisor eller Kata | Maksimal isolation fra værtssystemet |
| Høj-ydeevne workloads | Standard runC + hærdning | Laveste overhead, hærdning via seccomp/AppArmor |
| Udvikler-workstations | Podman rootless | Ingen root-krav, ingen dæmon, Docker-kompatibel |
Best practices-tjekliste for container-sikkerhed
Okay, vi har dækket en masse grund. Her er en samlet tjekliste, der samler alle anbefalingerne fra denne guide. Brug den som reference, når du opbygger og drifter container-baserede systemer.
Image-sikkerhed
- Brug minimale base-images — Alpine, Distroless eller scratch. Undgå store general-purpose images som ubuntu, medmindre du virkelig har brug for dem.
- Anvend multi-stage builds — Adskil bygge- og runtime-miljøer for at minimere angrebsfladen.
- Pin image-versioner — Brug altid specifikke tags eller SHA256-digests. Aldrig
:latesti produktion. - Scan images automatisk — Integrer Trivy eller Grype i CI/CD-pipelinen, og lad buildet fejle ved kritiske CVE'er.
- Signer container-images — Brug Cosign til at signere og verificere images i hele supply chain.
- Undgå hemmeligheder i images — Brug Docker secrets, Kubernetes secrets eller en ekstern vault i stedet.
- Anvend .dockerignore — Udeluk unødvendige filer fra build-konteksten.
Runtime-konfiguration
- Kør aldrig som root — Brug
USER-direktivet i Dockerfile og--userflaget ved kørsel. - Drop alle capabilities — Brug
--cap-drop ALLog tilføj kun de specifikke, nødvendige capabilities. - Aktiver no-new-privileges — Forhindrer privilege escalation via setuid/setgid binærer.
- Brug read-only filsystem — Monter containerens root-filsystem som read-only med
--read-only. - Begræns ressourcer — Sæt grænser for hukommelse, CPU og PID-antal med cgroup-begrænsninger.
- Anvend tilpassede seccomp-profiler — Tillad kun de systemkald, din applikation faktisk bruger.
- Konfigurer AppArmor-profiler — Begræns filadgang og netværksoperationer med MAC-politikker.
- Brug aldrig
--privileged— Det deaktiverer de fleste sikkerhedsmekanismer og giver næsten fuld værtsadgang. Bare lad være.
Netværkssikkerhed
- Isoler containernetværk — Opret dedikerede Docker-netværk for hver applikationsgruppe.
- Begræns udgående trafik — Brug netværkspolitikker (Kubernetes NetworkPolicy) til at begrænse egress.
- Kryptér kommunikation — Brug mTLS (mutual TLS) for kommunikation mellem containere.
- Publicer ikke unødvendige porte — Bind kun de porte, der faktisk er nødvendige.
Orkestrering og drift
- Hold runC opdateret — Sørg for at køre en patched version (minimum 1.2.8, 1.3.3 eller 1.4.0-rc.3).
- Opdater regelmæssigt — Følg sikkerhedsopdateringer for Docker, containerd og Kubernetes.
- Aktiver audit-logging — Brug Falco eller lignende værktøjer til at overvåge container-aktivitet.
- Implementer admission controllers — Brug OPA Gatekeeper eller Kyverno i Kubernetes til at håndhæve sikkerhedspolitikker.
- Brug Pod Security Standards — Konfigurer Pod Security Admission med "restricted" profilen.
- Overvej alternative runtimes — Brug gVisor eller Kata Containers til højrisiko-workloads.
- Implementer image-politikker — Tillad kun images fra godkendte registries og med gyldige signaturer.
Kubernetes-specifik sikkerhed
# Eksempel: Pod Security Standards (restricted)
apiVersion: v1
kind: Namespace
metadata:
name: sikker-namespace
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
# Eksempel: Kyverno-politik der forbyder privilegerede containere
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: forbyd-privilegerede-containere
spec:
validationFailureAction: Enforce
rules:
- name: tjek-privilegeret
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Privilegerede containere er ikke tilladt."
pattern:
spec:
containers:
- securityContext:
privileged: "false"
---
# Eksempel: NetworkPolicy der begrænser trafik
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: begræns-intern-trafik
namespace: sikker-namespace
spec:
podSelector:
matchLabels:
app: webserver
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to: # Tillad DNS-opslag
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
Konklusion: Container-sikkerhed er en rejse, ikke en destination
Container-sikkerhed er ikke noget, du løser én gang og så kan glemme alt om. Det er en kontinuerlig proces, der kræver opmærksomhed på flere niveauer — fra de images du bygger, over runtime-konfigurationen, til den overvågning du har i produktion.
De runC-sårbarheder, vi har gennemgået (CVE-2025-31133, CVE-2025-52565 og CVE-2025-52881), illustrerer en vigtig pointe: selv den mest fundamentale komponent i container-stakken kan have kritiske fejl. Patch management er ikke valgfrit. Sørg for, at din infrastruktur kører patchede versioner af runC, og hav processer på plads til hurtigt at reagere på fremtidige sikkerhedsopdateringer.
Men patching alene er ikke nok. Du har brug for en defense-in-depth-strategi:
- Minimer angrebsfladen — Brug minimale base-images, multi-stage builds og automatiseret sårbarhedsscanning.
- Begræns privilegier — Kør aldrig som root, drop alle capabilities, og anvend seccomp og AppArmor.
- Overvåg runtime-adfærd — Implementer Falco med tilpassede regler for at detektere mistænkelig aktivitet i realtid.
- Overvej stærkere isolation — For følsomme workloads bør du evaluere gVisor eller Kata Containers.
- Håndhæv politikker — Brug Kubernetes admission controllers, Pod Security Standards og netværkspolitikker.
Container-teknologien fortsætter med at udvikle sig, og det samme gør truslerne. WebAssembly (WASM) begynder at dukke op som et potentielt alternativ til containere i visse scenarier, og nye sandboxing-teknologier baseret på eBPF er under aktiv udvikling. Men uanset hvilken teknologi du ender med at bruge, forbliver de grundlæggende principper de samme: minimer angrebsfladen, begræns privilegier, overvåg adfærd, og patch hurtigt.
Ved at anvende de teknikker og værktøjer, vi har gennemgået i denne guide, er du godt rustet til at beskytte dine containeriserede applikationer — både mod de trusler, vi ser i 2026, og dem der uundgåeligt vil komme i fremtiden.