Saltar al contenido
Luis ZambranaDevOps · Seguridad · Forense

Servidor de correo seguro en Docker: docker-mailserver, Roundcube y Let's Encrypt

Servidor de correo completo en Docker con docker-mailserver, Rspamd, ClamAV y Roundcube, certificados de Let's Encrypt con Caddy, SPF, DKIM, DMARC y MTA-STS, y cómo resolver el bloqueo del puerto 25 en DigitalOcean.

Servidor de correo seguro: sobre con candado y los registros SPF, DKIM, DMARC y MTA-STS

En la entrada anterior armamos un servidor DNS propio con BIND9 en Docker, con primario, secundario y una zona preparada para el correo. Ahora le sacamos el jugo: vamos a montar un servidor de correo completo y seguro, con webmail, antispam, antivirus, certificados de Let's Encrypt y todos los registros que hacen que tus mails lleguen a la bandeja de entrada y no a spam.

Todo sobre Docker, en un droplet de DigitalOcean, para el dominio de ejemplo tudominio.ar. Y te cuento de entrada el problema que nadie te dice hasta que ya armaste todo: DigitalOcean bloquea el envío de correo por el puerto 25. Tiene solución, y la vamos a dejar resuelta.

Todo lo que está acá lo probé antes de publicarlo: el arranque del servidor, el cifrado en cada puerto, la firma DKIM, el relay, el login del webmail a través del proxy y los rechazos a quien quiera usar tu servidor para mandar spam o hacerse pasar por otro.

Qué vamos a armar

                               Internet
                                  │
                     ┌────────────┴─────────────┐
                     │  Cloud Firewall (DO)     │
                     │  25 · 80 · 443 · 465 · 993│
                     └────────────┬─────────────┘
                                  │
  Droplet mail.tudominio.ar ──────┼──────────────────────────────────
                                  │
        ┌─────────────────────────┼──────────────────────┐
        │ 80/443                  │ 25 · 465 · 993       │
        ▼                         ▼                      │
 ┌──────────────┐        ┌─────────────────────┐         │
 │    Caddy     │        │  docker-mailserver  │         │
 │ Let's Encrypt├───────▶│ Postfix · Dovecot   │         │
 │ proxy + HSTS │ certs  │ Rspamd · ClamAV     │         │
 └──────┬───────┘        │ Fail2ban            │         │
        │                └─────────┬───────────┘         │
        ▼                          │ IMAP 993 / SMTP 465  │
 ┌──────────────┐                  │ (con TLS)            │
 │  Roundcube   │◀─────────────────┘                      │
 │   webmail    │                                         │
 └──────────────┘                                         │
                                   │ salida: puerto 2525  │
                                   ▼                      │
                          Relay SMTP externo ─────────────┘──▶ Gmail, Outlook...
Pieza Qué hace
docker-mailserver 16 El servidor de correo en sí: Postfix (envía y recibe), Dovecot (buzones e IMAP), Rspamd (antispam y firma DKIM), ClamAV (antivirus) y Fail2ban (bloquea a quien prueba contraseñas). Todo configurado con variables, sin pelearse con decenas de archivos.
Roundcube 1.6 El webmail, en su versión nonroot: el proceso nunca corre como root.
Caddy 2 Proxy web que saca y renueva solo los certificados de Let's Encrypt. El mismo certificado lo usa el servidor de correo.
Relay SMTP Un servicio externo que entrega tus mails salientes, para esquivar el bloqueo del puerto 25.

¿Por qué docker-mailserver y no Mailcow? Mailcow es excelente, pero pide como mínimo unos 6 GB de RAM y esconde mucho de lo que quiero que entiendas. Con docker-mailserver cada pieza está a la vista y alcanza con un droplet bastante más chico.

Requisitos: los DNS

Esto es lo primero, porque un servidor de correo sin los DNS correctos no funciona: no te llega nada, o lo que mandás va directo a spam. Si seguiste la entrada anterior, casi todo ya está en tu zona de BIND. Si tu DNS está en otro lado (Cloudflare, tu hosting), cargá lo mismo ahí.

Registro Nombre Valor ¿Ya está?
A mail IP del droplet de correo Sí
A webmail IP del droplet de correo Sí
A mta-sts IP del droplet de correo Sí
MX @ 10 mail.tudominio.ar. Sí
TXT (SPF) @ v=spf1 mx -all (lo ajustamos en el paso 3) Sí
TXT (DMARC) _dmarc v=DMARC1; p=none; adkim=s; aspf=r; rua=mailto:[email protected] Sí
TXT (MTA-STS) _mta-sts v=STSv1; id=2026102101 Sí
TXT (TLS-RPT) _smtp._tls v=TLSRPTv1; rua=mailto:[email protected] Sí
SRV _submissions._tcp / _imaps._tcp 465 / 993 en mail.tudominio.ar Sí
CAA @ 0 issue "letsencrypt.org" Sí
TXT (DKIM) mail._domainkey Se genera en el paso 6 No, todavía
PTR (DigitalOcean) mail.tudominio.ar Se define en el paso 1

