diff --git a/sql/README.md b/sql/README.md new file mode 100644 index 0000000..f563d7f --- /dev/null +++ b/sql/README.md @@ -0,0 +1,27 @@ +# Postfix TLS Policy Maps - SQL Proxy + +If you don't want to use a plain file Postfix lookup table to manage TLS policy maps, but a SQL backend, you'll very likely find the files in this directory helpful. You can use them as follows: + +1. Create a `tls_policy` table in the SQL database you want to use with Postfix. You can use the provided [`scheme.sql`](scheme.sql) if you want to. + +2. Create a proxy configuration file (e.g. `/etc/postfix/tls_policy.cf`) to tell Postfix the SQL query to use. You can again use the provided [`postfix_proxy.cf`](postfix_proxy.cf) as a blueprint, but don't forget to change username and password. + +3. Configure Postfix to actually use the proxy configuration file by setting the `smtp_tls_policy_maps` parameter in Postfix's `main.cf` accordingly. Don't forget to reload/restart Postfix afterwards. + ``` + smtp_tls_policy_maps = mysql:/etc/postfix/tls_policy.cf + ``` + +4. Use the provided [`update_database.sh`](update_database.sh) to convert the plain file Postfix lookup table to SQL queries and execute them. You can e.g. simply pipe stdout of the script to the `mysql` command. + ``` + $ ./update_database.sh ../tls_policy | mysql --user=db_user --password "db_name" + ``` + You can change the built-in SQL query template by setting the environment variable `TEMPLATE`. You can use the placeholders `{domain}`, `{policy}` and `{params}` in the template. As a reference, this is the script's default template: + ``` + DELETE FROM tls_policy WHERE domain = '{domain}'; INSERT INTO tls_policy (domain, policy, params) VALUES ('{domain}', '{policy}', '{params}'); + ``` + +5. You may want to repeat Step 4 on a regular basis (e.g. weekly) to always use the newest upstream TLS policy maps on your server. The provided `update_database.sh` always validates the policy file before converting it into SQL queries, so you can safely automatize this task with a cronjob. The following crontab line is intended to provide inspiration for you to create your own cronjob (it will work with Debian only). Most importantly, you'll have to find a way to safely pass the password of the SQL user to the cronjob. + ``` + 0 4 * * 7 root curl -sS "https://raw.githubusercontent.com/csware/postfix-tls-policy/master/tls_policy" | /path/to/update_database.sh - | mysql --defaults-file="/etc/mysql/debian.cnf" --silent "db_name" + ``` + diff --git a/sql/postfix_proxy.cf b/sql/postfix_proxy.cf new file mode 100644 index 0000000..edac9b2 --- /dev/null +++ b/sql/postfix_proxy.cf @@ -0,0 +1,10 @@ +user = db_user +password = db_password +hosts = 127.0.0.1 +dbname = db_name + +query = + SELECT policy, + params + FROM tls_policy + WHERE domain = '%s' diff --git a/sql/scheme.sql b/sql/scheme.sql new file mode 100644 index 0000000..e1eec63 --- /dev/null +++ b/sql/scheme.sql @@ -0,0 +1,6 @@ +CREATE TABLE tls_policy ( + domain varchar(255) NOT NULL, + policy enum('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL, + params varchar(255), + PRIMARY KEY (domain) +); diff --git a/sql/update_database.sh b/sql/update_database.sh new file mode 100755 index 0000000..0261220 --- /dev/null +++ b/sql/update_database.sh @@ -0,0 +1,42 @@ +#!/bin/sh +if [ -z "$1" ]; then + echo "Usage: $(basename "$0") POLICY_FILE" >&2 + exit 1 +fi +if [ -z "$TEMPLATE" ]; then + TEMPLATE="DELETE FROM tls_policy WHERE domain = '{domain}';" + TEMPLATE="$TEMPLATE INSERT INTO tls_policy (domain, policy, params) VALUES ('{domain}', '{policy}', '{params}');" +fi + +DATABASE="$1" +[ "$DATABASE" = "-" ] && DATABASE="/dev/stdin" + +EXIT_CODE=0 +while read -r LINE || [ -n "$LINE" ]; do + [ -n "$LINE" ] || continue + [ "$(echo "$LINE" | cut -c 1)" = "#" ] && continue + + DATA="$(echo "$LINE" | sed -e 's/\s\+/ /')" + DOMAIN="$(echo "$DATA" | cut -s -d ' ' -f 1)" + POLICY="$(echo "$DATA" | cut -s -d ' ' -f 2)" + PARAMS="$(echo "$DATA" | cut -s -d ' ' -f 3-)" + + if [ -z "$(echo "$DOMAIN" | sed -ne '/^[a-zA-Z0-9._-]\{1,255\}$/p')" ] \ + || ( case "$POLICY" in none|may|encrypt|dane|dane-only|fingerprint|verify|secure) false ;; esac ) \ + || ( case "$POLICY" in fingerprint|verify|secure) false ;; esac && [ -n "$PARAMS" ] ) \ + || ( [ "$POLICY" = "fingerprint" ] && [ -z "$(echo "$PARAMS" | sed -ne '/^match=[a-fA-F0-9:|]\+$/p')" ] ) \ + || ( ! case "$POLICY" in verify|secure) false ;; esac && [ -z "$(echo "$PARAMS" | sed -ne '/^match=[a-zA-Z0-9:._-]\+$/p')" ] ) + then + echo "Invalid row: $LINE" >&2 + EXIT_CODE=1 + continue + fi + + echo "$TEMPLATE" | sed \ + -e "s/{domain}/$(echo "$DOMAIN" | sed -e 's/[\/&]/\\&/g')/g" \ + -e "s/{policy}/$(echo "$POLICY" | sed -e 's/[\/&]/\\&/g')/g" \ + -e "s/{params}/$(echo "$PARAMS" | sed -e 's/[\/&]/\\&/g')/g" +done < "$DATABASE" + +exit $EXIT_CODE +