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.
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:
- 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.
- 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):
| 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:
-
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 -
Actualizar el SPF. Ahora quien entrega tus mails es el relay, así que tiene que estar autorizado. Cada proveedor te dice qué
includeusar:@ IN TXT "v=spf1 mx include:spf.tu-relay.com -all" -
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é: siluis@intenta mandar comojefe@, el servidor respondeSender 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_limitcuenta 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 = 1va de la mano conSPOOF_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.cfyproxy_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
:roen 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:16recibe 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 atudominio.ar-mail. Si el comando no lo encuentra, mirá condocker 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.

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
- 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é.
- Mandate un mail a una cuenta de Gmail y abrí Mostrar original. Tenés que ver
SPF: PASS,DKIM: PASSyDMARC: PASS. - 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.ary 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!