Pular para conteúdo

Instalação e Hardening do Nginx no Debian 13 (Trixie)

Este guia instala o Nginx a partir do repositório oficial do nginx.org, faz o hardening do SSH, configura o firewall com nftables e publica o site em HTTPS com certificado emitido por uma CA própria, sem depender de Cloudflare nem de domínio público.

Pré-requisitos:

  • Debian 13 instalado, com acesso à internet
  • Senha do root ou um usuário que já tenha permissão de sudo (o Passo 0 configura isso)
  • Um computador cliente (Linux ou Windows) na mesma rede para acessar o servidor

Valores usados neste guia. Troque pelos do seu ambiente sempre que aparecerem nos comandos:

Item Valor no guia
IP do servidor 192.168.1.10
Nome do servidor nginx.example.internal
Usuário seu_usuario
Porta do SSH 2222

O sufixo .internal é reservado pela ICANN para uso em redes privadas e nunca vai existir na internet, então não há risco de conflito com um domínio real.


O que é o Nginx

O Nginx é um servidor web: ele recebe requisições HTTP e HTTPS e devolve o conteúdo pedido. Esse conteúdo pode ser um arquivo do disco (HTML, imagem, CSS) ou a resposta de outra aplicação que está rodando atrás dele. Nesse segundo caso, o Nginx atua como proxy reverso.

Na prática, você vai usar o Nginx para duas coisas:

  • Hospedar sites, um ou vários no mesmo servidor
  • Publicar a interface web de outras aplicações, como o front-end do Zabbix, GLPI ou qualquer sistema que rode em uma porta interna

Como uma requisição chega até o Nginx

  1. O usuário digita exemplo.com.br no navegador. O DNS traduz esse nome para o endereço IP do servidor.
  2. O navegador abre uma conexão TCP com esse IP, na porta 80 (HTTP) ou 443 (HTTPS). É aqui que acontece o three-way handshake do TCP: SYN, SYN-ACK, ACK. Sem ele não existe conexão, e nenhum dado do site trafega.
  3. Se for HTTPS, acontece o handshake TLS, que negocia a criptografia antes de qualquer conteúdo ser enviado. Nessa etapa o navegador informa o nome do site que quer acessar (o SNI) e confere o certificado do servidor.
  4. O navegador envia a requisição HTTP. Dentro dela vai o cabeçalho Host: exemplo.com.br.
  5. O Nginx lê o nome pedido e decide qual site (bloco server) responde. Depois devolve o arquivo do disco ou repassa a requisição para a aplicação de trás.

O passo 5 é o que permite hospedar vários sites em um único IP. Funciona como a recepção de um prédio comercial: o endereço é o mesmo para todas as empresas, e a recepção pergunta para qual sala você vai antes de liberar a entrada.

As portas 80 e 443 são as padrão, mas o Nginx pode escutar em qualquer porta que você definir na configuração.


Parte 1: Instalação

Passo 0: Instalar o sudo e dar permissão ao seu usuário

O sudo permite que um usuário comum execute comandos administrativos, um de cada vez, sem precisar logar como root. A senha pedida é a do próprio usuário, e cada comando executado fica registrado no log do sistema.

No Debian, isso depende de uma escolha feita na instalação. Se você definiu uma senha para o root, o sudo não vem instalado e o seu usuário não tem permissão administrativa. Se deixou a senha do root em branco, o instalador já fez tudo isso e você pode pular para o Passo 1.

0.1 Verificar se este passo é necessário

Logado com o seu usuário:

groups

Se sudo aparecer na lista, pule para o Passo 1. Se não aparecer, continue.

0.2 Entrar como root

su -

Digite a senha do root, definida na instalação. O hífen faz diferença: su - carrega o ambiente completo do root, incluindo a pasta /usr/sbin no caminho de busca de comandos. Com su sem hífen, comandos administrativos como o usermod retornam command not found.

Se você está acessando por SSH, entre com o seu usuário e rode o su - dentro da sessão. O SSH do Debian já vem bloqueando login do root com senha.

0.3 Instalar o sudo

apt update
apt install -y sudo

Os comandos vão sem sudo na frente porque você já está como root.

0.4 Adicionar o seu usuário ao grupo sudo

usermod -aG sudo seu_usuario
  • -G sudo: coloca o usuário no grupo sudo. O Debian já traz no arquivo /etc/sudoers a regra que libera esse grupo para executar qualquer comando como root, então não é preciso editar o sudoers.
  • -a: adiciona o grupo sem remover os que o usuário já tem. Sem o -a, o usuário sai de todos os outros grupos e fica só no sudo.

0.5 Sair e entrar de novo

Os grupos de um usuário são lidos no momento do login. A sessão que já estava aberta não enxerga o grupo novo.

exit

Esse primeiro exit sai do root. Depois encerre a sessão do seu usuário (exit de novo, ou feche a conexão SSH) e faça login outra vez.

