Security · 45 min

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:

Pré-requisitos

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:

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

  1. Gere um certificado confiável localmente com mkcert

    Instale o mkcert e 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 para localhost:

    mkcert -install
    mkdir certs && cd certs
    mkcert localhost 127.0.0.1 ::1
    

    Isso 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. O mkcert em 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 o mkcert emite 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 -dates
    

    Você deve ver uma linha subject mencionando localhost e uma
    janela de validade (notBefore/notAfter) cobrindo a data de hoje.

    Entregável deste passo: um diretório certs/ contendo um
    certificado e chave para localhost emitidos pelo mkcert,
    confirmado via openssl x509.

  2. 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.js
    

    Trilha 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.py
    

    Visite no navegador

    Abra https://localhost:8443 no 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 do mkcert
    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:8443
    

    O curl deve completar a requisição sem -k/--insecure — se
    você precisou de -k para funcionar, o certificado não está sendo
    confiado corretamente e você deve rodar mkcert -install de novo e
    confirmar que apontou o servidor para os arquivos .pem certos.

    Entregável deste passo: um servidor HTTPS rodando em
    https://localhost:8443, carregando no navegador com um cadeado
    válido e sem avisos, e um curl -I bem-sucedido sem --insecure.

  3. 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 para example.com está ali de propósito —
    você vai ver o Content-Security-Policy que 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. Use SAMEORIGIN em vez
      disso se você legitimamente enquadra suas próprias páginas em outro
      lugar da sua aplicação.
    • X-Content-Type-Options: nosniff — aceita o Content-Type que
      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 o default-src em si se puder evitar.
    • Referrer-Policy: strict-origin-when-cross-origin — envia a URL
      completa como Referer para 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:8443
    

    Você 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.

  4. 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:8443
    

    Você 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-origin
    

    Agora 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 grep deve 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 o s — é 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 com curl — é
    exatamente onde você vai olhar ao debugar headers num site real
    implantado depois, já que você nem sempre vai ter acesso curl a 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 no curl/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 de curl -sI mostrando os cinco headers.

  5. 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

    1. Abra https://localhost:8443 no seu navegador.
    2. Abra o DevTools → aba Console.
    3. 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.com ou
    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.com sem abrir tudo o resto:

    Content-Security-Policy: default-src 'self'; img-src 'self' https://example.com
    

    Atualize 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 de default-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-Security só 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 puro
    

    Critério de submissão

    Submeta quando tudo o que segue for verdade:

    1. Um servidor HTTPS (Node.js ou Python) rodando em
      https://localhost:8443 com um certificado emitido pelo mkcert
      e confiável pelo navegador (sem avisos, sem precisar de
      curl --insecure).
    2. Os cinco headers presentes com valores corretos, verificados pela
      tabela preenchida do Passo 4.
    3. Uma captura de tela ou log de Console copiado mostrando a violação
      de CSP sendo bloqueada para https://example.com/tracker.png sob a
      política estrita default-src 'self'.
    4. A política img-src afrouxada aplicada e reverificada, com uma
      nota curta explicando por que restringir a exceção a img-src é
      mais seguro que alargar o default-src.