KM Associates of New York

Structural engineering firm in NYC. G&M has worked with them previously. Currently re-engaging for an urgent website security engagement (kmaofny.com was compromised in April 2026).

Project Info

Job Number TBD — not yet assigned
Phase Active — Security incident response
Billable TBD — needs confirmation

Client Contacts

Site Reference

Local Path /Users/edowns/Local Sites/km-associates/app/public/wp-content/themes/km_associates
Local URL https://km-associates.local
Production URL https://kmaofny.com
GitHub TBD — no local .git found in theme
Host / Hosting TBD — need to confirm where kmaofny.com is hosted (Flywheel? other?)
Theme Docs Custom WordPress theme. No CLAUDE.md at theme root. Has acf-json/, custom CPTs: clients, faqs, projects, careers.

Integrations

Service Status Details
Harvest ✅ Active Client: KM Associates (id 4335283) · Project: Web Fixes (id 48048591) · Task: Web Updates (id 4840855) · Billable at studio rate
Todoist (Team) TBD Not set up
Todoist (Dev) TBD Not set up
Basecamp TBD Check if existing KMA project exists
Masterdoc TBD Need to locate or create Masterdoc in Drive
Notion None Not in use for this project
Figma N/A — no design work scoped This is security/cleanup work, not a redesign
Cloudflare TBD Need to confirm if kmaofny.com is on Cloudflare
SendGrid TBD Likely unrelated to current issue
What Where
WP admin — All users https://kmaofny.com/wp-admin/users.php
Wordfence — Login Security (2FA) https://kmaofny.com/wp-admin/admin.php?page=WFLS
Wordfence — All Options / Email Alerts https://kmaofny.com/wp-admin/admin.php?page=WordfenceSecOpt
Wordfence — Scan findings https://kmaofny.com/wp-admin/admin.php?page=WordfenceScan
Wordfence Central dashboard (site view) https://wordfence.com/central/ — site ID a19d21c1-a18d-43ec-b1d0-267aa3be516f
Flywheel site dashboard https://app.getflywheel.com/ (site: kmaofny)
KMA Masterdoc — Client Logins tab https://docs.google.com/spreadsheets/d/1WUmbvNjOPUPwHziIqI1jOj8XnYyF_0TQEjjEzMjTOm0

2FA Status — "Required" vs "Activated" (read this before worrying)

If a user shows "Not activated" in Wordfence Login Security, that does not mean 2FA is optional for them. Two separate concepts:

Since admin + editor are the only roles in use (no authors/contributors/subscribers on this site), all 5 users are covered by the requirement. Current activation state as of 2026-04-23 PM:

User Role Activated?
gmlaunch (Eric) Admin ✅ Paired via 1Password
KM_Associates Admin Pending first login
ewarren (Liz) Admin Pending first login
cquinn (Chris) Admin Pending first login
jwyman (Jessica) Editor Pending first login

Wordfence alert email recipients (set 2026-04-23 PM)

wp_wfconfig.alertEmails = cquinn@kmaofny.com,lwarren@kmaofny.com,support@neighbortech.net. Change via wp-admin → Wordfence → All Options → Email Alert Preferences, or via wp eval 'wfConfig::set("alertEmails", "...");' over SSH.


Current Issue — Security Incident (2026-04-22)

What happened: - kmaofny.com was compromised. Unauthorized pages (including foreign-language content) were added by an attacker. - AECOM (external party) flagged the kmaofny.com domain for malicious activity. - KMA is receiving emails from AECOM, but outbound emails from kmaofny.com are not reaching AECOM. - AECOM has asked KMA to complete a full security audit before they will review again. - Liz is concerned the same thing could be happening with other clients.

What's already underway: - Jessica Wyman is removing the visible unauthorized pages.

What KMA is asking G&M for: - A deeper security audit and cleanup: - Identify and remove any remaining malicious code / backdoors - Password resets and credential rotation - Lock the site down going forward - Time-sensitive — Liz asked for availability and timing.

