Miles Huynh

My real SSH port was just as exposed as my fake one

Reading time: 5 minutes

Once the domain and HTTPS were live, I went looking for the next thing to fix and found something dumber than expected: the box that hosts my honeypot had a second, unintentional honeypot running right next to it — my real SSH.

Checking what was actually listening

I'd been assuming the only thing exposed to the world was Cowrie, the fake SSH on port 2222. A quick check said otherwise:

sudo ss -tlnp | grep -E ':22|:2222'

Port 2222 was twistd, the Cowrie process, exactly as expected. But port 22 — the real sshd — was also listening on 0.0.0.0, and a look at the EC2 security group confirmed it: SSH was open to 0.0.0.0/0, same as the honeypot port. Any scanner on the internet had just as much access to my real login as to the fake one. The only difference was that one of them was supposed to be open.

Closing the real door

Fixed it in the security group rather than on the box itself — changed the source on the port 22 rule from 0.0.0.0/0 to "My IP", which auto-fills your current public IP. That's a live IP restriction with no risk of locking myself out, since editing a security group doesn't require SSH access in the first place.

The one tradeoff: my office IP isn't static, so if it changes I'll need to go back and update that rule. Worth it — it's a two-minute fix from a browser, not a config file I could typo my way into a lockout with.

Checked sshd itself while I was in there. Turned out password auth was already disabled — key-only login, set at some point I don't even remember. So even with the port wide open, nobody was ever going to brute-force their way in. The security group fix was about cutting log noise and attack surface, not plugging an active hole. Good to know the floor was already there; better to not be relying on it as the only floor.

Locking /admin behind a second door

The Bludit login at /admin was sitting behind nothing but its own password field. Added HTTP Basic Auth in front of it at the Nginx level, so a browser prompt has to be cleared before the actual login page even loads. One thing that's easy to get wrong: a top-level Nginx location and a regex .php location don't share context by default, so the auth_basic directive has to be duplicated inside a nested location for the PHP handler too, or the rule silently stops applying once the request gets internally rewritten to index.php.

One thing I nearly did wrong: I almost pointed fail2ban at the honeypot port too, out of a vague instinct that "more banning is more secure." That would have defeated the entire point — Cowrie exists to keep attackers talking, not to get rid of them faster. Checked what fail2ban was actually watching, and found one jail, sshd, already running, already correctly scoped to the real port only. It had apparently been there the whole time and I'd just never checked.

What I actually learned

I'd mentally filed "the honeypot is exposed" as the interesting fact about this box and stopped thinking about what else was exposed by default. Every port a security-group wizard opens for you is equally open to everyone, whether you meant it to be a lure or not.

Restricting a security group rule to "my IP" is safer than it sounds, precisely because fixing it wrong just means editing the rule again from a browser — there's no way to lock yourself out of the one thing (the AWS console) you'd need to fix it.

"Appendix: every command from today's hardening pass

sudo ss -tlnp | grep -E ':22|:2222'
sudo grep -i "^Port" /etc/ssh/sshd_config
sudo grep -iE "PasswordAuthentication|PermitRootLogin|PubkeyAuthentication" /etc/ssh/sshd_config
sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status fail2ban
sudo fail2ban-client status

Plus one change made entirely in the AWS console: the EC2 security group's port 22 rule, source changed from 0.0.0.0/0 to my own IP"

Next up: back to the actual honeypot — see what's actually landed in the Cowrie logs and write it up.

Getting a real domain and real HTTPS

Reading time: 4 minutes

Picking up from where the honeypot post left off — today's goal was the "next up" from that post: stop serving this site off a bare IP address and get it behind a real domain with real HTTPS.

Free domain, the lazy way

I didn't want to pay for a domain just to find out if I even like doing this, so I went with DuckDNS (duckdns.org) — free dynamic DNS, sign in with an existing account, type a subdomain, done. Mine ended up mileshuynh.duckdns.org. The only gotcha: when you're on the "current ip" field, DuckDNS auto-fills the IP of whatever machine is looking at the page — which was my office IP, not my EC2 box. Had to manually overwrite it with the server's public IP before the domain actually pointed anywhere useful.

Pointing Nginx at the new domain