0.6 Testar

groups
sudo whoami

Saída esperada:

seu_usuario cdrom floppy sudo audio dip video plugdev users netdev
root

A lista de grupos varia de instalação para instalação. O que importa é sudo estar nela e o sudo whoami responder root depois de pedir a sua senha.

É este usuário que vai administrar o servidor daqui em diante. É por isso que o Passo 10 consegue bloquear o login do root pelo SSH sem você perder o acesso.

Passo 1: Instalar os pré-requisitos

sudo apt update
sudo apt install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring
  • curl: baixa a chave de assinatura do Nginx
  • gnupg2: converte e verifica a chave
  • ca-certificates: permite validar o certificado HTTPS do nginx.org
  • lsb-release: fornece o comando que identifica a versão do Debian (trixie)

Passo 2: Importar a chave de assinatura do Nginx

Todo pacote do repositório oficial é assinado digitalmente. A chave importada aqui é o que permite ao apt confirmar que o pacote veio mesmo do nginx.org e não foi alterado no caminho.

curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
    | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

O gpg --dearmor converte a chave do formato texto para o formato binário que o apt lê. A flag -f faz o curl falhar se o download der erro, em vez de gravar uma página de erro no lugar da chave.

Passo 3: Conferir a impressão digital da chave

Em uma instalação nova, o seu usuário ainda não tem a pasta de configuração do GnuPG, e o gpg encerra com Fatal: /home/seu_usuario/.gnupg: directory does not exist!. Crie a pasta antes:

mkdir -p -m 700 ~/.gnupg

O -m 700 deixa a pasta acessível só para o seu usuário. Com permissão mais aberta, o gpg reclama que a pasta é insegura.

gpg --dry-run --quiet --no-keyring --import --import-options import-show \
    /usr/share/keyrings/nginx-archive-keyring.gpg

A saída precisa conter a impressão digital 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62:

pub   rsa4096 2024-05-29 [SC]
      8540A6F18833A80E9C1653A42FD21310B49F6B46
uid                      nginx signing key <signing-key-2@nginx.com>

pub   rsa2048 2011-08-19 [SC] [expires: 2027-05-24]
      573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
uid                      nginx signing key <signing-key@nginx.com>

pub   rsa4096 2024-05-29 [SC]
      9E9BE90EACBCDE69FE9B204CBCDCD8A38D88A2B3
uid                      nginx signing key <signing-key-3@nginx.com>

As outras duas chaves (signing-key-2 e signing-key-3) também são do nginx.org e assinam outros pacotes. Isso é normal. Se a impressão digital apareceu, siga direto para o Passo 4.

Só se a impressão digital 573BFD...BF62 NÃO aparecer: a chave baixada não é a oficial. Apague o arquivo com sudo rm /usr/share/keyrings/nginx-archive-keyring.gpg e não continue. Se a chave estiver correta, não rode esse comando: sem o arquivo, o apt recusa o repositório do nginx.org e instala o pacote do Debian no lugar.

Passo 4: Adicionar o repositório estável do Nginx

echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
https://nginx.org/packages/debian $(lsb_release -cs) nginx" \
    | sudo tee /etc/apt/sources.list.d/nginx.list
  • signed-by=: amarra esse repositório à chave do Passo 2. Pacote assinado com outra chave é recusado.
  • $(lsb_release -cs): retorna trixie, o codinome do Debian 13.

Passo 5: Dar prioridade ao repositório do nginx.org

O Debian 13 também tem um pacote nginx no repositório dele, em versão mais antiga. Sem esta regra, o apt pode instalar o pacote do Debian em vez do oficial.

echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
    | sudo tee /etc/apt/preferences.d/99nginx

A prioridade 900 é maior que a padrão dos repositórios do Debian (500), então o apt sempre escolhe o pacote do nginx.org.

Passo 6: Instalar o Nginx

sudo apt update && sudo apt install -y nginx

O && só executa o apt install se o apt update terminar sem erro. Se o repositório do nginx.org falhar na verificação da chave, a instalação para ali, em vez de seguir e instalar o pacote do Debian.

Confirme que o pacote instalado veio do nginx.org:

apt policy nginx

A linha marcada com *** precisa apontar para https://nginx.org/packages/debian trixie/nginx. Se apontar para deb.debian.org, revise os Passos 4 e 5.

Passo 7: Iniciar o serviço e habilitar no boot

sudo systemctl enable --now nginx

O enable faz o Nginx subir sozinho quando o servidor reinicia. O --now inicia o serviço agora. Se ele já estiver rodando, o comando não altera nada.

O comando pode mostrar a mensagem Synchronizing state of nginx.service with SysV service script. Ela não é erro: o pacote do nginx.org traz, além da unidade do systemd, um script de inicialização no formato antigo (SysV, em /etc/init.d/nginx), e o systemd só deixa os dois sincronizados. Quem controla o serviço continua sendo o systemd.