En la zona de ejemplo, la IP del correo es 203.0.113.30 (rango de documentación, reemplazala por la tuya).

El problema del puerto 25 en DigitalOcean

Vamos de frente con esto, porque es lo que más frustración genera. La documentación oficial de DigitalOcean dice textualmente que los puertos SMTP 25, 465 y 587 están bloqueados en los droplets para evitar spam y abusos. El bloqueo aplica a todos los droplets por defecto, incluso si usás una Reserved IP.

Hay que entender bien qué está bloqueado:

  • Recibir correo funciona. El bloqueo es de salida. Gmail, Outlook o quien sea pueden entregarte mails a tu puerto 25 sin problema.
  • Tus usuarios pueden conectarse a tu servidor por 465 y 993 (son conexiones entrantes).
  • Lo que no funciona es entregar tus mails a otros servidores, porque eso sale por el puerto 25.

¿Cómo se resuelve? Hay dos caminos:

  1. Pedir el desbloqueo por ticket. Podés intentarlo, pero la documentación oficial no ofrece un proceso de desbloqueo y recomienda usar un proveedor externo. En cuentas con antigüedad y pagos al día a veces lo habilitan, pero no es algo con lo que puedas contar.
  2. Usar un relay SMTP (smarthost), que es lo que vamos a hacer. Tu servidor le pasa los mails salientes a un servicio especializado, que los entrega por vos. Para esquivar el bloqueo, muchos relays aceptan conexiones en el puerto 2525, que DigitalOcean no bloquea. SendGrid, Mailgun, Postmark, Brevo y SMTP2GO, entre otros, lo ofrecen (confirmalo en la documentación del que elijas).

El relay, además, mejora la entrega: las IPs de los proveedores cloud suelen tener mala reputación, porque cualquiera crea un droplet y manda spam. Un relay tiene IPs con buena reputación y gente dedicada a cuidarlas. Por eso hasta empresas que sí pueden usar el puerto 25 terminan usando un relay.

Lo importante es que vos seguís teniendo el control: los buzones, la recepción, el antispam, el webmail y la firma DKIM de tus mails son tuyos. El relay solo hace el último tramo de la entrega.

Paso 1: el droplet de correo

En DigitalOcean, Create → Droplets (si no tenés cuenta, con este enlace te dan crédito de prueba y a mí me suma para seguir con estas guías):

Crear cuenta en DigitalOcean con crédito de prueba

Opción Valor
Imagen Ubuntu 24.04 LTS (o la LTS más nueva)
Plan Basic, Regular, 4 GB RAM / 2 vCPU (USD 24/mes)
Autenticación SSH Key
IPv6 Sin activar (ver más abajo)
Monitoring Activado
Nombre mail.tudominio.ar

Tres decisiones importantes:

  • El nombre del droplet es obligatorio que sea mail.tudominio.ar. DigitalOcean arma el PTR (DNS inverso) con el nombre del droplet. Gmail, Outlook y casi todos los servidores verifican que la IP que les habla resuelva hacia atrás a un nombre que a su vez apunte a esa IP. Sin eso, te rechazan o te mandan a spam. Si ya lo creaste con otro nombre, renombralo desde el panel.
  • 4 GB por el antivirus. ClamAV carga su base de firmas en memoria y usa bastante más de 1 GB. Si no querés antivirus, alcanza con 2 GB (USD 12/mes) poniendo ENABLE_CLAMAV=0, pero para un correo de verdad lo recomiendo.
  • IPv6 apagado, salvo que lo configures completo. Si el droplet tiene IPv6, también tenés que agregar el registro AAAA y cuidar el PTR de IPv6. Un IPv6 a medio configurar es una causa clásica de rechazos. Arrancá con IPv4 y, si querés, sumalo después.

Cuando el droplet esté creado, comprobá el PTR desde tu máquina:

dig -x 203.0.113.30 +short
# mail.tudominio.ar.
dig mail.tudominio.ar +short
# 203.0.113.30

Las dos consultas tienen que "cerrar el círculo". Después preparalo igual que los servidores DNS (usuario sin root, SSH solo con clave, actualizaciones automáticas, Docker y el daemon.json): está todo en el paso 4 de la entrada anterior. Además, poné la hora local:

sudo timedatectl set-timezone America/Argentina/Buenos_Aires

Paso 2: Cloud Firewall del correo

Igual que con el DNS, filtramos antes del droplet, porque Docker se saltea UFW. Creá un firewall correo:

Puerto Protocolo Origen Para qué
22 TCP Solo tu IP (o tu VPN) SSH
25 TCP Todos Recibir correo de otros servidores
80 TCP Todos Validación de Let's Encrypt y redirección a HTTPS
443 TCP Todos Webmail y política MTA-STS
465 TCP Todos Envío desde clientes (TLS directo)
993 TCP Todos IMAP para leer el correo (TLS directo)

Fijate lo que no está: ni 110 (POP3), ni 143 (IMAP sin cifrar), ni 587 (envío con STARTTLS). Solo dejamos los puertos con TLS desde el primer byte (465 y 993), que es lo que recomienda hoy el RFC 8314. Menos puertos abiertos, menos superficie de ataque.

Paso 3: preparar el relay

Creá una cuenta en el relay que elijas. Todos piden lo mismo:

  1. Verificar el dominio. Te van a pedir que agregues algunos registros en tu DNS, normalmente uno o dos CNAME para su propia firma DKIM y, a veces, un registro para el "return-path". Cargalos en tu zona de BIND (en ns1), subí el serial y recargá, como vimos en la entrada anterior:

    cd /opt/dns
    nano zonas/db.tudominio.ar
    docker exec bind9 named-checkzone tudominio.ar /var/lib/bind/zonas/db.tudominio.ar
    docker exec bind9 rndc reload tudominio.ar
  2. Actualizar el SPF. Ahora quien entrega tus mails es el relay, así que tiene que estar autorizado. Cada proveedor te dice qué include usar:

    @        IN  TXT "v=spf1 mx include:spf.tu-relay.com -all"
  3. Anotar las credenciales SMTP: servidor (por ejemplo smtp.tu-relay.com), puerto 2525, usuario y clave. Usá una clave o API key exclusiva para este servidor, así si alguna vez se filtra la revocás sin tocar nada más.

Paso 4: los archivos

En el droplet de correo:

sudo mkdir -p /opt/correo
sudo chown luis: /opt/correo
cd /opt/correo
mkdir -p caddy/sitio/mta-sts/.well-known roundcube/config roundcube/db docker-data/dms/config

La estructura final:

/opt/correo/
├── docker-compose.yml
├── mailserver.env
├── caddy/
│   ├── Caddyfile
│   └── sitio/mta-sts/.well-known/mta-sts.txt
├── roundcube/
│   ├── config/seguridad.php
│   └── db/                      ← base SQLite del webmail
└── docker-data/dms/             ← buzones, estado, logs y configuración
    └── config/fail2ban-jail.cf

caddy/Caddyfile

{
    email [email protected]
    # Siempre Let's Encrypt (coincide con el registro CAA del DNS)
    acme_ca https://acme-v02.api.letsencrypt.org/directory
}

(seguridad) {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "same-origin"
        Permissions-Policy "camera=(), microphone=(), geolocation=()"
        -Server
        -X-Powered-By
    }
}

# Caddy saca el certificado de mail.tudominio.ar, que usa el servidor de correo
mail.tudominio.ar {
    import seguridad
    redir https://webmail.tudominio.ar{uri} permanent
}

webmail.tudominio.ar {
    import seguridad
    reverse_proxy roundcube:8000
}

mta-sts.tudominio.ar {
    import seguridad
    root * /srv/mta-sts
    file_server
}

Un detalle clave: el certificado de mail.tudominio.ar lo saca Caddy y el servidor de correo lo lee directamente del volumen de Caddy. Un solo lugar emite y renueva, y docker-mailserver detecta la renovación solo. Fijamos acme_ca a Let's Encrypt porque nuestro registro CAA solo autoriza a Let's Encrypt. Si Caddy probara con otra CA, la emisión fallaría.

También sacamos X-Powered-By, que si no delata la versión exacta de PHP del webmail.

caddy/sitio/mta-sts/.well-known/mta-sts.txt

version: STSv1
mode: testing
mx: mail.tudominio.ar
max_age: 86400

Esta es la política MTA-STS: le dice a Gmail, Outlook y compañía "para entregarme correo, usá TLS sí o sí, y solo con un certificado válido de mail.tudominio.ar". Sin esto, un atacante en el medio podría forzar una entrega sin cifrar. Arrancamos en testing (solo reporta) y en el paso 9 la pasamos a enforce.

mailserver.env

# --- General ---
LOG_LEVEL=info
[email protected]

# --- Antispam y firma DKIM: todo con Rspamd ---
ENABLE_RSPAMD=1
ENABLE_OPENDKIM=0
ENABLE_OPENDMARC=0
ENABLE_POLICYD_SPF=0
ENABLE_AMAVIS=0
ENABLE_SPAMASSASSIN=0
RSPAMD_LEARN=1
RSPAMD_GREYLISTING=1

# --- Antivirus y bloqueo de ataques ---
ENABLE_CLAMAV=1
ENABLE_FAIL2BAN=1

# --- Nadie puede enviar como otra persona ---
SPOOF_PROTECTION=1

# --- TLS: solo versiones y cifrados modernos, certificado de Caddy ---
TLS_LEVEL=modern
SSL_TYPE=manual
SSL_CERT_PATH=/caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.tudominio.ar/mail.tudominio.ar.crt
SSL_KEY_PATH=/caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.tudominio.ar/mail.tudominio.ar.key

# --- Salida por el relay (puerto 2525, no bloqueado en DigitalOcean) ---
DEFAULT_RELAY_HOST=[smtp.tu-relay.com]:2525

Lo que conviene entender:

  • Rspamd hace todo: antispam, verificación de SPF/DKIM/DMARC de lo que llega y firma DKIM de lo que sale. Por eso apagamos OpenDKIM, OpenDMARC, SpamAssassin y Amavis: hacer lo mismo dos veces solo suma consumo y errores. Si querés afinarlo, tenés la entrada de configuración de Rspamd.
  • RSPAMD_LEARN=1: cuando un usuario mueve un mail a la carpeta Junk, Rspamd aprende que es spam. Y al revés.
  • RSPAMD_GREYLISTING=1: al primer mail de un remitente desconocido le contesta "probá de nuevo en un rato". Los servidores legítimos reintentan; muchos bots de spam no. El costo es que el primer mail de alguien nuevo puede demorar unos minutos.
  • SPOOF_PROTECTION=1: un usuario autenticado solo puede mandar desde su dirección. Lo probé: si luis@ intenta mandar como jefe@, el servidor responde Sender address rejected: not owned by user. Es la defensa básica contra el fraude del "mail del jefe".
  • TLS_LEVEL=modern: solo TLS 1.2 y 1.3, con cifrados fuertes.
  • DEFAULT_RELAY_HOST: todo lo saliente va al relay. Las credenciales no van acá: las cargamos con un comando en el paso 5.

POP3 no aparece porque viene apagado por defecto, y así lo dejamos.

docker-data/dms/config/fail2ban-jail.cf

[DEFAULT]
# No banear nunca al webmail (ver explicación abajo)
ignoreip = 127.0.0.1/8 172.30.0.20
bantime = 1h
findtime = 10m
maxretry = 5

Esto evita un problema que encontré probando y que casi ninguna guía menciona. Cuando alguien prueba contraseñas en el webmail, el servidor de correo ve los fallos como si vinieran de la IP del contenedor de Roundcube (172.30.0.20). Fail2ban terminaría baneando a Roundcube, y el webmail se cae para todos: un atacante te tiraría el servicio con solo errarle a la clave. Por eso Roundcube queda excluido. Los intentos contra el webmail los frena el propio Roundcube, que lo configuramos a continuación.

Para los ataques directos a IMAP o SMTP, Fail2ban banea la IP 1 hora después de 5 fallos en 10 minutos.

roundcube/config/seguridad.php

<?php
// Sesión corta y atada a la IP del usuario
$config['session_lifetime'] = 30;
$config['ip_check'] = true;
$config['session_samesite'] = 'Strict';

// Máximo 3 logins fallidos por minuto por cuenta
$config['login_rate_limit'] = 3;

// El usuario puede editar su firma y su nombre, pero no la dirección de envío
$config['identities_level'] = 1;

// Caddy es el proxy de confianza: así Roundcube registra la IP real del usuario
$config['proxy_whitelist'] = ['172.30.0.10'];
  • login_rate_limit cuenta los fallos por cuenta, no por IP. Lo probé: después de 3 claves erróneas, esa cuenta queda frenada un rato aunque el atacante cambie de IP. Es muy efectivo contra ataques distribuidos, y el único costo es que el usuario legítimo puede tener que esperar un minuto mientras dura el ataque.
  • proxy_whitelist: sin esto, Roundcube vería a todo el mundo con la IP de Caddy. Con esto, en los logs aparece la IP real: Login failed for [email protected] ... (X-Forwarded-For: 198.51.100.77).
  • identities_level = 1 va de la mano con SPOOF_PROTECTION: el usuario no puede ni intentar cambiar su dirección de remitente.

docker-compose.yml

services:
  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    networks:
      correo:
        ipv4_address: 172.30.0.10
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy/sitio:/srv:ro
      - caddy-data:/data
      - caddy-config:/config
    read_only: true
    tmpfs:
      - /tmp
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]
    security_opt:
      - no-new-privileges:true

  mailserver:
    image: mailserver/docker-mailserver:16
    container_name: mailserver
    hostname: mail.tudominio.ar
    restart: unless-stopped
    env_file: mailserver.env
    ports:
      - "25:25"
      - "465:465"
      - "993:993"
    networks:
      correo:
        ipv4_address: 172.30.0.30
        # Roundcube encuentra al servidor por su nombre real, y el certificado valida
        aliases:
          - mail.tudominio.ar
    volumes:
      - ./docker-data/dms/mail-data/:/var/mail/
      - ./docker-data/dms/mail-state/:/var/mail-state/
      - ./docker-data/dms/mail-logs/:/var/log/mail/
      - ./docker-data/dms/config/:/tmp/docker-mailserver/
      - caddy-data:/caddy-data:ro
      - /etc/localtime:/etc/localtime:ro
    cap_add:
      - NET_ADMIN   # Fail2ban necesita crear reglas de bloqueo
    stop_grace_period: 1m

  roundcube:
    image: roundcube/roundcubemail:1.6.x-apache-nonroot
    container_name: roundcube
    restart: unless-stopped
    networks:
      correo:
        ipv4_address: 172.30.0.20
    volumes:
      - ./roundcube/db:/var/roundcube/db
      - ./roundcube/config:/var/roundcube/config:ro
    environment:
      - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mail.tudominio.ar
      - ROUNDCUBEMAIL_DEFAULT_PORT=993
      - ROUNDCUBEMAIL_SMTP_SERVER=ssl://mail.tudominio.ar
      - ROUNDCUBEMAIL_SMTP_PORT=465
      - ROUNDCUBEMAIL_USERNAME_DOMAIN=tudominio.ar
      - ROUNDCUBEMAIL_PLUGINS=archive,zipdownload,managesieve,markasjunk
      - ROUNDCUBEMAIL_SKIN=elastic
    depends_on:
      - mailserver

networks:
  correo:
    ipam:
      config:
        - subnet: 172.30.0.0/24

volumes:
  caddy-data:
  caddy-config:

