# Deployment Runbook — advmfr.net anti-spam

Operator: Bastien (clicks in cPanel). Verifier: Claude (checks each step via browser, logs to CHANGELOG.md).
One step at a time. After each step, say "done" and wait for verification before the next.

cPanel path for all filter steps: **Email → Global Email Filters**
(`mail/filters/userfilters.html`).

> **Platform constraint discovered 2026-08-14**: in a *global* filter, cPanel rejects
> `Redirect to Email` toward any address on a domain the account controls
> ("To prevent recursive email delivery issues…"). So the quarantine cannot be the
> `quarantine@advmfr.net` mailbox. Instead we deliberately use the documented behavior of
> `Deliver to Folder` in global filters: matched mail lands in a folder of the **system
> account** (`sc6mixq8738`). That folder — `Quarantine` — is our central quarantine,
> readable via cPanel → Email Accounts → `sc6mixq8738 (System)` → Check Email.

---

## Step 0 — Create the central Quarantine folder

1. cPanel → **Email Accounts** → filter "System Account" → row `sc6mixq8738` → **Check Email**.
2. In the webmail (Roundcube): **Settings → Folders → Create** a top-level folder named
   exactly `Quarantine`.

## Step 1 — Delete the `X-Spam-Flag: YES` filter

1. Open Global Email Filters.
2. On the `X-Spam-Flag: YES` row, click **Delete**, confirm.

Why: it bounces a failure notice to the spoofed sender of every detected spam (backscatter),
and the Spam Box already sorts those messages.

## Step 2 — Convert `Executables & scripts` to quarantine

1. On the `Executables & scripts` row, click **Edit**.
2. Leave Rules untouched (Body · matches regex · existing pattern).
3. In **Actions**: change `Fail With Message` → **`Deliver to Folder`**.
4. Click **Browse** and select the `Quarantine` folder of the system account
   (path ends in `/mail/.Quarantine`).
5. Click **Save**.

## Step 3 — Convert `EXEC` to quarantine

Same as Step 2, on the `EXEC` row.

## Step 4 — Create the score-10 quarantine filter

1. Global Email Filters → **Create a New Filter**.
2. Filter Name: `10-quarantine-score10`
3. Rules: **`Spam Bar`** · **`contains`** · value: `++++++++++`  ← exactly 10 plus signs
4. Actions: **`Deliver to Folder`** → Browse → `Quarantine` (system account).
5. Click **Create**.

⚠️ Not `Discard Message` yet — that switch happens at Step 8 after 7 clean days.

## Step 5 — Create the whitelist (needs your input)

Blocked on: list of critical sender domains (bank, accountant, key clients, monitoring, o2switch).

1. Create a New Filter. Name: `00-whitelist-critical`
2. Rules, OR-joined (use the **+** button, pick **or**):
   `From` · `contains` · `@critical-domain-1.tld` — one line per domain/sender.
3. Actions: **`Stop Processing Rules`**.
4. Create, then use the **arrows** on the filter list to move it to **position 1** (top).

## Step 6 — Intelligent DMARC (approved 2026-08-18, enterprise-grade, zero third party)

Reports stay fully in-house: rua points at a local mailbox, and a local script
(`dmarc_report.py`, stdlib only, no network egress besides IMAP to the server)
digests the XML into a weekly Markdown report under `dmarc-reports/`.

**6a. Create the report mailbox** (AKE):
cPanel → Email Accounts → Create → `dmarc@advmfr.net`, quota 1024 MB.
Give the password to Bastien (it goes into the local `.env`, never into this folder).

**6b. Publish the record** (AKE):
cPanel → **Zone Editor** → advmfr.net → Manage → edit the `_dmarc` TXT record.
cPanel opens a dedicated DMARC form. Fill exactly ONE field and leave every other
setting on its default value:

| Form field | Value |
|---|---|
| Policy | None (default, leave) |
| Subdomain Policy | None (default, leave  changes only at the reject step) |
| DKIM Mode / SPF Mode | Relaxed (default, leave) |
| Percentage | 100 (default, leave) |
| Generate Failure Reports When / Report Format / Report Interval | defaults, leave |
| **Send Aggregate Mail Reports To** | `mailto:dmarc@advmfr.net` |
| Send Failure Reports To | **empty**  failure reports can contain message content, we do not want them |

