Servidor DNS propio con BIND9 en Docker: primario, secundario y bien blindado
Cómo montar un DNS autoritativo propio con BIND9 en Docker: primario y secundario en dos droplets de DigitalOcean, transferencias firmadas con TSIG, contenedores endurecidos, delegación en NIC.ar y la zona lista para el servidor de correo.
Tener tu propio servidor DNS es de esas cosas que separan a quien "usa" internet de quien la administra. Cada vez que alguien escribe tu dominio, manda un mail o valida un certificado, termina preguntándole a un servidor DNS. Si ese servidor es tuyo, controlás todo: qué registros existen, cuánto duran en caché, quién puede copiar la zona y cómo se comporta ante un ataque.
En esta entrada vamos a montar un DNS autoritativo con BIND9 sobre Docker, con dos servidores (primario y secundario) en dos droplets de DigitalOcean, para el dominio de ejemplo tudominio.ar registrado en NIC Argentina. Nada de laboratorio de juguete: es la misma arquitectura que usa cualquier empresa que se toma en serio su DNS. Además, la zona va a quedar lista para la próxima entrada, donde montamos un servidor de correo seguro con webmail que se apoya en todo esto.
Todo lo que está acá lo probé antes de publicarlo: la configuración de BIND, las transferencias firmadas, el endurecimiento del contenedor, la recarga de zonas y las pruebas de seguridad. Si copiás y pegás cambiando el dominio y las IPs, tiene que andar.
Qué vamos a armar
Internet
│
┌────────────────┴────────────────┐
│ consultas DNS (UDP/TCP 53) │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ ns1.tudominio.ar │ NOTIFY │ ns2.tudominio.ar │
│ 203.0.113.10 │ ────────▶ │ 203.0.113.20 │
│ BIND9 PRIMARIO │ ◀──────── │ BIND9 SECUNDARIO │
│ (acá se edita) │ AXFR │ (copia automática) │
└─────────────────────┘ firmado └─────────────────────┘
Droplet 1 con TSIG Droplet 2
- ns1 es el primario: ahí vive el archivo de zona y ahí hacés los cambios.
- ns2 es el secundario: copia la zona sola cada vez que ns1 le avisa que hubo un cambio.
- La copia de la zona (transferencia) va firmada con una clave TSIG: nadie más puede pedirla ni hacerse pasar por el primario.
- Los dos son solo autoritativos: responden por
tudominio.ary por nada más. No son resolvers abiertos, que es justamente lo que buscan los atacantes para hacer ataques de amplificación.
Las IPs
203.0.113.xson del rango reservado para documentación (RFC 5737). Reemplazalas por las IPs reales de tus droplets.
¿Por qué dos servidores y no uno?
Porque NIC.ar te exige al menos dos DNS para delegar el dominio, y tiene sentido: si tenés uno solo y se cae, tu dominio desaparece de internet. Sin DNS no hay web, no hay correo y no hay nada. Con dos servidores en regiones distintas, si se cae un datacenter el otro sigue respondiendo. Es la regla básica de cualquier DNS en producción.
Lo que vas a necesitar
| Qué | Para qué | Costo aproximado (octubre 2026) |
|---|---|---|
Dominio tudominio.ar en NIC.ar |
El nombre que vamos a servir | $25.500 ARS por año (.com.ar: $8.500) |
| Droplet 1 (ns1) | Servidor primario | USD 6/mes (1 GB RAM, 1 vCPU) |
| Droplet 2 (ns2) | Servidor secundario | USD 6/mes (1 GB RAM, 1 vCPU) |
| Clave Fiscal de ARCA | Operar en NIC.ar | Gratis |
Para BIND sobra con el droplet más chico: un DNS autoritativo con un par de zonas usa menos de 50 MB de RAM.
Si todavía no tenés cuenta en DigitalOcean, podés crearla con mi enlace de referido. Te dan crédito de prueba para hacer todo este laboratorio sin gastar, y a mí me suma para seguir haciendo estas guías:
Paso 1: registrar el dominio en NIC.ar
Los dominios .ar se registran en NIC Argentina a través de Trámites a Distancia (TAD), entrando con CUIT/CUIL y Clave Fiscal de ARCA (la ex AFIP). Acá está el detalle que frena a mucha gente, así que vamos despacio:
- Para registrar y administrar tus propios dominios, NIC.ar pide Clave Fiscal nivel 2 o superior.
- Si vas a gestionar dominios de otra persona o de una empresa (un cliente, la empresa donde trabajás) o querés que alguien opere los tuyos, se necesita nivel 3 o superior, porque entra en juego la representación.
Mi consejo: sacá directamente el nivel 3. Si te dedicás a esto, tarde o temprano vas a administrar el dominio de un cliente o de tu empresa, y el nivel 3 también te lo piden en muchos otros trámites de ARCA. Se gestiona en ARCA validando tu identidad (en una dependencia o por los medios de validación que ARCA tenga habilitados). Mejor tenerlo antes de necesitarlo que trabarte en medio de un trabajo.
Con la clave en regla:
- Entrá a nic.ar, hacé clic en INGRESO y elegí CUIT/CUIL y Clave Fiscal.
- Buscá el dominio que querés y seguí el trámite de registro.
- Pagá el arancel. El registro dura un año y se renueva todos los años: anotá el vencimiento, porque un dominio vencido se pierde.
Todavía no delegues el dominio. La delegación (decirle a NIC.ar cuáles son tus servidores DNS) se hace al final, cuando ns1 y ns2 ya estén respondiendo. Si delegás a servidores que todavía no existen, el dominio queda sin resolver.
Paso 2: crear los dos droplets
En el panel de DigitalOcean, Create → Droplets, dos veces:
| ns1 | ns2 | |
|---|---|---|
| Imagen | Ubuntu 24.04 LTS (o la LTS más nueva) | Ubuntu 24.04 LTS (o la LTS más nueva) |
| Plan | Basic, Regular, USD 6/mes | Basic, Regular, USD 6/mes |
| Región | Nueva York (NYC1) | Otra región, por ejemplo Toronto (TOR1) o San Francisco (SFO3) |
| Autenticación | SSH Key (nunca contraseña) | SSH Key |
| Nombre (hostname) | ns1.tudominio.ar |
ns2.tudominio.ar |
Dos detalles que parecen menores pero no lo son:
- El nombre del droplet importa. DigitalOcean arma el registro PTR (DNS inverso) con el nombre del droplet, y solo si es un nombre completo como
ns1.tudominio.ar. Para el DNS es prolijidad; para el correo, en la próxima entrada, va a ser obligatorio. - Regiones distintas. Si los dos droplets están en el mismo datacenter, cualquier corte del datacenter te deja sin DNS. Y si querés ir un paso más allá, lo ideal profesional es tener el secundario en otro proveedor.
Tildá también Monitoring (es gratis) para tener gráficos de CPU, RAM y red.
Paso 3: firewall en la nube (y por qué UFW no alcanza)
Antes de instalar nada, cerramos todo desde afuera. Acá hay una trampa clásica: Docker se saltea UFW. Cuando publicás un puerto con Docker, Docker escribe sus propias reglas de iptables y el tráfico entra sin pasar por las reglas de UFW. Mucha gente cree que tiene un puerto cerrado con ufw deny y en realidad está abierto a todo internet.
La solución práctica en DigitalOcean es el Cloud Firewall, que filtra el tráfico antes de que llegue al droplet, sin importar lo que haga Docker adentro. En Networking → Firewalls → Create Firewall, creá uno llamado dns con estas reglas de entrada:
| Tipo | Protocolo | Puerto | Origen |
|---|---|---|---|
| SSH | TCP | 22 | Solo tu IP (o tu VPN) |
| DNS | UDP | 53 | Todos (IPv4 e IPv6) |
| DNS | TCP | 53 | Todos (IPv4 e IPv6) |
Las reglas de salida pueden quedar como vienen. Aplicá el firewall a los dos droplets (en Apply to Droplets podés poner los dos, o una etiqueta dns que tengan ambos).
¿Por qué TCP 53 además de UDP? El DNS usa UDP para casi todo, pero las respuestas grandes (por ejemplo con DNSSEC) y las transferencias de zona entre ns1 y ns2 van por TCP. Si cerrás TCP 53, el secundario no puede copiar la zona y algunas consultas fallan.
Si tu IP de casa cambia seguido, no abras el 22 a todo el mundo: usá una VPN como la que armamos en Headscale y Tailscale y permití solo esa IP. Y si algún día te quedás afuera, siempre tenés la Recovery Console del panel de DigitalOcean.
Paso 4: preparar los servidores
Esto se hace en los dos droplets. Si querés el detalle completo del endurecimiento, lo tenés en Seguridad para Servidores Linux. Acá va lo mínimo indispensable.
Entrá como root con tu clave SSH y creá un usuario de trabajo:
adduser luis
usermod -aG sudo luis
# Copiar tu clave SSH al usuario nuevo
rsync --archive --chown=luis:luis ~/.ssh /home/luis
Probá en otra terminal que podés entrar como luis y usar sudo. Recién ahí cerrá el acceso de root y por contraseña:
sudo tee /etc/ssh/sshd_config.d/10-endurecido.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
sudo sshd -t && sudo systemctl reload ssh
Actualizaciones de seguridad automáticas:
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Instalá Docker desde el repositorio oficial. Tengo un tutorial viejito de Docker y docker-compose sobre Ubuntu, pero hoy lo más simple es el script oficial (leelo antes de ejecutarlo, como cualquier script que bajes de internet):
curl -fsSL https://get.docker.com -o get-docker.sh
less get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker luis # cerrá sesión y volvé a entrar
Por último, una configuración global de Docker para que los logs no llenen el disco y ningún contenedor pueda ganar privilegios:
sudo tee /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" },
"no-new-privileges": true,
"live-restore": true
}
EOF
sudo systemctl restart docker
Paso 5: generar las claves
Necesitamos dos tipos de clave:
- TSIG (
ns1-ns2): un secreto compartido entre ns1 y ns2. Firma las transferencias de zona y los avisos de cambio. Es la misma en los dos servidores. - rndc: la llave para administrar BIND en caliente (recargar zonas sin reiniciar). Cada servidor tiene la suya.
En ns1, creá la estructura y generá las claves usando la misma imagen de BIND, así no instalás nada en el sistema:
sudo mkdir -p /opt/dns/config /opt/dns/zonas
sudo chown -R luis: /opt/dns
cd /opt/dns
IMG=internetsystemsconsortium/bind9:9.20
# Clave TSIG compartida
docker run --rm --entrypoint tsig-keygen $IMG -a hmac-sha256 ns1-ns2 > config/tsig.key
# Clave rndc de este servidor
docker run --rm --entrypoint rndc-confgen $IMG -A hmac-sha256 -k rndc-key 2>/dev/null \
| sed -n '/^key/,/^};/p' > config/rndc.key
cat config/tsig.key
Vas a ver algo así (tu secreto va a ser otro):
key "ns1-ns2" {
algorithm hmac-sha256;
secret "dxo8sPcrxDpb5Yp05cs//NvIffBwHiBeheXedQCk000=";
};
Ahora los permisos. Dentro del contenedor, BIND corre como el usuario bind (uid 53) y tiene que poder leer las claves, pero nadie más:
sudo chown root:53 config/*.key
sudo chmod 640 config/*.key
Tratá
tsig.keycomo una contraseña. Con ese secreto, cualquiera podría pedirte la zona entera o mandarle cambios falsos al secundario. Guardalo en tu gestor de contraseñas (si no tenés uno, mirá Passbolt) y nunca lo subas a un repositorio.
Paso 6: ns1, el servidor primario
La estructura final en ns1 queda así:
/opt/dns/
├── docker-compose.yml
├── config/
│ ├── named.conf
│ ├── rndc.key
│ └── tsig.key
└── zonas/
└── db.tudominio.ar
La configuración: config/named.conf
Este archivo es el corazón del servidor. Cada bloque está comentado para que sepas por qué está ahí:
// ns1.tudominio.ar - servidor PRIMARIO (autoritativo, sin recursión)
include "/etc/bind/tsig.key"; // clave compartida con ns2
include "/etc/bind/rndc.key"; // clave para administrar con rndc
controls {
inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { "rndc-key"; };
};
options {
directory "/var/cache/bind";
listen-on { any; };
listen-on-v6 { none; };
// Autoritativo puro: no resuelve nombres ajenos para nadie.
recursion no;
allow-recursion { none; };
allow-query-cache { none; };
allow-query { any; };
// Nadie transfiere ni actualiza la zona salvo lo que se habilite por zona.
allow-transfer { none; };
allow-update { none; };
notify explicit;
// No regalar información del servidor.
version none;
hostname none;
server-id none;
// Respuestas chicas: menos material para ataques de amplificación.
minimal-responses yes;
dnssec-validation no; // no resolvemos, no hace falta validar
// Response Rate Limiting contra amplificación y floods.
rate-limit {
responses-per-second 10;
window 5;
};
};
logging {
channel salida {
stderr;
severity info;
print-time yes;
print-category yes;
print-severity yes;
};
category default { salida; };
category security { salida; };
category xfer-out { salida; };
category notify { salida; };
category rate-limit { salida; };
category lame-servers { null; };
};
zone "tudominio.ar" {
type primary;
file "/var/lib/bind/zonas/db.tudominio.ar";
// Solo ns2, y solo si firma el pedido con la clave TSIG.
allow-transfer { key "ns1-ns2"; };
also-notify { 203.0.113.20 key "ns1-ns2"; };
};
Lo más importante, en criollo:
recursion no: el servidor solo contesta por tu dominio. Si alguien le pregunta porgoogle.com, respondeREFUSED. Un DNS recursivo abierto en internet es una de las herramientas favoritas para ataques DDoS de amplificación, y tu IP termina en listas negras.allow-transfer { key "ns1-ns2"; }: la transferencia de zona (AXFR) le da a quien la pida todos tus registros, incluidos subdominios internos que quizá no querés mostrar. Solo la puede hacer quien tenga la clave TSIG.version none: sin esto, cualquiera puede preguntarversion.bindy saber exactamente qué versión corrés, y con eso buscar vulnerabilidades conocidas.rate-limit: el Response Rate Limiting (RRL) limita cuántas respuestas idénticas recibe una misma red por segundo. Un cliente normal ni se entera; un ataque de amplificación pierde casi toda su fuerza.loggingastderr: así todo se ve condocker logs, y la rotación ya la configuramos endaemon.json.
La zona: zonas/db.tudominio.ar
Acá van los registros del dominio. Ya la dejo completa con todo lo que va a necesitar el servidor de correo, así entendés desde ahora qué hace cada cosa:
; Zona tudominio.ar - archivo maestro en ns1
; Cada vez que cambies algo, subí el serial (formato AAAAMMDDNN).
$TTL 3600
$ORIGIN tudominio.ar.
@ IN SOA ns1.tudominio.ar. hostmaster.tudominio.ar. (
2026102101 ; serial
3600 ; refresh: cada cuánto el secundario consulta cambios
900 ; retry: si falla, reintenta a los 15 minutos
1209600 ; expire: el secundario deja de responder a los 14 días sin primario
300 ) ; TTL negativo: cuánto se recuerda un "no existe"
; --- Servidores de nombres (los mismos que cargás en NIC.ar) ---
@ IN NS ns1.tudominio.ar.
@ IN NS ns2.tudominio.ar.
ns1 IN A 203.0.113.10
ns2 IN A 203.0.113.20
; --- Solo Let's Encrypt puede emitir certificados para el dominio ---
@ IN CAA 0 issue "letsencrypt.org"
@ IN CAA 0 issuewild ";"
@ IN CAA 0 iodef "mailto:[email protected]"
; --- Sitio web (opcional) ---
; @ IN A 203.0.113.40
; www IN CNAME tudominio.ar.
; --- Correo ---
mail IN A 203.0.113.30
webmail IN A 203.0.113.30
mta-sts IN A 203.0.113.30
@ IN MX 10 mail.tudominio.ar.
; SPF: solo el servidor MX envía correo del dominio.
@ IN TXT "v=spf1 mx -all"
; DMARC: arrancar en p=none, y subir a quarantine/reject cuando los reportes estén limpios.
_dmarc IN TXT "v=DMARC1; p=none; adkim=s; aspf=r; rua=mailto:[email protected]"
; DKIM: la clave pública se genera en el servidor de correo (ver la segunda entrada).
; mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
; "p=MIIBIjANBgkqh...resto-de-la-clave..." )
; MTA-STS y TLS-RPT: obligan a usar TLS para entregarte correo y te avisan si falla.
_mta-sts IN TXT "v=STSv1; id=2026102101"
_smtp._tls IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
; Autoconfiguración de clientes (RFC 6186): Thunderbird y compañía encuentran solos los puertos.
_submissions._tcp IN SRV 0 1 465 mail.tudominio.ar.
_imaps._tcp IN SRV 0 1 993 mail.tudominio.ar.
_submission._tcp IN SRV 0 0 0 .
_imap._tcp IN SRV 0 0 0 .
_pop3._tcp IN SRV 0 0 0 .
_pop3s._tcp IN SRV 0 0 0 .
Vamos por partes, porque acá está la mitad del aprendizaje:
- SOA: la "ficha" de la zona.
hostmaster.tudominio.ar.es en realidad el correo[email protected](el primer punto reemplaza a la arroba). El serial es lo que mira el secundario para saber si hay cambios: si te olvidás de subirlo, ns2 nunca se entera. El formatoAAAAMMDDNN(fecha más un número de cambio del día) es el estándar de facto. - NS + A de ns1/ns2: quiénes son los servidores autoritativos. Como
ns1yns2están dentro del mismo dominio, NIC.ar también va a necesitar sus IPs (los famosos registros "glue"), lo vemos en el paso 9. - CAA: dice qué autoridades certificantes pueden emitir certificados para tu dominio. Con esto, si alguien intenta sacar un certificado de
tudominio.aren otra CA, la CA lo tiene que rechazar.issuewild ";"prohíbe los certificados comodín (*.tudominio.ar), que no vamos a usar. - Los registros de correo los explico uno por uno en la tabla del final, porque son la base de la segunda entrada.
Las líneas que empiezan con ; son comentarios. El sitio web y el DKIM quedan comentados hasta que los necesites.
El docker-compose.yml de ns1
Averiguá la IP pública del droplet:
ip -4 addr show eth0 | grep inet
La primera IP que aparece es la pública (la otra, 10.x.x.x, es interna de DigitalOcean). Con esa IP armá el compose:
services:
bind9:
image: internetsystemsconsortium/bind9:9.20
container_name: bind9
restart: unless-stopped
ports:
- "203.0.113.10:53:53/udp"
- "203.0.113.10:53:53/tcp"
volumes:
- ./config:/etc/bind:ro
- ./zonas:/var/lib/bind/zonas:ro
- cache:/var/cache/bind
# Contenedor endurecido
read_only: true
tmpfs:
- /run/named
- /tmp
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE, SETUID, SETGID]
security_opt:
- no-new-privileges:true
mem_limit: 256m
pids_limit: 100
volumes:
cache:
Qué hace cada parte del endurecimiento:
- Imagen oficial de ISC (los que desarrollan BIND), rama estable 9.20. La etiqueta
9.20recibe los parches de seguridad de esa rama cada vez que hacéspull. - Puerto publicado solo en la IP pública: Ubuntu trae
systemd-resolvedescuchando en127.0.0.53:53. Si publicás en0.0.0.0:53choca y el contenedor no arranca. Atando el puerto a la IP pública, cada uno vive en lo suyo. :roen configuración y zonas: aunque alguien comprometiera BIND, no podría modificar ni la configuración ni los registros.read_only: true: todo el sistema de archivos del contenedor es de solo lectura. Solo se puede escribir en la caché y en dostmpfsen memoria.cap_drop: [ALL]y después solo tres capacidades: abrir el puerto 53 (NET_BIND_SERVICE) y pasar de root al usuariobind(SETUIDySETGID). BIND arranca, abre el puerto y deja de ser root.no-new-privileges: ningún proceso del contenedor puede volver a ganar privilegios.- Límites de memoria y procesos: si algo se descontrola, se descontrola adentro de su cajita.
Levantar y verificar
Antes de levantar, BIND trae herramientas para revisar la sintaxis. Usalas siempre: un error en la zona puede dejar tu dominio sin responder.
cd /opt/dns
docker compose run --rm --entrypoint named-checkconf bind9 /etc/bind/named.conf
docker compose run --rm --entrypoint named-checkzone bind9 tudominio.ar /var/lib/bind/zonas/db.tudominio.ar
La segunda tiene que terminar con:
zone tudominio.ar/IN: loaded serial 2026102101
OK
Si está todo bien, levantamos:
docker compose up -d
docker logs bind9
Deberías ver zone tudominio.ar/IN: loaded serial 2026102101 y running. Un aviso de notify to 203.0.113.20#53 failed es normal ahora: ns2 todavía no existe.
Paso 7: ns2, el servidor secundario
En ns2 repetí la estructura, pero sin carpeta de zonas: la zona la va a copiar sola desde ns1.
sudo mkdir -p /opt/dns/config
sudo chown -R luis: /opt/dns
cd /opt/dns
Copiá la misma tsig.key desde ns1 (desde tu máquina, con scp, o pegando el contenido a mano) y generá una rndc.key propia para ns2:
IMG=internetsystemsconsortium/bind9:9.20
nano config/tsig.key # pegar EXACTAMENTE el mismo contenido que en ns1
docker run --rm --entrypoint rndc-confgen $IMG -A hmac-sha256 -k rndc-key 2>/dev/null \
| sed -n '/^key/,/^};/p' > config/rndc.key
sudo chown root:53 config/*.key
sudo chmod 640 config/*.key
El config/named.conf de ns2 es muy parecido al de ns1. Cambian el bloque server y la zona:
// ns2.tudominio.ar - servidor SECUNDARIO (copia la zona desde ns1)
include "/etc/bind/tsig.key";
include "/etc/bind/rndc.key";
controls {
inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { "rndc-key"; };
};
// Todo lo que se hable con ns1 va firmado con la clave TSIG.
server 203.0.113.10 {
keys { "ns1-ns2"; };
};
options {
directory "/var/cache/bind";
listen-on { any; };
listen-on-v6 { none; };
recursion no;
allow-recursion { none; };
allow-query-cache { none; };
allow-query { any; };
allow-transfer { none; };
allow-update { none; };
notify no;
version none;
hostname none;
server-id none;
minimal-responses yes;
dnssec-validation no;
rate-limit {
responses-per-second 10;
window 5;
};
};
logging {
channel salida {
stderr;
severity info;
print-time yes;
print-category yes;
print-severity yes;
};
category default { salida; };
category security { salida; };
category xfer-in { salida; };
category notify { salida; };
category rate-limit { salida; };
category lame-servers { null; };
};
zone "tudominio.ar" {
type secondary;
primaries { 203.0.113.10 key "ns1-ns2"; };
// Solo acepta avisos de cambio firmados por ns1.
allow-notify { key "ns1-ns2"; };
file "/var/lib/bind/db.tudominio.ar";
};
Fijate que el secundario tampoco entrega la zona a nadie (allow-transfer { none; }): un secundario mal configurado es la forma más común de filtrar una zona entera.
El docker-compose.yml de ns2 cambia la IP y, en lugar de montar zonas de solo lectura, usa un volumen donde BIND guarda la copia:
services:
bind9:
image: internetsystemsconsortium/bind9:9.20
container_name: bind9
restart: unless-stopped
ports:
- "203.0.113.20:53:53/udp"
- "203.0.113.20:53:53/tcp"
volumes:
- ./config:/etc/bind:ro
- secundaria:/var/lib/bind
- cache:/var/cache/bind
read_only: true
tmpfs:
- /run/named
- /tmp
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE, SETUID, SETGID]
security_opt:
- no-new-privileges:true
mem_limit: 256m
pids_limit: 100
volumes:
secundaria:
cache:
Levantalo y mirá los logs:
docker compose run --rm --entrypoint named-checkconf bind9 /etc/bind/named.conf
docker compose up -d
docker logs bind9
Si la clave TSIG coincide y el firewall deja pasar TCP 53, vas a ver la magia:
xfer-in: info: transfer of 'tudominio.ar/IN' from 203.0.113.10#53: connected using 203.0.113.10#53 TSIG ns1-ns2
xfer-in: info: zone tudominio.ar/IN: transferred serial 2026102101: TSIG 'ns1-ns2'
xfer-in: info: transfer of 'tudominio.ar/IN' from 203.0.113.10#53: Transfer status: success
Si en cambio aparece tsig verify failure o BADKEY, las dos tsig.key no son idénticas. Si aparece timed out, revisá que el Cloud Firewall tenga TCP 53 abierto.
Paso 8: probar que funciona... y que es seguro
Desde tu máquina (no desde los servidores), con dig. Las dos primeras pruebas son de funcionamiento y el resto son de seguridad. Todas tienen que dar lo esperado:
# 1. Los dos responden la misma zona, con el mismo serial
dig +short @203.0.113.10 tudominio.ar SOA
dig +short @203.0.113.20 tudominio.ar SOA
# 2. Responden con autoridad (flag "aa")
dig @203.0.113.20 mail.tudominio.ar A +norec | grep flags
# ;; flags: qr aa; ...
# 3. NO son resolvers abiertos: tiene que dar REFUSED
dig @203.0.113.10 google.com A | grep status
# ;; ->>HEADER<<- opcode: QUERY, status: REFUSED
# 4. NO entregan la zona sin clave: "Transfer failed."
dig @203.0.113.10 tudominio.ar AXFR
dig @203.0.113.20 tudominio.ar AXFR
# 5. NO dicen su versión: respuesta vacía
dig @203.0.113.10 version.bind CHAOS TXT +short
Estas cinco pruebas son las que haría cualquier auditor (o cualquier atacante) en el primer minuto. Si alguna no da lo esperado, no sigas: volvé a revisar la configuración.
El rate limiting también lo podés ver en acción: si mandás una ráfaga de consultas iguales desde la misma red, en
docker logs bind9aparecerate-limit: info: limit responses to .... Para un usuario normal es invisible.
Paso 9: delegar el dominio en NIC.ar
Con los dos servidores respondiendo bien, ahora sí le decimos al mundo dónde está tu DNS. Como ns1 y ns2 están dentro de tudominio.ar, se usa la opción Autodelegar: NIC.ar necesita, además del nombre de cada servidor, su IP. Esos son los registros glue, que evitan el problema del huevo y la gallina ("para encontrar ns1.tudominio.ar tengo que preguntarle a ns1.tudominio.ar").
- Entrá a nic.ar → INGRESO → CUIT/CUIL y Clave Fiscal.
- En Trámites a Distancia, abrí tu Lista de dominios y hacé clic en Delegar al lado de
tudominio.ar. - Elegí Autodelegar. El dominio se completa solo.
- Cargá los dos hosts con sus IPs:
ns1.tudominio.ar→203.0.113.10ns2.tudominio.ar→203.0.113.20
- Guardar y después Ejecutar Cambios.
La delegación suele quedar activa en el día. La podés seguir con:
# Ver la cadena completa desde la raíz: tiene que terminar en tus servidores
dig +trace tudominio.ar SOA
# Preguntarle a un resolver público
dig @1.1.1.1 mail.tudominio.ar A +short
dig @8.8.8.8 tudominio.ar MX +short
Para una revisión profesional, pasá el dominio por Zonemaster, que verifica decenas de puntos (coherencia entre servidores, glue, SOA, conectividad TCP/UDP, etc.), y por DNSViz, que dibuja toda la cadena de delegación.
Paso 10: el día a día
Cambiar un registro
Todo se edita en ns1. El flujo es siempre el mismo, y conviene hacerlo de memoria:
cd /opt/dns
nano zonas/db.tudominio.ar # 1. editar y SUBIR EL SERIAL
docker exec bind9 named-checkzone tudominio.ar /var/lib/bind/zonas/db.tudominio.ar # 2. validar
docker exec bind9 rndc reload tudominio.ar # 3. recargar
ns1 recarga la zona sin reiniciar, le avisa a ns2 (NOTIFY firmado) y ns2 se trae la versión nueva en segundos. Comprobalo con:
dig +short @203.0.113.20 tudominio.ar SOA
Actualizar BIND
cd /opt/dns
docker compose pull
docker compose up -d
Hacelo primero en ns2 y después en ns1: mientras uno se reinicia, el otro sigue respondiendo y nadie se entera. Para enterarte de las vulnerabilidades nuevas, suscribite a los avisos de seguridad de ISC (la lista bind-announce).
Backups
Lo que no podés perder está todo en ns1: la carpeta /opt/dns entera (zonas, configuración y claves). ns2 se reconstruye solo a partir de ns1. Un tar diario a otro lugar alcanza. Si querés algo centralizado, en Backups centralizados con Duplicati tenés una opción. Y recordá: las claves van a un backup cifrado, nunca a un repositorio.
Extra: DNSSEC, el estado real en .ar
DNSSEC firma criptográficamente las respuestas DNS para que nadie pueda falsificarlas en el camino. BIND 9.20 lo hace casi solo: con dos líneas en la zona genera las claves, firma y renueva las firmas automáticamente. Lo probé y funciona perfecto. Pero para que sirva de verdad, la cadena de confianza se tiene que cerrar: hay que publicar un registro DS en la zona superior (.ar), y eso se carga en el registrador, o sea, en NIC.ar.
¿Por qué el DS no va en mi propio BIND?
Es la duda más común, y tiene una respuesta muy lógica: el DS existe para que otro dé fe por vos. Funciona como una cadena de confianza:
- Tu BIND publica tu clave pública (registro
DNSKEY) y firma tus registros con ella. - Pero alguien que falsifique tus respuestas también puede inventarse una
DNSKEYy firmar con ella. Tu clave sola no prueba nada. - Por eso la zona de arriba,
.ar, publica un DS: un resumen (hash) de tuDNSKEY, firmado con la clave de.ar. A.ar, a su vez, la avala la raíz de internet. - El resolver arranca desde la raíz, en la que ya confía, y baja eslabón por eslabón: raíz →
.ar→ DS de tu dominio → tuDNSKEY→ tus registros.
Si el DS estuviera en tu propio servidor, sería tu servidor diciendo "confiá en mí porque lo digo yo", y eso cualquier atacante lo copia. Es lo mismo que ya pasa con la delegación: cuando en NIC.ar cargás ns1 y ns2 con sus IPs, esos datos no van a tu BIND, NIC.ar los publica en la zona .ar. El DS sería un dato más en ese mismo formulario de delegación.
El problema en NIC.ar
Y acá está el problema. .ar está firmado desde 2015 y existen dominios .ar con DS publicado, pero el panel de NIC.ar no tiene dónde cargar un DS. Consulté directamente a NIC Argentina por sus canales oficiales y la respuesta fue que hay que consultarlo con el "proveedor de hosting". Cuando el DNS es propio, el proveedor sos vos, así que hoy un titular con DNS autogestionado no tiene una vía documentada para cerrar la cadena.
Cómo activarlo igual
Te dejo cómo activarlo, por si NIC.ar lo habilita o por si administrás dominios en otras extensiones (.com, .net, .org), donde los registradores sí suelen permitir cargar el DS.
En ns1, dale a BIND permiso de escritura en la carpeta de zonas (necesita guardar la versión firmada y las claves) y agregá estas líneas a la zona en named.conf:
sudo mkdir -p /opt/dns/zonas/claves
sudo chown -R 53:53 /opt/dns/zonas
# En docker-compose.yml, sacale el ":ro" a ./zonas:/var/lib/bind/zonas
zone "tudominio.ar" {
type primary;
file "/var/lib/bind/zonas/db.tudominio.ar";
dnssec-policy default;
inline-signing yes;
key-directory "/var/lib/bind/zonas/claves";
allow-transfer { key "ns1-ns2"; };
also-notify { 203.0.113.20 key "ns1-ns2"; };
};
Después docker compose up -d. La política default crea una única clave ECDSA P-256 (CSK) sin vencimiento, así que el DS no cambia nunca. Desde ese momento editás la zona con sudo nano, porque la carpeta es de BIND. Para ver el estado y obtener el DS:
docker exec bind9 rndc dnssec -status tudominio.ar
docker exec bind9 sh -c 'dig @127.0.0.1 tudominio.ar DNSKEY +noall +answer | dnssec-dsfromkey -a SHA-256 -f - tudominio.ar'
# tudominio.ar. IN DS 16331 13 2 66236FC6B7...
Firmar sin DS publicado no rompe nada: los resolvers ven la zona como "no firmada" y responden igual. Una advertencia importante para el futuro: si alguna vez hay un DS publicado y te mudás de DNS, primero se saca el DS y después se apaga DNSSEC. Al revés, tu dominio deja de resolver para todos los que validan.
Los registros que va a necesitar el correo
Esta tabla es el puente con la próxima entrada. El servidor de correo requiere que estos registros existan y estén bien. Casi todos ya están en la zona que armamos:
| Registro | Nombre | Valor de ejemplo | Para qué sirve |
|---|---|---|---|
| A | mail |
203.0.113.30 |
La IP del servidor de correo. |
| PTR | (en DigitalOcean) | mail.tudominio.ar |
DNS inverso. Lo define el nombre del droplet, no tu zona. Sin PTR, Gmail y Outlook te rechazan. |
| MX | @ |
10 mail.tudominio.ar. |
Dónde se entrega el correo para @tudominio.ar. Apunta a un nombre, nunca a una IP. |
| TXT (SPF) | @ |
v=spf1 mx -all |
Quién puede enviar correo en nombre del dominio. -all rechaza al resto. |
| TXT (DKIM) | mail._domainkey |
v=DKIM1; k=rsa; p=... |
Clave pública para verificar la firma de cada mail. Se genera en el servidor de correo. |
| TXT (DMARC) | _dmarc |
v=DMARC1; p=none; ... |
Qué hacer con los mails que fallan SPF/DKIM y adónde mandar los reportes. |
| A + TXT (MTA-STS) | mta-sts y _mta-sts |
IP del correo / v=STSv1; id=... |
Obliga a los demás servidores a entregarte correo siempre cifrado. |
| TXT (TLS-RPT) | _smtp._tls |
v=TLSRPTv1; rua=mailto:... |
Reportes diarios si alguien no pudo entregarte con TLS. |
| A | webmail |
203.0.113.30 |
El webmail (y su certificado de Let's Encrypt). |
| SRV | _submissions._tcp, _imaps._tcp |
465 y 993 en mail.tudominio.ar |
Que los clientes de correo se configuren solos. Los SRV con destino . dicen "este servicio no existe". |
| CAA | @ |
0 issue "letsencrypt.org" |
Solo Let's Encrypt puede emitir certificados. |
Error clásico que veo seguido: cargar el DMARC con el nombre
dmarcen lugar de_dmarc. Sin el guion bajo, el registro existe pero nadie lo busca ahí, así que es como no tenerlo. Lo mismo pasa con_domainkey,_mta-stsy_smtp._tls: el guion bajo es parte del nombre.
Si querés profundizar en la teoría de estos registros, tenés Seguridad en Servidores de Emails, Firmar correos con DKIM y, como frutilla del postre, BIMI para que aparezca tu logo en la bandeja de entrada.
Checklist final
- ✅ Dos servidores autoritativos en regiones distintas.
- ✅ Sin recursión: no somos parte de ningún ataque de amplificación.
- ✅ Transferencias y avisos de cambio firmados con TSIG; nadie más puede copiar la zona.
- ✅ Rate limiting activo y versión oculta.
- ✅ Contenedores de solo lectura, sin capacidades extra y sin root después del arranque.
- ✅ Cloud Firewall delante (porque Docker se saltea UFW) y SSH solo con clave desde tu IP.
- ✅ Actualizaciones automáticas del sistema y BIND en la rama estable.
- ✅ Zona con CAA y lista para el correo.
- ✅ Delegación con glue en NIC.ar verificada con
dig +tracey Zonemaster.
Con esto tenés un DNS propio de nivel profesional. En la próxima entrada lo usamos: montamos un servidor de correo seguro con webmail, con todos los registros de esta zona, certificados de Let's Encrypt y la solución al bloqueo del puerto 25 de DigitalOcean. ¡Nos vemos ahí!