# Guía para agentes: implementar `/.well-known/security.txt`

Estás ayudando a un humano a publicar un archivo `security.txt` (RFC 9116) en su sitio web. Este archivo le dice a investigadores de seguridad **a quién y cómo reportar una vulnerabilidad**. Es parte del checklist de cumplimiento mínimo de https://jon.soy/factory/compliance (área "Cabeceras y seguridad básica").

Sigue esta guía en orden. No inventes datos de contacto.

## 1. Qué debes obtener del usuario antes de escribir nada

Pregunta (o busca en el repositorio: `README`, `SECURITY.md`, `package.json`, footer del sitio) y confirma con el usuario:

| Dato                      | Obligatorio | Ejemplo                                       |
| ------------------------- | ----------- | --------------------------------------------- |
| Dominio canónico (HTTPS)  | Sí          | `https://example.com`                         |
| Contacto de seguridad     | Sí          | `mailto:security@example.com`                 |
| Política de divulgación   | No          | `https://example.com/security-policy`         |
| Clave PGP pública         | No          | `https://example.com/.well-known/pgp-key.txt` |
| Página de agradecimientos | No          | `https://example.com/security/hall-of-fame`   |
| Idiomas preferidos        | No          | `es, en`                                      |
| Página de empleo          | No          | `https://example.com/careers`                 |

Reglas:

- **Nunca inventes** un correo, un teléfono ni una URL. Si el usuario no tiene un buzón de seguridad, sugiere crear un alias (`security@dominio`) y espera su confirmación.
- No uses un correo personal sin que el usuario lo autorice expresamente: el archivo es público y lo leen bots de spam.
- Si el dominio ya tiene un `security.txt`, léelo y actualízalo en vez de reemplazarlo a ciegas.

## 2. Formato del archivo (RFC 9116)

Es texto plano UTF-8, una directiva por línea con la forma `Campo: valor`. Las líneas que empiezan con `#` son comentarios. Los nombres de campo no distinguen mayúsculas, pero se escriben con la capitalización de abajo.

| Campo                 | Obligatorio | Repetible | Notas                                                                                            |
| --------------------- | ----------- | --------- | ------------------------------------------------------------------------------------------------ |
| `Contact`             | **Sí**      | Sí        | URI: `mailto:`, `https:` o `tel:`. Pon primero el preferido.                                     |
| `Expires`             | **Sí**      | **No**    | Fecha/hora ISO 8601 con zona (`2027-10-08T00:00:00.000Z`). Recomendado: menos de 1 año a futuro. |
| `Encryption`          | No          | Sí        | URL (https) a la clave pública, o `openpgp4fpr:<huella>`. No pongas la clave inline.             |
| `Acknowledgments`     | No          | Sí        | URL a la página de agradecimientos.                                                              |
| `Preferred-Languages` | No          | **No**    | Lista separada por comas de códigos de idioma: `es, en`.                                         |
| `Canonical`           | No\*        | Sí        | URL absoluta (https) donde vive **este** archivo. \*Obligatorio en la práctica si lo firmas.     |
| `Policy`              | No          | Sí        | URL a la política de divulgación de vulnerabilidades.                                            |
| `Hiring`              | No          | Sí        | URL a ofertas de trabajo de seguridad.                                                           |

Errores comunes que debes evitar:

- Olvidar `Expires` o `Contact` (el archivo queda inválido).
- Repetir `Expires` o `Preferred-Languages`.
- Poner un `Expires` en el pasado o a más de un año: los escáneres lo marcan como inválido o desactualizado.
- Usar `http://` en cualquier URL.
- Meter el correo sin el prefijo `mailto:`.

### Plantilla

```text
# Reporte de vulnerabilidades de example.com
# Gracias por ayudarnos a mantener seguro el sitio.

Contact: mailto:security@example.com
Expires: 2027-10-08T00:00:00.000Z
Preferred-Languages: es, en
Canonical: https://example.com/.well-known/security.txt
Policy: https://example.com/security-policy
```

Quita las líneas opcionales que el usuario no haya confirmado. Un archivo corto y correcto es mejor que uno largo con datos dudosos.

## 3. Dónde publicarlo

- Ruta obligatoria: **`/.well-known/security.txt`** en el dominio raíz (`https://example.com/.well-known/security.txt`).
- Opcional por compatibilidad: `/security.txt` redirigiendo (301) o sirviendo el mismo contenido.
- Debe servirse **solo por HTTPS**, con `Content-Type: text/plain; charset=utf-8` y código 200 (sin redirecciones intermedias hacia otra página HTML ni login).
- Si el sitio tiene subdominios que gestionan datos de usuarios, cada uno puede tener el suyo, o el raíz puede cubrirlos.
- Asegúrate de que la ruta **no** esté bloqueada por `robots.txt`, por un firewall o por una regla de "ocultar dotfiles" del servidor.

