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æ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.
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:
Direktiv
Effekt
Sikker default i 2026
NoNewPrivileges
Blokerer setuid/setgid og fil-capabilities
yes
ProtectSystem
Gør /usr, /boot, /etc read-only eller utilgængelige
strict
ProtectHome
Skjuler /home, /root, /run/user
yes
PrivateTmp
Privat /tmp per service
yes
PrivateDevices
Skjuler fysiske enheder; kun /dev/null, zero, random
yes
PrivateNetwork
Tjeneste får eget tomt netværks-namespace
yes hvis muligt
ProtectKernelTunables
Read-only /proc/sys og /sys
yes
ProtectKernelModules
Blokerer init_module/finit_module
yes
ProtectKernelLogs
Blokerer /dev/kmsg og syslog-syscalls
yes
RestrictNamespaces
Forbyder oprettelse af nye namespaces
yes
Tre direktiver, der altid skal med
Hvis du absolut intet andet gør, så slå disse tre til på enhver custom service:
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:
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:
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:
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:
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:
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:
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:
Systemd-direktiverne (dette dokument): første forsvarslag, indbygget, gratis.
SELinux eller AppArmor: MAC-politik, der kapsler hele processtræet ind, også når sandboxing er løs.
Landlock LSM: applikationen kan selv-sandboxe sine subprocesses uden CAP_SYS_ADMIN.
nftables: kernel-pakkefilter foran IPAddressDeny=.
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.
Komplet 2026-guide til SSH-hærdning på Linux: FIDO2-hardware-nøgler, certifikat-baseret CA, post-kvante mlkem768x25519, kopier-klar sshd_config og audit med ssh-audit.
Lær at konfigurere Linux auditd til systemrevision, trusseldetektion og compliance i 2026. Guide med revisionsregler, loganalyse med ausearch og aureport, ydelsestuning og SIEM-integration.
Lær at automatisere CIS Benchmark-compliance på Linux-servere med OpenSCAP og Ansible. Praktisk guide med installation, scanning, remediation, tailoring og CI/CD-integration til RHEL 9 og Ubuntu 24.04.