Skip to content
TutorialDevOps

Locking Down Secrets and SSH with AI (and the Cloud-Init Trap)

D

Dinesh Wijethunga

August 20, 2026Reviewed Jul 12, 2026 6 min readIntermediate
ShareX / TwitterLinkedIn
Locking Down Secrets and SSH with AI (and the Cloud-Init Trap)

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 .env to 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 .env files are the quiet #1 risk on a multi-project box. Fix to 640, 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?

Frequently Asked Questions

What file permissions should a Laravel .env file have?
A .env file should be mode 640, owned by the user your web server runs as. This means the owner can read and write, the group can read, and no one else has any access. The common insecure default is 644, which lets every user on the system read your database passwords and API keys.
How do I disable password login for SSH on Ubuntu?
Set "PasswordAuthentication no" in /etc/ssh/sshd_config, but critically, also check /etc/ssh/sshd_config.d/ for drop-in files that override it. On Ubuntu, a cloud-init file often sets it back to "yes". Install and test your SSH key first, then disable passwords, validate with "sshd -t", and reload SSH.
Why is my SSH PasswordAuthentication setting being ignored?
SSH reads its Include directive near the top of the config, so a drop-in file in /etc/ssh/sshd_config.d/ can override your main sshd_config. On Ubuntu, 50-cloud-init.conf commonly re-enables password authentication. Always grep the entire /etc/ssh/ directory, not just the main file, to see what's actually in effect.
How do I safely switch SSH to key-only without locking myself out?
Do it in order: create a key on your computer, install it on the server with ssh-copy-id, then prove key login works before changing anything. Only then disable password authentication, and keep your current session open as a lifeline while you test a fresh connection from another terminal.
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