Skip to content
TutorialDevOps

The Exposed API Claude AI Found in Its First Hour

D

Dinesh Wijethunga

August 18, 2026 6 min readIntermediate
ShareX / TwitterLinkedIn
The Exposed API Claude AI Found in Its First Hour

Part 2 of a 5-part series on using Claude AI to run, secure, and ship a real production server. In Part 1 we connected Claude safely and ran a read-only audit. It found something alarming. Now we fix it.

The Exposed API Claude Found in Its First Hour (Part 2)

The audit from Part 1 ranked its top risks, and number three stopped me cold: a Python FastAPI service — the backend for a trading bot — listening on 0.0.0.0:8100, directly on the public internet, with no TLS and no nginx in front of it. Everything else on my server sat safely behind a reverse proxy. This one was naked.

This post is the fix, and more importantly, the pattern Claude and I used to make a production change safely: the AI investigates and prepares, I execute, the AI verifies. It's the workflow I now trust for anything that matters.

Quick Lesson: 0.0.0.0 vs 127.0.0.1

If you remember one thing from this series, make it this:

  • 127.0.0.1 (loopback) — only reachable from the server itself. The internet can't touch it.
  • 0.0.0.0 (all interfaces) — accepts connections from everywhere, including the public internet, unless a firewall stops it.

Tutorials default to 0.0.0.0 because it "just works." That convenience is exactly how services end up accidentally public. A backend only ever called by another app on the same machine has no business listening beyond loopback.

Claude's Move: Investigate Before Touching

Here's what impressed me. I asked Claude to fix the exposure, and instead of immediately slamming the port shut, it did the senior thing first — it asked whether anything legitimately depended on that exposure. Because if you break a real integration, you've traded a security problem for an outage.

Claude ran a structured, read-only investigation and reported back:

CheckWhat Claude found
nginx configs referencing :8100None — nothing proxies to it
Cron jobs / scheduled tasksNothing related
The only consumer's source codeA dashboard that expects the API at 127.0.0.1:8100 — loopback, server-side
How the service startsA systemd unit with the bind address hardcoded
Live reachability testA curl to the public IP returned a live response — confirming it really was open

The conclusion wrote itself: nothing needed the public binding. The only consumer already expected loopback. The 0.0.0.0 was an oversight in one line of a systemd file — not a design decision. Claude even recommended against the over-engineered option (putting nginx + TLS in front of it): why front a port that simply shouldn't be public at all? Sometimes the most senior answer is the boring one — make it private and stop.

