WordPress Management
Editing the real shop and keeping every honeypot in lockstep: the WordPress Studio and WordPress Update dashboard pages
A honeypot is only convincing while its decoys look exactly like the real shop. Two dashboard pages keep that true without ever leaving the console: WordPress Studio edits the real WordPress + WooCommerce storefront and fans safe changes out to every pool, and WordPress Update upgrades WordPress core and WooCommerce across production and every pool behind a maintenance page. Both reuse the engine's existing content replication and pool plumbing rather than inventing a second copy of it.
Both are strictly defensive: they operate on the defender's own instances to keep the decoys believable. Neither reaches out to an adversary, tracks back, or performs any offensive action.
Why Manage From the Dashboard
The periodic content-sync service already keeps pools current on a timer, but an operator editing the live catalogue wants their change reflected now — before the next sync tick — so a diverted adversary never sees a stale price or a missing product that betrays the decoy. Likewise, a WordPress or WooCommerce release has to land on production and every pool together, or a version mismatch becomes a honeypot tell. The Studio and Update pages make both one-click, operator-driven actions with a progress bar, instead of a shell session against each instance.
WordPress Studio
The Studio page embeds the real wp-admin panel — pointed at production — directly
in the dashboard. The operator edits pages, posts, WooCommerce products and settings exactly as they would in
WordPress. On demand, pressing “Propagate to honeypot pools” mirrors the change into every
ready pool and shows a progress bar fanning out one pool at a time.
Propagation does not re-implement the “what is safe to copy” policy. It runs the honeypot content-sync
service's own replication script (replicate_content_to_honeypot.sh) in its single-pool
--once N mode, so a manual push from the Studio and the periodic background sync apply the
exact same allow-list and denylist — there is no second policy to drift out of step. Target pools are
the ones the router reports ready (falling back to the compose-declared static pools 1–3 when
Redis cannot be reached), so propagation never silently does nothing.
Staying Pinned to Production
A live editing session must never be diverted into a honeypot — otherwise the operator would be editing a
throwaway decoy. The embedded browser presents two headers so the edge force-routes it to production:
X-Studio-Backend: production and X-Internal-Test-Auth carrying the edge's
INTERNAL_TEST_SECRET.
The studio passthrough is fail-closed: nginx honours it only when the secret header is
present and matches the server-side secret (see router_rules.is_studio_passthrough and
the early return in nginx.conf). No external client that lacks the secret can use these headers
to reach production. If no secret is configured, the admin panel is routed normally — and may be diverted —
which the page warns about rather than silently sending an empty secret.
What Propagates, What Never Does
The split is deliberate and security-driven: the decoys must match production's content and presentation, but a compromised honeypot must leak nothing of value. So content and presentational settings propagate; identities, commerce records and secrets never do — they stay only in production.
| Propagates to every pool |
|---|
| Pages and posts |
| WooCommerce products, variations and prices |
| Product categories, tags and attributes |
| Media / attachment metadata |
| Safe site & store settings (address, currency, tax & catalog display, theme customizer) |
| Never leaves production |
|---|
| User accounts and passwords |
| Orders, customers and carts |
| Payment-gateway & SMTP settings (API keys, secrets, tokens) |
| WooCommerce API keys and licence keys |
Any option whose name matches api / key / secret / token / salt / password / … |
The denylist is enforced in the replication script itself (its SETTINGS_ALLOW allow-list and
SETTINGS_DENY_RE pattern, plus scoped content dumps), so the guarantee holds for both the manual
and the periodic sync.
WordPress Updates
The Update page checks whether a newer WordPress core or WooCommerce release is available and, on the operator's confirmation, upgrades production and every static pool together. Pressing “Check for updates” runs a read-only version probe; if an update is offered, “Update all instances” runs the staged plan below behind a site-wide maintenance page, with a weighted progress bar and per-stage status.
The check is scripts/update_wordpress.sh --check, run in a one-shot wp-cli seed
service; it prints one JSON object that core/wp_update.py parses (versions here are illustrative):
docker compose run --rm --no-deps production_db_seed sh /update_wordpress.sh --check
{"ok":true,"wordpress":{"installed":"6.6.2","latest":"6.7","update_available":true},
"woocommerce":{"installed":"9.3.3","latest":"9.3.3","update_available":false},"errors":""}
Update Stages
The update follows an update-production-then-re-sync model, so core and WooCommerce are downloaded once and every instance ends up byte-identical:
- Maintenance on. A Redis flag is set; the edge mirrors it into its shared dict and serves a
503 maintenance page to all traffic (
lua/maintenance.lua). - Update production. The newest core and WooCommerce files are downloaded into the production
install and its database is migrated (
update_wordpress.sh --apply). - Re-sync files to pools.
init_setupre-runs; its rsync mirrors the updated production files into every pool volume. - Migrate each static pool's database. Pools 1–3 run the database migration only — their files
are already in place (
update_wordpress.sh --apply-db-only). - Maintenance off. The flag is cleared and the shop returns.
Only the static pools 1–3 have a dedicated seed service and are migrated in place here. Dynamic pools (built on demand by the pool manager) rsync production's now-updated files and clone a freshly-migrated database at build time, so they pick up the new version automatically the next time they are built — rather than being migrated live.
Downtime & Self-Recovery
While the update runs, every visitor — production and honeypot alike — sees a scheduled-maintenance page instead of the store. Expect a few minutes of downtime for the download, file re-sync and four database migrations. The maintenance flag is TTL-guarded in Redis (a 30-minute fail-safe cap), so if the dashboard dies mid-update the flag expires and the site self-recovers rather than being stranded offline. The page also waits out the edge's 5-second mirror interval after turning maintenance on, so no request slips through to a half-upgraded store.
Preserving the Database Router
On the database-proxy layer, wp-content/db.php is a drop-in that
binds each request to the production or honeypot database. A core update must not drop it, or the single-eshop /
two-database topology silently collapses back to one database.
WordPress's own updater already skips wp-content when copying new core files, but the update
script makes it a guarantee independent of WordPress internals: it snapshots db.php before touching
any files and restores it byte-for-byte afterwards if the update removed or changed it. The production/honeypot
database routing is always preserved across an upgrade.