Configure HTTPS e Security Headers
Suba um certificado HTTPS local com o mkcert, sirva uma aplicação web pequena via TLS e adicione os cinco headers de resposta de segurança essenciais — testando cada um com curl e o DevTools do navegador até você conseguir provar que ele está de fato fazendo seu trabalho.
Problema
Dois dos controles de segurança mais baratos e de maior alavancagem que você pode adicionar a qualquer aplicação web são: (1) servi-la via HTTPS em vez de HTTP em texto puro, e (2) enviar um punhado de headers de resposta que dizem ao navegador como tratar seu conteúdo defensivamente. Nenhum dos dois exige tocar na sua lógica de negócio. Ambos são frequentemente pulados em desenvolvimento local — o que significa que desenvolvedores nunca veem de fato eles funcionando, e eles acabam sendo parafusados (ou esquecidos) de última hora antes de um release real. Sem HTTPS, toda requisição e resposta — incluindo cookies, tokens de sessão e envios de formulário — viaja em texto puro. Qualquer um na mesma rede (um Wi-Fi de cafeteria, um roteador comprometido, um proxy malicioso) pode ler ou adulterar isso. Sem security headers, um navegador vai tranquilamente deixar sua página ser colocada num frame pelo site de um atacante (clickjacking), deixar um upload malformado ser farejado e executado como script (confusão de MIME), ou carregar scripts controlados por atacante se qualquer parte da sua página refletir entrada não sanitizada (XSS) — proteções que o navegador está *disposto* a aplicar, mas só se você disser a ele para fazer isso. Neste lab você vai configurar um certificado HTTPS local confiável com o `mkcert`, servir uma aplicação mínima sobre ele, e empilhar os cinco headers que a própria checklist de segurança do método DARE classifica como obrigatórios em produção: `Strict-Transport-Security`, `X-Frame-Options`, `X-Content-Type-Options`, `Content-Security-Policy` e `Referrer-Policy`. Você não vai só adicioná-los — vai verificar que cada um está realmente fazendo efeito, usando `curl` e o DevTools do seu navegador, do mesmo jeito que você verificaria num deployment real. > Este lab tem como alvo apenas um servidor de desenvolvimento local > (`localhost` / `127.0.0.1`). Tudo que você configura aqui — o > certificado, os headers — é exatamente o que você levaria para um > deployment real, mas o teste acontece inteiramente na sua própria > máquina.
Objetivos
Ao final deste lab você será capaz de:
- Explicar o que é um certificado autoassinado vs. um confiável
localmente (mkcert), e por que navegadores rejeitam um mas não o
outro. - Gerar um certificado TLS confiável localmente e servir uma aplicação
web via HTTPS na sua própria máquina. - Explicar o que cada um dos cinco headers de segurança essenciais faz:
Strict-Transport-Security,X-Frame-Options,X-Content-Type-Options,
Content-Security-Policy,Referrer-Policy. - Adicionar os cinco headers a uma resposta HTTP real, usando um servidor
mínimo escrito à mão ou um middleware de framework. - Verificar que cada header está presente e fazendo seu trabalho usando
curl -Ie as abas Network e Console do DevTools do navegador —
incluindo observar uma violação de CSP sendo de fato bloqueada.
Pré-requisitos
- Node.js 18+ ou Python 3.9+ instalado (o lab mostra os dois; escolha
um). -
mkcertinstalado (brew install mkcertno macOS,choco install mkcert
no Windows, ou o equivalente para sua distro Linux — veja
https://github.com/FiloSottile/mkcert para instruções específicas de
plataforma). - Um navegador moderno (Chrome, Firefox ou Edge) com DevTools.
-
curldisponível no seu terminal (pré-instalado no macOS/Linux; vem com
o Windows moderno).
O que você vai construir
Uma aplicação web local minúscula servida via HTTPS com um certificado
confiável pelo navegador, respondendo a toda requisição com os cinco
headers de segurança essenciais — e uma checklist de comandos
curl/DevTools que prova que cada um funciona.
Por que HTTPS localmente, não só em produção
É tentador tratar HTTPS como "uma preocupação de deployment" e
desenvolver inteiramente sobre HTTP puro. Duas coisas quebram essa
suposição:
-
Algumas APIs do navegador exigem um "contexto seguro" — service
workers, a Clipboard API, geolocalização em alguns navegadores, entre
outras, simplesmente se recusam a funcionar sobrehttp://(exceto em
localhost, que ganha uma isenção restrita). Se sua aplicação de
produção usa qualquer uma dessas, desenvolver sobre HTTP puro esconde
bugs que você só vai encontrar depois de fazer deploy. -
HSTS e flags
Securede cookie se comportam diferente localmente vs.
em produção se você nunca os testar — e testá-los exige uma conexão
HTTPS real, autoassinada ou não.
O mkcert resolve a parte chata: navegadores rejeitam certificados
autoassinados puros com um aviso assustador, porque nada os endossa. O
mkcert cria uma autoridade certificadora local, instala o certificado
raiz dela na sua trust store do sistema/navegador, e então emite
certificados para localhost que seu navegador confia completamente —
sem avisos, sem cliques manuais de "prosseguir mesmo assim".
Os cinco headers e o que cada um garante
| Header | O que faz | O que impede |
|---|---|---|
Strict-Transport-Security |
Diz ao navegador "sempre use HTTPS para esta origem, por N segundos" | Ataques de downgrade — um usuário digitando http:// ou clicando num link antigo http:// é silenciosamente promovido antes de qualquer requisição sair do navegador |
X-Frame-Options |
Diz ao navegador se esta página pode ser embutida num <iframe>
|
Clickjacking — um atacante sobrepondo sua página invisivelmente para enganar usuários a clicarem nos seus botões |
X-Content-Type-Options |
Diz ao navegador para não adivinhar ("farejar") o tipo de conteúdo de uma resposta | Ataques de confusão de MIME — um arquivo enviado como .txt sendo farejado e executado como JavaScript |
Content-Security-Policy |
Diz ao navegador exatamente de quais origens scripts/estilos/imagens/etc. podem carregar | O raio de explosão do XSS — mesmo que um atacante injete uma tag <script>, o navegador se recusa a rodá-la se a origem dela não estiver na allowlist |
Referrer-Policy |
Diz ao navegador quanto da URL atual vazar no header Referer de requisições/links de saída |
Vazamento de dados sensíveis de path/query (identificadores de sessão, termos de busca, URLs internas) para sites de terceiros linkados pela sua página |
Repare no padrão: cada um destes é o navegador aplicando uma regra que
você entrega a ele. Você não está implementando lógica de segurança
você mesmo — está dizendo a um software já preocupado com segurança (o
navegador) qual das suas defesas embutidas ligar para a sua origem. É
isso que torna headers uma alavanca tão poderosa: algumas linhas de
configuração de servidor ativam proteções que de outra forma exigiriam
código client-side significativo.
Como trabalhar neste lab
Faça os cinco passos em ordem. Os Passos 1–2 te dão um servidor HTTPS
confiável; os Passos 3–4 adicionam e verificam os headers; o Passo 5 une
tudo e adiciona uma violação de CSP que você pode ver sendo bloqueada ao
vivo.
Passos
-
Gere um certificado confiável localmente com mkcert
Instale o
mkcerte configure sua autoridade certificadora local.
Este é um passo único por máquina.# macOS brew install mkcert brew install nss # só necessário se você usa Firefox # Windows (com Chocolatey) choco install mkcert # Linux — veja https://github.com/FiloSottile/mkcert#linux para sua distro, # ou baixe o binário pré-compilado na página de releases.Instale a CA local nas trust stores do seu sistema/navegador, depois
emita um certificado paralocalhost:mkcert -install mkdir certs && cd certs mkcert localhost 127.0.0.1 ::1Isso produz dois arquivos:
localhost+2.pem(o certificado) e
localhost+2-key.pem(a chave privada). Guarde os dois — você vai
apontar seu servidor para eles no Passo 2.Por que isso é diferente de um certificado autoassinado puro
Um certificado autoassinado puro (ex.: via
openssl req -x509 ...) é
criptograficamente válido, mas não é endossado por ninguém — seu
navegador não tem motivo para confiar nele, então mostra um aviso de
página inteira. Omkcertem vez disso cria sua própria CA raiz
privada e instala o certificado dessa CA na trust store do seu
SO/navegador. Todo certificado que omkcertemite depois disso é
confiado transitivamente, exatamente do jeito que um certificado real
emitido por uma CA de produção é confiado — só que restrito à sua
máquina apenas.Checkpoint
Confirme que os arquivos de certificado existem e são legíveis:
ls -la localhost+2.pem localhost+2-key.pem openssl x509 -in localhost+2.pem -noout -subject -datesVocê deve ver uma linha
subjectmencionandolocalhoste uma
janela de validade (notBefore/notAfter) cobrindo a data de hoje.Entregável deste passo: um diretório
certs/contendo um
certificado e chave paralocalhostemitidos pelomkcert,
confirmado viaopenssl x509. -
Sirva uma aplicação pequena via HTTPS
Escreva um servidor mínimo que termina TLS usando o certificado do
Passo 1. Escolha sua stack.Trilha Node.js
// server.js const https = require("https"); const fs = require("fs"); const options = { key: fs.readFileSync("certs/localhost+2-key.pem"), cert: fs.readFileSync("certs/localhost+2.pem"), }; const server = https.createServer(options, (req, res) => { res.writeHead(200, { "Content-Type": "text/html" }); res.end("<h1>Olá via HTTPS</h1>"); }); server.listen(8443, () => { console.log("Ouvindo em https://localhost:8443"); });node server.jsTrilha Python
# server.py import http.server import ssl class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(b"<h1>Ola via HTTPS</h1>") httpd = http.server.HTTPServer(("localhost", 8443), Handler) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.load_cert_chain( certfile="certs/localhost+2.pem", keyfile="certs/localhost+2-key.pem", ) httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) print("Ouvindo em https://localhost:8443") httpd.serve_forever()python server.pyVisite no navegador
Abra
https://localhost:8443no seu navegador. Você deve ver a
página carregar sem nenhum aviso de certificado e um ícone de
cadeado na barra de endereço — isso é a confiança local domkcert
funcionando exatamente como pretendido.Checkpoint
Confirme o handshake TLS e a cadeia de certificado pela linha de
comando:curl -v https://localhost:8443 2>&1 | grep -A2 "SSL certificate verify" curl -I https://localhost:8443O
curldeve completar a requisição sem-k/--insecure— se
você precisou de-kpara funcionar, o certificado não está sendo
confiado corretamente e você deve rodarmkcert -installde novo e
confirmar que apontou o servidor para os arquivos.pemcertos.Entregável deste passo: um servidor HTTPS rodando em
https://localhost:8443, carregando no navegador com um cadeado
válido e sem avisos, e umcurl -Ibem-sucedido sem--insecure. -
Adicione os cinco security headers
Adicione os cinco headers a toda resposta. Estenda o servidor do
Passo 2.Trilha Node.js
// server.js — handler atualizado const SECURITY_HEADERS = { "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "default-src 'self'", "Referrer-Policy": "strict-origin-when-cross-origin", }; const server = https.createServer(options, (req, res) => { for (const [name, value] of Object.entries(SECURITY_HEADERS)) { res.setHeader(name, value); } res.writeHead(200, { "Content-Type": "text/html" }); res.end(` <h1>Ola via HTTPS</h1> <img src="https://example.com/tracker.png" alt="imagem externa"> `); });Trilha Python
# server.py — handler atualizado SECURITY_HEADERS = { "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "default-src 'self'", "Referrer-Policy": "strict-origin-when-cross-origin", } class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/html") for name, value in SECURITY_HEADERS.items(): self.send_header(name, value) self.end_headers() self.wfile.write(b""" <h1>Ola via HTTPS</h1> <img src="https://example.com/tracker.png" alt="imagem externa"> """)A tag
<img>apontando paraexample.comestá ali de propósito —
você vai ver oContent-Security-Policyque acabou de configurar
bloqueá-la no Passo 5.O que cada valor significa, precisamente
-
Strict-Transport-Security: max-age=31536000; includeSubDomains
— armazena em cache "sempre use HTTPS" por um ano (31536000
segundos), e aplica também a todo subdomínio. Nota: navegadores só
respeitam esse header sobre uma conexão HTTPS — ele não tem
sentido enviado sobre HTTP puro, e é exatamente por isso que a
primeiríssima requisição HTTPS importa. -
X-Frame-Options: DENY— nunca permite esta página num frame,
em nenhum site, incluindo o seu próprio. UseSAMEORIGINem vez
disso se você legitimamente enquadra suas próprias páginas em outro
lugar da sua aplicação. -
X-Content-Type-Options: nosniff— aceita oContent-Typeque
enviamos como está; nunca adivinha um tipo diferente a partir dos
bytes. -
Content-Security-Policy: default-src 'self'— para todo tipo
de recurso não especificado de outra forma (scripts, estilos,
imagens, fontes, conexões), só permite carregar da mesma origem.
Esta é a política inicial mais estrita possível; aplicações reais
geralmente precisam afrouxar diretivas específicas (ex.:img-src
para um CDN), nunca alargar odefault-srcem si se puder evitar. -
Referrer-Policy: strict-origin-when-cross-origin— envia a URL
completa comoRefererpara requisições de mesma origem, mas só a
origem (sem path/query) para as de origem cruzada, e nada quando
houver downgrade de HTTPS para HTTP.
Reinicie seu servidor com este handler atualizado antes de continuar.
Checkpoint
Confirme que o servidor inicia sem erros e ainda responde:
curl -I https://localhost:8443Você deve ver os cinco nomes de header na resposta bruta, embora vá
verificar seus valores precisamente no Passo 4.Entregável deste passo: um servidor atualizado que envia os cinco
headers em toda resposta, confirmado rodando. -
-
Verifique cada header com curl
Adicionar um header e verificar que ele fez efeito são habilidades
diferentes. Rode cada checagem abaixo e confirme o valor exato, não
só a presença.curl -sI https://localhost:8443Você deve ver algo como:
HTTP/1.1 200 OK Content-Type: text/html Strict-Transport-Security: max-age=31536000; includeSubDomains X-Frame-Options: DENY X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'self' Referrer-Policy: strict-origin-when-cross-originAgora cheque cada um individualmente, o que te força a notar erros de
digitação ou capitalização errada que um olhar rápido deixaria
passar:curl -sI https://localhost:8443 | grep -i "strict-transport-security" curl -sI https://localhost:8443 | grep -i "x-frame-options" curl -sI https://localhost:8443 | grep -i "x-content-type-options" curl -sI https://localhost:8443 | grep -i "content-security-policy" curl -sI https://localhost:8443 | grep -i "referrer-policy"Cada
grepdeve imprimir exatamente uma linha com o valor que você
definiu no Passo 3. Se alguma linha estiver faltando, recheque seu
código de handler por um erro de digitação no nome do header (nomes
de header são case-insensitive no fio, mas um nome mal escrito como
X-Frame-Option— faltando os— é um no-op silencioso).Cheque cruzado no navegador
Abra
https://localhost:8443, abra o DevTools, vá na aba
Network, recarregue, clique na requisição do documento de topo, e
olhe Response Headers. Você deve ver os mesmos cinco headers.
Vale a pena fazer isso mesmo depois da checagem comcurl— é
exatamente onde você vai olhar ao debugar headers num site real
implantado depois, já que você nem sempre vai ter acessocurla uma
origem de produção do mesmo jeito.Uma pegadinha comum: reverse proxies e CDNs
Num deployment real, um reverse proxy ou CDN na frente da sua
aplicação pode remover ou sobrescrever headers que o código da
sua aplicação define. Se um header que você adicionou no código da
aplicação não aparece nocurl/DevTools contra a URL real e
publicamente acessível, a aplicação nem sempre é a culpada — cheque a
configuração do proxy/CDN também. A configuração deste lab não tem
proxy na frente, então o que você vê é exatamente o que seu código
enviou — útil para aprender os próprios headers antes de precisar
debugar através de uma camada de proxy.Checkpoint
Preencha esta tabela para o seu próprio servidor (todos os cinco
devem estar "presente" e "correto"):Header Presente? Valor bate com o Passo 3? Strict-Transport-Security X-Frame-Options X-Content-Type-Options Content-Security-Policy Referrer-Policy Entregável deste passo: a tabela preenchida acima, mais uma cópia
da saída bruta decurl -sImostrando os cinco headers. -
Veja o CSP bloquear uma violação real, depois submeta
A melhor prova de que um header funciona não é ler seu valor — é ver
o navegador de fato aplicá-lo. Você já plantou uma violação no Passo
3: a tag<img src="https://example.com/tracker.png">, que o seu
Content-Security-Policy: default-src 'self'deveria bloquear.Veja o bloqueio acontecer
- Abra
https://localhost:8443no seu navegador. - Abra o DevTools → aba Console.
- Recarregue a página.
Você deve ver uma mensagem de violação de CSP no console, parecida
com:Refused to load the image 'https://example.com/tracker.png' because it violates the following Content Security Policy directive: "default-src 'self'".Depois cheque a aba Network: a requisição para
example.comou
nunca aparece, ou aparece marcada como bloqueada — o navegador se
recusou até a tentar. Essa é a diferença prática entre CSP e "só
filtrar no servidor": CSP é aplicado do lado do cliente, antes da
requisição de rede acontecer, então ele impede vazamentos que o
código do lado do servidor não consegue ver (ex.: uma tag<img>
injetada por atacante exfiltrando dados via a URL para um domínio que
o servidor nunca toca).Tente afrouxar a política e reteste
Para ver a sintaxe de diretiva do CSP em ação, permita só imagens de
example.comsem abrir tudo o resto:Content-Security-Policy: default-src 'self'; img-src 'self' https://example.comAtualize o valor do header do seu servidor para isto, reinicie,
recarregue a página, e confirme no Console que a violação sumiu — a
requisição da imagem tem sucesso (ou falha só por motivos de rede,
não por CSP). Isso demonstra o padrão correto para aplicações reais:
comece dedefault-src 'self'e adicione exceções com escopo estreito
por diretiva, nunca um afrouxamento geral.Confirme também o comportamento do HSTS
Confirme que o header
Strict-Transport-Securitysó aparece sobre
HTTPS, nunca sobre HTTP puro (se você tiver uma variante HTTP puro do
seu servidor, ou puder comentar temporariamente o wrapping de TLS
para testar):# sobre HTTPS — header presente curl -sI https://localhost:8443 | grep -i strict-transport-security # uma requisição HTTP puro não tem nada para carregar HSTS — o header # seria sem sentido ali, e é por isso que navegadores o ignoram se um # servidor o envia por engano sobre HTTP puroCritério de submissão
Submeta quando tudo o que segue for verdade:
- Um servidor HTTPS (Node.js ou Python) rodando em
https://localhost:8443com um certificado emitido pelomkcert
e confiável pelo navegador (sem avisos, sem precisar de
curl --insecure). - Os cinco headers presentes com valores corretos, verificados pela
tabela preenchida do Passo 4. - Uma captura de tela ou log de Console copiado mostrando a violação
de CSP sendo bloqueada parahttps://example.com/tracker.pngsob a
política estritadefault-src 'self'. - A política
img-srcafrouxada aplicada e reverificada, com uma
nota curta explicando por que restringir a exceção aimg-srcé
mais seguro que alargar odefault-src.
- Abra