Ir para o conteúdo

TLS e proxy reverso

Os servidores HTTP do próprio PEART nunca terminam TLS. Uma instalação fica atrás de um proxy reverso que termina TLS na frente das duas superfícies e encaminha para as portas de loopback delas. Definir tls: true na configuração diz ao PEART que ele está atrás de um proxy desses e liga o cabeçalho Strict-Transport-Security.

As duas superfícies

O proxy reverso fica na frente de duas superfícies independentes, idealmente em dois hostnames:

  • a superfície interna (peart) na :8080, alcançável só da rede restrita ou VPN da consultoria,
  • o portal (peart-portal) na :8081, alcançável pelo cliente pela internet.

Mantê-las em hostnames separados (e exposições separadas) espelha a fronteira de confiança sobre a qual a arquitetura é construída: a superfície interna não deve ser alcançável de onde o cliente alcança o portal.

O tls é uma chave só, para as duas superfícies

O tls é uma configuração única do deploy, não uma por superfície. Ligá-lo diz "este deploy está atrás de algo que termina TLS", e vale igualmente para a superfície interna e para o portal: as duas enviam Strict-Transport-Security, e cada public_url que você definir precisa ser https. Não há como pôr o portal atrás de TLS e deixar a interna em HTTP puro, e isso é deliberado: a interna carrega os achados ainda não publicados, que são a metade mais sensível do sistema.

Um túnel é um front válido, para o portal

O proxy não precisa ser um host seu. Um túnel que termina TLS num provedor de borda e alcança a porta de loopback por dentro (um túnel da Cloudflare, por exemplo) é front legítimo para o portal: o cliente chega à borda por HTTPS, e o host não precisa de nenhuma porta aberta para entrada. Ponha tls: true e o public_url do portal no nome público, exatamente como faria com um proxy local.

Não tunele a superfície interna do mesmo jeito. Publicá-la na internet, por túnel ou por porta aberta, leva a superfície do pentester para onde o navegador do cliente alcança o portal, que é justamente a fronteira que a arquitetura de dois processos existe para manter. Alcance a interna pela VPN ou pela rede restrita da consultoria.

Com tls: true as duas superfícies ficam atrás de um terminador, e o da interna é local -- nginx ou Caddy no próprio host, alcançável apenas pela rede restrita.

Atrás de um terminador, diga qual endereço é o do cliente

Um proxy ou um túnel termina a conexão, então toda requisição chega ao PEART vindo do terminador: o endereço de loopback do próprio conector, no caso do túnel, ou o do proxy, no caso local. Sem mais nada, é esse o endereço que o log de acesso registra, o que fica gravado em toda linha de sessão, a chave que o limitador conta e a metade por origem do tranca-conta. Todo cliente parece o mesmo cliente.

A consequência mais séria é a mais silenciosa. Uma investigação começa pela trilha, e atrás de um túnel a trilha diz o conector todas as vezes; ninguém percebe até o dia em que alguém precisa dela.

Duas chaves por superfície resolvem, e as duas vêm desligadas:

portal:
  listen_addr: ":8081"
  trusted_proxies: ["127.0.0.1"]
  trusted_proxy_header: "CF-Connecting-IP"

O trusted_proxies lista os pares cujo cabeçalho encaminhado é acreditado, como endereços avulsos ou faixas CIDR. Liste endereços que você controla, e não endereços em que você confia genericamente. Qualquer coisa nessa lista pode declarar o endereço de cliente que quiser, então um par que não seja de fato o seu terminador devolve esse poder ao cliente, o que é pior do que deixar tudo desligado.

O trusted_proxy_header é o nome a ler, e o PEART nunca o adivinha. Um túnel da Cloudflare envia CF-Connecting-IP; um nginx envia o que você mandar, comumente X-Forwarded-For. Configurar uma chave sem a outra é recusado no arranque, porque qualquer uma sozinha é configuração que parece ligada e não está.

O cabeçalho só é lido de um par que esteja na lista, e nunca fora disso. Requisição que chegue de qualquer outro lugar fica com o endereço dela, carregue o cabeçalho que carregar, e é isso que impede um cliente de simplesmente se declarar outra pessoa.

Se um par da lista não enviar endereço legível, o PEART usa o do próprio par e registra um aviso nomeando o par e o cabeçalho. Ele não recusa a requisição: proxy mal configurado não deve virar indisponibilidade. O aviso é o que impede esse estado de parecer exatamente com sucesso.

Ligar isso muda o que as linhas novas da tabela de sessão significam: as anteriores carregam o endereço do conector, as posteriores o do cliente. Não há migração, há essa leitura.

Exemplo: um proxy reverso

Um proxy reverso mínimo termina TLS e encaminha para a porta de loopback. Com o Caddy:

portal.example.com {
    reverse_proxy 127.0.0.1:8081
}
internal.example.com {
    reverse_proxy 127.0.0.1:8080
}

O Caddy obtém e renova certificados sozinho. Com o nginx, termine TLS num bloco server por hostname e faça proxy_pass para 127.0.0.1:8081 e 127.0.0.1:8080.

O PEART não descobre pela requisição que o TLS foi terminado na frente dele, e isso é deliberado: um cabeçalho encaminhado vem do cliente, e um processo que tirasse a própria postura de segurança de um deles entregaria a chave ao atacante. O tls: true da configuração é tudo, e é ele que torna o cookie de sessão Secure e envia HSTS. Encaminhe o Host e o que o seu proxy precisar para o registro dele, e saiba que o X-Forwarded-Proto não é lido: HTTPS na borda com tls: false atrás produz cookie não seguro numa conexão que parece cifrada, e é a única combinação que vale conferir depois de mexer no proxy.

Um hostname interno que só resolve na VPN

O exemplo acima supõe os dois nomes alcançáveis pela internet, que é como o Caddy obtém certificado para eles sozinho. Um hostname interno que só resolve na VPN da consultoria não consegue responder a um desafio ACME por HTTP, porque a autoridade certificadora não tem rota até ele: ou emita o certificado daquele nome por desafio de DNS, ou use um wildcard que você já tenha para a zona interna e aponte o proxy para os arquivos. Nada mais muda na superfície interna, e em particular ela continua escutando só no loopback e continua sendo alcançada por um terminador na rede restrita.

Rodar sem TLS

tls: false recusa subir a menos que accept_insecure_http: true também esteja definido. Essa combinação existe para uma instalação em HTTP puro deliberada (um laboratório fechado, um teste local), nunca como default acidental. Não rode uma instalação real assim; ponha o proxy na frente e defina tls: true.