Sobre demostración de inexistencia de un tipo en NSEC

Quería contarles de un caso muy interesante de problemas de resolución de un nombre de host, que llevó a identificar un error en una implementación DNSSEC que es bastante difícil de detectar. Creo que vale la pena explicar cómo se llega y el detalle de análisis y comandos empleados.

Se cambiaron los nombres de la zona real y las direcciones IP para proteger a los inocentes.

Llegó a mis oídos un problema antiguo con un nombre que fallaba en su resolución. Daba error desde muchos lugares. Digamos que el nombre es “ns.ejemplo.cl”.

Preguntemos a mi resolver local, un unbound ¡con validación activa, por supuesto!

“` $ dig ns.ejemplo.cl a

; <<>> DiG 9.12.1 <<>> ns.ejemplo.cl a ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 59383 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;ns.ejemplo.cl. IN A

;; Query time: 458 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Tue Mar 23 17:20:15 -03 2021 ;; MSG SIZE rcvd: 42 “`

Ahí tenemos el error, indicado con el “status: SERVFAIL”.

Lo primero que uno aprende es a probar de inmediato SIN validación DNSSEC, agregando la opción “checking disabled” al comando:

“` $ dig ns.ejemplo.cl a +cd

; <<>> DiG 9.12.1 <<>> ns.ejemplo.cl a +cd ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22194 ;; flags: qr rd ra cd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;ns.ejemplo.cl. IN A

;; ANSWER SECTION: ns.ejemplo.cl. 8696 IN A 10.0.0.53

;; Query time: 0 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Tue Mar 23 17:20:27 -03 2021 ;; MSG SIZE rcvd: 58 “`

¡Ahí tenemos respuesta! Entonces es algún problema DNSSEC. Lo primero a revisar es si la firma está expirada. Pidamos los registros DNSSEC en forma explícita:

“` $ dig ns.ejemplo.cl a +cd +dnssec

; <<>> DiG 9.12.1 <<>> ns.ejemplo.cl a +cd +dnssec ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51000 ;; flags: qr rd ra cd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;ns.ejemplo.cl. IN A

;; ANSWER SECTION: ns.ejemplo.cl. 8584 IN A 10.0.0.53

;; Query time: 490 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Tue Mar 23 17:22:20 -03 2021 ;; MSG SIZE rcvd: 58 “`

Mmmm algo está raro. ¡No hay registros DNSSEC! Y la zona está firmada, por ejemplo pidamos el SOA:

“` $ dig ejemplo.cl soa +dnssec

; <<>> DiG 9.12.1 <<>> ejemplo.cl soa +dnssec ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40928 ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;ejemplo.cl. IN SOA

;; ANSWER SECTION: ejemplo.cl. 86400 IN SOA ns.ejemplo.cl. hostmaster.ejemplo.cl. 2021012700 3600 3600 3600 3600 ejemplo.cl. 86400 IN RRSIG SOA 13 2 86400 20210427233724 20210127223735 3942 ejemplo.cl. av5Gvy3qss…

;; Query time: 17 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Tue Mar 23 17:23:32 -03 2021 ;; MSG SIZE rcvd: 195 “`

(el material criptográfico de llaves y firmas se cortó, para mayor legibilidad)

y si pedimos las llaves con sus firmas:

“` $ dig ejemplo.cl dnskey +dnssec

; <<>> DiG 9.12.1 <<>> ejemplo.cl dnskey +dnssec ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13772 ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;ejemplo.cl. IN DNSKEY

;; ANSWER SECTION: ejemplo.cl. 1964 IN DNSKEY 256 3 13 JU/KXGAULi9PEFoL7cCwi5… ejemplo.cl. 1964 IN DNSKEY 257 3 13 0RwyeYwVgxn+xWYgN4jSeL… ejemplo.cl. 1964 IN RRSIG DNSKEY 13 2 3600 20210427233724 20210127224032 56252 ejemplo.cl. Cbn3kz95F0…

;; Query time: 3 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Tue Mar 23 17:23:54 -03 2021 ;; MSG SIZE rcvd: 305 “`

