Rockstable Wiki:

rspamd

About

Fast, free and open-source spam filtering system.

Rspamd filtering system is designed to be fast, modular and easily scalable system. Rspamd core is written in C language using event driven processing model. Plugins for rspamd can be written in Lua programming language. Rspamd is designed to process connections completely asynchronous and do not block anywhere in code.

Please see:

Installation

   1 #aptitude install rspamd redis
   2 ### BETTER FROM BACKPORTS
   3 aptitude install -t buster-backports redis rspamd

Show running configuration

   1 rspamadm configdump|less

Ports

https://rspamd.com/doc/workers/

Socket

Name

Beschreibung

Status

127.0.0.1:6379

redis

Key-Value-Storage

Aktiv

127.0.0.1:11332

rspamd worker-proxy

handles requests forwarding and milter protocol

Aktiv

127.0.0.1:11333

rspamd worker-normal

scan mail messages

Aktiv

127.0.0.1:11334

rspamd worker-controller

configuration actions

Aktiv

127.0.0.1:11335

rspamd worker-fuzzy

stores fuzzy hashes

Inaktiv

Testing the spam filter - GTUBE

The GTUBE ("Generic Test for Unsolicited Bulk Email") is a 68-byte test string used to test anti-spam systems, in particular those based on SpamAssassin. In SpamAssassin, it carries an antispam score of 1000 by default, which would be sufficient to trigger any installation.

Wike En GTUBE

GTUBE pattern

   1 XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

   1 ADDRESS="user@domain.tld"
   2 GTUBE='XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X'
   3 echo "$GTUBE" \
   4         |mail -s "Test $(date '+%F %T')" \
   5         "$ADDRESS"

Configuration

Unix Permissions

Adjust Unix-Permissions

   1 find \
   2         /etc/rspamd/local.d \
   3         /var/lib/rspamd/dkim \
   4         -type f \
   5         -exec chown root:_rspamd {} \; \
   6         -exec chmod 0640 {} \;

Redis backend

Older rspamd versions default to SQLite backends, nowadays redis hasbecome the default backend, but may need redundancy.

Ports

Redis OS optimizations

Redis complains to get some optimizations.

29516:M 22 Oct 2020 17:54:01.615 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
29516:M 22 Oct 2020 17:54:01.615 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').

Redis replication

The idea is to have two servers.

Node1 is a writeable master,
node2 is a read-only slave replicating from master.

Any rspamd instance should write to node1, read from both and failover to node2 if node1 is gone. For higher redundancy, use sentinel.

The detailed redis.conf is lengthy with about 1865 lines of code. I'll stick to the changes from the defaults for my setup.

TLS enablement and hardening not yet included. First need a certificate.

Redis replication master

/etc/redis/redis.conf

   1 ################################## NETWORK #####################################
   2 
   3 # By default, if no "bind" configuration directive is specified, Redis listens
   4 # for connections from all the network interfaces available on the server.
   5 # It is possible to listen to just one or multiple selected interfaces using
   6 # the "bind" configuration directive, followed by one or more IP addresses.
   7 #
   8 # Examples:
   9 #
  10 # bind 192.168.1.100 10.0.0.1
  11 # bind 127.0.0.1 ::1
  12 #
  13 # ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
  14 # internet, binding to all the interfaces is dangerous and will expose the
  15 # instance to everybody on the internet. So by default we uncomment the
  16 # following bind directive, that will force Redis to listen only into
  17 # the IPv4 loopback interface address (this means Redis will be able to
  18 # accept connections only from clients running into the same computer it
  19 # is running).
  20 #
  21 # IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
  22 # JUST COMMENT THE FOLLOWING LINE.
  23 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  24 #bind 127.0.0.1 ::1
  25 bind 127.0.0.1 ::1 IP.ADD.RE.SS
  26 
  27 # If the master is password protected (using the "requirepass" configuration
  28 # directive below) it is possible to tell the replica to authenticate before
  29 # starting the replication synchronization process, otherwise the master will
  30 # refuse the replica request.
  31 #
  32 # masterauth <master-password>
  33 #
  34 # However this is not enough if you are using Redis ACLs (for Redis version
  35 # 6 or greater), and the default user is not capable of running the PSYNC
  36 # command and/or other commands needed for replication. In this case it's
  37 # better to configure a special user to use with replication, and specify the
  38 # masteruser configuration as such:
  39 #
  40 # masteruser <username>
  41 #
  42 # When masteruser is specified, the replica will authenticate against its
  43 # master using the new AUTH form: AUTH <username> <password>.
  44 masteruser replicator
  45 masterauth zail1vaiThihair6al6HohsetohaiNgood8eighaibakethee4Fahgeingoaz5uo
  46 
  47 # Using an external ACL file
  48 #
  49 # Instead of configuring users here in this file, it is possible to use
  50 # a stand-alone file just listing users. The two methods cannot be mixed:
  51 # if you configure users here and at the same time you activate the exteranl
  52 # ACL file, the server will refuse to start.
  53 #
  54 # The format of the external ACL user file is exactly the same as the
  55 # format that is used inside redis.conf to describe users.
  56 #
  57 aclfile /etc/redis/users.acl
  58 

This file contains the credentials and is going to be synced between the nodes. Unfortunately rspamd does not yet support redis acls and the user default must be configured.
/etc/redis/users.acl

   1 user replicator on +@all ~* >zail1vaiThihair6al6HohsetohaiNgood8eighaibakethee4Fahgeingoaz5uo
   2 user default    on +@all ~* >rau4aivie9she0thoo1JeiWul5doovohTh3dah5leinee3ahvie7aino6kee7Cha

Make sure /etc/redis/users.acl does not contain tabulators.

29516:M 22 Oct 2020 17:54:01.615 # Aborting Redis startup because of ACL errors: '/etc/redis/users.acl:1: username 'replicator  on' contains invalid characters. '/etc/redis/users.acl:2: username 'default     on' contains invalid characters. WARNING: ACL errors detected, no change to the previously active ACL rules was performed

node1 than node2

   1 systemctl stop rspamd.service
   2 systemctl restart redis-server.service
   3 systemctl start rspamd.service

Check replication

Primary

   1 # redis-cli
   2 127.0.0.1:6379> AUTH secure_long_password
   3 OK
   4 ### WITHOUT A REPLICATING NODE
   5 127.0.0.1:6379> ROLE
   6 1) "master"
   7 2) (integer) 0
   8 3) (empty array)
   9 ### WITH A REPLICATING NODE
  10 127.0.0.1:6379> ROLE
  11 1) "master"
  12 2) (integer) 112
  13 3) 1) 1) "192.168.1.42"
  14       2) "6379"
  15       3) "112"
  16 127.0.0.1:6379> INFO

Secondary

   1 # redis-cli
   2 127.0.0.1:6379> AUTH secure_long_password
   3 OK
   4 ### REPLICATION BROKEN
   5 127.0.0.1:6379> ROLE
   6 1) "slave"
   7 2) "192.168.0.11"
   8 3) (integer) 6379
   9 4) "connect"
  10 5) (integer) -1
  11 ### REPLICATION WORKING
  12 127.0.0.1:6379> ROLE
  13 1) "slave"
  14 2) "80.64.180.48"
  15 3) (integer) 6379
  16 4) "connected"
  17 5) (integer) 70
  18 127.0.0.1:6379> INFO

Redis replication slave

Additionally the replication slave has been pointed to the master.