Verifique:

systemctl is-active nginx
systemctl is-enabled nginx

Saída esperada:

active
enabled

Passo 8: Testar localmente

curl -I http://localhost

Saída esperada:

HTTP/1.1 200 OK
Server: nginx/1.x.x
Content-Type: text/html

O número da versão muda conforme a release estável atual do nginx.org. Se aparecer nginx/1.26.3, o pacote instalado é o do Debian, e não o oficial: volte ao Passo 5.


Parte 2: Hardening do SSH

O SSH é a porta de entrada administrativa do servidor. Do jeito que vem instalado, ele aceita login por senha na porta 22, e essa combinação é exatamente o que os robôs de força bruta procuram. Nesta parte você troca a senha por chave criptográfica, bloqueia o login direto do root e muda a porta.

A ordem dos passos importa: primeiro a chave funciona, depois a senha é desligada. Inverter isso é a forma mais comum de perder o acesso ao servidor.

Passo 9: Criar e instalar a chave SSH

9.1 Gerar a chave no seu computador (cliente)

Rode no seu computador, não no servidor.

Linux:

ssh-keygen -t ed25519 -C "$USER@$(hostname)"

Windows (PowerShell):

ssh-keygen -t ed25519 -C "$env:USERNAME@$env:COMPUTERNAME"
  • -t ed25519: algoritmo de chave moderno, curto e mais rápido que RSA, com segurança equivalente ou maior
  • -C: um comentário gravado no final da chave. Não tem papel nenhum no login, serve só para identificar de qual computador a chave veio. O comando já preenche sozinho com o seu usuário e o nome do seu computador, por exemplo usuario@notebook.

Por que o comentário identifica o computador de origem, e não o servidor: no servidor, todas as chaves autorizadas ficam juntas no arquivo ~/.ssh/authorized_keys, uma por linha. Quando um notebook for perdido ou um acesso precisar ser revogado, é pelo comentário que você descobre qual linha apagar.

O comando faz três perguntas:

  1. Onde salvar a chave. Aperte Enter para aceitar o padrão (~/.ssh/id_ed25519). Se digitar um nome, a chave é salva na pasta em que você está, e não em ~/.ssh. Veja o aviso do 9.2.
  2. Passphrase. Uma senha que protege a chave caso alguém copie o arquivo do seu computador. É local: não tem relação com a senha do servidor.
  3. Confirmação da passphrase.

O comando cria dois arquivos: id_ed25519 (chave privada, nunca sai do seu computador) e id_ed25519.pub (chave pública, vai para o servidor).

9.2 Copiar a chave pública para o servidor

Neste momento o SSH ainda está na porta 22 e aceita senha. No Linux:

ssh-copy-id seu_usuario@192.168.1.10

No Windows (PowerShell), que não tem ssh-copy-id:

type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh seu_usuario@192.168.1.10 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Os dois fazem a mesma coisa: adicionam a chave pública no arquivo ~/.ssh/authorized_keys do seu usuário no servidor, com as permissões que o SSH exige. Se a pasta ou o arquivo tiverem permissão aberta demais, o SSH ignora a chave.

Erros comuns aqui:

  • Rodar ssh-copy-id com sudo. O sudo faz o comando procurar as chaves do root, não as suas, e o resultado é ERROR: No identities found. A chave é do seu usuário, então o comando roda sem sudo.
  • Dar outro nome à chave no ssh-keygen. Se você digitou um nome em vez de aceitar o caminho padrão, a chave fica salva na pasta onde você estava, não em ~/.ssh. Mova a chave para lá e informe o arquivo com -i:
mv ~/nome-da-chave ~/nome-da-chave.pub ~/.ssh/
ssh-copy-id -i ~/.ssh/nome-da-chave.pub seu_usuario@192.168.1.10
  • Connection reset by peer depois de algumas tentativas. O OpenSSH do Debian 13 pune temporariamente o IP que acumula falhas de autenticação (recurso PerSourcePenalties) e passa a derrubar as conexões novas dele já na abertura. Isso acontece muito quando o seu computador tem várias chaves carregadas e oferece todas ao servidor. Espere alguns minutos ou reinicie o SSH pelo console do servidor (sudo systemctl restart ssh) e tente de novo com -o IdentitiesOnly=yes.

9.3 Testar o login com a chave

ssh seu_usuario@192.168.1.10

O login tem que entrar sem pedir a senha do servidor. Se pedir a passphrase da chave, está certo: ela é local, do seu computador. Se ainda pedir a senha do usuário, a chave não foi instalada. Resolva isso antes de seguir.

9.4 (Recomendado) Criar um atalho no ~/.ssh/config

No seu computador, crie uma entrada para o servidor:

cat >> ~/.ssh/config <<'EOF'

Host nginx-lab
    HostName 192.168.1.10
    User seu_usuario
    Port 22
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config