viene todo correcto. Esto está raro. ¿Por qué no están los registros DNSSEC de ns.ejemplo.cl/A? En este momento ya corresponde dejar nuestro resolver y preguntar directamente a los autoritativos. Ambos responden igual, por lo que mostraremos solo uno de ellos. Repitamos la pregunta por el A al primario, pidiendo los registros DNSSEC:

“` $ dig @10.0.0.53 ns.ejemplo.cl a +dnssec +norec

; <<>> DiG 9.12.1 <<>> @10.0.0.53 ns.ejemplo.cl a +dnssec +norec ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9290 ;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 3, ADDITIONAL: 2

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;ns.ejemplo.cl. IN A

;; ANSWER SECTION: ns.ejemplo.cl. 86400 IN A 10.0.0.53

;; AUTHORITY SECTION: ejemplo.cl. 86400 IN NS secundario.nic.cl. ejemplo.cl. 86400 IN NS ns.ejemplo.cl. ejemplo.cl. 86400 IN RRSIG NS 13 2 86400 20210427233724 20210127223730 3942 ejemplo.cl. EDHIfzzLJ…

;; ADDITIONAL SECTION: ns.ejemplo.cl. 86400 IN AAAA 2001:DB8::2

;; Query time: 15 msec ;; SERVER: 10.0.0.53#53(10.0.0.53) ;; WHEN: Tue Mar 23 17:25:20 -03 2021 ;; MSG SIZE rcvd: 230 “`

¡Ahí está la firma! La validé con una librería local, y es correcta. ¡Además los otros registros de la zona (como www, el SOA del apex, etc) están correctamente firmados y validan!

En este momento y sin mayor idea, puse mi resolver en modo debug y revisé el log para tener alguna idea de dónde puede estar fallando. Y descubrí algo interesante. En un resolver vacío (cache limpio), luego de obtener una respuesta correcta por el registro A, junto a sus firmas RRSIG, el validador debe comenzar a seguir la cadena de confianza hacia arriba, para llegar a algún trust anchor o llave de confianza que ya conozca. Y entonces lo primero que hace es pedir el registro DS de ns.ejemplo.cl. Esto es porque necesita saber si hay alguna delegación (“corte de zona”) en ese nombre. Por lo general esto no es así, y al recibir la respuesta correcta “no hay DS para ese nombre”, el validador seguirá la cadena “hacia arriba”, preguntando por el DS de ejemplo.cl, el que sí existe y debe validar.

Y acá está el detalle. Como veremos más adelante, no existe DS para ns.ejemplo.cl (porque es un registro al final del árbol DNS), pero la zona en vez de decir “no existe DS ahí, y este es el NSEC que lo demuestra”, en cambio dice ¡”NO EXISTE EL NOMBRE ns.ejemplo.cl”!!!

Entonces, preguntemos por el DS de ese nombre:

“` $ dig @10.0.0.53 ns.ejemplo.cl ds +dnssec +norec

; <<>> DiG 9.12.1 <<>> @10.0.0.53 ns.ejemplo.cl ds +dnssec +norec ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 22500 ;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;ns.ejemplo.cl. IN DS

;; Query time: 22 msec ;; SERVER: 10.0.0.53#53(10.0.0.53) ;; WHEN: Tue Mar 23 18:03:13 -03 2021 ;; MSG SIZE rcvd: 42 “`

Acá dió un SERVFAIL, sin mucho detalle. Puede ser que si la zona esté mal firmada es el propio servidor DNS que sirve esa zona que se “marea” y no sabe cómo responder. Pero acá vemos un comportamiento distinto en el otro NS del dominio. Miren lo que responde a la misma pregunta:

“` $ dig @secundario.nic.cl ns.ejemplo.cl ds +dnssec +norec

; <<>> DiG 9.12.1 <<>> @secundario.nic.cl ns.ejemplo.cl ds +dnssec +norec ; (2 servers found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57070 ;; flags: qr aa; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;ns.ejemplo.cl. IN DS

;; AUTHORITY SECTION: ejemplo.cl. 3600 IN SOA ns.ejemplo.cl. hostmaster.ejemplo.cl. 2021012700 3600 3600 3600 3600 ejemplo.cl. 86400 IN RRSIG SOA 13 2 86400 20210427233724 20210127223735 3942 ejemplo.cl. av5Gvy3qs… mail.ejemplo.cl. 3600 IN NSEC www.ejemplo.cl. A MX NSEC mail.ejemplo.cl. 3600 IN RRSIG NSEC 13 3 3600 20210427233724 20210127223945 3942 ejemplo.cl. s2RIZqtn…

;; Query time: 13 msec ;; SERVER: 200.7.5.7#53(200.7.5.7) ;; WHEN: Tue Mar 23 18:04:56 -03 2021 ;; MSG SIZE rcvd: 349 “`

