Configurar bien las cabeceras de seguridad en nginx y Apache

Unas pocas líneas en la configuración del servidor protegen contra clickjacking, MIME sniffing y muchas consecuencias del XSS. Lo importante es que lleguen de verdad con cada respuesta.

Qué cabeceras tienen sentido

X-XSS-Protection ya no hace falta. Los navegadores modernos la ignoran y en los antiguos podía incluso perjudicar.

nginx

# /etc/nginx/snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;

# en el bloque server:
include snippets/security-headers.conf;
server_tokens off;

nginx tiene dos trampas:

Apache

# sudo a2enmod headers
<VirtualHost *:443>
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
    Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
    Header always unset X-Powered-By
</VirtualHost>

# global, p. ej. en security.conf
ServerTokens Prod
ServerSignature Off

En un alojamiento compartido sin acceso a la configuración del servidor, las mismas líneas Header suelen funcionar en el .htaccess de la raíz. La versión de PHP en la cabecera X-Powered-By se desactiva con expose_php = Off en php.ini.

Implantar la CSP sin romper la web

Una CSP demasiado estricta bloquea tus propios scripts, vídeos incrustados o herramientas de analítica. Así que por etapas:

  1. Primero como Content-Security-Policy-Report-Only. El navegador no bloquea nada, pero muestra las infracciones en la consola.
  2. Revisar la consola en las páginas importantes y añadir las fuentes necesarias, p. ej. script-src 'self' https://cdn.example.net.
  3. Mover los scripts en línea a archivos o darles un nonce. 'unsafe-inline' en script-src anula en gran parte la protección contra XSS.
  4. Solo entonces cambiar a Content-Security-Policy.

Cuidado con HSTS

HSTS no se puede retirar sin más: los navegadores lo recuerdan durante el tiempo indicado. Empieza con max-age=300, comprueba que de verdad todos los subdominios admiten HTTPS y sube luego a dos años. includeSubDomains afecta también a nombres internos como intranet.example.com si están bajo el mismo dominio.

Comprobar

curl -sI https://example.com
curl -sI https://example.com/no-existe   # ¿también en 404?

El comprobador de cabeceras de seguridad valora todas las cabeceras, detecta errores típicos como 'unsafe-inline' y genera las líneas adecuadas para tu servidor.

FAQ

¿Basta X-Frame-Options o necesito frame-ancestors?

frame-ancestors en la CSP es la forma moderna y admite varias fuentes. Para navegadores muy antiguos, añadir X-Frame-Options: SAMEORIGIN no hace daño.

¿Por qué veo las cabeceras duplicadas?

Normalmente las ponen tanto la aplicación (p. ej. PHP o un CMS) como el servidor web. Si hay dos CSP se aplican ambas y gana la más estricta. Mejor ponerlas en un solo sitio.

¿Necesitan las API cabeceras de seguridad?

Sí, al menos HSTS, nosniff y una CSP estricta como default-src 'none'; frame-ancestors 'none'. No cuestan nada e impiden que las respuestas se usen de forma indebida en el navegador.

¿Pone la CDN las cabeceras por mí?

Solo si lo configuras allí. Muchas CDN simplemente pasan las cabeceras del servidor de origen.