The Human-in-the-Loop Wall (And Why It's a Good Thing)

When it came time to actually apply the fix, Claude hit a wall — and this is the best part of the story. Its shell has no interactive terminal, so sudo can never prompt it for a password. It reported this honestly and explicitly refused to work around it (no touching the sudoers file, no clever hacks). That refusal is exactly what earned my trust.

So we turned the wall into the workflow. This is the pattern I now use for every AI-assisted production change:

  1. Claude writes a complete fix script — with a timestamped backup, a diff checkpoint that aborts if the change looks wrong, the fix itself, and full verification.
  2. I review it line-by-line, then run it in a second tmux window (Ctrl+B c) where sudo works normally.
  3. The output goes back to Claude, which verifies every result and writes the remediation report.

Here's the shape of what it produced — study the safety pattern, not just the commands:

UNIT=/etc/systemd/system/crypto-bot-api.service
BACKUP="${UNIT}.bak.$(date +%Y%m%d%H%M%S)"

# 1. Backup first — every change must be reversible
sudo cp -v "$UNIT" "$BACKUP"

# 2. Surgical edit: ONLY the bind flag changes
sudo sed -i 's/--host 0\.0\.0\.0/--host 127.0.0.1/' "$UNIT"

# 3. Abort checkpoint: if nothing changed, STOP
diff -u "$BACKUP" "$UNIT"

# 4. Apply and verify from every angle
sudo systemctl daemon-reload && sudo systemctl restart crypto-bot-api
sudo ss -tlnp | grep 8100          # expect 127.0.0.1 only now
curl http://127.0.0.1:8100/        # local consumer still works

# 5. Defense in depth — firewall the port in case the bind ever regresses
sudo ufw deny 8100/tcp

Verify Like You Mean It — From Outside

A fix isn't done when a command exits cleanly. It's done when you've proven the new behaviour from every angle. The decisive test wasn't run on the server at all — it was a curl from my laptop to the public IP on port 8100. Before: a live response. After: connection timed out. That external timeout is the ground truth that the hole is closed. Checking only from inside the server can fool you.

Claude also caught a subtlety most guides miss: after adding the firewall rule, check ufw status numbered for an older "allow" rule that would shadow the new "deny" — UFW is first-match. There was none, so the deny stands clean on both IPv4 and IPv6.

The Human-Only Cleanup

One thing I did not delegate: rotating the API key. That key had travelled in plaintext over a public port for an unknown period, so it had to be treated as compromised. Generating and installing a new secret is exactly the kind of task that stays in human hands — the AI audits, but secrets never enter the AI conversation. That line stays bright.

Key Takeaways

  • sudo ss -tlnp takes ten seconds and shows exactly what your server offers the world.
  • Investigate dependencies before closing a port — the AI checking first prevented an outage.
  • Fix at the source (rebind) and add a second layer (firewall). Layers, not either/or.
  • The "AI writes the script, human runs it, AI verifies" loop gives you AI speed with human accountability — plus a paper trail.

Risk #3 from the audit: closed, verified, documented. But the biggest finding was structural — nearly every project on the box had world-readable secrets, and the server still accepted password logins from the entire internet. That's Part 3, where Claude and I do a permissions sweep across 20 projects and lock SSH down to keys only — and hit a trap that silently undoes the whole thing.

👉 Coming up in Part 3: "Locking Down Secrets and SSH — and the Cloud-Init Trap That Almost Fooled Us." Would you let an AI agent close a port on your production server? What guardrail would you insist on?

Frequently Asked Questions

What is the difference between 0.0.0.0 and 127.0.0.1?
127.0.0.1 (loopback) means a service is only reachable from the server itself — the internet cannot connect to it. 0.0.0.0 means the service accepts connections on every network interface, including the public one. A backend only used by other apps on the same machine should bind to 127.0.0.1, not 0.0.0.0.
How do I check what ports are open on my Linux server?
Run "sudo ss -tlnp". It lists every listening port and the process behind it. Look for anything bound to 0.0.0.0 that should only be internal — that's a service exposed to the internet. It takes ten seconds and often reveals something you forgot was public.
How do I safely close an exposed port without breaking my app?
First investigate whether anything legitimately uses it — check nginx configs, cron jobs, and the consuming app's source. If nothing needs public access, rebind the service to 127.0.0.1 at its source (for example, in its systemd file), then add a firewall deny rule as a second layer. Verify from an external machine that the port now times out.
Can an AI agent make production changes safely?
Yes, using a human-in-the-loop pattern: the AI writes a fix script with backups and verification, you review and run it in your own terminal, and the AI verifies the output. This gives you AI speed with human accountability, and produces a documented paper trail of exactly what changed.
D
Dinesh Wijethunga

Senior Full Stack Developer · Building SaaS products & teaching Laravel/React · 10+ years experience · Founder of Orion360 · Based in Dubai, UAE.

Was this post helpful?

Add a comment

Comments

Guest comments are held for moderation.

You might also like

Deploy Laravel to a VPS with Laravel Forge: Complete Walkthrough
TutorialIntermediateDevOps

Deploy Laravel to a VPS with Laravel Forge: Complete Walkthrough

Laravel Forge isn't a host — it turns any VPS into a managed Laravel server. Full walkthrough: provisioning, GitHub push-to-deploy, the deploy script, one-click SSL, queue workers, the scheduler, and an honest look at zero-downtime options.

D
Dinesh Wijethunga
5 months ago
6m