Hærdning af systemd-tjenester på Linux i 2026: Sandboxing og produktionsklare profiler

Lær at hærde systemd-tjenester med sandboxing-direktiver som ProtectSystem og SystemCallFilter. Indeholder en produktionsklar template, systemd-analyze security i praksis og fejlfindingstips fra ægte drift.

Hærd systemd-tjenester: Linux Guide 2026

Opdateret: 6. juni 2026

Hærdning af systemd-tjenester på Linux betyder at låse hver .service-unit ned med systemd's indbyggede sandboxing-direktiver (NoNewPrivileges=yes, ProtectSystem=strict, PrivateTmp=yes, SystemCallFilter= og CapabilityBoundingSet=), så et kompromitteret daemon ikke kan eskalere rettigheder, skrive til /etc eller læse andre brugeres data. På et moderne system (systemd ≥ 256) kan jeg reducere en gennemsnitlig Exposure Level fra over 9.0 til under 2.0 ved kun at redigere unit-filen. Ingen kerneopdateringer, ingen LSM-politikker, ingen container-runtime. I denne guide gennemgår jeg de vigtigste direktiver, viser systemd-analyze security i praksis og leverer en produktionsklar template, jeg har brugt i ægte drift.

  • systemd-analyze security scorer hver unit fra 0.0 (perfekt) til 10.0 (kritisk eksponeret). Målet i produktion er under 3.0.
  • NoNewPrivileges=yes er nul-omkostnings og blokerer setuid-eskalering. Det burde være standard på enhver tjeneste fra dag ét.
  • ProtectSystem=strict + ProtectHome=yes gør filsystemet read-only for tjenesten. Kombiner med ReadWritePaths= for de få nødvendige skrivelokationer.
  • SystemCallFilter=@system-service aktiverer en seccomp-allowlist, der typisk blokerer 80–90 % af kernens syscall-overflade for almindelige services.
  • DynamicUser=yes kører tjenesten som en kortlivet UID/GID uden hjemmemappe og overflødiggør manuel brugeroprettelse i de fleste tilfælde.
  • Hærdning sker i lag: systemd-sandboxing oven på Landlock LSM, SELinux/AppArmor og en stram nftables-firewall.

Hvorfor hærde systemd-tjenester overhovedet?

Standardadfærden for en .service-unit er forbavsende generøs. Tjenesten kører som root, hvis du ikke siger andet, har fuld læseadgang til hele filsystemet, fuld adgang til kernens netværks- og fil-namespaces, kan kalde enhver syscall og kan oprette nye processer, der bevarer alle capabilities. Hvis CVE-numret på dit favoritbibliotek (curl, libxml2, openssl) lander dårligt næste uge, betyder det, at et exploit i din log-forwarder potentielt får root-shell på maskinen.

Den traditionelle modforanstaltning (at skrive en SELinux- eller AppArmor-politik) kræver dyb ekspertise og brydes hver gang programmet skifter version. Systemd-sandboxing er et meget billigere første lag: direktiverne ligger i selve unit-filen, opdateres sammen med tjenesten, og virker uanset hvilken distribution du kører på. Lennart Poettering selv kalder det "the cheapest hardening you can buy". I mit arbejde med Debian 13 og RHEL 10 servere ser jeg typisk, at omhyggelig sandboxing reducerer brugbar angrebsoverflade mere end at indføre en LSM-politik, og det tager 30 minutter pr. tjeneste i stedet for 30 timer.

Vigtigst er dog, at hærdning er kumulativ med andre forsvarslag. Når du allerede har en stram nftables-firewall og auditd-overvågning, fungerer systemd-direktiverne som forhindringslaget i defense-in-depth-modellen. De stopper et exploit i at få sin payload til at virke, selv hvis den slipper forbi netværk og log-detektion.

Hvordan bruger jeg systemd-analyze security?

Den hurtigste måde at vurdere en tjenestes nuværende eksponeringsniveau er kommandoen systemd-analyze security. Den scorer hver unit fra 0.0 (helt sandkasset) til 10.0 (alt åbent), og fortæller dig præcis hvilke direktiver der mangler. Kør den uden argumenter for at få en oversigt over alle aktive units:

# Vis sikkerhedsscores for alle running services
systemd-analyze security --no-pager

# Detaljeret rapport for én specifik tjeneste
systemd-analyze security nginx.service