Recon — 2026-04-22 (what I've pulled so far)

Infrastructure: - Site A record: 151.101.130.159 — that's a Fastly edge IP. Origin is behind Fastly CDN, not directly reachable by IP. - Nameservers: EarthLink (dns1/2/3.earthlink.net) — legacy ISP DNS hosting - MX: arsmtp.com — AppRiver / Zix Email Security (inbound filtering/security layer) - SPF: v=spf1 include:spf.protection.outlook.com -all — outbound via Microsoft 365 / Exchange Online - DMARC: v=DMARC1; p=none; — DMARC published but in monitor-only mode. Not enforcing alignment. This is almost certainly a major contributor to the AECOM delivery issue. - DKIM: not yet checked (need to look up M365 default selectors selector1._domainkey / selector2._domainkey)

Business context (from kmaofny.com): - NYC building-code + zoning consulting, project expediting, special inspections - Clients include Amazon, Apple, JPMC, Columbia, Verizon, Cartier, Whole Foods - 60,000+ applications filed, 111M+ sq ft construction completed - Tagline: "Moving Projects Forward" - Sister company: Crosscheck Inspections

Public site surface: - Homepage + nav look clean — no foreign-language spam visible to crawler - Either Jessica Wyman already removed the visible pages OR the unauthorized pages are at URLs not in the nav (orphaned / sitemap-only)

WordPress login page: - wp-login.php is publicly accessible - No 2FA visible, no apparent rate-limiting - This is a likely attack vector — combined with the compromise, login hardening should be priority one after cleanup

1Password sweep: - Searched all vaults (G&M, Personal, Ventures, Claude Bot) — zero matches for KMA / kmaofny / km associates - Either the creds are stored under a different title, in a shared team vault I can't see, or we don't have them - Blocker: need production access before forensic work can start

Local copy status: - Theme: ~/Local Sites/km-associates/app/public/wp-content/themes/km_associates (custom G&M-built, last touched June 2025) - Has wp-login-lockdown.php in theme functions — some login hardening was built in at theme level - Plugins: ACF Pro + ACFE Pro, FacetWP + Map + Flyout, Gravity Forms, Relevanssi, Yoast, Redirection, Imagify, WP All Import Pro, CPT UI, Admin Columns Pro — full G&M stack, confirms prior build - The Local copy is stale — likely not reflective of current production state

Working theory

Two likely root causes, not mutually exclusive:

  1. WP admin compromise — weak/reused admin password, no 2FA, no rate-limiting on wp-login.php → attacker got in, added foreign-language SEO spam pages (common "Japanese keyword hack" or similar doorway page attack for pharma/gambling SEO).
  2. DMARC not enforcingp=none means spoofed emails claiming to be from @kmaofny.com are not being rejected by receiver policy. If the attacker is also using the domain to send spam, AECOM's gateway is blocking all mail from the domain because the reputation tanked. Without p=quarantine or p=reject, we can't stop that.

The email delivery issue and the page injection are symptoms of the same compromise, not two unrelated problems.

Investigation Checklist (to run today)

Project Status

2026-04-22 — Phase 1 complete, Phase 2 in progress

Phase 1 — Stop the Bleeding: COMPLETE (~20 min). All attacker access vectors neutralized on production: - bot-token REST API application password revoked (never used — good) - bot + admlnlx rogue admins demoted to subscriber - 6 web shells in /www/ quarantined (renamed .quarantine, verified 404 externally) - fileview.php stray shell in simple-301-redirects/assets/css/ quarantined - 3 malicious plugin folders renamed to .quarantine (one_images_user, widget-1776587769, widget-1776587767) - 5 legit admin passwords reset to strong unique values (stored at forensic-workspace/NEW-PASSWORDS-20260422.txt, mode 600) - Salt rotation: wp-cli blocked (wp-config.php root-owned on Flywheel). Requires manual click in Flywheel dashboard → Site → Advanced → Shuffle Secret Keys. Eric to handle. - Full DB (14MB) + wp-content tarball (45MB) snapshotted locally as rollback baseline.

Client communication: - 2:48 PM CDT — Eric emailed Liz a layman's explanation of the compromise + cleanup plan, including a 3-4 hour cleanup estimate at G&M's $225 studio rate, requesting formal approval. - Eric is proceeding with Phase 2 proactively without waiting for written approval (decision made 2026-04-22 afternoon).

Phase 2 — Full Cleanup: IN PROGRESS. Decisions made: - wp-file-manager v8.0.3 (the entry point, CVE-2020-25213) will be REMOVED entirely, not updated. - All 9 premium plugins will be reinstalled from fresh vendor downloads (keeping scope equivalent to current site). - training-videos is a custom G&M plugin (verified by reading plugin header in prod) — no reinstall needed, source is with us. - 17 free plugins will be reinstalled from wp.org via wp plugin install --force.

1Password access finding: - Claude Code is authenticated to 1Password via a service account ("Claude Bot" integration). - That service account only has access to the "Claude Bot" vault, not the G&M vault. - Eric is granting Claude Bot service account read access to the G&M vault so Claude can pull the premium plugin licenses automatically. - Best practice recommendation: Store ALL plugin licenses in 1Password G&M vault going forward. Create a naming convention like Plugin License — [Plugin Name] so Claude can fuzzy-match on future engagements.

Billing: - This is billable at $225/hour studio rate. - Estimated 3-4 hours total for Phases 1 + 2. - Phases 3 (email reputation) + 4 (hardening + portfolio audit) will be separately scoped — likely another 2-3 hours.

Status: Phase 2 active — waiting on Eric for FacetWP license activation + 4 premium plugin ZIPs.

Phase 2 progress (as of mid-afternoon)

Completed: - All 16 free plugins reinstalled fresh from wp.org (wp plugin install --force) - wp-file-manager REMOVED entirely (the entry point, CVE-2020-25213) - simple-add-pages-or-posts REMOVED (closed on wp.org for security) - 2013 google-analytics-for-wordpress empty shell REMOVED - ACF Pro reinstalled via WP-CLI (6.7.0.2 → 6.8.0.1) - Gravity Forms reinstalled via WP-CLI (2.9.28 → 2.10.0) - All rogue DB options deleted - All quarantined malicious plugin folders deleted from disk - All quarantined web shells deleted

Remaining: - FacetWP + Flyout + Map Facet reinstall (blocked: license not activated for kmaofny.com on facetwp.com/account — Eric approving) - Admin Columns Pro, ACF Extended Pro, WP All Import Pro, WPAI ACF Add-On reinstall (blocked: Eric dropping ZIPs in ~/Desktop/kma-premium-plugins/) - Second-pass Wordfence scan - Functional test - Delete bot + admlnlx user records (holding for final evidence preservation)

Plugin count now: 27 (was 31 before cleanup). Site still returning 200.

Key discoveries / new artifacts


Docs

File Description
INCIDENT-REPORT.md Full forensic incident report — client-facing, ready to share with KMA / AECOM / insurance when finalized
REMEDIATION-PLAN.md 4-phase execution plan
DNS-CHANGES.md Running log of every DNS modification made (for client email)
forensic-workspace/ Malicious file evidence, DB snapshot, password files, screenshots
~/Desktop/kma-aecom-email-for-liz.md Forwardable AECOM summary for Liz to send to her AECOM contact

🅿️ Session parked: 2026-05-06 ~3:40 PM CDT (RESUME FROM HERE)

Today's work (2026-05-06)

Wordfence 2FA enforcement — actual root cause found. The 2026-04-28 entry attributed Jessica/Liz/KM_Associates not being prompted to a 10-day "user grace period" introduced by Wordfence 8.1.4. Clearing that on 2026-04-28 helped Chris (he found the panel link manually), but it didn't fix the underlying problem. Jessica logged in again on 2026-04-29 and 2026-05-05 and was never prompted. The actual root cause: Wordfence 8.x renamed the role-required setting key from require_2fa.<role> (legacy, what we wrote in April) to required-2fa-role.<role> (current). The plugin's does_user_role_require_2fa() reads the new key only. Both rows for that key were missing from wp_wfls_settings, so requires_2fa returned false for every user, meaning Wordfence has been silently no-op enforcing 2FA on this site since the 8.1.4 auto-update around 2026-04-25. Confirmed via the plugin's own getter: Controller_Settings::shared()->get_required_2fa_role_activation_time("administrator") returned false.

Fix on prod. Set both required-2fa-role.administrator and required-2fa-role.editor to time() via Controller_Settings::shared()->set(). Verified via reflection on does_user_role_require_2fa() that Jessica now returns requires_2fa=true, in_grace=NULL, required_at=1778097526. Then force-destroyed all WP sessions for jwyman, ewarren, KM_Associates so each will be prompted on their next login. Chris and Eric (already enrolled) are unaffected.

Jessica promoted to administrator. Per Liz's request earlier on the 2FA thread, jwyman (user 11) was set-role from editor → administrator. She'll see Gravity Forms in her sidebar after re-login. The role change combined with the new role-required key now applies, so she'll be prompted to enroll 2FA before reaching the dashboard.

Gravity Forms deliverability issue identified (NOT fixed tonight). Jessica reported on 2026-05-05 that her form-notification email landed in Spam and Liz didn't receive the entry at all. Verified Form #6 ("Request Proposal Short") IS correctly configured: lwarren@kmaofny.com, jessica@wymanprojects.com, info@kmaofny.com, psantantonio@kmaofny.com are all on the recipient list. The form config is fine. Real cause: site mail goes out via Flywheel's local PHP mail() path, not through M365 SMTP. After our 2026-04-23 SPF/DMARC tightening (SPF only authorizes spf.protection.outlook.com, DMARC at p=quarantine), anything the site sends now fails authentication and gets filtered. This is a side effect we caused. Two paths to fix, neither shipped tonight:

Either is ~1-2 hours plus credential handoff. Should be scoped as a separate small engagement with Liz.

Email reply drafted to Jessica. Gmail draft r-529193132455750159 on thread 19dbb3e06b64a760. To: Jessica. CC: Liz + Chris. Two short paragraphs: confirms admin promotion (Gravity Forms in sidebar) and explains the 2FA root cause + that her next login will force enrollment. Per Eric's call mid-session, the spam-mail explanation was dropped from the draft (line was hand-wavy without an actual ticket); deliverability fix should go to Liz as a separate scoping note, not tucked into a reply to Jessica. Awaiting Eric's review/send.

Internal tooling work (NOT KMA-billable)

In the same session, Eric asked me to bulletproof Gmail drafting against three recurring failure modes: tone-profile leaks (em-dashes, AI tells, jargon), Reply-All disrespect (fresh chains instead of inline replies, dropped CCs), and hard line breaks within paragraphs. Plan and implementation captured at ~/.claude/plans/i-use-email-constantly-wild-jellyfish.md. Phases shipped tonight:

34-case test harness at ~/.claude/hooks/test-fixtures/gmail-guards.test.sh, all passing. Latency 30 ms per hook. Wired in both ~/.claude/settings.json and ~/.claude-royal/settings.json. Hook scripts are hardlinked between ~/.claude/hooks/ and ~/.claude-royal/hooks/.

Current enrollment state on prod (replaces the 2026-04-28 table)

User Role 2FA
gmlaunch (Eric) Admin ✅ enrolled
cquinn (Chris) Admin ✅ enrolled
ewarren (Liz) Admin ❌ not yet — sessions cleared, will prompt on next login
KM_Associates Admin ❌ not yet — sessions cleared, will prompt on next login
jwyman (Jessica) Admin (promoted today) ❌ not yet — sessions cleared, will prompt on next login

Gotcha for future sessions (replaces the 2026-04-28 gotcha)

When auditing Wordfence Login Security on a 8.x install, don't trust the legacy require_2fa.<role> keys. They are no longer read. Check required-2fa-role.<role> instead. Quick validation:

ssh <site>+<theme>@ssh.getflywheel.com "cd /www && wp eval 'use WordfenceLS\\Controller_Settings; echo Controller_Settings::shared()->get_required_2fa_role_activation_time(\"administrator\") === false ? \"NOT-SET\" : \"SET\"; echo PHP_EOL;'"

If this returns NOT-SET, 2FA enforcement is silently broken regardless of what the legacy keys or the WFLS UI shows. Set via Controller_Settings::shared()->set("required-2fa-role.<role>", time());. Also worth auditing every other G&M client site running Wordfence 8.x — same migration likely caused the same silent break elsewhere.

Outstanding (not blocking)

Reference for resuming


🅿️ Session parked: 2026-04-28 ~6:15 PM CDT

Today's work (2026-04-28)

Wordfence weekly summary follow-up (Liz). Liz forwarded the standing weekly summary asking whether the highlighted real-looking usernames in the Top 10 Failed Logins list were a problem. Diagnosed: the highlighted names that aren't real KMA accounts (lsanchez, vgerbino, chartel, etc.) are bot wordlists; the only real account they hit was km_associates and Wordfence shut them down via lockout. Drafted a plain-English explanation and sent.

Maintenance pitch drafted for Liz (CC Brooke + Kristin). Pattern E proactive pitch. Now that the cleanup is wrapped, packaging maintenance plan as the natural follow-on. Pitch leads with what was preventable about the original incident, names what's included, and brings the G&M Maintenance Portal in as a client-facing differentiator framed in KMA's terms. Gmail draft r811697463585903205. Awaiting review/send.

Production 2FA enforcement fix. Discovered Wordfence had auto-updated to 8.1.4 since our 2026-04-23 hardening, and the new schema introduced a 2fa-user-grace-period setting defaulting to 10 days. That was overriding our role-required setting and letting unenrolled users (Chris, Jessica) log in without an enrollment prompt. Cleared the value to 0 on production. Forensic record: forensic-workspace/wfls-settings-CHANGE-2026-04-28-PM.md.

Replies on the 2FA setup thread. Chris ack (he enrolled the long way, manually via Wordfence panel). Jessica got a longer reply explaining why the prompt didn't fire (the 10-day grace bug, now fixed) plus an answer on Gravity Forms not being in her Editor sidebar (default WP behavior; Liz still gets all form submissions because notifications route to info@kmaofny.com). Both drafts on thread 19dbb3e06b64a760.

Skills work (outside this repo, listed for cross-reference). Created ~/.claude/skills/maintenance/VALUE-AND-SALES.md (sales companion to the operational SKILL.md), cross-linked maintenance/SKILL.md and gm-portal/SKILL.md, created ~/.claude/project-notes/gm-maintenance/README.md index, saved feedback memory feedback_sales_voice_client_perspective.md (rule: sales emails articulate value in CLIENT terms, not vendor terms).

Current enrollment state on prod

User Role 2FA
gmlaunch (Eric) Admin ✅ enrolled
cquinn (Chris) Admin ✅ enrolled
ewarren (Liz) Admin ❌ not yet
KM_Associates Admin ❌ not yet
jwyman (Jessica) Editor ❌ not yet

Next login by any of the bottom three should now force enrollment thanks to the grace-period fix.

Gotcha for future sessions

When Wordfence updates, audit wp_wfls_settings for new keys with permissive defaults. The 8.x line is migrating from underscored keys (require_2fa.administrator) to dashed keys (require-2fa.administrator); right now both schemas coexist with the dashed ones empty. If a future update flips to reading the dashed schema preferentially, the empty values would silently disable enforcement.

Outstanding (not blocking)


🅿️ Session parked — 2026-04-27 ~12:30 PM CDT

2026-04-27 (cont.) — Wordfence alert tuning

Roberto forwarded a routine "user locked out" alert from 2026-04-25 (a brute-force attempt from an Ethiopian IP that Wordfence blocked correctly — the system worked, nothing for a human to do). That kind of event is constant background noise on any public WordPress site. Tuned the alert flags so the team only gets emailed for events that actually require human action.

Diff applied via SSH + wfConfig::set():

Wordfence key Before After Reason
alertOn_loginLockout 1 0 Brute-force lockouts are constant noise; the lockout itself is the defensive action.
alertOn_block 1 0 Firewall blocks automatically; no human action needed.
alertOn_breachLogin 1 0 Strong unique passwords + 2FA already cover this at credential level.
alertOn_lostPasswdForm 1 0 Surprise default. Password-recovery attempts happen all the time.
alertOn_firstAdminLoginOnly 0 1 Now only fires admin-login alerts the first time a user signs in from a new device.
alertOn_severityLevel 25 (Low+) 100 (Critical only) Scan-finding alerts now fire only for Critical severity (malware-tier), not Low/Medium warnings.

Unchanged (still ON): alertOn_scanIssues, alertOn_adminLogin (paired with firstAdminLoginOnly), alertOn_wordfenceDeactivated, alertOn_wafDeactivated. Already OFF before tuning: alertOn_throttle, alertOn_update, alertOn_nonAdminLogin, alertOn_firstNonAdminLoginOnly. Rate cap alert_maxHourly = 5 left in place.

Recipients unchanged: cquinn@kmaofny.com, lwarren@kmaofny.com, support@neighbortech.net.

What recipients will still get emails for (the things that actually need a human): - Wordfence scan finds malware or critical infection (severity = Critical) - Wordfence plugin gets deactivated - WAF (firewall) gets deactivated - An admin signs in from a brand-new device for the first time

What they will no longer get emails for: - Routine brute-force lockouts (the system handled it) - IP blocks from the firewall - Password-recovery attempts - Logins from already-known devices - Low/Medium scan findings

Forensic record: before/after dumps at forensic-workspace/wordfence-alert-config-before-2026-04-27.txt and wordfence-alert-config-after-2026-04-27.txt.

Skipped: the controlled-lockout live test from the plan. DB-level verification is dispositive (Wordfence reads the flag at email-send time; no cache layer), and the live test would have locked out a clean test IP for 4 hours unnecessarily.


2026-04-27 (cont. 2) — Salesforce DKIM piece officially closed ✅

Final verification arrived ~12:11 PM CDT. Chris forwarded the full Outlook headers from a Salesforce-to-criewaldt@kmaofny.com test (the original failure path: Salesforce → AppRiver → Microsoft 365). Authentication-Results came back all green:

dkim=pass (signature was verified) header.d=kmaofny.com
dmarc=pass action=none header.from=kmaofny.com
compauth=pass reason=100

The compauth=pass reason=100 is the bit. On Friday's flagged message it was compauth=fail reason=605 — that's exactly the code Microsoft uses to render the "can't verify sender" banner. Now it's pass. Banner is gone for good.

AppRiver's own headers (separate from Microsoft's) also confirm independently: X-Note: DKIM: Pass: kmaofny.com, X-Note-DMARC: DMARC: Pass, X-Note-Adkim: DMARC/ADKIM aligned: Header sender domain matches DKIM header domain. Two verifiers agree.

