go-policyd
is an anti-spam plugin for Postfix (written in Golang) that does sasl-based throttling.
Project purpose is to rate limit postfix outgoing emails sent per user via 'authenticated' abused user when phishing succeeds.
This daemon has been inspired from a existing policy golang daemon : polka. Go language is easy to install, making this project easy to read and adapt to suit your needs.
go-policyd
use postfix policy protocol (check Postfix SMTP Access Policy Delegation).
Based on recipients numbers cumulated by day it responds DUNNO (neutral)/ HOLD (store in quarantine)/ REJECT ( refuse mail.).
Using a centralized database, it may be used for multiple authenticated postfix relays or clustering.
Using this projects we successfully reduced our spam volume during phishing campaigns from 60.000 users spammed to 1500 per day.
Quota of total recipients by day for an authenticated sender.
Hold queue when over quota for mail analysis and requeue if whitelisting or errors.
Rejection when recipients sum is over 2x quota max (customizable)
Persistant Mysql/Mariadb storage of policyd events.
Easy upgrade of a Single binary
serving as network daemon, allowing multiple remote postfix smtps (de)centralisation.
Whitelisting only during offices hours (not weekends). Blacklisted entries are permanent.
Default accept mode when encountering database issues.
Usual go commands, such as go mod tidy
, go build
should download dependencies and build binary
Version tag is given from git tag , a simple makefile is :
NAME= $(notdir $(shell pwd))
TAG=$(shell git tag)
{NAME}:
go build -ldflags '-w -s -X main.Version=${NAME}-${TAG}' -o ${NAME}
- Create mariadb database,
- copy binary in /local/bin/policyd,
- Adapt config file contrib/policyd.cfg to /etc/postfix/policyd.cfg,
- Enable and start (CentOS systemd) service /local/etc/policyd.service,
- Configure postfix for policyd restrictions.
Mariadb should be enabled and running in order to create database.
> CREATE USER 'policyd_daemon'@'localhost' IDENTIFIED BY 'yourChoiceOfPassword';
Query OK, 0 rows affected (0.01 sec)
> CREATE DATABASE policyd;
Query OK, 1 row affected (0.00 sec)
> USE policyd;
> CREATE TABLE IF NOT EXISTS `events` (
`ts` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`sasl_username` char(8) NOT NULL DEFAULT '',
`sender` char(80) NOT NULL DEFAULT '',
`client_address`char(80) NOT NULL DEFAULT '',
`recipient_count` int(6) DEFAULT NULL,
PRIMARY KEY (`ts`,`sasl_username`,`sender`,`client_address`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
> GRANT ALL PRIVILEGES ON policyd.* TO 'policyd_daemon'@'localhost';
Query OK, 0 rows affected (0.00 sec)
Nota :
- DATETIME(3) avoid key collision when multiples connections occurs.
- The cleaning of records older than 7 days is done daily every 24 hours.
- A policyd top 20 usage display utility is available in contrib/policyd-top20.sh
warning : sasl_username length
may be an issue
for you if your logins length are over 8 chars
# mkdir -p /local/bin/
# cp policyd /local/bin
# cp contrib/unit.service /local/etc/policyd.service
# cp contrib/policyd.cfg /etc/postfix/policyd.cfg
# edit /etc/postfix/policyd.cfg
# systemctl daemon-reload
# systemctl enable /local/etc/policyd.service
# systemctl start policyd.service
# systemctl status policyd.service
Binary and config file localisation may be modified
Edit /etc/postfix/main.cf
to add :
# Policyd restrictions ( at end_of_data stage for nbrcpt )
smtpd_end_of_data_restrictions = check_policy_service inet:127.0.0.1:9093
Then verify configuration with postfix check
command
The daemon once started by systemd is restarted in the event of an unexpected shutdown.
However the daemon is also monitored by monit via the lines
check program policyd with path "/usr/bin/systemctl --quiet is-active policyd"
if status != 0 then restart
start program = "/usr/bin/systemctl start policyd.service"
stop program = "/usr/bin/systemctl stop policyd.service"
## When user is over quota, mail is stored to postfix HOLD Queue, send alert
check file daemon.info with path /var/log/daemon.info
ignore content = "monit"
if match "DEFERRING overquota" then alert
go-policyd use syslog "daemon.ERR|INFO" facility
# tail /var/log/daemon.err
Sep 23 11:08:56 smtps policyd[8168]: ERROR while UPDATING db :dial tcp 127.0.0.1:3306: connect: connection refused
Sep 23 11:09:03 smtps policyd[8168]: ERROR while UPDATING db :dial tcp 127.0.0.1:3306: connect: connection refused
Sep 25 11:19:55 smtps systemd: Failed to start Policyd go daemon for Postfix.
Sep 25 11:20:20 smtps systemd: Failed to start Policyd go daemon for Postfix.
Nov 9 04:28:46 smtps policyd[25479]: Skipping policyVerify db.Ping Error: driver: bad connection
# tail /var/log/daemon.info
Sep 26 16:23:34 smtps policyd[18771]: Updating db: nathxxx/nathalie.xxxxxxxx@mydomain.fr/192.168.39.7/1/6
Sep 26 16:24:22 smtps policyd[18771]: Updating db: anaxxx/anabelle.xxxxxxx@mydomain.fr/192.168.39.96/1/46
Sep 26 16:28:15 smtps policyd[18771]: Updating db: marxxx/maria.xxxxxxx@sub.mydomain.fr/192.168.24.154/1/14
Nov 9 04:28:46 smtps policyd: [mysql] 2021/11/09 04:28:46 packets.go:123: closing bad idle connection: unexpected read from socket
Nov 9 04:28:46 smtps policyd[25479]: Skipping policyVerify db.Ping Error: driver: bad connection
The log format is identifier / email / ip / recipients / recipientssumforthelast24h.
Sep 26 16:27:53 smtps policyd[18771]: Updating db: anaxxx/anabelle.xxxxxxx@mydomain.fr/192.168.39.96/2/50
indicates the sending of an email to 2 recipients, bringing the total of recipients over 24 hours to 50.
Legit messages can be re-enqueued after policyd user whitelisting reconfiguration and a restart of policyd daemon, with commands like :
# postqueue -p | grep legit-user | awk '{ print $1 }' | sed "s/\!//g" | xargs -n 1 postsuper -r
Cleaning of spam/phishing can be done with :
# cd /var/spool/postfix/hold ; ls | postsuper -d -
# tail /etc/postfix/main.cf
# For denial of service mitigations (by ip) please read :
# http://www.postfix.org/TUNING_README.html#conn_limit
# http://www.jonsblog.org/2011/11/30/stay-off-of-blacklists-limit-postfix-recipients/
# anvil_rate_time_unit (default: 60s).
anvil_rate_time_unit=1h
smtpd_client_connection_count_limit=10
smtpd_client_connection_rate_limit=120
smtpd_client_message_rate_limit=120
smtpd_client_recipient_rate_limit=1000
smtpd_client_event_limit_exceptions=127.0.0.1 <hosts> ...
Code is short and basic. It is intended to be adapted to suit your needs with few modifications.
So clone, fork, (star,) comment, adapt to your needs, and let me know what we can improve !