/etc/redis/redis.conf

   1 ################################# REPLICATION #################################
   2 
   3 # Master-Replica replication. Use replicaof to make a Redis instance a copy of
   4 # another Redis server. A few things to understand ASAP about Redis replication.
   5 #
   6 #   +------------------+      +---------------+
   7 #   |      Master      | ---> |    Replica    |
   8 #   | (receive writes) |      |  (exact copy) |
   9 #   +------------------+      +---------------+
  10 #
  11 # 1) Redis replication is asynchronous, but you can configure a master to
  12 #    stop accepting writes if it appears to be not connected with at least
  13 #    a given number of replicas.
  14 # 2) Redis replicas are able to perform a partial resynchronization with the
  15 #    master if the replication link is lost for a relatively small amount of
  16 #    time. You may want to configure the replication backlog size (see the next
  17 #    sections of this file) with a sensible value depending on your needs.
  18 # 3) Replication is automatic and does not need user intervention. After a
  19 #    network partition replicas automatically try to reconnect to masters
  20 #    and resynchronize with them.
  21 #
  22 # replicaof <masterip> <masterport>
  23 replicaof 192.168.0.11 6379

Rspamd client

Adjust the local configuration of each rspamd service
/etc/rspamd/local.d/redis.conf

   1 write_servers = "mail1.example.com:6379";
   2 #write_servers = "192.168.0.11";
   3 read_servers = "master-slave:mail1.example.com:6379:30,mail3.example.com:6379:10";
   4 #read_servers = "master-slave:192.168.0.11:6379:30;192.168.0.13:6379:10";
   5 #read_servers = "master-slave:mail3.example.com:6379:30,mail1.example.com:6379:10";
   6 #read_servers = "master-slave:192.168.0.13:6379:30;192.168.0.11:6379:10";
   7 password = rau4aivie9she0thoo1JeiWul5doovohTh3dah5leinee3ahvie7aino6kee7Cha

Redis redundancy with sentinel

With multiple nodes it is possible to achieve

Links

There is no setup with just 2 sentinels
src/sentinel.c

   1 // because it requires quorum ≥ 1.
   2 1645        if (quorum <= 0) return "Quorum must be 1 or greater.";
   3 
   4 // because there must be more votes than 50% of the nodes
   5 // and the number of votes must be greater than the configured quorum).
   6 4026     voters_quorum = voters/2+1;
   7 4027     if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
   8 4028         winner = NULL;

Example: 2 nodes, 1 down

   1 quorum=1
   2 voters=2
   3 voters_quorum=2/2+1=2
   4 max_votes=1
   5 max_votes < voters_quorum = 1 < 2 
   6 -> winner = NULL;

Install redis-sentinel

   1 aptitude install -t buster-backports redis-sentinel

Changes to sentinel.conf

