Post

mailcow dockerized sécurisé

mailcow dockerized sécurisé

Ce billet est une suite à mailcow dockerized.

Prérequis

On suppose que votre configuration réseau est pleinement fonctionnelle.

En ajout au précédent article, je précise que :

  • Le DNS inverse (Reverse DNS ou rdns) doit être activé et paramétré
  • Vous devez avoir un IP v4 fixe

Cette configuration est à réaliser avec votre fournisseur d’accès internet.

Chez Free, cela passe par la gestion de votre compte; partie “Ma Freebox”, “Fonctionnalités avancées” :

  • Demander une adresse IP fixe V4 full-stack
  • Personnaliser mon reverse DNS

Introduction

L’objectif ici est de sécuriser, autant que possible, une installation mailcow: dockerized.

Le nom de domain ‘example.com’ est ici générique ; à vous de le remplacer par le votre !

Avec pour objectifs :

  • Une intégration avec Crowdsec (bouncer)
  • Un support IPv4 et IPv6

Ce qui suppose un réseau IPv4/v6 & une instance Crowdsec (LAPI) OK

Intégration avec Traefik (revue)

Nous partons sur une configuration avec Traefik 3 sous Docker avec gestion des services par fichiers yaml.

traefik.yml

Dans le fichier traefik.yml (configuration principale) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
global:
  checkNewVersion: false
  sendAnonymousUsage: false

api:
  debug: false
  dashboard: true
  insecure: false

ping: {}

entryPoints:
  http:
    address: ":80"
    forwardedHeaders:
      trustedIPs: &trustedIps # cloudflare (only usefull if cloudflare usage)
        - "173.245.48.0/20"
        - "103.21.244.0/22"
        - "103.22.200.0/22"
        - "103.31.4.0/22"
        - "141.101.64.0/18"
        - "108.162.192.0/18"
        - "190.93.240.0/20"
        - "188.114.96.0/20"
        - "197.234.240.0/22"
        - "198.41.128.0/17"
        - "162.158.0.0/15"
        - "104.16.0.0/13"
        - "104.24.0.0/14"
        - "172.64.0.0/13"
        - "131.0.72.0/22"
        - "2400:cb00::/32"
        - "2606:4700::/32"
        - "2803:f800::/32"
        - "2405:b500::/32"
        - "2405:8100::/32"
        - "2a06:98c0::/29"
        - "2c0f:f248::/32"
        # local & lan ip ranges
        # - "127.0.0.1/16"
        # - "192.168.xxx.xxx/24"
        # - "172.xxx.xxx.xxx/12"
        # - "IPv6::/64"
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https

  https:
    address: ":443"
    forwardedHeaders:
      trustedIPs: *trustedIps
    http3:
      advertisedPort: 443
    http:
      tls:
        certResolver: letsencrypt
        domains:
          # adjust 'example.com' with your main base domain name
          - main: example.com
            sans:
              - "*.example.com"

certificatesResolvers:
  letsencrypt:
    acme:
      caServer: https://acme-v02.api.letsencrypt.org/directory # production
      # https://acme-staging-v02.api.letsencrypt.org/directory # staging/testing
      storage: /etc/traefik/acme.json
      # here we use DNS Challenge, adjust if you use another challenge
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"
          - "1.0.0.1:53"
          - "8.8.4.4:53"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock" # adjust if you use Docker Socket Proxy
    exposedByDefault: false
    network: proxy
    watch: true
  file:
    directory: etc/traefik/traefik.d/
    watch: true

log:
  level: "INFO" # DEBUG
  maxSize: 32
  maxBackups: 16
  maxAge: 7
  compress: true

accessLog:
  filePath: "/log/access.log"
  format: common
  filters:
    statusCodes:
      - "200-299"
      - "400-599"
  bufferingSize: 0
  fields:
    headers:
      defaultMode: drop
      names:
        User-Agent: keep

experimental:
  fastProxy: {}
  plugins: # adjust versions to keep on 'latest'
    # Refer to crowdsec.yml
    bouncer: # Crowdsec Bouncer plugin (to communicate with Crowdsec LAPI and Appsec)
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: "v1.4.4"
    # Refer to geoblock.yml
    geoblock: # Block access based on geographic location (detected by the ip address)
      moduleName: "github.com/PascalMinder/geoblock"
      version: "v0.3.3"

Réseau Traefik (Docker)

La création du réseau Docker pour Traefik se fait comme suit :

