网络信息安全|第8章:申请 Let's encrypt 通配符证书 自动续期

  • 原创
  • Madman
  • /
  • /
  • 0
  • 1576 次阅读

网络信息安全-min.jpg

Synopsis: 用 Golang 调用 Aliyun DNS 的 API 接口,实现自动化申请或续期 Let's Encrypt 下的通配符证书(Wildcard Certificate)

SSL Labs get A+

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 记录,并等它生效后才能敲回车继续

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-cloudflarecertbot-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)
    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)
        }
    }
}

编译:

[root@CentOS ~]# go build -o certbot-dns-aliyun main.go

然后将编译好的可执行程序 certbot-dns-aliyun (或者直接到我的 Github 下载),复制到你指定的位置(比如 /etc/letsencrypt/ 目录下)即可,并在该目录下新建 config.json 文件,里面是你的 AccessKey,格式如下:

{
    "accessKeyID": "你的AccessKeyID",
    "accessKeySecret": "你的AccessKeySecret"
}

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)
                                
                            
未经允许不得转载: LIFE & SHARE - 王颜公子 » 网络信息安全|第8章:申请 Let's encrypt 通配符证书 自动续期

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.

专题系列