Saltar al contenido
Luis ZambranaDevOps · Seguridad · Forense

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.

Servidor DNS propio con BIND9: ns1 primario y ns2 secundario unidos por TSIG

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.ar y 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.x son 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:

Crear cuenta en DigitalOcean con crédito de prueba

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:

  1. Entrá a nic.ar, hacé clic en INGRESO y elegí CUIT/CUIL y Clave Fiscal.
  2. Buscá el dominio que querés y seguí el trámite de registro.
  3. 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.key como 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 por google.com, responde REFUSED. 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 preguntar version.bind y 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.
  • logging a stderr: así todo se ve con docker logs, y la rotación ya la configuramos en daemon.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 formato AAAAMMDDNN (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 ns1 y ns2 está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.ar en 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.20 recibe los parches de seguridad de esa rama cada vez que hacés pull.
  • Puerto publicado solo en la IP pública: Ubuntu trae systemd-resolved escuchando en 127.0.0.53:53. Si publicás en 0.0.0.0:53 choca y el contenedor no arranca. Atando el puerto a la IP pública, cada uno vive en lo suyo.
  • :ro en 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 dos tmpfs en memoria.
  • cap_drop: [ALL] y después solo tres capacidades: abrir el puerto 53 (NET_BIND_SERVICE) y pasar de root al usuario bind (SETUID y SETGID). 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 bind9 aparece rate-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").

  1. Entrá a nic.ar → INGRESO → CUIT/CUIL y Clave Fiscal.
  2. En Trámites a Distancia, abrí tu Lista de dominios y hacé clic en Delegar al lado de tudominio.ar.
  3. Elegí Autodelegar. El dominio se completa solo.
  4. Cargá los dos hosts con sus IPs:
    • ns1.tudominio.ar → 203.0.113.10
    • ns2.tudominio.ar → 203.0.113.20
  5. 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:

  1. Tu BIND publica tu clave pública (registro DNSKEY) y firma tus registros con ella.
  2. Pero alguien que falsifique tus respuestas también puede inventarse una DNSKEY y firmar con ella. Tu clave sola no prueba nada.
  3. Por eso la zona de arriba, .ar, publica un DS: un resumen (hash) de tu DNSKEY, firmado con la clave de .ar. A .ar, a su vez, la avala la raíz de internet.
  4. 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 → tu DNSKEY → 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 dmarc en 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-sts y _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 +trace y 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í!

Comentarios

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

Seguí leyendo

Linux

Ciberseguridad con Wazuh

Wazuh es una plataforma de ciberseguridad de código abierto que ofrece funciones de detección y respuesta extendidas (XDR) y gestión de información y eventos de seguridad (SIEM)

7 min