1
2
3
4
5
6
7
8
9
docker network create \
    --driver bridge \
    --attachable \
    --subnet=xxx.xxx.xxx.xxx/xx \
    --ip-range=xxx.xxx.xxx.xxx/xx \
    --opt "com.docker.network.bridge.name=traefik" \
    --opt "com.docker.network.driver.mtu=1500" \
    --opt "com.docker.network.enable_ipv6=true" \
    traefik

docker-compose.yml

Pour informations, le docker-compose.yml ressemble à ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
networks:
  traefik:
    external: true

services:
  traefik:
    extends: # used to define common settings over my other composes
      file: ../_common/common.yml
      service: x-common
    container_name: traefik
    hostname: traefik
    image: traefik:${VERSION} # aka latest
    restart: always
    healthcheck:
      test: traefik healthcheck || exit 1
    ports:
      - "${PORT_HTTP}:80"
      - "${PORT_HTTPS}:443/tcp"
      - "${PORT_HTTPS}:443/udp"
    expose:
      - "80"
      - "443"
    networks:
      traefik:
        ipv4_address: xxx.xxx.xxx.xxx # ip adress of previously created traefik docker network
    secrets:
      - cloudflare_api_email
      - cloudflare_api_key
      - cloudflare_zone_api_key
    env_file:
      - ./env/traefik.env
    deploy:
      resources:
        limits:
          memory: 2G
    tmpfs:
      - /tmp:rw,async,noatime,nodiratime,uid=${VM_UID},gid=${VM_GID},noexec,nosuid,size=1G
      - /plugins-storage:rw,async,noatime,nodiratime,uid=${VM_UID},gid=${VM_GID},noexec,nosuid,size=512M
    volumes:
      - ./conf/traefik.yml:/etc/traefik/traefik.yml:ro # main traefik configuration file
      - ./conf/traefik:/etc/traefik/traefik.d:ro # dynamic services as single yaml file
      - ./conf/bouncer/ban.min.html:/ban.html:ro # html page to serve banned from Crowdsec plugin
      - ./datas/traefik/acme.json:/etc/traefik/acme.json:rw # ssl certificate contents for Traefik
      - /var/logs/traefik:/log:rw # log files (formatted for simplified analysis)

traefik.env

Le fichier traefik.env est relativement simple :

1
2
3
4
5
6
7
# If you use Cloudflare as DNS providers, adjust for others
CF_API_EMAIL_FILE: /run/secrets/cloudflare_api_email
CF_API_KEY_FILE: /run/secrets/cloudflare_api_key

GIN_MODE: 'release'
MASTER_DOMAIN: 'example.com'
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL: 'user@provider.com'

mailcow.yml

Dans le fichier mailcow.yml (gestion dynamique) :

Les routeurs sont séparés si vous utilisez “kereis/traefik-certs-dumper” pour obtenir des certificats SSL individuels pour les sous-domaines

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
http:
  routers: # routers ar separate in case you use 'kereis/traefik-certs-dumper' to get subdomains individual ssl certificates
    mailcow-autodiscover:
      entryPoints:
        - https
      rule: Host(`autodiscover.example.com`)
      middlewares:
        - mailcow@file
      tls:
        certResolver: letsencrypt
      service: mailcow@file

    mailcow-autoconfig:
      entryPoints:
        - https
      rule: Host(`autoconfig.example.com`)
      middlewares:
        - mailcow@file
      tls:
        certResolver: letsencrypt
      service: mailcow@file

    mailcow-mta-sts:
      entryPoints:
        - https
      rule: Host(`mta-sts.example.com`)
      middlewares:
        - mailcow@file
      tls:
        certResolver: letsencrypt
      service: mailcow@file

    mailcow-mail:
      entryPoints:
        - https
      rule: Host(`mail.example.com`)
      middlewares:
        - mailcow@file
      tls:
        certResolver: letsencrypt
      service: mailcow@file

    mailcow-webmail:
      entryPoints:
        - https
      rule: Host(`webmail.example.com`)
      middlewares:
        - mailcow@file
      tls:
        certResolver: letsencrypt
      service: mailcow@file

  services:
    mailcow:
      loadBalancer:
        servers:
          - url: https://ip:port # ip:port of mailcow instance

  middlewares:
    httpsredirect:
      redirectScheme:
        scheme: https
    ratelimit:
      rateLimit:
        average: 256
        burst: 512
        period: 10s
    defaults:
      headers:
        frameDeny: false
        customFrameOptionsValue: SAMEORIGIN
        browserXssFilter: false
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 15552000
        hostsProxyHeaders:
          - "X-Forwarded-Host"
          - "X-Forwarded-Proto"
          - "X-Forwarded-For"        
        customRequestheaders:
          Alt-Svc: "h3=':443'; ma=86400"
        customResponseHeaders:
          Alt-Svc: "h3=':443'; ma=86400"
    security:
      headers:
        customRequestheaders:
          X-Content-Type-Options: ""
          X-Forwarded-Proto: https
        customResponseHeaders:
          Permissions-Policy: "fullscreen=(*), display-capture=(self), accelerometer=(), battery=(), camera=(), autoplay=(self), vibrate=(self), geolocation=(self), midi=(self), notifications=(*), push=(*), microphone=(self), magnetometer=(self), gyroscope=(self), payment=(self)"
          X-Forwarded-Proto: https
          X-Permitted-Cross-Domain-Policies: "none"
          X-Content-Type-Options: ""
        sslProxyHeaders:
          X-Forwarded-Proto: https
        referrerPolicy: strict-origin-when-cross-origin

    mailcow:
      chain:
        middlewares:
          - httpsredirect@file
          - ratelimit@file
          - defaults@file
          - security@file