# Sammenlign flere units side om side
systemd-analyze security nginx.service postgresql.service redis.service

Outputtet for en typisk unit ligner dette (forkortet):

  NAME                                        DESCRIPTION                          EXPOSURE
✗ PrivateNetwork=                              Service has access to the host's    0.5
✓ User=/DynamicUser=                           Service runs as root user             0.4
✗ CapabilityBoundingSet=~CAP_SYS_ADMIN         Service has no administrator         0.3
✗ SystemCallFilter=~@swap                      Service may issue swap syscalls      0.3
...
→ Overall exposure level for nginx.service:    8.6 EXPOSED 🙁

Hvert direktiv, som er markeret med , repræsenterer en konkret hærdningsmulighed, og den vægtede sum giver det samlede tal. Et mål under 3.0 OK er realistisk for de fleste tjenester. Under 1.0 PROTECTED kræver typisk DynamicUser, PrivateNetwork og fuld syscall-filtrering, hvilket ikke altid er muligt for netværksdaemons. Bemærk, at en høj score ikke nødvendigvis betyder, at tjenesten er sårbar; den er bare eksponeret. Forskellen er vigtig: en intern admin-CLI er måske fint at lade være.

Kernedirektiverne, du skal kende

Du behøver ikke memorere alle ~60 sandboxing-direktiver. Følgende ti dækker over 80 % af gevinsten ifølge min erfaring og den officielle systemd.exec(5) man-page:

DirektivEffektSikker default i 2026
NoNewPrivilegesBlokerer setuid/setgid og fil-capabilitiesyes
ProtectSystemGør /usr, /boot, /etc read-only eller utilgængeligestrict
ProtectHomeSkjuler /home, /root, /run/useryes
PrivateTmpPrivat /tmp per serviceyes
PrivateDevicesSkjuler fysiske enheder; kun /dev/null, zero, randomyes
PrivateNetworkTjeneste får eget tomt netværks-namespaceyes hvis muligt
ProtectKernelTunablesRead-only /proc/sys og /sysyes
ProtectKernelModulesBlokerer init_module/finit_moduleyes
ProtectKernelLogsBlokerer /dev/kmsg og syslog-syscallsyes
RestrictNamespacesForbyder oprettelse af nye namespacesyes

Tre direktiver, der altid skal med

Hvis du absolut intet andet gør, så slå disse tre til på enhver custom service:

[Service]
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes

De tre linjer alene flytter typisk scoren fra 9.x til 6.x, og koster ingenting i drift. ProtectSystem=strict er strengere end den ældre ProtectSystem=full, fordi den også gør /etc read-only. Hvis din tjeneste skal skrive til en konfigurationsfil eller en cache, skal du eksplicit liste det:

ReadWritePaths=/var/lib/myservice /var/log/myservice
ReadOnlyPaths=/etc/myservice

Seccomp-filtrering med SystemCallFilter

Linux har omkring 380 syscalls. De fleste daemons bruger måske 50 af dem. SystemCallFilter= lader dig blokere alt andet via kernens seccomp-BPF infrastruktur. Systemd har predefinerede grupper (@system-service, @network-io, @file-system, @process), der gør dette praktisk uden at du skal liste hver enkelt syscall:

[Service]
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @debug @mount @swap @reboot @raw-io
SystemCallArchitectures=native

Linje 2 starter med en allowlist (@system-service er en kurateret samling, som er sikker for almindelige tjenester). Linje 3 trækker fra: tilde-præfikset betyder "fjern fra det tilladte sæt". Linje 4 begrænser tjenesten til værtens native arkitektur, hvilket blokerer 32-bit syscall-tabellen på en x86_64-host. Det har historisk været en kilde til CVE'er via misalignment mellem de to ABI'er.

Restrict-direktiver til netværk og adresser

Hvis din tjeneste kun bruger TCP og UDP, skal den ikke kunne åbne en raw-socket eller en AF_PACKET-socket. RestrictAddressFamilies= låser dette:

RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes

MemoryDenyWriteExecute=yes blokerer, at en hukommelsesside er både writable og executable. Det er den klassiske mitigation mod shellcode-injektion. Vær opmærksom på, at JIT-runtimes (V8, JVM, .NET) bryder denne mitigation; for de tjenester er du nødt til at lade den være no.

CapabilityBoundingSet og Linux capabilities