/etc/redis/sentinel.conf

   1 # Example sentinel.conf
   2 
   3 # *** IMPORTANT ***
   4 #
   5 # By default Sentinel will not be reachable from interfaces different than
   6 # localhost, either use the 'bind' directive to bind to a list of network
   7 # interfaces, or disable protected mode with "protected-mode no" by
   8 # adding it to this configuration file.
   9 #
  10 # Before doing that MAKE SURE the instance is protected from the outside
  11 # world via firewalling or other means.
  12 #
  13 # For example you may use one of the following:
  14 #
  15 # bind 127.0.0.1 ::1 192.168.1.1
  16 bind 127.0.0.1 ::1 192.168.0.11
  17 #
  18 # protected-mode no
  19 
  20 # port <sentinel-port>
  21 # The port that this sentinel instance will run on
  22 #port 26379
  23 port 5000
  24 
  25 # By default Redis Sentinel does not run as a daemon. Use 'yes' if you need it.
  26 # Note that Redis will write a pid file in /var/run/redis-sentinel.pid when
  27 # daemonized.
  28 daemonize yes
  29 
  30 # When running daemonized, Redis Sentinel writes a pid file in
  31 # /var/run/redis-sentinel.pid by default. You can specify a custom pid file
  32 # location here.
  33 pidfile "/var/run/sentinel/redis-sentinel.pid"
  34 
  35 # Specify the log file name. Also the empty string can be used to force
  36 # Sentinel to log on the standard output. Note that if you use standard
  37 # output for logging but daemonize, logs will be sent to /dev/null
  38 logfile "/var/log/redis/redis-sentinel.log"
  39 
  40 # sentinel announce-ip <ip>
  41 # sentinel announce-port <port>
  42 #
  43 # The above two configuration directives are useful in environments where,
  44 # because of NAT, Sentinel is reachable from outside via a non-local address.
  45 #
  46 # When announce-ip is provided, the Sentinel will claim the specified IP address
  47 # in HELLO messages used to gossip its presence, instead of auto-detecting the
  48 # local address as it usually does.
  49 #
  50 # Similarly when announce-port is provided and is valid and non-zero, Sentinel
  51 # will announce the specified TCP port.
  52 #
  53 # The two options don't need to be used together, if only announce-ip is
  54 # provided, the Sentinel will announce the specified IP and the server port
  55 # as specified by the "port" option. If only announce-port is provided, the
  56 # Sentinel will announce the auto-detected local IP and the specified port.
  57 #
  58 # Example:
  59 #
  60 # sentinel announce-ip 1.2.3.4
  61 
  62 # dir <working-directory>
  63 # Every long running process should have a well-defined working directory.
  64 # For Redis Sentinel to chdir to /tmp at startup is the simplest thing
  65 # for the process to don't interfere with administrative tasks such as
  66 # unmounting filesystems.
  67 dir "/var/lib/redis"
  68 
  69 # sentinel monitor <master-name> <ip> <redis-port> <quorum>
  70 #
  71 # Tells Sentinel to monitor this master, and to consider it in O_DOWN
  72 # (Objectively Down) state only if at least <quorum> sentinels agree.
  73 #
  74 # Note that whatever is the ODOWN quorum, a Sentinel will require to
  75 # be elected by the majority of the known Sentinels in order to
  76 # start a failover, so no failover can be performed in minority.
  77 #
  78 # Replicas are auto-discovered, so you don't need to specify replicas in
  79 # any way. Sentinel itself will rewrite this configuration file adding
  80 # the replicas using additional configuration options.
  81 # Also note that the configuration file is rewritten when a
  82 # replica is promoted to master.
  83 #
  84 # Note: master name should not include special characters or spaces.
  85 # The valid charset is A-z 0-9 and the three characters ".-_".
  86 sentinel myid b7e6832629034548dd2d1e63cbac146b9dfd189b
  87 
  88 # sentinel auth-pass <master-name> <password>
  89 #
  90 # Set the password to use to authenticate with the master and replicas.
  91 # Useful if there is a password set in the Redis instances to monitor.
  92 #
  93 # Note that the master password is also used for replicas, so it is not
  94 # possible to set a different password in masters and replicas instances
  95 # if you want to be able to monitor these instances with Sentinel.
  96 #
  97 # However you can have Redis instances without the authentication enabled
  98 # mixed with Redis instances requiring the authentication (as long as the
  99 # password set is the same for all the instances requiring the password) as
 100 # the AUTH command will have no effect in Redis instances with authentication
 101 # switched off.
 102 #
 103 # Example:
 104 #
 105 # sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
 106 
 107 # sentinel auth-user <master-name> <username>
 108 #
 109 # This is useful in order to authenticate to instances having ACL capabilities,
 110 # that is, running Redis 6.0 or greater. When just auth-pass is provided the
 111 # Sentinel instance will authenticate to Redis using the old "AUTH <pass>"
 112 # method. When also an username is provided, it will use "AUTH <user> <pass>".
 113 # In the Redis servers side, the ACL to provide just minimal access to
 114 # Sentinel instances, should be configured along the following lines:
 115 #
 116 #     user sentinel-user >somepassword +client +subscribe +publish \
 117 #                        +ping +info +multi +slaveof +config +client +exec on
 118 
 119 # sentinel down-after-milliseconds <master-name> <milliseconds>
 120 #
 121 # Number of milliseconds the master (or any attached replica or sentinel) should
 122 # be unreachable (as in, not acceptable reply to PING, continuously, for the
 123 # specified period) in order to consider it in S_DOWN state (Subjectively
 124 # Down).
 125 #
 126 # Default is 30 seconds.
 127 sentinel deny-scripts-reconfig yes
 128 
 129 # requirepass <password>
 130 #
 131 # You can configure Sentinel itself to require a password, however when doing
 132 # so Sentinel will try to authenticate with the same password to all the
 133 # other Sentinels. So you need to configure all your Sentinels in a given
 134 # group with the same "requirepass" password. Check the following documentation
 135 # for more info: https://redis.io/topics/sentinel
 136 
 137 # sentinel parallel-syncs <master-name> <numreplicas>
 138 #
 139 # How many replicas we can reconfigure to point to the new replica simultaneously
 140 # during the failover. Use a low number if you use the replicas to serve query
 141 # to avoid that all the replicas will be unreachable at about the same
 142 # time while performing the synchronization with the master.
 143 sentinel monitor mymaster 127.0.0.1 6379 2
 144 
 145 # sentinel failover-timeout <master-name> <milliseconds>
 146 #
 147 # Specifies the failover timeout in milliseconds. It is used in many ways:
 148 #
 149 # - The time needed to re-start a failover after a previous failover was
 150 #   already tried against the same master by a given Sentinel, is two
 151 #   times the failover timeout.
 152 #
 153 # - The time needed for a replica replicating to a wrong master according
 154 #   to a Sentinel current configuration, to be forced to replicate
 155 #   with the right master, is exactly the failover timeout (counting since
 156 #   the moment a Sentinel detected the misconfiguration).
 157 #
 158 # - The time needed to cancel a failover that is already in progress but
 159 #   did not produced any configuration change (SLAVEOF NO ONE yet not
 160 #   acknowledged by the promoted replica).
 161 #
 162 # - The maximum time a failover in progress waits for all the replicas to be
 163 #   reconfigured as replicas of the new master. However even after this time
 164 #   the replicas will be reconfigured by the Sentinels anyway, but not with
 165 #   the exact parallel-syncs progression as specified.
 166 #
 167 # Default is 3 minutes.
 168 sentinel config-epoch mymaster 0
 169 
 170 # SCRIPTS EXECUTION
 171 #
 172 # sentinel notification-script and sentinel reconfig-script are used in order
 173 # to configure scripts that are called to notify the system administrator
 174 # or to reconfigure clients after a failover. The scripts are executed
 175 # with the following rules for error handling:
 176 #
 177 # If script exits with "1" the execution is retried later (up to a maximum
 178 # number of times currently set to 10).
 179 #
 180 # If script exits with "2" (or an higher value) the script execution is
 181 # not retried.
 182 #
 183 # If script terminates because it receives a signal the behavior is the same
 184 # as exit code 1.
 185 #
 186 # A script has a maximum running time of 60 seconds. After this limit is
 187 # reached the script is terminated with a SIGKILL and the execution retried.
 188 
 189 # NOTIFICATION SCRIPT
 190 #
 191 # sentinel notification-script <master-name> <script-path>
 192 #
 193 # Call the specified notification script for any sentinel event that is
 194 # generated in the WARNING level (for instance -sdown, -odown, and so forth).
 195 # This script should notify the system administrator via email, SMS, or any
 196 # other messaging system, that there is something wrong with the monitored
 197 # Redis systems.
 198 #
 199 # The script is called with just two arguments: the first is the event type
 200 # and the second the event description.
 201 #
 202 # The script must exist and be executable in order for sentinel to start if
 203 # this option is provided.
 204 #
 205 # Example:
 206 #
 207 # sentinel notification-script mymaster /var/redis/notify.sh
 208 
 209 # CLIENTS RECONFIGURATION SCRIPT
 210 #
 211 # sentinel client-reconfig-script <master-name> <script-path>
 212 #
 213 # When the master changed because of a failover a script can be called in
 214 # order to perform application-specific tasks to notify the clients that the
 215 # configuration has changed and the master is at a different address.
 216 #
 217 # The following arguments are passed to the script:
 218 #
 219 # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
 220 #
 221 # <state> is currently always "failover"
 222 # <role> is either "leader" or "observer"
 223 #
 224 # The arguments from-ip, from-port, to-ip, to-port are used to communicate
 225 # the old address of the master and the new address of the elected replica
 226 # (now a master).
 227 #
 228 # This script should be resistant to multiple invocations.
 229 #
 230 # Example:
 231 #
 232 # sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
 233 
 234 # SECURITY
 235 #
 236 # By default SENTINEL SET will not be able to change the notification-script
 237 # and client-reconfig-script at runtime. This avoids a trivial security issue
 238 # where clients can set the script to anything and trigger a failover in order
 239 # to get the program executed.
 240 
 241 sentinel leader-epoch mymaster 0
 242 
 243 # REDIS COMMANDS RENAMING
 244 #
 245 # Sometimes the Redis server has certain commands, that are needed for Sentinel
 246 # to work correctly, renamed to unguessable strings. This is often the case
 247 # of CONFIG and SLAVEOF in the context of providers that provide Redis as
 248 # a service, and don't want the customers to reconfigure the instances outside
 249 # of the administration console.
 250 #
 251 # In such case it is possible to tell Sentinel to use different command names
 252 # instead of the normal ones. For example if the master "mymaster", and the
 253 # associated replicas, have "CONFIG" all renamed to "GUESSME", I could use:
 254 #
 255 # SENTINEL rename-command mymaster CONFIG GUESSME
 256 #
 257 # After such configuration is set, every time Sentinel would use CONFIG it will
 258 # use GUESSME instead. Note that there is no actual need to respect the command
 259 # case, so writing "config guessme" is the same in the example above.
 260 #
 261 # SENTINEL SET can also be used in order to perform this configuration at runtime.
 262 #
 263 # In order to set a command back to its original name (undo the renaming), it
 264 # is possible to just rename a command to itsef:
 265 #
 266 # SENTINEL rename-command mymaster CONFIG CONFIG
 267 # Generated by CONFIG REWRITE
 268 protected-mode no
 269 user default on nopass ~* +@all
 270 sentinel current-epoch 0

rspamd client

Activate usage of sentinels in rspamd to determine write_servers and read_servers
/etc/rspamd/local.d/redis.conf

   1 sentinels = "192.168.0.11, 192.168.0.12, 192.168.0.13"; # Servers list (default port 5000)                                                                
   2 sentinel_watch_time = 1min;                             # How often Rspam will query sentinels for masters and slaves                                     
   3 sentinel_masters_pattern = "^mymaster.*$";              # Defines masters pattern to match in Lua syntax (no pattern means all masters)                   
   4 servers = "192.168.0.11, 192.168.0.12, 192.168.0.13";   # Defines the initial set of servers that would be used if no sentinel can be accessed            
   5 