crowdsec.yml

Dans le fichier crowdsec.yml (gestion dynamique) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
http:
  middlewares:
    crowdsec:
      plugin:
        bouncer:
          enabled: true
          logLevel: ERROR
          updateIntervalSeconds: 60
          updateMaxFailure: 0
          defaultDecisionSeconds: 60
          httpTimeoutSeconds: 10
          forwardedHeadersCustomName: 'X-Custom-Header'
          remediationHeadersCustomName: 'cs-remediation'
          forwardedHeadersTrustedIPs:
            # Cloudflare IPs
            # same as 'trustedIPs: &trustedIps' in traefik.yml
          clientTrustedIPs:
            # same as 'local & lan ip ranges' in traefik.yml
          crowdsecMode: stream
          crowdsecLapiKey: "[CHANGEME]"
          crowdsecLapiHost: "[IP]:[PORT]" # ip:port of crowdsec LAPI instance
          crowdsecLapiScheme: http
          crowdsecLapiTLSInsecureVerify: false
          CrowdsecAppsecEnabled: true
          CrowdsecAppsecHost: "[IP]:[PORT]" # ip:port of crowdsec Appsec instance
          CrowdsecAppsecFailureBlock: true
          crowdsecAppsecUnreachableBlock: true
          banHTMLFilePath: "./ban.html" # html page to display "you're banned" message
          redisCacheEnabled: true # false if you don't want to use Redis for caching
          redisCacheHost: "[IP]:[PORT]" # ip:port of Redis instance

geoblok.yml

Dans le fichier geoblok.yml (gestion dynamique) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
http:
  middlewares:
    geoblock:
      plugin:
        geoblock:
          silentStartUp: true
          allowLocalRequests: true
          logLocalRequests: false
          logAllowedRequests: false
          logApiRequests: false
          api: https://get.geojs.io/v1/ip/country/{ip}
          apiTimeoutMs: 750
          cacheSize: 512
          forceMonthlyUpdate: true
          allowUnknownCountries: false
          unknownCountryApiResponse: "nil"
          addCountryHeader: true
          allowedIPAddresses:
            # same as 'local & lan ip ranges' in traefik.yml
          blackListMode: true # all country codes listed in 'countries' are not allowed to access
          countries:
            - RU # Russian Federation (the)

Mailcow

docker-compose.override.yml

L’utilité de la surcharge du docker-compose.yml est de rendre les modification apportés non modifiables à chaque mise à jour de mailcow: dockerized.

Cela permet de définir les points suivants :

  • Spécification des serveur DNS à utiliser pour les résolutions
  • Health check pour chaque conteneur ou c’est possible
  • Limitation de mémoire (et possiblement de cpu)
  • Exclusion de la prise en charge par Watchtower
  • Simplification du nommage des conteneurs (utile pour paramétrer Uptime Kuma)
  • Ajout des rapport DMARC de RSpamd (Ofelia)
  • utilisation de traefik-certs-dumper pour récupérer les certificats SSL depuis Traefik
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
x-defaults: &x-defaults
  healthcheck:
    interval: 60s
    timeout: 10s
    retries: 5
    start_period: 60s
  deploy:
    resources:
      limits:
        memory: 64M