The server was already running Nginx + PHP-FPM (I'd forgotten I set that up), serving Bludit off the default catch-all server block — server_name _; meaning "answer to any hostname." I changed that to my actual domain so Certbot would have something concrete to attach a certificate to:

server_name mileshuynh.duckdns.org;

Certbot, and it just worked

I ran:sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d mileshuynh.duckdns.org

Answered a couple of prompts, picked "redirect HTTP to HTTPS," and that was it — cert issued, deployed, renewal scheduled automatically. First time I've set up a real Let's Encrypt cert myself instead of it being invisible platform magic, and it's a genuinely small amount of work for what it buys you.

The site loaded... with no styling

Switched over to https:// and the page came back as plain unstyled HTML — no fonts, no layout, nothing. Classic mixed content: the browser console pointed straight at it —

Mixed Content: The page at https://mileshuynh.duckdns.org/ was loaded over HTTPS, but requested an insecure resource http://18.208.211.75/bl-themes/.../favicon.png. This request has been blocked.

Bludit had the site's base URL saved as the old http://<ip> from before I touched DNS or TLS, so every asset it generated a link for was still pointing at plain HTTP on the raw IP. Browsers refuse to load insecure sub-resources on an otherwise-secure page, so the CSS/JS/images all got silently blocked. Updated the URL setting to the real https:// domain and the page came back looking like an actual website again.

A brief detour into panicking about AWS billing

Checked Cost Explorer out of habit and watched the number tick from $0.16 to $0.21 over the course of the day, which is a great way to briefly convince yourself you've done something expensive. Turned out to be nothing dramatic — I'm on AWS's newer credit-based free plan, not the old 12-month free tier, so everything (compute hours, EBS storage, and — new since Feb 2024 — a per-hour charge on every public IPv4 address, even the free-tier default one) draws down from a $100 credit balance instead of hitting a card. At the current burn rate the credit comfortably outlasts the free plan's 184-day window. Worth knowing before I go add an Elastic IP: it costs exactly the same as the IP I already have, as long as it stays attached to a running instance.

What I actually learned

A "free" domain and "free" HTTPS are each about 10 minutes of work once you know the two gotchas — DuckDNS grabs your IP, not the server's, and Bludit caches its own base URL and won't update it for you.

Mixed content errors are one of the most self-explanatory error messages a browser gives you — the console told me exactly what was broken and where, no guessing required.

AWS's billing dashboard is intimidating mostly because it's spread across four different pages (Cost Explorer, Bills, Free Tier, Billing summary) that each show a different slice of the same number. Once you find the per-usage-type breakdown, it stops being scary.

Next up: back to the actual point of the box — let the Cowrie honeypot run for a few days and write up what it caught.

Building my first honeypot on AWS

Building my first honeypot on AWS

Reading time: 3 minutes


I've spent most of my time in IT Support handling tickets, MFA resets, and onboarding new hires — not touching cloud infrastructure directly. Today I wanted to change that, so I gave myself one goal: launch a real AWS server, put something live on the internet, and see what actually happens to it.

  •  Launching the instance

I picked the simplest possible starting point — a `t3.micro` running Ubuntu, free-tier eligible, with a $100 credit covering the cost. No key pair, connecting straight from the browser through EC2 Instance Connect. Within a few minutes I had a real public IP and a real terminal into a machine I'd never touched before.

  •  Putting something live

First step was nginx, just to prove I could serve something. Then I built a personal page — hand-written HTML and CSS, deployed by pasting the file into `nano` over SSH. It's a small thing, but it was the first server I've ever owned end to end.

Then I got curious about who else was watching

Any public IP gets scanned constantly — that's just background noise on the internet. Instead of ignoring it, I set up [Cowrie](https://github.com/cowrie/cowrie), a fake SSH server that pretends to be a real Linux box on port 2222. Anyone who connects gets a convincing fake shell — `whoami`, `uname -a`, even a fake `/etc/passwd` — and every command they type gets logged.

I set it up under `systemd` so it survives reboots, opened the port to the world, and within a few hours the first scanner found it — connected, grabbed the banner, and disconnected in 34 milliseconds. No login attempt, no commands. Just reconnaissance, the internet doing what it always does.

  • What I actually learned

- Launching a real EC2 instance is a lot less scary than it sounds — the free tier and a small credit make it safe to experiment.
- A public IP gets attention within hours, not days. That surprised me.
- Logging is the whole point of a honeypot — the fake shell is just bait, the log file is the actual value.
- Deploying by hand (nano over SSH) is slow but it forced me to understand every file instead of copy-pasting a script I didn't read.

Next up: getting a free domain and real HTTPS on this site, and writing up whatever the honeypot picks up over the next few days.