利用golang申请Let's Encrypt的HTTPS实现证书自动部署

一.HTTPS的好处

1.目前已经都普及用https协议了,但是还是有一些没用https,https协议可以标识网站是否安全,简单的说就是网站传输数据的时候https更安全,http可能会被窃听数据等,而https数据传输会进行加密,所以是更安全可靠的协议,更大谷歌浏览器这些都大力推进用https。

二.申请HTTPS证书

一.首先说一下流程吧申请一个HTTPS证书的简单步骤

    1.向一个颁发证书的机构(CA)发出申请
    2.CA验证你域名的控制权
    3.下发证书

二.上面说到申请的步骤很简单就3步,可以看一下Let's Encrypt这个机构的证书是免费的,有的证书颁发机构是收费的,有啥区别呢,Let's Encrypt是免费的一个颁发证书的CA机构,不好的一点就是一个证书期限只有90天,到期可以续签,都是免费的,Let's Encrypt设置90天期限也是有他道理的,毕竟免费的证书万一也发生你的密钥泄露了,然而Let's Encrypt不可能你的泄露了,还找别人麻烦,所以90天自动失效可以避免密钥的安全,申请的证书还分种类的OV,DV,EV,这里我就不介绍这些证书的种类吧可以网上查一下就知道了,申请证书的工具cerbot。

三.golang申请证书

1.需要用到的包
github.com/xenolf/lego/acme 核心的包
github.com/janeczku/rancher-letsencrypt 这个库使用的上面acme的包给他重新封装了一下

这个包是申请证书的核心用到的acme,cerbot这个工具申请证书也需要acme实现github上有大佬用go写了acme,就可以利用这个库来进行操作,向Let's Encrypt机构申请证书需要一个账号,账号可以标识你申请证书的速率限制这些。

2.使用这个包申请证书

github.com/janeczku/rancher-letsencrypt 这个包下的有个方法NewClient(...)

    client, err = letsencrypt.NewClient(
        app.Config.Cert.Email,
        letsencrypt.KeyType(app.Config.Cert.KeyType),
        letsencrypt.ApiVersion(app.Config.Cert.ApiVersion),
        app.Config.Cert.DnsResolvers,
        provider,
    )

app.Config.Cert.Email:你的邮箱([email protected])
app.Config.Cert.KeyType: 加密方式(RSA-2048)
app.Config.Cert.DnsResolvers: DNS 如果使用基于DNS的质询,则使用其中一个受支持的DNS提供商的现有帐户
app.Config.Cert.ApiVersion: 环境(生产环境[Production]和测试环境[Stagin])
Production是生产环境有限制,Stagin是测试环境无限制,限制指的是速率限制

3.通过NewCilent创建好client后

1.这个client有几个方法分别是:申请证书,更新证书 这两个是最主要的方法

func (c *Client) Issue(certName string, domains []string) (*AcmeCertificate, error) {
    certRes, err := c.client.ObtainCertificate(domains, true, nil, false)
    if err != nil {
        return nil, err
    }

    dnsNames := dnsNamesIdentifier(domains)
    acmeCert, err := c.saveCertificate(certName, dnsNames, *certRes)
    if err != nil {
        logrus.Fatalf("Error saving certificate '%s': %v", certName, err)
    }

    return acmeCert, nil
}

Issue这个方法是申请证书的方法,2个参数第一是证书名称,我看了一下他的源码这个certName代表申请下来后自动存储到本地的文件夹用做的名字,为了避免混淆还是域名是啥就写啥吧,申请下来后自动创建一个文件夹的名称就是你的域名名称好记一些,第二个参数是传入切片,可同时申请多个域名。
这个函数执行申请证书他会等待你去验证这个域名你是否有控制权,这是最关键的点,这个称之为 "挑战" 需要你去挑战。
2.挑战
挑战有3种方式我简单说两种吧DNS挑战和HTTP挑战
DNS: Let's Encrypt(CA)会给你下发任务去完成,例:它给你一个字符串叫你给这个域名的DNS添加一条TXT记录的值是它给的字符串,修改好了他会去验证,验证成功后颁发证书。
HTTP: CA会给你下发任务,列:给你一个key,返回Value给它,这里绑定了80端口,CA会发一个请求过来domain.com/.well-known/acme-challenge他会发一个key过来,然后通过Key获取Value返回给它代表验证成功,这个可以代理转发一下就实现了

