如今,很难想象有公共 API 端点的系统没有 TLS 证书保护。颁发证书的方法有以下几种:
在这篇文章中,我将主要讨论可以在 AWS 内部使用的免费证书,但不限于 AWS 服务。显然,如果您只使用托管的 AWS 服务并且没有严格的安全要求,那么使用AWS 证书管理器以外的任何东西都是没有意义的。AWS 证书管理器提供了一种通过 DNS 或 HTTP 质询颁发证书的非常方便和快速的方法;但是,如果您需要在 AWS 服务(API 网关、ALB、NLB 等)之外使用这些证书,您将面临基本的 AWS 限制,例如运行需要物理证书文件的 Nginx 的 EC2 实例。此外,即使您请求它,AWS 证书管理器也不会显示证书内容。
此时,是时候提醒您有关LetsEncrypt的好时机了,这是一种比证书管理器使用更广泛的工具——至少因为它不依赖于云。不幸的是,AWS 中没有内置的 LetsEncrypt 证书颁发技术。您可以将 certbot 工具用于您的 EC2 或 ECS 服务,但在这种情况下,您需要考虑如何配置续订过程。我也不想结合不同的策略,因为我认为最好对所有事情都采用单一程序,因为这样可以降低整个系统的复杂性。
考虑到这一点,我创建了一个 Lambda 函数,它可以自动颁发和更新 LetsEncrypt 证书,而无需复杂的配置。在初始证书颁发后,可以使用 ARN 以及 AWS 证书管理器证书在任何 AWS 服务中使用该证书。此外,您可以在您选择的任何位置使用保存在AWS Secrets Manager中的物理证书版本,无论是运行 Nginx 的 EC2 实例还是其他地方。
在本文中,我假设您的 DNS 区域由 AWS Route53 管理。
本文描述的 Lambda 函数基于 Go v1.22 编写。所有结果资源(例如 DNS 记录、机密或证书)均由 Amazon IAM 角色控制,该角色默认通过 Terraform 代码创建。Lambda 操作的顺序如下:
aws_cloudwatch_event_target
由 cron 执行的结果。事件示例: { "domainName": "hackernoon.referrs.me", "acmeUrl": "prod", "acmeEmail": "[email protected]", "reImportThreshold": 10, "issueType": "default", "storeCertInSecretsManager" : true }
reImportThreshold
,则启动 LetsEncrypt DNS-01
质询。此步骤涉及 Lambda 创建将域名与 AWS Route53 区域匹配的TXT
记录并等待您的证书准备就绪。storeCertInSecretsManager
为真,Lambda 将会把证书文件存储在 AWS Secrets Manager 中。
代码
Lambda 是用 Go 1.22 编写的。使用尽可能少的库有助于我保持代码简洁的目标。所需 Go 库的完整列表:
网址 | 描述 |
---|---|
帮助 Go 开发人员开发 AWS Lambda 函数的库、示例和工具。 | |
适用于 Go 编程语言的 AWS SDK。 | |
LetsEncrypt / ACME 客户端和库。 | |
合理处理可空值。 | |
适用于 Go 的结构化、可插入式日志记录。 |
Docker 映像
我使用gcr.io/distroless/static:nonroot作为基本 docker 镜像。对于不需要 libc 的 Go 应用程序来说,此镜像非常完美。它并非完全scratch
,包含以下内容:
构建过程
在大型软件项目中,监督构建过程可能成为一项费力且耗时的苦差事。Makefile 可以帮助自动化和简化此过程,确保您的项目高效且一致地构建。因此,我更喜欢对我的所有 Golang 项目使用 Makefile。该文件很简单:
##@ 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 ./...
从 CICD 方面,我使用了 Go 应用程序的典型设置
GitHub Actions作为持续集成。
ghcr.io作为 docker 注册表。与 DockerHub 相比,它提供了两个关键功能,因此我更喜欢使用它:
kvendingoldo/semver-action :我的 GitHub Actions 插件,用于自动版本控制。这是一个 GitHub 操作,可为存储库提交生成与SemVer兼容的标签。该操作可以管理版本、生成 GitHub 版本,并控制存储库内的版本分支。它与 single 和 monorepos 配合得很好。
自动生成变更日志。我喜欢变更日志!在我管理的其他开源项目(例如https://github.com/tofuutils/tenv 、 https://github.com/tofuutils/tofuenv等)中,我的团队认识到向用户告知变更的重要性。
golangci-lint 。在我看来,所有代码都应该由静态代码分析器检查。SonarQube 无法为所有项目设置,但我认为 golangci 足以满足中小型 Go 项目的需求。
本页讨论的代码对于 Terraform 和 OpenTofu 是相同的,但是从 Terraform v1.6 开始,Hashicorp 已将 Terraform 许可证修改为商业源许可证 (BSL) v1.1。如果您正在 Terraform 上开发商业产品,请尽快切换到 OpenTofu。
如果需要管理多个版本的 OpenTofu 或 Terraform,请使用
可以在Git 存储库中的示例文件夹中找到更多 Terraform / OpenTofu 示例。
要通过 OpenTofu 使用 AWS LetsEncrypt Lambda,您需要执行以下步骤:
将模块添加到您的 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 }
指定变量
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 } ] }
注意变量ecr_proxy_username
和ecr_proxy_access_token
。默认情况下,AWS Lambda 无法从 AWS ECR 以外的来源提取图像。幸运的是,AWS 团队创建了 ECR 代理缓存,它可以从 DockerHub 或 GHCR 等公开可用的注册表中获取图像并将其存储在 ECR 中。尽管存在这种可能性,但 AWS 不允许在没有令牌的情况下提取图像,即使是从开放的公共存储库中提取图像,因此您必须获取个人 GitHub 令牌才能访问预先构建的 Docker 图像。或者,您可以提取我的 GitHub 存储库,在本地构建图像,然后将其上传到您预先存在的 ECR 存储库。在这种情况下,可以按如下方式修改示例:
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>" }
完成代码更改后,运行以下命令通过 OpenTofu 版本转换器tenv安装 OpenTofu:
$ tenv tofu install
最后,执行以下命令来应用生成的代码:
$ tofu init $ tofu plan $ tofu apply
等待代码部署到 AWS 并触发事件。几分钟后,您将在证书管理器中看到已准备好的证书。示例:
从现在开始,AWS 可以在任何通过 ARN 提供的服务上使用颁发的证书。
如果您需要在 AWS 服务之外使用证书或访问其内容,请将storeCertInSecretsManager
事件选项设置为true
。在这种情况下,当 Lambda 完成基本执行时,证书将保存在 AWS Secrets Manager 中。它为用户提供了更大的灵活性:他们可以检查证书的内容,直接从 EC2 使用它等。要了解有关 AWS Secrets Manager 的更多信息,请阅读官方指南。
姓名 | 描述 | 可能值 | 默认值 | 例子 | 必需的 |
---|---|---|---|---|---|
| 日志的格式化程序类型 | JSON | 文本 | 文本 | JSON | ❌ |
| 应用程序模式。设置 | 云 | 本地 | 云 | 云 | ✅ |
| 日志记录级别 | 恐慌|致命|错误|警告|信息|调试|跟踪 | 警告 | 警告 | ❌ |
| 默认 AWS 区域。部署到 AWS 后,它会自动设置。 | <任何有效的 AWS 区域> | - | 美国东部-1 | ✅ |
| 正在颁发或续订证书的域名 | 任何有效的域名 | - | ✅ | |
| 如果设置为 | 产品 | 阶段 | 产品 | 产品 | ✅ |
| 与 LetsEncrypt 证书关联的电子邮件地址 | 任何有效的电子邮件 | ✅ | ||
| 如果证书的有效时间 (TTL) 等于 | 任何整数 > 0 | 10 | 10 | ✅ |
| 如果为 | “真” | “假” | “错误的” | “错误的” | ❌ |
在使用aws-letsencrypt-lambda的工作范围内,您可能偶尔想要查看日志。这很容易实现:
/aws/lambda/kvendingoldo-letsencrypt-lambda
转到通过 OpenTofu 创建的 Lambda 函数。单击“测试”按钮。
填写Test Event
,点击Test
{ "domainName": "<YOUR_VALID_DOMAIN>", "acmeUrl": <stage | prod>, "acmeEmail": "<ANY_VALID_EMAIL>", "reImportThreshold": 10, "issueType": "<default | force>", "storeCertInSecretsManager" : <true | false> }
示例 1:
{ "domainName": "hackernoon.referrs.me", "acmeUrl": "prod", "acmeEmail": "[email protected]", "reImportThreshold": 10, "issueType": "default" }
等待执行完成。您可以在 Cloudwatch 中查看执行日志。通常初始问题大约需要 5 分钟。
将https://github.com/kvendingoldo/aws-letsencrypt-lambda存储库克隆到您的笔记本电脑
通过官方指南配置 AWS Cli 凭证。
检查环境变量部分并设置所需的最小变量数。由于 LetsEncrypt 将限制ACME_URL="prod"
每小时的重试次数,因此我建议使用ACME_URL="stage"
进行测试。环境变量示例:
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"
通过以下命令在本地执行 lambda:
go run main.go
Lambda 成功执行后,将出现以下日志。
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
就是这样。从现在开始,AWS 可以通过 ARN 在任何服务上使用颁发的证书,或者通过从 AWS Secrets Manager 获取证书,在物理上需要的其他位置使用颁发的证书。
我在生产中使用 Lambda 函数已有近四年时间。多年来,初始实现的各个方面都发生了变化:
以前,AWS 禁止使用任何非 ECR 注册表作为 Lambda 源。这一规定没有改变,但 AWS 为 GitHub、DockerHub 和一些其他注册表添加了 ECR 代理。如果没有此功能,我们必须手动将 Lambda 镜像推送到我们自己的 ECR,并在 Terraform 代码中将 URL 替换为镜像。现在,OpenTofu 代码可以通过 ECR 代理自动执行此操作。
一开始我考虑过引入各种挑战,比如http-01
或tls-alpn-01
,但四年来没有人质疑过我。它仍然存在于 GitHub 问题上,如果需要这种能力,我们可以一起创造它。
当项目最初启动时,我并不想在纯 EC2 实例上使用 LetsEncrypt 证书,但现在这已成为标准做法。正如我之前所说,在某些情况下,可以使用 AWS cli 从 AWS Secrets Managed 中检索证书。
这些年来,我编写了很多新的 Go 代码,所以我可以看出,我存储库中的原始 Lambda 代码并不像它应该的那样花哨。它与我最近的 Go 项目tenv (OpenTofu、Terraform、Terragrunt 和 Atmos 版本管理器,用 Go 编写)之间存在显著差异,但无论如何,该代码仍然受到普遍支持,因此对其进行修改不会有太大问题。偶尔,我会进行重大重构,使代码更优雅。
多年来,同一个 Lambda 已在多个不同的项目中使用。此外,我还是 DevOps 平台cloudexpress.app的联合创始人,我们的团队使用AWS LetsEncrypt Lambda为所有客户管理 TLS 证书,以简化自动化流程。
现在我们来谈谈数字。在4 年的时间里,这个项目帮助了很多人,并被用于众多开源项目和30 多个商业项目。Lambda 颁发了2000多份证书,并且并不想止步于此。
如果您需要以下解决方案, AWS LetsEncrypt Lambda是适合您的解决方案:
如果您发现上述至少一点适用于您的情况,欢迎您使用 AWS Lambda。此外,如果您希望参与开发,我随时欢迎您在 GitHub 上提出新问题和 Pull 请求。项目网址: https://github.com/kvendingoldo/aws-letsencrypt-lambda 。