#!/bin/bash
# =============================================================================
# SPF + DKIM Configuration
# Targets: Ubuntu 22.04/24.04/26.04, Trisquel 11/12/13
# Requires: PowerDNS running, Postfix configured, domain zone created
# =============================================================================
set -euo pipefail
# --- Configuration ---
domain_name="tuxmail.io"
# --- Debugging helpers ---
LOG_FILE="/var/log/spf-dkim-install.log"
PASS_COUNT=0
FAIL_COUNT=0
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
die() {
log "FATAL: $*" >&2
exit 1
}
check() {
local desc="$1"
local cmd="$2"
if eval "$cmd" &>/dev/null; then
log " [PASS] $desc"
((PASS_COUNT++)) || true
else
log " [FAIL] $desc"
((FAIL_COUNT++)) || true
fi
}
verify() {
log "--- VERIFICATION ---"
}
# =============================================================================
# Section 1: SPF Record
# =============================================================================
section_spf() {
log "=== SECTION: SPF ==="
if pdnsutil list-zone "$domain_name" 2>/dev/null | grep -q "v=spf1"; then
log "SPF record already exists, skipping"
else
pdnsutil add-record "$domain_name" @ TXT '"v=spf1 mx ~all"'
log "SPF record added"
fi
verify
check "SPF record exists in zone" "pdnsutil list-zone '$domain_name' | grep -q 'v=spf1'"
check "SPF resolvable locally" "dig @127.0.0.1 $domain_name TXT +short | grep -q 'v=spf1'"
}
# =============================================================================
# Section 2: Postfix SPF Policy Daemon
# =============================================================================
section_policyd_spf() {
log "=== SECTION: Postfix SPF Policy Daemon ==="
apt-get install -y postfix-policyd-spf-python
if ! grep -q "^policyd-spf" /etc/postfix/master.cf; then
cat >> /etc/postfix/master.cf << 'EOF'
policyd-spf unix - n n - 0 spawn
user=policyd-spf argv=/usr/bin/policyd-spf
EOF
log "policyd-spf added to master.cf"
else
log "policyd-spf already in master.cf"
fi
postconf -e "policyd-spf_time_limit = 3600"
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, check_policy_service unix:private/policyd-spf"
systemctl restart postfix
log "Postfix SPF policy configured"
verify
check "policyd-spf in master.cf" "grep -q '^policyd-spf' /etc/postfix/master.cf"
check "check_policy_service in restrictions" "postconf -h smtpd_recipient_restrictions | grep -q 'policyd-spf'"
check "Postfix is running" "systemctl is-active postfix"
}
# =============================================================================
# Section 3: OpenDKIM Installation + Configuration
# =============================================================================
section_opendkim() {
log "=== SECTION: OpenDKIM ==="
apt-get install -y opendkim opendkim-tools
if ! id -nG postfix | tr ' ' '\n' | grep -q '^opendkim$'; then
gpasswd -a postfix opendkim
log "postfix added to opendkim group"
else
log "postfix already in opendkim group"
fi
cat > /etc/opendkim.conf << 'EOF'
# OpenDKIM configuration
Syslog yes
LogWhy yes
PidFile /run/opendkim/opendkim.pid
# Signing configuration
SubDomains no
Mode sv
AutoRestart yes
AutoRestartRate 10/1M
Background yes
DNSTimeout 5
SignatureAlgorithm rsa-sha256
# Socket (inside Postfix chroot)
Socket local:/var/spool/postfix/opendkim/opendkim.sock
# User
UserID opendkim
# Tables
KeyTable refile:/etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
# Trusted hosts
ExternalIgnoreList /etc/opendkim/trusted.hosts
InternalHosts /etc/opendkim/trusted.hosts
EOF
log "opendkim.conf written"
if [ -f /etc/default/opendkim ]; then
sed -i 's|^SOCKET=.*|SOCKET="local:/var/spool/postfix/opendkim/opendkim.sock"|' /etc/default/opendkim
log "/etc/default/opendkim socket path set"
fi
# Ensure runtime directory for PID file
mkdir -p /run/opendkim
chown opendkim:opendkim /run/opendkim
# Socket directory inside Postfix chroot
mkdir -p /var/spool/postfix/opendkim
chown opendkim:opendkim /var/spool/postfix/opendkim
log "Runtime and socket directories created"
# Systemd override so /run/opendkim is created on every boot
mkdir -p /etc/systemd/system/opendkim.service.d/
cat > /etc/systemd/system/opendkim.service.d/override.conf << 'EOF'
[Service]
RuntimeDirectory=opendkim
RuntimeDirectoryMode=0755
RuntimeDirectoryOwner=opendkim
RuntimeDirectoryGroup=opendkim
TimeoutStartSec=30
EOF
systemctl daemon-reload
verify
check "opendkim.conf exists" "test -f /etc/opendkim.conf"
check "PidFile set in config" "grep -q '^PidFile' /etc/opendkim.conf"
check "Socket path points to chroot" "grep -q 'local:/var/spool/postfix/opendkim/opendkim.sock' /etc/opendkim.conf"
check "Runtime directory exists" "test -d /run/opendkim"
check "Socket directory exists" "test -d /var/spool/postfix/opendkim"
check "postfix in opendkim group" "id -nG postfix | tr ' ' '\n' | grep -q '^opendkim$'"
}
# =============================================================================
# Section 4: DKIM Keys + Tables
# =============================================================================
section_dkim_keys() {
log "=== SECTION: DKIM Keys ==="
mkdir -p /etc/opendkim/keys/"$domain_name"
chown -R opendkim:opendkim /etc/opendkim
chmod go-rw /etc/opendkim/keys
cat > /etc/opendkim/signing.table << EOF
*@$domain_name default._domainkey.$domain_name
*@*.$domain_name default._domainkey.$domain_name
EOF
log "signing.table written"
cat > /etc/opendkim/key.table << EOF
default._domainkey.$domain_name $domain_name:default:/etc/opendkim/keys/$domain_name/default.private
EOF
log "key.table written"
cat > /etc/opendkim/trusted.hosts << EOF
127.0.0.1
localhost
.$domain_name
EOF
log "trusted.hosts written"
if [ -f "/etc/opendkim/keys/$domain_name/default.private" ]; then
log "DKIM key already exists, skipping generation"
else
opendkim-genkey -b 2048 -d "$domain_name" -D "/etc/opendkim/keys/$domain_name" -s default -v
chown opendkim:opendkim "/etc/opendkim/keys/$domain_name/default.private"
chmod 600 "/etc/opendkim/keys/$domain_name/default.private"
log "DKIM key generated"
fi
verify
check "Private key exists" "test -f /etc/opendkim/keys/$domain_name/default.private"
check "Private key owned by opendkim" "stat -c '%U' /etc/opendkim/keys/$domain_name/default.private | grep -q '^opendkim$'"
check "Private key permissions are 600" "stat -c '%a' /etc/opendkim/keys/$domain_name/default.private | grep -q '^600$'"
check "signing.table exists" "test -f /etc/opendkim/signing.table"
check "key.table exists" "test -f /etc/opendkim/key.table"
check "trusted.hosts exists" "test -f /etc/opendkim/trusted.hosts"
}
# =============================================================================
# Section 5: DKIM DNS Record
# =============================================================================
section_dkim_dns() {
log "=== SECTION: DKIM DNS Record ==="
local public_key
public_key=$(grep -oP 'p=\K[A-Za-z0-9+/=]+' "/etc/opendkim/keys/$domain_name/default.txt")
if [ -z "$public_key" ]; then
die "Could not extract DKIM public key from /etc/opendkim/keys/$domain_name/default.txt"
fi
log "DKIM public key extracted (${#public_key} chars)"
if pdnsutil list-zone "$domain_name" 2>/dev/null | grep -q "default._domainkey.*v=DKIM1"; then
log "DKIM DNS record already exists, skipping"
else
pdnsutil add-record "$domain_name" default._domainkey TXT '"v=DKIM1; k=rsa; p='"${public_key}"'"'
log "DKIM DNS record added"
fi
verify
check "DKIM record in zone" "pdnsutil list-zone '$domain_name' | grep -q 'default._domainkey'"
check "DKIM resolvable locally" "dig @127.0.0.1 default._domainkey.$domain_name TXT +short | grep -q 'v=DKIM1'"
}
# =============================================================================
# Section 6: Postfix Milter Configuration
# =============================================================================
section_milter() {
log "=== SECTION: Postfix Milter ==="
postconf -e "milter_default_action = accept"
postconf -e "milter_protocol = 6"
postconf -e "smtpd_milters = local:opendkim/opendkim.sock"
postconf -e "non_smtpd_milters = \$smtpd_milters"
log "Postfix milter configured"
verify
check "smtpd_milters set" "postconf -h smtpd_milters | grep -q 'opendkim'"
check "milter_protocol set" "postconf -h milter_protocol | grep -q '^6$'"
}
# =============================================================================
# Section 7: Start Services
# =============================================================================
section_start() {
log "=== SECTION: Start Services ==="
# Ensure runtime dir exists (in case /run was cleared by reboot)
mkdir -p /run/opendkim
chown opendkim:opendkim /run/opendkim
systemctl enable opendkim
systemctl restart opendkim
systemctl restart postfix
sleep 3
verify
check "OpenDKIM is running" "systemctl is-active opendkim"
check "Postfix is running" "systemctl is-active postfix"
check "OpenDKIM socket exists" "test -S /var/spool/postfix/opendkim/opendkim.sock"
check "OpenDKIM PID file exists" "test -f /run/opendkim/opendkim.pid"
}
# =============================================================================
# Section 8: Final Verification
# =============================================================================
section_final_test() {
log "=== SECTION: Final Verification ==="
verify
check "SPF record resolvable" "dig @127.0.0.1 $domain_name TXT +short | grep -q 'v=spf1'"
check "DKIM record resolvable" "dig @127.0.0.1 default._domainkey.$domain_name TXT +short | grep -q 'v=DKIM1'"
check "OpenDKIM running" "systemctl is-active opendkim"
check "Postfix running" "systemctl is-active postfix"
check "Postfix can resolve domain via SQL" "postmap -q '$domain_name' mysql:/etc/postfix/sql/mysql_virtual_domains_maps.cf | grep -q '$domain_name'"
}
# =============================================================================
# Main
# =============================================================================
ALL_SECTIONS=(
section_spf
section_policyd_spf
section_opendkim
section_dkim_keys
section_dkim_dns
section_milter
section_start
section_final_test
)
if [ $# -gt 0 ]; then
for section in "$@"; do
case "$section" in
section_*)
log "Running: $section"
"$section"
;;
all)
for s in "${ALL_SECTIONS[@]}"; do
log "Running: $s"
"$s"
done
;;
*)
die "Unknown section: $section"
;;
esac
done
else
for s in "${ALL_SECTIONS[@]}"; do
log "Running: $s"
"$s"
done
fi
# --- Summary ---
log "=============================================="
log " PASSED: $PASS_COUNT"
log " FAILED: $FAIL_COUNT"
log "=============================================="
if [ "$FAIL_COUNT" -gt 0 ]; then
log "WARNING: Some checks failed. Review [FAIL] entries above."
log "Full log: $LOG_FILE"
exit 1
else
log "All checks passed."
fi
log "=== SPF & DKIM configuration complete ==="