x-labels: &x-labels
  <<: *x-defaults
  labels:
    logging: "metrics"
    com.centurylinklabs.watchtower.enable: false

x-common: &x-common
  <<: *x-labels
  dns:
    - [DNS N° 1]
    - [DNS N° 2]

services:

  unbound-mailcow:
    <<: *x-labels
    container_name: ${COMPOSE_PROJECT_NAME}-unbound
    deploy:
      resources:
        limits:
          memory: 256M

  mysql-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-mysql
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 512M

  redis-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-redis
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 128M

  clamd-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-clamd
    deploy:
      resources:
        limits:
          memory: 4G

  rspamd-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-rspamd
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 2G
    labels:
      ofelia.enabled: "true"
      ofelia.job-exec.rspamd_dmarc_reporting_yesterday.schedule: "@every 24h"
      ofelia.job-exec.rspamd_dmarc_reporting_yesterday.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/bin/rspamadm dmarc_report $(date --date yesterday '+%Y%m%d') > /var/lib/rspamd/dmarc_reports_last_log 2>&1 || exit 0\""

  php-fpm-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-phpfpm
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 256M

  sogo-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-sogo
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 512M

  dovecot-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-dovecot
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 512M

  postfix-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-postfix
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 1G
    ulimits:
      nofile:
        soft: 65535
        hard: 65535

  memcached-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-memcached
    healthcheck:
      test: uname -a || exit 1

  nginx-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-nginx
    healthcheck:
      test: uname -a || exit 1

  acme-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-acme
    healthcheck:
      test: uname -a || exit 1

  netfilter-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-netfilter
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 128M

  watchdog-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-watchdog
    environment:
      - CHECK_UNBOUND=0
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 128M

  dockerapi-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-dockerapi
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 256M

  olefy-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-olefy
    healthcheck:
      test: uname -a || exit 1

  ofelia-mailcow:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-ofelia
    healthcheck:
      test: uname -a || exit 1
    deploy:
      resources:
        limits:
          memory: 1G
    depends_on:
      - rspamd-mailcow

  certdumper:
    <<: *x-common
    container_name: ${COMPOSE_PROJECT_NAME}-certdumper
    image: ghcr.io/kereis/traefik-certs-dumper:latest
    command: --restart-containers mailcowdockerized-postfix,mailcowdockerized-dovecot,mailcowdockerized-nginx
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "/usr/bin/healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 512M
    network_mode: none
    environment:
      POST_HOOK_FILE_PATH: "/hook/hook.sh"
      DOMAIN: ${MAILCOW_HOSTNAME}
      ACME_FILE: /acme.json
      OVERRIDE_UID: 0
      OVERRIDE_GID: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /opt/docker/traefik/datas/traefik:/traefik:ro # acme.json folder
      - /opt/mailcow/output:/output:rw
      - /opt/mailcow-dockerized/data/assets/ssl:/certificates:rw
      - /opt/mailcow/posthook.sh:/hook/hook.sh

Précision concernant certdumper :

  • Il est nécessaire de créer le répertoire /opt/mailcow/output (sinon ajuster dans le compose)
  • Et le script shell posthook.sh suivant doit être créé :
1
2
3
#!/bin/bash
cp -r /output/. /certificates/
exit 0

start/stop/restart .sh

L’utilité de ces petits scripts est minime , sauf dans le cas d’intéractions par des commandes externes :)

start.sh

1
2
3
4
5
6
if [ "$(id -u)" != "0" ]; then
        echo "This script must be run as root" 1>&2
        exit 1
fi
docker compose up -d
exit 0

stop.sh

1
2
3
4
5
6
if [ "$(id -u)" != "0" ]; then
        echo "This script must be run as root" 1>&2
        exit 1
fi
docker compose down
exit 0

restart.sh

1
2
3
4
cd /opt/mailcow-dockerized # adjust if you installed elsewhere
sh stop.sh
sh start.sh
exit 0

mailcow.conf

Les seuls éléments à modifier/vérifier ou mettre à jour dans ce fichier de configuration sont :

  • MAILCOW_HOSTNAME (qui doit être le nom complet du serveur de mail, exemple: mail.example.com)
  • HTTP_PORT / HTTPS_PORT (si les ports 80 et 443 sont déjà pris)
  • TZ (qui définit la zone horaire du serveur)
  • ADDITIONAL_SAN (qui doit contenir au minimum mta-sts.*)
  • ADDITIONAL_SERVER_NAMES (comme pour ADDITIONAL_SAN)
  • WATCHDOG_NOTIFY_EMAIL (si vous désirez recevoir des notifications sur la surveillance du serveur)
  • WATCHDOG_NOTIFY_BAN y si vous souhaitez être notifié par mail des IP bannies)
  • WATCHDOG_NOTIFY_START y si vous souhaitre recevoir un mail indiquant le démarrage du serveur)
  • **IPV4NETWORK** _(à ajuster si au démarrage du serveur il vous insulte parce que des IPs sont déjà utilisées…)

