Blog / Segurança

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

· 3 min de leitura · Equipe N3XUS

Duas câmeras de segurança presas a uma parede cinza

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_header só são herdados do nível de cima se o nível atual não tiver nenhum add_header.
  • Um único add_header dentro de um location apaga todos os do server.
  • Correções: colocar os headers comuns num arquivo e dar include em cada bloco, ou usar add_header_inherit merge no nginx 1.29.3 ou mais novo.
  • Teste com curl -I em 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

Quer uma revisão dos headers e do nginx do seu ambiente? Fale com a gente.