Når en tjeneste skal binde til en port under 1024 eller læse rå pakker, bruger du typisk CAP_NET_BIND_SERVICE eller CAP_NET_RAW i stedet for at køre den som root. Med systemd kombineres dette ofte med AmbientCapabilities=, så capability'en faktisk er aktiv i den nye proces:

[Service]
User=webapp
Group=webapp
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

Resten af kernens 40+ capabilities forsvinder. Det betyder, at selv hvis processen får root-UID via en exploit, kan den ikke loade kernel-moduler (CAP_SYS_MODULE), montere filsystemer (CAP_SYS_ADMIN) eller ændre tidszonen (CAP_SYS_TIME). For tjenester, der slet ikke behøver nogen capability (f.eks. en stateless API-server bag en reverse proxy), sæt blot CapabilityBoundingSet= tom og lad User= være en ikke-priviligeret bruger.

Produktionsklart eksempel: en hærdet web-API

Her er en komplet .service-fil, jeg bruger som template til Go- og Rust-baserede API'er. Den scorer typisk 1.8–2.4 i systemd-analyze security:

[Unit]
Description=My production API
Documentation=https://internal.example.com/myapi
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/local/bin/myapi --config=/etc/myapi/config.toml
Restart=on-failure
RestartSec=5s

# Identitet
DynamicUser=yes
SupplementaryGroups=

# Filsystem
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectHostname=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
ProcSubset=pid
ReadWritePaths=/var/lib/myapi
ReadOnlyPaths=/etc/myapi
StateDirectory=myapi
ConfigurationDirectory=myapi

# Processer
NoNewPrivileges=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
UMask=0077

# Capabilities
CapabilityBoundingSet=
AmbientCapabilities=

# Netværk
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
IPAddressDeny=any
IPAddressAllow=localhost 10.0.0.0/8

# Syscalls
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @debug @mount @swap @reboot @raw-io @cpu-emulation @obsolete

# Namespaces
RestrictNamespaces=yes
PrivateUsers=yes

# Ressourcer (DoS-beskyttelse)
LimitNOFILE=65536
TasksMax=512
MemoryHigh=512M
MemoryMax=768M

[Install]
WantedBy=multi-user.target

Bemærk StateDirectory= og ConfigurationDirectory=: de instruerer systemd til at oprette /var/lib/myapi og /etc/myapi med de korrekte ejer-ID'er fra DynamicUser, så du ikke selv skal lave chown-magi i pre-start-hooks. Ærligt talt er det en af de mest oversete features i hele systemd.

Hvad gør DynamicUser, og hvornår skal jeg bruge det?

DynamicUser=yes instruerer systemd til at allokere en kortlivet UID og GID i intervallet 61184–65519 hver gang tjenesten starter. Brugeren findes ikke i /etc/passwd. Den eksisterer kun i kernel-tabellerne mens tjenesten kører, og forsvinder igen ved stop. Det betyder:

  • Du behøver ikke oprette en system-bruger med useradd --system i pakke-postinst-scripts.
  • Tjenestens UID kan ikke kollidere med eksisterende brugere på systemet.
  • Hjemmemappen er en tmpfs, som forsvinder ved restart, hvilket er perfekt mod persistens-angreb.
  • State persisterer kun gennem StateDirectory=, som systemd mounter ind og chown'er ved hver opstart.

Hvornår skal du ikke bruge det? Hvis din tjeneste skal eje filer, der overlever den (f.eks. en database, der lægger filer på en NFS-share), eller hvis du har en hardcoded UID i tredjeparts-kode (visse legacy enterprise-pakker). Til 90 % af nye services er DynamicUser=yes dog det rigtige valg.

Fejlfinding: når en hærdet tjeneste pludselig fejler

Den mest almindelige fejlmeddelelse efter aggressiv hærdning er Operation not permitted eller Permission denied, og standardloggingen siger sjældent hvilket direktiv der blokerede. Værktøjerne, der har sparet mig timer:

1. Kør med strace i staging

# Tilføj midlertidigt til [Service]-sektionen:
Environment=SYSTEMD_LOG_LEVEL=debug
ExecStart=/usr/bin/strace -f -e trace=openat,connect,bind,mount /usr/local/bin/myapi

Den blokerede syscall vises som EPERM i journalctl-outputtet. Fra det kan du udlede, hvilket direktiv der skal slækkes.

2. Brug systemd-analyze filesystems og syscall-filter

