Skip to main content

cbpolicyd Access Control on Zimbra

Access control is the simplest module in cbpolicyd and has the smallest table: a policy, a verdict and some text. Everything interesting happens in the policy that selects which transactions it sees, which is why it is worth reading the policy model before using it.

Part of the series that starts with cbpolicyd on Zimbra: An Introduction.

Enable it

Unlike quotas, this module is off by default:

zmprov ms $(zmhostname) zimbraCBPolicydAccessControlEnabled TRUE
zmmtactl restart

The table

CREATE TABLE access_control (
ID ...,
PolicyID ..., -- which policy this belongs to
Name ...,
Verdict ..., -- OK, REJECT, DEFER, HOLD, DISCARD, ...
Data ..., -- text returned with the verdict
Comment ...,
Disabled ...
);

That is the whole module. There are no conditions inside it, because the conditions are the policy's job.

The verdicts

VerdictEffect
OKAccept immediately, stop evaluating further restrictions
REJECT5xx, permanent refusal during the session
DEFER4xx, the sender retries later
HOLDAccepted into Postfix's hold queue instead of being delivered
DISCARDAccepted and silently dropped
REDIRECTDelivered to a different address instead
DUNNONo opinion, let the remaining restrictions decide

DUNNO is the default when nothing matches, and it is what makes the module safe to layer: a policy that does not apply simply says nothing rather than accepting or refusing.

Use case: block a domain outright

INSERT INTO policies (Name, Priority, Description, Disabled)
VALUES ('Blocked senders', 5, 'Domains and addresses we refuse', 0);

INSERT INTO policy_members (PolicyID, Source, Destination, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'Blocked senders'),
'@spam.example', 'any', 0);

INSERT INTO access_control (PolicyID, Name, Verdict, Data, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'Blocked senders'),
'Refuse blocked domains',
'REJECT',
'Mail from this domain is not accepted.',
0);

Priority 5 puts this ahead of the stock direction policies at 10 and 20, so the refusal is decided before anything else weighs in. Add more senders by adding policy_members rows to the same policy rather than creating a policy each time.

Use case: stop one person mailing one person

Harassment and dispute cases usually need a narrow block rather than a domain-wide one, and Destination is what makes that possible:

INSERT INTO policies (Name, Priority, Description, Disabled)
VALUES ('Targeted blocks', 4, 'Specific sender to specific recipient', 0);

INSERT INTO policy_members (PolicyID, Source, Destination, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'Targeted blocks'),
'someone@external.example',
'victim@yourdomain.com', 0);

INSERT INTO access_control (PolicyID, Name, Verdict, Data, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'Targeted blocks'),
'Blocked pairing', 'REJECT',
'Delivery to this recipient is not permitted.', 0);

Consider DISCARD instead of REJECT here. A rejection tells the sender they have been blocked, which in a harassment case often prompts them to find another route. A discard tells them nothing. Weigh that against the fact that discarded mail is gone with no record, and that you are silently accepting responsibility for a message you then destroy.

Use case: quarantine instead of refusing

HOLD puts the message in Postfix's hold queue, where it sits untouched until you decide:

INSERT INTO access_control (PolicyID, Name, Verdict, Data, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'Blocked senders'),
'Hold for review', 'HOLD', 'Held for administrator review', 0);

Inspect and release with the normal Postfix tools:

mailq # held messages show with a ! marker
postsuper -H ALL # release everything back into the queue
postsuper -d <queue-id> # delete one

This is a good way to trial a block before committing to it. Run it as HOLD for a week, look at what accumulated, then switch the verdict to REJECT once you trust it.

Use case: an allow rule in front of a block

Because every matching policy contributes in priority order, an OK at a lower number acts as an exemption from a broader refusal below it:

INSERT INTO policies (Name, Priority, Description, Disabled)
VALUES ('Always allow', 1, 'Partners that must never be blocked', 0);

INSERT INTO policy_members (PolicyID, Source, Destination, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'Always allow'),
'@partner.example', 'any', 0);

INSERT INTO access_control (PolicyID, Name, Verdict, Data, Disabled)
VALUES ((SELECT ID FROM policies WHERE Name = 'Always allow'),
'Allow partner', 'OK', 'Whitelisted partner', 0);

OK tells Postfix to accept and stop processing further restrictions, so use it deliberately. It bypasses the checks that would have run afterwards, including ones you may want to keep, such as recipient validation.

When a Postfix access map is the better tool

Be honest about this one. For a static list of blocked senders, Postfix's own check_sender_access map is simpler, faster, needs no database and is maintained with a text file:

zmprov mcf +zimbraMtaRestriction \
"check_sender_access lmdb:/opt/zimbra/conf/postfix_reject_sender"

cbpolicyd access control earns its place when the decision depends on something a flat map cannot express: the combination of sender and recipient, the client network, whether the session was authenticated, or a rule that should only apply to one direction of mail. If your rule is just a list of bad senders, use the map.

MSH Zimbra Rules covers the same ground with a managed interface and per-domain delegation, which matters when the people maintaining the list are not the people with shell access.

Checking what you built

SELECT p.Priority, p.Name AS Policy, m.Source, m.Destination,
a.Verdict, a.Data
FROM access_control a
JOIN policies p ON p.ID = a.PolicyID
LEFT JOIN policy_members m ON m.PolicyID = p.ID
WHERE a.Disabled = 0
ORDER BY p.Priority;

Reading that ordered by priority is the quickest way to spot the usual mistake: a broad REJECT at a lower number than the OK that was supposed to exempt something from it.

zmcbpolicydctl restart

Next in this series

cbpolicyd Greylisting, which refuses mail temporarily on purpose and is far more effective than it sounds.