Always fix #Unix Permissions.

worker-controller

Create passwords for read-only and write access with rspamadm pw

   1 rspamadm pw
   2 Enter passphrase:
   3 $2$dc3s6ifn37iccs1tmnk74hzwxb6gpcbn$qak7a9wy1wcom7f99hwtxf1t3nxcht5pfsyd4t7w46qxrqofswgy
   4 rspamadm pw
   5 Enter passphrase:
   6 $2$6zfmwfmsebg31wwtrkxuezjuiiymddhi$6mbejg5camddsg1fqeu1oh1jnd15p1qsk1jbfkyzj97b5qbb8q5b

Change passwords for website
/etc/rspamd/local.d/worker-controller.inc

   1 ### RSPAMD local.d config
   2 
   3 count = 1;
   4 
   5 # password for read-only commands
   6 password = "$2$dc3s6ifn37iccs1tmnk74hzwxb6gpcbn$qak7a9wy1wcom7f99hwtxf1t3nxcht5pfsyd4t7w46qxrqofswgy";
   7 # password for write commands
   8 enable_password: "$2$6zfmwfmsebg31wwtrkxuezjuiiymddhi$6mbejg5camddsg1fqeu1oh1jnd15p1qsk1jbfkyzj97b5qbb8q5b";
   9 # list or map with IP addresses that are treated as secure
  10 # so all commands are allowed from these IPs without passwords
  11 secure_ip = "127.0.0.1";
  12 secure_ip = "::1";
  13 # directory where interface static files are placed (usually ${WWWDIR})
  14 static_dir = "${WWWDIR}";
  15 # path where controller save persistent stats about rspamd
  16 # (such as scanned messages count)
  17 #stats_path =

Always fix #Unix Permissions.

Check Webfrontend (over ssh-tunnel)

   1 ssh -L 8080:localhost:11334 mx1.rockstable.it

Then simply call in your browser: http://localhost:8080/

DomainKeys Identified Mail (DKIM)

Configure DKIM

   1 DOMAIN="rockstable.it"
   2 SELECTOR="$(date +%Y)"
   3 DKIM_PATH="/var/lib/rspamd/dkim"
   4 DKIM_FILE="$DKIM_PATH/$DOMAIN.$SELECTOR"
   5 
   6 install -o root -g _rspamd -m 2710 \
   7         -d "$DKIM_PATH"
   8 rspamadm dkim_keygen \
   9         -b 2048 -s "$SELECTOR" \
  10         -k "$DKIM_FILE".pem \
  11         |tee "$DKIM_FILE".txt
  12 
  13 2019._domainkey IN TXT ( "v=DKIM1; k=rsa; "
  14         "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm1b+tmkhLzUAW0p2PpePX5XOXBqPobfUnkyEmcc4QJ6WArk67m8CSO1KStZMbK2PC81hWtSoMf4S0qprfn30ARRvlqMJYGuyE26Tk4yYZvG7oS6tszEB9HKe88SEjD6Ax6YMFtcNAZctd9kcjfLZI7YmTJXKkGaxJH8qNklr5oXco5Ka2ZQuSQKiOCiDoffeck6yQSNdMIH77GXzw"
  15         "K+g2v6dab57FSOFzbNXWEkdfqZd9Ig12/vLBhPQ4gWxlpeHKR/y6LLZXfi3xBS5XtWVrt6PHqYhsLt39X0lwJosoFGcq6AfUaCgTljHq1fhkvR5cS+JJ14dpTZ6cqnbii9V2QIDAQAB"
  16 ) ;

Or as a shell script
/usr/local/sbin/generate_dkim.sh

   1 #!/bin/bash
   2 
   3 if [ "${#@}" -ge "1" ]; then
   4         DOMAINS=( "$@" )
   5 else
   6         echo "No domains given as arguments. Exiting…"
   7         exit 1
   8 fi
   9 
  10 SELECTOR="$(date +%Y)"
  11 DKIM_PATH="/var/lib/rspamd/dkim"
  12 RSPAMD_GRP="_rspamd"
  13 [ -d "$DKIM_PATH" ] || \
  14         install -o root -g "$RSPAMD_GRP" -m 2710 \
  15         -d "$DKIM_PATH"
  16 
  17 for DOMAIN in "${DOMAINS[@]}"; do
  18         echo
  19         echo "Processing domain '$DOMAIN'"
  20 
  21         DKIM_FILE="$DKIM_PATH/$DOMAIN.$SELECTOR"
  22 
  23         if [ -f "$DKIM_FILE.pem" ] || [ -f "$DKIM_FILE.txt" ] ; then
  24                 echo "DKIM file for domain '$DOMAIN' already exists. Skipping…"
  25         else
  26                 rspamadm dkim_keygen \
  27                         -b 2048 -s "$SELECTOR" \
  28                         -k "$DKIM_FILE".pem \
  29                         |tee "$DKIM_FILE".txt
  30                 if [ "$?" ]; then
  31                         echo "DKIM file for domain '$DOMAIN' has been created."
  32                 else
  33                         echo "DKIM file for domain '$DOMAIN' could not be created. Error …"
  34                 fi
  35         fi
  36         find "$DKIM_PATH" -type f -name "$DOMAIN.$SELECTOR*"
  37 done
  38 
  39 echo "Fixing unix permissions"
  40 find \
  41         /etc/rspamd/local.d \
  42         /var/lib/rspamd/dkim \
  43         -type f \
  44         -exec chown root:"$RSPAMD_GRP" {} \; \
  45         -exec chmod 0640 {} \;

Use it like

   1 ### SIMPLE
   2 dkim_generate.sh domain1.tld domain2.tld domain3.tld
   3 ### OR WITH INPUT FROM FILE WITH XARGS
   4 grep '^\s*[^#]' /etc/postfix/virtual_mailbox_domains \
   5         |cut -f1 -d\ 
   6         |xargs dkim_generate.sh
   7 ### OR WITH A SUBSHELL
   8 dkim_generate.sh \
   9         $(grep '^\s*[^#]' /etc/postfix/virtual_mailbox_domains \
  10         |cut -f1 -d\ )

