Introduction
A fresh VPS is one of the simplest yet most dangerous environments you can start with. It is clean, but also completely exposed. There are no safeguards, no structure, and no assumptions about how it will be used. Everything is open by default, which means everything depends on how quickly and carefully you set things up.
This guide is not meant to be a strict set of instructions. Instead, it is a record of how I learned to turn a raw server into something more stable, controlled, and safe to build on.
First Access
The first time I connected to the VPS, it didn’t feel like setting up a system yet. It felt more like stepping into an empty room that I had complete control over—but also complete responsibility for.
Access was given through SSH as the root user:
ssh root@IP_VPS.Once I was inside, there were no restrictions at all. Root had full control over the system. At first, this felt convenient, everything was immediately accessible. But very quickly, it became clear that this level of access was also the main risk.
There was no separation between safe actions and dangerous ones. One wrong command could affect the entire system.
That realization made one thing clear, this root access was not meant to be permanent. It was only a temporary entry point, used to prepare the system before making it safer and more structured.
Updating
Right after that first login, I was already faced with the first practical decision, whether the system could actually be trusted in its current state.
Even though the server had just been created, the packages inside it were still tied to the base Debian image it was built from. That meant there was no guarantee everything was up to date or patched.
So the first real action I took was updating the system:
sudo apt update && apt upgrade -yAt first glance, it feels like a routine command, something you run without thinking. But at that moment, it felt like the first time I was actually changing the state of the machine.
It wasn’t just maintenance. It was the beginning of shaping the server into something stable, something I could safely build on.
In a way, this was the point where the server stopped being “just created” and started becoming something I was responsible for.
Choosing What to Install
Once the system was up to date, the next question wasn’t about commands or configuration yet. It was something simpler, but more important: what should actually be installed on this machine?
At that point, it was easy to fall into the trap of installing everything that seemed useful. There are countless tools that promise convenience, automation, and control. But the more I thought about it, the clearer it became that adding too much too early would only create unnecessary complexity.
So instead of trying to prepare for every possible scenario, I shifted the focus to something more minimal: only installing tools that would directly help with managing and understanding the system.
This meant a small set of utilities for everyday administration and future development—tools for transferring data, working with code, monitoring system behavior, managing network access, and running containers.
Packages like curl, git, btop, ufw, fail2ban, and podman were not installed because I needed everything they offer immediately. They were chosen because together, they gave just enough capability to observe, control, and build on the system without turning it into a cluttered environment.
In a way, this step wasn’t about adding tools. It was about resisting the urge to add too many.
Why Podman?
One question that naturally comes up here is why I chose podman instead of the more commonly used docker, especially since Docker is still the industry standard in many production environments.
The decision wasn’t about rejecting Docker or ignoring industry practices. It was more about the context of this server. This server is not part of a large production cluster. It is a single machine that I manage alone, where simplicity and security boundaries matter more than ecosystem conventions.
podman offers a few subtle but important advantages in that context. It can run containers without a daemon, which reduces a central point of failure. It also supports rootless containers more naturally, which aligns better with the idea of minimizing risk on a freshly exposed server.
Docker, on the other hand, is widely used in the industry for good reasons—its ecosystem, tooling, and community support are far more mature. But it also introduces a heavier architecture that assumes a more complex operational environment.
In this case, I wasn’t trying to match industry standards. I was trying to build a system that I could fully understand and control on my own terms. So the choice of podman was less about replacing Docker, and more about reducing what I didn’t need yet.
Switching to SSH Keys
At some point after getting comfortable with basic access, I started thinking more seriously about security. So far, I had been relying on password-based SSH login. It worked, but it also felt like a weak foundation for something that was exposed to the internet.
The problem with passwords is that they depend on human behavior — something predictable, reusable, and ultimately vulnerable to guessing or brute-force attacks. On a public server, that felt like an unnecessary risk to keep around. That’s when I decided to switch to key-based authentication.
Instead of a password, SSH uses a cryptographic key pair created with:
ssh-keygen -t ed25519 -C "vps-personal"This generates two parts: a private key that stays only on my local machine, and a public key that gets copied to the server.
To make sure I had it correctly, I checked the public key directly:
cat ~/.ssh/id_ed25519.pubThis was the part that would be shared with the server.
What changed my perspective was how authentication worked after that. The server no longer needed to “trust” a password at all. It simply checked whether the private key I held matched the public key stored in authorized_keys.
There is no guessing involved anymore. No human-readable secret to brute force. Just a mathematical relationship between two keys. It was a small change in setup, but it fundamentally changed how access to the server was protected.
Installing the Key on the Server
Once the key pair was generated, the next step was to actually give the server access to it.
This is done by placing the public key into:
~/.ssh/authorized_keysSo on the server, I first prepared the directory:
mkdir -p ~/.sshThen I opened the file where the key would be stored:
nano ~/.ssh/authorized_keysAt this point, I simply copied the public key from my local machine and pasted it there.
What I didn’t realize at first was that SSH wouldn’t accept this setup immediately unless the permissions were correct. So after placing the key, I also had to make sure the permissions were correct, because SSH is very strict about file and directory permissions.
So I set them explicitly:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keysWithout this, SSH can simply refuse to use the key entirely, even if everything else is correct.
The process started to feel less like “adding a login method” and more like defining who is allowed to enter the system. The server wasn’t just accepting credentials anymore. It was being told which cryptographic identity it should recognize as valid.
What I didn’t fully appreciate at first is how strict Linux is about this setup. The .ssh directory and its files need to have very specific permissions. If they are too open — even slightly — the SSH service will simply refuse to use them. There is no warning in the sense of “this might still work but is insecure.” It is a hard refusal.
At first, this felt strict. But over time, it became clear that this behavior is intentional. The system would rather block access completely than accept a configuration that could be exploited later. It was another small moment where I realized: on a server, security is not optional behavior — it is enforced by default.
Before moving forward, I needed to confirm that the key-based authentication was actually working on the server.
From my local machine, I tried logging in again:
ssh root@IP_VPSIf everything had been set up correctly, this connection should succeed without asking for a password, using the SSH key instead.
This step acted as a quick validation of everything done so far in the previous setup, from placing the key into ~/.ssh/authorized_keys to enforcing the correct permissions on the server.
Creating New User
After getting SSH key authentication working for the root user, the next step was to introduce separation in how the system is used.
Running everything as root might feel convenient at first, but it removes any boundary between normal actions and system-level changes. Over time, that becomes risky, especially on a machine exposed to the internet.
So instead of continuing with root as the default identity, I created a new user that would handle everyday operations:
adduser adminThen I granted it administrative privileges through sudo, so elevated actions would still be possible when needed, but not used by default:
usermod -aG sudo adminThis introduced an important separation: root would no longer be the default identity for interaction, but a controlled fallback.
Once the user existed, I needed to transfer SSH access to it. Instead of generating a new key, I reused the existing configuration by moving the authorized keys into the new user’s home directory.
First, I prepared the SSH directory:
mkdir -p /home/admin/.sshThen I copied the existing SSH configuration:
cp ~/.ssh/authorized_keys /home/admin/.ssh/authorized_keysAfter that, I fixed ownership and permissions to ensure SSH would accept it:
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keysAt this point, access was no longer tied to root. It was now tied to a normal user account, with controlled escalation through sudo.
To confirm everything still worked, I tested the login using the new user:
ssh admin@IP_VPSIf everything was set up correctly, this would still authenticate using the same SSH key, but land me inside the new non-root user environment instead of root.
Reducing Server Exposure
After confirming that SSH key authentication worked with the new user, the next phase was reducing the server’s exposure.
At this point, the system was already functional, but still open in ways that are commonly targeted by automated attacks. The goal was to remove as many unnecessary entry points as possible.
So I edited the SSH configuration file:
nano /etc/ssh/sshd_configInside this file, I adjusted a few key settings.
First, I changed the default port. This does not add real security by itself, but it does reduce noise from automated bots that constantly scan the default SSH port:
Port 22022Then I disabled password-based authentication entirely, forcing all access to rely on SSH keys:
PasswordAuthentication no
PubkeyAuthentication yesFinally, I disabled direct root login. This ensures that even if someone gains access, they cannot immediately operate with full system privileges:
PermitRootLogin noThese two changes—disabling password login and blocking root access—are the most meaningful reductions in attack surface. They eliminate the most common entry points used in automated SSH attacks.
After saving the configuration, I restarted the SSH service to apply the changes:
systemctl restart sshFrom this point on, the server became noticeably more resistant to random login attempts and automated scanning. Access was now tightly bound to SSH keys and a non-root user, rather than exposed credentials or direct root entry.
Next Part
At this point, the server was no longer in its initial “fresh install” state. The most critical access paths had been secured, and the system was now operating with a clear separation between root and a normal user, enforced SSH key authentication, and reduced exposure through SSH hardening.
But this is only the beginning of what a usable server actually requires.
Security at the login level does not automatically make a system production-ready. There are still layers above this—firewall rules, service management, monitoring, and the actual setup of the applications that will eventually run on top of it.
So instead of continuing in the same direction, this is where I pause this part of the journey. In the next part, I’ll continue from this hardened baseline and start building the environment that actually runs on top of it.