From ecede40f7f64aa457833ff61ced3d7cfc154d27a Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Mon, 12 Oct 2026 22:14:55 +1100 Subject: [PATCH] Scan inbound and outbound via the smarthost only, harden rspamd and Valkey - tag inbound instead of rejecting, keep virus reject and tempfail when ClamAV fails - score inbound against the first hop before the edge relays - sign and skip GPT only for smarthost mynetworks sources - elect the highest-offset Valkey on full restart, cap memory at 384mb - read a hashed controller password --- apps/base/mail/kustomization.yaml | 5 ++ apps/base/mail/rspamd-deployment.yaml | 24 +------ apps/base/mail/rspamd-valkey.yaml | 9 +-- apps/base/mail/rspamd-valkey/start.sh | 74 +++++++++++++++++---- apps/base/mail/rspamd-vault.yaml | 2 +- apps/base/mail/rspamd/dkim_signing.conf | 3 +- apps/base/mail/rspamd/external_relay.conf | 8 +++ apps/base/mail/rspamd/force_actions.conf | 7 ++ apps/base/mail/rspamd/gpt.conf | 5 +- apps/base/mail/rspamd/local_networks.map | 30 +++++++++ apps/base/mail/rspamd/milter_headers.conf | 2 + apps/base/mail/rspamd/options.inc | 3 +- apps/base/mail/rspamd/settings.conf | 19 ++++++ apps/base/mail/rspamd/worker-controller.inc | 4 +- 14 files changed, 146 insertions(+), 49 deletions(-) create mode 100644 apps/base/mail/rspamd/external_relay.conf create mode 100644 apps/base/mail/rspamd/force_actions.conf create mode 100644 apps/base/mail/rspamd/local_networks.map create mode 100644 apps/base/mail/rspamd/milter_headers.conf create mode 100644 apps/base/mail/rspamd/settings.conf diff --git a/apps/base/mail/kustomization.yaml b/apps/base/mail/kustomization.yaml index fd9d126..0a73449 100644 --- a/apps/base/mail/kustomization.yaml +++ b/apps/base/mail/kustomization.yaml @@ -23,11 +23,16 @@ configMapGenerator: - rspamd/actions.conf - rspamd/antivirus.conf - rspamd/dkim_signing.conf + - rspamd/external_relay.conf + - rspamd/force_actions.conf - rspamd/gpt.conf - rspamd/greylist.conf - rspamd/history_redis.conf + - rspamd/local_networks.map + - rspamd/milter_headers.conf - rspamd/options.inc - rspamd/redis.conf + - rspamd/settings.conf - rspamd/spamassassin.conf - rspamd/worker-controller.inc - rspamd/worker-normal.inc diff --git a/apps/base/mail/rspamd-deployment.yaml b/apps/base/mail/rspamd-deployment.yaml index dd0c933..2764651 100644 --- a/apps/base/mail/rspamd-deployment.yaml +++ b/apps/base/mail/rspamd-deployment.yaml @@ -46,11 +46,11 @@ spec: secretKeyRef: name: rspamd-litellm key: key - - name: RSPAMD_CONTROLLER_PASSWORD + - name: RSPAMD_CONTROLLER_PASSWORD_HASH valueFrom: secretKeyRef: name: rspamd-controller - key: password + key: password_hash ports: - containerPort: 11332 name: milter @@ -121,7 +121,7 @@ spec: matchLabels: app.kubernetes.io/name: rspamd --- -# Milter for the smarthost (rspamd.mail.svc:11332); controller/web UI is in-cluster only. +# Milter for the smarthost only (rspamd.mail.svc:11332); controller/web UI is in-cluster only. apiVersion: v1 kind: Service metadata: @@ -138,21 +138,3 @@ spec: - name: controller port: 11334 targetPort: controller ---- -# Milter for the DMZ gateways over WireGuard. -apiVersion: v1 -kind: Service -metadata: - name: rspamd-milter - namespace: mail - annotations: - purelb.io/addresses: 198.18.200.17 - purelb.io/service-group: common -spec: - type: LoadBalancer - selector: - app.kubernetes.io/name: rspamd - ports: - - name: milter - port: 11332 - targetPort: milter diff --git a/apps/base/mail/rspamd-valkey.yaml b/apps/base/mail/rspamd-valkey.yaml index ecf49e1..4d9cd67 100644 --- a/apps/base/mail/rspamd-valkey.yaml +++ b/apps/base/mail/rspamd-valkey.yaml @@ -1,9 +1,9 @@ --- # Sentinel-managed Valkey for rspamd (bayes, history, ratelimit, gpt cache). # rspamd has no Redis Cluster support, so the valkey-operator (cluster mode) -# cannot be used. Pod 0 seeds a fresh set; afterwards start.sh follows the -# master the sentinels report. Hostnames carry a trailing dot so lookups of a -# deleted pod fail fast instead of walking the search list (sentinel TILT). +# cannot be used. start.sh follows the master the sentinels report; on a full +# restart the highest replication offset wins. Hostnames carry a trailing dot so +# lookups of a deleted pod fail fast instead of walking the search list (sentinel TILT). apiVersion: apps/v1 kind: StatefulSet metadata: @@ -13,6 +13,7 @@ metadata: configmap.reloader.stakater.com/reload: "rspamd-valkey-scripts" spec: serviceName: rspamd-valkey + podManagementPolicy: Parallel replicas: 3 selector: matchLabels: @@ -51,7 +52,7 @@ spec: protocol: TCP readinessProbe: exec: - command: ["valkey-cli", "ping"] + command: ["sh", "-c", "valkey-cli info replication | grep -q '^master_host:127.0.0.1' && exit 1; valkey-cli ping | grep -q PONG"] initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: diff --git a/apps/base/mail/rspamd-valkey/start.sh b/apps/base/mail/rspamd-valkey/start.sh index 9609465..b30cdce 100755 --- a/apps/base/mail/rspamd-valkey/start.sh +++ b/apps/base/mail/rspamd-valkey/start.sh @@ -1,20 +1,66 @@ #!/bin/sh -# Usage: start.sh valkey|sentinel. Ask peer sentinels for the current master; -# with no answer (fresh cluster) pod 0 is the master. +# Usage: start.sh valkey|sentinel. valkey boots read-only as a replica of +# 127.0.0.1:1 ("undecided") so peers can compare offsets; decide() then follows +# the sentinels' master, a live peer master, or the highest-offset node. set -eu name="$(hostname)" sts="${name%-*}" self="${name}${DOMAIN_SUFFIX}" -master="" -for i in 0 1 2; do - peer="${sts}-${i}${DOMAIN_SUFFIX}" - [ "$peer" = "$self" ] && continue - master="$(timeout 3 valkey-cli -h "$peer" -p 26379 --raw sentinel get-master-addr-by-name rspamd 2>/dev/null | head -n1 || true)" - [ -n "$master" ] && break -done -[ -n "$master" ] || master="${sts}-0${DOMAIN_SUFFIX}" + +info() { + timeout 3 valkey-cli -h "$1" info replication 2>/dev/null | tr -d '\r' || true +} + +field() { + printf '%s\n' "$1" | sed -n "s/^$2://p" +} + +undecided() { + [ "$(field "$1" master_host)" = 127.0.0.1 ] +} + +choose_master() { + deadline=$(($(date +%s) + 60)) + while :; do + best="" + best_off=-1 + answered=0 + for i in 0 1 2; do + peer="${sts}-${i}${DOMAIN_SUFFIX}" + if [ "$peer" = "$self" ]; then + r="$(info 127.0.0.1)" + else + m="$(timeout 3 valkey-cli -h "$peer" -p 26379 --raw sentinel get-master-addr-by-name rspamd 2>/dev/null | head -n1 || true)" + [ -n "$m" ] && { echo "$m"; return; } + r="$(info "$peer")" + [ -n "$r" ] || continue + answered=$((answered + 1)) + [ "$(field "$r" role)" = master ] && { echo "$peer"; return; } + fi + undecided "$r" || continue + off="$(field "$r" master_repl_offset)" + [ "${off:-0}" -gt "$best_off" ] && best="$peer" best_off="${off:-0}" + done + if [ "$best" = "$self" ] && { [ "$answered" -eq 2 ] || [ "$(date +%s)" -ge "$deadline" ]; }; then + echo "$self" + return + fi + sleep 3 + done +} + +decide() { + until valkey-cli ping >/dev/null 2>&1; do sleep 1; done + master="$(choose_master)" + target="$master 6379" + [ "$master" = "$self" ] && target="no one" + until valkey-cli replicaof $target; do sleep 1; done +} if [ "$1" = sentinel ]; then + until r="$(info 127.0.0.1)" && [ -n "$(field "$r" role)" ] && ! undecided "$r"; do sleep 2; done + master="$self" + [ "$(field "$r" role)" = master ] || master="$(field "$r" master_host)" cat > /sentinel/sentinel.conf <