Overrides and Sandboxing

Drop-In Overrides, Environment Files and Sandboxing

A drop-in is a fragment in /etc/systemd/system/<unit>.d/*.conf layered over the main file: the safe way to change a packaged unit such as apache2.service. sudo systemctl edit hello-api opens an editor on override.conf, --stdin (systemd 256 142,543 and later) reads it from a script, and both reload systemd. To replace a list setting such as ExecStart=, assign it empty first.

Secrets do not belong in a unit file, which anyone can read with systemctl cat. Put them in an environment file owned by root with mode 600: systemd reads it before switching to User=, so the application gets the values but cannot read the file. Its values override Environment=.

A secrets file and a hardening drop-in, scored before and afterShell
systemd-analyze security hello-api | grep -o "Overall.*[A-Z]"
printf 'APP_ENV=staging\nPORT=8105\nAPP_SECRET=change-me-9f3a\n' \
  | sudo install -m 600 /dev/stdin /etc/hello-api.env
sudo systemctl edit --stdin hello-api <<'EOF'
[Service]
EnvironmentFile=/etc/hello-api.env
NoNewPrivileges=yes
CapabilityBoundingSet=
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
SystemCallFilter=@system-service
EOF
sudo systemctl restart hello-api
curl -s http://127.0.0.1:8105/
systemd-analyze security hello-api | grep -o "Overall.*[A-Z]"
Output
Overall exposure level for hello-api.service: 9.2 UNSAFE
{"app":"hello-api","env":"staging","pid":2149,"php":"8.5.4","user":"helloapi"}
Overall exposure level for hello-api.service: 2.4 OK

The kernel enforces the sandbox with namespaces, capabilities and seccomp. ProtectSystem=strict makes the filesystem read-only for the service, so add ReadWritePaths= for anything it must write, such as uploads. NoNewPrivileges= and an empty CapabilityBoundingSet= stop it gaining privileges, and SystemCallFilter= blocks system calls an ordinary service never makes. Add directives one at a time and test after each; a service killed with signal=SYS tripped the system-call filter.