使用golang部署运行tls的https服务时,不用停机,高效证书下放,如何实现?
第一部分
这篇文章主要介绍如何在应用golang语言开发http/https服务时,如何让tls自动获取证书,而不必在证书更新或重置以后,还要重启服务器来让业务重新起效,本文分成三部分,第一部分会介绍tls加密的常用加密算法进行分析总结,虽然与主干关系不特别大,但是该段络会帮你厘清一个日常使用中,非常容易被混淆的问题;第二部分会重点介绍如何部署一个不需要重启也能tls自动更新的高抽象度的http服务;第三部分会对整个文章进行总结,相信基于该文章的学习,你一定会对tls领域和流量监测、安全防护领域常见的算法有相对深刻的理解,也对如何高度抽象一个自签名的golang服务有全新的认识。那么文章开始!
我之前已经有文章分享过如果来做爬虫,用python的scrapy和基于node的puppeteer之间的优劣对比。也分享过我之前抓取某国外站点的时候,由于页面验签要正确的传输参数,在post请求中提交到后台才能返回正确的结果,这个在国内的也有很多站点有这种验签机制的存在,比如说我在写基于puppeteer的自动发布文章应用的时候,一些平台,像今日头条平台、像简书用浏览器缓存的cookies就可以避免重复登录,直接模拟浏览器登录发布文章就好了。但是像百度的百家号,只有cookie是无法成功验签的,还要获取下放的token,才能完整成功完成发布百家号的流程,总之,就是“兵来将挡,水来土淹”。
但是你有考虑过,这些平台其实也是基于tls
的验签机会来保护客户端和服务器端数据交互安全性的,但是遵循的底层原理却是各有不同的。比如说JA3
指纹算法,它能基于TLS
客户端与服务端之间握手消息内容生成一个指纹,具体来说,就是在进行TLS
握手时,客户端会发送一些包含有关自身支持的加密套件、TLS/SSL
版本等信息的消息给服务器,服务器会回应类似的消息,JA3
也是根据这些传输消息生成的指纹。它是基于四层网络传输协议,在第四层,即传输层被使用的。而我上面举的例子,比如说向浏览器种token
,把它添加到传输报文的报文头中,服务器对于浏览器提交带着的这个token
进行校验,以确定其合法性,其实它作用的是应用层协议(第七层)中使用的身份验证机制,而并非传输层(四层网络传输协议的第4层),这里要注意区分。
对于tls的生成,其实有很多算法,但是JA3
算法被最广泛使用,那相比于其它算法,它有什么样的优势和劣势呢?我做了个图表进行总结,供大家参考:
算法 | 优点 | 缺点 |
---|---|---|
JA3 指纹算法 | 可以识别 TLS 客户端版本;可以基于握手消息内容生成指纹,具有更高的精度;在不同设备和操作系统上的一致性较好; 它是一种开放标准,任何人都可以实现它并将其集成到自己的应用程序或工具中,这使它成为一个通用的、可扩展的方案; 可用来验证TLS是否被篡改,与SSL证书指纹不同,JA3算法可以检测中间人攻击等网络层面的攻击行为; | 无法判断代理层的影响;无法识别使用自定义密码套件的客户端;只能用于 TLS 握手识别。 |
SSL/TLS 证书指纹算法 | 不受代理层、客户端版本等因素的影响;可以识别采用自定义密码套件的客户端。 | 无法识别中间人攻击;证书签发机构可能存在错误或欺诈。 |
HTTP 消息头指纹算法 | 可以识别代理层、CDN 等影响;适用范围广,可用于 HTTP 流量识别。 | 可能存在误判;对于加密流量而言,只能识别应用层信息。 |
TCP/IP 指纹算法 | 可以识别代理层、NAT 等影响;可以在网络层识别流量。 | 具有较高的误判率;对于加密流量而言,只能识别网络层信息。 |
DNS 指纹算法 | 可以在域名解析阶段进行指纹识别;不受代理层等因素的影响。 | 无法识别加密流量;可能存在 DNS 缓存的干扰。 |
python中有JA3算法也非常常用,特别是下列一些场景:
- 识别恶意软件:通过 JA3 算法可以识别出具有特定 JA3 指纹的恶意软件,从而帮助网络安全人员及时发现和防范攻击。
- 流量识别:JA3 算法可以用于流量识别和分类,帮助工程师进行流量监控、分析等操作。
- 加密流量检测:由于 JA3 算法可以识别 TLS 客户端版本和加密套件,因此它可以被用来检测加密流量是否合法以及是否遵循最佳实践。
- 网络侦查:JA3 算法可以用于识别用户访问网站的类型、客户端操作系统、浏览器版本等信息,帮助网络侦查人员了解对方的技术能力和行为习惯。
- 安全策略制定:通过对 JA3 数据的统计和分析,可以了解不同客户端的使用情况,并据此制定相应的安全策略和措施,提高网络安全性。
第二部分
那如何来部署golang服务,让其支持动态更新TLS certificates
而无需停机?我们知道Transport Layer Security(TLS)
是一种基于SSLv3
的加密协议,用于在两个站点之间加密和解密流量。换言之,TLS
确保你正在访问的站点和你之间数据的传输数据不被侦测到。这是通过相互交换数字证书来实现的:一个存在于web
服务器上的私有证书,另一个通常随web浏览器分发的公共证书。
在生产环境,服务都是以安全方式运行,但服务验证经过一定周期就会过期。然后对于服务响应去验证、重新生成,同时不用停机,就可以重新使用生成的验签证书。这篇文章,演示一下TLS
验证是在基于golang
语言的HTTPS
服务是如何使用的。
这篇教程有先要满足下面这些先决条件。
- 要对
客户端-服务端
模型要有基本理解 Golang
的基础知识
配置HTTP Server
开始这篇文章之前,先演示一个简单的HTTP
服务,只需要使用http.ListenAndServe
函数启动一个HTTP
服务,再用http.HandleFunc
函数对于特定endpoint
注册一个response handler
。
开始,配置HTTP
服务:
package main import ( "net/http" "fmt" "log" )
func main(){
mux := http.NewServeMux()
mux.HandleFun("/",func(res http.ResponseWriter, req *http.Request){
fmt.Fprint(res, "Running Http Servcer")
})srv := &http.Server{ Addr: fmt.Sprintf(":%d",8080), Handler: mux, } //在8080端口运行 log.Fatal(srv.ListenAndServe())
}
上面例子,用go run server.go
,会在HTTP
服务的8080
端口运行,浏览器输入http://localhost:8080
,你就会看到Hello World!
输出在屏幕上。
srv.ListenAndServe()
调用了go语言HTTP
服务的标准库配置,然而,你可以使用Server
结构类型来定制server
。
启动一个HTTPS
服务,用配置调用函数srv.ListenAndServeTLS(certFile,keyFile)
,与srv.ListenAndServe()
函数相似。ListenAndServe
和ListenAndServeTLS
两个函数在HTTP
包和Server
结构中都是可用的。
ListenAndServeTLS
与ListenAndServe
函数类似,除了前者对HTTPS
服务的支持。
func ListenAndServeTLS(certFile string,keyFile string) error
从以上函数签名上可以看出,两者唯一的区别在于额外的certFile
和keyFile
参数,分别代表SSL Certificate
文件的路径和private key
文件。
生成private key
和SSL certificate
以下是生成根key和certificate
的步骤:
-
- 生成根
key
- 生成根
openssl genrsa -des3 -out rootCA.key 4096
-
- 创建和对根
certificate
做self-signature(自签名)
- 创建和对根
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt
接着,按下面的方式为每个服务生成certificate
:
-
- 创建
certificate key
openssl genrsa -out localhost.key 2048
- 创建
-
- 创建
certificate-signing request(CSR)
。CSR
是在哪里指定你想生成的certificate
的详情。根key
的拥有者将执行request
来生成certificate
。当创建CSR
时,重要的是指定提供IP地址的Common Name
,或者服务的域名,否则certificate
无法验证。
- 创建
openssl req -new -key localhost.key -out localhost.csr
-
- 使用
TLS CSR
和密钥以及CA
根密钥生成证书
- 使用
openssl x509 -req -in localhost.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out localhost.crt -days 500 -sha256
最后,遵循同样步骤为每个客户端生成certificate
。
配置一个HTTPS Server
既然有private key
和certificate
文件,就可以对先前的go程序做修改了,这次用ListenAndServeTLS
代替。
package main
import(
"net/http"
"fmt"
"log"
)func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func( res http.ResponseWriter, req *http.Request ) {
fmt.Fprint( res, "Running HTTPS Server!!" )
})srv := &http.Server{
Addr: fmt.Sprintf(":%d", 8443),
Handler: mux,
}
// run server on port "8443"
log.Fatal(srv.ListenAndServeTLS("localhost.crt", "localhost.key"))
}
运行以上程序,将使用包含运行文件同级目录下的localhost.crt
作为certFile
,使用localhost.key
作为keyFile
启动一个HTTPS
服务。再浏览器访问https://localhost:8443
,或者命令行工具(CLI)
,可以看到如下输出:
$ curl https://localhost:8443/ --cacert rootCA.crt --key client.key --cert client.crt
Running HTTPS Server!!
就是这样!这是大多数人启动HTTPS服务器必须做的事情。是Go管理TLS通信的默认行为和功能。
配置HTTPS服务以自动更新证书
当运行以上的HTTPS
服务,你把certFile
和keyFile
传给了ListenAndServeTLS
函数,然而,如果因为certificate
过期certFile
和keyFile
发生变化,服务需要重启来使变化生效,为了克服这种中断导致的的短暂服务中断,可以使用net/http
包的TLSConfig
。
cryto/tls
包的TLSConfig
结构会配置服务的TLS
参数,包括服务证书等。所有TLSConfig
的参数都是可选项,同时也要注意给TLSconfig
的参数配置选项赋以空结构,就等同于赋个nil
值给它。然而,配置GetCertificate
字段却是相当有益的。
type Config struct{
GetCertificate func(*ClientHelloInfo) (*Certicate,error)
}
TLSConfig
的GetCertificate
字段会基于ClientHelloInfo
返回证书。
我需要实现GetCertificate
闭包函数,该函数使用tls.LoadX509KeyPair(certFile string, keyFile string) 或者 tls.X509KeyPair(certFile []byte, keyFile []byte)
函数来获取证书,举两个例子:
#这是使用tls.LoadX509KeyPair(certFile string, keyFile string)函数的例子
func GetCertificate(certFile string, keyFile string) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatal(err)
}
return func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return &cert, nil
}
}
#这是使用tls.X509KeyPair的例子:
func GetCertificate(certData []byte, keyData []byte) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := tls.X509KeyPair(certData, keyData)
if err != nil {
log.Fatal(err)
}
return func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return &cert, nil
}
}
#以上两个函数使用后都会返回一个闭包,可以用作tls.config结构体中的GetCertificate字段。
现在我将使用TLSConfig
字段值创建Server
结构:
package main
import (
"crypto/tls"
"fmt"
"log"
"net/http"
)func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func( res http.ResponseWriter, req *http.Request ) {
fmt.Fprint( res, "Running HTTPS Server!!\n" )
})srv := &http.Server{
Addr: fmt.Sprintf(":%d", 8443),
Handler: mux,
TLSConfig: &tls.Config{
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
// 总是获取最新的localhost.crt和localhost.key
// 将证书文件保存在全局位置中,这样创建新证书时可以更新它们,并且该闭包函数可以引用它。
cert, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key")
if err != nil {
return nil, err
}
return &cert, nil
},
},
}
// 在8443端口运行服务
log.Fatal(srv.ListenAndServeTLS("", ""))
}
以上程序中,我实现了GetCertificate
闭包函数,通过使用LoadX509KeyPair
及证收和之前创建的私有文件,返回了一个类型为Certificate
的cert
对象。同时函数了一个error
,方便调试和追踪。
由于我正在用TLS
配置,为srv
服务对象做预配置,我不需要给srv.ListenAndServeTLS
函数调用提供certFile
和keyFile
。运行服务,它会像之前一样运行,但是区别点就在于,我从调用对象中抽象了所有的服务配置,因此这些配置即便更新,也会动态加载,而不必重启服务。
$ curl https://localhost:8443/ --cacert rootCA.crt --key client.key --cert client.crt
Running HTTPS Server!!
第三部分
好了,这篇有关如何抽象TLS
服务配置,达到不需要重启服务就能加载变更证书的文章就分享至些,感谢阅读,我特别将可用于tls
加密的指纹算法提到第一段来讲,并把JA3
指纹算法在四层服务传输协议中的使用,和浏览器token
验签属于应用层(七层网络传输服务)协议中使用的身份验证机制做了区分,方便你在今后使用的过程中有更深刻的理解。
四层服务传输协议(TCP/IP模型)和七层服务传输协议(OSI模型)的对比如下:
OSI模型 | TCP/IP模型 |
---|---|
应用层(7) | 应用层(4) |
表示层(6) | |
会话层(5) | |
传输层(4) | 传输层(3) |
网络层(3) | 网际层(2) |
数据链路层(2) | 网络接口层(1) |
物理层(1) |
其中,TCP/IP模型将原本的“会话层、表示层、应用层”合并为一个应用层,而将原本的“网络层、数据链路层、物理层”分别放在了网际层、网络接口层两个层级中。
下面是四层服务传输协议(TCP/IP模型)和七层服务传输协议(OSI模型)每层常见的使用场景的对比图表:
OSI 模型 | TCP/IP 模型 | 常见的使用场景 |
---|---|---|
应用层(7) | 应用层(4) | HTTP、FTP、SMTP等应用程序 |
表示层(6) | 数据压缩和加密 | |
会话层(5) | 远程访问和RPC | |
传输层(4) | 传输层(3) | TCP和UDP协议 |
网络层(3) | 网际层(2) | IP、ICMP、ARP等 |
数据链路层(2) | 网络接口层(1) | 以太网、WiFi、ATM等 |
物理层(1) | 传输介质及物理设备 |
在 OSI 模型中,每一层都有自己的功能和特点。应用层负责定义应用程序之间的交互规则;表示层用于对应用数据进行编码和解码;会话层为不同主机上的应用程序之间建立会话连接;传输层提供端到端的可靠数据传输服务;网络层负责将数据包从源主机传输到目标主机;数据链路层管理网络节点之间的数据帧传输;物理层负责传输介质及物理设备。
在 TCP/IP 模型中,应用层包含了 OSI 模型的应用层、表示层和会话层的功能;传输层提供端到端的可靠数据传输服务;网际层负责将数据包从源主机传输到目标主机;网络接口层管理网络节点之间的数据帧传输。
总之,在网络通信中,无论是使用 OSI 模型还是 TCP/IP 模型,每一层都有各自的功能和特点,能够互相配合完成数据传输和网络通信的任务。
这两个图表,相信可以让你对服务传输有更深刻的理解。感谢阅读。