Skip to main content

cbpolicyd on Zimbra: An Introduction

Every Zimbra installation already has a policy daemon sitting unused. cbpolicyd ships with the MTA package, stays switched off until you enable it, and does one thing nothing else in Zimbra can: it counts. How many messages a sender has sent this hour, how much volume a domain has pushed today, whether this client has been seen before. If you have ever wanted to stop a compromised mailbox from sending ten thousand messages overnight, this is the tool that was already on the server.

It also has a history worth knowing before you build anything on it.

This is the first article in a series about cbpolicyd on Zimbra.

What it is

cbpolicyd is Policyd v2, codenamed Cluebringer. It is a Postfix policy delegation service: a daemon Postfix consults over a socket during the SMTP session, sending it the details of the transaction and acting on the verdict it gets back.

On Zimbra it runs on 127.0.0.1:10031, stores everything in a SQLite database, and is wired into Postfix automatically when you enable the service:

smtpd_recipient_restrictions = check_policy_service inet:localhost:10031, ...
smtpd_end_of_data_restrictions = check_policy_service inet:localhost:10031

Because it answers during the SMTP conversation, it can defer or reject a message before your server has accepted responsibility for it. That is the property everything else follows from.

Where it sits

Sending mail server
|
| SMTP, port 25
v
Postfix (smtpd) <-- cbpolicyd answers here, on 127.0.0.1:10031
| and here, MSH Zimbra Rules runs as a milter
|
| message accepted and queued
v
Amavis <-- spam and virus checks
|
v
Mailbox server <-- Sieve runs here, at final delivery
v
A folder in the recipient's mailbox

cbpolicyd and a milter both act during the SMTP session, but they answer different questions. A milter reads the message and can rewrite it. cbpolicyd mostly does not look at the message at all - it looks at who is talking to whom, how often, and decides whether to let the transaction continue.

A short history

Policyd v1 was a small C daemon for greylisting and throttling. Policyd v2 was a ground-up rewrite in Perl under the codename Cluebringer, started by Nigel Kukard in 2007. The copyright headers trace the handover: Nigel Kukard in 2007, LinuxRulz in 2008, AllWorldIT from 2009 to 2015.

Version 2.0.8 was released in October 2009. Development carried on into 2.1.x snapshots through 2013, and then effectively stopped. Zimbra picked it up and began shipping it as zimbra-cbpolicyd, with the whole zimbraCBPolicyd* configuration family arriving in ZCS 8.5.

Current status, honestly

The upstream project describes itself in the past tense. Its own wiki says:

PolicyD v2 is historical, unmaintained software.

and advises testing in an isolated environment before touching a production mail server.

Zimbra's position is narrower than most people assume. cbpolicyd is supported, but:

The only supported way of configuring cbpolicyd is through the CLI, editing the sqlite database.

The web UI that exists in the source tree is explicitly not supported, and Zimbra says so in as many words: CLI yes, AdminUI no. Plenty of tutorials walk you through enabling that web interface. It works, after a fashion, and you are on your own with it.

So the realistic summary is: frozen upstream, bundled and supported by Zimbra within narrow limits, configured by writing SQL by hand. That is an unusual place for a piece of production infrastructure to be, and it is worth deciding deliberately rather than by accident.

What it can do

cbpolicyd is a set of modules, each switched on separately. On Zimbra only one is on by default:

ModuleDefaultWhat it does
QuotasenabledCounts messages or bytes per sender, recipient, domain or IP over a time window
Access controldisabledAllow and deny by sender, recipient, IP or SASL user
GreylistingdisabledTemporarily defer unknown senders, which stops naive spam software
CheckSPFdisabledAct on SPF results during the session
CheckHelodisabledReject malformed or blacklisted HELO names
AccountingdisabledTrack traffic per calendar day, week or month, and optionally enforce a limit
AmavisdisabledPass per-policy settings through to amavis

Quotas is the reason most people turn cbpolicyd on, and it is the one this series starts with.

What it is genuinely good at