Lo que hay que destacar:

  • IPs fijas en la red interna: son las que usan fail2ban-jail.cf y proxy_whitelist. Si las cambiás, cambialas en los tres lugares.
  • El alias mail.tudominio.ar: Roundcube se conecta al servidor de correo con TLS (ssl://) aunque estén en la misma máquina, y como lo busca por su nombre real, el certificado de Let's Encrypt valida. Nada de "desactivar la verificación del certificado" para que ande.
  • El volumen de Caddy montado :ro en el mailserver: el servidor de correo puede leer los certificados, pero no tocarlos.
  • Caddy endurecido como el BIND de la entrada anterior: solo lectura, sin capacidades salvo abrir los puertos 80 y 443, y sin escalada de privilegios.
  • Roundcube nonroot corre como www-data (uid 33). Por eso la carpeta de su base necesita ese dueño (lo hacemos en el paso 5).
  • Versiones fijadas por rama: docker-mailserver:16 recibe los parches de la versión 16 pero nunca salta solo a la 17, que va a traer cambios incompatibles (ya se anunció que Redis pasa a Valkey). Los saltos de versión mayor se hacen a mano y después de leer el changelog.

Paso 5: levantar, en orden

El orden importa, por dos motivos: el servidor de correo necesita que el certificado ya exista, y docker-mailserver tiene una particularidad que no está muy a la vista.

Primero, Caddy y el certificado. Los registros mail, webmail y mta-sts ya tienen que resolver a la IP del droplet:

cd /opt/correo
sudo chown 33:33 roundcube/db
docker compose up -d caddy
docker logs -f caddy

Esperá a ver certificate obtained successfully para los tres nombres y salí con Ctrl+C. Si falla, casi siempre es el DNS (todavía no propagó) o el puerto 80 cerrado en el Cloud Firewall.

Después, el servidor de correo, y creá la cuenta enseguida. Esta es la particularidad: docker-mailserver se apaga si en 120 segundos no encuentra ninguna cuenta. Lo vi en el laboratorio: avisa cada 10 segundos (You need at least one mail account to start Dovecot) y al final se cierra. Así que tené el comando listo:

docker compose up -d mailserver
docker exec -it mailserver setup email add [email protected]
# Te pide la contraseña dos veces: usá una larga y única

Ahora creá los alias para todas las direcciones que usamos en el DNS. Si no existen, los reportes de DMARC, TLS-RPT y Let's Encrypt rebotan:

for a in postmaster hostmaster abuse seguridad dmarc tlsrpt; do
  docker exec mailserver setup alias add [email protected] [email protected]
done

Cargá las credenciales del relay con el comando de docker-mailserver (no en variables de entorno, que quedan visibles con docker inspect):

docker exec -it mailserver setup relay add-auth tudominio.ar USUARIO_DEL_RELAY
# Te pide la clave
chmod 600 docker-data/dms/config/postfix-sasl-password.cf

Recreá el contenedor para que tome todo (las credenciales del relay y el fail2ban-jail.cf se aplican al crear el contenedor, no con un simple reinicio) y levantá el webmail:

docker compose up -d --force-recreate mailserver
docker compose up -d
docker logs mailserver | tail -5
# ... mail.tudominio.ar is up and running

Podés verificar que el relay quedó bien configurado:

docker exec mailserver postconf relayhost smtp_sasl_auth_enable smtp_tls_security_level
# relayhost = [smtp.tu-relay.com]:2525
# smtp_sasl_auth_enable = yes
# smtp_tls_security_level = encrypt

Ese encrypt es importante: Postfix exige TLS para hablar con el relay. Tu usuario y tu clave nunca viajan en texto plano.

Paso 6: DKIM, la firma de tus mails

DKIM firma cada mail que sale con una clave privada que solo tiene tu servidor. El que recibe busca la clave pública en tu DNS y verifica que el mail no fue alterado y que realmente salió de tu dominio. Si querés la teoría completa, la tenés en Firmar correos con DKIM.

Generá las claves (RSA de 2048 bits, selector mail):

docker exec mailserver setup config dkim

El comando muestra el registro que hay que publicar:

Here is the content of the TXT DNS record mail._domainkey.tudominio.ar that you need to create:

v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzIgB8d05TwIo...

Acá hay una trampa: una cadena TXT no puede superar los 255 caracteres, y esta clave tiene unos 400. En BIND hay que partirla en varios pedazos entre comillas. Este comando lo hace por vos:

docker exec mailserver cat /tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-tudominio.ar.public.dns.txt \
  | tr -d '\n"' | fold -w 200 | sed 's/.*/    "&"/' \
  | { echo 'mail._domainkey IN TXT ('; cat; echo; echo '    )'; }

Sale algo así, listo para pegar en la zona (en ns1), en lugar de las líneas de DKIM que dejamos comentadas:

mail._domainkey IN TXT (
    "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzIgB8d05TwIoyqJwHO6K60I7Aeq28D2pdnHz7hme0QBUFDptXpFxja7ykMDjuP7ic5vvmXyk6KX00X7b1+bRU4fY8ofHoT886cbZZ8Uu6IP7A4uHlN+0Z5QgEL1OGdYO/p2HJE1vil"
    "xx8FejUE8OeurhaqfoYqW2rpNBBOP2Yszj0Jr9Rtsg8Z98FnL3gkSIuuhLtqJRIGJuJJ6pzLD2NALn5mWZ35BnRl/8iEhJ2kUaPvBEHphW2FIFJwqmxK77rEyboH4KaA0PXs4CpnfZJonpyXLaj4JZ+9ma5HOBsUPyT78BUYQKZnHDl6q8cT6vii4NCGGwf2CaWvdGk7"
    "f2eQIDAQAB"
    )

Subí el serial, validá y recargá BIND como siempre, y verificá desde afuera:

dig +short mail._domainkey.tudominio.ar TXT

Los resolvers unen los pedazos solos, así que el que recibe tu mail ve la clave completa. Lo probé cargándola en BIND y verificando el mail firmado: lleva la cabecera DKIM-Signature: v=1; a=rsa-sha256; d=tudominio.ar; s=mail.

Ojo con el nombre de la clave. En docker-mailserver 16.0 el archivo se llama rsa-2048-mail-tudominio.ar.public.dns.txt. La versión 17 ya anuncia que lo cambia a tudominio.ar-mail. Si el comando no lo encuentra, mirá con docker exec mailserver ls /tmp/docker-mailserver/rspamd/dkim/.

Paso 7: el webmail

Entrá a https://webmail.tudominio.ar. Gracias a ROUNDCUBEMAIL_USERNAME_DOMAIN, el usuario es solo luis (sin @tudominio.ar) y la clave es la que creaste en el paso 5.

Pantalla de inicio de sesión de Roundcube en webmail.tudominio.ar

Ya podés leer y enviar. El plugin managesieve te deja crear filtros y respuestas automáticas desde Configuración → Filtros, y markasjunk agrega el botón de "Spam", que además le enseña a Rspamd.

Para el celular o la compu, cualquier cliente moderno (Thunderbird, K-9/Thunderbird para Android, Mail de iOS) se configura con:

Servidor Puerto Seguridad Usuario
Entrante (IMAP) mail.tudominio.ar 993 SSL/TLS [email protected]
Saliente (SMTP) mail.tudominio.ar 465 SSL/TLS [email protected]

Gracias a los registros SRV que pusimos en el DNS, Thunderbird lo encuentra solo con poner la dirección.

Opcional: doble factor en el webmail

Roundcube 1.6 no trae doble factor de fábrica. Existe un plugin de la comunidad muy usado, twofactor_gauthenticator, que agrega códigos TOTP (Google Authenticator, Aegis, etc.). No publica versiones estables, así que fijalo a un commit concreto que hayas revisado, para que no baje código distinto cada vez que el contenedor arranca:

    environment:
      - ROUNDCUBEMAIL_COMPOSER_PLUGINS=alexandregz/twofactor_gauthenticator:dev-master#5e7bf2e
      - ROUNDCUBEMAIL_PLUGINS=archive,zipdownload,managesieve,markasjunk,twofactor_gauthenticator

Lo instalé con esa configuración y carga sin errores en Roundcube 1.6. Como es código de terceros que va a ver las sesiones de tus usuarios, evaluá vos si te conviene: revisá el repositorio y activalo primero en una cuenta de prueba.

Paso 8: probar todo (y que sea seguro)

El cifrado en cada puerto

Desde tu máquina:

# Envío (465) e IMAP (993): TLS directo
openssl s_client -connect mail.tudominio.ar:465 -brief </dev/null
openssl s_client -connect mail.tudominio.ar:993 -brief </dev/null

# Recepción (25): STARTTLS
openssl s_client -connect mail.tudominio.ar:25 -starttls smtp -brief </dev/null

Las tres tienen que mostrar Protocol version: TLSv1.3 (o 1.2) y Verification: OK. Muchos proveedores de internet hogareños bloquean la salida al puerto 25, así que si la tercera no conecta desde tu casa, probala desde otro servidor o con las herramientas online del final.

No somos un open relay

Un open relay es un servidor que acepta mails de cualquiera para cualquiera: el sueño de un spammer, y la forma más rápida de terminar en todas las listas negras. Desde otro servidor con swaks:

swaks --server mail.tudominio.ar --from [email protected] --to [email protected] --quit-after RCPT
# <** 554 5.7.1 <[email protected]>: Relay access denied

Relay access denied es exactamente lo que queremos ver. También podés usar el test de open relay de MXToolbox.

No se puede suplantar a otro usuario

swaks --server mail.tudominio.ar:465 --tlsc \
  --auth-user [email protected] --from [email protected] --to [email protected] --quit-after RCPT
# <~* 553 5.7.1 <[email protected]>: Sender address rejected: not owned by user [email protected]

La nota de entrega

  1. Entrá a mail-tester.com, copiá la dirección que te da y mandale un mail desde el webmail, con un asunto y un texto normales (no "prueba"). Con todo bien configurado deberías estar en 10/10 o muy cerca, y si falta algo te dice exactamente qué.
  2. Mandate un mail a una cuenta de Gmail y abrí Mostrar original. Tenés que ver SPF: PASS, DKIM: PASS y DMARC: PASS.
  3. Pasá el dominio por el test de correo de internet.nl, que revisa TLS, DANE, SPF, DKIM, DMARC y MTA-STS, y es de lo más exigente que hay. DANE y DNSSEC te van a quedar en rojo: los dos necesitan la cadena de DNSSEC cerrada, y como conté en la entrada del DNS, hoy NIC.ar no ofrece cómo cargar el DS. Para eso está MTA-STS, que cumple una función parecida (exigir TLS con un certificado válido) sin depender de DNSSEC.

Los bloqueos funcionando

docker exec mailserver setup fail2ban            # IPs baneadas
docker exec mailserver fail2ban-client status dovecot
docker logs roundcube | grep "Login failed"     # intentos contra el webmail, con IP real

Paso 9: subir la seguridad con el tiempo

Arrancamos con DMARC en p=none y MTA-STS en testing a propósito: primero observás, después exigís. Si exigís de entrada y algo está mal configurado, tus propios mails empiezan a rebotar.

Semana 1-2: mirá los reportes. A dmarc@ te van a llegar reportes diarios de Google, Microsoft y otros, con cada IP que mandó correo con tu dominio y si pasó SPF y DKIM. Son XML poco amigables; servicios como dmarcian o Postmark DMARC los traducen gratis.

Cuando estén limpios, en la zona de BIND:

; DMARC: primero cuarentena, después rechazo
_dmarc   IN  TXT "v=DMARC1; p=quarantine; adkim=s; aspf=r; rua=mailto:[email protected]"
; ...y unas semanas después:
_dmarc   IN  TXT "v=DMARC1; p=reject; adkim=s; aspf=r; rua=mailto:[email protected]"

Con p=reject, nadie puede mandar mails haciéndose pasar por tu dominio: los servidores que respetan DMARC (todos los grandes) los rechazan. Y con DMARC estricto ya cumplís el requisito para BIMI, el logo de tu marca en la bandeja de entrada.

Para MTA-STS, cambiá mode: testing por mode: enforce en mta-sts.txt, subí max_age a 604800 (una semana) y cambiá el id del registro _mta-sts en el DNS (por ejemplo, a la fecha del cambio). Si no cambiás el id, los demás servidores no se enteran de que la política es nueva.

El día a día

Cuentas y alias:

docker exec -it mailserver setup email add [email protected]
docker exec -it mailserver setup email update [email protected]   # cambiar clave
docker exec mailserver setup email del [email protected]
docker exec mailserver setup alias add [email protected] [email protected]
docker exec mailserver setup email list

Logs:

docker logs -f mailserver
docker exec mailserver tail -f /var/log/mail/mail.log

Actualizar (una vez por semana es un buen ritmo):

cd /opt/correo
docker compose pull
docker compose up -d

Backups: lo que no podés perder está en /opt/correo: docker-data/ (buzones, cuentas, claves DKIM y configuración), roundcube/ y los archivos de configuración. Los certificados no hacen falta, Caddy los vuelve a sacar. Ojo, que ahí están las claves DKIM y las credenciales del relay: el backup va cifrado. Hay ideas en Backups centralizados con Duplicati y en The Backup Guardian.

Reputación: revisá cada tanto que tu IP no esté en listas negras con el blacklist check de MXToolbox. Y si querés ir más allá con el antispam, en esta entrada armamos una lista automatizada de IPs para bloquear.

Checklist final

  • ✅ PTR correcto: el droplet se llama mail.tudominio.ar y la IP resuelve en las dos direcciones.
  • ✅ Cloud Firewall con solo 25, 80, 443, 465 y 993 abiertos (SSH solo para vos).
  • ✅ Solo TLS: 465 y 993 con TLS directo, 25 con STARTTLS, TLS 1.2 como mínimo, certificados de Let's Encrypt que se renuevan solos.
  • ✅ SPF, DKIM y DMARC publicados y verificados, con el plan para llegar a p=reject.
  • ✅ MTA-STS y TLS-RPT: nadie puede forzar una entrega sin cifrar sin que te enteres.
  • ✅ No es open relay y no se puede suplantar a otro usuario (probado).
  • ✅ Rspamd antispam con aprendizaje, ClamAV antivirus y greylisting.
  • ✅ Fail2ban contra fuerza bruta en IMAP y SMTP, sin poder usarse para tirar el webmail.
  • ✅ Webmail sin root, con sesión corta, límite de intentos por cuenta, IP real en los logs y cabeceras de seguridad.
  • ✅ Salida por relay en el puerto 2525, con credenciales cifradas en tránsito y fuera de las variables de entorno.
  • ✅ Versiones fijadas por rama y backups cifrados.

Con esto tenés un servidor de correo propio de nivel profesional, sobre un DNS que también es tuyo. Es bastante trabajo, pero ahora entendés cada pieza de algo que la mayoría usa sin saber cómo funciona. Y si algo no te sale, dejame un comentario. ¡Nos vemos en la próxima!

Comentarios

Todavía no hay comentarios. ¡Sé el primero!

Seguí leyendo

Docker

Seguridad en Servidores de Emails

En otras oportunidades hablamos de spam, envios de mails masivos y otros tantos, pero se nos pasó un tema interezante que puso de moda (y exige) las empresas Hotmail y Gmail, hablamos de la Seguridad en Servidores de Emails implementando SPF – DMARC y DKIM (hay yapa al final)

11 min