On October 28, during QA on the development copy of this site, one of my QA agents was working through a list of scanner-style paths. These are the files attackers go looking for, like a stray .env file, the .git folder and backup copies of the WordPress config, and the point was to confirm the host refused every one of them. The host’s firewall concluded that my Mac was the attacker. Within seconds my IP was graylisted, every web request came back as a “One moment, please” challenge page, and SSH was blocked as well, so the deploy tooling couldn’t reach the server either. Headless browsers fail that challenge and only a person passing it in a real browser clears it, so no agent could get me back in, and I was locked out of my own site for more than fifteen minutes.
The tripwire
Every QA tool that touches the site now checks every response for that challenge, whether it shows up as the firewall’s server header, the “One moment” page title or the challenge marker in the page body. At the first sign the run stops and sends nothing more. Scanner paths are banned from QA briefs for this host, agents space their requests at least a quarter of a second apart, and only one agent works against the site at a time.
The deploy script
The copy of the site on my own machine is the master, and one re-runnable script, which I built with agents in the last week of October, moves it to the host.
A pinned host key. The script will only talk to a server whose key matches a fingerprint I read off the host’s own control-panel terminal and saved ahead of time, so nothing is trusted on first connection.
Secrets off the command line. Passwords travel inside the SSH stream and never appear in a command. Testing showed that the WordPress command-line tool echoes a piped password back twice on its output, and the agent found that with a dummy value before the real run, so no real secret was ever printed and those calls now discard that output.
A snapshot before every change. Before every theme push, content sync and rollback, the script dumps the database and archives the theme, must-use plugins, uploads and server config on the host. It keeps the last twenty and never deletes the one being restored.
A dry run. The sync takes a dry-run flag that plans every change without writing anything. The bundle it sends is checksummed before upload and checked again on the host before it’s applied, and the sync refuses to run against database tables that can’t roll back a failed write.
The verify gate
After a sync, verify asks the host which bundle it last applied and checks against that bundle, rather than whatever I exported most recently. Every page, redirect and setting has to match byte for byte in the database, every page has to render the same as it does on my machine, the development copy has to stay hidden from search engines on every kind of file, and a set of security checks has to pass, down to how many days the certificate has left, and a single failure makes it exit with an error. On the first real run it caught a setting the sync had recorded as written that WordPress had quietly skipped, and the sync now writes those values directly, re-checks them and rolls itself back if they still differ. When the development site went up, verify ran 490 checks and all of them passed.
For launch day, verify can also ask the new server for the real domain directly before DNS moves, and check the canonical address, the search-engine settings and the www redirect while nobody else can see the result.
The rollback
Rolling back is one command with a snapshot’s timestamp. It makes me type the timestamp again to confirm, takes a fresh snapshot of the current state first, and when it finishes it reminds me to rotate passwords, because an old database brings its old passwords back with it, and then to run verify.
Where it stands
The development copy runs on this tooling today, and its most recent verify passed with one warning. Launch is waiting on one decision of mine about how the development and live names share a certificate.