{"id":1093,"date":"2026-09-09T22:43:38","date_gmt":"2026-09-09T22:43:38","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=1093"},"modified":"2026-10-10T22:02:18","modified_gmt":"2026-10-10T22:02:18","slug":"vps-security-audit-checklist-findings","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/","title":{"rendered":"VPS Security Audit: Turning Checklist Findings Into Automated Remediation"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A security audit that ends with a PDF of findings is worthless if nobody patches anything. The useful audit is one where every finding maps to a command you can run and verify. This walkthrough runs against a Debian 12 server and produces machine-checkable output at every step, so you finish with a host whose state you can re-verify on demand rather than a list of regrets.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this on a <a href=\"https:\/\/virtualserversvps.com\/\">freshly provisioned VPS before it serves traffic<\/a>, and re-run the verification block after any major change. The same discipline applies to the hardening workflow described on the <a href=\"https:\/\/virtualserversvps.com\/\">main site<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Establish the baseline<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before changing anything, capture what is actually running. Unmanaged boxes accumulate surprises &#8212; a leftover service, an old package with a known CVE, a world-writable file in <code>\/etc<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apt-get update &amp;&amp; apt list --upgradable 2&gt;\/dev\/null | tail -n +2\nsystemctl list-units --type=service --state=running --no-pager\nss -tulpn\nfind \/etc -xdev -type f -perm -o+w 2&gt;\/dev\/null\nlastb | head -20<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The upgradable list is your CVE exposure, the running-services list is your attack surface, the listening-sockets list is what is reachable, and the world-writable find is your local privilege-escalation surface.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Verify SSH is not the weak link<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|pubkeyauth|x11forward|maxauthtries|allowtcpforwarding'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected hardened values:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes\nX11Forwarding no\nMaxAuthTries 3\nAllowTcpForwarding no<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>sshd -T<\/code> reports the effective config after all includes are merged, which is what actually matters &#8212; editing <code>sshd_config.d\/*.conf<\/code> and reading the file back proves nothing. If <code>PasswordAuthentication yes<\/code> appears, fix it in a drop-in and reload, keeping your existing session open so you do not lock yourself out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Close the firewall to a default-deny posture<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>cat &lt;&lt;'EOF' &gt; \/etc\/nftables.conf\n#!\/usr\/sbin\/nft -f\nflush ruleset\ntable inet filter {\n  chain input {\n    type filter hook input priority 0; policy drop;\n    ct state established,related accept\n    ct state invalid drop\n    iif lo accept\n    ip protocol icmp accept\n    tcp dport { 22, 80, 443 } accept\n    tcp flags syn limit rate 20\/second accept\n    counter drop\n  }\n  chain forward { type filter hook forward priority 0; policy drop; }\n  chain output { type filter hook output priority 0; policy accept; }\n}\nEOF\nnft -f \/etc\/nftables.conf &amp;&amp; systemctl enable --now nftables<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then prove it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nft list ruleset\nnmap -Pn -p- &lt;your-public-ip&gt;   # run from an external host<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A default-drop policy with an explicit allow list is the only firewall posture worth having. The rate limit on new SYNs blunts SYN floods without a separate tool. The nmap run from outside is the ground truth &#8212; a local firewall listing means nothing until a remote scan confirms ports are unfiltered or closed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Attack a brute-force problem with data, not kneejerk blocks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before adding fail2ban, quantify the threat. If <code>lastb<\/code> shows thousands of failed attempts, the fix is key-only auth (already done above) plus a jail for defence in depth, not endless IP blocking.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[sshd]\nenabled = true\nport = 22\nmaxretry = 4\nfindtime = 600\nbantime = 3600\nbantime.increment = true\nbantime.factor = 2\nbantime.maxtime = 1w<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>bantime.increment<\/code> settings make repeat offenders get progressively longer bans &#8212; a single global 1-hour ban is trivially waited out by a botnet. Verify with <code>fail2ban-client status sshd<\/code> and check the jail is actually reading logs, since a wrong <code>logpath<\/code> produces a silent no-op jail that gives false comfort.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Bound what every service can touch<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A compromise that escapes the service process is far worse than one contained inside it. systemd gives you sandboxing for free. A hardened web-app unit looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Service]\nNoNewPrivileges=yes\nProtectSystem=strict\nProtectHome=yes\nPrivateTmp=yes\nPrivateDevices=yes\nProtectKernelTunables=yes\nProtectKernelModules=yes\nProtectControlGroups=yes\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX\nRestrictNamespaces=yes\nLockPersonality=yes\nMemoryDenyWriteExecute=yes\nSystemCallFilter=@system-service\nSystemCallFilter=~@privileged @resources\nReadWritePaths=\/var\/lib\/myapp \/var\/log\/myapp<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ProtectSystem=strict<\/code> makes the entire filesystem read-only except explicitly whitelisted paths &#8212; this one directive defeats a large class of web-shell persistence attempts. <code>SystemCallFilter=~@privileged<\/code> blocks calls a web app never legitimately needs. Test with <code>systemd-analyze security myapp.service<\/code>, which scores the unit and itemises every remaining exposure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Kernel-level hardening<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>kernel.kptr_restrict = 2\nkernel.dmesg_restrict = 1\nkernel.yama.ptrace_scope = 2\nkernel.unprivileged_bpf_disabled = 1\nnet.core.bpf_jit_harden = 2\nfs.protected_hardlinks = 1\nfs.protected_symlinks = 1\nfs.suid_dumpable = 0\nkernel.randomize_va_space = 2\nnet.ipv4.conf.all.rp_filter = 1\nnet.ipv4.conf.all.accept_redirects = 0\nnet.ipv6.conf.all.accept_redirects = 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each of these removes an information leak or a local escalation primitive with negligible performance cost. <code>kernel.dmesg_restrict=1<\/code> stops an unprivileged user reading kernel addresses from the ring buffer; <code>kptr_restrict=2<\/code> hides them from everyone but root. Reboot-dependent settings should be applied through <code>\/etc\/sysctl.d\/<\/code> so they survive.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: Automate the audit itself<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A one-time audit decays. Wrap the verification commands in a script that exits non-zero when a check regresses, then run it from a systemd timer or CI. Keep it dependency-light so it never fails because a package is missing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\nset -u\nfail=0\nchk(){ if eval \"$2\"; then echo \"PASS $1\"; else echo \"FAIL $1\"; fail=1; fi; }\nchk \"root-ssh-off\"      \"[ \\\"\\$(sshd -T | grep -c 'permitrootlogin no')\\\" -eq 1 ]\"\nchk \"password-auth-off\" \"[ \\\"\\$(sshd -T | grep -c 'passwordauthentication no')\\\" -eq 1 ]\"\nchk \"firewall-default-drop\" \"nft list ruleset | grep -q 'policy drop'\"\nchk \"no-world-writable-etc\" \"[ -z \\\"\\$(find \/etc -xdev -type f -perm -o+w 2&gt;\/dev\/null)\\\" ]\"\nchk \"autoupdates-on\"    \"systemctl is-enabled --quiet unattended-upgrades\"\nexit $fail<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hook it into a timer that mails or webhooks on failure. Drift detection turns the audit from a project into a guarantee.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prioritising what you found<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Not every finding deserves equal urgency. Rank by reachability first: an exposed service with a known CVE outranks a local hardening gap every time. Fix in this order &#8212; patch exposed software, disable unused services, enforce key-only auth, default-deny the firewall, then layer kernel and systemd hardening. Re-run the verification script after each change so you always know your current posture instead of assuming it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A security audit that ends with a PDF of findings is worthless if nobody patches anything. The useful audit is one where every finding maps to a command you can&#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":3,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1093","post","type-post","status-publish","format-standard","hentry","category-vps-guides-tutorials"],"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>VPS Security Audit: Turning Checklist Findings Into Automated Remediation - Virtual Servers VPS Blog<\/title>\n<meta name=\"description\" content=\"A security audit that ends with a PDF of findings is worthless if nobody patches anything. The useful audit is one where every finding maps to a command\" \/>\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\/vps-security-audit-checklist-findings\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"VPS Security Audit: Turning Checklist Findings Into Automated Remediation\" \/>\n<meta property=\"og:description\" content=\"VPS Security Audit: Turning Checklist Findings Into Automated Remediation\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-09T22:43:38+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-10T22:02:18+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=\"5 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/\",\"name\":\"VPS Security Audit: Turning Checklist Findings Into Automated Remediation - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-09-09T22:43:38+00:00\",\"dateModified\":\"2026-10-10T22:02:18+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"description\":\"A security audit that ends with a PDF of findings is worthless if nobody patches anything. The useful audit is one where every finding maps to a command\",\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"VPS Security Audit: Turning Checklist Findings Into Automated Remediation\"}]},{\"@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":"VPS Security Audit: Turning Checklist Findings Into Automated Remediation - Virtual Servers VPS Blog","description":"A security audit that ends with a PDF of findings is worthless if nobody patches anything. The useful audit is one where every finding maps to a command","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\/vps-security-audit-checklist-findings\/","og_locale":"en_US","og_type":"article","og_title":"VPS Security Audit: Turning Checklist Findings Into Automated Remediation","og_description":"VPS Security Audit: Turning Checklist Findings Into Automated Remediation","og_url":"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-09-09T22:43:38+00:00","article_modified_time":"2026-10-10T22:02:18+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"5 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/","url":"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/","name":"VPS Security Audit: Turning Checklist Findings Into Automated Remediation - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-09-09T22:43:38+00:00","dateModified":"2026-10-10T22:02:18+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"description":"A security audit that ends with a PDF of findings is worthless if nobody patches anything. The useful audit is one where every finding maps to a command","breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/vps-security-audit-checklist-findings\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"VPS Security Audit: Turning Checklist Findings Into Automated Remediation"}]},{"@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\/1093","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=1093"}],"version-history":[{"count":2,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1093\/revisions"}],"predecessor-version":[{"id":1352,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1093\/revisions\/1352"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=1093"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=1093"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=1093"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}