View TXT records

   1 for FILE in /var/lib/rspamd/dkim/*.txt; do
   2         echo; echo "$FILE";
   3         cat "$FILE"
   4 done

Or as a more powerful shell script, which can generate MX, DKIM, SPF, DMARC records, as an overview.

Be careful with the heredocs with <<-EOF Only the prefixing TAB-chars are removed, the wiki gives spaces…

/usr/local/sbin/dns_generate.sh

   1 #!/bin/bash
   2 
   3 # NO WARRANTY, NO LIABILITY, NO FITNESS, AS IS, …
   4 # HAVE FUN
   5 
   6 PROGRAM="$(basename "$0")"
   7 SPF_POLICY='mx'
   8 DKIM_SELECTOR="$(date +%Y)"
   9 DKIM_PATH="/var/lib/rspamd/dkim"
  10 DMARC_POLICY="p=none;adkim=r;aspf=r"
  11 RSPAMD_GRP="_rspamd"
  12 CA="letsencrypt.org"
  13 CAA="false"
  14 SPF="false"
  15 DKIM="false"
  16 DMARC="false"
  17 DOMAIN_EXIT=false
  18 MX="false"
  19 WIDTH_MAX=64
  20 NO_DOMAIN="true"
  21 
  22 usage () {
  23         cat <<-EOF
  24                 $PROGRAM [OPTIONS] [--] domain1 [domain2] [domainN]
  25 
  26                 Options:
  27                   -c[ca]|--caa[=ca]                     Generate CAA records
  28                   -d[selector]|--dkim[=selector]        Generate DKIM records
  29                   -h|--help                             Show this help
  30                   -m'mx1,mx2'|--mx'mx1,mx2'             Generate MX records
  31                   -p[policy|--dmarc[=policy]            Generate DMARC records
  32                   -s[policy]|--spf[=policy]             Generate SFP records
  33 
  34                   When specifiing the optional arguments,
  35                   make sure to leave no whitespace or
  36                   it wil be interpreted as a domain.
  37         EOF
  38         exit 0
  39 }
  40 
  41 # Note that we use "$@" to let each command-line parameter expand to a
  42 # separate word. The quotes around "$@" are essential!
  43 # We need TEMP as the 'eval set --' would nuke the return value of getopt.
  44 TEMP=$(getopt \
  45         -o 'c::,h,d::,p::,s::,m:' \
  46         --long 'caa::,help,dkim::,dmarc::,spf::,mx:' \
  47         -n "$PROGRAM" -- "$@")
  48 
  49 if [ $? -ne 0 ]; then
  50         echo 'Terminating...' >&2
  51         exit 1
  52 fi
  53 
  54 # Note the quotes around "$TEMP": they are essential!
  55 eval set -- "$TEMP"
  56 unset TEMP
  57 
  58 while true; do
  59         case "$1" in
  60                 '-h'|'--help')
  61                         usage
  62                         shift
  63                         continue
  64                 ;;
  65                 '-m'|'--mx')
  66                         MX="true"
  67                         MX_RAW="$2"
  68                         shift 2
  69                         continue
  70                 ;;
  71                 '-c'|'--caa')
  72                         # c has an optional argument. As we are in quoted mode,
  73                         # an empty parameter will be generated if its optional
  74                         # argument is not found.
  75                         CAA="true"
  76                         case "$2" in
  77                                 '')
  78                                         :
  79                                 ;;
  80                                 *)
  81                                         CA="$2"
  82                                 ;;
  83                         esac
  84                         shift 2
  85                         continue
  86                 ;;
  87                 '-d'|'--dkim')
  88                         # c has an optional argument. As we are in quoted mode,
  89                         # an empty parameter will be generated if its optional
  90                         # argument is not found.
  91                         DKIM="true"
  92                         case "$2" in
  93                                 '')
  94                                         :
  95                                 ;;
  96                                 *)
  97                                         DKIM_SELECTOR="$2"
  98                                 ;;
  99                         esac
 100                         shift 2
 101                         continue
 102                 ;;
 103                 '-p'|'--dmarc')
 104                         # c has an optional argument. As we are in quoted mode,
 105                         # an empty parameter will be generated if its optional
 106                         # argument is not found.
 107                         DMARC="true"
 108                         case "$2" in
 109                                 '')
 110                                         :
 111                                 ;;
 112                                 *)
 113                                         DMARC_POLICY="$2"
 114                                 ;;
 115                         esac
 116                         shift 2
 117                         continue
 118                 ;;
 119                 '-s'|'--spf')
 120                         # c has an optional argument. As we are in quoted mode,
 121                         # an empty parameter will be generated if its optional
 122                         # argument is not found.
 123                         SPF="true"
 124                         case "$2" in
 125                                 '')
 126                                         :
 127                                 ;;
 128                                 *)
 129                                         SPF_POLICY="$2"
 130                                 ;;
 131                         esac
 132                         shift 2
 133                         continue
 134                 ;;
 135                 '--')
 136                         shift
 137                         break
 138                 ;;
 139                 *)
 140                         echo 'Internal error!' >&2
 141                         exit 1
 142                 ;;
 143         esac
 144 done
 145 
 146 
 147 if [ "${#@}" -ge "1" ]; then
 148         DOMAINS=( "$@" )
 149         NO_DOMAIN=false
 150 else
 151         echo "No domains given as arguments." >&2
 152 fi
 153 
 154 ### CHECK DNS DOMAINS AGAINST REGEX
 155 DNS_NAME_REGEX='(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?'
 156 echo 'Processing domains:'
 157 for arg; do
 158         if ! grep -P "$DNS_NAME_REGEX" <<< "$arg"; then
 159                 echo "Domain '$arg' is not a valid domain."
 160                 DOMAIN_EXIT=true
 161         fi      
 162 done
 163 
 164 if $DOMAIN_EXIT; then
 165         echo "Exiting…"
 166         exit 2
 167 fi
 168 
 169 declare -a MXS
 170 MXS=( ${MX_RAW//,/ } )
 171 
 172 [ -d "$DKIM_PATH" ] || \
 173         install -o root -g "$RSPAMD_GRP" -m 2710 \
 174         -d "$DKIM_PATH"
 175 grep -q '^v=spf1 ' <<< "$SPF_POLICY" || \
 176         SPF_POLICY="v=spf1 $SPF_POLICY"
 177 
 178 grep -q '^v=DMARC1; ' <<< "$DMARC_POLICY" || \
 179         DMARC_POLICY="v=DMARC1; $DMARC_POLICY"
 180 
 181 DMARC_POLICY="$(sed -r 's/([^ ]);([^ ])/\1; \2/g' <<< "$DMARC_POLICY")"
 182 
 183 cat <<-EOI
 184 
 185         Options:
 186         CAA             '$CAA'          '$CA'
 187         MX              '$MX'           '${MXS[*]}'
 188         SPF             '$SPF'          '$SPF_POLICY'
 189         DKIM SELECTOR   '$DKIM'         '$DKIM_SELECTOR'
 190         DMARC           '$DMARC'                '$DMARC_POLICY'
 191 
 192 EOI
 193 
 194 if $NO_DOMAIN || ( ! $MX && ! $SPF && ! $DKIM && ! $DMARC); then
 195         echo "Nothing to do." >&2
 196         exit 0
 197 fi
 198 
 199 
 200 dns_multiline () {
 201         local STRING
 202         local BRACED
 203 
 204         STRING="$1"
 205         BRACED="${2:-false}"
 206 
 207         if [ "${#STRING}" -gt "$WIDTH_MAX" ]; then
 208                 STRING="$(fold -s -w "$WIDTH_MAX" <<< "$STRING" \
 209                         |sed 's/^/\"/;s/$/\"/')"
 210                 if $BRACED; then
 211                         STRING="$(echo -e  "( $STRING\n) ;" )"
 212                         STRING="$( sed \
 213                                 -e '1p' \
 214                                 -e '1d;s/^\s*/\t\t\t  /' \
 215                                 -e '$s/^\s*//' \
 216                                 <<< "$STRING" )" 
 217                 else
 218                         STRING="$( sed \
 219                                 -e 's/^\s*/\t\t\t  /' \
 220                                 <<< "$STRING" )" 
 221                 fi
 222                 echo "$STRING"
 223         else
 224                 echo "\"$STRING\""
 225         fi
 226 }
 227 
 228 SPF_POLICY="$(dns_multiline "$SPF_POLICY")"
 229 DMARC_POLICY="$(dns_multiline "$DMARC_POLICY" "true")"
 230 
 231 ### CAA
 232 if $MX && $CAA; then
 233         cat <<-EOF
 234 
 235                 ### MX CAA
 236                 $(for MX_RECORD in "${MXS[@]}"; do
 237                         echo "$MX_RECORD.       CAA      128 issue \"$CA\""
 238                 done
 239                 )
 240         EOF
 241 fi
 242 
 243 for DOMAIN in "${DOMAINS[@]}"; do
 244         echo -e '\n\n'"Processing domain '$DOMAIN'"
 245 
 246         ### MX
 247         if $MX; then
 248                 cat <<-EOF
 249 
 250                         ### MX
 251                         \$ORIGIN $DOMAIN.
 252                         $(for MX_RECORD in "${MXS[@]}"; do
 253                                 echo "@ MX      10      $MX_RECORD."
 254                         done
 255                         )
 256                 EOF
 257         fi
 258 
 259         ### SPF
 260         if $SPF; then
 261                 cat <<-EOF
 262                         
 263                         ### SPF
 264                         \$ORIGIN $DOMAIN.
 265                         @       TXT             $SPF_POLICY
 266                 EOF
 267         fi
 268 
 269         ### DKIM
 270         if $DKIM; then
 271                 echo -e "\n### DKIM"
 272                 DKIM_FILE="$DKIM_PATH/$DOMAIN.$DKIM_SELECTOR"
 273                 echo -n "DKIM file for domain '$DOMAIN' "
 274                 if [ -f "$DKIM_FILE.pem" ] || [ -f "$DKIM_FILE.txt" ] ; then
 275                         echo "already exists. Skipping…"
 276                 else
 277                         rspamadm dkim_keygen \
 278                                 -b 2048 -s "$DKIM_SELECTOR" \
 279                                 -k "$DKIM_FILE".pem \
 280                                 > "$DKIM_FILE".txt
 281                         if [ "$?" ]; then
 282                                 echo " has been created."
 283                         else
 284                                 echo " could not be created. Error …"
 285                         fi
 286                 fi
 287                 ls -1 "$DKIM_FILE"*
 288                 #cat "$DKIM_FILE.txt"
 289                 head -n1 "$DKIM_FILE.txt"
 290                 DKIM_PUB="$(sed -e '1d;$d' "$DKIM_FILE.txt" \
 291                         |sed -e ':a;/$/{N;s/\n//;ba}' \
 292                         |sed 's/[[:space:]"]*//g')"
 293                 dns_multiline "$DKIM_PUB"
 294                 tail -n1 "$DKIM_FILE.txt"
 295         fi
 296 
 297         ### DMARC
 298         if $DMARC; then
 299                 cat <<-EOF
 300 
 301                         ### DMARC
 302                         \$ORIGIN $DOMAIN.
 303                         _dmarc  TXT             $DMARC_POLICY
 304 
 305                 EOF
 306         fi
 307 done
 308 
 309 echo "Fixing unix permissions" >&2
 310 find \
 311         /etc/rspamd/local.d \
 312         /var/lib/rspamd/dkim \
 313         -type f \
 314         -exec chown root:"$RSPAMD_GRP" {} \; \
 315         -exec chmod 0640 {} \;