A partir daqui, ssh nginx-lab substitui o comando inteiro. Quando a porta mudar no Passo 10, basta trocar a linha Port 22 por Port 2222. Se você deu outro nome à chave, ajuste o IdentityFile.

O IdentitiesOnly yes é o que importa aqui. Sem ele, o cliente SSH oferece ao servidor todas as chaves que tem carregadas, uma por vez. Depois do hardening, o servidor aceita só 3 tentativas (MaxAuthTries 3), e quem tem várias chaves no computador é recusado com Too many authentication failures antes de chegar na chave certa.

Passo 10: Aplicar a configuração de hardening

10.1 Criar o arquivo de configuração

No Debian 13, o arquivo principal /etc/ssh/sshd_config carrega primeiro tudo que está em /etc/ssh/sshd_config.d/. No SSH, o primeiro valor lido vence. Por isso a configuração vai em um arquivo separado com nome começando em 00: ele é lido antes de qualquer outro e tem prioridade, inclusive sobre arquivos que algumas imagens de cloud colocam ali liberando senha.

Rode no servidor, logado com o usuário que vai administrar o servidor, trocando 2222 pela porta que você escolheu.

O AllowUsers não precisa de edição: o $USER é substituído automaticamente pelo nome do usuário logado no momento em que o comando roda. Isso evita o erro mais comum deste passo, que é deixar um nome de exemplo nessa linha e ter o próprio login recusado, mesmo com a chave certa. O teste do 10.2 mostra o nome que foi gravado.

sudo tee /etc/ssh/sshd_config.d/00-hardening.conf > /dev/null <<EOF
# Porta: 2222 ou a porta que você escolheu
Port 2222

# Root não faz login direto. Entre com seu usuário e use sudo
PermitRootLogin no

# Só chave. Senha desligada
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
AuthenticationMethods publickey

# Só estes usuários podem entrar por SSH
AllowUsers $USER

# Limita tentativas e tempo para concluir o login
MaxAuthTries 3
LoginGraceTime 30
MaxStartups 10:30:60

# Encerra sessões abandonadas após cerca de 10 minutos sem resposta
ClientAliveInterval 300
ClientAliveCountMax 2

# Recursos que um servidor web não precisa
X11Forwarding no
AllowAgentForwarding no
EOF

O porquê dos itens que mais confundem:

  • PermitRootLogin no: todo robô tenta root, porque é o único usuário que existe em qualquer Linux. Bloqueando o root, o atacante precisa adivinhar também o nome do usuário.
  • AuthenticationMethods publickey: garante que nenhum outro método de autenticação seja aceito, mesmo que outro arquivo tente reativar.
  • AllowUsers: lista branca. Qualquer usuário que não estiver aqui é recusado, mesmo com chave válida.
  • MaxStartups 10:30:60: quando há mais de 10 conexões sem login concluído, o SSH começa a recusar novas aleatoriamente (30% de chance), chegando a 100% com 60. Freia ataque em massa sem derrubar o administrador.
  • Port 2222: não é segurança de verdade, porque um scanner encontra a porta. Mas tira o servidor da mira dos robôs que só testam a 22 e limpa o log de lixo, o que facilita enxergar uma tentativa real.

10.2 Validar antes de aplicar

sudo sshd -t

Sem saída significa que a configuração está correta. Confira os valores que o SSH vai usar de fato:

sudo sshd -T | grep -Ei '^(port|permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowusers|authenticationmethods) '

Saída esperada:

port 2222
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
authenticationmethods publickey
allowusers seu_usuario

Se algum valor estiver diferente, outro arquivo em /etc/ssh/sshd_config.d/ está sendo lido antes. Liste a pasta com ls /etc/ssh/sshd_config.d/ e confira.

10.3 Aplicar

Não feche a sessão atual. Ela continua aberta depois do restart e é o seu caminho de volta se algo der errado.

if systemctl is-enabled --quiet ssh.socket 2>/dev/null; then
    sudo systemctl daemon-reload && sudo systemctl restart ssh.socket
else
    sudo systemctl restart ssh
fi

O SSH pode rodar de dois jeitos no systemd: como serviço (ssh.service) ou ativado por socket (ssh.socket). No modo socket, quem escuta a porta é o systemd, e ele só lê a porta nova depois do daemon-reload. O bloco acima detecta qual modo está ativo e faz o certo para cada um.

10.4 Testar em um terminal novo

No seu computador, abra outro terminal:

ssh -p 2222 seu_usuario@192.168.1.10

Tem que entrar só com a chave. Agora confirme que senha foi recusada:

ssh -p 2222 -o PubkeyAuthentication=no seu_usuario@192.168.1.10

Resultado esperado:

seu_usuario@192.168.1.10: Permission denied (publickey).

O (publickey) no final é o servidor dizendo que só aceita chave. Com os dois testes certos, a sessão antiga pode ser fechada.

Se o login com a chave der Permission denied (publickey), olhe o log pela sessão que ficou aberta:

sudo journalctl -u ssh -n 20 --no-pager | grep -Ei 'not allowed|invalid|failed'

A causa mais comum é esta linha:

User seu_usuario from 192.168.1.84 not allowed because not listed in AllowUsers

Ela significa que o AllowUsers ficou com um nome diferente do seu usuário, normalmente o seu_usuario do exemplo copiado sem troca. Nesse caso o servidor recusa o login mesmo com a chave certa, e ainda registra você como invalid user: para quem está de fora, o SSH não revela se o usuário existe ou não. Corrija o nome e reinicie:

sudo sed -i "s/^AllowUsers .*/AllowUsers $USER/" /etc/ssh/sshd_config.d/00-hardening.conf
sudo sshd -t && sudo systemctl restart ssh

Se o login novo continuar falhando, use a sessão que ficou aberta para remover o arquivo e voltar ao estado anterior:

sudo rm /etc/ssh/sshd_config.d/00-hardening.conf
sudo systemctl restart ssh

Parte 3: Firewall

Passo 11: Configurar o firewall com nftables

Até aqui o servidor aceita conexão em qualquer porta. Neste passo você define uma política de bloquear tudo e libera só o que o servidor precisa: SSH e Nginx.

Atenção: uma regra errada aqui bloqueia o seu próximo acesso SSH. A sessão atual continua aberta (a regra ct state established garante isso), mas não feche o terminal até testar uma nova conexão em outra janela.

11.1 Instalar e habilitar o nftables

sudo apt install -y nftables
sudo systemctl enable nftables

O serviço nftables carrega o arquivo /etc/nftables.conf a cada boot. Regras criadas só com nft add rule ficam apenas na memória e somem no reboot, por isso as regras vão no arquivo.

11.2 Fazer backup do arquivo original

sudo cp /etc/nftables.conf /etc/nftables.conf.bak

11.3 Escrever as regras

Antes de colar, ajuste a porta do SSH. O exemplo usa 2222, mas o valor certo é a porta que você definiu no Passo 10. Para confirmar em qual porta o SSH está escutando:

sudo ss -tlnp | grep sshd
sudo tee /etc/nftables.conf > /dev/null <<'EOF'
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;

        # Respostas de conexões que o próprio servidor abriu ou já aceitou
        ct state established,related accept
        ct state invalid drop

        # Tráfego interno da própria máquina
        iif "lo" accept

        # Ping e ICMPv6 (o IPv6 não funciona sem ICMPv6)
        meta l4proto { icmp, ipv6-icmp } accept

        # SSH: 2222 ou a porta que você escolheu
        tcp dport 2222 accept

        # Nginx: HTTP e HTTPS liberados para qualquer origem
        tcp dport { 80, 443 } accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}
EOF
  • flush ruleset: limpa as regras atuais antes de carregar as novas. Sem isso, as regras seriam somadas às antigas a cada recarga.
  • policy drop: tudo que não bater em nenhuma regra accept é descartado.
  • ct state established,related accept: aceita o retorno de conexões já existentes. É essa linha que mantém sua sessão SSH viva durante a troca de regras.

11.4 (Opcional) Liberar o Nginx só para uma rede específica

Para um front-end interno, como o Zabbix, o Nginx não precisa ficar aberto para a internet. Troque a linha do Nginx no arquivo:

        tcp dport { 80, 443 } accept

por esta, ajustando o IP ou a rede:

        ip saddr { 192.168.1.50, 10.0.0.0/24 } tcp dport { 80, 443 } accept

Use uma das duas linhas, nunca as duas. Se a linha aberta para todos continuar no arquivo, a restrição por IP não tem efeito. A mesma lógica vale para o SSH: ip saddr { 192.168.1.0/24 } tcp dport 2222 accept restringe o acesso administrativo à sua rede.

11.5 Validar e aplicar

Valide a sintaxe sem aplicar nada:

sudo nft -c -f /etc/nftables.conf

Sem saída significa que o arquivo está correto. Aplique:

sudo nft -f /etc/nftables.conf

Confira as regras carregadas:

sudo nft list ruleset

Agora abra outro terminal e teste uma nova conexão SSH (ssh -p 2222 seu_usuario@192.168.1.10). Só feche a sessão original depois que a nova funcionar.

Se algo der errado, restaure o backup pela sessão que continua aberta:

sudo cp /etc/nftables.conf.bak /etc/nftables.conf
sudo nft -f /etc/nftables.conf

Passo 12: Testar o Nginx a partir de outra máquina

Do seu computador, abra o navegador em http://192.168.1.10 ou use o curl:

curl -I http://192.168.1.10

A resposta tem que ser HTTP/1.1 200 OK. Se funcionou no Passo 8 e falha aqui, o bloqueio está no firewall: revise o Passo 11.


Parte 4: HTTPS com certificado local

