Introduction: Systemd Beyond the Desktop and Server
Systemd has become the de facto init system for most mainstream Linux distributions, offering robust process management and service orchestration. While its capabilities are well-documented for traditional desktop and server environments, its power for embedded systems – particularly those running a custom Linux base powering Android – is often underestimated. In such bespoke setups, managing and securing non-Android native services becomes paramount. This guide delves into an advanced application of Systemd: sandboxing custom Android services using unit file directives to drastically reduce their attack surface.
The Critical Need for Sandboxing Custom Android Services
Android’s native security model, built around UID/GID separation, SELinux, and app sandboxes, is formidable. However, custom Android builds, often found in specialized devices, industrial applications, or enthusiast projects, frequently introduce additional Linux-based ‘helper’ services that operate outside the traditional Android application framework. These services might interact with hardware, custom kernel modules, or low-level system properties, often requiring elevated privileges. A poorly secured helper service, running with broad permissions, represents a significant security vulnerability. Should such a service be compromised, it could provide an attacker with an unrestricted foothold into the system.
Systemd offers a powerful, kernel-level mechanism to confine these services. By explicitly defining what a service can and cannot do – from filesystem access to network communication and even specific system calls – we can create a robust sandbox that significantly minimizes potential damage from a breach.
Systemd’s Sandboxing Arsenal: Key Directives
Systemd provides a rich set of directives within the [Service] section of a unit file to implement fine-grained sandboxing. Here are some of the most crucial ones:
PrivateTmp=true: Provides the service with its own isolated/tmpand/var/tmpdirectories, preventing it from interacting with temporary files of other processes.PrivateDevices=true: Creates a new, empty/devnamespace for the service, making only a minimal set of device nodes (/dev/null,/dev/zero, etc.) visible. This prevents unauthorized access to hardware devices.ProtectSystem=full|strict: Makes the root filesystem read-only for the service.fullprotects/boot,/etc,/usr,/opt, and/srv.strictadds/rootand/run.ProtectHome=true|read-only: Makes user home directories (/home,/root) inaccessible or read-only for the service.NoNewPrivileges=true: Prevents the service from gaining new privileges viasetuidorsetgidbinaries.CapabilityBoundingSet=: Limits the set of kernel capabilities that the service can ever acquire. By default, processes inherit all capabilities. Explicitly dropping unnecessary capabilities is a powerful security measure.AmbientCapabilities=: Controls which capabilities are preserved across anexecve()call for non-root users.SystemCallFilter=: Allows filtering specific system calls. This is an advanced directive that requires careful tuning. You can whitelist (@grouporsyscall_name) or blacklist (~@groupor~syscall_name) calls.RootDirectory=/path/to/chroot: Changes the root directory for the service, effectively creating a chroot environment. This offers strong isolation but requires careful setup of the chroot directory.User=,Group=: Runs the service under a specific, unprivileged user and group. Always prefer non-root users.MemoryDenyWriteExecute=true: Prevents a process from mapping memory areas as both writable and executable, a common technique for exploit payloads.RestrictSUIDSGID=true: Prevents the service from executing SUID/SGID binaries.
Case Study: Sandboxing a Custom Android Hardware Service
Consider a hypothetical custom Android helper service, android-hw-monitor.service, which runs on the Linux base. Its sole purpose is to periodically read a specific hardware status value from /sys/class/android_hw/monitor_device/status and log it, then expose it via a local Unix domain socket. It doesn’t need network access, direct hardware control beyond its specific /sys path, or access to user data.
Step 1: Identify Minimal Requirements
Based on the scenario, the service needs:
- Read access to
/sys/class/android_hw/monitor_device/status. - Write access to a log file, e.g.,
/var/log/android-hw-monitor.log. - Ability to create and listen on a Unix domain socket in
/run/android-hw-monitor/sock. - Minimal system capabilities (likely none beyond basic process execution).
- To run as an unprivileged user, e.g.,
hwmonitor.
Step 2: Basic Unit File Creation (Insecure Starting Point)
First, ensure a dedicated user and group exist for the service:
sudo useradd --system --no-create-home --shell /sbin/nologin hwmonitor
Now, a very basic /etc/systemd/system/android-hw-monitor.service:
[Unit]Description=Android Hardware Monitor ServiceAfter=syslog.target[Service]ExecStart=/usr/local/bin/android-hw-monitorType=simpleUser=hwmonitorGroup=hwmonitor[Install]WantedBy=multi-user.target
Step 3: Iterative Sandboxing with Systemd Directives
Let’s progressively add sandboxing directives. Edit /etc/systemd/system/android-hw-monitor.service:
Applying Fundamental Isolation
We start with broad isolation to prevent access to temporary files, devices, and the filesystem outside its needs.
[Unit]Description=Android Hardware Monitor ServiceAfter=syslog.target[Service]ExecStart=/usr/local/bin/android-hw-monitorType=simpleUser=hwmonitorGroup=hwmonitor# Basic IsolationPrivateTmp=truePrivateDevices=trueProtectSystem=fullProtectHome=trueNoNewPrivileges=trueMemoryDenyWriteExecute=trueRestrictSUIDSGID=true[Install]WantedBy=multi-user.target
Filesystem Access Refinement
Next, we grant specific, minimal access to the required paths. By default, ProtectSystem=full will prevent access to most of the root filesystem. We need to explicitly allow read access to our /sys path and write access to our log directory.
# ... (previous directives)ReadWritePaths=/var/log/android-hw-monitorReadOnlyPaths=/sys/class/android_hw/monitor_device# Runtime directory for the Unix socketRuntimeDirectory=android-hw-monitorRuntimeDirectoryMode=0750# ... (rest of the file)
Make sure the log directory exists and has appropriate permissions:
sudo mkdir /var/log/android-hw-monitorsudo chown hwmonitor:hwmonitor /var/log/android-hw-monitor
Capability and System Call Filtering
Given the service’s simple requirements (read a file, write a log, Unix socket), it should need very few, if any, special kernel capabilities. We can drop all, or specify only those absolutely essential. For a Unix domain socket, CAP_NET_BIND_SERVICE might be required if binding to a privileged port (though not strictly for Unix sockets), but often none are truly needed if running as an unprivileged user.
System call filtering is the most granular and potentially fragile. It’s best to start by blocking broad categories of dangerous calls and then allow specific ones if the service fails to function. A good starting point is to block anything related to mounting, raw I/O (if not directly interacting with raw devices), debugging, and resource management beyond its own scope.
# ... (previous directives)# Capability ManagementCapabilityBoundingSet= # Drops all capabilitiesAmbientCapabilities=# System Call Filtering (carefully crafted)SystemCallFilter=~@mount @raw-io @debug @resources @privileged @system-service @network-io @chown @chmod @fchdir @fchown @fchmod @fsetxattr @setuid @setgid @setresuid @setresgidSystemCallFilter=~@process @ipc @chroot @clone @unshare @execve @link @symlink @mknod @pivot_root @ptrace @reboot @swapon @sync @umount @vhangup @vmsplice @splice @tee @kexec_file_load @keyctl @move_pages @perf_event_open @rt_sigqueueinfo @set_robust_list @settimeofday @sigaltstack @signalfd @sysfs @timerfd @userfaultfd @utimes @vfork @waitidSystemCallFilter=~@io_uring @epoll @inotify # If not specifically needed# Network restrictions (if no network at all)RestrictAddressFamilies=AF_UNIXRemoveIPC=trueRestrictRealtime=trueLockPersonality=true# ... (rest of the file)
The SystemCallFilter directive is powerful. ~ indicates blacklisting. The @ prefix refers to predefined groups of system calls. You must test extensively when using this, as accidentally blocking a required syscall will prevent your service from working. Use strace to observe required syscalls if debugging. For a service only communicating via Unix sockets, blocking all other network families with RestrictAddressFamilies=AF_UNIX is highly effective.
Step 4: Deploy and Test
After creating/modifying your unit file:
- Reload Systemd daemon to pick up changes:
sudo systemctl daemon-reload - Enable the service to start on boot:
sudo systemctl enable android-hw-monitor.service - Start the service:
sudo systemctl start android-hw-monitor.service - Check its status and logs for errors:
sudo systemctl status android-hw-monitor.servicejournalctl -u android-hw-monitor.service - Test the service’s functionality (e.g., attempt to read the Unix socket, check log output).
- If the service fails, examine
journalctloutput closely. Systemd often logs which specific sandboxing directive caused a failure, providing valuable debugging information. You might need to adjustSystemCallFilter,CapabilityBoundingSet, orReadWritePathsbased on runtime errors.
Considerations and Advanced Topics
- SELinux Integration: Systemd sandboxing operates at the kernel level. On Android, SELinux provides an additional, mandatory access control layer. Both should be used in conjunction for maximum security. Systemd’s directives are complementary to SELinux policies, not a replacement.
systemd-analyze security: This command can analyze your unit files and provide a security score and recommendations, helping identify potential weaknesses.- Debugging: When a sandboxed service fails,
strace -f -p <PID>can trace its system calls, showing which ones are being blocked or causing issues. Combine this withjournalctlfor comprehensive debugging. RootDirectory(Chroot): While powerful,RootDirectoryadds complexity by requiring a complete, self-contained filesystem tree for the service. Use it when extreme isolation is needed and other methods aren’t sufficient, but be prepared for the overhead of managing the chroot environment.
Conclusion
Integrating Systemd unit file sandboxing into your custom Android service deployment strategy is a potent method for enhancing system security. By meticulously defining the privileges, filesystem access, and system calls available to a service, you drastically minimize its attack surface. This proactive security posture is vital for embedded systems where the consequences of a breach can range from data loss to physical device compromise. Mastering these Systemd directives allows you to build more resilient and secure bespoke Linux-based Android environments, turning your ‘hacker’s guide’ into a best practice for system hardening.
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →