Dwelling/homepage/views/articles/setting_up_a_mail_server.pug

386 lines
24 KiB
Plaintext

extends article.pug
block article
header
h2 Setting up a mail server (WIP)
div.menu
a(href='/stuff#articles') Go back to articles list
time(datetime='2021-09-20') 20 September 2021
nav
h3 Contents
ol
li #[a(href='#art-1') Introduction]
li #[a(href='#art-2') Installing]
li #[a(href='#art-3') Postfix SMTP server]
ol
li #[a(href='#art-3-1') main.cf]
li #[a(href='#art-3-2') master.cf]
li #[a(href='#art-3-3') User aliases]
li #[a(href='#art-3-4') Starting Postfix]
li #[a(href='#art-4') Dovecot POP3/IMAP server with Sieve mail filter]
li #[a(href='#art-5') SpamAssassin spam filter]
ol
li #[a(href='#art-5-1') Updating built-in rule set]
li #[a(href='#art-5-2') Bayesian classifier training]
li #[a(href='#art-6') OpenDKIM signing and verifying filter]
ol
li #[a(href='#art-6-1') opendkim.conf]
li #[a(href='#art-6-2') Generating keys]
li #[a(href='#art-6-3') Populating KeyTable and SigningTable]
li #[a(href='#art-6-4') internal-hosts file]
li #[a(href='#art-6-5') Starting OpenDKIM]
li #[a(href='#art-7') OpenDMARC email policy filter]
li #[a(href='#art-8') DNS records]
ol
li #[a(href='#art-8-1') MX and A/AAAA]
li #[a(href='#art-8-2') PTR]
li #[a(href='#art-8-3') SPF]
li #[a(href='#art-8-4') DMARC]
li #[a(href='#art-8-5') DKIM]
li #[a(href='#art-9') Setting up a ClamAV antivirus]
h3#art-1 #[a(href='#art-1') 1. Introduction]
p I use Postfix as a SMTP and Dovecot (with Pigeonhole (Sieve)) as an IMAP server. ClamAV for an antivirus. For antispam I use SpamAssassin. For DKIM and DMARC — OpenDKIM and OpenDMARC respectively.
p It is vital to make the DKIM, DMARC and SPF DNS records. Also, if you want your mail server to be trusted by every other mail servers then you should get a static IP-address if you don't yet. And you have to ask your ISP to edit PTR DNS record for your static IP-address to point to your domain.
p Unfortunately for me I don't have neither, and I'm afraid that even if I get the static IP-address, my ISP won't edit PTR record, because that's available only for bussiness customers. And about domains, there is such things as untrustworthy abused TLDs, and top TLD is in that list. That's definitely because it's a cheap TLD.
p Server is configured in a simple way using PAM (real system users) with user's passwords and with mail stored in ~/Maildir.
h3#art-2 #[a(href='#art-2') 2. Installing]
p You need to install following packages: #[code postfix], #[code dovecot], #[code pidgeonhole] (or could be #[code dovecot-sieve]), #[code clamav], #[code opendkim], #[code opendmarc], and #[code spamassassin].
h3#art-3 #[a(href='#art-3') 3. Postfix SMTP server]
p Its configuration files are in directory #[code /etc/postfix]. There are two configuration files we'll work with. The first one is a #[code main.cf] file. Then we configure services in #[code master.cf]. Also I'll show you how to make aliases for users.
h4#art-3-1 #[a(href='#art-3-1') 3.1. main.cf]
p Let's take a look at all base options that should be modified:
pre
| myhostname = mail.example.org
| mydomain = example.org
| myorigin = $mydomain
|
| inet_interfaces = all
|
| mydestination = $myhostname, localhost, $mydomain, mail.$mydomain
|
| local_recipient_maps = unix:passwd.byname $alias_maps
|
| mynetworks = localhost, 192.168.0.0/24
|
| alias_maps = hash:/etc/postfix/aliases
| alias_database = $alias_maps
|
| recipient_delimiter = +
|
| home_mailbox = Maildir/
|
| mailbox_transport = lmtp:unix:private/dovecot-lmtp
|
| inet_protocols = ipv4
p Now let's clarify what are they doing.
p Set #[code myhostname] to a hostname of a server (e.g. #[code mail.example.org]). Set #[code mydomain] to your domain name (e.g. #[code example.org]). Set #[code myorigin] to #[code $mydomain] to set origin of mail being sent from your server.
p #[code mydestination] is a list of domains that are delivered through a local transport. If server should go outside then this option must include #[code $mydomain] alongside names for the local machine. E.g. #[code $myhostname, localhost, $mydomain, mail.$mydomain].
p #[code local_recipient_maps] are lookup tables with all names and/or addresses of local recipients. In my case it set to #[code unix:passwd.byname $alias_maps].
p I have #[code inet_interfaces = all] to listen on all the interfaces.
p In #[code mynetworks], as stated in a Postfix's manual, we specify a list of “trusted” clients that have more privileges than “strangers”. In particular, such clients are allowed to relay mail through Postfix. I have it set to localhost and my LAN.
p In #[code alias_maps] we specify a list of lookup tables that contain aliases for existing users. And in #[code alias_database] just add #[code $alias_maps]. #[code alias_database] is, as stated in a manual, separate because not all the tables specified with #[code $alias_maps] have to be local files.
p #[code recipient_delimeter = +]. Here we set a delimeter to a plus sign (that's just a usual practice that I obeyed).
p I use a Maildir-style mailboxes, so #[code home_mailbox] is set to #[code Maildir/] (slash is necessary).
p We use Dovecot, so #[code mailbox_transport] should be set to #[code lmtp:unix:private/dovecot-lmtp]. Here we point to where Dovecot LMTP server listens, in our case it is a UNIX-socket.
p Optionaly, you can set #[code inet_protocols] to IP versions used by you, I set it just to #[code ipv4] for a quite legitimate reason of not having IPv6 address. :) It is a space-separated list, so to support both write #[code ipv4 ipv6].
p Next I'll cover how to make encryption working, set up milters (mail filters (i.e. OpenDKIM and OpenDMARC)), and restrictions.
p Next let's configure our milters:
pre
| milter_default_action = accept
| milter_protocol = 6
| smtpd_milters =
| unix:/var/spool/opendmarc/opendmarc.sock
| unix:/var/spool/opendkim/opendkim.sock
| inet 192.168.0.54:7357
| non_smtpd_milters = $smtpd_milters
p #[code milter_default_action] specifies default action of a milter. Here we accept an e-mail message.
p #[code milter_protocol] specifies protocol version used by milters, current is 6.
p #[code smtpd_milters] is a list of milters the messages will go through. You can connect to milter with internet protocol with #[code inet:], with a unix socket #[code unix:]. The last one with port 7357 is a ClamAV by the way.
p Now lets do some tweaks:
pre
| biff = no
| strict_rfc821_envelopes = yes
| disable_vrfy_command = yes
| smtpd_helo_required = yes
| smtpd_delay_reject = yes
|
| mailbox_size_limit = 0
| message_size_limit = 52428800
p #[code biff] set to no, so the local service for new mail notifications disabled. We run on a server machine, we don't need them.
p #[code strict_rfc821_envelopes] set to yes require addresses to be enclosed with <>.
p Disabling VRFY command with #[code disable_vrfy_command] set to yes prevents some email addresses harvesting techniques.
p #[code smtpd_helo_required] requires remote client to send HELO or EHLO command. This may stop some poorly written spam bots.
p #[code smtpd_delay_reject] makes Postfix wait until RCPT TO command before evaluating some restrictions.
p #[code mailbox_size_limit] and #[code message_size_limit] sets maximum size of a whole mailbox and of each email. Here I set no limit for a mailbox, and max of 50MiB for an email message.
p And now it's time for SASL configuration:
pre
| smtpd_sasl_type = dovecot
| smtps_sasl_path = private/auth
| smtps_sasl_auth_enable = yes
| smtpd_sasl_security_options = noanonymous
| smtpd_sasl_local_domain = $mydomain
| broken_sasl_auth_clients = no
p It's pretty clear. We chose dovecot as our SASL, set path to it withing /var/spool, and enable it. Then we set option to not allow anonymous connections. Set our domain for SASL, and wether let the clients with obsolete version of AUTH command in or not, we chose not to.
p It's time for encryption, lets specify a list of high ciphers to use:
pre
| tls_high_cipherlist = ECDHE:DHE:kGOST:!aNULL:!eNULL:!RC4:!MD5:!3DES:!AES128:!CAMELLIA128:!ECDHE-RSA-AES256-SHA:!ECDHE-ECDSA-AES256-SHA
p High means just that those ciphers are strong. I took this ciphers set from #[a(href="https://pub.nethence.com/security/ciphers") https://pub.nethence.com/security/ciphers].
p Lets do client part of encryption.
pre
| smtp_use_tls = yes
| smtp_tls_security_level = encrypt
| smtp_tls_note_starttls_offer = yes
| smtp_tls_connection_reuse = yes
| smtp_tls_key_file = /etc/letsencrypt/live/example.org/privkey.pem
| smtp_tls_cert_file = /etc/letsencrypt/live/example.org/fullchain.pem
| smtp_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
| smtp_tls_protocols = $smtp_tls_mandatory_protocols
| smtp_tls_mandatory_ciphers = high
p First we enable it with a #[code smtp_use_tls] option. Setting security level with (#[code smtp_tls_security_level]) to encrypt enforces use of encryption. Noting servers of our ability to use encryption with option #[code smtp_tls_note_starttls_offer]. Set to reuse connection instead of opening the new one each time.
p #[code smtp_tls_key_file] and #[code smtp_tls_cert_file] are paths to our encryption key and certificate.
p #[code smtp_tls_mandatory_protocols] and #[code smtp_tls_protocols] here we disallow old vulnerable protocols to use. Here only TLS version 1.2 and 1.3 are allowed.
p In #[code smtp_tls_mandatory_ciphers] we declare to use only good secure cyphers.
p With server part everything is the same, just a few more options added:
pre
| smtpd_use_tls = yes
| smtpd_tls_security_level = encrypt
| smtpd_tls_auth_only = yes
| smtpd_tls_key_file = /etc/letsencrypt/live/example.org/privkey.pem
| smtpd_tls_cert_file = /etc/letsencrypt/live/example.org/fullchain.pem
| smtpd_tls_mandatory_protocols = $smtp_tls_mandatory_protocols
| smtpd_tls_protocols = $smtpd_tls_mandatory_protocols
| smtpd_tls_mandatory_ciphers = $smtp_tls_mandatory_ciphers
| smtpd_tls_loglevel = 1
| smtpd_tls_received_header = yes
| smtpd_tls_session_cache_timeout = 3600s
| tls_random_source = dev:/dev/urandom
p Security level is may, but for auth TLS is required. #[code smtpd_tls_loglevel] is for logging a summary of a TLS handshake.
p #[code smtpd_tls_received_header] makes Postfix include information about the protocol and cypher used to a Received: header.
p #[code smtpd_tls_session_cache_timeout] is for how long to store session. #[code tls_random_source] is for setting an entropy source.
p The final part is my "favorite". :) The restrictions! There is a set of them for each stage the message falls through. Here's the ones I configured:#[code smtpd_helo_restrictions], #[code smtpd_relay_restrictions], #[code smtpd_data_restrictions], #[code smtpd_sender_restrictions], and #[code smtpd_recipient_restrictions].
p So lets roll. This my working restrictions setup:
pre
| smtpd_helo_restrictions =
| reject_unknown_helo_hostname,
| reject_invalid_helo_hostname,
| reject_non_fqdn_helo_hostname
pre
| smtpd_data_restrictions =
| reject_multi_recipient_bounce,
| reject_unauth_pipelining
pre
| smtpd_sender_restrictions =
| permit_sasl_authenticated,
| reject_non_fqdn_sender,
| reject_unknown_sender_domain
pre
| smtpd_recipient_restrictions =
| reject_unknown_recipient_domain,
| reject_non_fqdn_recipient
p In order to explain what every restriction does I'd have to copy-paste from #[code man 5 postconf]. :)
p The first set of restrictions are for HELO or EHLO command, that we force the client to send with priorly set option #[code smtpd_helo_required] to yes. Here #[code reject_unknown_helo_hostname] rejects hostnames that doesn't have DNS A or MX records. #[code reject_invalid_helo_hostname] rejects malformed hostnames, and #[code reject_non_fqdn_helo_hostname] ensures that the hostname is a fully-qualified domain name.
p The second one are for DATA command. And here man page is better than me at explaining it. Here is the link for #[a(href='http://www.postfix.org/postconf.5.html#reject_multi_recipient_bounce') reject_multi_recipient_bounce] and #[a(href='http://www.postfix.org/postconf.5.html#reject_unauth_pipelining') reject_unauth_pipelining]. All I can say is that it is better to have them than not to. :)
p The rest is simpler, #[code permit_sasl_authenticated] in #[code smtpd_sender_restrictions] accepts the senders that were authenticated by SALS (e.g. Dovecot or Cyrus). And all the #[code reject_unknown_*] and #[code reject_non_fqdn_*] has the same meaning as for theirs *_helo_* counterparts, just used in theirs specific places.
h4#art-3-2 #[a(href='#art-3-2') 3.2. master.cf]
p The following services are needed: #[code smtp], #[code submission], #[code smtps], and we add #[code spamassassin] service. The rest in this file left untouchable.
pre
| smtp inet n - n - - smtpd
| -o content_filter=spamassassin
| submission inet n - n - - smtpd
| -o syslog_name=postfix/submission
| -o smtpd_tls_security_level=encrypt
| -o smtpd_sasl_auth_enable=yes
| -o smtpd_tls_auth_only=yes
| -o milter_macro_daemon_name=ORIGINATING
| -o content_filter=spamassassin
| smtps inet n - n - - smtpd
| -o content_filter=spamassassin
| -o syslog_name=postfix/smtps
| -o smtpd_tls_wrappermode=yes
| -o smtpd_sasl_auth_enable=yes
|
| spamassassin unix - n n - - pipe
| user=spamd argv=/bin/vendor_perl/spamc
| -e /sbin/sendmail -oi -f ${sender} ${recipient}
p #[code smtp] is listening on port 25, and #[code smtps] on port 465. #[code submission] is listening on port 587 and is used by mail client to send mail.
h4#art-3-3 #[a(href='#art-3-3') 3.3. User aliases]
p User aliases are in #[code aliases] file. They has a form "#[code <alias>: <username>]", e.g. #[code me: arav]. Where #[code username] may be other alias. After modifications you need to run #[code newaliases] program to update #[code aliases.db] database file.
h4#art-3-4 #[a(href='#art-3-4') 3.4. Starting Postfix]
p To start a Postfix service on systemd-based Linux distro run #[code systemctl start postfix]. To make Postfix run on every boot run #[code systemctl enable postfix].
h3#art-4 #[a(href='#art-4') 4. Dovecot POP3/IMAP server with Sieve mail filter]
h3#art-5 #[a(href='#art-5') 5. SpamAssassin spam filter]
p Here we are working with a #[code local.cf] file to configure SpamAssassin.
p I didn't any fancy tweaking, didn't make any custom rules. I just changed options presented in file.
p I left #[code rewrite_header] option commented since I don't want to append anything to a Subject header of spam messages.
p Option #[code report_safe] I set to 2 to save spam messages as a text/plain attachment instead of modifying original message.
p #[code trusted_networks] sets networks and hosts that are considered trusted, i.e. not spammers.
p #[code lock_method] left to be flock, since I don't use NFS.
p #[code required_score] left to be its default value of 5.0.
p I use Bayesian classifier, so options #[code use_bayes] and #[code bayes_auto_learn] are set to 1.
p I chose to normalise charset to UTF-8 with option #[code normalize_charset 1].
h4#art-5-1 #[a(href='#art-5-1') 5.1. Updating built-in rule set]
p I sometimes run #[code sa-update] util to update built-in rules and pre-compile them with #[code sa-compile] util. After that restart SpamAssassin.
p And so SpamAssassin make use of compiled rules ensure that a plugin Rule2XSBody in a #[code v320.pre] is uncommented.
h4#art-5-1 #[a(href='#art-5-2') 5.2. Bayesian classifier training]
p After you set up SpamAssassin for the first time you have to train Bayesian classifier. It will start to work after 200 messages will be examined.
p For training use #[code sa-learn] utility and use #[code --ham] and #[code --spam] to mark messages as normal mail and spam. I additionaly have to specify a path to database with #[code --dbpath /var/lib/spamassassin/.spamassassin] option, otherwise it will complain, so try first without it.
h3#art-6 #[a(href='#art-6') 6. OpenDKIM signing and verifying filter]
p On ArchLinux OpenDKIM is unable to write in #[code /run], so I created #[code /var/spool/opendkim] directory for it.
h4#art-6-1 #[a(href='#art-6-1') 6.1. opendkim.conf]
p Well, that's main config file
pre
| KeyTable refile:/etc/opendkim/keytable
| SigningTable refile:/etc/opendkim/signingtable
| InternalHosts refile:/etc/opendkim/internal-hosts
|
| Socket local:/var/spool/opendkim/opendkim.sock
| PidFile /var/spool/opendkim/opendkim.pid
| UMask 000
| UserID opendkim:opendkim
|
| Mode sv
| SubDomains yes
|
| Canonicalization relaxed/simple
|
| Syslog yes
| SyslogSuccess yes
| LogWhy yes
|
| SoftwareHeader yes
p I myself set up a multi-domain variant just in case. So, here we have two main tables: #[code KeyTable] and #[code SigningTable]. Those files tells OpenDKIM where to find keys and what domains to sign. You may use one key for all domains or generate keys for each domain.
p #[code InternalHosts] tells OpenDKIM what hosts should be signed rather than verified.
p #[code Socket] tells where to listen to connections, in this case we use UNIX sockets.
p #[code Mode] selects operating mode(s). In our case we have two modes: (s)igner and (v)erifier.
p #[code SubDomains] set to yes tells that we allow subdomains of our domains to be signed and verified.
p #[code Canonicalization] selects the canonicalization method(s) to be used with signing. We set relaxed for header and simple for body. I don't fully understand it and just use what suggested.
p Below are logging options that tells to write in syslog.
p With #[code SoftwareHeader] set to yes OpenDKIM will be always adding "DKIM-Filter" header field.
h4#art-6-2 #[a(href='#art-6-2') 6.2. Generating keys]
pre
| opendkim-genkey -r -s myselector -b 2048 -d example.com
p This command will generate a key pair stored in files "myselector.private" and "myselector.txt" for a given domain.
p #[code -r] restricts the key to emails use only. #[code -s] is a name of selector.#[code -b] is the size of the key in bits. #[code -d] is our domain.
p Name of a selector is usually a #[code mail], but that's just what I use, you can choose whatever you want.
h4#art-6-3 #[a(href='#art-6-3') 6.3. Populating KeyTable and SigningTable]
p KeyTable has following structure (a line per domain):
pre
| myselector._domainkey.example.com example.com:myselector:/etc/opendkim/myselector.private
p And SigningTable this one:
pre
| *@example.com myselector._domainkey.example.com
h4#art-6-4 #[a(href='#art-6-4') 6.4. internal-hosts file]
p As stated above in this file we put hosts whose mail should be signed rather than verified. And its structure is the following:
pre
| 127.0.0.1
| 192.168.0.0/24
p #[code 127.0.0.1] is necessary to be there according to a manual.
h4#art-6-5 #[a(href='#art-6-5') 6.5. Starting OpenDKIM]
p #[code systemctl start opendkim] and #[code systemctl enable opendkim] to start and enable OpenDKIM service to run on OS start up if you got Poetteringed just like me. :)
h3#art-7 #[a(href='#art-7') 7. OpenDMARC email policy filter]
p Its configuration lies in #[code /etc/opendmarc/opendmarc.conf] and is fully documented. Here are the options I changed:
pre
| AuthservID OpenDMARC
| FailureReports true
| FailureReportsBcc admin@example.org
| FailureReportsSentBy admin@example.org
| IgnoreAuthenticatedClient yes
| RejectFailures true
| RequiredHeaders yes
| Socket unix:/var/spool/opendmarc/opendmarc.sock
| SoftwareHeader true
| SPFSelfValidate true
| Syslog true
| TrustedAuthservIDs mail.example.org,example.org
| UMask 002
p What's in a #[code Socket] option should be added to Postfix's #[code smtpd_milters] and #[code non_smtpd_milters].
p Creating DMARC DNS record covered in <a href="#art-8-4">8.4</a>.
h3#art-8 #[a(href='#art-8') 8. DNS records]
h4#art-8-1 #[a(href='#art-8-1') 8.1. MX and A/AAAA]
p It's good to have a dedicated A (IPv4 address) or AAAA (IPv6 address) record for a mail server's hostname instead of a CNAME record so other servers won't need to do two DNS requests. Hostname is usually mail.example.org if there's just one server, you can call it whatever you want. Remind you that we set it in Postfix in #[code myhostname] option.
p And A record looks like this:
pre
| mail&nbsp;&nbsp;IN&nbsp;&nbsp;86400&nbsp;&nbsp;A&nbsp;&nbsp;203.0.113.4
p Where #[code mail] is a hostname, 86400 is a TTL of a record in seconds.
p Next we need to add a MX (mail exchanger) record that looks like this:
pre
| &nbsp;&nbsp;MX 10 mail.example.org.
p Here 10 is a priority of a record. The lower a number the higher a priority.
p A period at the end of the hostnames is necessary in DNS records.
h4#art-8-2 #[a(href='#art-8-2') 8.2. PTR]
p PTR is a reverse DNS record that stands for pointer and is used to &ldquo;bind&rdquo; a hostname to IP-address. Mail servers looks for this record and check so this name equals to a hostname provided in EHLO. Most servers will reject your mail if your PTR looks something like 1.2.3.4.pppoe.someisp.net or not set at all.
p There are three ways to set this record: ask your hosting or internet-provider, or get your own Autonomous System (:^)).
p Example of this record:
pre
| 1 IN PTR mail.example.org.
h4#art-8-3 #[a(href='#art-8-3') 8.3. SPF]
p SPF stands for Sender Policy Framework and in my case it looks exactly like this:
pre
| v=spf1 +a +mx -all
p So, #[code v] is a version of a protocol. #[code +a +mx] means that only servers specified in the A and MX DNS records could send email, and #[code -all] that no one else could do that.
h4#art-8-4 #[a(href='#art-8-4') 8.4. DMARC]
p DMARC stands for Domain-based Message Authentication Reporting and Conformance. And its DNS record could be like this one that I use:
pre
| _dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:admin@example.org; ruf=mailto:admin@example.org"
p #[code v] is a version of a protocol.
p #[code p] is a default policy that could be set to #[code none], #[code quarantine] and #[code reject]. I chose to #[code reject] mail that comes from &laquo;me&rdquo; if there's something wrong with a origin of a message. If you could get email from subdomains then you need to set #[code sp] as well.
p #[code rua] is an address for the reports and #[code ruf] is for the forensic reports.
h4#art-8-5 #[a(href='#art-8-5') 8.5. DKIM]
p In 5.2 we generated a key pair for our domain and now we'll take what's inside a #[code myselector.txt] file and add it to our DNS.
p DKIM DNS record looks like this:
pre
| myselector._domainkey IN TXT ( "v=DKIMv1; k=rsa; s=email; p=&lt;public key goes here&gt;" )
p By the way, brackets are used in case a content of a record doesn't fit on one line.
h3#art-9 #[a(href='#art-9') 9. Setting up a ClamAV antivirus]
p All you need to make it work together with Postfix is to add #[code /run/clamav/milter.sock] to #[code smtpd_milters] and #[code non_smtpd_milters] options in Postfix, also make some changes in configs of ClamAV.
p In #[code clamav-milter.conf] you need the following:
pre
| MilterSocket unix:/run/clamav/milter.sock
| ClamdSocket unix:/run/clamav/clamd.ctl
p Also, in case you need ClamAV to add headers also in case a message is free of viruses add #[code AddHeader Add] or #[code AddHeader Replace] option. The difference between them is detaily described in config file itself.
p Before starting ClamAV you need to update its virus definitions with #[code freshclam] util. Also, enable and start #[code clamav-freshclam] systemd service to keep definitions recent.
p I don't know how it is in other distros, but, for whatever reason, an Arch Linux's package doesn't have a systemd service file for the ClamAV milter. So I just copy it here from <a href="https://wiki.archlinux.org/index.php/ClamAV#Using_the_milter" rel="noopener noreferrer" target="_blank">ArchWiki</a>:
pre
| [Unit]
| Description='ClamAV Milter'
| After=clamav-daemon.service
|
| [Service]
| Type=forking
| ExecStart=/usr/bin/clamav-milter --config-file /etc/clamav/clamav-milter.conf
|
| [Install]
| WantedBy=multi-user.target
p Save it as #[code /usr/lib/systemd/system/clamav-milter.service] and run #[code systemctl daemon-reload].
p Next you need to enable and start #[code clamav-daemon] and #[code clamav-milter].