The DKIM-Signature header in the test message used selector s=kma2026, confirming the new key we activated this morning is what's actively signing live mail.

Eric sent a wrap-up note to the client confirming the Salesforce email-authentication piece is officially complete.

Engagement piece status: ✅ Salesforce DKIM email authentication — closed.

Outstanding KMA items remaining: legacy DKIM selector cleanup pending Chris's reply on the older km / kmdnsselector keys (low priority — they've been silently broken for years, no rush), and Liz/Laura re-send of Friday's quarantined invoice batch (Liz's call, no action on our side).


2026-04-27 (cont. 3) — Crosscheck DKIM scoping question (parked, awaiting client decision)

Chris replied on the same DKIM thread (19dcf14c95c230d9, ~1:43 PM CDT) asking whether we manage DNS for crosscheckinspections.com as well — the sister-company domain — because they need a DKIM key on that side too.

Recon (15 min):

Reply drafted to Chris (CC Liz, Roberto) on thread 19dcf14c95c230d9 — Gmail draft r-6513533522790793220. Asks two things:

  1. Add eric@grainandmortar.com as a user on the iwantmyname.com account that holds crosscheckinspections.com so we can publish records.
  2. Approve another 1-2 hours of scope at $225 studio rate to cover the work.

Once both are in place, the workflow mirrors what we just ran for KMA: Chris generates the key in the Crosscheck Salesforce org → sends the CNAMEs → I publish at iwantmyname → verify propagation → he clicks Activate.

