Honeypot Live Stats — 15844 connections today from 41 unique IPs (125500 total, 1534 unique IPs all-time)
Miles Huynh

Honeypot report: 2026-10-03

Reading time: ~1 minute

16556 connections hit the honeypot on 2026-10-03, from 53 unique IPs.

Top source IPs

8.217.18.158  —  16236 connections
3.133.226.214  —  19 connections
5.161.113.195  —  16 connections
178.156.189.113  —  15 connections
5.161.215.244  —  14 connections

Auto-generated from the Cowrie session logs.

The honeypot diaries: three days, one EC2 box

Reading time: 6 minutes

AUG 18 Domain & bastion AUG 19 Docker & near-miss AUG 20 Ansible & monitoring

Three days, one EC2 box, and a lot of things that broke in interesting ways. A recap of what actually got learned — not the polished version, the real order it happened in.

Aug 18 — Domain, hardening, and infrastructure-as-code

  • Free domain via DuckDNS + real HTTPS via Certbot, then chased down a mixed-content bug when the site lost its styling over the new https://
  • Locked the EC2 security group's SSH rule down to "my IP only," confirmed key-only login, added HTTP Basic Auth in front of /admin
  • Built a custom Bludit plugin from scratch: a live honeypot stats box, reading a JSON file a root cron job writes every 5 minutes
  • Automated blog publishing through the Bludit REST API instead of pasting into the editor by hand
  • SEO pass: Open Graph tags, sitemap, RSS, verified with Google Search Console
  • First real Git session — add/commit/diff/log on the actual automation scripts, not a tutorial repo
  • Provisioned a second EC2 — a bastion host — entirely through Terraform: security group, SSH key pair, and instance defined as code

The moment that actually taught something: SSH to the new bastion timed out for hours despite the security group, network ACL, and route table all checking out — even AWS's own Reachability Analyzer called the path fully reachable. The real cause was two layers down: the home ISP's route to us-east-1, not AWS at all. Switching to phone data connected instantly.

Aug 19 — Containers, indexes, and a five-figure near-miss

  • Locked the real server's SSH down to bastion-only, verified with a ProxyJump one-liner — direct access now times out on purpose
  • Dockerized Bludit locally (nginx + PHP-FPM + MySQL) and learned the counterintuitive part: containers don't save disk space, they isolate
  • Built 300,000 fake rows in MySQL to watch an index actually work — EXPLAIN flipped from type: ALL to type: ref, invisible on the clock, huge on the query plan
  • Nearly subscribed to AWS Bedrock Provisioned Throughput at $80–99/hour instead of the per-token rate — backed out one click before a five-figure monthly line item
  • Designed and shipped an actual logo: four SVG concepts, picked one, hand-embedded it into the theme's navbar.php
  • Cross-posted to dev.to to test the waters outside the blog's own audience

The moment that actually taught something: the Bedrock "Purchase options" page looked like routine setup right up until the number was $80–99 per hour, not per token. Same product name, two completely different pricing models sitting one click apart on the same page.

Aug 20 — Ansible and self-hosted monitoring

  • First Ansible session, on the (disposable) bastion rather than the real server — ad-hoc commands, then a real playbook
  • Watched idempotency happen firsthand: the same yum install htop command reported changed the first run, "already installed" the second
  • Wrote a first two-task playbook, learned to read a PLAY RECAP line before anything else
  • Self-hosted Uptime Kuma via Docker on the bastion — one docker run, and the blog + honeypot now have a real monitoring dashboard
  • Worked out why that dashboard should never point at a company's Fortinet without sign-off — and why "WAN access already disabled" quietly answers the question anyway

The moment that actually taught something: the instinct to monitor company infrastructure from a personal AWS account came from a good place — but the same honeypot logic learned on day one applies in reverse: repeated probes from an unfamiliar IP are exactly the pattern a real security team is watching for.

Kubernetes, day one: pods, deployments, and why self-healing actually matters

Reading time: 4 minutes

Second infra-learning session this week, this time on Kubernetes — picked up right where Ansible left off, since K8s solves the same kind of problem ("keep this thing in the state I described") but for containers instead of servers.

Getting a cluster running

No separate install needed — Docker Desktop ships its own single-node Kubernetes cluster, off by default. Settings → Kubernetes → Enable Kubernetes → Apply & Restart, a few minutes, and kubectl get nodes showed one Ready control-plane node.

A Pod alone proves nothing

Started with the ad-hoc equivalent of docker run:

kubectl run web --image=nginx --port=80

