Part 3 of a 5-part series on using Claude AI to run, secure, and ship a real production server. Part 2 closed an exposed API. Now we tackle the biggest structural risk the audit found — and lock the front door.
Locking Down Secrets and SSH with AI — and the Trap That Almost Fooled Us (Part 3)
The audit's number-one risk wasn't dramatic, but it was the scariest: across nearly 20 projects, the .env files — the ones holding database passwords and API keys — were world-readable. Any process, any local user, any path-traversal bug in the weakest app could read every other client's secrets. This post is how Claude and I fixed all of them at once, then shut off password-based SSH entirely — and hit a trap that silently tried to undo the whole thing.
Why World-Readable .env Files Are a Slow-Motion Disaster
A .env at mode 644 means "owner can write, everyone can read." On a server with one project, that's sloppy. On a server with 20 unrelated client projects sharing a web user, it means a single vulnerability anywhere gives an attacker every tenant's credentials. The fix is simple — 640, owned by the right user — but doing it across 20 live sites without breaking any of them takes care.
Claude's Approach: One Script, Verify Every Step
I asked Claude to write a single script following the same safety pattern from Part 2: back up, change, verify, and — crucially — check every website still works after each change. The clever part it added on its own: a baseline pass that curls every site before touching anything, so a site that was already broken wouldn't be misreported as something the script broke.
# For each project: capture current perms, fix, verify the live site
for project in "${PROJECTS[@]}"; do
# backup-aware: record old mode/owner first
sudo chown www-data:www-data "$project/.env"
sudo chmod 640 "$project/.env"
# then curl the site and compare against the pre-change baseline
done
Claude also caught two things a blanket script would have broken:
-
Next.js apps under pm2 run as a different user than PHP apps — chowning their
.envto the web user would lock out the process that reads it. Claude special-cased those. - One .env shared by two apps (a Laravel API and a Next.js frontend) needed split ownership so both could still read it while dropping public access.
That's the difference between "AI runs a chmod loop" and "AI understands the runtime." It ran across 18 projects plus a world-writable storage directory and a trading-bot secret — every one verified against baseline, zero regressions.
The Bonus: Baseline Checks Found Pre-Existing Problems
The baseline pass earned its keep immediately. Before changing a thing, it revealed three sites that were already broken — two APIs returning 500 errors and one domain that wouldn't connect at all. None caused by the script; all surfaced by it. That's a lovely side effect of doing things carefully: you discover problems you didn't know you had.
Then: Locking SSH to Keys Only
Next, the front door. The server still accepted password logins from the entire internet — meaning it was brute-forceable. The plan: install a personal SSH key, prove it works, then disable passwords. Order matters, because a mistake here locks you out of your own server.
# 1. On my Mac: create a key if I didn't have one
ssh-keygen -t ed25519
# 2. Install it on the server
ssh-copy-id deploy_user@SERVER_IP
# 3. PROVE key auth works BEFORE disabling passwords
ssh -o PasswordAuthentication=no deploy_user@SERVER_IP
Only after that login succeeded did we disable passwords in sshd_config. And this is where the trap sprang.
⚠ The Cloud-Init Trap That Almost Fooled Us
I set PasswordAuthentication no in the main SSH config. Clean. Done, right? Claude insisted on one more check — grepping all the config, including the /etc/ssh/sshd_config.d/ drop-in directory. And there it was:
/etc/ssh/sshd_config:65: PasswordAuthentication no
/etc/ssh/sshd_config.d/50-cloud-init.conf:1: PasswordAuthentication yes ← !!!
A cloud-init drop-in file was setting PasswordAuthentication yes — and here's the killer detail: SSH reads the "Include" directive near the top of the config, so the drop-in file's setting wins over the main file. Without catching this, my "hardening" would have changed nothing. Password auth would have stayed wide open while I believed it was closed. Claude also caught that root login was still enabled and flagged that too.
We fixed both files, validated with sshd -t (test before reload, like nginx -t), reloaded, and then ran the two-sided proof from my laptop:
ssh deploy_user@SERVER_IP 'echo OK' # key works
ssh -o PubkeyAuthentication=no deploy_user@SERVER_IP # password refused
ssh root@SERVER_IP # root refused
Result: key works, password bounces, root bounces. Three for three.
The Deeper Lesson: Verify the Baseline, Not Just Your Changes
This was the second time that day a "should already be hardened" assumption turned out false when Claude actually checked. That's the real discipline this whole experience taught me: verify-don't-assume applies to the starting state, not just to the changes you make. The cloud-init file had been silently overriding intent for who knows how long. An AI that grepped everything instead of trusting the obvious file is what caught it.
Key Takeaways
- World-readable
.envfiles are the quiet #1 risk on a multi-project box. Fix to640, but verify each site still runs after. - Always baseline before a bulk change, so you can tell your breakage apart from pre-existing breakage.
- Install and prove your SSH key before disabling passwords — keep your current session open as a lifeline.
- On Ubuntu, always grep
sshd_config.d/— a drop-in file can silently override your main config.
The server was now genuinely locked down: secrets protected, SSH key-only, root disabled — all verified from outside. With the house secure, it was time to build something: a proper CI/CD pipeline so I could ship code changes with zero downtime and one-command rollbacks. That's Part 4, where Claude becomes a code-review partner and we ship a real deployment pipeline — and hit our first live deploy failure.
👉 Coming up in Part 4: "Building a Zero-Downtime CI/CD Pipeline with an AI Pair." Run sudo grep -r PasswordAuthentication /etc/ssh/ right now — are you sure it says what you think it does?
Originally published at dineshstack.com — read the full version with code samples and updates there.
Top comments (0)