Create and harden systemd service unit files following modern best practices...
This skill provides guidance for creating and hardening systemd service unit files following modern Linux service management best practices. It covers proper service type selection, dependency management, security sandboxing, and filesystem isolation to create reliable, secure services.
Use this skill when:
User request involves systemd service?
│
├─ Creating a new service?
│ │
│ ├─ Web application/API → Use "Creating a New Service" workflow
│ │ → Start from assets/basic-webapp.service or assets/hardened-webapp.service
│ │
│ ├─ Background worker/daemon → Use "Creating a New Service" workflow
│ │ → Start from assets/background-worker.service
│ │
│ └─ One-time initialization → Use "Creating a New Service" workflow
│ → Start from assets/oneshot-init.service
│
└─ Hardening existing service?
│
└─ Use "Hardening an Existing Service" workflow
→ Reference references/systemd_options.md for security options
→ Compare against assets/hardened-webapp.service for patterns
When creating a new systemd service, follow these steps:
Start with the most relevant template from assets/:
basic-webapp.service: Simple web application without heavy sandboxing (development/internal use)hardened-webapp.service: Production web application with full security hardeningbackground-worker.service: Queue processor, scheduled task, or background daemononeshot-init.service: One-time initialization script or setup taskCopy the template and customize the following sections in order:
Update the service metadata and dependencies:
[Unit]
Description=Clear description of what this service does
Documentation=https://example.com/docs or man:program(8)
After=network-online.target database.service
Wants=network-online.target
Requires=database.service
Key decisions:
After=network-online.target and Wants=network-online.targetSelect the appropriate service type based on the application's capabilities:
Recommended service types (in order of preference):
Type=notify: Application supports sd_notify protocol (best option for reliability)Type=exec: Standard long-running process (good default)Type=oneshot: One-time execution that exits (initialization scripts)Avoid Type=simple (poor error detection) and avoid Type=forking (deprecated pattern).
[Service]
Type=exec
ExecStart=/usr/bin/node /opt/webapp/server.js
WorkingDirectory=/opt/webapp
Always use absolute paths for ExecStart= and related commands.
Set the execution user and environment variables:
# Option 1: Dynamic user (recommended for security)
DynamicUser=yes
# Option 2: Specific user/group
User=webapp
Group=webapp
# Environment configuration
Environment="NODE_ENV=production"
EnvironmentFile=-/etc/webapp/webapp.env
Best practice: Use DynamicUser=yes for services that don't need a specific user. For secrets, use EnvironmentFile= with restricted permissions instead of embedding credentials in the service file.
Set appropriate restart behavior:
Restart=on-failure # For web apps (restart on crashes)
RestartSec=10 # Wait 10 seconds before restart
TimeoutStartSec=30 # Fail if startup takes >30 seconds
TimeoutStopSec=30 # Force kill if graceful stop takes >30 seconds
Common patterns:
Restart=on-failure with RestartSec=10Restart=always with RestartSec=5Restart= (default is no restart)For production services, apply progressive security hardening:
Start with these baseline options:
# Filesystem protection
ProtectSystem=strict # Entire filesystem read-only except specified paths
ProtectHome=yes # Hide /home directories
PrivateTmp=yes # Private /tmp namespace
ReadWritePaths=/var/lib/myapp /var/log/myapp # Writable paths whitelist
NoNewPrivileges=yes # Prevent privilege escalation
# Device and kernel protection
PrivateDevices=yes # Restrict device access
ProtectKernelTunables=yes # Prevent /proc/sys, /sys writes
ProtectKernelModules=yes # Prevent kernel module loading
ProtectControlGroups=yes # Protect cgroup filesystem
For internet-facing services, add:
# Network restrictions
RestrictAddressFamilies=AF_INET AF_INET6 # Only IPv4/IPv6
# Capability restrictions
CapabilityBoundingSet= # Remove all capabilities
# System call filtering
SystemCallFilter=@system-service
SystemCallArchitectures=native
# Memory protection
MemoryDenyWriteExecute=yes # W^X protection
LockPersonality=yes
Testing strategy: Start with maximum restrictions. If the service fails, use journalctl -xeu <service> to identify which restriction caused the failure, then selectively relax only that restriction.
Use systemd-analyze security <service> to verify the security posture after configuration.
Set the target for service enablement:
[Install]
WantedBy=multi-user.target # Most common target (non-graphical multi-user system)
Common targets:
multi-user.target: Standard for most servicesgraphical.target: Services requiring graphical environmentdefault.target: Alias for the default system targetPlace the service file and test:
# Copy to system directory
sudo cp myapp.service /etc/systemd/system/
# Reload systemd to recognize new service
sudo systemctl daemon-reload
# Start and check status
sudo systemctl start myapp
sudo systemctl status myapp
# View logs
journalctl -xeu myapp
# Enable to start on boot
sudo systemctl enable myapp
When hardening an existing service file, follow these steps:
Read the existing service file and identify security gaps:
# Check current security exposure
systemd-analyze security <service-name>
# Review current configuration
systemctl cat <service-name>
Look for:
Rather than modifying the vendor-supplied service file directly, create a drop-in override:
sudo systemctl edit <service-name>
This creates /etc/systemd/system/<service-name>.service.d/override.conf and preserves vendor updates.
Add security options in stages, testing after each stage:
Stage 1: Basic isolation
[Service]
# Filesystem protection
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/myapp # Add paths service needs to write
# Basic device protection
PrivateDevices=yes
Test: sudo systemctl restart <service> and check journalctl -xeu <service>
Stage 2: Kernel and capability restrictions
[Service]
# Kernel protection
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
# Remove capabilities
NoNewPrivileges=yes
CapabilityBoundingSet=
Test again.
Stage 3: Network and system call filtering
[Service]
# Network restrictions (adjust for service needs)
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# System call filtering
SystemCallFilter=@system-service
SystemCallArchitectures=native
# Memory protection
MemoryDenyWriteExecute=yes
LockPersonality=yes
Test and verify functionality.
If the service fails after adding restrictions:
Error: "Permission denied" accessing filesystem
ReadWritePaths= or relax ProtectSystem= to full instead of strictError: "Operation not permitted" for network operations
RestrictAddressFamilies= includes needed protocolsAF_UNIXError: System call blocked
journalctl for blocked syscall nameSystemCallFilter=@system-service <syscall-name>Error: Cannot access devices
DeviceAllow= instead of removing PrivateDevices=After applying all restrictions:
# Check security score (lower is better)
systemd-analyze security <service-name>
# Verify service functionality
sudo systemctl restart <service-name>
sudo systemctl status <service-name>
# Test application-level functionality
# (Make requests, check logs, verify operations)
Type=notify for services that can implement sd_notify protocol - provides reliable startup verificationType=exec for standard long-running processes - good error detectionType=simple - provides no startup verification and poor error handlingType=forking - deprecated pattern, use Type=notify or Type=exec insteadAfter= and Requires=/Wants=After= controls startup sequenceRequires=/Wants= controls dependency relationshipsWants= for soft dependencies (tolerate failures)Requires= for hard dependencies (fail if dependency fails)After=network-online.target and Wants=network-online.targetDynamicUser=yes unless the service needs a specific userEnvironmentFile= for sensitive valuesNoNewPrivileges=yes to prevent privilege escalationCapabilityBoundingSet=, add back only what's neededsystemd-analyze security to verify hardening effectivenessProtectSystem=strict with explicit ReadWritePaths= whitelistPrivateTmp=yes unless sharing /tmp is explicitly requiredProtectHome=yes unless home directory access is needed/var/lib/ for application dataRestart=on-failure with RestartSec=10Restart=always with RestartSec=5TimeoutStartSec=30 and TimeoutStopSec=30 (adjust based on application)SuccessExitStatus= if application uses non-zero exits for successStandardOutput=journal and StandardError=journalSyslogIdentifier= for easier log filteringjournalctl -xeu <service> (shows extended info and follows)systemctl list-dependencies <service>Comprehensive reference documentation for all systemd unit and service options. Read this file when:
Use grep to search for specific options:
grep -i "ProtectSystem" references/systemd_options.md
Production-ready service file templates:
basic-webapp.service: Starting point for web applications without heavy sandboxinghardened-webapp.service: Fully hardened web application with maximum securitybackground-worker.service: Queue processor or background daemon with security hardeningoneshot-init.service: One-time initialization script patternUse these as starting points and customize for specific application needs.
Service fails to start after hardening:
journalctl -xeu <service>Service starts but behaves incorrectly:
WorkingDirectory= is correctEnvironmentFile=)ReadWritePaths=)Service restarts repeatedly:
journalctl -xeu <service>ExecStart= path is correct and executableRestartSec= to prevent rapid restart loopsType= matches application behaviorCannot access network after hardening:
RestrictAddressFamilies= includes required protocols (IPv4: AF_INET, IPv6: AF_INET6, Unix sockets: AF_UNIX)AF_INET is included