cbpolicyd HELO Checking on Zimbra
The HELO command is the first thing a sending server says in an SMTP conversation: this is who I am. It is unverified, and spam software has historically been careless with it, which makes it a cheap and surprisingly effective signal.
The cbpolicyd checkhelo module acts on three different things a bad HELO can tell you: that the sender is impersonating someone, that they are rotating through made-up names, and that the name is simply not valid.
Part of the series that starts with cbpolicyd on Zimbra: An Introduction.
Enable it
zmprov ms $(zmhostname) zimbraCBPolicydCheckHeloEnabled TRUE
zmmtactl restart
What it can check
| Column | What it does |
|---|---|
UseBlacklist | Check the HELO against checkhelo_blacklist |
BlacklistPeriod | How long to keep a host blacklisted after a hit; 0 means check live |
UseHRP | HELO randomisation prevention |
HRPPeriod | Window in which to count distinct HELOs from one address |
HRPLimit | How many distinct HELOs are allowed in that window |
RejectInvalid | Refuse a malformed HELO |
RejectIP | Refuse a HELO that is a bare IP address |
RejectUnresolvable | Refuse a HELO that does not resolve in DNS |
NULL means inherit, not off.
The blacklist ships with sensible entries
checkhelo_blacklist is populated out of the box with the names nothing legitimate should
ever announce:
127.0.0.1
[127.0.0.1]
localhost
localhost.localdomain
The valuable addition is your own identity. No external server has any business announcing itself as your mail host or your domain, so anything that does is forging:
INSERT INTO checkhelo_blacklist (Helo, Comment, Disabled)
VALUES ('mail.yourdomain.com', 'Nobody outside should claim to be us', 0);
INSERT INTO checkhelo_blacklist (Helo, Comment, Disabled)
VALUES ('yourdomain.com', 'Nobody outside should claim to be us', 0);
Then whitelist your own networks so your own servers and clients are exempt.
checkhelo_whitelist takes the form SenderIP:a.b.c.d/e:
INSERT INTO checkhelo_whitelist (Source, Comment, Disabled)
VALUES ('SenderIP:10.0.0.0/8', 'Internal networks', 0);
INSERT INTO checkhelo_whitelist (Source, Comment, Disabled)
VALUES ('SenderIP:127.0.0.0/8', 'Loopback', 0);
Do the whitelist first. Blacklisting your own hostname without exempting your own networks will block your own submission, and you will find out in an unpleasant way.
A working configuration
INSERT INTO policies (Name, Priority, Description, Disabled)
VALUES ('HELO checks inbound', 28, 'HELO validation for inbound mail', 0);
INSERT INTO policy_members (PolicyID, Source, Destination, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'HELO checks inbound'),
'$-', -- unauthenticated sessions only
'%internal_domains',
0);
INSERT INTO checkhelo
(PolicyID, Name, UseBlacklist, BlacklistPeriod,
UseHRP, HRPPeriod, HRPLimit,
RejectInvalid, RejectIP, RejectUnresolvable, Disabled)
VALUES
((SELECT ID FROM policies WHERE Name = 'HELO checks inbound'),
'Inbound HELO checks',
1, 0, -- use the blacklist, checked live
1, 3600, 5, -- at most 5 distinct HELOs per address per hour
1, -- reject malformed
0, -- but allow bare IP literals
0, -- and allow unresolvable
0);
Again $- restricts this to unauthenticated sessions, so your own users are never judged
on the HELO their mail client happens to send. That matters more here than elsewhere,
because mail clients send all sorts of things.
HELO randomisation prevention
UseHRP counts how many different HELO names one client address announces inside
HRPPeriod. A real mail server uses one name consistently. Software cycling through
invented names to dodge blacklists trips the limit.
Five per hour is a forgiving starting point. Be aware that a large provider sending from one NAT address with many hosts behind it can legitimately present several names, so tighten this only after watching the tracking table:
SELECT Address, Helo, datetime(LastUpdate, 'unixepoch') AS LastUpdate
FROM checkhelo_tracking
ORDER BY LastUpdate DESC LIMIT 30;
-- addresses presenting many names
SELECT Address, COUNT(DISTINCT Helo) AS names
FROM checkhelo_tracking
GROUP BY Address HAVING names > 3 ORDER BY names DESC;
That second query is the one to run before enabling HRP. If it returns familiar, legitimate senders, your limit is too low.
The three RFC checks, in order of risk
RejectInvalid is the safe one. A HELO that is syntactically malformed is broken, and
almost nothing legitimate sends one.
RejectIP refuses a HELO that is a bare IP address. A correctly formed IP literal in
brackets, [192.0.2.1], is legal in the standards, and some small or appliance-based
senders use one. This check refuses them anyway. Enable it only if you are prepared to
handle the reports.
RejectUnresolvable is the aggressive one. It requires the HELO name to resolve in DNS,
which sounds reasonable and fails on plenty of real senders: misconfigured but legitimate
business servers, hosts behind split-horizon DNS, and anything with a transient DNS
problem at the moment your server asked. A DNS outage on their side becomes lost mail on
yours.
Leave both RejectIP and RejectUnresolvable at 0 unless you have a specific spam
problem they solve, and turn them on one at a time.
Testing without waiting for spam
swaks lets you set the HELO explicitly, so you can prove each check:
# should be refused once your hostname is blacklisted
swaks --server yourserver --helo mail.yourdomain.com \
--from test@external.example --to user@yourdomain.com
# should be refused with RejectInvalid on
swaks --server yourserver --helo "not a valid helo" \
--from test@external.example --to user@yourdomain.com
Run these from outside your whitelisted networks, or the whitelist will exempt you and every test will pass.
What this catches and what it does not
HELO checking is cheap, runs before any content is transferred, and reliably catches forgeries claiming to be you. It is a good use of the module.
It does not catch competent spam. Anything sent through a compromised account on a real mail server presents a perfectly valid HELO, because it is a real mail server. Treat this as one inexpensive layer, not as a spam solution.
Next in this series
cbpolicyd Accounting, which measures traffic over calendar periods rather than rolling windows.