Hoje em dia, é um desafio imaginar sistemas que tenham endpoints de API públicos sem proteção de certificado TLS. Existem várias maneiras de emitir certificados:
No contexto desta postagem, discutirei principalmente certificados gratuitos que podem ser usados dentro da AWS, mas não apenas pelos serviços da AWS. Claramente, usar qualquer coisa diferente do AWS Certificate Manager não faz sentido se você usa exclusivamente serviços gerenciados da AWS e não tem requisitos rígidos de segurança. O AWS Certificate Manager oferece um método muito conveniente e rápido de emissão de certificados por meio de desafios DNS ou HTTP; no entanto, você enfrentará limitações básicas da AWS se precisar usar esses certificados fora dos serviços da AWS (API Gateway, ALB, NLB, etc.), como uma instância EC2 executando Nginx que precisa de um arquivo de certificado físico. Além disso, mesmo que você solicite, o AWS Certificate Manager não exibe o conteúdo do certificado.
Neste ponto, é um bom momento para lembrá-lo do LetsEncrypt , uma ferramenta mais usada que o Certificate Manager – pelo menos porque não depende da nuvem. Infelizmente, não existem técnicas integradas de emissão de certificado LetsEncrypt disponíveis na AWS. É possível utilizar a ferramenta certbot para seus serviços EC2 ou ECS, mas nesse cenário, você precisará considerar como configurar o processo de renovação. Também não quero combinar estratégias diferentes, pois acho melhor ter um procedimento único para tudo, pois reduz toda a complexidade do sistema.
Levando isso em consideração, criei uma função Lambda que emite e renova automaticamente certificados LetsEncrypt sem exigir configurações complexas. O certificado pode ser utilizado em qualquer serviço AWS usando ARN juntamente com certificados AWS Certificate Manager após a emissão inicial do certificado. Além disso, você pode usar uma versão de certificado físico que é mantida no AWS Secrets Manager em qualquer local escolhido, seja uma instância EC2 executando Nginx ou outro local.
Neste artigo, presumirei que sua zona DNS é gerenciada pelo AWS Route53.
A função Lambda descrita neste artigo foi escrita em Go v1.22. Todos os recursos de resultado, como registros DNS, segredos ou certificados, são controlados pela função do Amazon IAM, que é criada por meio do código Terraform por padrão. A sequência de ações do Lambda é a seguinte:
aws_cloudwatch_event_target
. Exemplo de evento: { "domainName": "hackernoon.referrs.me", "acmeUrl": "prod", "acmeEmail": "[email protected]", "reImportThreshold": 10, "issueType": "default", "storeCertInSecretsManager" : true }
DNS-01
se o número de dias até a data de expiração for menor que reImportThreshold
. Esta etapa envolve o Lambda criando um registro TXT
que corresponde ao nome de domínio à zona AWS Route53 e aguardando que seu certificado esteja pronto.storeCertInSecretsManager
for verdadeiro.
O código
O Lambda está escrito em Go 1.22. Usar o mínimo de bibliotecas possível me ajudou a manter meu objetivo de manter o código seco. A lista completa de bibliotecas go necessárias:
URL | Descrição |
---|---|
Bibliotecas, exemplos e ferramentas para ajudar os desenvolvedores Go a desenvolver funções do AWS Lambda. | |
AWS SDK para a linguagem de programação Go. | |
Cliente e biblioteca LetsEncrypt / ACME. | |
Tratamento razoável de valores anuláveis. | |
Registro estruturado e conectável para Go. |
Imagem do Docker
Usei gcr.io/distroless/static:nonroot como uma imagem básica do docker. Para aplicativos Go que não requerem libc, esta imagem é perfeita. Não está completamente vazio como scratch
e inclui o seguinte:
Processo de construção
Em grandes projetos de software, supervisionar o processo de construção pode se tornar uma tarefa trabalhosa e demorada. Makefiles podem ajudar a automatizar e agilizar esse processo, garantindo que seu projeto seja construído de forma eficiente e consistente. Por esse motivo, prefiro usar Makefile para todos os meus projetos Golang. O arquivo é simples:
##@ General help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) fmt: ## Run go fmt against code. go fmt ./... vet: ## Run go vet against code. go vet ./... ##@ Build build: fmt vet ## Build service binary. go build -o bin/lambda main.go run: vet ## Run service from your laptop. go run ./main.go ##@ Test lint: ## Run Go linter golangci-lint run ./... test: ## Run Go tests go test ./...
Do lado do CICD, usei a configuração típica do aplicativo Go
Ações do GitHub como integração contínua.
ghcr.io como registro docker. Comparado ao DockerHub, este oferece dois recursos principais que o tornam minha preferência de uso:
kvendingoldo/semver-action : meu plugin GitHub Actions para versionamento automático. É uma ação do GitHub que gera tags compatíveis com SemVer para commits de repositório. A ação pode gerenciar versões, gerar lançamentos GitHub e controlar ramificações de lançamento dentro do repositório. Funciona maravilhosamente bem com single e monorepos.
Geração automática de changelog. Eu gosto de changelogs! No contexto de outros projetos OpenSource que gerencio (por exemplo, https://github.com/tofuutils/tenv , https://github.com/tofuutils/tofuenv , etc.), minha equipe reconheceu a importância de informar os usuários sobre mudanças.
golangci-lint . Da minha perspectiva, todo código deve ser revisado por um analisador de código estático. O SonarQube não pode ser configurado para todos os projetos, no entanto, o golangci, na minha opinião, é suficiente para projetos Go de pequeno a médio porte.
O código discutido nesta página é o mesmo para Terraform e OpenTofu, mas a partir do Terraform v1.6, a Hashicorp modificou a licença do Terraform para Business Source License (BSL) v1.1. Terraform, mude para OpenTofu o mais rápido possível.
Se você precisar gerenciar várias versões do OpenTofu ou Terraform, use o
Mais exemplos do Terraform/OpenTofu podem ser encontrados na pasta de exemplos do repositório Git .
Para trabalhar com AWS LetsEncrypt Lambda via OpenTofu você precisa seguir as seguintes etapas:
Adicione o módulo ao seu código OpenTofu/Terraform
module "letsencrypt_lambda" { source = "[email protected]:kvendingoldo/aws-letsencrypt-lambda.git//files/terraform/module?ref=0.31.4" blank_name = "kvendingoldo-letsencrypt-lambda" tags = var.tags cron_schedule = var.letsencrypt_lambda_cron_schedule events = var.letsencrypt_lambda_events ecr_proxy_username = var.ecr_proxy_username ecr_proxy_access_token = var.ecr_proxy_access_token }
Especifique variáveis
variable "tags" { default = { hackernoon : "demo" } } variable "ecr_proxy_username" { default = "kvendingoldo" } variable "ecr_proxy_access_token" { default = "ghp_xxx" } variable "letsencrypt_lambda_cron_schedule" { default = "rate(168 hours)" } variable "letsencrypt_lambda_events" { default = [ { "acmRegion" : "us-east-1", "route53Region" : "us-east-1", "domainName" : "hackernoon.referrs.me", "acmeUrl" : "stage", "acmeEmail" : "[email protected]", "reImportThreshold" : 100, "issueType" : "default", "storeCertInSecretsManager" : false } ] }
Preste atenção às variáveis ecr_proxy_username
e ecr_proxy_access_token
. Por padrão, o AWS Lambda não pode extrair imagens de fontes diferentes do AWS ECR. Felizmente, a equipe da AWS criou o cache ECR Proxy, que pode buscar imagens de registros disponíveis publicamente, como DockerHub ou GHCR, e armazená-las dentro do ECR. Apesar dessa possibilidade, a AWS não permite que imagens sejam extraídas sem token, mesmo de repositórios públicos abertos, portanto, você deve adquirir um token GitHub pessoal para obter acesso a imagens Docker pré-construídas. Alternativamente, você pode extrair meu repositório GitHub, construir a imagem localmente e depois carregá-la em seu repositório ECR pré-existente. Neste cenário, o exemplo pode ser modificado da seguinte forma:
module "letsencrypt_lambda" { source = "../../" blank_name = "kvendingoldo-letsencrypt-lambda" tags = var.tags cron_schedule = var.letsencrypt_lambda_cron_schedule events = var.letsencrypt_lambda_events ecr_proxy_enabled = false ecr_image_uri = "<YOUR_ACCOUNT_ID>.dkr.ecr.us-east-2.amazonaws.com/aws_letsencrypt_lambda:<VERSION>" }
Ao concluir a alteração do código, execute o seguinte comando para instalar o OpenTofu pelo OpenTofu version switcher tenv :
$ tenv tofu install
E por fim, execute os seguintes comandos para aplicar o código produzido:
$ tofu init $ tofu plan $ tofu apply
Aguarde até que o código seja implantado na AWS e os eventos sejam acionados. Após alguns minutos, você verá certificados prontos dentro do gerenciador de certificados. Exemplo:
A partir de agora, a AWS pode usar o certificado emitido em qualquer serviço da ARN.
Se você precisar usar o certificado fora dos serviços da AWS ou tiver acesso ao seu conteúdo, defina a opção de evento storeCertInSecretsManager
como true
. Nessa situação, quando o Lambda concluir a execução básica, o certificado será salvo no AWS Secrets Manager. Dá aos usuários mais flexibilidade: eles podem inspecionar o conteúdo do certificado, trabalhar com ele diretamente do EC2, etc. Para saber mais sobre o AWS Secrets Manager, leia o guia oficial.
Nome | Descrição | Valores possíveis | Valor padrão | Exemplo | Obrigatório |
---|---|---|---|---|---|
| Tipo de formatador para logs | JSON | TEXTO | TEXTO | JSON | ❌ |
| Modo de aplicação. Defina o modo | nuvem | local | nuvem | nuvem | ✅ |
| Nível de registro | pânico | fatal | erro | avisar | informações | depurar | rastreamento | avisar | avisar | ❌ |
| Região padrão da AWS. Após a implantação na AWS, ele é configurado automaticamente. | <qualquer região válida da AWS> | - | nós-leste-1 | ✅ |
| Nome de domínio para o qual o certificado está sendo emitido ou renovado | qualquer nome de domínio válido | - | meio-dia. refere.me | ✅ |
| A URL LetsEncrypt de produção será utilizada se estiver definida como | produzir | estágio | cutucar | cutucar | ✅ |
| Endereço de e-mail vinculado ao certificado LetsEncrypt | qualquer e-mail válido | ✅ | ||
| O certificado será renovado se seu tempo de vida (TTL) for igual | qualquer int > 0 | 10 | 10 | ✅ |
| Se | “verdadeiro” | "falso" | "falso" | "falso" | ❌ |
No escopo do trabalho com aws-letsencrypt-lambda, ocasionalmente você pode querer revisar os logs. É muito fácil de realizar:
/aws/lambda/kvendingoldo-letsencrypt-lambda
Vá para a função Lambda que foi criada via OpenTofu. Clique no botão “Testes”.
Preencha Test Event
e clique em Test
{ "domainName": "<YOUR_VALID_DOMAIN>", "acmeUrl": <stage | prod>, "acmeEmail": "<ANY_VALID_EMAIL>", "reImportThreshold": 10, "issueType": "<default | force>", "storeCertInSecretsManager" : <true | false> }
Exemplo 1:
{ "domainName": "hackernoon.referrs.me", "acmeUrl": "prod", "acmeEmail": "[email protected]", "reImportThreshold": 10, "issueType": "default" }
Aguarde até que a execução seja concluída. Você pode ver o log de execução disponível no Cloudwatch. Normalmente, o problema inicial leva cerca de 5 minutos.
Clone o repositório https://github.com/kvendingoldo/aws-letsencrypt-lambda para o seu laptop
Configure as credenciais do AWS Cli por meio do guia oficial .
Examine a seção de variáveis de ambiente e defina o número mínimo de variáveis necessárias. Como LetsEncrypt limitará a quantidade de novas tentativas por hora para ACME_URL="prod"
, aconselho usar ACME_URL="stage"
para teste. Exemplo de variáveis de ambiente:
export AWS_REGION="us-east-2" export MODE=local export DOMAIN_NAME="hackernoon.referrs.me" export ACME_URL="stage" export ACME_EMAIL="[email protected]" export REIMPORT_THRESHOLD=10 export ISSUE_TYPE="default" export STORE_CERT_IN_SECRETSMANAGER="true"
Execute o lambda localmente por meio do seguinte comando:
go run main.go
Após a execução bem-sucedida do Lambda, o log a seguir aparecerá.
INFO[0000] Starting lambda execution ... INFO[0000] Lambda will use STAGING ACME URL; If you need to use PROD URL specify it via 'ACME_URL' or pass in event body INFO[0000] Certificate found, arn is arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af. Trying to renew ... INFO[0000] Checking certificate for domain 'hackernoon.referrs.me' with arn 'arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af' INFO[0000] Certificate status is 'ISSUED' INFO[0000] Certificate in use by [] INFO[0000] Certificate valid until 2024-08-31 13:50:49 +0000 UTC (89 days left) INFO[0000] Try to get certificate for hackernoon.referrs.me domain 2024/06/02 17:56:23 [INFO] acme: Registering account for [email protected] 2024/06/02 17:56:24 [INFO] [hackernoon.referrs.me, www.hackernoon.referrs.me] acme: Obtaining bundled SAN certificate 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/12603809394 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/12603809404 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Could not find solver for: tls-alpn-01 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Could not find solver for: http-01 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: use dns-01 solver 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: Could not find solver for: tls-alpn-01 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: Could not find solver for: http-01 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: use dns-01 solver 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Preparing to solve DNS-01 2024/06/02 17:56:26 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:57:00 [INFO] [www.hackernoon.referrs.me] acme: Preparing to solve DNS-01 2024/06/02 17:57:00 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:57:30 [INFO] [hackernoon.referrs.me] acme: Trying to solve DNS-01 2024/06/02 17:57:30 [INFO] [hackernoon.referrs.me] acme: Checking DNS record propagation. [nameservers=109.122.99.130:53,109.122.99.129:53] 2024/06/02 17:57:34 [INFO] Wait for propagation [timeout: 5m0s, interval: 4s] 2024/06/02 17:57:46 [INFO] [hackernoon.referrs.me] The server validated our request 2024/06/02 17:57:46 [INFO] [www.hackernoon.referrs.me] acme: Trying to solve DNS-01 2024/06/02 17:57:46 [INFO] [www.hackernoon.referrs.me] acme: Checking DNS record propagation. [nameservers=109.122.99.130:53,109.122.99.129:53] 2024/06/02 17:57:50 [INFO] Wait for propagation [timeout: 5m0s, interval: 4s] 2024/06/02 17:58:30 [INFO] [www.hackernoon.referrs.me] The server validated our request 2024/06/02 17:58:30 [INFO] [hackernoon.referrs.me] acme: Cleaning DNS-01 challenge 2024/06/02 17:58:30 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:59:09 [INFO] [www.hackernoon.referrs.me] acme: Cleaning DNS-01 challenge 2024/06/02 17:59:09 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:59:43 [INFO] [hackernoon.referrs.me, www.hackernoon.referrs.me] acme: Validations succeeded; requesting certificates 2024/06/02 17:59:43 [INFO] Wait for certificate [timeout: 30s, interval: 500ms] 2024/06/02 17:59:45 [INFO] [hackernoon.referrs.me] Server responded with a certificate. INFO[0203] Certificate has been successfully imported. Arn is arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af INFO[0204] Secret updated successfully. SecretId: arn:aws:secretsmanager:us-east-2:004867756392:secret:hackernoon.referrs.me-NioT77 INFO[0204] Lambda has been completed
É isso. A partir de agora, a AWS pode usar o certificado emitido em qualquer serviço da ARN ou em outros locais onde seja fisicamente necessário, obtendo-o no AWS Secrets Manager.
Uso a função Lambda em produção há quase quatro anos. Ao longo dos anos, vários aspectos da implementação inicial mudaram:
Anteriormente, a AWS proibia o uso de quaisquer registros não ECR como fontes Lambda. Isso não mudou, no entanto, a AWS adicionou proxy ECR para GitHub, DockerHub e alguns registros adicionais. Sem essa funcionalidade, tivemos que enviar manualmente as imagens Lambda para nosso ECR pessoal e substituir o URL da imagem no código Terraform. Agora o código OpenTofu faz isso automaticamente via ECR Proxy.
No início, pensei em introduzir vários desafios, como http-01
ou tls-alpn-01
, mas ninguém me questionou sobre isso durante quatro anos. Ele ainda está presente nos problemas do GitHub e, se esse recurso for necessário, podemos trabalhar juntos para criá-lo.
Eu não queria utilizar certificados LetsEncrypt em instâncias EC2 puras quando o projeto foi iniciado originalmente, mas hoje em dia é uma prática padrão. Como afirmei anteriormente, em certas situações, um certificado pode ser recuperado do AWS Secrets Managed usando o AWS cli.
Escrevi muitos códigos Go novos ao longo dos anos, então posso dizer que o código Lambda original em meu repositório não é tão sofisticado quanto poderia ser. Há uma diferença significativa entre ele e meu projeto Go mais recente, tenv (gerenciador de versões OpenTofu, Terraform, Terragrunt e Atmos, escrito em Go), mas em qualquer caso, o código ainda é geralmente suportado, portanto, fazer modificações nele ganhou não seja muito problemático. Ocasionalmente, realizarei refatorações significativas para tornar o código mais elegante.
O mesmo Lambda está sendo usado há anos em diversos projetos diferentes. Além disso, sou cofundador da plataforma DevOps cloudexpress.app , onde nossa equipe gerencia certificados TLS para todos os nossos clientes usando o AWS LetsEncrypt Lambda para simplificar os processos de automação.
Agora vamos falar sobre números. Durante um período de 4 anos , este projeto ajudou muitas pessoas e foi usado em vários projetos OpenSource e em mais de 30 projetos comerciais . O Lambda emite mais de 2.000 certificados e não quer parar por aí.
AWS LetsEncrypt Lambda é uma solução adequada para você, se
Se você descobriu que pelo menos um desses pontos se aplica à sua situação, você pode usar o AWS Lambda. Além disso, se você deseja participar do desenvolvimento, estou sempre aberto a novos problemas e solicitações pull no GitHub. URL do projeto: https://github.com/kvendingoldo/aws-letsencrypt-lambda .