Programação Por Equipe DevBras

Capa do artigo: HTTPS no Kestrel dentro do Docker: PEM, PFX e portas

HTTPS no Kestrel dentro do Docker: PEM, PFX e portas

HTTPS no Kestrel dentro do Docker: exporte o certificado em PEM ou PFX, configure portas e caminho por variável de ambiente e entenda cada erro de partida.

Na maioria das instalações, quem cuida do HTTPS é um proxy na frente da aplicação. Mas há casos em que o próprio Kestrel, o servidor web do ASP.NET Core, precisa apresentar o certificado: não há proxy, a rede interna também precisa ser cifrada, ou você quer reproduzir localmente, em container, o mesmo HTTPS que tinha no Visual Studio. Este guia mostra esse caminho com o .NET 10: gerar o certificado, entregá-lo ao container por arquivo, configurar as portas pelas variáveis certas e reconhecer os erros que aparecem quando algo falta. Todos os comandos e mensagens abaixo foram testados com o SDK 10.0.402 no Linux.

Se a sua aplicação está atrás de Nginx, Traefik ou Caddy, o guia certo é ASP.NET atrás de proxy no Docker, que trata de forwarded headers, redirecionamento em loop e cookie sem Secure.

Por que o HTTPS do Visual Studio não vai junto

O comando dotnet dev-certs https cria um certificado autoassinado para localhost e o guarda no repositório de certificados do seu usuário. O container é outro sistema, com outro usuário e sem esse repositório. Na partida, o Kestrel procura um certificado, não encontra e para com esta mensagem:

System.InvalidOperationException: Unable to configure HTTPS endpoint.
No server certificate was specified, and the default developer certificate
could not be found or is out of date.

A solução é exportar o certificado para um arquivo, entregar o arquivo ao container por volume e dizer ao Kestrel onde ele está. A documentação da Microsoft sobre HTTPS no Docker descreve esse fluxo.

Passo 1: exportar o certificado

Há dois formatos. O PFX junta certificado e chave em um arquivo protegido por senha:

dotnet dev-certs https -ep certs/app.pfx -p senha-local

O PEM separa certificado e chave em dois arquivos de texto, o mesmo formato que o Let's Encrypt entrega:

dotnet dev-certs https --format PEM -ep certs/app.pem --no-password
ls certs/
app.key  app.pem

Para produção, troque o certificado de desenvolvimento por um emitido por uma autoridade reconhecida. Os arquivos fullchain.pem e privkey.pem do Let's Encrypt entram exatamente no lugar de app.pem e app.key.

Passo 2: portas e certificado por variável de ambiente

Desde o .NET 8, as imagens oficiais do ASP.NET escutam por padrão na porta 8080, não mais na 80, como registra a nota de mudança de porta. As variáveis ASPNETCORE_HTTP_PORTS e ASPNETCORE_HTTPS_PORTS definem as portas sem precisar escrever URLs completas. O certificado padrão do Kestrel vem das chaves de configuração Kestrel:Certificates:Default, que em variável de ambiente usam dois sublinhados no lugar dos dois-pontos.

Testando fora do container, com os mesmos valores que ele receberia:

ASPNETCORE_HTTP_PORTS=8080 \
ASPNETCORE_HTTPS_PORTS=8443 \
ASPNETCORE_Kestrel__Certificates__Default__Path=certs/app.pem \
ASPNETCORE_Kestrel__Certificates__Default__KeyPath=certs/app.key \
dotnet run

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://[::]:8443
$ curl -k https://localhost:8443/
Hello World!

Com PFX, troque KeyPath por Password. Os outros formatos de configuração de endpoint, incluindo vários certificados por nome de host (SNI), estão em configurar endpoints do Kestrel.

Passo 3: o Compose

No Compose, o certificado entra como volume somente leitura e as variáveis repetem o teste acima:

services:
  api:
    image: minha-api:1.4.2
    ports:
      - "8443:8443"
    environment:
      ASPNETCORE_HTTPS_PORTS: "8443"
      ASPNETCORE_Kestrel__Certificates__Default__Path: /https/app.pem
      ASPNETCORE_Kestrel__Certificates__Default__KeyPath: /https/app.key
    volumes:
      - ./certs:/https:ro
    restart: unless-stopped

Três cuidados. O certificado e, principalmente, a chave nunca entram na imagem com COPY: qualquer pessoa com acesso à imagem levaria a chave junto, como explica o post sobre segredo fora da imagem. Se usar PFX, a senha também não vai escrita no arquivo do Compose versionado; use um arquivo .env fora do Git ou o mecanismo de segredos da sua plataforma. E se o container roda com usuário sem privilégios, como recomenda o guia de usuário não root no container, a chave precisa ter permissão de leitura para esse usuário, ou o Kestrel não consegue abri-la.

Quem administra vários containers por painel pode guardar esse Compose, com variáveis e volume do certificado, no Dockyard, que mantém o YAML salvo e sobe a stack no host escolhido.

Redirecionar HTTP para HTTPS

Com as duas portas abertas, falta mandar quem chega por HTTP para o endereço seguro. O middleware de redirecionamento usa a porta definida em ASPNETCORE_HTTPS_PORTS, sem configuração extra:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}
app.UseHttpsRedirection();

app.MapGet("/", () => "Hello World!");
app.Run();
$ curl -sI http://localhost:8080/
HTTP/1.1 307 Temporary Redirect
Server: Kestrel
Location: https://localhost:8443/

O cabeçalho HSTS, que manda o navegador usar só HTTPS nas próximas visitas, não é enviado para localhost por padrão; ele só aparece quando a aplicação é acessada pelo nome real. Publique a porta HTTP no Compose apenas se quiser esse redirecionamento.

Os erros e o que cada um significa

"No server certificate was specified"

O Kestrel recebeu uma porta HTTPS, mas nenhum certificado. Confira o nome das variáveis (são dois sublinhados entre cada nível), se o volume foi montado no caminho que a variável aponta e se a aplicação está lendo variáveis de ambiente. Rode docker compose exec api ls -l /https para ver se os arquivos estão lá.

"The certificate data cannot be read with the provided password"

System.Security.Cryptography.CryptographicException: The certificate data
cannot be read with the provided password, the password may be incorrect.

Senha errada no PFX, ou o PFX foi gerado com uma senha e a variável recebeu outra. Um detalhe comum: o valor no .env com aspas ou espaço no fim. Gere de novo o arquivo com a senha que vai para a variável.

O HTTPS sobe, mas o navegador recusa

O certificado de desenvolvimento só vale para localhost e só é confiável na máquina onde você rodou dotnet dev-certs https --trust. Acessando por outro nome, IP ou de outro computador, o navegador vai reclamar, e com razão. Para qualquer acesso além do seu próprio computador, use um certificado emitido para o nome real.

Nada responde na porta

Confira se a porta publicada no Compose é a mesma de ASPNETCORE_HTTPS_PORTS. Depois da mudança do .NET 8, imagens antigas que publicavam 443:443 ou 80:80 sem ajustar a variável ficam escutando em outra porta.

A renovação

Certificado servido pela aplicação tem dono: alguém precisa renová-lo e fazer o container usar o novo arquivo. Com Let's Encrypt, o certificado vale 90 dias. Automatize a renovação no host e reinicie o container depois de cada troca, e coloque um alerta de vencimento independente, como mostra o post sobre certificado que vence. Se essa rotina parece trabalho demais para uma aplicação só, é sinal de que o proxy na borda, que renova para todas de uma vez, é o desenho certo para você.

Avalie este artigo

Referência: https://docs.microsoft.com/en-us/aspnet/core/security/docker-https?view=aspnetcore-6.0