Como ven, ¡no hay un NSEC que tenga de “owner name” a ns.ejemplo.cl, y más encima viene uno que demuestra que entre mail.ejemplo.cl y www.ejemplo.cl no hay nada! Con esto el resolver invalida el registro A obtenido anteriormente, y responde con SERVFAIL.

Acá nos detendremos un poco para explicar lo del NSEC. Una zona firmada con DNSSEC debe armar una ”cadena” NSEC (o NSEC3, pero eso lo dejaremos para otra oportunidad) que recorre todos los nombres de la zona, enlazando unos con otros. Por ejemplo “a.ejemplo.cl NSEC b.ejemplo.cl”, indicando que después de a.ejemplo.cl viene b.ejemplo.cl, ¡y no hay nada entremedio! Con esto podemos demostrar la ”inexistencia” de un nombre.

Pero además de esta función, que es la más conocida, el registro NSEC publica los tipos que existen para el primer nombre. Mirando el ejemplo de la salida anterior, el NSEC dice “mail.ejemplo.cl … NSEC www.ejemplo.cl A MX NSEC”. Esas tres últimas etiquetas quieren decir que para mail.ejemplo.cl existe un registro A, un MX y un NSEC. Es decir, ¡tampoco existe un AAAA para mail.ejemplo.cl!

El registro NSEC entonces cumple dos funciones: demostrar que no hay nada entre dos nombres, y qué tipos existen para el primer nombre.

Una respuesta correcta cuando uno consulta por un DS que no existe es de este estilo, en otra zona correctamente firmada:

“` $ dig www.ok—firmado.cl ds +dnssec

; <<>> DiG 9.12.1 <<>> www.ok—firmado.cl ds +dnssec ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63549 ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1

;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;www.ok—firmado.cl. IN DS

;; AUTHORITY SECTION: ok—firmado.cl. 3600 IN SOA ns.ok—firmado.cl. hostmaster.nic.cl. 2020041849 21600 7200 2592000 43200 ok—firmado.cl. 43200 IN RRSIG SOA 8 2 43200 20210720124001 20210322124001 42990 ok—firmado.cl. BVCoV2Tk… www.ok—firmado.cl. 43200 IN NSEC ok—firmado.cl. A RRSIG NSEC www.ok—firmado.cl. 43200 IN RRSIG NSEC 8 3 43200 20210623141939 20210223141939 42990 ok—firmado.cl. DleYTBp…

;; Query time: 165 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Tue Mar 23 17:36:22 -03 2021 ;; MSG SIZE rcvd: 472 “`

Ahí viene un NSEC con owner name www.ok–firmado.cl, y en su mapa de tipos se ve que existe A, RRSIG y NSEC; con lo que queda demostrado que NO existe un tipo DS para ese nombre.

Luego de esta demostración de problema en el firmado, me enviaron el archivo de la zona ejemplo.cl y efectivamente, por un error de implementación, el firmador consideró que el nombre “ns.ejemplo.cl” era “glue” de la zona, por lo tanto no era autoritativo, y entonces no generó el NSEC con ese nombre. Es correcto que un registro glue no genera NSEC, pero el error acá radica en que un nombre como ns.ejemplo.cl (que es NS de la zona) efectivamente se considera “glue” pero en la zona del padre, en este caso CL. En la zona del hijo, el mismo nombre YA NO ES GLUE, y es completamente autoritativo, por lo que es obligación que la cadena NSEC pase por ahí (NSEC3, al contrario, posee una técnica llamada opt-out que permite “saltarse” ciertos nombres). NSEC no posee opt-out.

¡Caso resuelto! Gracias por llegar hasta acá. Espero que haya quedado claro, y que les sirva en sus propias cacerías DNS ;)


Next post: Límite a la cantidad de iteraciones en NSEC3

Previous post: La mala práctica de “localhost” en dominios