Use it like

   1 dns_generate.sh -c -d2021 \
   2                -m'mail1.example.com mail3.example.com' \
   3                -p'v=DMARC1; p=none; adkim=r; aspf=r' \
   4                -s'v=spf1 mx' -- \
   5                domain1.tld domain2.tld
   6 grep -v '^#' /etc/postfix/virtual_mailbox_domains \
   7         |cut -f1 -d\  \
   8         |xargs dns_generate.sh \
   9                 -c -d2021 \
  10                 -m'mail1.example.com mail3.example.com' \
  11                 -p'v=DMARC1; p=none; adkim=r; aspf=r' \
  12                 -s'v=spf1 mx' -- \
  13         |tee vmd.txt

View TXT records

   1 for FILE in /var/lib/rspamd/dkim/*.txt; do
   2         echo; echo "$FILE";
   3         cat "$FILE"
   4 done

/etc/rspamd/local.d/dkim_signing.conf

   1 # If true, username does not need to contain matching domain
   2 ### ENABLE DKIM SIGNING FOR ALIAS SENDER ADDRESSES
   3 allow_username_mismatch = true;
   4 
   5 # Default selector to use
   6 # UNIFY TO REUSE CONFIG FOR DMARC
   7 selector = "2019";

Always fix #Unix Permissions.

Configure your TXT record in DNS as in file
/var/lib/rspamd/dkim/rockstable.it.2019.txt

   1 $TTL    86400
   2 $ORIGIN rockstable.it.
   3 
   4 ; SOA RECORD WITH INCREMENTED SERIAL OMITTED
   5 
   6 ; DKIM DOMAINKEY
   7 2019._domainkey         TXT     ( "v=DKIM1; k=rsa; "
   8         "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Ld6R37vRthQgqQlLkWs+HDxPNz/sTn/qCQooBtWMlTjz3487TM7owU4ryEg1RKsflxPvZrxcDnEthib6ckiH4LpFRYdK3kCkpoCNshPZruXcY0fV+Qv/pJ12nlKBe7jTcqAVhvbh/P6eyYdERCuDFN6yiWJtjKOKgt4sKZHtSR12ZPlPLHguR9C+rXQzoNbV69WaWOo0HpDaPEvj"
   9         "ZRXawEBfczQ7ArYzEu6V737PIlsBdEK29Jg/rQozBBN3ZzrfdR3gFHYytDZimVdx7rZ2KIvFR+E1l+EQnjw8EW/6Hk5yCjCr0HcgnWPJ88enNOS2EifhYLp8Wyocj+UFBcY1QIDAQAB"
  10 ) ;

Reload rspamd

   1 systemctl reload rspamd.service

Check it!

There should be a DKIM-Header in the mails you send. The selector is specified by s=2019.

   1 
   2 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=rockstable.it;
   3     s=2019; t=1602958680; h=from:from:sender:reply-to:subject:subject:date:date:
   4     message-id:message-id:to:to:cc:mime-version:mime-version:
   5     content-type:content-type:
   6     content-transfer-encoding:content-transfer-encoding:
   7     in-reply-to:in-reply-to:references:references;
   8     bh=VJsvh7GQqRsC7z/aMdedXfoWJ3TGCqWZKhAQC8zBYtU=;
   9     b=oJzj33E6XjYdJrvSIB1rrSnaZuWT4oD3xDUbn3VkACul6z+gaAwFhKLJXv8Z6MwtjWfxjb
  10     3iUwbdfthJ7d8h84XT+cIUWX9INGK9YUSBiG4TjArt3Y/7AtlW/6z0qz/6/T1US0ULTdlZ
  11     97/DGp31mhypWSEYK+kCEOFJbMRQ0ZzDipRMhygrbZgFONaap16FXh5SDwyCcbRHuM18A2
  12     wEKjiDbHz80OToPRECsfSQr+Pag1+VN5/4pXTeXziIIagt+V/tWMgd5afhrotLGhP8sEBf
  13     PYWuHe3UFICHnn2xX1gmm2ZZ1F4iv2RMiIRx0KqoTVJbf2oCr3n7O4jazhpYyQ==
  14 

Authenticated Received Chain (ARC)

Just link to dkim_signing.conf because they are using the same backend for handling signatures.

   1 cd /etc/rspamd/local.d
   2 ln -s dkim_signing.conf arc.conf
   3 #ln -s /etc/rspamd/local.d/{dkim_signing.conf,arc.conf}
   4 

Antivirus Checks

Make sure http is allowed to the following hosts to download virus definitions.

Install clamav-daemon

   1 aptitude install clamav-daemon clamav

Adjust ClamAV Config

/etc/clamav/clamd.conf

   1 #Automatically Generated by clamav-daemon postinst
   2 #To reconfigure clamd run #dpkg-reconfigure clamav-daemon
   3 #Please read /usr/share/doc/clamav-daemon/README.Debian.gz for details
   4 #LocalSocket /var/run/clamav/clamd.ctl
   5 LocalSocket /run/clamav/clamd.ctl
   6 FixStaleSocket true
   7 LocalSocketGroup clamav
   8 LocalSocketMode 666
   9 # TemporaryDirectory is not set to its default /tmp here to make overriding
  10 # the default with environment variables TMPDIR/TMP/TEMP possible
  11 User clamav
  12 ScanMail true
  13 ScanArchive true
  14 ArchiveBlockEncrypted false
  15 MaxDirectoryRecursion 15
  16 FollowDirectorySymlinks false
  17 FollowFileSymlinks false
  18 ReadTimeout 180
  19 MaxThreads 12
  20 MaxConnectionQueueLength 15
  21 LogSyslog false
  22 LogRotate true
  23 LogFacility LOG_LOCAL6
  24 LogClean false
  25 LogVerbose false
  26 PreludeEnable no
  27 PreludeAnalyzerName ClamAV
  28 DatabaseDirectory /var/lib/clamav
  29 OfficialDatabaseOnly false
  30 SelfCheck 3600
  31 Foreground false
  32 Debug false
  33 ScanPE true
  34 MaxEmbeddedPE 10M
  35 ScanOLE2 true
  36 ScanPDF true
  37 ScanHTML true
  38 MaxHTMLNormalize 10M
  39 MaxHTMLNoTags 2M
  40 MaxScriptNormalize 5M
  41 MaxZipTypeRcg 1M
  42 ScanSWF true
  43 ExitOnOOM false
  44 LeaveTemporaryFiles false
  45 AlgorithmicDetection true
  46 ScanELF true
  47 IdleTimeout 30
  48 CrossFilesystems true
  49 PhishingSignatures true
  50 PhishingScanURLs true
  51 PhishingAlwaysBlockSSLMismatch false
  52 PhishingAlwaysBlockCloak false
  53 PartitionIntersection false
  54 DetectPUA false
  55 ScanPartialMessages false
  56 HeuristicScanPrecedence false
  57 StructuredDataDetection false
  58 CommandReadTimeout 30
  59 SendBufTimeout 200
  60 MaxQueue 100
  61 ExtendedDetectionInfo true
  62 OLE2BlockMacros false
  63 AllowAllMatchScan true
  64 ForceToDisk false
  65 DisableCertCheck false
  66 DisableCache false
  67 MaxScanTime 120000
  68 MaxScanSize 100M
  69 MaxFileSize 25M
  70 MaxRecursion 16
  71 MaxFiles 10000
  72 MaxPartitions 50
  73 MaxIconsPE 100
  74 PCREMatchLimit 10000
  75 PCRERecMatchLimit 5000
  76 PCREMaxFileSize 25M
  77 ScanXMLDOCS true
  78 ScanHWP3 true
  79 MaxRecHWP3 16
  80 StreamMaxLength 25M
  81 LogFile /var/log/clamav/clamav.log
  82 LogTime true
  83 LogFileUnlock false
  84 LogFileMaxSize 0
  85 Bytecode true
  86 BytecodeSecurity TrustSigned
  87 BytecodeTimeout 60000
  88 OnAccessMaxFileSize 5M
  89 ### ENABLE NETWORKING
  90 TCPAddr 127.0.0.1
  91 TCPSocket 3310

/etc/systemd/system/clamav-daemon.socket

   1 [Unit]
   2 Description=Clam AntiVirus userspace daemon
   3 
   4 [Install]
   5 WantedBy=sockets.target
   6 
   7 [Socket]
   8 ListenStream=
   9 ListenStream=/run/clamav/clamd.ctl
  10 ListenStream=127.0.0.1:3310

Reload systemd, enable socket restart clamav-daemon

   1 systemctl daemon-reload
   2 systemctl enable clamav-daemon.socket
   3 systemctl status clamav-daemon.socket
   4 systemctl enable clamav-daemon.service
   5 systemctl restart clamav-daemon.service
   6 systemctl status clamav-daemon.service

Check if daemon is listening ss -tulpen|grep 3310

   1 Netid  State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port                                                                                                                                                                                    
   2 tcp    LISTEN  0       15      127.0.0.1:3310      0.0.0.0:* users:(("clamd",pid=17811,fd=4)) uid:109 ino:414291 sk:d <->

Copy configuration

   1 cp /etc/rspamd/modules.d/antivirus.conf \
   2         /etc/rspamd/local.d/antivirus.conf

Enable Antivirus/ClamAV in Rspamd

/etc/rspamd/local.d/antivirus.conf

   1 # Please don't modify this file as your changes might be overwritten with
   2 # the next update.
   3 #
   4 # You can modify 'local.d/antivirus.conf' to add and merge
   5 # parameters defined inside this section
   6 #
   7 # You can modify 'override.d/antivirus.conf' to strictly override all
   8 # parameters defined inside this section
   9 #
  10 # See https://rspamd.com/doc/faq.html#what-are-the-locald-and-overrided-directories
  11 # for details
  12 #
  13 # Module documentation can be found at  https://rspamd.com/doc/modules/antivirus.html
  14 
  15 antivirus {
  16   # multiple scanners could be checked, for each we create a configuration block with an arbitrary name
  17   clamav {
  18     # If set force this action if any virus is found (default unset: no action is forced)
  19     action = "reject";
  20     # message = '${SCANNER}: virus found: "${VIRUS}"';
  21     # Scan mime_parts separately - otherwise the complete mail will be transferred to AV Scanner
  22     #scan_mime_parts = true;
  23     # Scanning Text is suitable for some av scanner databases (e.g. Sanesecurity)
  24     #scan_text_mime = false;
  25     #scan_image_mime = false;
  26     # If `max_size` is set, messages > n bytes in size are not scanned
  27     #max_size = 20000000;
  28     # symbol to add (add it to metric if you want non-zero weight)
  29     #symbol = "CLAM_VIRUS";
  30     # type of scanner: "clamav", "fprot", "sophos" or "savapi"
  31     type = "clamav";
  32     # For "savapi" you must also specify the following variable
  33     #product_id = 12345;
  34     # You can enable logging for clean messages
  35     #log_clean = true;
  36     # servers to query (if port is unspecified, scanner-specific default is used)
  37     # can be specified multiple times to pool servers
  38     # can be set to a path to a unix socket
  39     # Enable this in local.d/antivirus.conf
  40     servers = "127.0.0.1:3310";
  41     # if `patterns` is specified virus name will be matched against provided regexes and the related
  42     # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
  43     #patterns {
  44       # symbol_name = "pattern";
  45     #  JUST_EICAR = '^Eicar-Test-Signature$';
  46     #}
  47     #patterns_fail {
  48       # symbol_name = "pattern";
  49       #CLAM_PROTOCOL_ERROR = '^unhandled response';
  50     #}
  51     # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
  52     whitelist = "/etc/rspamd/antivirus.wl";
  53   }
  54 }

Always fix #Unix Permissions.

Restart rspamd

   1 systemctl restart rspamd.service

Test antivirus

The EICAR Anti-Virus Test File or EICAR test file is a computer file that was developed by the European Institute for Computer Antivirus Research (EICAR) and Computer Antivirus Research Organization (CARO), to test the response of computer antivirus (AV) programs. Instead of using real malware, which could cause real damage, this test file allows people to test anti-virus software without having to use a real computer virus.

EICAR test pattern

   1 X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Basic test script

   1 ADDRESS="user@domain.tld"
   2 EICAR='X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
   3 TEST_FILE="$(mktemp)"
   4 echo -n "$EICAR" > "$TEST_FILE"
   5 #wget -O "$TEST_FILE" 'https://secure.eicar.org/eicar_com.zip' 
   6 echo 'Testing the AV-scanner' \
   7         |mail -A "$TEST_FILE" \
   8                 -s "Test AV $(date '+%F %T')" \
   9                 "$ADDRESS"
  10 [ -f "$TEST_FILE" ] && rm "$TEST_FILE"

Olefy

olefy - oletools verify over TCP socket

Small Python Daemon to use oletools over TCP sockets. Mainly to use oletools in Rspamd.

Install olefy

Prepare a user, group …

   1 adduser --system --group \
   2         --home /var/lib/olefy \
   3         --gecos "oletools over TCP sockets for rspamd" \
   4         olefy

Clone olefy repo

   1 DIR_GIT="/opt/olefy"
   2 git clone https://github.com/HeinleinSupport/olefy.git "$DIR_GIT"
   3 cd "$DIR_GIT"
   4 ### CREATE A WORKING BRANCH
   5 git branch PROD
   6 git checkout PROD

Install requirements

   1 aptitude install python3-pip
   2 pip3 install -r "$DIR_GIT/requirements.txt"

Change a path in the systemd-service to honor path to git repo.

   1 sed -i "s:/usr/local/bin/olefy.py:\"$DIR_GIT/olefy.py\":" \
   2         "$DIR_GIT/olefy.service"

I like the idea to keep the files in git to have the ability to git diff and link everything symbolically.

   1 ln -s "$DIR_GIT/olefy.conf" /etc/olefy.conf
   2 ln -s "$DIR_GIT/olefy.service" /lib/systemd/system
   3 systemctl daemon-reload
   4 systemctl enable olefy.service
   5 systemctl start olefy.service

Changes
git diff master

   1 diff --git a/olefy.service b/olefy.service
   2 index 9527b73..3d4edbd 100644
   3 --- a/olefy.service
   4 +++ b/olefy.service
   5 @@ -8,7 +8,7 @@ User=olefy
   6  Group=olefy
   7  
   8  EnvironmentFile=/etc/olefy.conf
   9 -ExecStart=/usr/bin/python3 /usr/local/bin/olefy.py
  10 +ExecStart=/usr/bin/python3 "/opt/olefy/olefy.py"
  11  ExecReload=/bin/kill -HUP $MAINPID
  12  
  13  PIDFile=/run/olefy.pid

Test olefy

   1 echo PING |nc -q1 127.0.0.1 10050
   2 PONG

B-)

Configure rspamd to use olefy

Create local configuration (derived from /etc/rspamd/local.d/external_services.conf)
/etc/rspamd/local.d/external_services.conf

   1   oletools {
   2     # If set force this action if any virus is found (default unset: no action is forced)
   3     action = "reject";
   4     # If set, then rejection message is set to this value (mention single quotes)
   5     # If `max_size` is set, messages > n bytes in size are not scanned
   6     # max_size = 20000000;
   7     # log_clean = true;
   8     servers = "127.0.0.1:10050";
   9     # cache_expire = 86400;
  10     scan_mime_parts = true;
  11     scan_image_mime = false;
  12     # extended = false;
  13     # if `patterns` is specified virus name will be matched against provided regexes and the related
  14     # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
  15     patterns {
  16       # symbol_name = "pattern";
  17     }
  18     # mime-part regex matching in content-type or filename
  19     mime_parts_filter_regex {
  20       #GEN1 = "application\/octet-stream";
  21       DOC2 = "application\/msword";
  22       DOC3 = "application\/vnd\.ms-word.*";
  23       XLS = "application\/vnd\.ms-excel.*";
  24       PPT = "application\/vnd\.ms-powerpoint.*";
  25       GEN2 = "application\/vnd\.openxmlformats-officedocument.*";
  26     }
  27     # Mime-Part filename extension matching (no regex)
  28     mime_parts_filter_ext {
  29       doc = "doc";
  30       dot = "dot";
  31       docx = "docx";
  32       dotx = "dotx";
  33       docm = "docm";
  34       dotm = "dotm";
  35       xls = "xls";
  36       xlt = "xlt";
  37       xla = "xla";
  38       xlsx = "xlsx";
  39       xltx = "xltx";
  40       xlsm = "xlsm";
  41       xltm = "xltm";
  42       xlam = "xlam";
  43       xlsb = "xlsb";
  44       ppt = "ppt";
  45       pot = "pot";
  46       pps = "pps";
  47       ppa = "ppa";
  48       pptx = "pptx";
  49       potx = "potx";
  50       ppsx = "ppsx";
  51       ppam = "ppam";
  52       pptm = "pptm";
  53       potm = "potm";
  54       ppsm = "ppsm";
  55     }
  56     # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
  57     whitelist = "/etc/rspamd/antivirus.wl";
  58   }
  59   dcc {
  60     # If set force this action if any virus is found (default unset: no action is forced)
  61     # action = "reject";
  62     # If set, then rejection message is set to this value (mention single quotes)
  63     # If `max_size` is set, messages > n bytes in size are not scanned
  64     max_size = 20000000;
  65     #servers = "127.0.0.1:10045";
  66     # if `patterns` is specified virus name will be matched against provided regexes and the related
  67     # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
  68     patterns {
  69       # symbol_name = "pattern";
  70     }
  71     # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
  72     whitelist = "/etc/rspamd/antivirus.wl";
  73   }

Restart rspamd

   1 systemctl restart rspamd.service

Scores and actions

rspamd docs - Configuring scores and actions

When sending emails containing multiple charsets (like cyrillic А а, Б б, В в, Г г, Д д, Е е, Ё ё, Ж ж, … and German Ä ä Ö ö Ü ü ß ) rspamd scores additional 5.0.

This may be disabled by creating
/etc/rspamd/local.d/headers_group.conf

   1 symbols = {
   2     "R_MIXED_CHARSET" {
   3         weight = 0;
   4         description = "Mixed characters in a message";
   5         one_shot = true;
   6     }
   7 }

Restart rspamd

   1 systemctl restart rspamd.service

Greylisting

rspamd docs - Greylisting module

For weekly occuring tasks, the expiry of "1d" is to short.

Raise it to 10d in
/etc/rspamd/local.d/greylist.conf

   1 expire = 10d; # 1 day by default
   2 

Restart rspamd

   1 systemctl restart rspamd.service

Once received

rspamd docs - Once received module

If a email has only a single "Received" header, this email is suspected to be a hacked device (like personal computer) that directly spams to the internet and is honored with a higher score.

Unfortunatly having no or only a single header applies to some "internal" scripts using the mail-gateway directly. IP-based whitelist may be used to allow these internal networks send email.

/etc/rspamd/local.d/once_received.wl

   1 #IP.ADD.RE.SS/SNM
   2 

/etc/rspamd/local.d/once_received.conf

   1 whitelist = "$LOCAL_CONFDIR/local.d/once_received.wl"

Restart rspamd

   1 systemctl restart rspamd.service

Integration with MTA

For integration with Postfix, please return to postfix#rspamd

Rockstable Wiki: rspamd (last edited 2021-03-01 17:38:03 by RockStable)