cors: CORS correcto para NGINX, incluyendo preflight y Vary
Instalación
Puedes instalar este módulo en cualquier distribución basada en RHEL, incluyendo, entre otras:
- RedHat Enterprise Linux 7, 8, 9 y 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 y Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-cors
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-cors
Habilita el módulo añadiendo lo siguiente al principio de /etc/nginx/nginx.conf:
load_module modules/ngx_http_cors_module.so;
Este documento describe nginx-module-cors v1.0.0 publicado el 15 de agosto de 2026.
CORS para NGINX que gestiona las partes que la receta de add_header hace mal.
¿Por qué no usar simplemente add_header?
La receta map $http_origin + add_header Access-Control-Allow-* es la que
todo el mundo copia, y está rota de cuatro formas concretas:
add_headerse hereda o se reemplaza, por nivel. En el momento en que cualquierlocationañade una sola cabecera propia, todos losadd_headerdefinidos por encima — incluidas tus cabeceras CORS — desaparecen silenciosamente. Nada te avisa.- El preflight necesita un cortocircuito. Un preflight
OPTIONStiene que responderse con un204y las cabeceras correctas, sin llegar a tu aplicación. Hacerlo a mano implica un bloqueif, y eso interactúa mal contry_filesyproxy_pass. add_headersolo se dispara con una lista blanca de códigos de estado a menos que pasesalways. Así que tus respuestas de error pierden sus cabeceras CORS, y el navegador informa de un fallo CORS opaco en lugar del 404 o 502 que realmente ocurrió.Vary: Originse olvida. Cuando la respuesta depende del origen de la petición y no lo indicas, cualquier caché compartida o CDN delante de ti servirá alegremente la respuesta de un origen a otro. Esto es un bug de envenenamiento de caché, y es el más común en producción.
Este módulo hace las cuatro cosas correctamente, como un filtro de cabeceras
más un manejador en la fase preaccess, sin bloques if.
Sinopsis
location /api/ {
cors on;
cors_origin https://app.example.com https://*.staging.example.com;
cors_methods GET HEAD POST PUT DELETE;
cors_headers Authorization Content-Type;
cors_expose_headers X-Total-Count;
cors_credentials on;
cors_max_age 86400;
proxy_pass http://backend;
}
Un preflight entonces se ve así, y nunca llega a backend:
$ curl -i -X OPTIONS https://api.example.com/api/things \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: PUT'
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin, Access-Control-Request-Method
Directivas
cors
Sintaxis: cors on | off;
Por defecto: cors off;
Contexto: http, server, location, if in location
Habilita el procesamiento de CORS. Todas las demás directivas se heredan de
forma independiente, así que un location anidado que define una de ellas no
pierde el resto — que es justo la diferencia con add_header.
cors_origin
Sintaxis: cors_origin * | any | <spec> ...;
Por defecto: cors_origin *;
Contexto: http, server, location, if in location
Qué orígenes pueden leer el recurso. Dos palabras reservadas y tres formas de especificación:
| Valor | Significado |
|---|---|
* |
Emite un Access-Control-Allow-Origin: * literal. Lo mismo para todos, así que no se añade Vary: Origin. No se puede combinar con cors_credentials on. |
any |
Refleja el Origin que llegue. Seguro para credenciales, emite Vary: Origin. |
https://app.example.com |
Coincidencia exacta, sin distinguir mayúsculas. |
https://*.example.com |
Subdominio comodín. |
~^https://(a\|b)\.example\.com$ |
Expresión regular. ~* para no distinguir mayúsculas. |
Se pueden listar varias especificaciones. Cuando una coincide, ese origen se
devuelve tal cual. * y any no se pueden mezclar con otros valores.
Los comodines son deliberadamente estrictos: el * debe ir justo después de
:// y debe ir seguido de un .. https://*example.com se rechaza al
arrancar en lugar de coincidir silenciosamente con https://evilexample.com.
Un comodín coincide con cualquier número de etiquetas iniciales
(https://a.b.example.com coincide con https://*.example.com), pero no
coincide con el ápice desnudo y no ignora un puerto — un origen con puerto
necesita una entrada exacta o una expresión regular.
Origin: null — lo que envía un iframe en sandbox, un documento data: o una
página file:// — solo coincide con una entrada null explícita en la lista
cuando cors_credentials on. Ni any ni una expresión regular coincidirán con
él en ese caso. Reflejar null con credenciales entregaría a cualquier iframe
en sandbox de internet una lectura autenticada de la respuesta.
cors_methods
Sintaxis: cors_methods * | <method> ...;
Por defecto: cors_methods GET HEAD POST OPTIONS;
Contexto: http, server, location, if in location
El valor de Access-Control-Allow-Methods que se envía en un preflight. * no
se puede combinar con cors_credentials on.
cors_headers
Sintaxis: cors_headers * | any | <name> ...;
Por defecto: — (la cabecera se omite)
Contexto: http, server, location, if in location
El valor de Access-Control-Allow-Headers que se envía en un preflight. any
devuelve tal cual el Access-Control-Request-Headers de la petición; la
cabecera se omite cuando la petición no pidió ninguna. * no se puede combinar
con cors_credentials on.
cors_expose_headers
Sintaxis: cors_expose_headers <name> ...;
Por defecto: — (la cabecera se omite)
Contexto: http, server, location, if in location
Cabeceras de respuesta que el navegador debe hacer legibles para el script, más allá del conjunto de la lista segura. Se envían en respuestas reales, no en preflights.
cors_credentials
Sintaxis: cors_credentials on | off;
Por defecto: cors_credentials off;
Contexto: http, server, location, if in location
Emite Access-Control-Allow-Credentials: true, permitiendo cookies y
autenticación HTTP en peticiones cross-origin.
La especificación de CORS prohíbe emparejar credenciales con un comodín, y todos los navegadores lo aplican — así que una configuración que hace ambas cosas es una configuración cuyo CORS nunca funciona. NGINX se niega a arrancar en lugar de dejarte desplegarla:
cors_credentials on;
cors_origin *; # nginx: [emerg] ... cannot be combined with "cors_origin *"
Usa una lista explícita, o cors_origin any para reflejar. Ten en cuenta que
any más credenciales permite a cualquier sitio web leer respuestas
autenticadas de ese location; está permitido, y registra una advertencia al
arrancar.
cors_max_age
Sintaxis: cors_max_age <time>;
Por defecto: — (la cabecera se omite)
Contexto: http, server, location, if in location
Cuánto tiempo puede un navegador cachear el resultado del preflight.
cors_max_age 0; es un valor significativo y se emite; omitir la directiva
omite la cabecera.
cors_preflight
Sintaxis: cors_preflight on | off;
Por defecto: cors_preflight on;
Contexto: http, server, location, if in location
Si se deben responder internamente los preflights que coincidan. Desactívalo
cuando tu aplicación implemente OPTIONS por sí misma y solo quieras que se
añadan las cabeceras de respuesta.
Un preflight se cortocircuita solo cuando es genuinamente uno — un OPTIONS
que lleva tanto Origin como Access-Control-Request-Method — y el origen
coincide. Todo lo demás pasa sin tocarse, así que WebDAV y los OPTIONS a
nivel de aplicación siguen funcionando, y un preflight de un origen no
permitido simplemente no recibe Access-Control-Allow-Origin, que es lo que
hace que el navegador lo rechace.
Como el manejador se ejecuta en la fase preaccess, un preflight se responde
antes de que auth_basic, auth_request y deny tengan ocasión de rechazarlo.
Eso es deliberado: los navegadores nunca envían credenciales en un preflight,
así que cualquier autenticación delante de él rompería CORS por completo. Las
peticiones reales al mismo location sí se siguen autenticando con normalidad.
cors_vary
Sintaxis: cors_vary on | off;
Por defecto: cors_vary on;
Contexto: http, server, location, if in location
Si se debe emitir Vary: Origin cuando la respuesta depende del origen de la
petición.
Déjalo activado. Se emite siempre que la política dependa del origen —
incluido cuando el origen no coincidió, y cuando la petición no llevaba
ningún Origin, porque una copia cacheada de esa respuesta nunca debe
reproducirse ante una petición cuyo origen habría producido cabeceras
distintas. Deliberadamente no se emite para un cors_origin * estático, que
es igual para todos y no varía.
Desactívalo solo si sabes que no hay ninguna caché compartida delante de este
location, o si tu CDN ya indexa por Origin.
Notas
- Un
Varyque ya esté en la respuesta se amplía, nunca se reemplaza: un upstream que envíaVary: Accept-Languagesale comoVary: Accept-Language, Origin, y varias líneasVarydel upstream se pliegan en una. UnVary: *se deja tal cual, ya que según la RFC 9110 ya subsume todo. - Dos cosas añaden su
Varydespués de este módulo y por tanto llegan como una línea de campo separada en lugar de plegarse: unadd_header Vary ...en el mismo location, y elVary: Accept-Encodingdegzip_vary on. Varias líneas de campoVarysignifican exactamente lo mismo que una línea combinada (RFC 9110 §5.3) y todas las cachés lo manejan, así que esto es correcto — solo que no queda ordenado. Carganginx-module-compression-varysi quieres que todo se pliegue en una sola línea. - No definas también cabeceras CORS con
add_headeren el mismo location. Este módulo reemplaza cualquierAccess-Control-Allow-Originque encuentre — dos de ellas es un fallo grave en todos los navegadores — pero las demás cabeceras acabarían duplicadas. - Las cabeceras se emiten también en respuestas 304 y 206, así como en errores.
- Las subpeticiones se omiten, igual que el comportamiento de
add_header.