Gotchaa Lab
Back to Blog
cybersecuritydevopsserver-hardeningmalaysiainfrastructure

Server Hardening Basics: 5 Fixes Before Your App Goes Live

14 May 2026·9 min read·By Gotchaa Lab
Server Hardening Basics: 5 Fixes Before Your App Goes Live

TL;DR

  • Clean code on a misconfigured server is still wide open. The 5 fixes below close the doors most attackers actually walk through, not the exotic ones.
  • SSH keys, a firewall, Fail2Ban, a non-root app user, and a reverse proxy take roughly 30 minutes on a fresh VPS and stop the bulk of automated bot traffic.
  • Under PDPA, your business is on the hook for personal data on your servers. Basic hardening is the floor, not optional.

Listen to this podcast

Most developers obsess over writing secure code. Parameterised queries, input validation, dependency scans, the whole stack. Good habits.

Then they deploy that clean code onto a server with port 22 open to the world, root login enabled, the app running as root, and no rate limiting in front of anything. Clean code on a misconfigured server is still wide open. You did the hard part for nothing.

Here are 5 server hardening basics to lock down before your app goes live. None of them are exotic. All of them block the bulk of automated traffic that hits a fresh VPS within minutes of its IP being assigned.

Not a developer? Read this part.

The rest of this article gets technical fast. If you run a business but do not write code, you do not need to understand the commands. You just need to know what to ask your dev team before launch. Here are the 5 questions:

  1. "Did you turn off password login on the server and switch to SSH keys?" (yes / no)
  2. "Is there a firewall, and have you closed every port except the ones our app actually uses?" (yes / no)
  3. "Is Fail2Ban (or something like it) installed to block repeated failed login attempts?" (yes / no)
  4. "Does our app run as a regular user, not as root?" (yes / no)
  5. "Is there a reverse proxy like Nginx or Caddy in front of our app, handling HTTPS?" (yes / no)

If the answer to any of these is "no" or "not sure," do not launch yet. None of these take more than 30 minutes for someone who knows what they are doing. Whoever set up your server should be able to confirm all 5 in writing.

1. Disable root login and use SSH keys

The default state of a fresh Linux VPS is the worst possible state. Port 22 open, root login allowed, password authentication enabled. Within a few hours of provisioning, you will see thousands of failed login attempts from bots scanning for weak root passwords.

Two changes close that door.

First, create a regular user with sudo access and switch to SSH key authentication. Generate a key on your laptop with ssh-keygen -t ed25519, copy the public key to the server with ssh-copy-id user@yourserver, log in once to confirm it works, then disable password and root login in /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Restart SSH with sudo systemctl restart sshd and keep your original session open until you have confirmed key login works from a second terminal. If you lock yourself out, you will be paying your VPS provider for a console session at 2am.

SSH keys are not "harder to crack". They are mathematically infeasible to brute-force in any reasonable timeframe. Passwords are guessable. There is no contest.

2. Set up a firewall

Every open port is a service. Every service has a version. Every version has known vulnerabilities published somewhere. A firewall lets you say "only these ports, nothing else" and stop arguing.

On Ubuntu, UFW gets you 90% of the way in three commands:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp     # SSH
sudo ufw allow 80/tcp     # HTTP
sudo ufw allow 443/tcp    # HTTPS
sudo ufw enable

That is it. Postgres on 5432, Redis on 6379, your app server on 3000 or 8080? None of them are reachable from the internet. Your web app talks to them on localhost, which is exactly what you want.

A dim server corridor with most doors closed and only a few warmly lit doors open, illustrating a firewall that exposes only the ports you actually need A firewall is just a corridor of locked doors. Open only the ones your app actually walks through.

The most common mistake we see in code audits: a .env with DB_HOST=0.0.0.0 and a Postgres instance listening on all interfaces "for development convenience." A firewall saves you from yourself when that config slips into production.

If your VPS provider has a network-level firewall (DigitalOcean Cloud Firewalls, AWS Security Groups, Hetzner Cloud Firewall), use both. Defence in depth is real, not theatre.

3. Install Fail2Ban

Fail2Ban watches your log files. When an IP fails SSH logins, web app logins, or matches any pattern you configure, Fail2Ban temporarily adds an iptables rule to drop traffic from that IP. By default the ban is 10 minutes after 3 failed attempts within a 10-minute window.

Install and enable it:

sudo apt install fail2ban
sudo systemctl enable --now fail2ban

The default config already protects SSH. Custom jails for Nginx auth, WordPress login, or your app's failed-login endpoint are a few lines of config away.

Two things to know. First, Fail2Ban does not replace a firewall. The firewall decides what is reachable. Fail2Ban decides who has burned their welcome. They are layered. Second, if your app sits behind Cloudflare or a load balancer, every request looks like it comes from the same proxy IP, which makes Fail2Ban useless unless you configure it to read the real IP from X-Forwarded-For. Check before you trust the bans.

4. Never run your app as root

If you start your Node, Python, or Laravel app with sudo because "it just works that way," stop. You have handed every dependency, every NPM package, every Composer library full root on your server. One compromised supply chain attack (the LiteLLM attack earlier this year is a recent reminder) and the attacker owns everything.

Create a dedicated, unprivileged user for the app:

sudo adduser --system --group --no-create-home appuser
sudo chown -R appuser:appuser /var/www/myapp

Then run your app under that user. Systemd unit files make this trivial:

[Service]
User=appuser
Group=appuser
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node server.js

If appuser gets compromised, the attacker has the app's files and the app's database credentials. That is bad. But they cannot install rootkits, modify SSH config, read other users' files, or pivot to other services on the box. The blast radius is contained to one app.

This is also true on Docker. USER node or USER 1000 in your Dockerfile, not the default root. Containers are not a security boundary by default. Treat them like processes that need an unprivileged user.

5. Put a reverse proxy in front of your app

Your Node app on port 3000 is happy serving HTTP on a single thread. Then 10 bots discover it, hammer it with malformed requests, and your event loop is gone. Reverse proxies (Nginx, Caddy, Traefik) solve this by sitting between the internet and your app, handling the messy parts.

What you actually get:

  • TLS termination. Caddy gives you HTTPS with a few lines of config and auto-renewing Let's Encrypt certs. Nginx with Certbot takes 5 minutes. Stop letting your Node process handle SSL.
  • Header hardening. Strict-Transport-Security, X-Frame-Options, Content-Security-Policy, all added at the proxy layer without touching your app.
  • Rate limiting. limit_req in Nginx caps how often a single IP can hit your /login endpoint. Combined with Fail2Ban, this kills most credential-stuffing attacks before they reach your app.
  • Connection buffering. Slow clients (Slowloris-style attacks) eat proxy connections, not your app's. Nginx absorbs them far better than your Node or Express server, especially once you set sensible client_header_timeout, client_body_timeout, and limit_conn values. Nginx alone is not a magic fix, but it changes the failure mode from "app down" to "a few proxy connections busy."

Caddy is the lowest-friction option for small teams. A few lines in a Caddyfile and you have HTTPS, HTTP/2, and reasonable defaults. Nginx is the boring, battle-tested choice if you need fine control. Pick one. Do not run your app directly on port 80 or 443.

What this still does not protect you from

These five things stop the bots and the lazy. They do not stop:

  • A SQL injection in your code (write parameterised queries).
  • A leaked .env in a public GitHub repo (audit your commits).
  • A compromised developer laptop with SSH keys on it (use a passphrase, rotate keys).
  • A supply chain attack in a dependency (lock your versions, watch CVE feeds).
  • A determined attacker with a zero-day for your stack.

Hardening is the floor. Application security is a separate problem that lives in your code, your CI pipeline, and your dependency hygiene. We have written about vibe coding security risks and supply chain attacks on AI tools before. Read those if you want the application-layer view.

The PDPA angle

If your Malaysian business stores any personal data on this server (names, IC numbers, phone, email, anything that identifies a person), PDPA is part of the conversation whether you like it or not. The 2024 amendment introduced a 72-hour breach notification window for incidents that meet the threshold, and "we left root SSH open" is not a defence the regulator finds sympathetic.

Basic hardening is not a compliance silver bullet. But a breach caused by skipping it is one of the fastest ways to trigger a disclosure obligation and the reputational fallout that comes with it. Treat the 30 minutes this takes as the cheapest insurance you will ever buy.

What we think

We have audited a lot of Malaysian SME and startup deployments. The pattern is consistent: clean application code, weak server config. Devs are taught how to write SQL safely but rarely taught how to deploy safely. Most security incidents we see for early-stage companies do not start with a clever exploit. They start with port 22 open to the world, password login enabled, an attacker getting in via root:admin123, and 6 weeks of slow lateral movement nobody noticed.

The fix is unglamorous. It is also fast. A new VPS to production-hardened in 30 minutes if you know what you are doing. The first time, give yourself an hour and a checklist.

These 5 things will not make your server invincible. They will make attackers move on to easier targets. That is honestly the goal.

Want a second pair of eyes on your production setup before you ship? Our cybersecurity team does this kind of review regularly. WhatsApp us or drop a note and we will run through your config. No sales pitch.

References

  1. OpenSSH sshd_config Manual
  2. Ubuntu UFW Documentation
  3. Fail2Ban Official Documentation
  4. Caddy Server Documentation
  5. Nginx Rate Limiting Guide
  6. Malaysia Personal Data Protection Act (Amendment) 2024

Share this article

Frequently Asked Questions

What are the first three steps to secure a Linux server?
Patch the system (apt update && apt upgrade), disable root SSH login and switch to SSH keys, then turn on a firewall (UFW on Ubuntu) that only allows ports your app actually uses. Those three alone block most opportunistic bot traffic on a fresh VPS.
Why should I disable root login over SSH?
Bots scan the internet for SSH on port 22 and try logging in as root with common passwords. If root is disabled, they have to guess both a valid username and a key, which is several orders of magnitude harder. Always create a sudo-enabled user first, confirm you can log in, then disable root.
Does Fail2Ban replace a firewall?
No. A firewall controls which ports are reachable from the internet. Fail2Ban watches log files and temporarily blocks IPs that repeatedly fail to log in. They solve different problems. Run both.
Is server hardening enough for PDPA compliance in Malaysia?
It is the technical floor, not the full picture. PDPA also requires policies, breach notification within 72 hours, a Data Protection Officer for some businesses, and contracts with vendors who handle personal data. But a server compromise caused by skipping basic hardening is one of the fastest ways to trigger a breach you have to disclose.
Will these 5 fixes make my server unhackable?
No. Determined attackers can still find a way in through application vulnerabilities, zero-days, leaked credentials, or social engineering. The goal of basic hardening is to make your server uninteresting to the 99% of attacks that are automated and looking for easy targets.

Need help building this for your business?

We help Malaysian companies turn ideas like these into working software. Free consultation, no obligation.