Para o navegador aceitar uma conexão HTTPS sem aviso, o certificado do site precisa ter sido assinado por uma Autoridade Certificadora (CA) em que o navegador confia. Na internet, essa CA é uma entidade pública, como a Let's Encrypt. Em rede interna, você cria a sua própria CA, emite o certificado do servidor com ela e instala a CA nas máquinas que vão acessar.

O resultado é HTTPS de verdade, com cadeado e sem aviso, sem Cloudflare, sem domínio registrado e sem expor nada para a internet. É o modelo usado em front-ends internos como Zabbix, GLPI e painéis de equipamento.

Para um site público, acessado por qualquer pessoa na internet, o caminho é um certificado da Let's Encrypt. CA própria só funciona em máquinas onde você instalou essa CA.

Passo 13: Criar a CA local

Todos os arquivos ficam em /etc/nginx/ssl:

sudo mkdir -p /etc/nginx/ssl
cd /etc/nginx/ssl

Gere a chave privada da CA:

sudo openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key

Crie o arquivo de extensões. São essas extensões que dizem ao navegador que este certificado é uma CA e pode assinar outros:

sudo tee ca.ext > /dev/null <<'EOF'
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
EOF
  • CA:TRUE: marca o certificado como autoridade certificadora
  • pathlen:0: esta CA assina certificados de servidor, mas não pode criar outra CA abaixo dela
  • keyCertSign, cRLSign: o único uso permitido da chave é assinar certificados

Gere o certificado da CA, válido por 10 anos:

sudo openssl req -new -key ca.key -subj "/C=BR/O=Example Lab/CN=Example Lab CA" -out ca.csr
sudo openssl x509 -req -in ca.csr -signkey ca.key -days 3650 -sha256 -extfile ca.ext -out ca.crt

O primeiro comando cria o pedido de certificado (.csr) com o nome da CA. O segundo assina esse pedido com a própria chave da CA, por isso ela é chamada de autoassinada: é a raiz da cadeia.

A ca.key é a peça mais sensível de todo o processo: quem tem esse arquivo emite certificados que suas máquinas vão aceitar. Em laboratório ela fica no servidor. Em produção, a CA fica em uma máquina separada e só o certificado do servidor vai para o Nginx.

Passo 14: Emitir o certificado do servidor

Gere a chave privada do servidor:

sudo openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key

Crie o arquivo de extensões, trocando nginx.example.internal pelo nome do seu servidor:

sudo tee server.ext > /dev/null <<'EOF'
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = @alt_names

[alt_names]
DNS.1 = nginx.example.internal
EOF
  • extendedKeyUsage = serverAuth: o certificado só serve para identificar um servidor
  • subjectAltName: os nomes que o certificado cobre. Os navegadores só olham este campo para conferir o nome do site. Um certificado sem ele dá erro, mesmo que o nome esteja no CN. Para cobrir mais nomes, adicione DNS.2, DNS.3 e assim por diante.

Crie o pedido e assine com a CA:

sudo openssl req -new -key server.key -subj "/C=BR/O=Example Lab/CN=nginx.example.internal" -out server.csr
sudo openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -days 397 -sha256 -extfile server.ext -out server.crt
  • -CA e -CAkey: quem assina é a sua CA, não o próprio servidor
  • -CAcreateserial: cria o arquivo ca.srl, que controla o número de série de cada certificado emitido
  • -days 397: alguns sistemas, como os da Apple, recusam certificado de servidor com validade longa, mesmo vindo de CA própria. 397 dias funciona em todos. A CA dura 10 anos, o certificado do servidor é renovado todo ano.

Ajuste as permissões e remova os arquivos temporários:

sudo chmod 600 ca.key server.key
sudo chmod 644 ca.crt server.crt
sudo rm ca.csr server.csr

As chaves privadas ficam legíveis só pelo root. O processo principal do Nginx roda como root e consegue ler, os processos que atendem as requisições não.

Confira se o certificado foi emitido corretamente:

sudo openssl verify -CAfile ca.crt server.crt
openssl x509 -in server.crt -noout -subject -ext subjectAltName -dates

Saída esperada:

server.crt: OK
subject=C=BR, O=Example Lab, CN=nginx.example.internal
X509v3 Subject Alternative Name:
    DNS:nginx.example.internal
notBefore=...
notAfter=...

Passo 15: Hardening do Nginx

15.1 Configurações globais

Os arquivos .conf dentro de /etc/nginx/conf.d/ são carregados dentro do bloco http do nginx.conf. Um arquivo com nome começando em 00 concentra as regras que valem para todos os sites do servidor:

sudo tee /etc/nginx/conf.d/00-hardening.conf > /dev/null <<'EOF'
# Não informa a versão do Nginx nas respostas e páginas de erro
server_tokens off;

# Só TLS 1.2 e 1.3. Versões anteriores têm falhas conhecidas
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

