This skill outlines a methodology for determining the minimal systemd security configuration for any application by profiling its runtime behavior using strace.
Goal: Determine strictly which paths the application reads and writes to.
Run the application in the foreground (if possible) or attach to the process.
# Capture file operations
strace -ff -o /tmp/app_fs.strace -e trace=file,process <binary> <args>1. Identify Write Access (for ReadWritePaths)
Filter for flags indicating write operations (O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND).
grep -R --line-number 'openat(' /tmp/app_fs.strace* | \
grep -E 'O_WRONLY|O_RDWR|O_CREAT|O_TRUNC|O_APPEND' | \
sed -E 's/.*openat\([^,]+, "([^"]+)".*/\1/' | sort -u- Action: Add these paths to
ReadWritePaths=. - Tip: Verify if the application does "lazy" writing (e.g., temp files for large uploads/buffers) by forcing load/traffic during the trace.
2. Identify Read Access (for ReadOnlyPaths / InaccessiblePaths)
Filter for all openat calls to see what is touched.
grep -RhoE 'openat\([^,]+, "([^"]+)"' /tmp/app_fs.strace* | \
sed -E 's/.*"([^"]+)"/\1/' | sort -u- Noise Filtering:
ENOENTon/etc/ld.so.preload: Normal loader behavior.ENOENTon/var/run/nscd/socketor/var/lib/sss/*: Normal glibc NSS checks. Not a failure if you don't use LDAP/SSSD.
- Action: Ensure these paths are readable. Everything else can potentially be
InaccessiblePaths.
Goal: Prevent the application from executing arbitrary binaries (shell injection mitigation).
strace -ff -o /tmp/app_exec.strace -e trace=process <binary> <args>Look for execve calls. mmap of libraries does not count as execution for this directive.
grep -RhoE 'execve\("([^"]+)"' /tmp/app_exec.strace* | \
sed -E 's/.*"([^"]+)".*/\1/' | sort -u- Result:
- If only the binary itself appears:
ExecPaths=<path/to/binary>andNoExecPaths=/. - If other binaries appear (e.g., wrappers): Add them to
ExecPaths.
- If only the binary itself appears:
Goal: Prevent the application from manipulating process namespaces (container breakouts, hidden processes).
strace -ff -o /tmp/app_ns.strace -e trace=clone,unshare,setns <binary> <args>Check for usage of CLONE_NEW* flags, unshare(), or setns().
grep -R --line-number -E 'clone\(|unshare\(|setns\(' /tmp/app_ns.strace*- Interpretation:
- Standard
clone()for threads/processes is safe. - Look specifically for:
CLONE_NEWNS,CLONE_NEWNET,CLONE_NEWUSER,CLONE_NEWPID,CLONE_NEWIPC,CLONE_NEWUTS.
- Standard
- Action:
- If no namespace flags are seen:
RestrictNamespaces=yes. - If specific flags are seen:
RestrictNamespaces=~<namespace_type>(block others).
- If no namespace flags are seen:
Goal: Drop all unneeded Linux capabilities.
strace -ff -o /tmp/app_caps.strace \
-e trace=capget,capset,setuid,setgid,setgroups,prctl,bind,chown,fchownat \
<binary> <args>1. Port Binding
Check for bind() on ports < 1024.
grep 'bind(' /tmp/app_caps.strace*- Action: If found, keep
CAP_NET_BIND_SERVICE.
2. Privilege Dropping
Check for setuid, setgid, setgroups.
grep -E 'setuid|setgid|setgroups' /tmp/app_caps.strace*- Action: If the app starts as root and drops privileges, keep
CAP_SETUIDandCAP_SETGID. - Hardening Opportunity: If possible, configure
systemdto start withUser=<non-root>and useAmbientCapabilities=CAP_NET_BIND_SERVICE(if needed), removing the need forCAP_SETUID/GIDentirely.
3. Other Capabilities
CAP_DAC_OVERRIDE: Often indicates bad file permissions/ownership rather than a real need. Fix permissions instead of granting this.CAP_CHOWN: Only if the app explicitly runschown.
Final Configuration: Start with:
CapabilityBoundingSet=Add only what is strictly observed (e.g., CapabilityBoundingSet=CAP_NET_BIND_SERVICE).
After applying systemd changes:
- Reload & Restart:
systemctl daemon-reload systemctl restart <service>
- Check for Crashes:
journalctl -u <service> -b --no-pager | tail -200
- Audit Failures:
If the service fails, re-run
stracespecifically looking forEACCESorEPERMerrors.grep -R --line-number -E 'EACCES|EPERM' /tmp/debug.strace*