网络信息安全|第8章:申请 Let's encrypt 通配符证书 自动续期
Synopsis: 用 Golang 调用 Aliyun DNS 的 API 接口,实现自动化申请或续期 Let's Encrypt 下的通配符证书(Wildcard Certificate)
1. 用测试 API 体验如何申请通配符证书
2018 年 3 月 更新 了 ACME 协议的 v2 版本,开始支持签署 通配符证书(Wildcard Certificate)
,通配符证书允许你使用单个证书来保护域名下的所有子域。在某些情况下,通配符证书可以使证书管理更容易
如果你想获取通配符证书,就必须使用 ACME v2 协议,需要手动指定它的服务器地址 --server https://acme-v02.api.letsencrypt.org/directory
,另外必须通过 dns-01 的方式,即在你的域名下面添加一条 DNS TXT
记录
在你申请证书时,Let's Encrypt 需要确保你拥有此域名,比如你不可能申请到包含 google.com 域名的证书。Certbot 支持如下 插件 来验证你是否拥有域名:
Plugin | Authenticator | Installer | Notes | Challenge types (and port) |
---|---|---|---|---|
apache | Y | Y | Automates obtaining and installing a certificate with Apache. | http-01 (80) |
nginx | Y | Y | Automates obtaining and installing a certificate with Nginx. | http-01 (80) |
webroot | Y | N | Obtains a certificate by writing to the webroot directory of an already running webserver. | http-01 (80) |
standalone | Y | N | Uses a “standalone” webserver to obtain a certificate. Requires port 80 to be available. This is useful on systems with no webserver, or when direct integration with the local webserver is not supported or not desired. | http-01 (80) |
DNS plugins | Y | N | This category of plugins automates obtaining a certificate by modifying DNS records to prove you have control over a domain. Doing domain validation in this way is the only way to obtain wildcard certificates from Let’s Encrypt. | dns-01 (53) |
manual | Y | N | Helps you obtain a certificate by giving you instructions to perform domain validation yourself. Additionally allows you to specify scripts to automate the validation task in a customized way. | http-01 (80) or dns-01 (53) |
我们将使用 manual
插件,并指定 --preferred-challenges dns-01
。为了演示申请通配符证书的过程,我们先使用 Let's Encrypt 的 测试 API:
[root@CentOS ~]# certbot certonly \ --manual --preferred-challenges dns-01 \ -d *.madmalls.com -d madmalls.com \ --server https://acme-staging-v02.api.letsencrypt.org/directory Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator manual, Installer None Enter email address (used for urgent renewal and security notices) (Enter 'c' to cancel): wangy8961@163.com # 用于接收证书快过期的提醒邮件或安全邮件 Starting new HTTPS connection (1): acme-staging-v02.api.letsencrypt.org - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must agree in order to register with the ACME server at https://acme-staging-v02.api.letsencrypt.org/directory - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (A)gree/(C)ancel: A # 同意服务条款 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Would you be willing to share your email address with the Electronic Frontier Foundation, a founding partner of the Let's Encrypt project and the non-profit organization that develops Certbot? We'd like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (Y)es/(N)o: N # 不公开我的邮箱地址 Obtaining a new certificate Performing the following challenges: dns-01 challenge for madmalls.com dns-01 challenge for madmalls.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NOTE: The IP of this machine will be publicly logged as having requested this certificate. If you're running certbot in manual mode on a machine that is not your server, please ensure you're okay with that. Are you OK with your IP being logged? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (Y)es/(N)o: Y - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please deploy a DNS TXT record under the name _acme-challenge.madmalls.com with the following value: gVC175mlzBZU--D7yFmbEhoZOUbPX6JSE8Tjmz_1kN0 # 到你的域名下添加一条 DNS TXT 记录 Before continuing, verify the record is deployed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Press Enter to Continue
切记: 要先到你的域名下添加 DNS TXT 记录,并等它生效后才能敲回车继续
再打开一个新的 Shell 会话,确认 DNS 记录已生效:
[root@CentOS ~]# dig -t txt _acme-challenge.madmalls.com ; <<>> DiG 9.9.4-RedHat-9.9.4-37.el7 <<>> -t txt _acme-challenge.madmalls.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26864 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;_acme-challenge.madmalls.com. IN TXT ;; ANSWER SECTION: _acme-challenge.madmalls.com. 600 IN TXT "gVC175mlzBZU--D7yFmbEhoZOUbPX6JSE8Tjmz_1kN0" # 注意返回值是否一致
然后敲回车继续申请证书,可能还会要你添加 DNS TXT 记录,因为我们指定了多个 -d
域名值
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Press Enter to Continue Waiting for verification... Cleaning up challenges Resetting dropped connection: acme-staging-v02.api.letsencrypt.org Starting new HTTPS connection (2): acme-staging-v02.api.letsencrypt.org IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/madmalls.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/madmalls.com/privkey.pem Your cert will expire on 2019-09-05. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le
申请完证书后,请删除对应的 DNS TXT 记录!
2. 自动化申请
DNS 服务商一般会提供 API
接口,调用这些接口就能自动添加或删除域名解析记录。Certbot 目前支持的全是国外的 DNS 服务商插件( https://certbot.eff.org/docs/using.html#dns-plugins ) ,比如 certbot-dns-cloudflare
、certbot-dns-google
等,这样你在使用 Certbot 命令申请通配符证书时,只需要指定这些插件列表中的某一个名称(需要先安装此插件),就会自动帮你到你的域名下添加或删除 DNS TXT 记录,从而实现自动化申请通配符证书
国内的 Aliyun 和腾讯云 DNS 也提供了 API 接口,我的 DNS 解析是用的阿里云的(控制台: https://dns.console.aliyun.com/ ),我们只需要调用如下两个接口即可:
我准备用 Golang
来实现接口调用,Aliyun 有提供 DNS 管理的包: https://github.com/aliyun/alibaba-cloud-sdk-go/tree/master/services/alidns
然后,我们需要到 https://usercenter.console.aliyun.com/ 获取访问接口的 AccessKey
,建议使用 RAM
子用户的 AccessKey 来进行 API 调用
我写好的代码放在 Github 上了:
package main import ( "encoding/json" "flag" "fmt" "log" "os" "time" "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" ) // Config 保存 accesskey 的结构体 type Config struct { AccessKeyID string `json:"accessKeyID"` AccessKeySecret string `json:"accessKeySecret"` } // 从 JSON 配置文件中读取 accesskey func readJSONFile(filename string) Config { var config Config f, err := os.Open(filename) defer f.Close() if err != nil { log.Fatalf("Faild to open the JSON file: %s", err) } dec := json.NewDecoder(f) if err = dec.Decode(&config); err != nil { log.Fatalf("Faild to parse the JSON file: %s", err) } return config } // 通过阿里云的 SDK 添加一条 DNS TXT 解析记录,返回记录的 RecordId,后续删除时需要用到它 func addDomainRecord(client *alidns.Client, domainName string, value string) { request := alidns.CreateAddDomainRecordRequest() request.DomainName = domainName request.Type = "TXT" request.RR = "_acme-challenge" request.Value = value response, err := client.AddDomainRecord(request) if err != nil { fmt.Print(err.Error()) } fmt.Printf("[%s] Response from 'addDomainRecord()' is %v\n", time.Now().Format("2006-01-02 15:04:05"), response) } // 列出所有记录类型为 TXT,且记录名包含 '_acme-challenge' 的所有记录,返回 recordID 组成的切片,后续删除它们 func listDomainRecords(client *alidns.Client, domainName string) []string { request := alidns.CreateDescribeDomainRecordsRequest() request.DomainName = domainName request.TypeKeyWord = "TXT" request.RRKeyWord = "_acme-challenge" response, err := client.DescribeDomainRecords(request) if err != nil { fmt.Print(err.Error()) } fmt.Printf("[%s] Response from 'listDomainRecords()' is %v\n", time.Now().Format("2006-01-02 15:04:05"), response) var recordIds []string for _, r := range response.DomainRecords.Record { recordIds = append(recordIds, r.RecordId) } return recordIds } // 删除解析记录 func deleteDomainRecord(client *alidns.Client, recordID string) { request := alidns.CreateDeleteDomainRecordRequest() request.RecordId = recordID response, err := client.DeleteDomainRecord(request) if err != nil { fmt.Print(err.Error()) } fmt.Printf("[%s] Response from 'deleteDomainRecord()' is %v\n", time.Now().Format("2006-01-02 15:04:05"), response) } func main() { // 提供 -c 选项,用户可以指定JSON配置文件。注意,cfg 是一个指针 cfg := flag.String("c", "config.json", "Assign the JSON config file") // 操作类型,authenticator: 域名认证,添加 DNS TXT 记录; cleanup: 认证通过后,删除此 DNS TXT 记录 opt := flag.String("o", "authenticator", "Operate: authenticator or cleanup") // 提供 -h 选项,查看命令行帮助信息 help := flag.Bool("h", false, "show help infomation") flag.Parse() if *help { flag.Usage() os.Exit(0) } // 解析JSON配置文件 config := readJSONFile(*cfg) // Client for Aliyun DNS SDK client, err := alidns.NewClientWithAccessKey("cn-hangzhou", config.AccessKeyID, config.AccessKeySecret) if err != nil { log.Fatal(err.Error()) } // 判断操作类型 switch *opt { case "authenticator": // CERTBOT_DOMAIN 和 CERTBOT_VALIDATION 是 Certbot Hooks 传过来的环境变量 domainName := os.Getenv("CERTBOT_DOMAIN") value := os.Getenv("CERTBOT_VALIDATION") if domainName == "" || value == "" { log.Fatal("Error: This plugin can only be used for 'certbot' (Let's Encrypt)") } addDomainRecord(client, domainName, value) // Sleep to make sure the change has time to propagate over to DNS time.Sleep(30 * time.Second) /* 否则报错: Attempting to renew cert (madmalls.com) from /etc/letsencrypt/renewal/madmalls.com.conf produced an unexpected error: Failed authorization procedure. madmalls.com (dns-01): urn:ietf:params:acme:error:dns :: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.madmalls.com - check that a DNS record exists for this domain. Skipping.All renewal attempts failed. The following certs could not be renewed: /etc/letsencrypt/live/madmalls.com/fullchain.pem (failure) */ case "cleanup": // 先获取所有记录类型为 TXT,且记录名包含 '_acme-challenge' 的记录 ID recordIds := listDomainRecords(client, os.Getenv("CERTBOT_DOMAIN")) fmt.Printf("[%s] All record Ids that need to delete is: %v\n", time.Now().Format("2006-01-02 15:04:05"), recordIds) // 循环,删除它们 for _, id := range recordIds { deleteDomainRecord(client, id) } } }
编译:
然后将编译好的可执行程序 certbot-dns-aliyun
(或者直接到我的 Github 下载),复制到你指定的位置(比如 /etc/letsencrypt/
目录下)即可,并在该目录下新建 config.json
文件,里面是你的 AccessKey,格式如下:
2.1 RSA 通配符证书
[root@CentOS ~]# certbot certonly \ --non-interactive \ --email wangy8961@163.com \ --agree-tos \ --manual-public-ip-logging-ok \ --manual --preferred-challenges dns-01 \ --manual-auth-hook "/etc/letsencrypt/certbot-dns-aliyun -o authenticator" \ --manual-cleanup-hook "/etc/letsencrypt/certbot-dns-aliyun -o cleanup" \ -d *.madmalls.com -d madmalls.com \ --server https://acme-v02.api.letsencrypt.org/directory
你只需要替换 --email
和 -d
的值即可
如果你只是想测试 Certbot 功能,可以指定测试环境的 API:
--server https://acme-staging-v02.api.letsencrypt.org/directory
验证证书的内容:
[root@CentOS ~]# openssl x509 -text -in /etc/letsencrypt/live/madmalls.com/fullchain.pem -noout Certificate: Data: Version: 3 (0x2)
0 条评论
评论者的用户名
评论时间暂时还没有评论.