Unlike Docker's -p, K8s doesn't expose a port just because you declared one — kubectl port-forward pod/web 8080:80 was needed to actually load nginx's welcome page at localhost:8080. Then, on purpose, killed it:

kubectl delete pod web
kubectl get pods
# No resources found in default namespace.

Gone. Nobody brought it back — a bare Pod behaves exactly like a bare Docker container: delete it, it's dead.

Deployment: the part that actually self-heals

Same idea, wrapped in a Deployment instead:

kubectl create deployment web --image=nginx --port=80

This time the Pod (web-7fdc5fd5c-cvl8h) wasn't created directly by hand — the Deployment created it, and kept watching it. Deleted that exact Pod and checked again within seconds:

kubectl delete pod web-7fdc5fd5c-cvl8h
kubectl get pods
# web-7fdc5fd5c-7jxwn   1/1   Running   0   4s

A new Pod, 4 seconds old, same ReplicaSet hash (7fdc5fd5c) but a new random suffix. Nobody ran a command — the Deployment noticed the actual count had dropped below the desired count and fixed it on its own. That's the whole idea: declare the end state once, K8s enforces it continuously, not just at the moment you typed the command.

Service: an address that outlives the Pod behind it

One more problem: every time a Pod gets recreated, its name and internal IP change. Anything depending on a specific Pod name breaks on every restart. A Service fixes that with a stable address that doesn't care which Pod is currently behind it:

kubectl expose deployment web --port=80 --target-port=80
kubectl port-forward service/web 8080:80

With that port-forward pointed at the Service instead of a Pod directly, killed the Pod behind it again from a second terminal (kubectl delete pod -l app=web) while the browser tab stayed open. Refreshed localhost:8080 mid-replacement — it still loaded, no interruption visible from the outside, even though the Pod serving that request had been swapped out underneath.

What I actually learned

  • Docker Desktop already has a Kubernetes cluster available — no minikube/kind install needed to get started.
  • A bare Pod is just a managed container; it has no more resilience than docker run on its own.
  • Deployment is what actually gives you self-healing — it watches the desired replica count and recreates Pods that disappear, without being told to.
  • Service exists specifically because Pod identity is disposable — it's the stable address other things should depend on, never the Pod name itself.
  • Pod → Deployment → Service is the load-bearing trio almost everything else in Kubernetes builds on top of.

Next: taking this past Docker Desktop's single local node — probably k3s on the same free-tier EC2 box already doing Ansible duty, since a full kubeadm cluster wants more RAM than a t3.micro has to spare.

Ansible, day one: idempotency, ad-hoc commands, and a first playbook

Reading time: 5 minutes

First real session with Ansible today, on the bastion host rather than the live server — reused it as a disposable lab instead of touching the box actually running the blog and honeypot.

What it actually is

Instead of SSHing in and typing commands by hand, you describe what state a server should be in, and Ansible does the SSHing and the deciding. It's agentless — nothing installs on the target, it only needs SSH access, which is why this worked immediately on a bastion I hadn't touched since setting it up. An inventory is the list of managed servers; a module is a prebuilt unit of work (yum, copy, shell, and thousands more); a playbook is a YAML file that strings several tasks together.

First command: ad-hoc, no file needed

ansible all -i inventory-lab.ini -m shell -a 'uptime'

-m picks the module, -a passes it an argument, -i points at the inventory file. One catch: shell always reports changed, even for a read-only command like this — it has no way to know whether anything actually changed, which turns out to matter a lot for the next part.

Idempotency, seen firsthand

Ran the exact same command twice, installing htop with the yum module:

ansible all -i inventory-lab.ini -m yum -a 'name=htop state=present' --become

First run: changed, with "installed": ["htop"] in the output — it wasn't there, so Ansible installed it. Second run, same exact command: changed: false, "already installed" — it checked first, saw nothing to do, and did nothing. That's the whole idea of idempotent modules: a playbook can be re-run any number of times without side effects, unlike a bash script that blindly re-runs mkdir and errors out the second time around.

A first playbook

Two tasks, YAML, spaces not tabs:

---
- name: My first playbook
  hosts: lab_servers
  become: true
  tasks:
    - name: Install htop
      yum:
        name: htop
        state: present
- name: Create a test file in /tmp
  copy:
    content: "Hello from Ansible!\n"
    dest: /tmp/hello.txt</code></pre>

Run with ansible-playbook instead of plain ansible:

ansible-playbook -i inventory-lab.ini my-first-playbook.yml

