vps-hardening
Skill16 starsChanged 6 months agoMITView on GitHub
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.
- Installs packages
In this repository37 other files it gives its agents.
- agent-client-securitySkill
- ai-agent-guardrailsSkill
- android-securitySkill
- api-securitySkill
- auth-hardeningSkill
- backend-architectureSkill
- backup-disaster-recoverySkill
- cloudflare-hardeningSkill
- codebase-auditSkill
- dach-complianceSkill
- dependency-supply-chainSkill
- distributed-system-auditSkill
- dns-domain-securitySkill
- docker-container-securitySkill
- email-deliverability-securitySkill
- file-upload-securitySkill
- gdpr-technical-controlsSkill
- github-actions-securitySkill
- honeypot-tarpitsSkill
- incident-responseSkill
- ios-securitySkill
- kubernetes-securitySkill
- llm-app-securitySkill
- llm-coding-failure-modesSkill
- log-strategySkill
- mcp-securitySkill
- message-bus-securitySkill
- nextjs-securitySkill
- object-storage-securitySkill
- payload-cms-securitySkill
- postgres-hardeningSkill
- prompt-injection-defenseSkill
- rag-securitySkill
- secret-hygieneSkill
- site-server-auditSkill
- stripe-webhook-securitySkill
- wordpress-hardeningSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 5 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 50
If you do not have an SSH key yet:
- Distinctive: line 99
If the new session works, you are safe to drop the first.
- Distinctive: line 121
Default install bans on SSH brute-force.
- Very specific: line 185
If you cannot tolerate auto-reboots (stateful daemons without graceful restart), set Automatic-Reboot "false" and create a calendar …
- Very specific: line 226
If a service truly needs to run a single command with sudo (e.g.
- Distinctive: line 259
Test restore once: before you need it — most backups are corrupted in some way and only restore-tested ones are real
- Distinctive: line 263
Run this every month or after onboarding a new VPS:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 deployFrom 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@<vps-ip>
# Verify you can log in as deploy and run sudo
ssh deploy@<vps-ip> 'sudo -v && echo OK'If you do not have an SSH key yet:
# On the laptop
ssh-keygen -t ed25519 -C "deploy@<your-laptop-name>"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 deployThe "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 distrosIf 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 verboseFor 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.localIn /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 = 2hsudo systemctl enable --now fail2ban
sudo fail2ban-client statusA common wp-login.conf filter:
[Definition]
failregex = ^<HOST> .* "POST /wp-login.php
ignoreregex =Step 6 — Unattended security upgrades
sudo dpkg-reconfigure -plow unattended-upgrades # YesEdit /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 -30If 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 = 1Apply:
sudo sysctl --systemStep 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
sudogroup — they shouldn't need it; usesystemdunits with the rightUser=/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 myappValidate 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-journaldIn /etc/systemd/journald.conf:
Storage=persistent
SystemMaxUse=2G
SystemKeepFree=2G
SystemMaxFileSize=200MFor 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 -20Common follow-ups
- Docker on the same VPS → see
docker-container-security(planned). Note: Docker bypasses UFW by default; you need ufw-docker rules. - WordPress on this VPS → see
wordpress-hardening - Behind Cloudflare → see
cloudflare-hardening, 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 yesorPasswordAuthentication yeson internet-facing SSH
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