Save. The resulting record is `v=DMARC1; p=none; rua=mailto:dmarc@advmfr.net`
(cPanel may omit `pct=100`, which is the default and equivalent).
Same-domain destination → no external verification record needed.

**6c. Weekly digest** (Bastien): copy `.env.example` to `.env`, fill
`DMARC_IMAP_PASS`, then once a week:

    python3 dmarc_report.py

The digest states the alignment percentage and whether the next hardening step
is allowed. `--selftest` runs a bundled sample without any network access.

**6d. Hardening ladder** (driven by the digests, one step per week, each step
logged in CHANGELOG.md):

| Gate | New record value |
|---|---|
| 2 consecutive digests >= 98% aligned | `v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc@advmfr.net` |
| next clean week | `pct=50` |
| next clean week | `pct=100` |
| next clean week | `v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc@advmfr.net` |

Any digest below target or any false-positive report pauses the ladder (never
roll forward on a bad week; rolling back one step is always allowed).

**Out of scope but flagged to Narin**: advmfr.com (Google side) publishes three
conflicting DMARC records, which makes DMARC void there. Fix in Google Admin /
its DNS, then apply the same ladder.

## Step 7 — Tests (same day as Steps 1–4)

1. **Built-in filter test**: Global Email Filters → "Filter Test" box at the bottom.
   Paste the complete raw source (headers included) of one of the received malicious emails.
   Expected: the trace shows delivery ("save") into the `.Quarantine` folder path.
2. **GTUBE test** — ⚠️ result 2026-08-14: **not usable on o2switch to test account filters.**
   GTUBE's extreme score triggers the o2switch *upstream* layer, which rejects at SMTP time
   (sender gets `550 "The mail server detected your message as spam"`) before the message
   ever reaches the account — it doesn't even appear in Track Delivery. It validates the
   upstream layer only. Account-filter validation = the built-in Filter Test (step 1)
   plus real traffic observed in Track Delivery (real malware waves score ~43 and DO pass
   the upstream layer → they must now appear in `Quarantine`).
3. **Control test**: send a normal message from the same external mailbox.
   Expected: normal delivery, untouched.
4. **Track Delivery** (cPanel): both test messages visible with the correct outcome.

## Step 8 — Day 7: graduation to Discard

Precondition: 7 consecutive days of the `Quarantine` folder containing nothing legitimate
(2-minute daily check via the system account's webmail). Any false positive → whitelist it,
restart the counter.

1. Edit `10-quarantine-score10`: action `Deliver to Folder` → **`Discard Message`**.
2. Leave `EXEC` / `Executables & scripts` on Deliver to Folder (their body-regex rarely fires;
   quarantine costs nothing and keeps evidence of what they catch).

## Later (steady state — quarterly)

- Purge the `Quarantine` folder and every mailbox's Spam folder (disk quota).
- Read DMARC reports; when clean for several weeks: `p=none` → `p=quarantine` → `p=reject`.
- Delete any filter that never fired in a quarter.
- Consider hardening SPF `~all` → `-all` (mirror elevaseo.com).
- Clean up the ~20 dead 0-byte test mailboxes listed in the audit, and decide whether to
  keep or delete the now-unused `quarantine@advmfr.net` mailbox.
- Keep CMS/plugins updated on every site of the account.

---

## Permanent guardrails

1. Never **Fail With Message** (backscatter).
2. **Deliver to Folder** in a *global* filter always lands in the **system account's**
   folder, never the recipient's (o2switch-documented). We use that deliberately as the
   central quarantine — never use it expecting per-recipient sorting.
3. Never Discard on a rule that hasn't been observed in quarantine first.
4. Whitelist stays in position 1.
5. Never match on Body or attachment filenames (Base64 wall) — use Spam Bar, headers, addresses.
   (The two legacy body-regex filters are kept as a harmless extra layer, not relied upon.)
6. Filter names are unique: reusing a name silently overwrites the old filter.
7. One new rule per deployment day.
8. `Redirect to Email` in a global filter cannot target a domain hosted on this account
   (cPanel anti-recursion rule) — and redirecting spam to an *external* address is forbidden
   anyway (spam relaying).