main.cf

Il s’agit du fichier de configuration principale de Postfix ; situé dans le dossier data/conf/postfix.

Si vous mettez à jour votre instance mailcow, alors je vous recommande de la stopper. Ensuite vous retirez tout ce qui se trouve sous les lignes suivantes :

1
2
# DO NOT EDIT ANYTHING BELOW #
# User overrides #

extra.cf

Il s’agit de la sucharge du fichier main.cf, situé dans le même dossier, dans lequel vous mettez juste :

1
2
myhostname = mail.example.com
smtp_helo_name = mail.example.com

Rapports DMARC

La configuration, côté conteneur, est déjà spécifiée dans docker-compose.override.yml.

DMARC est une technologie qui utilise SPF et DKIM pour permettre aux propriétaires de domaines de publier des politiques concernant la manière dont les messages électroniques comportant leur domaine dans le champ “From” (RFC5322) doivent être traités.

Par exemple, DMARC peut être configuré pour exiger qu’un MTA récepteur mette en quarantaine ou rejette les messages qui ne disposent pas d’un identifiant DKIM ou SPF aligné.

DMARC peut également être configuré pour demander des rapports aux MTA distants concernant ces messages afin d’aider à identifier les abus et/ou les erreurs de configuration et de faciliter la prise de décisions éclairées concernant l’application des politiques.

Sources:

Pour activer ces rapports, il est nécessaire de créer le fichier data/conf/rspamd/local.d/dmarc.conf comme suit :

1
2
3
4
5
6
7
8
9
10
11
12
13
reporting {
    enabled = true;
    email = 'noreply@example.com';
    domain = 'example.com';
    org_name = 'Example';
    helo = 'rspamd';
    smtp = 'postfix';
    smtp_port = 25;
    from_name = 'DMARC Report';
    msgid_from = 'rspamd.mail.example.com';
    max_entries = 2k;
    keys_expire = 2d;
}

L’adresse noreply@example.com doit exister (ou être un alias valide)

Paramètres des boites mails

Pour passer certains tests de validations du serveur de mail, est nécessaire de désactiver les options suivantes :

  • Appliquer le TSL entrant
  • Appliquer le TSL sortant

Courriel / Configuration / Boites de réception / Edition de la boite de réception / Politique de chiffrement

DNS

Entrées DNS à mettre en place

RUA : rapports DMARC agrégé (vue complète de l’ensemble du trafic d’un domaine)

RUF : rapports DMARC médico-légal (copies expurgées des courriels individuels qui ne sont pas 100% conformes à DMARC)

Description Name Type Value
SPF @ TXT “v=spf1 a ra=postmaster -all”
DKIM dkim._domainkey TXT “v=DKIM1;k=rsa;t=s;s=email;p=(1)”
DMARC _dmarc TXT “v=DMARC1; p=reject; rua=mailto:(2)@dmarc-reports.cloudflare.net,mailto:postmaster@example.com; ruf=mailto:postmaster@example.com”
MTA STS mta-sts CNAME mail.example.com
MTA STS _mta-sts TXT “v=STSv1; id=20220324000000Z”

A noter ce qui suit :

  • (1) : la valeur de p est indiquée dans la configuration du domain dans la webui de Mailcow : Domaine: example.com (dkim._domainkey)
  • (2) : Cette entrée est propre à Cloudflare et ajoutée automatiquement une fois activée la gestion de DMARC

MTS STS

La configuration des entrées DNS a été mise en place comme ci-dessus (MTA STS).

Il convient aussi de créer le fichier mta-sts.txt dans le répertoire data/web/.well-known/ de mailcow dockerized comme suit :

1
2
3
4
5
version: STSv1
mode: enforce
max_age: 172800
mx: mail.example.com
mx: *.example.com

Vous devriez être en mesure de l’afficher en allant sur l’addresse ci-dessous:

1
https://mta-sts.example.com/.well-known/mta-sts.txt

Enregistrement DS

Cet enregistrement dns est à paramétrer chez votre gestionnaire de nom de domaine.

