Blog / Segurança
nginx: o add_header que apaga seus headers de segurança

Artigo
Você configura HSTS, CSP e X-Content-Type-Options no bloco server do nginx, testa a home e está tudo lá. Depois adiciona um Cache-Control para os arquivos estáticos e, sem perceber, esses arquivos passam a sair sem nenhum header de segurança.
Não é bug. É a regra de herança documentada do add_header, e ela pega muita gente.
Em resumo
- Os
add_headersó são herdados do nível de cima se o nível atual não tiver nenhumadd_header. - Um único
add_headerdentro de umlocationapaga todos os doserver. - Correções: colocar os headers comuns num arquivo e dar
includeem cada bloco, ou usaradd_header_inherit mergeno nginx 1.29.3 ou mais novo. - Teste com
curl -Iem cada tipo de resposta, e não só na home.
Como o problema aparece
Uma configuração comum para um site estático:
server {
add_header Strict-Transport-Security "max-age=31536000" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
A home responde com os três headers de segurança. Mas qualquer arquivo em /assets/ responde só com o Cache-Control. A documentação do nginx é explícita: os add_header do nível anterior valem apenas quando o nível atual não define nenhum.
Na prática, CSS, JavaScript e fontes ficam sem nosniff e sem CSP, justamente os arquivos em que o tipo de conteúdo mais importa.
Correção 1: um snippet incluído em todo bloco
É a solução que usamos, porque funciona em qualquer versão do nginx. Os headers comuns vão para um arquivo:
# /etc/nginx/snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
E cada bloco que define algum add_header também inclui o arquivo:
location /assets/ {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
O cuidado aqui é não repetir no bloco um header que já está no snippet. Fazendo isso, o mesmo header sai duas vezes com valores diferentes, e cada navegador decide de um jeito.
Correção 2: add_header_inherit (nginx 1.29.3 ou mais novo)
Versões recentes do nginx ganharam uma diretiva que muda esse comportamento:
location /assets/ {
add_header_inherit merge;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
Com merge, os headers do nível de cima são somados aos do bloco atual. Confira a sua versão com nginx -v antes de usar. A nossa imagem roda o 1.30.5, mas mantivemos o snippet por ser explícito e funcionar em qualquer ambiente.
Não esqueça o “always”
Sem o parâmetro always, o nginx só adiciona o header em respostas de sucesso e de redirecionamento. As páginas de erro, como a 404, saem sem ele. Em headers de segurança, use always sempre.
Como testar de verdade
Testar só a home não pega o problema. Verifique um endereço de cada tipo:
for u in / /assets/app.css /og.jpg /pagina-que-nao-existe; do
echo "== $u"
curl -sI "https://seu-site.com$u" | grep -iE 'strict-transport|content-security|x-content-type'
done
Se algum endereço voltar sem os três headers, há um location com add_header próprio sem o snippet. No nosso caso, a verificação passou a fazer parte da revisão de cada mudança no nginx.
Referências
- nginx: módulo ngx_http_headers_module (
add_header,alwayseadd_header_inherit) - MDN: Strict-Transport-Security
- OWASP: HTTP Security Response Headers Cheat Sheet
Quer uma revisão dos headers e do nginx do seu ambiente? Fale com a gente.