<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="es">
  <title>Blog de Asentic</title>
  <subtitle>Análisis técnico, casos reales y guías de remediación en ciberseguridad.</subtitle>
  <link href="https://www.asentic.cl/blog/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://www.asentic.cl/blog/" rel="alternate" type="text/html"/>
  <id>https://www.asentic.cl/blog/</id>
  <updated>2026-05-19T00:12:59.242538+00:00</updated>
  <author>
    <name>Asentic</name>
    <uri>https://www.asentic.cl/</uri>
    <email>contacto@asentic.cl</email>
  </author>
  <icon>https://www.asentic.cl/assets/img/logo-mark.svg</icon>
  <rights>© 2026 Asentic</rights>

  <entry>
    <title>DNSSEC en dominio .cl: activación desde Cloudflare y NIC.cl</title>
    <link href="https://www.asentic.cl/blog/dnssec-dominio-cl/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/dnssec-dominio-cl/</id>
    <published>2026-05-18T00:00:00+00:00</published>
    <updated>2026-05-18T00:00:00+00:00</updated>
    <summary type="text">DNSSEC protege la resolución DNS de tu dominio contra spoofing y envenenamiento de caché. Aquí el proceso completo para dominios .cl: qué es, cómo funciona la cadena de confianza y los pasos exactos para activarlo desde Cloudflare y enviarlo a NIC.cl.</summary>
    <content type="html"><![CDATA[<h2 id="el-problema-que-resuelve">El problema que resuelve</h2>
<p>Cuando alguien escribe <code>asentic.cl</code> en su navegador, ocurre una cadena de consultas DNS que termina entregando una IP. Esa cadena no tiene autenticación por defecto: cualquier intermediario en el camino —un resolver comprometido, un equipo en la misma red— puede responder con una IP falsa y redirigir al usuario a donde quiera.</p>
<p>Esto se llama <em>DNS cache poisoning</em> o <em>DNS spoofing</em>, y es un vector real. En 2008, Dan Kaminsky demostró que cualquier resolver del planeta podía ser envenenado en minutos. La industria aceleró el despliegue de DNSSEC como respuesta.</p>
<p>DNSSEC no cifra el tráfico DNS —eso lo hace DoH o DoT— sino que <strong>firma criptográficamente los registros</strong>. El resolver puede verificar que la respuesta viene del dueño legítimo del dominio y no fue alterada.</p>
<h2 id="como-funciona-la-cadena-de-confianza">Cómo funciona la cadena de confianza</h2>
<p>DNSSEC funciona como un árbol de confianza que parte desde la raíz de internet:</p>
<div class="post-code"><pre><span></span><code>.  (root, firmado por ICANN)
└── .cl  (firmado por NIC.cl)
    └── asentic.cl  (firmado por tu proveedor DNS — Cloudflare en este caso)
</code></pre></div>

<p>Cada nodo del árbol publica un registro <strong>DS</strong> (<em>Delegation Signer</em>) que enlaza hacia la clave del nodo hijo. Cuando un resolver valida <code>asentic.cl</code>, sube por la cadena hasta la raíz y verifica que cada firma sea coherente.</p>
<p>Para que la cadena esté completa necesitas <strong>dos pasos</strong>:</p>
<ol>
<li>Que tu zona esté firmada (Cloudflare lo hace automáticamente).</li>
<li>Que el registro DS de tu zona esté publicado en NIC.cl.</li>
</ol>
<p>El segundo paso es el que la mayoría omite —o no sabe que tiene que hacer.</p>
<h2 id="paso-1-activar-dnssec-en-cloudflare">Paso 1: activar DNSSEC en Cloudflare</h2>
<p>Cloudflare firma tu zona automáticamente en cuanto activas la opción.</p>
<ol>
<li>Entra a <a href="https://dash.cloudflare.com">dash.cloudflare.com</a> y selecciona tu dominio.</li>
<li>Ve a <strong>DNS → Configuración</strong> (o busca la pestaña <em>DNSSEC</em>).</li>
<li>Haz clic en <strong>Habilitar DNSSEC</strong>.</li>
</ol>
<p><img alt="Panel DNSSEC en Cloudflare mostrando el estado de activación" src="/assets/img/blog/dnssec-cf-paso1.png"></p>
<p>Cloudflare genera el par de claves (KSK + ZSK, algoritmo ECDSA P-256 / algoritmo 13) y firma todos los registros de tu zona. Inmediatamente te muestra el registro DS que debes publicar en tu registrador:</p>
<div class="post-code"><pre><span></span><code>Key Tag:     2371
Algorithm:   13 (ECDSA Curve P-256 with SHA-256)
Digest Type: 2 (SHA-256)
Digest:      F6F7FADC9C60B85F128FF3AE978FDB81...
</code></pre></div>

<p><img alt="Registro DS completo generado por Cloudflare con los campos Key Tag, Algorithm, Digest Type y Digest" src="/assets/img/blog/dnssec-cf-paso2.png"></p>
<p>Copia estos cuatro valores. Los necesitas para el siguiente paso.</p>
<h2 id="paso-2-publicar-el-ds-en-niccl">Paso 2: publicar el DS en NIC.cl</h2>
<p>NIC.cl gestiona todos los dominios <code>.cl</code>. Para publicar el DS:</p>
<ol>
<li>Entra a <a href="https://clientes.nic.cl">clientes.nic.cl</a> con tu cuenta de registrante.</li>
<li>Selecciona el dominio y, en la sección <strong>4. Configuración Técnica</strong>, haz clic en el enlace <strong>(DNSSec)</strong> al pie del formulario.</li>
<li>En el diálogo <strong>Administración de llaves para DNSSec</strong>, elige tipo <strong>DS</strong> e ingresa la clave que Cloudflare generó.</li>
<li>Marca la casilla <strong>Publicar</strong> y confirma.</li>
</ol>
<p><img alt="Diálogo Administración de llaves para DNSSec en NIC.cl con el registro DS ingresado y marcado para publicar" src="/assets/img/blog/dnssec-nicl-paso1.png"></p>
<p>NIC.cl publica el DS en la zona <code>.cl</code> en un plazo de <strong>24 a 72 horas</strong>. No envían notificación automática cuando queda activo —solo puedes verificarlo consultando directamente.</p>
<h2 id="como-verificar-que-la-cadena-esta-activa">Cómo verificar que la cadena está activa</h2>
<p>Una vez que NIC.cl publique el DS, la validación completa es verificable con herramientas estándar.</p>
<p><strong>Verificación rápida desde terminal:</strong></p>
<div class="post-code"><pre><span></span><code><span class="c1"># DS publicado en la zona .cl</span>
dig<span class="w"> </span>DS<span class="w"> </span>asentic.cl<span class="w"> </span>+short

<span class="c1"># RRSIG presente en los registros (zona firmada)</span>
dig<span class="w"> </span>A<span class="w"> </span>asentic.cl<span class="w"> </span>+dnssec<span class="w"> </span>+short

<span class="c1"># Validación completa con cadena de confianza</span>
delv<span class="w"> </span>@8.8.8.8<span class="w"> </span>asentic.cl<span class="w"> </span>A<span class="w"> </span>+rtrace
</code></pre></div>

<p>Si la cadena está completa, <code>delv</code> responde con <code>; fully validated</code>. Si el DS todavía no está publicado en <code>.cl</code>, el comando intenta buscarlo y devuelve vacío —la zona sigue firmada pero sin validación desde fuera.</p>
<p>Para consultar directamente si el DS ya aparece en NIC.cl antes de que propague a los resolvers públicos:</p>
<div class="post-code"><pre><span></span><code>dig<span class="w"> </span>DS<span class="w"> </span>asentic.cl<span class="w"> </span>@a.nic.cl
</code></pre></div>

<p>Mientras el DS está pendiente, la respuesta muestra <code>ANSWER: 0</code> con una sección AUTHORITY (SOA). Cuando queda publicado, <code>ANSWER: 1</code> incluye el registro DS.</p>
<p><img alt="Salida del comando dig DS asentic.cl @a.nic.cl consultando directamente a NIC.cl — ANSWER: 0 indica que el DS está pendiente de publicación" src="/assets/img/blog/dnssec-terminal.png"></p>
<p><strong>Verificador online:</strong> DNSSEC Analyzer de Verisign Labs (<code>dnssec-analyzer.verisignlabs.com</code>) muestra el estado de cada eslabón de la cadena en verde o rojo.</p>
<h2 id="que-pasa-mientras-esperas-la-publicacion">Qué pasa mientras esperas la publicación</h2>
<p>Mientras NIC.cl procesa el DS, tu zona <strong>ya está firmada</strong>. Los registros incluyen firmas RRSIG que los resolvers con validación DNSSEC pueden verificar si tienen el DS por otro medio. En la práctica, esto es transparente para los usuarios: sin el DS en <code>.cl</code>, los resolvers simplemente ignoran las firmas (modo <em>insecure</em>, no <em>bogus</em>) y el tráfico funciona con normalidad.</p>
<p>El riesgo de la transición es mínimo. Una vez que el DS queda publicado, los resolvers con validación activa —los de ISPs modernos, Google, Cloudflare, etc.— empiezan a verificar la cadena en cada consulta.</p>
<h2 id="por-que-vale-la-pena">Por qué vale la pena</h2>
<p>DNSSEC es especialmente relevante para:</p>
<ul>
<li><strong>Correo electrónico</strong>: los registros MX firmados impiden redirigir tu correo a un servidor atacante. Complementa bien a DMARC y MTA-STS.</li>
<li><strong>Confianza en subdominios</strong>: los registros de <code>vpn.asentic.cl</code>, <code>mail.asentic.cl</code> y similares también quedan protegidos.</li>
<li><strong>Señal de postura de seguridad</strong>: para empresas que participan en licitaciones o procesan datos sensibles, DNSSEC bien configurado es un indicador verificable externamente.</li>
</ul>
<p>La activación es un proceso de media hora. El costo operativo después es cero: Cloudflare rota las claves automáticamente y NIC.cl mantiene el DS vigente.</p>]]></content>
    <category term="dns"/>
    <category term="hardening"/>
  </entry>

  <entry>
    <title>Subresource Integrity: cómo evitar que un CDN comprometido inyecte código en tu sitio</title>
    <link href="https://www.asentic.cl/blog/subresource-integrity/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/subresource-integrity/</id>
    <published>2026-05-17T00:00:00+00:00</published>
    <updated>2026-05-17T00:00:00+00:00</updated>
    <summary type="text">En junio 2024, más de 100.000 sitios web fueron atacados cuando el CDN polyfill.io cambió de dueño y empezó a servir malware. Un atributo HTML de dos líneas habría bloqueado el ataque en todos ellos. Así funciona Subresource Integrity.</summary>
    <content type="html"><![CDATA[<p>En junio de 2024, investigadores de Sansec y Cloudflare detectaron que <strong>polyfill.io</strong>, un CDN ampliamente usado para servir código de compatibilidad JavaScript, había sido comprado meses antes por una empresa china. El nuevo propietario empezó a modificar el script que entregaba para inyectar código malicioso — redirecciones a sitios de apuestas, spam, potencialmente keyloggers — en <strong>más de 100.000 sitios web</strong> que lo referenciaban directamente.</p>
<p>El ataque no requirió hackear ningún servidor. No hubo contraseña robada ni CVE. El atacante simplemente compró el dominio, esperó, y aprovechó que decenas de miles de sitios confiaban ciegamente en lo que ese CDN decidiera enviarles.</p>
<p>Un atributo HTML que nadie había agregado — <code>integrity</code> — habría bloqueado el ataque en todos esos sitios antes de que el primer byte malicioso se ejecutara.</p>
<h2 id="que-es-subresource-integrity">Qué es Subresource Integrity</h2>
<p>Cuando cargas un script o stylesheet desde un CDN externo, el browser descarga el archivo y lo ejecuta. No sabe si ese archivo es el mismo que el desarrollador originalmente referenció, ni si fue modificado en tránsito o en origen. Confía.</p>
<p>Subresource Integrity (SRI) cambia eso. Es un mecanismo del browser que te permite declarar, en tu propio HTML, un <strong>hash criptográfico</strong> del recurso que estás cargando. Antes de ejecutar cualquier código, el browser calcula el hash del archivo recibido y lo compara con el que declaraste. Si no coinciden, <strong>bloquea la ejecución</strong> y lanza un error de integridad. No hay forma de eludir esto desde el servidor externo — el control queda en tu HTML.</p>
<h2 id="como-se-ve-en-codigo">Cómo se ve en código</h2>
<p>Sin SRI:</p>
<div class="post-code"><pre><span></span><code><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&quot;https://cdn.proveedor.com/jquery-3.7.1.min.js&quot;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div>

<p>Con SRI:</p>
<div class="post-code"><pre><span></span><code><span class="p">&lt;</span><span class="nt">script</span>
  <span class="na">src</span><span class="o">=</span><span class="s">&quot;https://cdn.proveedor.com/jquery-3.7.1.min.js&quot;</span>
  <span class="na">integrity</span><span class="o">=</span><span class="s">&quot;sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs&quot;</span>
  <span class="na">crossorigin</span><span class="o">=</span><span class="s">&quot;anonymous&quot;</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div>

<p>Dos atributos:</p>
<ul>
<li><strong><code>integrity</code></strong>: el algoritmo (<code>sha256</code>, <code>sha384</code> o <code>sha512</code>) seguido de un guión y el hash en Base64 del contenido esperado. SHA-384 o SHA-512 son los recomendados hoy.</li>
<li><strong><code>crossorigin="anonymous"</code></strong>: necesario para que el browser aplique CORS y tenga acceso al contenido del recurso para verificarlo. Sin este atributo, SRI no funciona aunque declares el hash.</li>
</ul>
<p>Lo mismo aplica para <code>&lt;link rel="stylesheet"&gt;</code>:</p>
<div class="post-code"><pre><span></span><code><span class="p">&lt;</span><span class="nt">link</span>
  <span class="na">rel</span><span class="o">=</span><span class="s">&quot;stylesheet&quot;</span>
  <span class="na">href</span><span class="o">=</span><span class="s">&quot;https://cdn.proveedor.com/bootstrap-5.3.3.min.css&quot;</span>
  <span class="na">integrity</span><span class="o">=</span><span class="s">&quot;sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH&quot;</span>
  <span class="na">crossorigin</span><span class="o">=</span><span class="s">&quot;anonymous&quot;</span><span class="p">&gt;</span>
</code></pre></div>

<h2 id="como-se-genera-el-hash">Cómo se genera el hash</h2>
<p>No tienes que calcularlo a mano. Tres opciones:</p>
<p><strong>Opción 1 — línea de comando</strong> (en cualquier sistema con <code>openssl</code> o <code>shasum</code>):</p>
<div class="post-code"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>https://cdn.proveedor.com/jquery-3.7.1.min.js<span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>openssl<span class="w"> </span>dgst<span class="w"> </span>-sha384<span class="w"> </span>-binary<span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>openssl<span class="w"> </span>base64<span class="w"> </span>-A
</code></pre></div>

<p>El resultado lo pegas después de <code>sha384-</code>.</p>
<p><strong>Opción 2 — generador web oficial de SRI Hash Generator</strong> (<code>srihash.org</code>): pegás la URL, elige el algoritmo, y te entrega el atributo <code>integrity</code> listo para copiar.</p>
<p><strong>Opción 3 — el CDN mismo</strong>: cdnjs.com, jsDelivr y Bootstrap CDN ya muestran el hash SRI en su interfaz junto al código de embed. Si copiás desde ahí, ya viene incluido.</p>
<h2 id="que-bloquea-y-que-no">Qué bloquea y qué no</h2>
<p>SRI protege contra:</p>
<ul>
<li><strong>CDN comprometido</strong> (el caso polyfill.io): si el proveedor modifica el archivo, el hash deja de coincidir y el browser bloquea.</li>
<li><strong>Ataque man-in-the-middle</strong> en la conexión al CDN: si alguien intercepta la respuesta y modifica el contenido en tránsito, el hash falla.</li>
<li><strong>Errores silenciosos de versión</strong>: si el CDN sirve una versión diferente a la que esperabas (accidental o malicioso), lo detectas de inmediato.</li>
</ul>
<p>SRI <strong>no</strong> protege contra:</p>
<ul>
<li><strong>Scripts cargados dinámicamente en runtime</strong> (con <code>document.createElement('script')</code>): SRI solo cubre atributos declarados estáticamente en el HTML.</li>
<li><strong>Recursos de tu propio dominio</strong>: SRI es para terceros. Tus propios archivos los controlás vos.</li>
<li><strong>XSS</strong>: si hay inyección XSS en tu página, el atacante puede inyectar un <code>&lt;script&gt;</code> nuevo sin <code>integrity</code>. SRI no es sustituto de CSP ni de sanitización de input.</li>
<li><strong>El recurso de primer partido</strong> (tu propio HTML): SRI cubre los recursos que tu página carga, no el HTML en sí.</li>
</ul>
<h2 id="el-atributo-crossorigin-importa-mas-de-lo-que-parece">El atributo <code>crossorigin</code> importa más de lo que parece</h2>
<p>Un error frecuente: agregar <code>integrity</code> pero olvidar <code>crossorigin="anonymous"</code>. El resultado es que el browser no puede verificar el hash (por restricciones CORS) y, dependiendo del browser y de si la URL es HTTPS o no, puede comportarse de formas inconsistentes — desde ignorar el atributo hasta bloquear el recurso igual.</p>
<p>La regla es simple: si usás <code>integrity</code>, siempre agregá <code>crossorigin="anonymous"</code>. Son inseparables.</p>
<h2 id="implementacion-en-wordpress">Implementación en WordPress</h2>
<p>WordPress es el CMS más frecuente entre los sitios que detectamos con <code>JS-SRI-MISSING</code>. Los scripts de jQuery, el loader de Google Fonts, Slick Slider y docenas de plugins se cargan desde CDNs externos sin SRI.</p>
<p>La forma más prolija de implementarlo en WordPress es enganchar el filtro <code>script_loader_tag</code>:</p>
<div class="post-code"><pre><span></span><code><span class="x">// En functions.php de tu tema hijo</span>
<span class="x">add_filter(&#39;script_loader_tag&#39;, function($tag, $handle, $src) {</span>
<span class="x">    $sri_map = [</span>
<span class="x">        &#39;jquery&#39;   =&gt; &#39;sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs&#39;,</span>
<span class="x">        &#39;slick-js&#39; =&gt; &#39;sha384-xxxxxxxxxxxxxxxx&#39;,</span>
<span class="x">    ];</span>
<span class="x">    if (isset($sri_map[$handle])) {</span>
<span class="x">        $hash = $sri_map[$handle];</span>
<span class="x">        $tag = str_replace(</span>
<span class="x">            &quot;src=&#39;$src&#39;&quot;,</span>
<span class="x">            &quot;src=&#39;$src&#39; integrity=&#39;sha384-{$hash}&#39; crossorigin=&#39;anonymous&#39;&quot;,</span>
<span class="x">            $tag</span>
<span class="x">        );</span>
<span class="x">    }</span>
<span class="x">    return $tag;</span>
<span class="x">}, 10, 3);</span>
</code></pre></div>

<p><strong>Atención con versiones</strong>: SRI vincula el hash a una versión exacta del archivo. Si tu plugin actualiza jQuery de <code>3.7.1</code> a <code>3.7.2</code>, el hash deja de coincidir y el script se bloquea. Tenés que mantener el mapa actualizado cada vez que cambia una versión. Por eso la implementación práctica es:</p>
<ol>
<li>Identifica los scripts de CDNs externos que no controlás.</li>
<li>Fija las versiones en tu <code>functions.php</code> (deshabilitá actualizaciones automáticas para esas dependencias o hacelas manualmente).</li>
<li>Generá el hash por versión y guardalo en el mapa.</li>
<li>Cada vez que actualizás una librería, regenerás el hash.</li>
</ol>
<p>Para plugins de terceros que cargan sus propias dependencias de CDN, revisá si el plugin tiene soporte SRI nativo (algunos ya lo implementan) o si podés desencolar su script y reencolar el tuyo con SRI.</p>
<h2 id="que-detecta-freescan">Qué detecta FreeScan</h2>
<p>El scanner <code>JS-SRI-MISSING</code> de FreeScan analiza el HTML de tu sitio e identifica tags <code>&lt;script src="..."&gt;</code> y <code>&lt;link rel="stylesheet" href="..."&gt;</code> que:</p>
<ol>
<li>Cargan desde un dominio <strong>diferente al tuyo</strong> (recursos de terceros).</li>
<li>No tienen atributo <code>integrity</code> declarado.</li>
</ol>
<p>Para cada recurso encontrado, el informe indica el dominio de origen y el tipo de tag. El hallazgo se clasifica como <strong>Media</strong> porque la explotabilidad depende de que el CDN sea comprometido — no es algo que un atacante externo pueda disparar directamente contra vos, pero el impacto cuando ocurre es total.</p>
<p>El fix es directamente aplicable: generás el hash del recurso actual y agregás el atributo. El trabajo es de minutos por recurso.</p>
<h2 id="un-patron-concreto-que-vemos-seguido">Un patrón concreto que vemos seguido</h2>
<p>El caso más frecuente en sitios chilenos que pasaron por FreeScan: jQuery cargado desde <code>code.jquery.com</code> o <code>cdnjs.cloudflare.com</code> sin SRI. jQuery en particular es un blanco de alta rentabilidad para atacantes — presente en millones de sitios, acceso al DOM completo, historial de vulnerabilidades. Si tu CDN de jQuery queda comprometido (o simplemente responde de forma distinta en un A/B test que salió mal), SRI es lo único que te protege antes de que el código llegue al browser de tus usuarios.</p>
<p>El segundo caso más frecuente: Google Fonts. Técnicamente es menos riesgoso (son solo estilos, no ejecutan código), pero el principio es el mismo.</p>
<hr>
<p>La línea de defensa de SRI es corta — dos atributos, un hash que calculás una vez — y actúa en el único momento en que todavía podés parar el ataque: antes de que el código se ejecute. El incidente de polyfill.io de 2024 mostró que los CDNs de terceros pueden cambiar de propietario, de política o de contenido sin aviso. Lo que no podés controlar en el origen, al menos podés verificarlo en destino.</p>]]></content>
    <category term="sri"/>
    <category term="javascript"/>
    <category term="supply-chain"/>
    <category term="cdn"/>
    <category term="hardening"/>
  </entry>

  <entry>
    <title>Seguridad web desde la vereda: lo que cualquiera ve de tu empresa antes que tú</title>
    <link href="https://www.asentic.cl/blog/seguridad-web-desde-la-vereda/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/seguridad-web-desde-la-vereda/</id>
    <published>2026-05-16T00:00:00+00:00</published>
    <updated>2026-05-16T00:00:00+00:00</updated>
    <summary type="text">Hay una diferencia importante entre auditar tu red interna y evaluar lo que cualquier persona en internet puede ver de tu sitio web. Una guía para cualquier persona, sea o no especialista técnico.</summary>
    <content type="html"><![CDATA[<p>Una pregunta que nos hacen con frecuencia: <em>&ldquo;¿En qué se diferencian de Nessus, OpenVAS o Qualys? ¿No hacen lo mismo?&rdquo;</em></p>
<p>La respuesta corta: no. Pero la confusión es completamente razonable — todos se venden bajo el paraguas de &ldquo;ciberseguridad&rdquo; y todos producen informes. La diferencia está en <strong>qué miran</strong>, <strong>desde dónde miran</strong> y <strong>para quién está escrito el resultado</strong>.</p>
<p>Esta nota explica la distinción en palabras que no requieren saber qué es un puerto TCP.</p>
<h2 id="que-hace-un-scanner-de-red-como-nessus-u-openvas">Qué hace un scanner de red como Nessus u OpenVAS</h2>
<p>Nessus, OpenVAS, Qualys y Nmap son herramientas de evaluación de <strong>infraestructura de red</strong>. Su función principal es:</p>
<ol>
<li>Conectarse a tu red interna (o que alguien con acceso lo haga).</li>
<li>Descubrir qué servidores, equipos e impresoras están encendidos.</li>
<li>Verificar qué servicios están escuchando en cada puerto (web, email, bases de datos, VPN, etc.).</li>
<li>Cruzar las versiones detectadas contra una base de datos de vulnerabilidades conocidas — el equivalente a decir &ldquo;tu servidor corre Apache 2.2.3 y esa versión tiene este CVE desde 2019&rdquo;.</li>
</ol>
<p>El resultado es típicamente una lista de cientos de hallazgos técnicos, ordenados por severidad, con identificadores CVE, puntajes CVSS y recomendaciones del tipo &ldquo;actualizar de la versión X a la Y&rdquo;. Es información precisa y valiosa — para el equipo de TI que tiene que ejecutar los parches.</p>
<p>Para un gerente general, un director de operaciones o un responsable de cumplimiento, ese reporte es literalmente ilegible sin traducción.</p>
<p>Hay otro límite igual de importante: <strong>esas herramientas necesitan acceso a tu red interna</strong>. No pueden ejecutarse desde afuera sin credenciales o agentes instalados. Miran adentro — lo que tú tienes en tu infraestructura privada.</p>
<h2 id="el-flanco-que-no-miran">El flanco que no miran</h2>
<p>Mientras las herramientas de red auditan lo que pasa dentro de tu perímetro, hay una superficie completamente distinta que está expuesta por definición: <strong>todo lo que publicas en internet</strong>.</p>
<p>Tu sitio web, los headers que entrega tu servidor, cómo está configurado tu correo, qué scripts externos cargas en tus páginas, si tus subdominios apuntan a servicios que ya no existen — todo eso es visible para cualquier persona con un browser y un poco de curiosidad. No requiere acceso a tu red. No requiere credenciales. Está ahí, todo el tiempo, esperando que alguien lo revise.</p>
<p>Es exactamente ese flanco el que evalúa FreeScan.</p>
<h2 id="que-evalua-freescan-y-como-se-ve-el-resultado">Qué evalúa FreeScan y cómo se ve el resultado</h2>
<p>FreeScan analiza tu dominio desde afuera, como lo haría un investigador de seguridad o un atacante en fase de reconocimiento. No se conecta a tu red interna, no necesita agentes, no requiere que abras ningún puerto. Sólo necesitas verificar que eres el dueño del dominio.</p>
<p>El informe que produce no está dirigido al equipo técnico que tiene que parchear servidores. Está dirigido a quien tiene que <strong>decidir qué arreglar primero</strong> y <strong>explicarle el estado de seguridad a un directorio o a un auditor</strong>.</p>
<p>Veamos cómo se ve en la práctica con el informe de un dominio real — <code>www.asentic.cl</code>, nuestro propio sitio.</p>
<h3 id="el-resumen-ejecutivo">El resumen ejecutivo</h3>
<p>Lo primero que aparece en el informe es una calificación directa:</p>
<div class="report-fragment">
  <div class="report-fragment__header">Extracto — Resumen ejecutivo · www.asentic.cl</div>
  <div class="report-fragment__body">
    <div class="report-grade-row">
      <div class="report-grade-badge grade-b">B</div>
      <div class="report-grade-detail">
        <strong>Score 88 / 100</strong><br>
        Postura aceptable con margen de mejora incremental.<br>
        <span class="report-counts">
          <span class="sev-baja">4 Bajos</span> ·
          <span class="sev-info">10 Informativos</span>
        </span>
      </div>
    </div>
    <p class="report-fragment__note">Método de verificación: email-domain · Escaneo completado en 47 s · 147 requests</p>
  </div>
</div>

<p>Una letra. Un número. Cuatro hallazgos que requieren acción, diez que son información de contexto. Cualquier gerente puede leer eso en diez segundos y saber si hay algo urgente.</p>
<p>En Nessus, el equivalente sería una tabla con 200 filas con columnas CVSS, CVE-ID, Plugin ID, Risk Factor. Ambas representaciones son correctas — para sus respectivas audiencias.</p>
<h3 id="un-hallazgo-con-todo-su-contexto">Un hallazgo, con todo su contexto</h3>
<p>Los hallazgos en FreeScan no son sólo una línea. Cada uno incluye descripción del riesgo real, el activo afectado, recomendación concreta y, relevante para esta nota, <strong>el mapeo a marcos regulatorios</strong>.</p>
<div class="report-fragment">
  <div class="report-fragment__header">Extracto — Hallazgo · EMAIL-MTA-STS-MISSING</div>
  <div class="report-fragment__body">
    <div class="report-finding-header">
      <span class="sev-badge sev-baja">Baja</span>
      <strong>MTA-STS ausente — SMTP vulnerable a downgrade attacks</strong>
    </div>
    <p><strong>Descripción:</strong> MTA-STS (RFC 8461) permite que el dominio publique una política que obliga a servidores externos a entregar el correo sólo con TLS válido. Sin esta política, un atacante con posición de red puede hacer downgrade del SMTP entrante a texto plano o interceptarlo.</p>
    <p><strong>Recomendación:</strong> Publicar registro DNS <code>_mta-sts.dominio</code> + archivo de política en <code>https://mta-sts.dominio/.well-known/mta-sts.txt</code> con <code>mode: enforce</code>. Agregar TLS-RPT para visibilidad de intentos de entrega.</p>
    <p class="report-compliance-tags"><strong>Marcos regulatorios:</strong>
      <span class="compliance-tag">ISO 27001:2022 A.8.21</span>
      <span class="compliance-tag">NIST CSF 2.0 PR.DS-02</span>
    </p>
  </div>
</div>

<p>Ese hallazgo tiene cero CVEs. No aparecería en Nessus. No es una vulnerabilidad de software con parche disponible — es una <strong>decisión de configuración</strong> que deja tu correo sin protección. Y es exactamente el tipo de riesgo que queda en el punto ciego de los scanners de red.</p>
<h3 id="el-mapa-de-cumplimiento-normativo">El mapa de cumplimiento normativo</h3>
<p>Aquí está la diferencia más práctica para quien debe reportar a un directorio, a un auditor externo o demostrar cumplimiento ante un cliente corporativo.</p>
<p>FreeScan cruza cada hallazgo contra seis marcos de cumplimiento simultáneamente: ISO 27001:2022, Ley 21.719 de Protección de Datos Personales, RGPD, NIST CSF 2.0, OWASP ASVS 5.0 y PCI DSS 4.0.1. El resultado es una tabla de preparación instantánea:</p>
<div class="report-fragment">
  <div class="report-fragment__header">Extracto — Compliance Snapshot · www.asentic.cl</div>
  <div class="report-fragment__body">
    <table class="compliance-table">
      <thead><tr><th>Marco</th><th>Preparación</th><th>Controles tocados</th><th>Hallazgos</th></tr></thead>
      <tbody>
        <tr><td>ISO 27001:2022</td><td><span class="readiness readiness-high">94 %</span></td><td>A.8.21 · A.8.24 · A.8.26 · A.8.3</td><td>10</td></tr>
        <tr><td>Ley 21.719</td><td><span class="readiness readiness-high">96 %</span></td><td>Art. 12 (información al titular)</td><td>2</td></tr>
        <tr><td>NIST CSF 2.0</td><td><span class="readiness readiness-high">94 %</span></td><td>PR.DS-02 · PR.PS-06 · PR.AA-01</td><td>9</td></tr>
        <tr><td>OWASP ASVS 5.0</td><td><span class="readiness readiness-high">96 %</span></td><td>V3 Frontend Security · V4 Access Control</td><td>5</td></tr>
        <tr><td>PCI DSS 4.0.1</td><td><span class="readiness readiness-high">96 %</span></td><td>6.4.3 · 7.2</td><td>5</td></tr>
        <tr><td>RGPD</td><td><span class="readiness readiness-high">96 %</span></td><td>Art. 13 · Art. 6</td><td>2</td></tr>
      </tbody>
    </table>
    <p class="report-fragment__note">Preparación calculada sobre el subconjunto de controles que FreeScan puede verificar desde el exterior.</p>
  </div>
</div>

<p>Esa tabla lleva a un directorio en cinco minutos de presentación. No requiere traducción técnica. Si tu sitio o tu organización está en proceso de certificación ISO 27001 y un hallazgo toca el control A.8.26, el área de cumplimiento sabe exactamente dónde registrarlo.</p>
<p>Ningún scanner de red genera esto. No porque sea incapaz técnicamente, sino porque no es su función — ellos reportan para el equipo de infraestructura, no para el área de cumplimiento.</p>
<h2 id="la-comparacion-directa">La comparación directa</h2>
<p>Si tienes que explicarle la diferencia a alguien en una frase:</p>
<blockquote>
<p>Nessus y OpenVAS revisan si las paredes de tu edificio tienen grietas estructurales.
FreeScan revisa si la vidriera está limpia, si el letrero de horario es correcto, si la puerta tiene cerrojo visible y si el local cumple la normativa de accesibilidad — todo desde la vereda, sin necesidad de entrar.</p>
</blockquote>
<p>Ambas evaluaciones son válidas y necesarias. Se complementan, no se reemplazan.</p>
<p>La tabla práctica:</p>
<table>
<thead>
<tr>
<th></th>
<th>Nessus / OpenVAS / Qualys</th>
<th>FreeScan</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Desde dónde</strong></td>
<td>Red interna (requiere acceso)</td>
<td>Internet abierto (sin acceso especial)</td>
</tr>
<tr>
<td><strong>Qué escanea</strong></td>
<td>Puertos, servicios, software instalado</td>
<td>Superficie web, headers, email, compliance, cadena de suministro JS</td>
</tr>
<tr>
<td><strong>Vulnerabilidades</strong></td>
<td>CVEs en software (Apache 2.2.3 tiene tal CVE)</td>
<td>Configuración incorrecta, exposición de datos, cadena de suministro</td>
</tr>
<tr>
<td><strong>Para quién</strong></td>
<td>Equipo de TI / Sysadmin</td>
<td>Gerencia, directorio, área de cumplimiento, responsable de seguridad</td>
</tr>
<tr>
<td><strong>Requiere</strong></td>
<td>Agente, credenciales o acceso a red</td>
<td>Solo el dominio y verificación de propiedad</td>
</tr>
<tr>
<td><strong>Output</strong></td>
<td>Lista técnica de CVEs con CVSS</td>
<td>Informe PDF con grade, prioridades P1-P5 y mapeo normativo</td>
</tr>
</tbody>
</table>
<h2 id="cuando-usar-cada-uno">Cuándo usar cada uno</h2>
<p><strong>Usa Nessus / Qualys / OpenVAS cuando:</strong>
- Necesitas auditar tu infraestructura interna (servidores, VMs, equipos de red).
- Tu equipo de TI tiene que priorizar parches de software.
- Estás en proceso de certificación que requiere escaneo de vulnerabilidades de red (algunos frameworks de PCI DSS lo exigen explícitamente para la red interna).
- Tienes un CISO o equipo de seguridad que consume y actúa sobre reportes técnicos.</p>
<p><strong>Usa FreeScan cuando:</strong>
- Quieres saber qué ve alguien de afuera antes de que te llamen para decirte que algo está mal.
- Tienes que presentarle el estado de seguridad a un directorio sin aburrirlos con CVEs.
- Necesitas demostrar preparación ante ISO 27001, Ley 21.719 o un auditor de cliente corporativo.
- Tu área de seguridad es una persona, no un equipo — o directamente no tienes área de seguridad.
- Cambiaste algo en tu sitio y quieres verificar que no abriste un hueco sin darte cuenta.</p>
<h2 id="lo-que-freescan-no-hace-y-es-importante-saberlo">Lo que FreeScan no hace (y es importante saberlo)</h2>
<p>Ser honesto sobre los límites es parte de la propuesta: FreeScan es una evaluación de la <strong>superficie externa</strong>. No:</p>
<ul>
<li>Audita tu red interna ni tus servidores por dentro.</li>
<li>Detecta vulnerabilidades en el código fuente de tu aplicación (eso requiere SAST o una revisión de código).</li>
<li>Reemplaza un pentest manual donde un especialista intenta explotar activamente los hallazgos.</li>
<li>Verifica si tus empleados caen en phishing (eso requiere simulaciones de ingeniería social).</li>
</ul>
<p>Esos son servicios distintos, con metodologías distintas. Lo que FreeScan sí hace es darte en menos de un minuto una foto objetiva de lo que cualquier persona puede ver de tu organización desde internet — con el contexto normativo que necesitas para priorizar y reportar.</p>
<hr>
<p><em>¿Quieres ver cómo quedaría el informe de tu dominio? Puedes solicitarlo en <a href="https://scan.asentic.cl/">scan.asentic.cl</a>. Si tienes preguntas sobre qué evaluar primero según tu industria o marco regulatorio, <a href="/contacto.html">escríbenos</a>.</em></p>]]></content>
    <category term="freescan"/>
    <category term="comparativa"/>
    <category term="hardening"/>
    <category term="compliance"/>
  </entry>

  <entry>
    <title>MTA-STS y TLS-RPT: la capa que cierra el correo en tránsito</title>
    <link href="https://www.asentic.cl/blog/mta-sts-tls-rpt/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/mta-sts-tls-rpt/</id>
    <published>2026-05-15T00:00:00+00:00</published>
    <updated>2026-05-15T00:00:00+00:00</updated>
    <summary type="text">DMARC impide que otros spoofeen tu dominio. MTA-STS impide que el correo que va hacia ti viaje en texto plano sin que nadie lo sepa. Los dos controles protegen cosas distintas — y sin el segundo, la pila está incompleta.</summary>
    <content type="html"><![CDATA[<p>Llegar a DMARC <code>p=reject</code> es la meta que la mayoría de las guías plantean como &ldquo;tener el correo seguro&rdquo;. Y sí — cerrar el spoofing del <code>From:</code> es un paso importante. Pero hay una superficie que DMARC no cubre: el transporte del mensaje mientras viaja entre servidores.</p>
<p>DMARC protege quién dice que manda el correo. MTA-STS protege cómo viaja ese correo hasta llegar a tu buzón. Son capas distintas del mismo problema.</p>
<h2 id="la-pila-completa-de-seguridad-del-correo">La pila completa de seguridad del correo</h2>
<p>Antes de entrar al detalle, un mapa rápido de lo que cubre cada control:</p>
<table>
<thead>
<tr>
<th>Control</th>
<th>Qué protege</th>
<th>Sin él</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>SPF</strong></td>
<td>Qué servidores pueden enviar a nombre de tu dominio</td>
<td>Cualquier servidor puede mandar como <code>@tu-dominio.cl</code></td>
</tr>
<tr>
<td><strong>DKIM</strong></td>
<td>Integridad y autenticidad del mensaje (firma criptográfica)</td>
<td>El mensaje puede modificarse en tránsito sin detección</td>
</tr>
<tr>
<td><strong>DMARC</strong></td>
<td>Alineación entre el <code>From:</code> visible y SPF/DKIM; política para fallos</td>
<td>Spoofing del <code>From:</code> visible pasa sin acción definida</td>
</tr>
<tr>
<td><strong>MTA-STS</strong></td>
<td>TLS obligatorio en la entrega SMTP hacia tus servidores</td>
<td>El transporte puede degradarse a texto plano en un ataque MITM</td>
</tr>
<tr>
<td><strong>TLS-RPT</strong></td>
<td>Visibilidad sobre fallos TLS en tránsito hacia tus servidores</td>
<td>Los fallos (incluyendo ataques activos) son invisibles</td>
</tr>
<tr>
<td><strong>DNSSEC</strong></td>
<td>Integridad de los registros DNS del dominio</td>
<td>Envenenamiento de caché puede redirigir al servidor equivocado</td>
</tr>
</tbody>
</table>
<p>El <a href="/blog/dmarc-en-reject/">post anterior sobre DMARC</a> cubre las primeras tres columnas en detalle. Acá nos enfocamos en MTA-STS y TLS-RPT — los dos controles que aparecen como hallazgo en la mayoría de los scans del FreeScan y que casi ninguna organización tiene configurados.</p>
<h2 id="el-problema-starttls-no-es-obligatorio">El problema: STARTTLS no es obligatorio</h2>
<p>SMTP existe hace décadas, mucho antes de que TLS fuera obligatorio en cualquier cosa. Para agregar cifrado sin romper los servidores viejos, se inventó STARTTLS: una extensión que le dice al servidor receptor &ldquo;¿soportas TLS? Si sí, actualicemos la conexión&rdquo;.</p>
<p>El problema de fondo: STARTTLS es <strong>oportunista</strong>, no obligatorio. El servidor que envía el correo pregunta, pero si el receptor dice &ldquo;no lo soporto&rdquo; — o si alguien en el medio intercepta la negociación y responde &ldquo;no&rdquo; en nombre del receptor — la entrega se hace igual, en texto plano.</p>
<p>Ese ataque se llama STARTTLS downgrade. No requiere romper criptografía: sólo interceptar el banner de la negociación y responder que TLS no está disponible. El servidor emisor, cumpliendo con el protocolo, entrega en claro.</p>
<p>Desde una red controlada (un ISP comprometido, un operador que hace inspección de tráfico, un router corporativo con MITM activo), este ataque es trivial y completamente silencioso para ambas partes — el que envía y el que recibe.</p>
<h2 id="mta-sts-hacer-tls-obligatorio-en-la-entrega-hacia-tu-dominio">MTA-STS: hacer TLS obligatorio en la entrega hacia tu dominio</h2>
<p>MTA-STS (Mail Transfer Agent Strict Transport Security, <a href="https://www.rfc-editor.org/rfc/rfc8461">RFC 8461</a>) es el mecanismo que convierte TLS de oportunista a obligatorio para los servidores que entregan correo hacia ti.</p>
<p>El mecanismo funciona así:</p>
<ol>
<li>Tu dominio publica un registro DNS que señala que tiene una política MTA-STS.</li>
<li>Esa política vive en un archivo accesible vía HTTPS (no DNS), lo que la ancla en TLS: si el DNS del dominio está envenenado, el servidor emisor falla porque no puede verificar la política por HTTPS.</li>
<li>Cuando un servidor externo va a entregarte correo, consulta esa política antes de conectarse.</li>
<li>Si la política dice <code>enforce</code> y la conexión TLS no puede establecerse correctamente, el servidor emisor <strong>rechaza la entrega</strong> en lugar de bajar a texto plano.</li>
</ol>
<h3 id="componente-1-el-registro-dns">Componente 1: el registro DNS</h3>
<p>Un TXT record en <code>_mta-sts.&lt;tu-dominio&gt;</code>:</p>
<div class="post-code"><pre><span></span><code>_mta-sts.organizacion-ejemplo.cl. IN TXT &quot;v=STSv1; id=20260515T120000&quot;
</code></pre></div>

<p>El campo <code>id=</code> es un identificador de versión que el servidor emisor usa para saber si su política en caché sigue siendo vigente. Cámbialo cada vez que actualices el archivo de política — un timestamp en formato <code>YYYYMMDDTHHmmss</code> funciona bien. Si el <code>id</code> no cambió, el servidor usa su caché; si cambió, re-descarga el archivo.</p>
<h3 id="componente-2-el-archivo-de-politica">Componente 2: el archivo de política</h3>
<p>El archivo vive en <code>https://mta-sts.&lt;tu-dominio&gt;/.well-known/mta-sts.txt</code> — sí, un subdominio <code>mta-sts</code> con un certificado TLS válido. Ese HTTPS es la parte que ancla el mecanismo: un atacante que envenene el DNS no puede redirigir la descarga del archivo si no tiene un cert válido para <code>mta-sts.organizacion-ejemplo.cl</code>.</p>
<p>Contenido del archivo:</p>
<div class="post-code"><pre><span></span><code>version: STSv1
mode: enforce
mx: mail.organizacion-ejemplo.cl
mx: *.organizacion-ejemplo.cl
max_age: 604800
</code></pre></div>

<p>Parámetros:</p>
<ul>
<li><strong><code>version: STSv1</code></strong> — obligatorio, primer campo.</li>
<li><strong><code>mode:</code></strong> — el campo más importante. Tres valores:</li>
<li><code>testing</code> — los fallos se reportan (si hay TLS-RPT) pero no bloquean la entrega. Equivale al <code>p=none</code> de DMARC: visibilidad sin protección.</li>
<li><code>enforce</code> — un fallo TLS bloquea la entrega. La entrega en texto plano queda prohibida.</li>
<li><code>none</code> — desactiva la política. Útil para retirarla gradualmente.</li>
<li><strong><code>mx:</code></strong> — patrón de los MX records autorizados. El servidor emisor verifica que el cert TLS del servidor receptor cubra alguno de estos patrones. Uno o más valores, uno por línea.</li>
<li><strong><code>max_age:</code></strong> — segundos que el servidor emisor puede cachear la política. 604800 = 7 días. Máximo recomendado: 31557600 (1 año) en producción estable.</li>
</ul>
<h3 id="el-subdominio-mta-sts">El subdominio <code>mta-sts</code></h3>
<p>El servidor que sirve el archivo HTTPS necesita:</p>
<ol>
<li>Un A/AAAA record para <code>mta-sts.organizacion-ejemplo.cl</code>.</li>
<li>Un certificado TLS válido (no self-signed) que cubra ese hostname.</li>
</ol>
<p>La mayoría de organizaciones con Cloudflare lo configuran como un Worker o una Page Function; las que tienen control del servidor simplemente sirven el archivo estático desde el mismo servidor web con un virtual host dedicado.</p>
<p>Un archivo de política en <code>testing</code> primero, <code>enforce</code> después de 1-2 semanas sin incidentes reportados en los logs de TLS-RPT.</p>
<h2 id="tls-rpt-visibilidad-sobre-fallos-en-transito">TLS-RPT: visibilidad sobre fallos en tránsito</h2>
<p>TLS-RPT (<a href="https://www.rfc-editor.org/rfc/rfc8460">RFC 8460</a>) es el mecanismo de reporte que acompaña a MTA-STS. Cuando un servidor externo intenta entregarte correo y falla el TLS (por política MTA-STS, por cert inválido, por cualquier fallo de handshake), te manda un reporte JSON diario con el detalle de qué pasó.</p>
<p>Sin TLS-RPT, un ataque STARTTLS downgrade activo es invisible. Con TLS-RPT, los intentos de downgrade aparecen en los reportes como <code>starttls-not-supported</code> o <code>validation-failure</code> — señal de que alguien en el camino está interfiriendo.</p>
<h3 id="el-registro-dns">El registro DNS</h3>
<p>Un TXT record en <code>_smtp._tls.&lt;tu-dominio&gt;</code>:</p>
<div class="post-code"><pre><span></span><code>_smtp._tls.organizacion-ejemplo.cl. IN TXT &quot;v=TLSRPTv1; rua=mailto:tls-rpt@organizacion-ejemplo.cl&quot;
</code></pre></div>

<p>El campo <code>rua=</code> apunta a donde quieres recibir los reportes. Puede ser un email, un endpoint HTTPS, o ambos separados por coma:</p>
<div class="post-code"><pre><span></span><code>v=TLSRPTv1; rua=mailto:tls-rpt@organizacion-ejemplo.cl,https://report.organizacion-ejemplo.cl/tls-rpt
</code></pre></div>

<p>En la práctica, para la mayoría de organizaciones, un buzón dedicado alcanza. Los reportes llegan como JSON comprimido en gzip — menos legibles que un XML de DMARC, pero con información más accionable: cada falla incluye el tipo de error (<code>certificate-expired</code>, <code>validation-failure</code>, <code>starttls-not-supported</code>), la cantidad de mensajes afectados, y el MX servidor donde ocurrió.</p>
<h2 id="el-orden-de-configuracion-recomendado">El orden de configuración recomendado</h2>
<ol>
<li>
<p><strong>Configura TLS-RPT primero</strong> — sólo el registro DNS, sin archivo de política. Así empiezas a recibir reportes sobre el estado actual del TLS hacia tus MX antes de tocar nada.</p>
</li>
<li>
<p><strong>Despliega MTA-STS en modo <code>testing</code></strong> — registro DNS + subdominio + archivo con <code>mode: testing</code>. Esto hace que los servidores que soportan MTA-STS te reporten fallos, pero sigan entregando aunque falle TLS. Equivale a <code>p=none</code> en DMARC.</p>
</li>
<li>
<p><strong>Revisa TLS-RPT durante 1-2 semanas</strong> — ¿Hay fallos? ¿Son de cert mal configurado en uno de tus MX? ¿Son fallos transient de handshake? ¿O son <code>starttls-not-supported</code> que podrían indicar interferencia activa?</p>
</li>
<li>
<p><strong>Cambia a <code>mode: enforce</code></strong> — cuando los reportes muestren que el TLS funciona limpio y sin fallos inesperados. Actualiza el <code>id=</code> en el registro DNS para forzar la re-descarga del archivo.</p>
</li>
</ol>
<h2 id="dnssec-la-capa-que-ancla-el-dns">DNSSEC: la capa que ancla el DNS</h2>
<p>MTA-STS ya mitiga el riesgo de DNS poisoning para la política, porque el archivo de política lo descarga por HTTPS con cert verificado. Pero la cadena todavía empieza con un lookup DNS: si el registro <code>_mta-sts</code> en sí está envenenado para apuntar a otro servidor o simplemente devuelve NXDOMAIN, los servidores emisores no encuentran la política y pueden caer al comportamiento por defecto (STARTTLS oportunista).</p>
<p>DNSSEC firma criptográficamente los registros DNS del dominio, haciendo que cualquier respuesta manipulada sea detectable. No es el control más urgente — MTA-STS en <code>enforce</code> ya da protección sólida — pero cierra la cadena completa.</p>
<p>Para dominios <code>.cl</code>, DNSSEC está soportado por NIC.cl. La configuración varía según el registrar y el DNS autoritativo que uses.</p>
<h2 id="verificacion-rapida-con-dig">Verificación rápida con dig</h2>
<div class="post-code"><pre><span></span><code><span class="c1"># Registro MTA-STS</span>
dig<span class="w"> </span>+short<span class="w"> </span>TXT<span class="w"> </span>_mta-sts.organizacion-ejemplo.cl
<span class="c1"># Esperado: &quot;v=STSv1; id=...&quot;</span>

<span class="c1"># Política en modo enforce</span>
curl<span class="w"> </span>-s<span class="w"> </span>https://mta-sts.organizacion-ejemplo.cl/.well-known/mta-sts.txt
<span class="c1"># Esperado: version: STSv1 / mode: enforce / mx: ...</span>

<span class="c1"># TLS-RPT</span>
dig<span class="w"> </span>+short<span class="w"> </span>TXT<span class="w"> </span>_smtp._tls.organizacion-ejemplo.cl
<span class="c1"># Esperado: &quot;v=TLSRPTv1; rua=mailto:...&quot;</span>

<span class="c1"># DNSSEC (buscar flags AD en la respuesta)</span>
dig<span class="w"> </span>+dnssec<span class="w"> </span>TXT<span class="w"> </span>organizacion-ejemplo.cl<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>flags
<span class="c1"># Esperado: flags: qr rd ra ad</span>
</code></pre></div>

<p>El flag <code>ad</code> (Authenticated Data) en la respuesta de dig confirma que el resolver validó DNSSEC para ese nombre.</p>
<h2 id="como-lo-detecta-el-freescan">Cómo lo detecta el FreeScan</h2>
<p>El <a href="https://scan.asentic.cl/">Asentic FreeScan</a> verifica los tres controles sobre cualquier dominio que escanees:</p>
<ul>
<li><strong><code>EMAIL-MTA-STS-MISSING</code></strong> — Baja. El dominio no tiene registro <code>_mta-sts</code> o el archivo de política no es accesible. Sin MTA-STS el transporte SMTP hacia tus MX puede degradarse a texto plano sin que nadie lo note.</li>
<li><strong><code>EMAIL-TLS-RPT-MISSING</code></strong> — Info. No hay registro <code>_smtp._tls</code>. Los fallos de TLS en tránsito son invisibles; ni tú ni el servidor emisor tienen forma de saber que el cifrado falló.</li>
<li><strong><code>EMAIL-DNSSEC-MISSING</code></strong> — Baja. El dominio no publica registros DNSSEC. La cadena de confianza del DNS queda sin firma criptográfica; posible vector de envenenamiento de caché.</li>
</ul>
<p>En los scans actuales, <code>EMAIL-MTA-STS-MISSING</code> y <code>EMAIL-TLS-RPT-MISSING</code> aparecen en más del 90% de los dominios analizados — incluyendo dominios con DMARC en <code>p=reject</code> correcto, lo que confirma que la implementación de email security se suele quedar incompleta en esa última capa.</p>
<h2 id="cierre">Cierre</h2>
<p>DMARC cierra el spoofing del <code>From:</code>. MTA-STS cierra el downgrade del transporte. TLS-RPT te da la visibilidad para saber cuándo algo falla en el camino.</p>
<p>Los tres controles juntos arman la pila completa. Sin MTA-STS, un atacante en el camino puede leer (o modificar) el correo que va hacia tu dominio, en silencio, aunque tengas DMARC en <code>p=reject</code> y todos los certificados en orden. Con MTA-STS en <code>enforce</code> y TLS-RPT activo, ese ataque produce un fallo visible y deja de ser silencioso.</p>
<p>La configuración completa toma menos de una hora: dos registros DNS, un subdominio con un archivo de texto, y un buzón donde recibir los reportes. El ratio esfuerzo/seguridad es de los mejores en toda la pila.</p>]]></content>
    <category term="dns"/>
    <category term="email"/>
    <category term="mta-sts"/>
    <category term="tls"/>
    <category term="hardening"/>
    <category term="dmarc"/>
  </entry>

  <entry>
    <title>DMARC en p=reject: el registro DNS que cierra el spoofing de tu dominio</title>
    <link href="https://www.asentic.cl/blog/dmarc-en-reject/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/dmarc-en-reject/</id>
    <published>2026-05-13T00:00:00+00:00</published>
    <updated>2026-05-13T00:00:00+00:00</updated>
    <summary type="text">SPF y DKIM no alcanzan: un atacante puede mandar email diciendo ser tu dominio y pasar ambos checks. DMARC cierra el hueco — pero sólo cuando llegas a p=reject. Camino seguro de p=none → p=quarantine → p=reject sin romper email legítimo.</summary>
    <content type="html"><![CDATA[<p>SPF y DKIM se ven completos en una auditoría superficial. Resuelven cosas distintas — SPF dice qué servidores pueden mandar email a nombre de tu dominio, DKIM firma el mensaje con clave criptográfica — y juntos parecen suficientes.</p>
<p>No lo son. Sin DMARC, un atacante puede mandar email desde cualquier servidor declarando que viene de tu dominio, y los mailbox providers no tienen instrucción clara de qué hacer con ese mensaje. Algunos lo dejan pasar a inbox, otros lo marcan como spam, otros lo bouncean — depende del provider, no de tu política.</p>
<p>DMARC cierra esa ambigüedad. Le dice a Google, Microsoft, Yahoo y al resto qué hacer cuando un mensaje falla SPF o DKIM, y te manda un reporte diario de qué pasó. Pero sólo si llegas a <code>p=reject</code>. Cualquier estado intermedio te da visibilidad sin protección efectiva.</p>
<h2 id="el-hueco-que-spf-y-dkim-dejan-abierto">El hueco que SPF y DKIM dejan abierto</h2>
<p>Para que un mensaje pase SPF, el servidor que lo entregó debe estar listado en el TXT record <code>v=spf1 ...</code> de tu dominio. Para que pase DKIM, el header <code>DKIM-Signature</code> debe verificar contra la clave pública publicada en <code>&lt;selector&gt;._domainkey.&lt;dominio&gt;</code>.</p>
<p>Lo que <strong>ni SPF ni DKIM verifican por sí solos</strong>: que el <code>From:</code> que el destinatario ve coincida con el dominio cubierto por SPF/DKIM. Un atacante puede:</p>
<ol>
<li>Mandar email desde su propio servidor, con su propio SPF válido para su dominio.</li>
<li>Firmar el mensaje con DKIM de su dominio.</li>
<li>Poner como <code>From:</code> <code>cobranzas@organizacion-ejemplo.cl</code>.</li>
</ol>
<p>SPF pasa — su servidor está autorizado para SU dominio. DKIM pasa — la firma valida contra SU clave pública. Y el destinatario ve un correo aparentemente de <code>organizacion-ejemplo.cl</code> que pasó todos los checks de autenticación.</p>
<p>DMARC resuelve esto con un concepto que se llama <strong>alineación</strong>: exige que el dominio del <code>From:</code> (lo que el usuario ve) sea el mismo, o un subdominio, del dominio cubierto por SPF/DKIM (lo que se verificó técnicamente).</p>
<p>Si la alineación falla, DMARC dispara la política que tú definiste.</p>
<h2 id="las-tres-politicas-y-cual-te-protege-de-verdad">Las tres políticas y cuál te protege de verdad</h2>
<div class="post-code"><pre><span></span><code>v=DMARC1; p=none;       rua=mailto:dmarc@organizacion-ejemplo.cl
v=DMARC1; p=quarantine; rua=mailto:dmarc@organizacion-ejemplo.cl
v=DMARC1; p=reject;     rua=mailto:dmarc@organizacion-ejemplo.cl
</code></pre></div>

<p>Tres niveles, cada uno con un propósito distinto:</p>
<ul>
<li>
<p><strong><code>p=none</code></strong> — recibes los reportes, pero no le dices al provider que haga nada. Si llega un email fraudulento que falla DMARC, el provider lo procesa como siempre. <strong>Esta política no te protege.</strong> Sirve sólo como modo observación para mapear tu propio tráfico legítimo antes de subir.</p>
</li>
<li>
<p><strong><code>p=quarantine</code></strong> — el provider debería poner el mensaje en spam. &ldquo;Debería&rdquo; porque Gmail/Outlook todavía aplican su propio criterio: un quarantine para un dominio con buena reputación puede igual aterrizar en inbox. Mejor que <code>none</code>, peor que <code>reject</code>.</p>
</li>
<li>
<p><strong><code>p=reject</code></strong> — el provider rechaza el mensaje en el SMTP, sin entregarlo. Es la única política que realmente cierra el spoofing.</p>
</li>
</ul>
<p>La trampa común: organizaciones que llegan a <code>p=none</code>, ven los reportes, los archivan, y se quedan ahí por años pensando que están &ldquo;implementando DMARC&rdquo;. No. Hasta que no estés en <code>p=reject</code>, el spoofing sigue funcionando.</p>
<h2 id="el-camino-seguro-a-preject">El camino seguro a p=reject</h2>
<p>Saltar directo a <code>p=reject</code> es peligroso si tu organización manda email desde lugares que no controlas. SaaS de CRM, plataformas de newsletters, sistemas de facturación, ERPs en la nube — cada uno manda email diciendo ser tu dominio. Si no están alineados con tu SPF/DKIM, un <code>p=reject</code> los hace bounce silencioso.</p>
<p>El camino recomendado tiene cuatro etapas:</p>
<h3 id="etapa-1-pnone-con-reportes-activos">Etapa 1 — <code>p=none</code> con reportes activos</h3>
<div class="post-code"><pre><span></span><code>v=DMARC1; p=none; rua=mailto:dmarc@organizacion-ejemplo.cl; pct=100
</code></pre></div>

<p>Lo dejas 30 días. Durante esos 30 días recibes reportes XML diarios (uno por proveedor receptor: Google, Microsoft, Yahoo, etc.) listando todos los servidores que mandaron email a nombre de tu dominio.</p>
<p>Tu trabajo en esta etapa: identificar cada IP/host que aparezca y clasificarlo en &ldquo;es legítimo&rdquo; (tu sistema de email transaccional, tu CRM, tu plataforma de marketing) o &ldquo;no lo reconozco&rdquo; (spam, spoofing, ex-proveedor que no migraste).</p>
<p>Para cada sender legítimo no autorizado todavía: agrégalo a tu SPF o configura DKIM en ese servicio.</p>
<h3 id="etapa-2-pquarantine-con-pct-gradual">Etapa 2 — <code>p=quarantine</code> con <code>pct</code> gradual</h3>
<div class="post-code"><pre><span></span><code>v=DMARC1; p=quarantine; pct=10; rua=mailto:dmarc@organizacion-ejemplo.cl
</code></pre></div>

<p><code>pct=10</code> significa &ldquo;aplica la política sólo al 10% de los mensajes que fallan; al resto sigue tratándolos como antes&rdquo;. Es un canary deploy.</p>
<p>Lo subes gradualmente: 10 → 25 → 50 → 100 a lo largo de 2-4 semanas, verificando en cada paso que no aparezcan reportes de bounce de email legítimo.</p>
<h3 id="etapa-3-pquarantine-pct100-estable">Etapa 3 — <code>p=quarantine; pct=100</code> estable</h3>
<div class="post-code"><pre><span></span><code>v=DMARC1; p=quarantine; rua=mailto:dmarc@organizacion-ejemplo.cl
</code></pre></div>

<p>Una o dos semanas en este estado. Sirve para detectar cualquier servicio que mande email muy esporádicamente — boletines mensuales, recordatorios trimestrales, encuestas anuales — que no apareció en las primeras 4-6 semanas de observación.</p>
<h3 id="etapa-4-preject">Etapa 4 — <code>p=reject</code></h3>
<div class="post-code"><pre><span></span><code>v=DMARC1; p=reject; rua=mailto:dmarc@organizacion-ejemplo.cl; adkim=s; aspf=s
</code></pre></div>

<p>Ahora el spoofing está cerrado. <code>adkim=s</code> y <code>aspf=s</code> activan modo strict de alineación: el dominio debe matchear exacto, no un subdominio. Para la mayoría de organizaciones strict alignment está bien, pero verifica los reportes 1-2 semanas más después del flip por si algún sender depende de relaxed.</p>
<h2 id="como-se-ven-los-reportes-rua">Cómo se ven los reportes RUA</h2>
<p>Los reportes llegan diariamente a la dirección que pusiste en <code>rua=</code>. Cada provider receptor agrega los mensajes que recibió de tu dominio en un XML comprimido. Estructura típica:</p>
<div class="post-code"><pre><span></span><code><span class="nt">&lt;feedback&gt;</span>
<span class="w">  </span><span class="nt">&lt;report_metadata&gt;</span>
<span class="w">    </span><span class="nt">&lt;org_name&gt;</span>google.com<span class="nt">&lt;/org_name&gt;</span>
<span class="w">    </span><span class="nt">&lt;date_range&gt;</span>
<span class="w">      </span><span class="nt">&lt;begin&gt;</span>1715558400<span class="nt">&lt;/begin&gt;</span>
<span class="w">      </span><span class="nt">&lt;end&gt;</span>1715644800<span class="nt">&lt;/end&gt;</span>
<span class="w">    </span><span class="nt">&lt;/date_range&gt;</span>
<span class="w">  </span><span class="nt">&lt;/report_metadata&gt;</span>
<span class="w">  </span><span class="nt">&lt;policy_published&gt;</span>
<span class="w">    </span><span class="nt">&lt;domain&gt;</span>organizacion-ejemplo.cl<span class="nt">&lt;/domain&gt;</span>
<span class="w">    </span><span class="nt">&lt;p&gt;</span>reject<span class="nt">&lt;/p&gt;</span>
<span class="w">  </span><span class="nt">&lt;/policy_published&gt;</span>
<span class="w">  </span><span class="nt">&lt;record&gt;</span>
<span class="w">    </span><span class="nt">&lt;row&gt;</span>
<span class="w">      </span><span class="nt">&lt;source_ip&gt;</span>185.211.x.x<span class="nt">&lt;/source_ip&gt;</span>
<span class="w">      </span><span class="nt">&lt;count&gt;</span>3<span class="nt">&lt;/count&gt;</span>
<span class="w">      </span><span class="nt">&lt;policy_evaluated&gt;</span>
<span class="w">        </span><span class="nt">&lt;disposition&gt;</span>reject<span class="nt">&lt;/disposition&gt;</span>
<span class="w">        </span><span class="nt">&lt;dkim&gt;</span>fail<span class="nt">&lt;/dkim&gt;</span>
<span class="w">        </span><span class="nt">&lt;spf&gt;</span>fail<span class="nt">&lt;/spf&gt;</span>
<span class="w">      </span><span class="nt">&lt;/policy_evaluated&gt;</span>
<span class="w">    </span><span class="nt">&lt;/row&gt;</span>
<span class="w">    </span><span class="nt">&lt;identifiers&gt;</span>
<span class="w">      </span><span class="nt">&lt;header_from&gt;</span>organizacion-ejemplo.cl<span class="nt">&lt;/header_from&gt;</span>
<span class="w">    </span><span class="nt">&lt;/identifiers&gt;</span>
<span class="w">  </span><span class="nt">&lt;/record&gt;</span>
<span class="nt">&lt;/feedback&gt;</span>
</code></pre></div>

<p>Procesar esto a mano es inviable cuando el volumen sube. Hay servicios SaaS que parsean los XML y te dan un dashboard (Postmark, dmarcian, Valimail). Para PYMEs el flujo &ldquo;buzón dedicado + revisión semanal&rdquo; alcanza durante los primeros meses.</p>
<h2 id="configuracion-del-txt">Configuración del TXT</h2>
<p>DMARC vive en un subdomain reservado: <code>_dmarc.&lt;tu-dominio&gt;</code>. En la zona DNS:</p>
<div class="post-code"><pre><span></span><code>_dmarc.organizacion-ejemplo.cl. IN TXT &quot;v=DMARC1; p=reject; rua=mailto:dmarc@organizacion-ejemplo.cl; adkim=s; aspf=s&quot;
</code></pre></div>

<p>Parámetros que vale la pena considerar:</p>
<ul>
<li><strong><code>pct=</code> (porcentaje)</strong> — útil sólo durante el ramp de <code>quarantine</code>. En <code>reject</code> final, omítelo (default es 100).</li>
<li><strong><code>adkim=s</code> / <code>aspf=s</code></strong> — modo strict. La alternativa <code>r</code> (relaxed) permite que <code>mail.organizacion-ejemplo.cl</code> aparezca como dominio firmante para emails con <code>From: @organizacion-ejemplo.cl</code>. Strict es más seguro; relaxed es más tolerante a subdomains organizacionales.</li>
<li><strong><code>sp=</code></strong> — política específica para subdomains. Si lo omites, hereda <code>p=</code>. Útil cuando tienes subdomains que no mandan email (la mayoría) — explicitar <code>sp=reject</code> cierra el spoofing de <code>cualquier-cosa.organizacion-ejemplo.cl</code>.</li>
<li><strong><code>fo=1</code></strong> — forensic options. <code>fo=1</code> dispara reporte cuando cualquier mecanismo falla (no sólo cuando ambos fallan). Más verbose, útil en troubleshooting.</li>
</ul>
<h2 id="el-caso-del-subdomain-olvidado">El caso del subdomain olvidado</h2>
<p>Un escenario que rompe muchas implementaciones: tu DMARC en <code>organizacion-ejemplo.cl</code> con <code>p=reject</code> está perfecto, pero alguien manda phishing desde <code>noresponder.organizacion-ejemplo.cl</code> o <code>notificaciones.organizacion-ejemplo.cl</code> — un subdomain que la organización nunca usa para email.</p>
<p>Sin <code>sp=</code> explícito, el provider hereda <code>p=</code> del apex y rechaza. Pero si en algún momento alguien puso <code>sp=none</code> &ldquo;para no romper nada&rdquo;, o si los subdomains tienen su propio <code>_dmarc.&lt;sub&gt;</code> con política más laxa, el atacante encuentra el hueco.</p>
<p>Revisa siempre que la zona no tenga un <code>_dmarc.&lt;algo&gt;.organizacion-ejemplo.cl</code> rezagado con <code>p=none</code>.</p>
<h2 id="como-verificas">Cómo verificas</h2>
<div class="post-code"><pre><span></span><code><span class="c1"># Política del apex</span>
dig<span class="w"> </span>+short<span class="w"> </span>TXT<span class="w"> </span>_dmarc.organizacion-ejemplo.cl

<span class="c1"># Política de un subdomain específico (raro pero posible)</span>
dig<span class="w"> </span>+short<span class="w"> </span>TXT<span class="w"> </span>_dmarc.notificaciones.organizacion-ejemplo.cl

<span class="c1"># Validador externo (sintaxis + preview de comportamiento)</span>
<span class="c1"># https://dmarcian.com/dmarc-inspector/?domain=organizacion-ejemplo.cl</span>
</code></pre></div>

<p>Lo importante en la respuesta: el primer token debe ser <code>v=DMARC1;</code> exacto. Errores comunes:</p>
<ul>
<li><code>"v=DMARC1, p=reject..."</code> con coma en vez de punto y coma → registro inválido, equivale a no tenerlo.</li>
<li>Múltiples TXT records con <code>v=DMARC1</code> en el mismo nombre → los providers tratan como inválido y caen al default (<code>p=none</code>).</li>
<li><code>rua=</code> apuntando a un dominio externo sin autorización en su zona (<code>_report._dmarc</code>) → los providers ignoran el reporte por anti-abuso. Si quieres recibir reportes en otro dominio, ese dominio debe publicar <code>&lt;tu-dominio&gt;._report._dmarc.&lt;otro-dominio&gt;</code> autorizándote.</li>
</ul>
<h2 id="como-lo-detecta-el-freescan">Cómo lo detecta el FreeScan</h2>
<p>El <a href="https://scan.asentic.cl/">Asentic FreeScan</a> distingue cuatro estados:</p>
<ul>
<li><strong>DMARC ausente</strong> — Media. El dominio no tiene <code>_dmarc</code> publicado; el spoofing está abierto.</li>
<li><strong>DMARC en <code>p=none</code></strong> — Media. Hay política, pero no protege. Lo más común en sitios que &ldquo;implementaron DMARC hace años y lo dejaron así&rdquo;.</li>
<li><strong>DMARC en <code>p=quarantine</code></strong> — Baja. Protección parcial; mejor que none, peor que reject.</li>
<li><strong>DMARC en <code>p=reject</code> sin <code>sp=</code></strong> — Info. La protección del apex está, pero los subdomains pueden estar expuestos según herencia.</li>
</ul>
<p>La distinción importa porque &ldquo;tiene DMARC&rdquo; en una auditoría no técnica oculta que el 70% del tiempo está en <code>p=none</code> y no protege nada.</p>
<h2 id="cierre">Cierre</h2>
<p>Llegar a <code>p=reject</code> toma 6-8 semanas si tu inventario de senders está claro, 3-6 meses si la organización tiene marketing automation, CRM, ERP y notificaciones transaccionales repartidas en múltiples SaaS.</p>
<p>Pero el día que estás en <code>p=reject</code>, el costo marginal para un atacante de mandar phishing con tu nombre sube de &ldquo;trivial&rdquo; a &ldquo;imposible sin comprometer un servidor autorizado&rdquo;. Para la mayoría de empresas chilenas mid-market, esa diferencia vale el esfuerzo del ramp.</p>
<p>Si tu dominio todavía está en <code>p=none</code>, ya tienes la mitad del trabajo hecho — el TXT publicado y los reportes llegando. Falta la parte que importa: subirlo.</p>]]></content>
    <category term="dns"/>
    <category term="dmarc"/>
    <category term="email"/>
    <category term="spf"/>
    <category term="hardening"/>
  </entry>

  <entry>
    <title>Subdomain takeover: cómo un CNAME huérfano se convierte en un dominio hostil</title>
    <link href="https://www.asentic.cl/blog/subdomain-takeover/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/subdomain-takeover/</id>
    <published>2026-05-13T00:00:00+00:00</published>
    <updated>2026-05-13T00:00:00+00:00</updated>
    <summary type="text">Un subdominio que apunta por CNAME a un servicio SaaS retirado puede ser reclamado por cualquiera. El atacante hereda tu nombre, tu cert TLS válido y la confianza de tus usuarios. Cómo detectarlo y prevenirlo.</summary>
    <content type="html"><![CDATA[<p>Un patrón recurrente en zonas DNS de organizaciones medianas y grandes: subdominios huérfanos que apuntan a servicios SaaS retirados. El equipo de marketing crea <code>promo2024.organizacion-ejemplo.cl</code> apuntando a un sitio de campaña en un hosting SaaS, termina la campaña, alguien borra la app — pero <strong>nadie toca el DNS</strong>.</p>
<p>Resultado: un atacante crea una cuenta en ese mismo proveedor SaaS, reclama exactamente el mismo nombre que tenía la app borrada, y ahora <code>promo2024.organizacion-ejemplo.cl</code> resuelve a un servidor controlado por él. Con TLS válido. En tu zona.</p>
<p>A esto se le llama <strong>subdomain takeover</strong>, y es una vulnerabilidad de impacto alto que vive en el espacio entre &ldquo;responsabilidad de DevOps&rdquo; y &ldquo;responsabilidad del equipo que usa el SaaS&rdquo;. Por eso suele quedar sin dueño.</p>
<h2 id="el-mecanismo-paso-a-paso">El mecanismo, paso a paso</h2>
<ol>
<li><strong>Tu zona DNS tiene un CNAME</strong> del tipo <code>sub.organizacion-ejemplo.cl. → algo.saas.com.</code></li>
<li><strong>El recurso en el SaaS se borra o expira</strong> (la app de Heroku, el bucket de S3, el sitio de Netlify, el subdomain de Shopify, etc.). El CNAME en tu zona queda apuntando a un nombre que <strong>ya no existe en ese SaaS</strong>.</li>
<li><strong>El SaaS permite &ldquo;claim&rdquo; del nombre</strong>: cualquier usuario puede crear un recurso nuevo con ese mismo subdomain interno. La mayoría de proveedores no valida que tú seas el dueño del CNAME que apunta hacia ahí.</li>
<li><strong>El atacante reclama el nombre</strong>. Su recurso ahora responde cuando alguien resuelve <code>sub.organizacion-ejemplo.cl</code> → CNAME a <code>algo.saas.com</code> → el contenido del atacante.</li>
<li><strong>Heredás la URL completa</strong>: tu nombre, en tu dominio, con TLS válido (porque muchos SaaS proveen cert automático para el hostname configurado), con la confianza acumulada de la zona.</li>
</ol>
<p>El atacante puede ahora:</p>
<ul>
<li>Servir un sitio de phishing con tu marca y URL legítima.</li>
<li>Robar cookies con dominio <code>.organizacion-ejemplo.cl</code> (si tu apex setea cookies sin <code>Path</code> estricto).</li>
<li>Ejecutar JS bajo el mismo origen que cualquier app que importe scripts desde ese subdomain.</li>
<li>Setear meta-tags arbitrarios y aparecer en SEO como tu sitio oficial.</li>
<li>Recibir emails enviados a <code>*@sub.organizacion-ejemplo.cl</code> si setea MX (escenario más raro pero posible).</li>
</ul>
<h2 id="donde-pasa-mas-seguido">Dónde pasa más seguido</h2>
<p>Cualquier SaaS que use CNAME a hostnames internos y permita claim libre. La lista cambia con el tiempo — muchos proveedores arreglaron el bug exigiendo verificación de ownership — pero a 2026 los patrones más comunes siguen siendo:</p>
<ul>
<li><strong>Heroku</strong> — <code>*.herokuapp.com</code> para apps borradas.</li>
<li><strong>GitHub Pages</strong> — repos archivados o borrados sin quitar el CNAME del custom domain.</li>
<li><strong>AWS S3 / CloudFront</strong> — buckets sin verificación de ownership.</li>
<li><strong>Azure</strong> — App Service, Blob Storage, Traffic Manager, Front Door.</li>
<li><strong>GCP</strong> — Cloud Storage, App Engine, Firebase Hosting, Cloud Run.</li>
<li><strong>Vercel / Netlify</strong> — proyectos eliminados con CNAME residual.</li>
<li><strong>Shopify, Webflow, Pantheon, Fastly, Tumblr, WordPress.com</strong> — el patrón se repite en hosting tipo SaaS.</li>
</ul>
<p>Cada proveedor tiene un &ldquo;fingerprint&rdquo; — texto en la respuesta HTTP que indica &ldquo;este nombre no está reclamado&rdquo;. Por ejemplo, GitHub Pages devuelve <code>"There isn't a GitHub Pages site here."</code>, Heroku devuelve <code>"No such app"</code>. Herramientas de detección como <a href="https://github.com/haccer/subjack">subjack</a> y <a href="https://github.com/m4ll0k/takeover">takeover</a> usan exactamente esa lista.</p>
<h2 id="como-verificas-tu-zona">Cómo verificas tu zona</h2>
<h3 id="manual-para-un-subdominio-puntual">Manual, para un subdominio puntual</h3>
<div class="post-code"><pre><span></span><code><span class="c1"># 1) ¿el sub tiene CNAME?</span>
dig<span class="w"> </span>+short<span class="w"> </span>CNAME<span class="w"> </span>sub.organizacion-ejemplo.cl

<span class="c1"># 2) ¿resuelve a una IP?</span>
dig<span class="w"> </span>+short<span class="w"> </span>A<span class="w"> </span>sub.organizacion-ejemplo.cl

<span class="c1"># 3) ¿qué responde el destino?</span>
curl<span class="w"> </span>-sI<span class="w"> </span>https://sub.organizacion-ejemplo.cl/
curl<span class="w"> </span>-s<span class="w"> </span>https://sub.organizacion-ejemplo.cl/<span class="w"> </span><span class="p">|</span><span class="w"> </span>head<span class="w"> </span>-20
</code></pre></div>

<p>Las señales rojas:</p>
<ul>
<li>Hay CNAME, pero <code>dig A</code> no resuelve a nada → posible NXDOMAIN del target.</li>
<li>El HTTP responde con uno de los strings conocidos del SaaS (<code>"No such app"</code>, <code>"There isn't a GitHub Pages site here"</code>, etc.).</li>
<li>El cert TLS no matchea el hostname (otro caso, llamado <strong>dangling IP recycled</strong> — un IP que era tuyo ahora es de otro tenant).</li>
</ul>
<h3 id="a-escala-para-todo-el-dominio">A escala, para todo el dominio</h3>
<p>Inventario de subdominios desde CT logs:</p>
<div class="post-code"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s1">&#39;https://crt.sh/?q=%25.organizacion-ejemplo.cl&amp;output=json&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">&#39;.[].name_value&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span><span class="s1">&#39;,&#39;</span><span class="w"> </span><span class="s1">&#39;\n&#39;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sort<span class="w"> </span>-u<span class="w"> </span>&gt;<span class="w"> </span>subs.txt
</code></pre></div>

<p>Después corrés algo como subjack contra esa lista. Lo importante: <strong>el inventario debe rehacerse seguido</strong>, porque CT logs solo te dan dominios que tuvieron cert TLS en algún momento, y los CNAME a SaaS típicamente disparan eso (los SaaS auto-provisionan TLS).</p>
<h2 id="la-parte-que-mas-cuesta-prevencion">La parte que más cuesta: prevención</h2>
<p>La detección es relativamente fácil. Lo difícil es que el patrón <strong>no reaparezca</strong>, y eso requiere disciplina organizacional, no técnica.</p>
<h3 id="regla-operativa-dns-first-on-creation-dns-last-on-retirement">Regla operativa: <strong>DNS first on creation, DNS last on retirement</strong></h3>
<ul>
<li>Cuando creas un subdomain apuntando a un SaaS: documenta quién es el dueño, en qué proyecto vive, y cuándo se revisará.</li>
<li>Cuando vas a retirar un servicio: <strong>primero borras el DNS, después borras el recurso en el SaaS</strong>. Si lo haces al revés (borras el recurso, dejas el DNS), creas exactamente la ventana de oportunidad.</li>
</ul>
<h3 id="inventario-versionado">Inventario versionado</h3>
<p>Tu zona DNS debería estar en git, o al menos exportarse periódicamente a un repo. Cualquier CNAME hacia un host externo necesita un comentario que diga <strong>para qué servicio es</strong> y <strong>quién lo creó</strong>. Si nadie reconoce el CNAME, candidato a borrar.</p>
<h3 id="verificacion-de-ownership-en-el-saas-cuando-exista">Verificación de ownership en el SaaS, cuando exista</h3>
<p>Varios proveedores hoy permiten &ldquo;domain verification&rdquo; — un TXT record en tu zona que prueba que tú eres el dueño. Si el SaaS lo soporta, actívalo. Eso bloquea el claim externo aunque el CNAME quede huérfano.</p>
<h3 id="monitoreo-continuo-no-auditoria-puntual">Monitoreo continuo, no auditoría puntual</h3>
<p>Una auditoría manual cada 6 meses NO sirve. El CNAME huérfano puede aparecer mañana y ser reclamado pasado mañana. La detección efectiva es diaria, automatizada, con alerta al equipo de seguridad cuando aparece un nuevo huérfano.</p>
<p>Es exactamente el problema que nos llevó a construir el <a href="/servicios/dangling-monitor.html">Dangling Monitor</a> de Asentic — auto-descubre subdominios desde CT logs, clasifica cada uno en 5 estados (<code>healthy</code>, <code>dangling-cname-nxdomain</code>, <code>dangling-cname-saas-claimable</code>, <code>dangling-ip-recycled</code>, <code>dangling-ns</code>) y alerta cuando aparece uno nuevo o cuando uno existente cambia de estado. El servicio corre como monitoreo continuo con cadencia diaria y entrega los hallazgos por email.</p>
<h2 id="los-patrones-mas-comunes-en-la-industria">Los patrones más comunes en la industria</h2>
<p>Las publicaciones de hallazgos en programas de bug bounty y los reportes de la industria muestran que los takeovers documentados siguen unos pocos patrones recurrentes, independientes del sector de la organización afectada:</p>
<ul>
<li><strong>Microsites de campaña sobre hostings SaaS de corta vida</strong>. Subdominios tipo <code>evento-2019.organizacion-ejemplo.cl</code> o <code>promo-fin-de-anio.organizacion-ejemplo.cl</code> apuntando a un hosting tipo Heroku, Netlify o Vercel. La app se borra cuando termina la campaña; el CNAME permanece. Años después el nombre interno queda disponible para que cualquier cuenta lo reclame.</li>
<li><strong>Storefronts retirados sobre plataformas de e-commerce</strong>. CNAMEs hacia tiendas que se desactivaron al consolidar el catálogo en un dominio principal. El proveedor permite que otro merchant active el mismo subdominio si nadie verificó el ownership.</li>
<li><strong>Documentación técnica sobre GitHub Pages</strong>. Repos archivados por ex-colaboradores en cuentas personales (no organizacionales), que el proveedor termina eliminando por inactividad. El CNAME custom domain en la zona corporativa sobrevive al repo.</li>
</ul>
<p>El denominador común en todos los casos: <strong>nadie en la organización tenía registro de que ese CNAME existía</strong>. No es un problema técnico — es un problema de inventario y trazabilidad.</p>
<h2 id="cierre">Cierre</h2>
<p>Si tu zona DNS tiene más de 20 subdominios y nunca corriste un check de takeover, asume que tienes al menos uno. La probabilidad sube con cada equipo que usa SaaS sin coordinación con seguridad — marketing, eventos, dev, customer success.</p>
<p>La detección manual te resuelve el problema de ahora. La detección continua te lo resuelve para siempre.</p>]]></content>
    <category term="dns"/>
    <category term="subdomain"/>
    <category term="takeover"/>
    <category term="hardening"/>
    <category term="monitoring"/>
  </entry>

  <entry>
    <title>HSTS en Apache: cómo activarlo sin romper subdominios</title>
    <link href="https://www.asentic.cl/blog/hsts-en-apache/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/hsts-en-apache/</id>
    <published>2026-05-12T00:00:00+00:00</published>
    <updated>2026-05-12T00:00:00+00:00</updated>
    <summary type="text">Strict-Transport-Security cierra la ventana de TLS stripping pero, mal puesto, deja inaccesibles subdominios HTTP legacy. Guía para llegar a max-age=2 años + includeSubDomains + preload sin sustos.</summary>
    <content type="html"><![CDATA[<p><code>Strict-Transport-Security</code> (HSTS) le dice al browser que tu sitio se accede SIEMPRE por HTTPS. La primera vez que un visitante llega por HTTPS y recibe el header, lo guarda; cualquier intento futuro de cargar el sitio por <code>http://</code> se reescribe a <code>https://</code> antes de salir del browser.</p>
<p>Es el control que cierra el último hueco que dejó la migración a HTTPS: el primer <code>http://</code> que un usuario tipea o que un link viejo apunta. Sin HSTS, ese primer request va en claro y puede ser interceptado para hacer downgrade (TLS stripping).</p>
<p>El problema: HSTS es <strong>pegajoso</strong>. Una vez que un browser lo memoriza, no hay forma práctica de bajarse rápido si te equivocaste. Por eso vale la pena entender qué activas antes de pegarlo.</p>
<h2 id="anatomia-del-header">Anatomía del header</h2>
<div class="post-code"><pre><span></span><code>Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
</code></pre></div>

<p>Tres piezas, cada una con su trade-off:</p>
<ul>
<li><strong><code>max-age=63072000</code></strong> — segundos que el browser memoriza la política. 63072000 = 2 años. Lo mínimo razonable para producción es 6 meses (<code>15552000</code>). Menos de eso es práctica de &ldquo;estoy probando&rdquo;.</li>
<li><strong><code>includeSubDomains</code></strong> — extiende la regla a TODOS los subdominios del host actual. Es lo que sube la postura de &ldquo;tu apex está protegido&rdquo; a &ldquo;tu zona DNS entera está protegida&rdquo;.</li>
<li><strong><code>preload</code></strong> — declara intención de aparecer en la <a href="https://hstspreload.org/">HSTS preload list</a>. Los browsers traen esa lista hardcoded; tu sitio queda protegido <em>antes</em> del primer request. Es la única forma de cerrar el &ldquo;primer-request gap&rdquo; del 100%.</li>
</ul>
<h2 id="la-trampa-de-includesubdomains">La trampa de <code>includeSubDomains</code></h2>
<p>Este es el campo que más se equivoca. Implica que cualquier subdominio bajo tu zona también va a forzarse a HTTPS.</p>
<p>Escenario típico que rompe sitios en producción:</p>
<ul>
<li><code>organizacion-ejemplo.cl</code> está en HTTPS con cert válido. Se le activa HSTS con <code>includeSubDomains</code>.</li>
<li>La misma zona tiene un <code>legacy.organizacion-ejemplo.cl</code> viejo, sin cert, sirviendo un sitio interno por HTTP plano (intranet, monitoreo Grafana, lo que sea).</li>
<li>A partir del primer visit a <code>organizacion-ejemplo.cl</code>, ese mismo browser ya no puede entrar a <code>legacy.organizacion-ejemplo.cl</code> por HTTP. Y como no tiene cert TLS, tampoco por HTTPS. El subdominio queda inalcanzable hasta que el max-age expire.</li>
</ul>
<p>La lógica defensiva es la correcta — un atacante que controle DNS del subdominio podría exfiltrar cookies del apex si HSTS no aplicara a subs. Pero en práctica te tomas el tiempo de inventariar todos tus subs antes de activar la regla.</p>
<h2 id="el-camino-seguro-aumentar-gradualmente">El camino seguro: aumentar gradualmente</h2>
<p>La receta recomendada:</p>
<h3 id="paso-1-max-age-corto-sin-includesubdomains-sin-preload">Paso 1 — <code>max-age</code> corto, sin <code>includeSubDomains</code>, sin <code>preload</code></h3>
<div class="post-code"><pre><span></span><code>Strict-Transport-Security: max-age=300
</code></pre></div>

<p>Cinco minutos. Si rompes algo, en 5 minutos los browsers olvidan. Lo dejas 24-48 horas en producción para verificar que el apex realmente sirve TLS bien (no hay request mixto, no hay redirect-loop, todo funciona).</p>
<h3 id="paso-2-max-age-6-meses-sin-includesubdomains-todavia">Paso 2 — <code>max-age</code> 6 meses, sin <code>includeSubDomains</code> todavía</h3>
<div class="post-code"><pre><span></span><code>Strict-Transport-Security: max-age=15552000
</code></pre></div>

<p>Lo dejas 1-2 semanas. Esto ya te da la protección real para el apex.</p>
<h3 id="paso-3-inventario-de-subdominios-decision-sobre-includesubdomains">Paso 3 — Inventario de subdominios + decisión sobre <code>includeSubDomains</code></h3>
<p>Antes de agregarlo, listá todos tus subs:</p>
<div class="post-code"><pre><span></span><code>dig +short NS organizacion-ejemplo.cl
dig +short ANY organizacion-ejemplo.cl
# o consulta los CT logs:
curl -s &#39;https://crt.sh/?q=%25.organizacion-ejemplo.cl&amp;output=json&#39; | jq -r &#39;.[].name_value&#39; | sort -u
</code></pre></div>

<p>Para cada uno, verifica:</p>
<ul>
<li>¿Sirve HTTPS con cert válido?</li>
<li>¿Es un subdominio &ldquo;vivo&rdquo; o queda DNS huérfano? (los huérfanos no importan para HSTS, pero deberías limpiarlos por otras razones).</li>
<li>¿Hay alguno que SÓLO sirva HTTP por diseño? (intranets viejas, dashboards internos sin TLS).</li>
</ul>
<p>Si todos sirven HTTPS bien, agregas <code>includeSubDomains</code>. Si hay alguno HTTP-only, primero lo migras a HTTPS o lo retiras del DNS.</p>
<div class="post-code"><pre><span></span><code>Strict-Transport-Security: max-age=15552000; includeSubDomains
</code></pre></div>

<p>Una semana más en este estado.</p>
<h3 id="paso-4-max-age-2-anos-preload">Paso 4 — <code>max-age</code> 2 años + <code>preload</code></h3>
<p>Cuando ya estás seguro de que todo el ecosistema soporta TLS y quieres cerrar el primer-request gap:</p>
<div class="post-code"><pre><span></span><code>Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
</code></pre></div>

<p>Submitís el dominio en <a href="https://hstspreload.org/">hstspreload.org</a>. Toma 1-3 meses entrar en la próxima release de Chromium, después Firefox/Safari/Edge la heredan.</p>
<p><strong>Importante:</strong> una vez en la preload list, salir es lento. Solicitar la remoción puede tomar meses y los browsers viejos pueden quedarte preloaded por años. No actives <code>preload</code> mientras dudes.</p>
<h2 id="config-en-apache">Config en Apache</h2>
<h3 id="via-htaccess-o-vhost">Vía <code>.htaccess</code> o vhost</h3>
<div class="post-code"><pre><span></span><code><span class="nt">&lt;IfModule</span><span class="w"> </span><span class="s">mod_headers.c</span><span class="nt">&gt;</span>
<span class="w">    </span><span class="nb">Header</span><span class="w"> </span>always<span class="w"> </span>set<span class="w"> </span>Strict-Transport-Security<span class="w"> </span><span class="s2">&quot;max-age=63072000; includeSubDomains; preload&quot;</span>
<span class="nt">&lt;/IfModule&gt;</span>
</code></pre></div>

<p>El flag <code>always</code> es importante: sin él, Apache aplica el header solo en respuestas 200; con <code>always</code> también lo manda en 30x/40x/50x, donde tradicionalmente se &ldquo;olvida&rdquo; — y es justamente en un 301 HTTP→HTTPS donde más útil resulta.</p>
<h3 id="redirect-301-obligatorio-antes">Redirect 301 obligatorio antes</h3>
<p>HSTS asume que tu sitio responde por HTTPS. El redirect HTTP→HTTPS debe estar antes:</p>
<div class="post-code"><pre><span></span><code><span class="nt">&lt;VirtualHost</span><span class="w"> </span><span class="s">*:80</span><span class="nt">&gt;</span>
<span class="w">    </span><span class="nb">ServerName</span><span class="w"> </span>organizacion-ejemplo.cl
<span class="w">    </span><span class="nb">ServerAlias</span><span class="w"> </span>www.organizacion-ejemplo.cl
<span class="w">    </span><span class="nb">Redirect</span><span class="w"> </span>permanent<span class="w"> </span>/<span class="w"> </span>https://organizacion-ejemplo.cl/
<span class="nt">&lt;/VirtualHost&gt;</span>
</code></pre></div>

<p>O en <code>.htaccess</code>:</p>
<div class="post-code"><pre><span></span><code><span class="nt">&lt;IfModule</span><span class="w"> </span><span class="s">mod_rewrite.c</span><span class="nt">&gt;</span>
<span class="w">    </span><span class="nb">RewriteEngine</span><span class="w"> </span><span class="k">On</span>
<span class="w">    </span><span class="nb">RewriteCond</span><span class="w"> </span>%{HTTPS}<span class="w"> </span><span class="k">off</span>
<span class="w">    </span><span class="nb">RewriteRule</span><span class="w"> </span>^<span class="w"> </span>https://%{HTTP_HOST}%{REQUEST_URI}<span class="w"> </span>[L,R=301]
<span class="nt">&lt;/IfModule&gt;</span>
</code></pre></div>

<p>El header HSTS debe servirse desde HTTPS (los browsers ignoran HSTS recibido por HTTP), así que el vhost :443 es el que lleva el <code>Header always set</code>.</p>
<h2 id="detras-de-cloudflare">Detrás de Cloudflare</h2>
<p>Si estás detrás de CF (o cualquier CDN), tienes dos opciones para servir HSTS:</p>
<ol>
<li><strong>Del origen</strong>: Apache pone el header, CF lo deja pasar. Pro: una sola fuente de verdad. Contra: si tu origen alguna vez se sirve directo (CF bypass), el header sigue saliendo.</li>
<li><strong>Del edge (CF dashboard)</strong>: CF inyecta el header. Pro: aunque tu origen se sirva en HTTP por algún motivo, CF te cubre. Contra: si migras de CDN o quitas CF, pierdes el header sin enterarte.</li>
</ol>
<p>En la práctica, <strong>ambos</strong>: el origen lo manda Y CF lo refuerza. Cloudflare detecta duplicados y deja uno solo.</p>
<h2 id="como-verificas">Cómo verificas</h2>
<h3 id="antes-de-pegarlo-en-produccion">Antes de pegarlo en producción</h3>
<div class="post-code"><pre><span></span><code># verifica que el sitio sirve HTTPS bien
curl -sI https://organizacion-ejemplo.cl/ | grep -i &quot;strict-transport&quot;

# verifica que ningún subdominio queda HTTP-only:
for sub in www app api blog dashboard mail; do
  echo &quot;--- $sub.organizacion-ejemplo.cl ---&quot;
  curl -sI --max-time 5 https://$sub.organizacion-ejemplo.cl/ 2&gt;&amp;1 | head -1
done
</code></pre></div>

<h3 id="despues-de-pegarlo">Después de pegarlo</h3>
<div class="post-code"><pre><span></span><code># debería verse el header completo
curl -sI https://organizacion-ejemplo.cl/ | grep -i &quot;strict-transport&quot;
# Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

# verifica que el redirect 301 desde HTTP funciona
curl -sI http://organizacion-ejemplo.cl/ | head -5
# debería devolver 301 hacia https://...
</code></pre></div>

<h3 id="antes-de-submitir-a-preload">Antes de submitir a preload</h3>
<p><a href="https://hstspreload.org/">hstspreload.org</a> hace los checks por ti: header presente, max-age suficiente, <code>includeSubDomains</code>, <code>preload</code>, redirect 301, cert válido, subdominios accesibles por HTTPS. Si fallas algún check, te dice cuál — y mejor arreglarlo antes que después.</p>
<h2 id="salida-de-emergencia">Salida de emergencia</h2>
<p>Si pegaste HSTS y quieres revertir:</p>
<ol>
<li><strong>Antes de preload</strong>: bajá <code>max-age</code> a 0 (no quites el header). Los browsers que reciban <code>max-age=0</code> borran la entrada. Tardás <code>max-age</code> original en cubrir a todos los visitantes que tuvieron la versión anterior.</li>
<li><strong>Después de preload</strong>: hay que solicitar remoción en la HSTS preload list + esperar la siguiente release de Chrome (~6 semanas) + heredan los demás browsers (~3 meses) + los browsers viejos pueden quedarte protegidos por años. No hay rollback rápido.</li>
</ol>
<h2 id="como-lo-detectamos-en-el-freescan">Cómo lo detectamos en el FreeScan</h2>
<p>El <a href="https://scan.asentic.cl/">Asentic FreeScan</a> reporta tres patrones distintos:</p>
<ul>
<li><strong>HSTS ausente</strong> — Medium. El sitio funciona en HTTPS pero no protege el primer request.</li>
<li><strong>HSTS con max-age corto</strong> — Low. El header está, pero <code>max-age &lt; 6 meses</code> te deja una ventana de exposición innecesaria.</li>
<li><strong>HSTS sin <code>includeSubDomains</code> o sin <code>preload</code></strong> — Info. Eligible para subir la postura, no es un riesgo per se.</li>
</ul>
<p>La distinción importa porque &ldquo;no implementado&rdquo; y &ldquo;implementado pobre&rdquo; se ven igual en una auditoría superficial, pero requieren acciones distintas.</p>
<h2 id="cierre">Cierre</h2>
<p>HSTS es de los headers con mejor ratio efecto/esfuerzo. La parte difícil no es el header en sí — son tres líneas de Apache — sino el inventario de subdominios que hay que hacer antes para no romper el ecosistema. Dedicarle un día a eso y migrar gradualmente vale el costo de la postura final.</p>
<p>Si tu sitio ya sirve HTTPS hace tiempo, lo más probable es que el camino al <code>preload</code> esté a una semana de distancia.</p>]]></content>
    <category term="hsts"/>
    <category term="tls"/>
    <category term="apache"/>
    <category term="headers"/>
    <category term="hardening"/>
  </entry>

  <entry>
    <title>CSP en WordPress: cómo escribir un header útil sin romper el sitio</title>
    <link href="https://www.asentic.cl/blog/csp-en-wordpress/" rel="alternate" type="text/html"/>
    <id>https://www.asentic.cl/blog/csp-en-wordpress/</id>
    <published>2026-05-11T00:00:00+00:00</published>
    <updated>2026-05-11T00:00:00+00:00</updated>
    <summary type="text">Content-Security-Policy es de los headers más útiles para frenar XSS y data exfil, pero también el más fácil de inutilizar. En WordPress, además, la mitad de los plugins inyecta inline scripts. Esta es una receta concreta para llegar a una CSP estricta sin dejar el panel inservible.</summary>
    <content type="html"><![CDATA[<p>Content-Security-Policy (CSP) es uno de los headers que más nos toca discutir con equipos de desarrollo. Es de los pocos controles que <strong>mitigan XSS aunque el código siga vulnerable</strong>: aunque un atacante consiga inyectar un <code>&lt;script&gt;</code> malicioso, el browser se niega a ejecutarlo si no encaja en la política. También bloquea exfiltración a dominios no listados, frena form-jacking y reduce el blast radius de un plugin comprometido.</p>
<p>El problema: en WordPress muy pocas instalaciones la tienen bien configurada. Las dos rutas habituales son:</p>
<ol>
<li><strong>CSP ausente</strong> — nadie la puso, el sitio queda con la postura por defecto del browser.</li>
<li><strong>CSP nominalmente puesta</strong> con <code>default-src *</code> o <code>script-src 'self' 'unsafe-inline' 'unsafe-eval'</code> — escrita para que &ldquo;no rompa nada&rdquo;, lo que en la práctica equivale a no tener CSP.</li>
</ol>
<p>Este post es una receta concreta para llevar un WordPress real desde &ldquo;sin CSP&rdquo; hasta &ldquo;CSP estricta funcional&rdquo;, sin pasar por el infierno del soporte llamando porque el admin no carga.</p>
<h2 id="que-hace-csp-en-60-segundos">Qué hace CSP, en 60 segundos</h2>
<p>La política se envía como cabecera HTTP (<code>Content-Security-Policy: ...</code>) o como <code>&lt;meta&gt;</code>. Cada directiva define qué orígenes están autorizados para cargar un tipo de recurso:</p>
<ul>
<li><code>script-src 'self'</code> — sólo JS del mismo origen.</li>
<li><code>script-src 'self' https://www.googletagmanager.com</code> — JS del mismo origen + GTM.</li>
<li><code>script-src 'self' 'sha256-abc...'</code> — JS del mismo origen + un script inline cuyo contenido tiene ese hash.</li>
<li><code>script-src 'self' 'nonce-XYZ'</code> — JS del mismo origen + scripts inline marcados con <code>nonce="XYZ"</code>.</li>
</ul>
<p>Cuando el browser ve un recurso que no encaja, lo bloquea y opcionalmente reporta. <strong>Mientras más estricta la política, mejor protege</strong>, pero también más fácil es que algo deje de funcionar. El truco está en saber qué relajar y qué mantener.</p>
<h2 id="por-que-wordpress-la-rompe">Por qué WordPress la rompe</h2>
<p>WP es un campo minado para CSP por cinco razones, todas estructurales:</p>
<ol>
<li><strong>Inline scripts en el admin.</strong> El bloque <code>wp-admin/</code> está lleno de <code>&lt;script&gt;</code> inline con configuración por request (nonces, <code>userSettings</code>, traducciones). Sin <code>'unsafe-inline'</code> ni hashes/nonces, el panel deja de funcionar.</li>
<li><strong>Plugins que inyectan inline.</strong> Yoast, Elementor, WooCommerce, RankMath, varios plugins de analítica y ads inyectan <code>&lt;script&gt;</code> inline con datos por request. El contenido cambia entre páginas — los hashes estáticos no sirven.</li>
<li><strong><code>wp_add_inline_script</code> y <code>wp_localize_script</code>.</strong> WordPress mismo expone APIs para que los plugins inyecten configuración inline. El core lo hace, los themes lo hacen.</li>
<li><strong>jQuery y bundled libraries.</strong> WP bundlea jQuery, jQuery UI, Underscore, Backbone, etc. Si tu theme los usa con <code>?ver=</code> cachebuster, vas a tener URLs ligeramente distintas por release.</li>
<li><strong>CDNs de terceros.</strong> Google Fonts, Font Awesome, Bootstrap CDN, embeds (YouTube, Vimeo, Maps), pasarelas de pago, reCAPTCHA, livechat — todos suman orígenes externos.</li>
</ol>
<p>La conclusión práctica: una CSP estricta para WordPress <strong>requiere conocer qué carga tu instalación específica</strong>. No hay una política universal copy-paste que funcione bien.</p>
<h2 id="estrategia-report-only-primero-enforce-despues">Estrategia: Report-Only primero, enforce después</h2>
<p>El error más caro es publicar la CSP en modo enforcement sin medir antes. Si te equivocas, el sitio se cae para los visitantes (o peor: para los admins, y vuelves a quedar bloqueado tú).</p>
<p>El header <code>Content-Security-Policy-Report-Only</code> hace <strong>exactamente lo mismo</strong> pero sin bloquear. El browser sólo reporta las violaciones que habrían ocurrido. Con eso medimos durante 1-2 semanas qué se rompería con la política propuesta, y la ajustamos antes de enforcement.</p>
<p>Flujo recomendado:</p>
<ol>
<li>Diseñar una política propuesta (más estricta de lo que crees que tolera el sitio).</li>
<li>Configurar un endpoint para recibir reports.</li>
<li>Publicar la política en modo <code>Report-Only</code> durante ~2 semanas.</li>
<li>Revisar los reports, ajustar la política para cubrir falsos positivos legítimos.</li>
<li>Pasar a enforce (<code>Content-Security-Policy</code>) cuando los reports queden estables.</li>
</ol>
<h3 id="endpoint-de-reports">Endpoint de reports</h3>
<p>Lo mínimo: un script que reciba el JSON POST y lo escriba a log. Si no quieres mantener uno, <a href="https://report-uri.com/">report-uri.com</a> ofrece un free tier suficiente. Para WordPress, un endpoint custom en un plugin propio basta:</p>
<div class="post-code"><pre><span></span><code><span class="x">add_action(&#39;rest_api_init&#39;, function () {</span>
<span class="x">    register_rest_route(&#39;csp/v1&#39;, &#39;/report&#39;, [</span>
<span class="x">        &#39;methods&#39;  =&gt; &#39;POST&#39;,</span>
<span class="x">        &#39;callback&#39; =&gt; function ($request) {</span>
<span class="x">            $body = $request-&gt;get_body();</span>
<span class="x">            error_log(&#39;[CSP] &#39; . $body, 3, WP_CONTENT_DIR . &#39;/csp-reports.log&#39;);</span>
<span class="x">            return new WP_REST_Response(null, 204);</span>
<span class="x">        },</span>
<span class="x">        &#39;permission_callback&#39; =&gt; &#39;__return_true&#39;,</span>
<span class="x">    ]);</span>
<span class="x">});</span>
</code></pre></div>

