这篇文章是接着上篇 SSL 界中 Linux:Let’s Encrypt 写的。(是的,这周灵感不够 🙈 )
功能 上次说到,如果我们实现的 SaaS/SaaS 服务中的客户需要自定义域名,我们需要给客户提供相应的功能。这个功能大致如何运作?
客户在 DNS 解析中,设置 CNAME 到我们给他提供的唯一子域名上 (注意,之后客户可以直接通过这个域名来访问我们的服务) ;
等待一定时间,让 DNS 记录生效,客户配置自定义域名,提交到我们的服务中;
服务开始验证域名是否解析成功,返回是否成功设置;
则告知客户结果,若成功我们需要等待几个小时甚至一两天来配置 HTTPS 证书,期间可以改成 HTTP 访问,或者还是使用我们提供的子域名访问,不成功则告知需要重新设置;
后端任务服务器开始排队生成 HTTPS 证书;
生成成功后,部署到相应的负载均衡器或者 Web 服务器中,取决于你们如何部署 HTTPS 证书;
通知客户证书部署成功,并且每过 60 天就需要更新证书;
因此,我们需要的功能,最关键的地方在于证书的获取以及部署,部署不用多说,我们一般部署在负载均衡器中,性能会比部署在 Web 服务中要好很多,而如果是云服务的负载均衡器的话,也可以通过相应的 API 去部署。
获取实现 接下来以 Golang 的 Web 服务来说明,我们用 lego 来实现。
首先让我们把 lego 文档上的代码抄过来,限于篇幅,删掉一些注释,以及修改一些代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 package mainimport ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "fmt" "log" "github.com/go-acme/lego/v3/certcrypto" "github.com/go-acme/lego/v3/certificate" "github.com/go-acme/lego/v3/challenge/http01" "github.com/go-acme/lego/v3/challenge/tlsalpn01" "github.com/go-acme/lego/v3/lego" "github.com/go-acme/lego/v3/registration" ) type MyUser struct { Email string Registration *registration.Resource key crypto.PrivateKey } func (u *MyUser) GetEmail() string { return u.Email } func (u MyUser) GetRegistration() *registration.Resource { return u.Registration } func (u *MyUser) GetPrivateKey() crypto.PrivateKey { return u.key } func main () { privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Fatal(err) } myUser := MyUser{ Email: "you@yours.com" , key: privateKey, } config := lego.NewConfig(&myUser) config.CADirURL = lego.LEDirectoryStaging config.Certificate.KeyType = certcrypto.RSA2048 client, err := lego.NewClient(config) if err != nil { log.Fatal(err) } err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("" , "5002" )) if err != nil { log.Fatal(err) } err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("" , "5001" )) if err != nil { log.Fatal(err) } reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true }) if err != nil { log.Fatal(err) } myUser.Registration = reg request := certificate.ObtainRequest{ Domains: []string {"mydomain.com" }, Bundle: true , } certificates, err := client.Certificate.Obtain(request) if err != nil { log.Fatal(err) } fmt.Printf("%#v\n" , certificates) }
这个例子足够我们进行下一步工作了。
如何与 SaaS/PaaS 服务结合 我们看到这个例子中:
使用 HTTP-01
以及 TLSALPN-01
来实现的,考虑到 SaaS/PaaS 服务中,我们无法控制客户的 DNS,因此只能用这两者来实现;
我们的 Web 服务实例放在负载均衡后面,并且不止一个,因此不能用例子中默认的内置服务器来实现这个功能;
TLSALPN-01
在云服务中,需要跟负载均衡器打交道,会比较麻烦,为了方便有效地实现,我们选用 HTTP-01
;
那么,我们的问题就简化为:如何在我们的 Web 服务中,实现 HTTP-01
。
我在前面 说过,Let’s Encrypt 在 HTTP-01
中会返回 token
与 KeyAuth
给你,然后通过 HTTP 请求来验证你是否在控制这个域名,那么,在我们房子负载均衡后面的 Web 服务中,我们如何去响应 LE 的请求?
很简单,放在数据库中 ,更具体点,那就是放在缓存(比如 Redis、Memcache)中,因为可以不用管过期删除的问题。
相对应的,我们可以通过 lego 的 Challenge Solver interface 来实现我们的 Solver:
1 2 3 4 type Provider interface { Present(domain, token, keyAuth string ) error CleanUp(domain, token, keyAuth string ) error }
我们用缓存实现 Preset,比如就把 keyAuth
存入 'lego' + domain + token
对应的 key 中,然后等待 LE 访问 /.well-known/acme-challenge/:token
这个接口,返回 keyAuth 即可。
获取证书后,记得先把存入数据库,再部署至负载均衡器,并且还要周期性地更新证书。
最后,如果你的客户量比较多,记得要向 LE 申请配额,不然会超过频率限制,这点很容易忘,而且你需要考虑申请通过的时间,不会太快。
P.S. 其实 Lego 内置了 Memcache 的 Solver 。
首发于 Github issues: https://github.com/xizhibei/blog/issues/121 ,欢迎 Star 以及 Watch
本文采用 署名-非商业性使用-相同方式共享(BY-NC-SA) 进行许可
作者:习之北 (@xizhibei)
原链接:https://blog.xizhibei.me/zh-cn/2019/09/23/how-to-impl-a-secure-saas-paas-service/