Rate limiting, which nothing else on the box can do. This is the honest case for cbpolicyd. Sieve has no memory between messages - it evaluates one message in isolation and forgets it - so "this sender has sent 200 messages in the last hour" simply cannot be expressed there at any level of cleverness.

Catching a compromised account. The signature of a stolen password is a normally quiet mailbox suddenly sending in volume. A quota on outbound messages per sender per hour turns that from a discovery you make the next morning into a message that never left.

Refusing before acceptance. A DEFER verdict is a 4xx during the session, so legitimate senders retry and nothing is lost, while your server never took custody of the message. Compare that with refusing after delivery, where the bounce has to be generated by your own server and, with a forged sender, lands on an innocent third party.

Granularity without touching mailboxes. Policies attach to individual addresses, whole domains, CIDR ranges, SASL users or named groups, and none of it involves the mailbox store.

What to expect going in

Upstream is frozen. No new features, no upstream fixes. What ships with your Zimbra version is what you get.

There is no supported interface. Configuration means sqlite3 and hand-written INSERT statements against a schema that is only lightly documented. Column order has varied between versions, so write named-column inserts rather than positional VALUES(...) lists.

The documentation is thin and partly wrong. The example SQL on Zimbra's own wiki contains placeholder characters that will not execute as written. Treat every example you find, including well-regarded blog posts, as something to read rather than paste.

It fails silently and invisibly. A policy whose group has no members matches nothing and reports nothing. The stock internal_domains group ships containing @example.com and @example.org, so on a fresh install the direction-based policies may match nothing real at all. At the default log level of 3 you will see very little about why.

Counting can surprise you. Postfix consults the policy service at RCPT stage, once per recipient, so one message addressed to three people can count three times against a message-count quota. Always watch the tracking table while testing rather than assuming.

When something else is the better answer

  • Rewriting the message - subjects, headers, disclaimers, attachments - belongs in a milter. MSH Zimbra Rules does this, and its Message count based on sender condition also covers rate limiting with a supported interface rather than raw SQL.
  • Deciding where a message lands in a mailbox - folders, tags, flags - belongs in Sieve. cbpolicyd has no concept of a folder.
  • Acting on the spam score belongs in Sieve too, because amavis runs after the SMTP session is over and the X-Spam-* headers do not exist yet when cbpolicyd is consulted.

The three tools do not overlap as much as they first appear. cbpolicyd governs the conversation, a milter shapes the message, and Sieve files the result.

Where everything lives

WhatPath
Database/opt/zimbra/data/cbpolicyd/db/cbpolicyd.sqlitedb
Configuration/opt/zimbra/conf/cbpolicyd.conf
Log/opt/zimbra/log/cbpolicyd.log
Daemon/opt/zimbra/cbpolicyd/bin/cbpolicyd
Listens on127.0.0.1:10031
Service controlzmcbpolicydctl with start, stop, restart or status

Useful settings, all with zmprov ms $(zmhostname) ..., and all requiring an MTA restart:

AttributeDefault
zimbraCBPolicydQuotasEnabledTRUE
zimbraCBPolicydAccessControlEnabledFALSE
zimbraCBPolicydGreylistingEnabledFALSE
zimbraCBPolicydCheckSPFEnabledFALSE
zimbraCBPolicydCheckHeloEnabledFALSE
zimbraCBPolicydAccountingEnabledFALSE
zimbraCBPolicydBindPort10031
zimbraCBPolicydLogLevel3, range 0 to 4

Should you use it

If you need rate limiting on a Zimbra server and do not want to add software, cbpolicyd is the answer that is already installed, and it does the job. Go in knowing that you are configuring a frozen project through raw SQL, keep a backup of the database before every change, and test on something that is not your production MTA.

If raw SQL against an unmaintained daemon is not a trade you want to make, the same outcomes are available from a maintained tool with an interface, and that is a perfectly reasonable place to land.

Next in this series

Enabling cbpolicyd on Zimbra and Reading the Policy Model covers switching the service on, confirming Postfix is actually consulting it, and reading the four tables that decide whether a rule ever fires - before writing a single one.