Install
$ agentstack add skill-goldenwing-360-claude-security-skills-vps-hardening ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
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
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 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 yesorPasswordAuthentication yeson 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.
- Author: GoldenWing-360
- Source: GoldenWing-360/claude-security-skills
- License: MIT
- Homepage: https://goldenwing.at
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.