AgentStack
SKILL verified MIT Self-run

Vps Hardening

skill-goldenwing-360-claude-security-skills-vps-hardening · by GoldenWing-360

Baseline-harden a Debian or Ubuntu VPS in roughly thirty minutes. Covers SSH key-only authentication, UFW firewall, fail2ban with web-app jails, unattended security upgrades, kernel sysctls, journalctl retention, and sudo policy. Invoke when provisioning a new VPS, inheriting one without documented hardening, or before exposing a service to the public internet.

No reviews yet
0 installs
18 views
0.0% view→install

Install

$ agentstack add skill-goldenwing-360-claude-security-skills-vps-hardening

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Vps Hardening? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

VPS Hardening

A pragmatic baseline for a single Debian/Ubuntu VPS hosting a few services. Not a CIS benchmark — those are excellent but too long for a vibe-coded side project to follow on day one. This is the 30-minute version that gets you 90% of the way.

Assumptions: you have root SSH access (typically via the provider's initial password). You will be the first and probably only operator.

When to invoke

  • New VPS provisioned, no hardening yet
  • Inherited a VPS with no documentation
  • Service is about to face public internet traffic
  • Periodic re-audit (quarterly is reasonable)

Step 0 — Get a backup before changing anything

# From your laptop, snapshot the VPS via your provider's panel/API first.
# All steps below are reversible, but a snapshot turns a mistake from a crisis into an inconvenience.

Step 1 — Update everything, then create a non-root user

# As root, on the VPS
apt update && apt upgrade -y
apt install -y sudo ufw fail2ban unattended-upgrades curl ca-certificates gnupg

# Create a personal user with sudo
adduser deploy                # set a strong passphrase (you will rarely use it)
usermod -aG sudo deploy

From now on, prefer to SSH as deploy and use sudo for privileged actions. root will be locked from SSH in step 3.

Step 2 — Authorize your SSH key for the new user

From your laptop:

ssh-copy-id deploy@
# Verify you can log in as deploy and run sudo
ssh deploy@ 'sudo -v && echo OK'

If you do not have an SSH key yet:

# On the laptop
ssh-keygen -t ed25519 -C "deploy@"

Ed25519 is the modern default; RSA-2048 also fine but slower.

Step 3 — Lock SSH down

Edit /etc/ssh/sshd_config (on the VPS, as root or sudo):

# Auth
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no

# Hygiene
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 4
ClientAliveInterval 300
ClientAliveCountMax 2

# Reduce surface
AllowAgentForwarding no
AllowTcpForwarding no       # set yes if you actually need SSH tunneling
X11Forwarding no
PrintMotd no
UseDNS no

# Restrict who can log in at all
AllowUsers deploy

The "change port from 22" debate is largely security theater for shared-host targeted attacks but does cut log noise — your call. If you change it, update UFW (next step).

Apply, but keep your current session open and open a second SSH session to verify before exiting the first:

sudo sshd -t                  # syntax check
sudo systemctl reload ssh     # or 'sshd' on some distros

If the new session works, you are safe to drop the first.

Step 4 — Firewall (UFW)

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp         # or your custom SSH port
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

For services bound to specific source IPs (admin endpoints, monitoring), use scoped rules:

sudo ufw allow from 203.0.113.5 to any port 9000 proto tcp comment 'admin from office'

Step 5 — fail2ban

Default install bans on SSH brute-force. Add web jails for the apps you actually run.

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

In /etc/fail2ban/jail.local, ensure [sshd] is enabled. For nginx-fronted auth endpoints:

[nginx-http-auth]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/error.log
maxretry = 5
bantime  = 1h

[wp-login]
enabled  = true
port     = http,https
filter   = wp-login          # create /etc/fail2ban/filter.d/wp-login.conf
logpath  = /var/log/nginx/access.log
maxretry = 5
findtime = 10m
bantime  = 2h
sudo systemctl enable --now fail2ban
sudo fail2ban-client status

A common wp-login.conf filter:

[Definition]
failregex = ^ .* "POST /wp-login.php
ignoreregex =

Step 6 — Unattended security upgrades

sudo dpkg-reconfigure -plow unattended-upgrades   # Yes

Edit /etc/apt/apt.conf.d/50unattended-upgrades:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    // "${distro_id}:${distro_codename}-updates";  // optional
};
Unattended-Upgrade::Mail "you@example.com";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Test:

sudo unattended-upgrade --dry-run --debug 2>&1 | head -30

If you cannot tolerate auto-reboots (stateful daemons without graceful restart), set Automatic-Reboot "false" and create a calendar reminder to reboot weekly.

Step 7 — Kernel sysctls

/etc/sysctl.d/99-hardening.conf:

# Network
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1

# Filesystem / kernel
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
kernel.unprivileged_bpf_disabled = 1

Apply:

sudo sysctl --system

Step 8 — sudo policy

By default, sudo requires a password and times out quickly. Keep it that way. Common bad patterns to avoid:

  • deploy ALL=(ALL) NOPASSWD: ALL — never, except for ephemeral CI users with no SSH access
  • Putting service accounts in sudo group — they shouldn't need it; use systemd units with the right User=/Group= instead

If a service truly needs to run a single command with sudo (e.g. systemctl restart myapp), allow exactly that:

deploy ALL=(root) NOPASSWD: /bin/systemctl restart myapp

Validate with visudo so you cannot save a broken file.

Step 9 — journalctl retention and log shipping

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

In /etc/systemd/journald.conf:

Storage=persistent
SystemMaxUse=2G
SystemKeepFree=2G
SystemMaxFileSize=200M

For multi-host setups, ship to a central log store (Loki, ELK, hosted) — but for a single-VPS side project, local retention is fine.

Step 10 — Backups (yes, this is security)

A VPS is hardened against attackers; backups are hardened against you. Set up:

  • Snapshots at the provider weekly, retain 4
  • App data export (DB dumps, uploads) to off-site storage (S3/R2) daily, retain 30 days
  • Test restore once before you need it — most backups are corrupted in some way and only restore-tested ones are real

Step 11 — Quick audit script

Run this every month or after onboarding a new VPS:

#!/usr/bin/env bash
set -u
echo "== Distro / kernel =="; lsb_release -d 2>/dev/null; uname -r
echo "== Unattended-upgrades =="; systemctl is-active unattended-upgrades || true
echo "== UFW =="; sudo ufw status
echo "== fail2ban =="; sudo fail2ban-client status
echo "== Listening ports =="; sudo ss -tlnp
echo "== Recent root logins =="; sudo last -F root 2>/dev/null | head -10
echo "== Failed SSH (last 200) =="; sudo grep 'Failed password' /var/log/auth.log* 2>/dev/null | tail -20
echo "== Users with shell =="; awk -F: '$7 !~ /nologin|false/ {print $1, $7}' /etc/passwd
echo "== sudoers files =="; sudo ls -la /etc/sudoers.d/
echo "== Cron jobs =="; for u in $(cut -d: -f1 /etc/passwd); do sudo crontab -u "$u" -l 2>/dev/null | sed "s|^|[$u] |"; done
echo "== Pending updates =="; sudo apt list --upgradable 2>/dev/null | head -20

Common follow-ups

  • Docker on the same VPS → see [docker-container-security](../docker-container-security/SKILL.md) (planned). Note: Docker bypasses UFW by default; you need ufw-docker rules.
  • WordPress on this VPS → see [wordpress-hardening](../wordpress-hardening/SKILL.md)
  • Behind Cloudflare → see [cloudflare-hardening](../cloudflare-hardening/SKILL.md), and restrict ingress to Cloudflare's IPs in UFW

What this skill will not do

  • Replace a full CIS benchmark for regulated environments
  • Help harden a server you do not own or operate
  • Endorse PermitRootLogin yes or PasswordAuthentication yes on internet-facing SSH

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.