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ê.
Referência: https://docs.microsoft.com/en-us/aspnet/core/security/docker-https?view=aspnetcore-6.0