L’enregistrement DS est utilisé pour vérifier l’authenticité des zones enfants des zones DNSSEC. L’enregistrement de clé DS d’une zone parent contient un hachage du KSK d’une zone enfant. Un résolveur DNSSEC peut donc vérifier l’authenticité de la zone enfant en hachant son enregistrement KSK et en le comparant à celui de l’enregistrement DS de la zone parent.

Par exemple, chez OVH avec Cloudflare ça se présente ainsi :

Key Tag Flag Algorithme Clé publique (encodée base64)
2371 257 13 (3)

La correspondance des champs OVH – Cloudflare est la suivante:

  • key tag : Balise clé
  • Flag : Indicateurs
  • Algorithme : Algorithme + Type Digest – 2
  • Clé publique (encodée base64) : Clé publique

(3) La valeur de la clée publique à renseigner dans le formulaire d’OVH est indiquée chez Cloudflare dans les paramètres du domaine :

1
Domain / DNS / Settings / DNSSEC / DS Record / Public Key

Tests

Une fois que la configuration de l’ensemble est correctement réalisée et que mailcow démarre bien, il est temps de tester le serveur.

Il faut au préalable avoir paramétrer un domain ainsi qu’une boite mail

dnssec-debugger.verisignlabs.com

Ce premier test permet de vérifier la partie DNSSEC du serveur.

Pour informations, toutes les cases doivent être cochées vertes.

mail-tester.com

Ce second test permet de vérifier par un envoit / réception de mail qu’il est fonctionnel.

Il suffit d’envoyer un mail quelconque à l’adresse spécifié sur le formulaire puis de cliquer sur le bouton de vérification.

Dans l’absolu, un score de 10/10 est attendu.

checktls.com/TestReceiver

Ce site permet de tester un peu plus profondément le serveur.

Voici comment procéder :

  • eMail Target : renseigner une adresse email valide
  • More Options : dépliez pour avoir toutes les options
  • Check MTA-STS : cochez
  • Check DANE : cochez
  • Check Cert Sigs : cochez
  • IPv4 : cochez
  • IPv6 : cochez (si vous souhaitez tester la partie IP v6)
  • Check DNSSEC : cochez
  • No DNS Cache : cochez (pour être certain d’utiliser les informations à jour)
  • Run Test : cliquez !

La aussi, dans l’absolu, vous devez avoir OK partout.

Vous pouvez analyser le rapport détaillé des actions en dessous du tableau récapitulatif

email-security-scans.org

Alors ce dernier test est à faire uniquement si les précédents indiquent que tout est ok.

Sinon, ça ne sert pas à grand chose de le faire.

Pensez à désactiver la limitation d’envoi sur le domaine et/ou la boite mail dans la webui de mailcow

Voici comment procéder :

  • Vous saisissez une adresse mail valide
  • Vous cliquez sur *start
  • Vous attendez qu’il vous dise que les mails sont parti et qu’il attend la réponse
  • Vous ouvrez votre client mail
  • Vous ouvrez le mail reçus de la part de email-security-scans.org
  • Vous faire “Répondres à Tous” puis vous envoyez LES mails

Vous n’avez plus qu’à attendre que le site réceptionne tous les mails que vous venez de lui envoyer (c’est long).

Si vous n’avez pas de dns inverse en IP v6, les tests liés échoueront !

Concernant la partie MTS-STS et Dane, il se peut que le serveur échoue.

Lisez bien les annotation de ces tests ; elles précisent que les tests ne portent pas sur votre serveur de mail mais sur sa capacité à répondre aux exigeances de email-security-scans.org.

Et là j’ai encore à fouiller pour savoir à quoi ça correspond :p

Résultats

Globalement, voici à quoi vous attendre comme résultats sur ces différents tests :

  • dnssec-debugger.verisignlabs.com : tout est coché au vert
  • mail-tester.com : score de 10/10
  • checktls.com/TestReceiver : tout sur OK
  • email-security-scans.org : note de 7/10

Conclusion

Avec cette configuration, normalement, vous disposez d’un serveur :

  • Opérationnel en IPv4
  • Opérationnel en IPv6
  • Protégé par Crowdsec
  • Protégé par Geoblock
  • Supportant DNSSEC ainsi que SPF, DKIM, DMARC

Ça vous donne un serveur pleinement opérationnel, protégé et à jour des mesures de protection efficace.

Cet article est sous licence CC BY 4.0 par l'auteur.