## 4. Cómo implementarlo según el stack

Detecta el stack mirando el repositorio y elige **una** variante.

### 4.1 Sitio estático (Astro, Next.js, Vite, Hugo, Eleventy, GitHub Pages, Cloudflare Pages/Workers assets)

Crea el archivo en la carpeta de archivos públicos:

- Astro / Vite / Next.js: `public/.well-known/security.txt`
- Hugo: `static/.well-known/security.txt`
- Eleventy / Jekyll: copia la carpeta `.well-known` a la salida (en Jekyll hay que incluirla en `include: [".well-known"]` porque ignora dotfiles).

Como `Expires` caduca, deja una nota para renovarlo (ver sección 6) o usa la variante dinámica.

### 4.2 Astro con endpoint dinámico (recomendado si ya hay adaptador/SSR)

`src/pages/.well-known/security.txt.ts`:

```ts
import type { APIRoute } from "astro";

// Contacto confirmado por el responsable del sitio.
const CONTACT = "mailto:security@example.com";

export const GET: APIRoute = ({ site }) => {
  // Siempre ~11 meses a futuro: el archivo nunca caduca mientras el sitio se despliegue.
  const expires = new Date();
  expires.setUTCMonth(expires.getUTCMonth() + 11);

  const base = site ?? new URL("https://example.com");
  const body = [
    "# Reporte de vulnerabilidades",
    `Contact: ${CONTACT}`,
    `Expires: ${expires.toISOString()}`,
    "Preferred-Languages: es, en",
    `Canonical: ${new URL("/.well-known/security.txt", base).href}`,
    "",
  ].join("\n");

  return new Response(body, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
};
```

Verifica que Astro no excluya la carpeta `.well-known` (las rutas con punto inicial funcionan en `src/pages/`; confirma con un build).

### 4.3 Next.js (App Router)

`app/.well-known/security.txt/route.ts` con un `GET` equivalente al de arriba y la cabecera `Content-Type: text/plain; charset=utf-8`.

### 4.4 Nginx

```nginx
location = /.well-known/security.txt {
    default_type text/plain;
    charset utf-8;
    alias /var/www/example/security.txt;
}
```

### 4.5 Apache

```apache
<Location "/.well-known/security.txt">
  ForceType "text/plain; charset=utf-8"
</Location>
```

### 4.6 Express / Node

```js
app.get("/.well-known/security.txt", (_req, res) => {
  res.type("text/plain; charset=utf-8").send(securityTxt);
});
```

### 4.7 WordPress

Sube el archivo a `<raíz>/.well-known/security.txt` (o usa un plugin que lo gestione) y verifica que ninguna regla de `.htaccess` lo reescriba hacia `index.php`.

## 5. Firma (opcional)

Si el usuario tiene clave PGP, puede firmar el archivo en formato _cleartext_ (`gpg --clearsign security.txt`). Entonces:

- Agrega `Canonical` con la URL exacta del archivo.
- Publica la clave pública y referénciala con `Encryption`.
- No edites el archivo después de firmarlo sin volver a firmarlo.

Si el usuario no usa PGP, **no lo firmes ni lo menciones como requisito**.

## 6. Mantenimiento

- Anota en el README o en `SECURITY.md` la fecha de `Expires` y quién la renueva.
- Con el endpoint dinámico (4.2) no hay renovación manual, pero conviene redeplegar al menos una vez al año.
- Al renovar, actualiza `Expires` y revisa que el `Contact` siga activo.
- Opcional: crea un recordatorio o una tarea en CI que falle si faltan menos de 30 días para `Expires`.

## 7. Verificación (obligatoria antes de dar la tarea por terminada)

1. En local: `curl -i http://localhost:<puerto>/.well-known/security.txt` → 200 y `content-type: text/plain; charset=utf-8`.
2. El archivo contiene `Contact` y un único `Expires` en el futuro (menos de 1 año).
3. Todas las URL son `https://` y los correos llevan `mailto:`.
4. En producción: `curl -sI https://<dominio>/.well-known/security.txt` → 200 (sin redirecciones a HTML).
5. Valida con una herramienta (por ejemplo https://securitytxt.org/ o un linter de RFC 9116).
6. Si hay `robots.txt`, comprueba que no bloquea `/.well-known/`.

## 8. Qué entregar al usuario

Resume en pocas líneas:

- Qué archivo(s) creaste o modificaste y su ruta.
- Qué contacto quedó publicado (para que lo confirme).
- La fecha de `Expires` y cómo se renueva.
- Cómo verificaste (comandos y resultado).
- Qué queda pendiente, si algo (por ejemplo, crear el alias de correo).

## Referencias

- RFC 9116: https://www.rfc-editor.org/rfc/rfc9116
- Generador: https://securitytxt.org/
- Checklist completo: https://jon.soy/factory/compliance
