From Vulnerable VPS to Automated Fortress — Part II
Part I was the diagnosis: what a typical unmanaged VPS looks like, why it's a problem, and the principles that guide hardening. Now we get into the actual work. This part is the execution plan — commands in order, with context, explained so you understand what each one does and why it goes where it goes. This isn't a copy-paste recipe. It's a master blueprint you can adapt to any server from day zero.
Architect: 0n3Z3r0 | Roles: Senior SysAdmin & Ethical Hacker.
Goal: Full hardening implementation across 5 sequential phases.
Series: Part II of III — The Master Script and Technical Glossary.
The Master Script: From Zero to Fortress in 5 Phases
Order matters. You can't configure the firewall before you create the users. You can't automate reports before you have the directory structure in place. Each phase sets the foundation for the next one. Follow the order and the result is predictable. Skip steps and you end up with a half-hardened server — which is almost worse than an unprotected one, because it gives you false confidence.
Phase 1: Cleanup — Getting Rid of the Bloatware
Before you build anything, you clean. A freshly provisioned VPS comes with services you never asked for and don't need: print management, local network discovery, modem management. In a cloud environment these serve no purpose, but they do add attack surface. First step: update the system to its latest state, then strip out everything that has no business being there.
sudo apt update && sudo apt upgrade -y
# 2. Remove services identified in the diagnosis — no use on a cloud server
sudo apt purge cups avahi-daemon modemmanager -y
sudo apt autoremove -y
# 3. Set the default boot target: console only, no GUI
sudo systemctl set-default multi-user.target
That third command is more significant than it looks. If the server had a graphical interface installed — common on VPS instances that started as personal labs — this tells systemd not to load it on the next boot. Everything else keeps working exactly the same, but without pulling in X11, the display manager, and all their dependencies. Less RAM consumed, smaller attack surface, more focused system.
| Command | What it actually does |
|---|---|
apt update && apt upgrade -y |
Syncs the package repositories and applies all available patches. Closes known vulnerabilities before configuring anything else. |
apt purge cups avahi-daemon modemmanager |
Completely removes — including config files — three services with no function on a cloud server: print management, mDNS discovery, and modem management. |
apt autoremove -y |
Cleans up orphaned dependencies left behind by the packages you just removed. Full cleanup, nothing left dangling. |
systemctl set-default multi-user.target |
Sets the default boot mode to console only. No graphical environment loaded on every restart. |
Non-technical explanation
Imagine moving into a new office and finding the previous tenant left things behind: a printer you'll never use, an old router plugged into the wall, papers everywhere. Before you set up your own equipment, you clear it all out. That's this phase — leaving the server clean and blank, free of the previous tenant's junk, before you build your own infrastructure on top of it.
Phase 2: Building the User Hierarchy
This is where the real security architecture begins. The premise is straightforward: no task should have more permissions than it strictly needs to get done. We create four users with very specific roles, and only one of them gets sudo access.
sudo adduser labadmin # Day-to-day server administration
sudo adduser services # Running processes and containers
sudo adduser pentest # Auditing tools and ethical hacking
# Only labadmin gets administrative privileges
sudo usermod -aG sudo labadmin
The services and pentest users have no sudo access. That's deliberate. If a Docker container gets compromised and the attacker lands in the services user, they're in a dead end: can't install software, can't modify system configs, can't escalate. The cage works.
| User | Role | Has sudo? |
|---|---|---|
root |
Kernel maintenance and critical emergencies only. SSH access disabled. | It's root — full access by design, but locked out of SSH |
labadmin |
Daily administration, deployments, general server management. | Yes — the only account with admin privileges |
services |
Runs processes and Docker containers. No interactive shell. | No — process jail with zero privileges |
pentest |
Offensive tools and auditing. Isolated environment by design. | No — isolated from the rest of the system |
Non-technical explanation
In a company, the intern can use the office computer but can't access the accounting server. The IT tech can reach the servers but can't move money. The CFO can move money but doesn't touch the servers. Nobody has access to everything. If someone makes a mistake or gets tricked, the damage stays in their lane. That's exactly what we're doing here with system users — not distrust, just damage control built into the architecture.
Phase 3: Directory Architecture Under /opt
A server without structure is a server you can't defend or audit properly. If you need to know tomorrow what's in production, what's in testing, and where the logs are, the answer should be immediate. The /opt structure is that answer.
sudo mkdir -p /opt/{services,lab,tools,data}
sudo mkdir -p /opt/lab/{redteam,blueteam,malware,poc}
sudo mkdir -p /opt/services/{web,api,proxy}
sudo mkdir -p /opt/data/{logs,captures,dumps}
# labadmin owns and manages everything under /opt
sudo chown -R labadmin:labadmin /opt/
The -p flag on mkdir creates the entire hierarchy at once, including intermediate directories that don't exist yet. The chown -R applies the ownership change recursively to the whole /opt tree. From this point, labadmin can work freely inside that structure without needing sudo for every file operation.
/opt/
├── services/ # Production applications
│ ├── web/ # Web services and frontends
│ ├── api/ # Backends and APIs
│ └── proxy/ # Nginx configurations
├── lab/ # Lab and research environments
│ ├── redteam/ # Offensive tools
│ ├── blueteam/ # Defensive tools
│ ├── malware/ # Samples for analysis (isolated)
│ └── poc/ # Proof of concept testing
├── tools/ # Maintenance scripts and binaries
└── data/ # Persistent data
├── logs/ # Centralized logs
├── captures/ # Network traffic captures
└── dumps/ # Memory or DB dumps
| Directory | What it's for |
|---|---|
/opt/services/ |
Everything in production: Docker Compose files, active service configurations. The stuff that must not break. |
/opt/lab/ |
Testing environments and ethical hacking tools. Separated from production so an experiment can't take down a live service. |
/opt/tools/ |
Maintenance scripts, custom binaries, sysadmin utilities. All outside the system's standard PATH. |
/opt/data/ |
Data that must survive even if you reinstall the server: historical logs, traffic captures, database backups. |
Non-technical explanation
Think about a well-organized mechanic's workshop. Precision tools in one drawer. Spare parts in another. Oils on the right shelf. If something breaks at 2am, you know exactly where to look without turning on the lights. That's /opt — not perfectionism, it's that when things go wrong you need to find what you're looking for in seconds, not minutes.
Phase 4: Network Hardening — Firewall and VPN
This is the phase with the most visible impact. Before these commands run, the server is reachable from anywhere on the internet across multiple ports. After, only three public doors exist. Everything else disappears off the map.
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 2. Open only what you can justify
sudo ufw allow 22/tcp # SSH — your remote access to the server
sudo ufw allow 80/tcp # HTTP — redirect to HTTPS only
sudo ufw allow 443/tcp # HTTPS — encrypted web traffic
# 3. Install Tailscale — the invisibility layer for internal services
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# 4. Enable the firewall — from this point the kill switch is ON
sudo ufw enable
The order here is critical. Define the rules first, then enable the firewall. If you do it the other way around and forget to allow port 22 before running ufw enable, you'll lock yourself out of your own server over SSH. Tailscale gets installed before UFW is enabled because its install script needs internet access to download the binaries.
Once active, Portainer, databases, and any admin panel no longer need a public port. They're only reachable via the Tailscale network IP. To Shodan, to Nmap, to any external scanner — those services simply don't exist.
| Command | What it does and why it's in this order |
|---|---|
ufw default deny incoming |
Sets the base policy. Anything arriving from the internet without an explicit allow rule: blocked. |
ufw allow 22/tcp |
Must come before ufw enable. Miss this and you lose SSH access the moment the firewall turns on. |
tailscale up |
Connects the server to your private mesh network. Internal services now have a private IP exclusive to your authorized devices. |
ufw enable |
Activates the firewall with all rules already in place. This is the moment the kill switch flips to ON. |
Non-technical explanation
It's like building the security fence around a property. First you decide how many gates you want (three: SSH, HTTP, HTTPS). Then you install the fence. If you install the fence before leaving the main gate open, you're locked outside your own property. The command order exists for exactly that reason: rules first, then the lock. And Tailscale is the private passage that connects your home to the property without anyone outside knowing it exists.
Phase 5: Automating Reports with Cron
The last pillar is observability. A server that doesn't report what's happening is a server you're administering blind. The ssh-summary.sh script is the minimum viable solution: an automatic daily summary that lands in your log without you having to do anything.
sudo nano /usr/local/bin/ssh-summary.sh
# [Your log analysis logic goes here — see Part III]
# 2. Make it executable
sudo chmod +x /usr/local/bin/ssh-summary.sh
# 3. Schedule it in crontab to run every night at 23:00
(crontab -l 2>/dev/null; echo "0 23 * * * /usr/local/bin/ssh-summary.sh >> /var/log/ssh-summary.log 2>&1") | crontab -
That last line deserves some attention. The crontab -l 2>/dev/null lists the existing scheduled tasks, sending any errors to nowhere in case the crontab is currently empty. The output gets combined with the new line via a pipe, and everything is passed to crontab - which rewrites the full crontab. This is the safe way to add a task without wiping out the ones you already have.
| Element | What it does |
|---|---|
chmod +x |
Grants execute permission to the script. Without this, cron will try to run it and fail silently — the most frustrating kind of failure. |
0 23 * * * |
Cron syntax: minute 0, hour 23, every day of the month, every month, every day of the week. Every night at 23:00, no exceptions. |
>> ssh-summary.log |
Appends output to the existing log file. The double >> matters: a single one would overwrite your entire history every night. |
2>&1 |
Redirects errors to the same place as standard output. If the script fails, the error gets logged — you'll know it happened. |
Non-technical explanation
Imagine hiring a night security guard for your building. You tell them exactly what to check every night — login attempts, triggered alarms, who came in and when — and ask them to leave a written report on your desk before you arrive in the morning. That's cron with the script. The chmod +x is handing the guard their keycard. The crontab line is the schedule they sign. And the >> means the report gets added to the same notebook, not that yesterday's pages get thrown away.
Technical Glossary: The Language of Hardening
If you've made it this far and there are terms that kept showing up in the posts without ever fully clicking, this section is for you. These aren't Wikipedia definitions — each one is grounded in exactly what we've been doing throughout this series.
| Term | What it means in this context |
|---|---|
| Hardening | The process of reducing a system's attack surface: removing what's unnecessary, restricting permissions, closing entry vectors. It's not installing a tool — it's designing the system to be hard to compromise in the first place. |
| Least Privilege | The principle that every user, process, or service should only have access to exactly what it needs to do its job. Nothing more. If something goes wrong, the damage stays contained within that scope. |
| Attack Surface | The total set of points where an attacker can try to get in: open ports, running services, active user accounts, installed software. The smaller it is, the better. Everything you don't need is surface you have to defend. |
| Bloatware | Software installed by default that serves no useful purpose in the current environment. On a cloud server, cups (printers) or avahi-daemon (local network discovery) are pure bloatware — useless overhead that only adds attack surface. |
| Lateral Movement | An attack technique where, after compromising one account or service, the attacker moves through the system hunting for accounts with higher privileges. Proper user separation makes this significantly harder. |
| Blast Radius | The damage radius if something gets compromised. If the services user is exploited, the blast radius is limited to what that user can actually do. The goal of the architecture is to keep that radius as small as possible. |
| VPN Mesh (Tailscale) | A private network where all authorized devices are directly connected to each other, without a central server routing the traffic. Tailscale implements this on top of WireGuard: high performance, simple setup, zero public ports exposed. |
| Reverse Proxy | A server (Nginx, Caddy) that receives all incoming web requests on ports 80 and 443 and routes them internally to the right containers. The outside world sees one door. Everything behind it is invisible. |
| Shadow IT | Services or processes running on the system that the admin forgot they deployed. Containers stopped for months, inherited scripts, unused packages. Invisible attack surface because nobody is watching it. |
| CVE | Common Vulnerabilities and Exposures. The global identification system for security vulnerabilities. When unattended-upgrades patches your system, it's closing known CVEs before someone gets the chance to exploit them. |
| Rootkit | Malicious software designed to hide inside a system with root-level privileges. It modifies system binaries to stay invisible. rkhunter finds them by comparing binary hashes against a known-good database. |
| Headless | A server running without a graphical interface — terminal only. It's the production standard: lower resource consumption, fewer dependencies, and it eliminates an entire category of vulnerabilities tied to desktop environments. |
Non-technical explanation
If you came straight to this glossary, welcome. Security has a lot of its own vocabulary that can feel designed to exclude people who aren't already in the club. It isn't. These are precise terms for specific situations. The important thing isn't memorizing definitions — it's understanding the logic behind them. And that logic, at its core, is always the same: limit access, watch what happens, and reduce the damage when something fails. Everything else is just implementation detail.
> SYSTEM_READY > NODE_ONLINE