{"id":587,"date":"2026-07-20T17:58:03","date_gmt":"2026-07-20T17:58:03","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=587"},"modified":"2026-08-18T22:14:50","modified_gmt":"2026-08-18T22:14:50","slug":"securing-vps-selinux-apparmor-mandatory-access-control","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/","title":{"rendered":"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Every Ubuntu VPS ships with AppArmor enabled, and almost nobody looks at it until a service starts failing with cryptic <code>permission denied<\/code> errors. AppArmor is a Mandatory Access Control (MAC) layer that restricts what programs can do even when they run as root \u2014 which makes it one of the few security controls that actually contains a compromised web app. The catch: default profiles are tuned for stock installs, and the moment you customize nginx, move a MySQL datadir, or add PAM modules to sshd, you start collecting denials. Here is the workflow that keeps your services running while the profiles still do their job.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: See what is actually loaded<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo aa-status\n# shows loaded profiles and their mode: enforce or complain<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Profiles in <strong>enforce<\/strong> mode block denied operations. Profiles in <strong>complain<\/strong> mode only log them. On a fresh Ubuntu image, <code>sshd<\/code> is typically in complain mode and a handful of other services ship with profiles you never asked for. Knowing which mode each profile is in is the difference between a quiet weekend and a 2 a.m. support ticket.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: The workflow \u2014 complain, read logs, enforce<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Never flip a profile straight to enforce after a config change. The repeatable loop is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo aa-complain \/etc\/apparmor.d\/usr.sbin.mysqld   # 1. put the profile in complain mode\nsudo systemctl restart mysql                        # 2. exercise the new config\nsudo journalctl -k --since \"10 min ago\" | grep -i apparmor   # 3. read the denials\nsudo aa-logprof                                     # 4. approve\/deny the suggested rules\nsudo aa-enforce \/etc\/apparmor.d\/usr.sbin.mysqld    # 5. only now enforce<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kernel denials also land in <code>\/var\/log\/syslog<\/code> and <code>dmesg<\/code>. A typical denial looks like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>audit: type=1400 audit(1750000000.123:456) apparmor=\"DENIED\"\n  operation=\"open\" profile=\"\/usr\/sbin\/nginx\"\n  name=\"\/srv\/www\/index.html\" comm=\"nginx\" requested_mask=\"r\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Read it left to right: which profile, which operation, which file, which access mask. That tells you exactly what rule to add.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Case 1: nginx with custom paths<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The stock nginx profile allows <code>\/etc\/nginx<\/code>, <code>\/var\/www<\/code>, <code>\/var\/log\/nginx<\/code> and the pid file. Serve sites from <code>\/srv\/www<\/code> or <code>\/home\/deploy\/sites<\/code> and you will see exactly the denial above. Two ways out:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Add rules with aa-logprof<\/strong> \u2014 the interactive tool proposes file rules from the denials; approve the ones that match paths you intend nginx to read.<\/li>\n<li><strong>Append rules by hand<\/strong> \u2014 add to the profile file and reload:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># inside \/etc\/apparmor.d\/usr.sbin.nginx\n  \/srv\/www\/ r,\n  \/srv\/www\/** r,\nsudo apparmor_parser -r \/etc\/apparmor.d\/usr.sbin.nginx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you run nginx with a PHP-FPM socket, remember the socket path too \u2014 <code>\/run\/php-fpm.sock<\/code> usually needs an explicit <code>rw<\/code> rule.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Case 2: MySQL\/MariaDB with a moved datadir<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Moving <code>\/var\/lib\/mysql<\/code> to a separate data volume is common on small VPSes, and the shipped profile (<code>usr.sbin.mysqld<\/code>) only knows the default path. The denials show up in the MySQL error log as failed file opens. Add the new location and reload the profile:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># inside \/etc\/apparmor.d\/usr.sbin.mysqld\n  \/data\/mysql\/ r,\n  \/data\/mysql\/** rwk,\nsudo apparmor_parser -r \/etc\/apparmor.d\/usr.sbin.mysqld<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Notice the <code>k<\/code> in the mask \u2014 MySQL needs <code>lock<\/code> permission on its files, and omitting it produces denials that look identical to filesystem permission problems. This is the single most common AppArmor debugging trap on database servers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Case 3: sshd plus 2FA or custom auth<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ubuntu ships the sshd profile in complain mode by default, which is why SSH almost never breaks when you add TOTP via <code>libpam-google-authenticator<\/code> or a custom <code>AuthorizedKeysCommand<\/code> \u2014 it is logging, not blocking. If you decide to enforce the profile, expect denials from the new PAM modules and key-helper binaries, and work through them with the same complain \u2192 log \u2192 enforce loop. If enforcing sshd gains you little and costs support time, leaving it in complain mode while enforcing profiles for your web-facing services is a reasonable trade.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The recovery plan<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If a service stops working after a profile change, you still have the provider&#8217;s console \u2014 AppArmor cannot stop you from logging in over the web console, and it does not modify files. The safe order is: <code>sudo aa-disable \/etc\/apparmor.d\/&lt;profile&gt;<\/code>, restart the service, get it back up, then investigate the denials and re-enable with proper rules. What you should never do is <code>chmod 644<\/code> or delete profile files across the board to make the errors go away \u2014 you are disabling the control, not fixing the policy.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A note on SELinux<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">RHEL-family images (CentOS, AlmaLinux, Rocky) use SELinux, which is the same idea with a different vocabulary: policy is label-based instead of path-based, and denials land in <code>\/var\/log\/audit\/audit.log<\/code> where <code>audit2why<\/code> translates them. If your provider&#8217;s default image is Ubuntu or Debian, AppArmor is the MAC you actually have, so this guide applies directly. Distro defaults are one of the things to check before you pick a provider \u2014 <a href=\"https:\/\/virtualserversvps.com\/#providers\">our VPS comparison table<\/a> tracks which images each provider offers out of the box.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Is MAC worth the effort on a single VPS? Compared with SSH keys, a firewall, and unattended upgrades, AppArmor is a smaller increment \u2014 but it is the only one of those that contains a compromised nginx or PHP process. For a web server that touches customer data, spending an afternoon building one good profile for the app layer is cheap insurance.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Every Ubuntu VPS ships with AppArmor enabled, and almost nobody looks at it until a service starts failing with cryptic permission denied errors. AppArmor is a Mandatory Access Control (MAC)&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":6,"footnotes":""},"categories":[4],"tags":[],"class_list":["post-587","post","type-post","status-publish","format-standard","hentry","category-security-compliance"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out - Virtual Servers VPS Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out\" \/>\n<meta property=\"og:description\" content=\"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-20T17:58:03+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-18T22:14:50+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"4 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/\",\"name\":\"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-07-20T17:58:03+00:00\",\"dateModified\":\"2026-08-18T22:14:50+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out - Virtual Servers VPS Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/","og_locale":"en_US","og_type":"article","og_title":"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out","og_description":"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out","og_url":"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-07-20T17:58:03+00:00","article_modified_time":"2026-08-18T22:14:50+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"4 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/","url":"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/","name":"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-07-20T17:58:03+00:00","dateModified":"2026-08-18T22:14:50+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/securing-vps-selinux-apparmor-mandatory-access-control\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"AppArmor on a VPS: Profiles for nginx, MySQL, and sshd Without Locking Yourself Out"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/587","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=587"}],"version-history":[{"count":2,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/587\/revisions"}],"predecessor-version":[{"id":914,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/587\/revisions\/914"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=587"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=587"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=587"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}