# Reaproveita sessões TLS sem reabrir o handshake completo
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
EOF
  • server_tokens off: o cabeçalho passa de Server: nginx/1.x.x para Server: nginx. Saber a versão exata ajuda quem procura uma vulnerabilidade específica.
  • ssl_ciphers: só algoritmos com forward secrecy (os ECDHE). Se a chave do servidor vazar no futuro, o tráfego gravado antes continua sem poder ser decifrado.
  • ssl_prefer_server_ciphers off: com essa lista, todos os algoritmos são fortes, então o cliente escolhe o mais rápido para o hardware dele.
  • ssl_session_tickets off: os tickets de sessão usam uma chave que raramente é trocada e enfraquecem o forward secrecy. O ssl_session_cache cumpre o mesmo papel sem esse problema.

15.2 Desativar o site padrão

O Nginx só carrega arquivos terminados em .conf. Renomear desativa o site padrão sem apagar o arquivo:

sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.disabled

15.3 Criar o site com HTTPS

Troque nginx.example.internal pelo nome do seu servidor:

sudo tee /etc/nginx/conf.d/example-lab.conf > /dev/null <<'EOF'
# Catch-all: recusa qualquer acesso que não use o nome do site,
# como scanners que batem direto no IP
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;

    ssl_reject_handshake on;
    return 444;
}

# HTTP: só redireciona para HTTPS
server {
    listen 80;
    server_name nginx.example.internal;

    return 301 https://$host$request_uri;
}

# HTTPS: o site de verdade
server {
    listen 443 ssl;
    http2 on;
    server_name nginx.example.internal;

    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Cabeçalhos de segurança
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    # add_header Strict-Transport-Security "max-age=31536000" always;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # Bloqueia arquivos ocultos como .git e .env
    location ~ /\. {
        deny all;
    }
}
EOF

O porquê de cada bloco:

  • Catch-all (default_server): quem acessa pelo IP ou por um nome qualquer cai aqui. Na porta 80, o return 444 fecha a conexão sem enviar nenhuma resposta. Na 443, o ssl_reject_handshake on recusa o handshake TLS antes mesmo de mostrar o certificado, então um scanner não descobre nem o nome do seu site.
  • Redirecionamento: quem digitar http:// é mandado para https:// com código 301 (redirecionamento permanente).
  • http2 on: ativa o HTTP/2, que carrega várias requisições em paralelo na mesma conexão.
  • X-Content-Type-Options nosniff: impede o navegador de "adivinhar" o tipo de um arquivo e executar como script algo que foi enviado como texto.
  • X-Frame-Options SAMEORIGIN: impede que outro site carregue o seu dentro de um iframe, técnica usada para enganar o usuário a clicar em algo (clickjacking).
  • Referrer-Policy: ao sair do seu site, o navegador informa ao destino só o domínio de origem, não o caminho completo da página.
  • always: envia o cabeçalho também nas respostas de erro (404, 500), não só nas de sucesso.
  • HSTS (comentado): obriga o navegador a usar HTTPS nesse nome por um ano, sem exceção. Só ative depois que a CA estiver instalada em todas as máquinas que acessam. Com ele ativo, um problema no certificado deixa o site inacessível naquele navegador até a regra expirar.

Os cabeçalhos add_header do bloco server deixam de valer em um location que tenha o seu próprio add_header. Se precisar de um cabeçalho específico em um location, repita os de segurança dentro dele também.

15.4 Testar e aplicar

sudo nginx -t

Saída esperada:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

Passo 16: Configurar o computador cliente

Para o navegador aceitar o certificado, o computador cliente precisa de duas coisas: saber qual IP corresponde ao nome do servidor e confiar na sua CA.

16.1 Resolver o nome do servidor

Em um ambiente com DNS interno, crie um registro A para nginx.example.internal apontando para o servidor. Sem DNS interno, use o arquivo hosts do cliente.

Linux:

echo "192.168.1.10 nginx.example.internal" | sudo tee -a /etc/hosts

Windows (PowerShell como Administrador):

Add-Content -Path "$env:SystemRoot\System32\drivers\etc\hosts" -Value "192.168.1.10 nginx.example.internal"

16.2 Instalar a CA como confiável

Copie o certificado da CA (ca.crt, nunca a ca.key) do servidor para o cliente:

scp -P 2222 seu_usuario@192.168.1.10:/etc/nginx/ssl/ca.crt ./example-lab-ca.crt

Linux (Debian/Ubuntu):

sudo cp example-lab-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

O update-ca-certificates só reconhece arquivos com extensão .crt nessa pasta. A saída deve informar 1 added.

Linux (Fedora, RHEL, Rocky, AlmaLinux):

sudo cp example-lab-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

A família Red Hat usa outra pasta e outro comando, mas o resultado é o mesmo: a CA passa a valer para o sistema inteiro. No Fedora, o Firefox e o Chrome também leem essa loja, então os navegadores passam a confiar na CA junto com o curl.

Windows (PowerShell como Administrador):

