{"id":699,"date":"2026-06-07T02:39:12","date_gmt":"2026-06-07T02:39:12","guid":{"rendered":"https:\/\/sunpathservers.net\/blog\/?p=699"},"modified":"2026-06-07T02:50:30","modified_gmt":"2026-06-07T02:50:30","slug":"beyond-permissions-application-isolation-via-systemd-security-flags","status":"publish","type":"post","link":"https:\/\/sunpathservers.net\/blog\/beyond-permissions-application-isolation-via-systemd-security-flags\/","title":{"rendered":"Application Isolation via Systemd Security Flags"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">When hardening a Linux server, we often stop after configuring firewalls, tightening SSH, and managing standard user permissions. However, if a web application or network service (like Nginx, Apache, or a Node.js API) is compromised via a remote code execution (RCE) vulnerability, standard user boundaries might not be enough. If the process runs as <code>www-data<\/code>, the attacker instantly inherits all privileges of <code>www-data<\/code> across the entire system.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To mitigate this, modern Linux distributions allow us to implement <strong>process-level sandboxing<\/strong> using systemd. By modifying a service\u2019s unit file, we can restrict its view of the filesystem, strip its kernel privileges, and block unauthorized network access\u2014even if the attacker gains execution privileges.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is how to turn systemd into a powerful application sandbox.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Concept of Sandboxing with Systemd<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Traditional hardening relies heavily on Discretionary Access Control (DAC)\u2014the classic owner\/group file permissions. Systemd sandboxing utilizes Linux kernel features like namespaces, control groups (cgroups), and Seccomp filtering right from the service configuration file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of rewriting your application&#8217;s code, you can declaratively restrict what the application can see and do in the operating system environment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Restricting Filesystem Access<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By default, a compromised process can browse directories like <code>\/tmp<\/code>, <code>\/home<\/code>, or <code>\/var<\/code> looking for sensitive data or configuration files. Systemd provides flags to render these areas completely invisible or read-only.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">ProtectSystem<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This directive protects the OS directory tree from being modified by the service.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ProtectSystem=true<\/code>: Mounts <code>\/usr<\/code> and the boot directories (<code>\/boot<\/code>, <code>\/efi<\/code>) as read-only.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ProtectSystem=full<\/code>: Additionally mounts <code>\/etc<\/code> as read-only. This is highly recommended for web servers that only need to read configuration files, not change them.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ProtectSystem=strict<\/code>: The ultimate setting. It flips the entire filesystem to <strong>read-only<\/strong> for the service, except for directories explicitly whitelisted using <code>ReadWritePaths=<\/code>.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">ProtectHome<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Prevents the service from accessing user data.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ProtectHome=true<\/code>: Makes <code>\/home<\/code>, <code>\/root<\/code>, and <code>\/run\/user<\/code> completely empty and inaccessible to the service.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Example Implementation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Service]\nExecStart=\/usr\/bin\/node \/var\/www\/my-api\/index.js\nUser=node-user\n\n# Filesystem Isolation\nProtectSystem=strict\nProtectHome=true\nReadWritePaths=\/var\/www\/my-api\/logs \/var\/www\/my-api\/uploads<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In this setup, if an attacker hijacks the Node.js application, they cannot write to <code>\/etc<\/code>, they cannot see <code>\/home<\/code>, and they can only write files to the specific <code>logs<\/code> and <code>uploads<\/code> paths.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Locking Down Kernel Namespaces<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Attackers often look for local privilege escalation vulnerabilities by interacting with system hardware, kernel tunables, or shared temporary files. We can abstract these away using kernel namespaces.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>PrivateTmp=true<\/code><\/strong>: Gives the process its own isolated, ephemeral <code>\/tmp<\/code> and <code>\/var\/tmp<\/code> directory. It prevents the service from seeing or tampering with temporary files belonging to other processes.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>PrivateDevices=true<\/code><\/strong>: Generates a custom <code>\/dev<\/code> folder for the service that excludes physical devices (like raw disk drives, system memory interfaces, and USB devices), leaving only virtual loops like <code>\/dev\/null<\/code>, <code>\/dev\/random<\/code>, and <code>\/dev\/zero<\/code>.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li>ProtectKernelTunables=true: Mounts kernel variables configurable via sysctl (\/proc\/sys, \/sys) as read-only, preventing the process from altering kernel behavior.<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># Kernel &amp; Environment Isolation\nPrivateTmp=true\nPrivateDevices=true\nProtectKernelTunables=true<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">3. Restricting Network and Address Families<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Does your backend processing service actually need to access the internet? Does your database need to initiate outbound TCP connections? Often, the answer is no. Systemd can restrict network sockets down to exactly what the application requires.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>RestrictAddressFamilies=<\/code><\/strong>: Restricts the low-level socket types the application can create. For a standard web service, you typically only need <code>AF_INET<\/code> (IPv4) and <code>AF_INET6<\/code> (IPv6). This prevents the application from utilizing obscure network protocols that might have unpatched kernel vulnerabilities.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>IPAddressDeny=any<\/code><\/strong>: Blocks all network access for the service. You can pair this with <code>IPAddressAllow=<\/code> to whitelist only specific internal IP addresses or databases (e.g., <code>IPAddressAllow=127.0.0.1 10.0.0.5<\/code>).<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># Network Isolation\nRestrictAddressFamilies=AF_INET AF_INET6\nIPAddressDeny=any\nIPAddressAllow=127.0.0.1<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">4. Stripping Root Privileges (Capability Bounding)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In Linux, the <code>root<\/code> user&#8217;s power is broken down into distinct permissions called <strong>Capabilities<\/strong>. For instance, binding to a port lower than 1024 requires <code>CAP_NET_BIND_SERVICE<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If a service must start as root (perhaps to bind to port 80\/443 before dropping privileges to a regular user), it carries a window of vulnerability. We can use systemd to drop all capabilities <em>except<\/em> the absolute bare essentials.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Allow binding to privileged ports, drop everything else\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nNoNewPrivileges=true<\/code><\/pre>\n\n\n\n<div style=\"height:40px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Note:<\/strong> <code>NoNewPrivileges=true<\/code> is one of the most critical security flags. It ensures that the service\u2014and any child processes it spawns\u2014can never gain more privileges than the parent process, completely breaking standard SUID binary exploitation techniques.<\/p>\n<\/blockquote>\n\n\n\n<div style=\"height:30px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Putting It Together: A Hardened Template<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl edit my-service.service<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Inside the text editor, paste your hardening block:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Service]\n# Filesystem\nProtectSystem=strict\nProtectHome=true\nReadWritePaths=\/var\/log\/my-service\nPrivateTmp=true\n\n# Kernel &amp; Devices\nPrivateDevices=true\nProtectKernelTunables=true\nProtectControlGroups=true\nNoNewPrivileges=true\n\n# Privileges &amp; Architecture\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nRestrictAddressFamilies=AF_INET AF_INET6\nSystemCallArchitectures=native<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Save the file and restart your service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl restart my-service.service<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Verifying Your Hardening: <code>systemd-analyze<\/code><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Unsure if your configuration is secure enough? Systemd includes a built-in security auditing tool that scores your services from 0 (perfectly secure) to 10 (completely exposed).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run the following command to check your target service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemd-analyze security my-service.service<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This will output a line-by-line breakdown of every security flag you missed, giving you an immediate roadmap to lock down your application layer effectively.<\/p>\n\n\n\n<div style=\"background-color: #121212; border-left: 4px solid #FFCF4D; padding: 25px 30px; margin-top: 40px; border-radius: 0 8px 8px 0; font-family: sans-serif;\">\n    <h4 style=\"color: #FFCF4D; margin-top: 0; margin-bottom: 14px; font-size: 1.5rem; letter-spacing: 1px; text-transform: uppercase; font-weight: 700;\">\n        \ud83d\udee1\ufe0f Sandbox-Ready Dedicated Infrastructure\n    <\/h4>\n    <p style=\"color: #e0e0e0; font-size: 1.5rem; line-height: 1.6; margin-bottom: 18px;\">\n        Restricting namespaces, stripping capabilities, and isolating systemd processes are highly effective techniques for building secure, bulletproof application sandboxes. However, even the most isolated local environment can be overwhelmed if your underlying host lacks the raw compute resources and edge defense to withstand heavily sustained request spikes or targeted infrastructure attacks.\n    <\/p>\n    <p style=\"color: #e0e0e0; font-size: 1.5rem; line-height: 1.6; margin-bottom: 0;\">\n        \ud83d\udc49 <a href=\"https:\/\/sunpathservers.net\/sunpath-inventory.html\" style=\"color: #40FFFF; text-decoration: none; border-bottom: 1px dashed #40FFFF;\">\n            View Our Live Unmanaged Server Inventory\n        <\/a> \n        to deploy your containerized workloads and hardened systemd layers on enterprise-grade hardware backed by automatic network-edge mitigation.\n    <\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>When hardening a Linux server, we often stop after configuring firewalls, tightening SSH, and managing standard user permissions. However, if a web application or network service (like Nginx, Apache, or a Node.js API) is compromised via a remote code execution (RCE) vulnerability, standard user boundaries might not be enough. If the process runs as www-data, [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":537,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[122,504,160,507,508,509,23,163,505,506],"class_list":["post-699","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-server-hardening","tag-devops","tag-linux-namespaces","tag-linux-security","tag-process-isolation","tag-sandboxing","tag-security-best-practices","tag-server-hardening","tag-sysadmin","tag-systemd","tag-systemd-analyze"],"_links":{"self":[{"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/posts\/699","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/comments?post=699"}],"version-history":[{"count":0,"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/posts\/699\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/media\/537"}],"wp:attachment":[{"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/media?parent=699"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/categories?post=699"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sunpathservers.net\/blog\/wp-json\/wp\/v2\/tags?post=699"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}