<p>Y en la cabecera: <code>report-uri https://organizacion-ejemplo.cl/wp-json/csp/v1/report;</code>. Para CSP 3, también: <code>report-to csp-endpoint;</code> con el grupo definido vía <code>Report-To</code> (más nuevo, no todos los browsers lo soportan aún — mantener ambos por un tiempo).</p>
<h2 id="identificar-inline-scripts-legitimos">Identificar inline scripts legítimos</h2>
<p>Para no caer en <code>'unsafe-inline'</code>, necesitas que los inline scripts que sí son legítimos pasen. Hay tres formas:</p>
<h3 id="hashes">Hashes</h3>
<p><code>'sha256-&lt;base64&gt;'</code>. Funciona bien para <strong>bloques fijos</strong>, ejemplo: un <code>&lt;script type="application/ld+json"&gt;</code> con tu Schema.org. Mientras el contenido no cambie, el hash sirve.</p>
<p>Para calcular el hash de un bloque (Python):</p>
<div class="post-code"><pre><span></span><code><span class="kn">import</span> <span class="nn">hashlib</span><span class="o">,</span> <span class="nn">base64</span>
<span class="n">script</span> <span class="o">=</span> <span class="s1">&#39;...&#39;</span>  <span class="c1"># contenido exacto entre &lt;script&gt;...&lt;/script&gt;, sin las tags</span>
<span class="nb">print</span><span class="p">(</span><span class="s1">&#39;sha256-&#39;</span> <span class="o">+</span> <span class="n">base64</span><span class="o">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">(</span><span class="n">script</span><span class="o">.</span><span class="n">encode</span><span class="p">())</span><span class="o">.</span><span class="n">digest</span><span class="p">())</span><span class="o">.</span><span class="n">decode</span><span class="p">())</span>
</code></pre></div>

<p>Limitación: si el bloque tiene cualquier cosa dinámica (timestamp, sessionId, nonce de form), el hash cambia por request y no sirve.</p>
<h3 id="nonces">Nonces</h3>
<p><code>'nonce-&lt;aleatorio-por-request&gt;'</code>. WordPress puede emitir un nonce CSP en cada response, y los plugins que cooperan lo van pegando a sus inline scripts:</p>
<div class="post-code"><pre><span></span><code><span class="x">add_action(&#39;init&#39;, function () {</span>
<span class="x">    if (!defined(&#39;CSP_NONCE&#39;)) {</span>
<span class="x">        define(&#39;CSP_NONCE&#39;, base64_encode(random_bytes(16)));</span>
<span class="x">    }</span>
<span class="x">});</span>
</code></pre></div>

<p>Y luego en el <code>&lt;script&gt;</code> que inyectas: <code>&lt;script nonce="&lt;?php echo esc_attr(CSP_NONCE); ?&gt;"&gt;</code>. Cabecera: <code>script-src 'self' 'nonce-&lt;?php echo CSP_NONCE; ?&gt;';</code>.</p>
<p>El problema en WP: <strong>los plugins de terceros no usan tu nonce</strong>. Tienes que parchearlos uno por uno, o usar un plugin &ldquo;CSP Manager&rdquo; que intercepta <code>wp_add_inline_script</code> y le agrega el nonce. La regla pragmática: vale la pena el esfuerzo para inline <em>propio</em>, no para perseguir cada plugin.</p>
<h3 id="strict-dynamic">strict-dynamic</h3>
<p>CSP 3 introduce <code>'strict-dynamic'</code>: si un script con nonce/hash crea otros scripts, los hijos heredan la confianza automáticamente. Es la dirección recomendada por el W3C para apps modernas. En WordPress es complicado porque WP no fue diseñado pensando en esto — el nonce tiene que llegar a todo el árbol de carga, lo que rara vez es práctico.</p>
<h2 id="un-header-funcional-para-un-wp-tipico">Un header funcional para un WP típico</h2>
<p>Suponiendo un WP institucional con Yoast, Cloudflare, Google Analytics y un form de contacto. La política mínima estricta razonable:</p>
<div class="post-code"><pre><span></span><code>Content-Security-Policy:
  default-src &#39;self&#39;;
  base-uri &#39;self&#39;;
  object-src &#39;none&#39;;
  frame-ancestors &#39;self&#39;;
  form-action &#39;self&#39;;
  img-src &#39;self&#39; data: https://secure.gravatar.com https://*.googleusercontent.com;
  font-src &#39;self&#39; https://fonts.gstatic.com data:;
  style-src &#39;self&#39; &#39;unsafe-inline&#39; https://fonts.googleapis.com;
  script-src &#39;self&#39; &#39;nonce-XYZ&#39; https://www.googletagmanager.com https://www.google-analytics.com;
  connect-src &#39;self&#39; https://www.google-analytics.com https://region1.google-analytics.com;
  frame-src &#39;self&#39; https://www.youtube-nocookie.com;
  report-uri /wp-json/csp/v1/report;
  upgrade-insecure-requests;
</code></pre></div>

<p>Notas sobre cada directiva:</p>
<ul>
<li><strong><code>default-src 'self'</code></strong>: catch-all conservador. Cualquier tipo no listado cae acá.</li>
<li><strong><code>base-uri 'self'</code></strong> y <strong><code>form-action 'self'</code></strong>: previenen ataques de manipulación del <code>&lt;base&gt;</code> y de redirección de submits.</li>
<li><strong><code>object-src 'none'</code></strong>: nadie usa <code>&lt;object&gt;</code> / <code>&lt;embed&gt;</code> legítimamente en 2026. Cerrarlo.</li>
<li><strong><code>frame-ancestors 'self'</code></strong>: reemplaza el viejo <code>X-Frame-Options: SAMEORIGIN</code>. Si tu sitio nunca se embebe, mejor <code>'none'</code>.</li>
<li><strong><code>img-src 'self' data:</code></strong>: el <code>data:</code> se necesita para imágenes inline base64 que algunos plugins usan. Los hosts adicionales son típicos de Gravatar y Google sign-in avatars.</li>
<li><strong><code>style-src 'self' 'unsafe-inline'</code></strong>: lamentablemente WordPress y la mayoría de los themes inyectan estilos inline. Por ahora <code>'unsafe-inline'</code> queda. Una versión más estricta usa nonces para <code>&lt;style&gt;</code> también, pero el ROI es bajo (XSS via CSS exfil existe pero es marginal).</li>
<li><strong><code>script-src 'self' 'nonce-XYZ'</code></strong> + hosts GTM/GA: la línea más sensible. Aquí es donde el modo Report-Only te dice qué más necesitas.</li>
<li><strong><code>connect-src</code></strong>: dónde puede hacer <code>fetch</code>/<code>XMLHttpRequest</code>/<code>WebSocket</code>. GA usa <code>region1.google-analytics.com</code> para EU/CL traffic — no olvidar.</li>
<li><strong><code>frame-src</code></strong>: si embebes YouTube, usar <code>youtube-nocookie.com</code> (privacidad-first). Para Vimeo: <code>https://player.vimeo.com</code>.</li>
<li><strong><code>upgrade-insecure-requests</code></strong>: si algún <code>src="http://..."</code> quedó hardcoded, el browser lo upgradea automáticamente. Útil mientras limpias el sitio.</li>
</ul>
<p><strong>Lo que no está</strong>: <code>'unsafe-inline'</code> en <code>script-src</code>. Esa es la línea que más cuesta defender ante un equipo de devs apurados, pero también la que da el 80% del valor del header.</p>
<h2 id="trampas-comunes-en-wordpress">Trampas comunes en WordPress</h2>
<h3 id="el-admin-se-cae-sin-unsafe-inline">El admin se cae sin <code>'unsafe-inline'</code></h3>
<p>Casi inevitable. La solución pragmática: aplicar dos políticas distintas según el path. En Apache:</p>
<div class="post-code"><pre><span></span><code><span class="nt">&lt;LocationMatch</span><span class="w"> </span><span class="s">&quot;^/wp-admin&quot;</span><span class="nt">&gt;</span>
<span class="w">    </span><span class="nb">Header</span><span class="w"> </span>always<span class="w"> </span>set<span class="w"> </span>Content-Security-Policy<span class="w"> </span><span class="s2">&quot;default-src &#39;self&#39;; script-src &#39;self&#39; &#39;unsafe-inline&#39; &#39;unsafe-eval&#39;; style-src &#39;self&#39; &#39;unsafe-inline&#39;; img-src &#39;self&#39; data: https:; font-src &#39;self&#39; data:; frame-ancestors &#39;self&#39;&quot;</span>
<span class="nt">&lt;/LocationMatch&gt;</span>

<span class="nt">&lt;LocationMatch</span><span class="w"> </span><span class="s">&quot;^/(?!wp-admin)&quot;</span><span class="nt">&gt;</span>
<span class="w">    </span><span class="nb">Header</span><span class="w"> </span>always<span class="w"> </span>set<span class="w"> </span>Content-Security-Policy<span class="w"> </span><span class="s2">&quot;[política estricta del frontend]&quot;</span>
<span class="nt">&lt;/LocationMatch&gt;</span>
</code></pre></div>

<p>El admin queda con una política más laxa, pero los visitantes anónimos (99% del tráfico) reciben la estricta. Es un trade-off aceptable mientras planificas migrar el admin a nonces.</p>
<h3 id="google-tag-manager-carga-otros-scripts">Google Tag Manager carga otros scripts</h3>
<p>GTM es un container que carga scripts dinámicamente (Analytics, Ads, Hotjar, Facebook Pixel, …). Cada tag agregada en GTM añade un origen al <code>script-src</code> y al <code>connect-src</code>. El patrón recomendado: cada vez que el equipo de marketing agrega una tag en GTM, revisar el report y agregar el host correspondiente. Si esto se vuelve un dolor crónico, vale la pena usar <code>'strict-dynamic'</code> con nonce en GTM (Google lo soporta).</p>
<h3 id="recaptcha-v3">reCAPTCHA v3</h3>
<p>Necesita: <code>script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/</code>, <code>frame-src https://www.google.com/recaptcha/</code>. Si lo olvidas, los forms con captcha quedan rotos sin error visible (el captcha simplemente no carga).</p>
<h3 id="embeds-youtube-vimeo-maps-soundcloud">Embeds (YouTube, Vimeo, Maps, SoundCloud)</h3>
<p>Cada uno agrega <code>frame-src</code> y muchas veces <code>script-src</code>/<code>connect-src</code>. La regla: <strong>prefiere el embed <code>nocookie</code> si existe</strong>. YouTube tiene <code>youtube-nocookie.com</code>, Vimeo respeta privacy mode. Reduce footprint de cookies y simplifica la política.</p>
<h3 id="stripe-webpay-khipu">Stripe / Webpay / Khipu</h3>
<p>Pasarelas chilenas: Webpay 3D Secure abre un iframe a <code>https://www.webpay.cl/</code>. Khipu API: <code>connect-src https://payment-api.khipu.com/</code>. Stripe Elements: <code>script-src https://js.stripe.com/</code>, <code>frame-src https://js.stripe.com/</code>. Documenta esto explícitamente en tus runbooks — son los que más cuesta diagnosticar.</p>
<h3 id="cloudflare-rocket-loader">Cloudflare Rocket Loader</h3>
<p>Si tienes Rocket Loader activado en Cloudflare, reescribe los <code>&lt;script&gt;</code> reales y los inyecta dinámicamente. Eso choca con CSP estricta. Dos opciones: deshabilitarlo (recomendado, su beneficio de performance es marginal con HTTP/2+Brotli activos) o marcar los scripts críticos con <code>data-cfasync="false"</code> para que no los toque.</p>
<h2 id="mantenimiento">Mantenimiento</h2>
<p>CSP no es &ldquo;ponla y olvídala&rdquo;. Cada nuevo plugin, cada feature nueva, cada tag de marketing puede romperla. La rutina mínima:</p>
<ul>
<li><strong>Revisión mensual</strong> del log de reports. Patrones nuevos → ajuste de política.</li>
<li><strong>Pre-deploy</strong>: si tocas plugins, themes o agregas un tag externo, deja la política nueva primero en <code>Report-Only</code>, valida 48h, luego enforce.</li>
<li><strong>Audit anual</strong> del header: ¿siguen siendo necesarios todos los hosts listados? Cada origen es superficie de ataque (si lo comprometen, sirven JS a tus visitantes).</li>
<li><strong>Alertas</strong>: agrega una métrica simple &ldquo;violaciones por hora&rdquo; en tu logging stack. Un spike de violaciones desde una IP particular es señal de un ataque XSS en curso siendo bloqueado.</li>
</ul>
<h2 id="como-lo-verificas">Cómo lo verificas</h2>
<p>El <a href="https://scan.asentic.cl/">Asentic FreeScan</a> revisa headers de seguridad incluido CSP. Detecta los tres patrones más comunes que vimos arriba: header ausente, header con <code>'unsafe-inline'</code> o <code>'unsafe-eval'</code>, y header sin <code>default-src</code> o sin <code>script-src</code> (que dejan el resto sin restricción). El reporte distingue entre &ldquo;no implementado&rdquo; y &ldquo;implementado pero ineficaz&rdquo; — porque el segundo caso es engañoso para auditores que sólo verifican presencia del header.</p>
<p>Para validación manual:</p>
<ul>
<li><strong><a href="https://csp-evaluator.withgoogle.com/">csp-evaluator.withgoogle.com</a></strong> — analiza tu política y marca debilidades específicas.</li>
<li><strong><code>curl -sI https://organizacion-ejemplo.cl/</code></strong> — confirma que el header está siendo enviado.</li>
<li><strong>DevTools del browser → Network → Response Headers</strong> — verifica la política en una request real (a veces hay middlewares que la sobreescriben).</li>
<li><strong>Console del browser</strong> — los bloqueos CSP aparecen como <code>Refused to execute inline script because it violates the following Content Security Policy directive…</code>.</li>
</ul>
<h2 id="cierre">Cierre</h2>
<p>CSP es una de esas herramientas que dan una ganancia de seguridad real con un costo de implementación que <strong>siempre subestimamos</strong>. Vale la pena pagarlo: cuando llegue el día en que un plugin se vea comprometido o un XSS pase la primera línea, el header es lo que decide entre un incidente menor y una filtración de datos.</p>
<p>Si tienes un WordPress en producción sin CSP, el primer paso es siempre el mismo: poner una política propuesta en <code>Report-Only</code>, dejarla correr una semana, y leer los reports. Todo lo demás sale de ahí.</p>]]></content>
    <category term="csp"/>
    <category term="wordpress"/>
    <category term="headers"/>
    <category term="xss"/>
    <category term="hardening"/>
  </entry>
</feed>