v1 := ginsrv.Engine.Group("")

// http挑战
v1.GET("/.well-known/acme-challenge/*token", api.ChallengeCert)

这里我写了个路由可以获取Key,ChallengeCert方法去转发去获取Value
这个路由写到你域名的服务器上它需要发请求到你这个主机上面来验证。
申请证书客户端我是这样接收挑战来验证的

// 证书挑战
func ChallengeCert(domain, token string) (value string, err error) {
    exist, key := memoryProviderServer.GetKeyAuth(domain, token)
    if !exist {
        err = errors.NewCoder(404, fmt.Sprintf("domain AND token not found: %s %s", domain, token))
        return
    }

    value = key
    return
}

上面token就是传过来的key,这边client已经有Value了,我这边挑战的时候实现一下挑战方法,把Value存到内存中,获取返回

// 将keyAuth存在内存, 通过暴露一个方法获取对应domain的证书
type MemoryProviderServer struct {
    data map[string]string // domain+token => keyAuth
    lock sync.Mutex
}

func NewMemoryProviderServer() *MemoryProviderServer {
    return &MemoryProviderServer{
        data: map[string]string{},
    }
}

func (s *MemoryProviderServer) Present(domain, token, keyAuth string) error {
    s.lock.Lock()
    s.data[domain+token] = keyAuth
    s.lock.Unlock()
    return nil
}

func (s *MemoryProviderServer) CleanUp(domain, token, keyAuth string) error {
    s.lock.Lock()
    delete(s.data, domain+token)
    s.lock.Unlock()
    return nil
}

func (s *MemoryProviderServer) GetKeyAuth(domain, token string) (exist bool, keyAuth string) {
    s.lock.Lock()
    keyAuth, exist = s.data[domain+token]
    s.lock.Unlock()
    return
}

挑战有2个接口
Present和CleanUp,到了挑战的时候会传入keyAuth这个就是value我们需要获取的
我们直接先存入map里保存在内存里等待GetKeyAuth获取 最后返回 完成了挑战
自己实现这两个接口CleanUp是挑战完后执行的方法。

DNS怎么重写挑战接口实现也是同理实现这两个方法,

const DefaultTTL = 120

type DNSProviderBestDNS struct {
    RecordId int64
    Fqdn     string
}

func NewDNSProviderBestDNS() *DNSProviderBestDNS {
    return &DNSProviderBestDNS{}
}

// 实现接口
func (d *DNSProviderBestDNS) Present(domain, token, keyAuth string) error {
    // fqdn是用于设置TXT记录的完全限定域名,value是记录的值,是记录上ttl设置的TTL
    fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
    req := &pb.AddRecordRq{
        Type:   "TXT",
        Value:  value,
        Ttl:    DefaultTTL,
        Domain: domain,
        Host:   acme.UnFqdn(strings.Replace(acme.UnFqdn(fqdn), domain, "", -1)),
    }
    log.Info(req.Value, req.Value[0])
    // 发出API请求在fqdn上设置txt记录值和ttl
    c := pb.NewDnsClient(client)
    rsp, err := c.AddRecord(context.Background(), req)
    if err != nil {
        err = errors.Wrap(err, "添加DNS调用GRPC服务错误")
        return err
    }
    if rsp.RecordId == 0 {
        err = errors.Wrap(err, "返回DNS记录的ID为0")
        return err
    }
    d.RecordId = rsp.RecordId
    d.Fqdn = fqdn
    return nil
}

func (d *DNSProviderBestDNS) CleanUp(domain, token, keyAuth string) error {
    // 清除你在Present中创建的任何状态,比如请求API删除txt记录
    // 发出API请求在fqdn上设置txt记录值和ttl
    req := &pb.DeleteRecordRq{
        HostId: d.RecordId,
        Domain: domain,
    }
    c := pb.NewDnsClient(client)
    _, err := c.DeleteRecord(context.Background(), req)
    if err != nil {
        err = errors.Wrap(err, "删除DNS调用GRPC服务错误")
        return err
    }
    return nil
}