Status: Parked. Eric to review the draft in Gmail and send. Then waiting on Chris/Liz for the access + scope decision.

If they say yes, the next session's playbook is identical to (cont. 1) above except DNS host is iwantmyname.com instead of Earthlink. iwantmyname's UI is more modern (no FQDN-double-append gotcha like Earthlink).

If they say no / not now, close the loop with a "no problem, here's a doc Chris can hand to whoever does manage Crosscheck DNS" reply and move on.


Layman's summary of today's work

The Salesforce email-verification fix is essentially done on our side. Chris Riewaldt (KMA's Salesforce admin, who's back from leave) generated the DKIM key in Salesforce this morning and sent us the two CNAME records we needed to publish. I added both at Earthlink, they propagated worldwide within ~15 minutes (way faster than Earthlink's stated "up to one business day"), and verified from public resolvers. We're now waiting on Chris to click Activate in Salesforce, then Jack to send a test email so I can confirm the Outlook "can't verify sender" banner is gone for good.

While I was in the DNS panel I also noticed the two older Salesforce DKIM records that were set up years ago have been silently broken since day one — whoever entered them typed the full domain into Earthlink's Name field, so Earthlink double-appended .kmaofny.com and the records have been parked at a name no mail server will ever query. They've effectively been zone clutter. Folded a question to Chris into the email asking whether those old selectors are deprecated on the Salesforce side; cleanup is on hold pending his answer.

Two emails sent today: the activation green-light to Chris (CC Liz), and a separate reply to Liz confirming Friday's invoice quarantine was the unsigned-DKIM issue and that Laura should re-send the batch after Chris activates.

Today's work (2026-04-27)

Waiting on (in priority order)

  1. ~~Chris Riewaldt — click Activate on both DKIM keys in Salesforce.~~ ✅ Done. DKIM public keys live at the CNAME targets, and the test email's DKIM-Signature header confirms selector kma2026 is signing live mail.
  2. Chris Riewaldt — confirm status of older selectors (km, kmdnsselector) in Salesforce DKIM Keys panel. (Note: Chris already said in his 2026-04-27 16:23 reply that he doesn't see any previous DKIM keys — meaning the old selectors are inactive on the Salesforce side. We could proceed with the broken-DNS cleanup. Holding for explicit Eric green-light before deleting.) - If deprecated → delete the two broken Earthlink CNAMEs (km._domainkey.kmaofny.com.kmaofny.com. and kmdnsselector._domainkey.kmaofny.com.kmaofny.com.) and log in DNS-CHANGES.md. - If still active → re-publish at the correct (relative) path, which would let those signatures actually verify for the first time.
  3. ~~Jack Ryan — send test email from Salesforce post-activation.~~ ✅ Resolved by Chris's own Salesforce → criewaldt@kmaofny.com test (same AppRiver → M365 path Jack would have used). Headers show dkim=pass, dmarc=pass, compauth=pass reason=100. Outlook banner confirmed gone.
  4. Laura Kauffmann (KMA Finance) — re-send Friday's invoice batch after Chris confirms activation. Recommended in today's email to Liz. Liz's call to coordinate; no action on our side.

What I do once Chris replies

Chris says I do
"Both old selectors are deprecated/inactive in Salesforce" Delete km._domainkey.kmaofny.com.kmaofny.com. and kmdnsselector._domainkey.kmaofny.com.kmaofny.com. in Earthlink DNS Manager. Log in DNS-CHANGES.md.
"km (or kmdnsselector) is still active in Salesforce" Add fresh CNAME at the correct relative name (km._domainkey or kmdnsselector._domainkey) pointing to the same Salesforce target. Optionally also delete the broken doubled-name version, or leave it as harmless dead weight.
"Activate clicked, records show as Active in Salesforce" Wait for Jack's test email forward.

What I do once Jack's test email arrives

  1. Read the Authentication-Results header in the forwarded message
  2. Confirm dkim=pass header.d=kmaofny.com, spf=pass, dmarc=pass
  3. Confirm no compauth=fail reason=605 in the headers
  4. Reply on the relevant thread with the confirmation. The Salesforce piece is done.

Open / parked items NOT touched today

Reference for resuming


🅿️ Session parked — 2026-04-24 ~4:00 PM CDT

Layman's summary of today's work (1 hour billable)

Today was project management plus email back-and-forth — no site code changes, no DNS changes. Here's the plain-English version:

What the client reported. Their IT vendor (Roberto at NeighborTech) noticed that an email Jack Ryan sent out through Salesforce was arriving in Outlook inboxes with a yellow "We couldn't verify that this message came from kmaofny.com" banner. Roberto flagged it to me and asked whether our DNS work from yesterday caused it.

What I figured out. Yes — partly. When we tightened their email security yesterday (moved DMARC from "just watching" to "quarantine suspicious mail"), receiving servers like Outlook started actually enforcing the rules. Jack's Salesforce-sent mail fails those rules because Salesforce is not yet set up as an approved sender for kmaofny.com. It's not a bug with our fix; it's a gap Salesforce needs to close on their side. I confirmed the diagnosis by reading the headers of a real flagged email Jack forwarded.

What I sent back. Two emails to Roberto walking him through the fix: a plain-language explanation of the cause, then a second email with click-by-click instructions for whoever runs the Crosscheck Salesforce org to generate a DKIM key and hand me the DNS records to publish. That's the permanent fix.

Where it landed. Roberto replied "we should definitely do the proper fix." Liz confirmed Jack will forward the instructions. Roberto then asked who on the KMA/Crosscheck side actually manages Salesforce — Liz looped in Chris Quinn to run that down (Chris Reiwaldt is out; checking Darryl or Jenna). Ball is in their court to generate the records.

Scope conversation. Flagged to Liz that the original four-hour cleanup estimate is used up and asked for 1-2 more hours to see the Salesforce email piece through. Liz approved in writing. She also asked whether it's risky to wait until Monday if nobody on the Salesforce side is reachable today — I told her no, most of their business email isn't moving over the weekend, so we're fine to pick it up Monday when the right person is back.

Net. One hour of project management and client communication. No production changes. We're parked waiting on the Crosscheck Salesforce admin to generate the DKIM records, at which point I publish them at Earthlink and verify.

Today's work (2026-04-24)

Waiting on

  1. Crosscheck Salesforce admin (Chris Quinn to route — Riewaldt out today, Darryl/Jenna as possible alternates) to run the 6-step DKIM setup in Salesforce and send us the DNS records Salesforce generates. That's the input we need to publish the DKIM CNAMEs at Earthlink. Confirmed with Liz it's fine if this slips to Monday.
  2. ~~Liz's approval on the additional 1-2 hours of scope.~~ ✅ Approved 2026-04-24 3:04 PM CDT. Ready to execute DNS work the moment the Salesforce records arrive.

Game plan: "Let's move forward" on Salesforce email authentication

When they approve the proper fix, this is the sequence. Do NOT start any of it until we have the .eml in hand — the Authentication-Results header will confirm whether SPF, DKIM, or both are failing, and which Salesforce domain is actually sending the mail.

Prereqs to confirm first

  1. Read the .eml's Authentication-Results header. Look for lines like spf=fail smtp.mailfrom=..., dkim=fail header.d=..., dmarc=fail. This tells us exactly what to fix and what the actual sender envelope is.
  2. Confirm with KMA/Crosscheck IT: are they using just Salesforce Sales/Service Cloud, or also Pardot (Account Engagement)? Crosscheck's SPF includes both; KMA's might only need Salesforce core. If unsure, mirror Crosscheck exactly (safer).
  3. Confirm who has Salesforce admin access in the Crosscheck org. DKIM keys are generated Salesforce-side and they publish the DNS record we have to add. Likely Roberto at NeighborTech or someone internal at Crosscheck. Without this, the fix can't complete.

DNS changes (Earthlink portal, control.earthlink.net)

  1. Update SPF on kmaofny.com: - Current: v=spf1 include:spf.protection.outlook.com -all - Target: v=spf1 include:spf.protection.outlook.com include:_spf.salesforce.com -all - Add include:aspmx.pardot.com only if Pardot is confirmed in use. - Watch the 10-DNS-lookup SPF limit. Current has 1 include; target has 2 or 3. Plenty of room.

  2. DKIM for Salesforce on kmaofny.com: - In Salesforce Setup: Email Admin → DKIM Keys → Create New Key - Domain: kmaofny.com, selector: something like sf2026 (Salesforce generates the key and provides the exact CNAME/TXT records to publish) - Publish whatever Salesforce specifies at Earthlink (usually two CNAME records pointing to *._domainkey.sendgrid.net or Salesforce-owned DKIM hosts, depending on the Salesforce edition) - Wait for propagation, then activate the key inside Salesforce (Salesforce has an "Activate" button that won't work until DNS resolves cleanly)

  3. Optional but recommended while we're in the DNS panel: - Add DMARC rua= reporting: v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@kmaofny.com; — requires that mailbox to accept mail. Gives us aggregate reports from receivers (Google, Microsoft, Yahoo) on who's trying to send as @kmaofny.com.

Verification

  1. dig +short TXT kmaofny.com @8.8.8.8 | grep spf — confirm new SPF propagated
  2. dig +short TXT <selector>._domainkey.kmaofny.com @8.8.8.8 — confirm DKIM record published
  3. Activate DKIM key in Salesforce
  4. Have Jack send a test email from Salesforce to an external Gmail or Outlook address you control
  5. View message headers in the received mail. Confirm Authentication-Results: spf=pass dkim=pass dmarc=pass for kmaofny.com
  6. Confirm the Outlook "can't verify sender" banner does NOT appear on the test

What NOT to do

Time estimate: ~30 min of DNS work on our end, plus whatever it takes the Salesforce admin to generate keys and activate. Total elapsed ~4-24 hours depending on propagation and admin availability. Billable at $225.


🅿️ Session parked — 2026-04-23 ~11:45 AM CDT

What's DONE on prod

Emails — STATUS BY DRAFT/SENT (updated 2026-04-23 morning)

Email Status Where
"First ack" (2026-04-22 3:57 PM) ✅ SENT Thread 19db676dd49ad9d8
"Detailed explanation + $225" (2026-04-22 4:48 PM) ✅ SENT Same thread
"You're welcome, proceeding" (2026-04-22 5:15 PM) ✅ SENT Same thread
"1PW heads-up + user trim" (2026-04-22 5:45 PM) ✅ SENT Same thread
Liz 1P share link + login ✅ SENT 10:25 AM Fresh thread, to LWarren@kmaofny.com only
Chris Quinn 1P share link + login ✅ SENT 10:25 AM Fresh thread, to cquinn@kmaofny.com only
Jessica Wyman 1P share link + login ✅ SENT 10:25 AM Fresh thread, to jessica@wymanprojects.com only
Comprehensive cleanup summary + AECOM block ✅ SENT 10:25 AM Existing thread 19db676dd49ad9d8, full CC group
Liz reply — Wordfence notifications ✅ SENT 2026-04-23 PM Reply on thread 19db676dd49ad9d8 asking Liz which email to route WF alerts to
Roberto reply — MFA confirmation ✅ SENT 2026-04-23 PM Reply on thread 19db676dd49ad9d8 confirming MFA strict enforcement
2FA setup instructions (strict) ✅ SENT 2026-04-23 PM Fresh thread to Liz + Chris + Jessica, post-enforcement instructions
Roberto reply — "Can't verify sender" ✅ SENT 2026-04-24 ~1:40 PM CDT Thread 19dc01580e396e51. Diagnosed DMARC/Salesforce-auth gap. Asked for .eml forward so we can read Authentication-Results. Draft ID r-8062856250348610966.
Roberto reply #2 — Salesforce DKIM instructions ✅ SENT 2026-04-24 ~3:45 PM CDT Same thread 19dc01580e396e51. Click-by-click DKIM setup for whoever runs Crosscheck's Salesforce. Draft ID r4850295149448935139. Roberto replied approving the proper fix; Liz confirmed Jack will forward.
Liz — scope/billing ask for +1-2 hours ✅ SENT 2026-04-24 2:35 PM CDT Thread 19db676dd49ad9d8. Msg 19dc0fdb89b908f8. Liz approved same day: "Yes, the additional time is approved." (msg 19dc117e65975d11).
Liz — weekend-risk reassurance ✅ SENT 2026-04-24 3:06 PM CDT Same thread. Msg 19dc1198ad1c5e72. Told Liz Monday pickup is fine; most business email pauses over the weekend. Liz: "Got it — thank you." (msg 19dc11efe7eed5f7).
Jack — acknowledgment of .eml ✅ SENT 2026-04-24 2:31 PM CDT Fresh thread 19dc0ec15a7a4847 ("Unverified Email Notice"). Short "Received. Thank you, Jack." reply to Jack's forwarded .eml.

Pending Eric actions (updated 2026-04-23 PM)

  1. ~~Flywheel dashboard "Shuffle Secret Keys"~~ (dropped per Eric 2026-04-23 PM)
  2. ~~Paste ACF Extended Pro real license key~~ (dropped per Eric 2026-04-23 PM)
  3. ~~Verify DMARC propagation~~ ✅ Verified 2026-04-23 PM — p=quarantine resolving cleanly from Google (8.8.8.8), Cloudflare (1.1.1.1), and Quad9 (9.9.9.9).
  4. ~~Wire up Wordfence alert emails~~ ✅ Done 2026-04-23 PM — Liz replied requesting cquinn@kmaofny.com, lwarren@kmaofny.com, support@neighbortech.net. alertEmails set in wp_wfconfig; verified via SELECT. Final draft in Gmail: r121076157454780519 (proper HTML with <p> tags — send this one). Two stale plain-text drafts must be trashed manually: r-6270618732975746693 and r-3703222724441272529 (API can't delete drafts).
  5. Monitor Liz/Chris/Jessica's first logins — each will be forced to pair TOTP on first access. Reply to inbound questions as they come.

Wordfence scan — COMPLETE (2026-04-23 PM)

Final wp_wfissues state: 3 findings total, all status=ignoreP (permanent-ignore), all severity=75 (Low, not High — earlier README entries mislabeled these): - wp-admin/includes/file.php — WordPress core file modified - wp-admin/includes/upgrade.php — WordPress core file modified - wp-settings.php — WordPress core file modified

All three are legitimate Flywheel platform modifications (documented in commit 38f7691). Already dismissed in DB; won't re-flag on future scans.

Completed 2026-04-23 PM (today's session)

Pending — check back later today

Phase 3 still open (tonight/tomorrow)

Phase 4 progress (2026-04-23 late morning)

✅ 2FA STRICTLY enforced on production (via Wordfence Login Security). No separate plugin — Wordfence's bundled Login Security module handles it. - require_2fa.administrator = true - require_2fa.editor = true (covers jwyman) - grace_period_enabled = false (no grace period — strict enforcement per Eric 2026-04-23) - grace_period = 0 - allow_remember = true (30-day device trust after enrollment) - enable_login_history = true (security monitoring) - Enrolled users: 0 — each user MUST pair a TOTP app on next login before reaching admin. No bypass. - 5 users covered: 4 admins (KM_Associates, gmlaunch, ewarren, cquinn) + 1 editor (jwyman) - Wordfence firewall: learning-mode (normal default; auto-graduates to blocking after ~1 week) - Login brute-force lockout: maxFailures=20, lockoutMins=240 (Wordfence defaults — could be tightened to 5/60 later) - Gmail draft created to Liz + Chris + Jessica with strict-enforcement 2FA setup instructions. Old draft (with grace-period language) superseded — Eric should send the newer one and delete the older. Draft IDs: new r4046085531964448619, old r-6983825105055880573 (delete).

✅ Wordfence free license registered, installed on prod, AND connected to Wordfence Central. - License pulled from G&M Wordfence account (eric@grainandmortar.com, 2FA enabled) via wordfence.com/licenses → "Get a Free License" - API key saved in 1P: KMA Wordfence License Key (free) in G&M vault - Installed via wfConfig::set("apiKey", ...) + keyType=free - ping_api_key returned ok:1 with live dashboard payload - Connected to Wordfence Central — site ID a19d21c1-a18d-43ec-b1d0-267aa3be516f. Now visible in Central dashboard alongside buildwithfoster.com and omahaplayhouse.com (the other two G&M sites still on WF — the rest have migrated to Cloudflare). KMA is a client-only WF install, not G&M-standard. - Note for future G&M Wordfence logins: account has 2FA enabled via TOTP on Eric's authenticator; need the 6-digit code (or recovery code) to log in.

Phase 4 still open

Reference material (for resuming)

First move when resuming tonight

Open ~/.claude/project-notes/km-associates/README.md, scroll to this "Session parked" section. Start with the "Pending Eric actions" list.

Open Questions for Eric

  1. Where is kmaofny.com hosted? (Flywheel, legacy shared host, something else?)
  2. Do we have existing SSH + WP admin credentials in 1Password from the prior engagement?
  3. Is there a Basecamp project for KMA already, or do we need to create one?
  4. Is this billable as T&M security work, or a fixed-scope engagement?
  5. What's the prior G&M engagement history with them? (The theme looks custom-built — did we build it originally?)