Output ends in a one-line PLAY RECAP that's worth reading before anything else: ok=3 changed=1 unreachable=0 failed=0 skipped=0. ok means the task succeeded and nothing needed to change; changed means it succeeded and actually did something; failed stops the play right there. Didn't take the recap's word for it either — ran cat /tmp/hello.txt on the box afterward to check the file was really there.

What I actually learned

  • Agentless means the target needs nothing but SSH access — the bastion I built yesterday was ready to be a lab with zero setup.
  • shell reports changed unconditionally; purpose-built modules like yum actually check state first, which is the whole point of idempotency.
  • Read the PLAY RECAP line first — it's the entire playbook's result in one line.
  • Trust but verify: Ansible said the file was created, so I checked it myself anyway.

Next: variables, handlers, loops, and roles — in that order, matching how the Ansible docs introduce them.

Docker, Index, and a very expensive lesson in reading pricing tables

Reading time: 3 minutes

Quiet learning day, mostly — until AWS almost let me subscribe to something that costs $57,600 a month.

Dockerizing Bludit, locally

Containerized nginx + PHP-FPM + a fresh Bludit clone with Docker Compose — but on my own machine, not the live EC2, after last week's MySQL RAM incident taught me not to experiment on the real box. Hit one snag (missing PHP gd extension, fixed with a small custom Dockerfile), then a useful realization: Docker doesn't save disk space. Each container carries its own copies of libraries, so my current bare-metal setup is actually leaner. Docker's value is isolation and reproducibility, not size.

What an Index actually buys you

Built 300,000 fake rows in a local MySQL container to see indexing in action. Query time looked identical before and after (0.00 sec both times) — but EXPLAIN told the real story: type: ALL (full scan) became type: ref (index lookup). The gap is invisible at this scale and enormous at millions of rows. Lesson: judge an index by the query plan, not the stopwatch.

The $80-an-hour near-miss

Tried wiring up AWS Bedrock to help draft posts. Hit a token quota error, went looking for a fix, and landed on a "Purchase options" page that looked like routine setup. It wasn't — it was Provisioned Throughput pricing, $80–99 per hour, not the per-token rate I'd already checked. One click from a five-figure monthly line item. Backed out, no charge.

Turns out I didn't need Bedrock at all — I already have Claude through my existing subscription, right here in this chat. That's the whole solution for "help me write" or "summarize this log." Bedrock only earns its complexity when something needs to run unattended on a schedule.

What I actually learned

  • Docker isolates; it doesn't shrink disk usage.
  • Check EXPLAIN, not the clock, when judging an index.
  • On AWS Marketplace, two very different products can wear the same name — read every number before clicking Subscribe.
  • The simplest tool is often the one already open in front of you.

A bastion host, Terraform, and a five-hour SSH mystery

Reading time: 5 minutes

My laptop blocked Real server My laptop Bastion Real server

Today's goal: stop SSH to the main server being reachable directly, and route it through a bastion host instead — a small jump box that's the only thing allowed to reach the real server's SSH port.

Provisioning it with Terraform

Wrote a small Terraform config locally: a security group scoped to my own IP, an SSH key pair generated by Terraform itself, and a t3.micro instance wired to both. First apply forgot the key and security group — neither can be added to a running instance afterward, so the fix meant a full replace, which Terraform flagged and handled cleanly.

The SSH mystery

Then hours of SSH timing out despite everything on the AWS side checking out — security group, NACL, route table, all correct, confirmed end-to-end by AWS's own Reachability Analyzer. The instance itself was healthy too: system log showed sshd installed, my key correctly in authorized_keys. EC2 Instance Connect and Session Manager both failed for unrelated reasons (missing agent, missing IAM role). The real cause: my home ISP's route to us-east-1. Switched to phone data and it connected instantly. Added a security-group rule for that IP and moved on.

Locking the real server down

Edited the original server's security group so port 22 only accepts traffic from the bastion's private IP. Direct SSH from my machine now times out, exactly as intended — the only way in is through the bastion, one hop or via a single ProxyCommand so the real key never touches the jump box:

ssh -i EC2.pem -o "ProxyCommand=ssh -i bastion-key.pem -W %h:%p ec2-user@<bastion-ip>" ubuntu@<private-ip>

What I actually learned

A correct security group doesn't guarantee a connection — routing, NACLs, and ISP path are separate failure domains, and AWS gives tools to check each one without the SSH access you're debugging. "Times out from one network, works from another" is a real, common VN-to-us-east-1 issue, not a config bug. And a bastion is only as good as the rule that closes the real door behind it.