DNS01Record(...)这个方法返回一个DNS记录,完成DNS01的挑战内部处理好了
DNS的txt记录的值和host最后我这里AddRecord(...)这个是一个GRPC服务写好了操作DNS的API实现修改删除等这些方法,直接调就ok了,GRPC还是蛮方便的以前写的方法操作DNS的一些东西,没想到这里需要操作DNS,直接调就好了,复用性还是不错,嘿嘿,完成了挑战本地会给你创建一个文件夹etc/letsencrypt/production/certs目录下2个PEM文件fullchain.pem和privkey.pem,一个公钥一个私钥。

4.部署证书

我自己实现了一个自动部署证书的方法,申请完证书后我存入mysql数据库
在需要部署证书的服务器上运行一个client这个client是部署机器client和上面申请证书的client不一样我还是把它叫做deploy-client(部署客户端)吧和server保持TCP长连接,
server里我写了一个路由用做Apply-cilent(这个就是申请证书的client)申请好后通过一个API把申请好的证书给server看看发送给server的代码吧

// 请求api通知front-api
func SendToFrontApi(fullchina, privkey, domain string) (err error) {
    urls := app.Config.FrontApi + certNotice
    resp, err := http.PostForm(urls,
        url.Values{"fullchina": {fullchina}, "privkey": {privkey}, "domain": {domain}})
    if err != nil {
        err = errors.NewCodere(400, err, "请求front_api出错")
        return
    }
    defer resp.Body.Close()
    return
}

三个东西:域名,私钥,公钥。
server和deplop-client保持着TCP通信的当server拿到证书的数据后发送到deplop-client去处理,看看我怎么处理的吧

            case "cert_event":
                pem := CertPem{}
                e := json.Unmarshal([]byte(in.Raw), &pem)
                if e != nil {
                    log.Errorf("CertPem Unmarshal err:%+v", err)
                    break
                }

                e = ssl.DeployNginxSsl(pem.Fullchina, pem.Privkey, pem.Domain)
                if e != nil {
                    log.Errorf("DeployNginxSsl err:%+v", e)
                    break
                }

                log.Infof("%s 部署成功", pem.Domain)
            }

deplop-client这边等待接收数据,判断数据的类型用switch处理对应的消息,json反序列化出来,DeployNginxSsl(...)函数去在本地的nginx里去部署。我这边我是这样做的,重所周知部署证书很简单,就是Nginx的conf配置里写好你证书的路径和域名,重启nginx生效,可以看一下我怎么处理的

// 部署证书
func DeployNginxSsl(fullChain string, privKey string, domain string) (err error) {
    // 生成证书文件
    err = writePemFile(fullChain, privKey, domain)
    if err != nil {
        err = errors.Wrap(err, "writePemFile error", domain)
        return
    }
    // 写入nginx配置conf
    err = writeNginxConf(domain)
    if err != nil {
        err = errors.Wrap(err, "writeNginxConf error", domain)
        return
    }
    // 测试配置
    err = nginxTest()
    if err != nil {
        err = errors.Wrap(err, "nginxTest error", domain)
        return
    }

    // 重启nginx生效
    err = reloadNginxByDocker()
    if err != nil {
        err = errors.Wrap(err, "reloadNginxByDocker error", domain)
        return
    }

    return
}

一个完整的自动申请,部署完成
接下来就是续签的问题证书到期时间为90天

deplop-client里面我写了一个定时器每天凌晨12点检查证书是否过期,过期就去Apply-client里面拿, Apply-client里我写了一个自动检查更新,去更新证书,更新好之后放入数据库,方便deplop-client去拿,Apply-client一般要早与deplop-client的更新,避免未更新拿到原来的证书数据。

你可能感兴趣的:(利用golang申请Let's Encrypt的HTTPS实现证书自动部署)