Skip to content

ADR-0017: Security & performance toolkit

Status: Proposed Date: 2026-05-31 Deciders: Ramón Kamibayashi + Claude Opus 4.7 Related: ADR-0010, ADR-0013, ADR-0014, ADR-0015, ADR-0016

Story: WP-Pulse should give the operator proactive signals on security posture + page performance. Today these are reactive ("customer says it's slow", "WPScan emails me"). Bring them into WP-Pulse + add the easy buttons to act.


Decision

8 features, 2 sub-agents in parallel (security / performance), no shared file ownership.

H — Security

# Feature UX
H1 File integrity check Daily cron diffs WP core files in worktree vs canonical SHA256 from api.wordpress.org/core/checksums/1.0/?version=X.Y.Z&locale=en_US. Any mismatch → flag site as tampered + email/Telegram alert. UI: per-site "Integrity" status chip + drilldown listing modified core files.
H2 Login attempts dashboard Daily cron tails the prod error_log (or a dedicated auth-fail plugin) for failed wp-login.php POSTs. Surfaces in a table: IP, attempts last 24h, last_seen. Inline "Block this IP" → adds rule to .htaccess (preview-first).
H3 2FA enforcement audit Cross-checks usermeta for the 2FA plugins commonly installed (WP 2FA, Two Factor, Wordfence). Lists admin users without 2FA enabled. Bulk "Enable 2FA prompt on next login" via a small mu-plugin queue.
H4 Backup verification drill Monthly cron: pick the latest site_versions row for each site → spin ephemeral stack (F4 infra) → smoke test "homepage returns 200 + admin/index.php 200 + no fatals in error_log" → record outcome in backup_drills table → alert if fail.

Data model (alembic 023)

CREATE TABLE site_integrity_checks (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  site_id uuid REFERENCES sites(id) ON DELETE CASCADE,
  checked_at timestamptz DEFAULT now(),
  status text NOT NULL,         -- "clean|tampered|error"
  tampered_files jsonb,         -- ["wp-includes/load.php", ...]
  wp_version_checked text
);
CREATE INDEX ON site_integrity_checks (site_id, checked_at DESC);

CREATE TABLE site_login_failures (
  site_id uuid REFERENCES sites(id) ON DELETE CASCADE,
  ip text NOT NULL,
  attempts_24h int NOT NULL,
  first_seen timestamptz,
  last_seen timestamptz,
  PRIMARY KEY (site_id, ip)
);

CREATE TABLE backup_drills (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  site_id uuid REFERENCES sites(id) ON DELETE CASCADE,
  version_id uuid REFERENCES site_versions(id),
  drilled_at timestamptz DEFAULT now(),
  status text NOT NULL,         -- "pass|fail|error"
  details jsonb
);

API

  • GET /api/v1/sites/{slug}/integrity → latest check.
  • POST /api/v1/sites/{slug}/integrity/recheck → triggers immediate check.
  • GET /api/v1/sites/{slug}/login-failures → IP table.
  • POST /api/v1/sites/{slug}/login-failures/block body {ip} → adds .htaccess Deny from rule (preview-first).
  • GET /api/v1/sites/{slug}/2fa-audit → admin users + 2FA status.
  • POST /api/v1/sites/{slug}/2fa-enforce body {user_ids} → enqueues nudge.
  • GET /api/v1/sites/{slug}/backup-drills → list.

UI

  • /sites/[slug]/security (new tab) with 4 sections (cards per feature).
  • New components: FileIntegrityCard.tsx, LoginFailuresTable.tsx, TwoFAAuditCard.tsx, BackupDrillsCard.tsx.
  • Fleet-level /fleet?tab=security summary tab with one row per site, worst-status-first.

Cron jobs

  • file_integrity_check.py — daily 02:00, walks worktree (wp-includes + wp-admin paths only), SHA256-diffs against canonical.
  • login_failures_sync.py — every hour, parses prod error_log tail (agent exposes log via a new tail_log command) for wp-login.php 200/401 POSTs.
  • backup_drills_run.py — weekly Sunday 03:00. Picks one random site, runs the drill.

I — Performance

# Feature UX
I1 Object cache toggle One-button install of Redis Object Cache plugin + auto-config WP_REDIS_HOST. Detects whether the customer host has Redis available; if not, surfaces "Contact host".
I2 Page cache config helper Detects installed cache plugins (WP Rocket, W3TC, LiteSpeed, etc). Exposes Purge buttons from WP-Pulse UI. For configurable plugins, surfaces the most-used 5 settings.
I3 CDN config helper Cloudflare Page Rules helper: detects whether site is behind CF, then auto-suggests rules for /wp-content/uploads/, /wp-admin/, /wp-content/cache/. Operator clicks "Apply" → uses CF API (token already in env) to push.
I4 Lighthouse on demand "Score this site" button → spawns lighthouse-ci container against prod URL (or preview if checkbox selected) → returns 4 scores + waterfall summary. Compares to baseline stored in site_lighthouse_runs table.

Data model (alembic 024)

CREATE TABLE site_lighthouse_runs (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  site_id uuid REFERENCES sites(id) ON DELETE CASCADE,
  ran_at timestamptz DEFAULT now(),
  target text NOT NULL,         -- "prod|preview"
  url text NOT NULL,
  scores jsonb NOT NULL,        -- {performance, accessibility, best_practices, seo}
  audits jsonb,                  -- selected high-impact audit results
  duration_ms int
);
CREATE INDEX ON site_lighthouse_runs (site_id, ran_at DESC);

API

  • POST /api/v1/sites/{slug}/perf/object-cache/install — install + activate Redis Object Cache plugin (preview-first via plugin manager from ADR-0014).
  • GET /api/v1/sites/{slug}/perf/cache-status → which cache plugin is installed + status.
  • POST /api/v1/sites/{slug}/perf/cache-purge → purges via the detected plugin.
  • GET /api/v1/sites/{slug}/perf/cf-status → whether CF, current rules.
  • POST /api/v1/sites/{slug}/perf/cf-page-rule body {pattern, settings} → push via CF API.
  • POST /api/v1/sites/{slug}/perf/lighthouse-run body {target: "prod|preview"} → 202 + job_id.
  • GET /api/v1/sites/{slug}/perf/lighthouse/{job_id} → status + result.

UI

  • /sites/[slug]/performance (new tab) with 4 cards.
  • Components: ObjectCacheCard.tsx, PageCacheCard.tsx, CdnCard.tsx, LighthouseCard.tsx, LighthouseScoreBars.tsx.
  • Fleet-level /fleet?tab=performance showing per-site latest Lighthouse score (color chips).

Lighthouse container

New docker-compose service (host-side, not per-site) running patrickhulce/lhci (or rolling our own). Triggered on demand. Cap 1 concurrent run server-wide; queue if busy.


Migration

Alembic 023 (security tables) and 024 (lighthouse runs). Independent; either can apply first.

Rollout

Two sub-agents in parallel, each owns its sub-section. No shared backend touchpoints beyond the alembic chain.

Open questions

  1. CVE in security tab: F1 already cross-refs Patchstack (ADR-0014). Security tab links to the plugin list filter ?has_vuln=true.
  2. Backup drill cost: ephemeral stacks consume RAM. Cap to 1 drill per night.
  3. CF API token storage: per-site or global? Recommended global — one CF account fronts all the operator's sites in this homelab model. Use existing server_settings.
  4. Lighthouse from inside vs external: running locally hits the same network as the agent → unrealistic latencies. Recommended: use Google PageSpeed Insights API as fallback for "real internet" scores. Run local LHCI for diff-against-baseline.