I run IT and security for a Domino’s franchise group — around a hundred stores, a small office staff, and an in-house operations platform I wrote. This year two real phishing attacks landed on us. One of them I took apart in detail elsewhere on this blog. The other cost us a password.
So with sign-off from our executives, I built an authorized phishing simulation and sent the first wave.
I expected the hard part to be the lure. It wasn’t. Getting it to arrive in an inbox took an evening and a handful of sends, down two dead ends, and the guidance out there is inconsistent in a way that costs you sends. Every phishing-simulation vendor publishes a Workspace allowlisting guide; they do not agree on which mechanism matters, and the one several of them lead with does not work on its own.
This is part one, written on day one. The delivery problem is solved and that part is finished. The results are still developing, though we have some early findings already.
Details that would identify individuals are left out. The technical content is exactly as I found it.
Dead end one: Chrome killed the domain before I sent anything
The first decision is what domain the lure comes from. The rule I set myself was: use a domain the group already owns and can retire, never register a lookalike. A lookalike becomes a real asset you have to guard, and if it ever lapses, somebody else owns a domain your staff have been trained to half-trust.
We own a disposable .app domain with nothing on it. I picked a subdomain that read like a Google security page and pointed it at a Cloudflare tunnel.
Chrome served this instead:
Did you mean google.com? The site you just tried to visit looks fake. Attackers sometimes mimic sites by making small, hard-to-see changes to the URL.
That is Chrome’s lookalike-domain protection, and it matched on the brand token in my hostname. Most of my staff are on Chrome, which means the wave would have measured Chrome’s heuristic rather than anybody’s judgement. A full-page interstitial before the lure even loads is not a test of anything.
Two things worth taking from that.
It is a real control, working, for free. I found out my browser fleet catches lookalike domains without spending a wave to learn it. That goes in the report as a finding rather than a setback.
The fix is also the more faithful choice. I dropped the brand token entirely and used a neutral hostname. That is closer to how the attacks that actually hit us worked: the one I wrote up came from a genuine, compromised school-district mailbox, not a lookalike. Real credential phishing mostly uses neutral or hijacked infrastructure now, for exactly the reason I had just walked into.
There is a Chrome enterprise policy — LookalikeWarningAllowlistDomains — that would suppress the warning on managed devices and let me keep the brand-themed hostname. Do not do this. You would be switching off the control that just protected you in order to run training about the threat it protects you from.
Dead end two: the allowlist everybody documents does not work
With a clean hostname, real DKIM, SPF authorizing the sending IP and a DMARC record, I sent myself the first live lure.
It went to spam.
So I did what several of those guides lead with. Hook Security’s, for instance, walks you through the IP allowlist, an inbound gateway, and then this. In the Workspace admin console: Apps → Google Workspace → Gmail → Spam, phishing and malware → Spam, tick “Bypass spam filters for messages from senders or domains in selected lists”, point it at an approved-senders address list containing my sending domain, apply at the root OU.
Sent again. Spam.
Waited for propagation. Sent again. Spam.
Three sends, and the authentication was not the problem. Pulling the actual header off one of the spam-foldered messages:
Authentication-Results: mx.google.com;
dkim=pass header.i=@<sending-domain> header.s=phish1
spf=pass designates <office-ip> as permitted sender
dmarc=pass (p=NONE sp=NONE dis=NONE)
All three pass. Allowlist configured. Still spam.
The answer is in Google’s own documentation for advanced phishing and malware protection, and it is unambiguous:
Generally, these advanced security features work independently of other spam settings you might have previously turned on. For example, even if you’ve listed a domain as a safe sender in spam settings, the enhanced security features are still applied.
The spam allowlist governs spam classification. It does not govern phishing classification, and there is no documented way to exempt a sender from the latter. My message was not being scored as spam. It was being scored as phishing — which it was, structurally: a Google-branded credential-entry page, from a subdomain created twenty minutes earlier, with no sending history. That is precisely what Enhanced pre-delivery message scanning exists to catch, and it was doing its job.
No amount of SPF, DKIM or DMARC fixes this. Authentication proves the sender is who they claim; it says nothing about whether the content is a credential harvest.
What actually works: content compliance on a header you control
Every phishing-simulation vendor — Phin, Ethena, Hook, Guardz, Trend Micro — documents the same workaround, and it is not the spam setting. It is a content compliance rule, which sits at the routing layer, ahead of the spam classifier.
Apps → Google Workspace → Gmail → Compliance → Content compliance → Add:
- Email messages to affect: Inbound
- Expression:
Advanced content match→ Location Full headers → Contains text → your marker - Action: ✅ Bypass spam filter for this message
The send immediately after that rule went live reached the inbox. The three before it did not. Same sender, same content, same authentication — one variable.
One caveat on my own testing: I did not try the inbound-gateway route some guides recommend. It may work too. What I can say is that the approved-senders spam bypass did not, and that content compliance did, on the first send after it was applied.
The part I would do differently from most of those guides: match on a custom header, not on the sending domain or IP.
My tooling stamps every simulated lure with a header of my own:
X-Simulation: hishmeh-security-awareness
The compliance rule matches on that string. That matters for a reason worth being precise about. A rule keyed to a domain or an IP creates a standing bypass around your phishing protection for anything arriving from that source — and a sending domain is a public fact that anyone can spoof the From of. A rule keyed to a header nobody outside your tooling knows about cannot be ridden by someone who has not already read your source code.
It also means a new sending domain inherits inbox delivery for free. When I built the second lure family the following afternoon, with a completely different pretext and sender, it delivered on the first attempt with no configuration at all.
Take the rule down when the wave ends. It is a bypass around a live control. Mine is labelled REMOVE AFTER WAVE for that reason.
Why I built it instead of buying it
Two decisions that saved trouble.
No GoPhish. The obvious choice, and I had it installing before I stopped. Its latest release, v0.12.1, is dated 2022-09-14 — four years to the week, and nothing newer exists. The repository is not archived, but its last commit was 2024-09-23, two years after that release and two years ago now, with 761 issues open. Self-hosting a credential-capture-shaped application in that state, on the same box as production, was a trade I did not want. I wrote a small FastAPI service instead: four routes, a tracking pixel, a lure page, a teaching page. The results surface in the operations platform we already use, behind the auth that already exists.
No transactional email provider. This is the one that surprised me. SendGrid’s acceptable-use policy prohibits “content that is fraudulent or intended to mislead a recipient, such as phishing emails.” Mailgun’s never uses the word, but bars any activity “which might reasonably be considered… deceptive… fraudulent.” Neither carves out authorized security-awareness testing. A credential-phishing simulation is that content on its face, whatever your intent, and running a wave through one risks the account and the domain’s reputation.
It also buys nothing. Every recipient is on my own Workspace tenant. There is exactly one hop to make, and I can make it myself:
hosts = sorted(dns.resolver.resolve(domain, "MX"), key=lambda r: r.preference)
payload = dkim_sign(msg, selector=b"phish1", domain=sender_domain, privkey=key)
with smtplib.SMTP(str(hosts[0].exchange).rstrip("."), 25, timeout=30) as smtp:
smtp.starttls()
smtp.sendmail(envelope_from, [rcpt], payload)
Outbound 25 from the office is open, so the mail goes straight to Google’s MX, signed with a key I generated. No provider, no AUP to violate, no account to lose. Deliverability into my own tenant comes from the compliance rule, not from somebody else’s IP reputation — which is the thing a provider actually sells you.
One guarantee worth building the schema around
The simulation presents a password field. It has never stored a password, and the design makes that a property rather than a promise:
- The password input carries no
nameattribute. Browsers do not serialize unnamed controls, so the value cannot leave the page even with scripting disabled and the native POST firing. - The page submits a bare
fetch()carrying only the tracking token. - The submit handler never reads the request body. There is no parse step to get wrong.
- There is no column in the schema that could hold one.
There are tests that post a real-looking password at the endpoint and assert it reaches neither the recorder nor the database, plus a static check that fails the moment anyone adds a body-parse step to that handler. After the first live wave I swept every text column across all three tables for the submitted string and for fragments of it, and grepped the service journal. Nothing.
A simulation that logs credentials has manufactured a worse exposure than the one it was measuring. If those tests ever fail, the wave does not go out.
Day one, honestly
Nineteen people. One lure. Here is everything I can defend after a day:
- Two people clicked and submitted a password. One about forty seconds after delivery, one a couple hours later.
- Two people reported it by picking up the phone and calling me.
- Most haven’t done anything observable at all.
The thing I did not expect was how much the shape of a response varies between people who were equally alarmed. Two people both found it suspicious. One picked up the phone. Another resolved it themselves, immediately, by clicking the thing to find out what it was.
More when there is more. The second lure family went out today.