ESC
> PressHaven alpha
Home › Docs ›Engine › WordPress Management

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.

Fail-closed passthrough

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:

  1. 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).
  2. Update production. The newest core and WooCommerce files are downloaded into the production install and its database is migrated (update_wordpress.sh --apply).
  3. Re-sync files to pools. init_setup re-runs; its rsync mirrors the updated production files into every pool volume.
  4. 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).
  5. Maintenance off. The flag is cleared and the shop returns.
Static vs. dynamic pools

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.

Byte-for-byte guarantee

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.