Securing a VPS Before You Put It in Your Deploy Pipeline
A deploy pipeline puts code on a server automatically, from the internet, on every push. Building one on a box you have not hardened is a decision to automate access to something you have not secured. This is the hour of work that goes first — and the order matters more than the list.
A deploy pipeline is a machine that puts code on a server automatically, from the internet, on every push. Building one on a box you have not hardened is not a shortcut — it is a decision to automate access to something you have not secured, and the automation is the part that makes it interesting to somebody else.
This is the hour of work that goes before the pipeline. Not a comprehensive security guide; the specific things that matter because you are about to point a CI runner at this machine.
That first number is not rhetorical. Bring up a VPS with a public IP and password authentication and watch /var/log/auth.log: automated credential stuffing finds it within minutes, continuously, forever. It is background radiation, and the whole point of the list below is to make it irrelevant rather than to stop it.
Order
Do these before the box does anything useful
1 & 2 — SSH, in the only safe order
# Do this ONLY after you have confirmed key login works in a second terminal
# that you keep open. If you get this wrong with one session, you are locked out.
PasswordAuthentication no
PermitRootLogin no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers you deploy # an explicit list beats any blocklist
# Cheap, and closes the "hold the connection open forever" class of abuse.
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
sshd -t before reloading. A config error plus a reload plus one open session is the classic way to lose access to a box you have just started paying for.
3 — Firewall: deny by default
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
# MySQL, Redis and your app port are NOT in that list, deliberately.
# They bind to 127.0.0.1 and are reached over an SSH tunnel when you need them.
The single most common real exposure on a small VPS is not a clever exploit — it is Redis or MySQL bound to 0.0.0.0 with a weak password because a tutorial said to make it reachable. Check what is actually listening: ss -tlnp. Anything on 0.0.0.0 that is not your web server needs a reason.
4 — Unattended security updates
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
# In /etc/apt/apt.conf.d/50unattended-upgrades keep ONLY the security origin
# enabled. Automatic feature updates on a production box is how you find out
# your PHP minor version changed at 3am.
Unattended-Upgrade::Automatic-Reboot "false";
Automatic reboot off, deliberately: a box that reboots itself at 3am without draining connections is its own outage. Patch automatically, reboot on your schedule, and watch for the reboot-required flag.
5 — The deploy user
This is the step that exists specifically because of the pipeline, and the one people skip. Full detail is in the pipeline post; the shape is a user that owns only the app directory, a key forced to one command, and two explicit sudoers lines.
The rest
Worth an extra twenty minutes
| Measure | What it buys | Worth it? |
|---|---|---|
| fail2ban | Bans repeat offenders; mostly quieter logs once passwords are off | Cheap, marginal |
| Separate app user from deploy user | PHP-FPM cannot overwrite its own code | Yes — stops a web exploit becoming persistence |
.env at 600, owned by deploy | Not world-readable on a multi-user box | Yes, and check it |
| Off-box backups, restore tested | The only control that helps after everything else failed | Yes, above all of this |
| Log shipping | Logs survive the box being compromised | Yes, once it matters |
| 2FA on the hosting panel | Protects the console that can reset root | Yes — people harden the box and forget the panel |
Honesty
What this list does not cover
- Application security. Every control here is at the OS boundary. SQL injection, broken authorisation and dependency vulnerabilities walk straight past all of it.
- Supply chain. Your pipeline installs hundreds of packages from the internet on every build. Lockfiles and pinned action SHAs are the minimum, and they are not on this page.
- Detection. This is all prevention. Whether you would notice a compromise is a different discipline and a much harder one.
- Compliance. If you have real obligations, this list is a starting point and not an answer.
- Multi-tenant risk. A VPS is a shared kernel. This does not address the hypervisor.
The checklist
- Key login working and tested in a second session before touching sshd.
PasswordAuthentication noandPermitRootLogin no, validated withsshd -t.AllowUsersas an explicit list.- Deny incoming by default; only 22, 80 and 443 open.
ss -tlnpreviewed — nothing unexpected on0.0.0.0.- Unattended security upgrades on, automatic reboot off.
- Deploy user with a forced-command key and two sudoers lines.
- App user separate from deploy user, so the web process cannot rewrite its own code.
.envat600, owned by deploy, verified.- Off-box backups with a restore you have actually performed.
Where to start on Monday
Run ss -tlnp on a server you already have. If a database, a cache or an application port is listening on 0.0.0.0, that is today's job and it is more urgent than everything else on this page.
- key-only SSH
- AllowUsers
- deny by default
- ss -tlnp
- unattended-upgrades
- forced command
- narrow sudo
- tested restore
Once this is done, the pipeline and the zero-downtime deploy are the next two steps, and the CI/CD guide covers whether you wanted a VPS in the first place.
Comments (0)
No comments yet
Be the first to share a thought on this article.
Join the conversation