# Hvilke syscalls indeholder @system-service?
systemd-analyze syscall-filter @system-service

# Hvilke filsystemer ville RestrictFileSystems= blokere?
systemd-analyze filesystems

3. Læn dig på auditd til den skarpe fejlsøgning

Hvis du allerede har auditd kørende, kan du tilføje en regel, der fanger alle EPERM-fejl for tjenestens bruger:

auditctl -a always,exit -F arch=b64 -F success=0 -F exit=-EPERM -F uid=<dyn_uid> -k systemd-block
# senere:
ausearch -k systemd-block --start recent

Det giver dig en præcis liste over hvilke filer og syscalls der blev nægtet. Uvurderligt, når strace fylder loggen.

Integration med SELinux, AppArmor og auditd

Systemd-sandboxing erstatter ikke en Mandatory Access Control-politik. Den supplerer den. Når jeg bygger sikkerhedsstak i produktion, lægger jeg lagene i denne rækkefølge:

  1. Systemd-direktiverne (dette dokument): første forsvarslag, indbygget, gratis.
  2. SELinux eller AppArmor: MAC-politik, der kapsler hele processtræet ind, også når sandboxing er løs.
  3. Landlock LSM: applikationen kan selv-sandboxe sine subprocesses uden CAP_SYS_ADMIN.
  4. nftables: kernel-pakkefilter foran IPAddressDeny=.
  5. Auditd og overvågning: detektion, når et lag er kompromitteret.

Tom Hatzenbichler og andre i systemd-fællesskabet har dokumenteret denne lag-tilgang grundigt, og systemd's egen NEWS-fil annoncerer regelmæssigt nye direktiver. I 256-udgivelsen kom ImportCredential= og forbedret ProtectProc=ptraceable, som begge er værd at læse om.

Så til sidst: husk at hærdning er en proces, ikke en engangsopgave. Kør systemd-analyze security som del af jeres CI-pipeline, gennemgå scores ved hver pakke-opgradering, og dokumentér i unit-filens kommentarer hvorfor du har slækket et direktiv. Det redder den næste vagthavende fra at gætte.

Ofte stillede spørgsmål

Hvad er en god systemd-analyze security score?

Under 3.0 OK er målet for de fleste produktionstjenester. Under 1.0 PROTECTED er muligt for selvstændige daemons med DynamicUser og PrivateNetwork, mens netværksvendte API'er sjældent kommer under 1.8, fordi de skal binde til en port. En score over 6.0 betyder, at tjenesten kører tæt på default-eksponering, og der er lavt-hængende frugt at høste.

Bryder hærdning eksisterende systemd-tjenester?

Ja, det kan det. Især SystemCallFilter=, MemoryDenyWriteExecute=yes og RestrictNamespaces=yes rammer ofte JIT-runtimes, container-tools og programmer, der opretter user namespaces. Test altid i staging, indfør direktiver gradvist, og overvåg journalctl -u tjenestenavn for EPERM-fejl i de første 24 timer efter ændringen.

Hvad er forskellen på ProtectSystem=full og ProtectSystem=strict?

full gør /usr, /boot og /efi read-only, men lader /etc være skrivbart. strict gør hele filsystemet read-only på nær /dev, /proc og /sys, så du må eksplicit liste skrivbare stier med ReadWritePaths=. strict er det rigtige valg til alle nye services; full findes primært af bagudkompatibilitetshensyn.

Skal jeg bruge DynamicUser eller oprette en dedikeret system-bruger?

Brug DynamicUser=yes, medmindre du har en konkret grund til det modsatte. For eksempel hvis tjenesten skal eje filer på en NFS-share, der persisterer over restarts, eller hvis en tredjepartspakke hardcoder UID'et. Dynamic users har færre fejlmuligheder, lavere vedligehold og bedre sikkerhedsegenskaber for state-lette netværkstjenester.

Kan jeg bruge systemd-hærdning sammen med Docker eller Podman?

Ja, og det er en god idé. Når du kører podman run eller docker run som en systemd-service (genereret med podman generate systemd eller quadlet), gælder unit-direktiverne stadig for selve container-runtime. Vær opmærksom på at PrivateNetwork=yes og RestrictNamespaces=yes typisk skal være no for container-runners, da de selv har brug for at oprette netværks- og pid-namespaces.

Om Forfatteren Editorial Team

Our team of expert writers and editors.