Import-Certificate -FilePath .\example-lab-ca.crt -CertStoreLocation Cert:\LocalMachine\Root

No Windows, o Chrome e o Edge usam o repositório de certificados do sistema e passam a confiar na CA imediatamente. Feche e abra o navegador.

Navegador e terminal não usam necessariamente a mesma loja de certificados. O curl sempre lê a loja do sistema. Já os navegadores, dependendo da distribuição, podem ter loja própria: no Debian e no Ubuntu é comum o Firefox e o Chrome não enxergarem a CA instalada no sistema. Se o navegador continuar mostrando aviso, importe a CA pelo próprio navegador. No Firefox, o caminho é Configurações > Privacidade e Segurança > Certificados > Ver certificados > Autoridades > Importar.

O contrário também acontece: a página abrir no navegador e o curl falhar com unable to get local issuer certificate. Nesse caso, a CA está só no navegador, ou você aceitou o aviso de risco na tela e o navegador criou uma exceção. Instale a CA no sistema com os comandos acima.

Passo 17: Validar o HTTPS

Do computador cliente:

curl -I https://nginx.example.internal

Saída esperada:

HTTP/2 200
server: nginx
content-type: text/html
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin

Pontos para conferir: HTTP/2, server: nginx sem versão e os três cabeçalhos de segurança. O curl não reclamou do certificado, o que confirma que a CA foi instalada corretamente.

Teste o redirecionamento:

curl -I http://nginx.example.internal
HTTP/1.1 301 Moved Permanently
Location: https://nginx.example.internal/

Teste o acesso direto pelo IP, que agora precisa ser recusado:

curl -I http://192.168.1.10
curl -I https://192.168.1.10

O primeiro retorna Empty reply from server (o return 444). O segundo retorna o erro abaixo, gerado pelo ssl_reject_handshake:

curl: (35) TLS connect error: error:0A000458:SSL routines::tlsv1 unrecognized name

O unrecognized name é o servidor dizendo que não reconhece o nome pedido no handshake. Acessando pelo IP, nenhum nome é enviado, então o Nginx encerra a conexão antes de mostrar o certificado. O teste do Passo 12 deixa de funcionar a partir daqui, e esse é o comportamento esperado.

Por último, abra https://nginx.example.internal no navegador: o cadeado aparece sem nenhum aviso.


Onde ficam os arquivos

O pacote do nginx.org usa uma estrutura diferente do pacote do Debian. Não existem as pastas sites-available e sites-enabled: cada site é um arquivo .conf dentro de conf.d.

Caminho O que é
/etc/nginx/nginx.conf Configuração global do serviço
/etc/nginx/conf.d/ Um arquivo .conf por site. Todo .conf aqui é carregado automaticamente
/etc/nginx/conf.d/00-hardening.conf Hardening global: versão oculta e parâmetros de TLS
/etc/nginx/conf.d/example-lab.conf Site com catch-all, redirecionamento e HTTPS
/etc/nginx/ssl/ca.crt e ca.key Certificado e chave da CA local
/etc/nginx/ssl/server.crt e server.key Certificado e chave do servidor
/usr/share/nginx/html/ Arquivos da página padrão
/var/log/nginx/access.log Registro de cada requisição recebida
/var/log/nginx/error.log Erros do serviço. Primeiro lugar a olhar quando algo não funciona
/etc/ssh/sshd_config.d/00-hardening.conf Hardening do SSH
/etc/nftables.conf Regras do firewall

Operação do dia a dia

Sempre que editar qualquer arquivo de configuração, teste antes de aplicar:

sudo nginx -t
sudo systemctl reload nginx
Comando Quando usar
sudo systemctl reload nginx Depois de editar a configuração. Relê os arquivos sem derrubar as conexões abertas
sudo systemctl restart nginx Quando o reload não resolve ou o serviço travou. Encerra e inicia o processo, derrubando as conexões em andamento
systemctl status nginx Ver se o serviço está rodando e as últimas mensagens de log
sudo journalctl -u nginx -n 50 Ver as últimas 50 linhas de log do serviço no systemd
nginx -v Ver a versão instalada
sudo sshd -t Validar a configuração do SSH antes de reiniciar
sudo nft -c -f /etc/nftables.conf Validar as regras do firewall antes de aplicar

O nginx -t antes do reload não é opcional. Se a configuração tiver erro, o reload falha e o Nginx continua com a configuração antiga, mas um restart com configuração quebrada derruba o serviço e ele não volta.

Renovar o certificado do servidor

O certificado do servidor vale 397 dias. Para conferir a data de vencimento:

openssl x509 -in /etc/nginx/ssl/server.crt -noout -enddate

Para renovar, repita o Passo 14 a partir do pedido de certificado (openssl req -new -key server.key ...) e rode sudo nginx -t && sudo systemctl reload nginx. A CA continua a mesma, então os clientes não precisam de nenhuma alteração.


Referências oficiais: