All posts
§03 · Writing26.05.203 min read

Your Server Nobody Cares About — Until You Expose It to the Internet

Your server is irrelevant to the internet. Nobody knows it exists, nobody cares.

Until you give it a public IP.

Within a few hours of deploying my EC2 instance, the Nginx access logs were already full of traffic from IPs I'd never heard of — probing paths like /admin, /.env, /wp-login.php. No WordPress, no .env, no admin panel at those paths. Didn't matter. Bots don't check first. They just knock.

This is normal. It's not a hack. It's the background noise of the internet. But it was the first time I'd ever operated a server directly exposed to the public internet, and it reframed how I thought about security entirely.

The Attacker's Economics

Port scanners run continuously across the entire IPv4 space. Tools like Shodan, Censys, and dozens of criminal equivalents map every public IP, every open port, every exposed service. The moment your instance gets an Elastic IP, it's indexed.

The bots aren't targeting you specifically. They're running automated playbooks: try default credentials on SSH port 22, probe for known vulnerabilities on common ports, check for misconfigured admin endpoints. Most of it is noise. Some of it finds something.

Understanding this changed how I approached the security model for krealalejo.dev. The goal isn't to be invisible — it's to have nothing worth finding.

Defense in Layers

I didn't want to spend money on security tooling. The entire stack needed to stay within AWS Free Tier. So the approach was layered controls, each cheap or free:

Layer 1: AWS Security Groups

Security Groups are stateful firewalls at the instance level. My EC2 only accepts inbound traffic on ports 80 (HTTP) and 443 (HTTPS) from anywhere, and port 22 (SSH) from my specific IP only. Everything else is dropped before it reaches the instance. Port scanners see a closed port and move on.

This is the outermost layer. It's free, managed by AWS, and it eliminates entire categories of attack before any application code runs.

Layer 2: Nginx as Reverse Proxy

Nginx sits in front of both the Nuxt frontend and the Spring Boot API. It handles SSL termination, and it's the only process listening on the public-facing ports. The API runs on port 8080, bound to localhost — unreachable from the outside.

At the Nginx level I can rate-limit requests, block specific user agents, and return 444 (no response) for traffic that matches known bot patterns. Most of the scan traffic that gets past the Security Groups hits Nginx and gets dropped here.

Layer 3: Application-Level Auth

The admin panel is a separate concern. It's not protected by a static password or a simple secret — it uses AWS Cognito's Hosted UI with an OAuth2 authorization code flow. The browser redirects to Cognito, authenticates there, and comes back with a short-lived token stored in an httpOnly cookie. JavaScript can't read it. A CSRF attack can't use it cross-origin.

Write operations to the API never come directly from the browser. They go through Nuxt server routes, which read the cookie server-side and forward a Bearer token. The Spring Boot API validates that token against Cognito's public keys on every authenticated request.

What This Costs

Zero euros per month. Security Groups are part of EC2. Nginx is open source. Cognito's free tier covers 50,000 monthly active users — I'm the only user of the admin panel.

The lesson: layered security doesn't require enterprise tooling. It requires thinking through each surface and closing what doesn't need to be open.


Have you ever looked at your server's access logs right after deploying? The first time is always a surprise.