使用golang部署运行tls的https服务时,不用停机,高效证书下放,如何实现?

使用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算法也非常常用,特别是下列一些场景:

  1. 识别恶意软件:通过 JA3 算法可以识别出具有特定 JA3 指纹的恶意软件,从而帮助网络安全人员及时发现和防范攻击。
  2. 流量识别:JA3 算法可以用于流量识别和分类,帮助工程师进行流量监控、分析等操作。
  3. 加密流量检测:由于 JA3 算法可以识别 TLS 客户端版本和加密套件,因此它可以被用来检测加密流量是否合法以及是否遵循最佳实践。
  4. 网络侦查:JA3 算法可以用于识别用户访问网站的类型、客户端操作系统、浏览器版本等信息,帮助网络侦查人员了解对方的技术能力和行为习惯。
  5. 安全策略制定:通过对 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服务:

代码语言:javascript
复制
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()函数相似。ListenAndServeListenAndServeTLS两个函数在HTTP包和Server结构中都是可用的。

ListenAndServeTLSListenAndServe函数类似,除了前者对HTTPS服务的支持。

代码语言:javascript
复制
func ListenAndServeTLS(certFile string,keyFile string) error

从以上函数签名上可以看出,两者唯一的区别在于额外的certFilekeyFile参数,分别代表SSL Certificate文件的路径和private key文件。

生成private keySSL certificate

以下是生成根key和certificate的步骤:

    1. 生成根key

openssl genrsa -des3 -out rootCA.key 4096

    1. 创建和对根certificateself-signature(自签名)

openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt

接着,按下面的方式为每个服务生成certificate:

    1. 创建certificate key openssl genrsa -out localhost.key 2048
    1. 创建certificate-signing request(CSR)CSR是在哪里指定你想生成的certificate的详情。根key的拥有者将执行request来生成certificate。当创建CSR时,重要的是指定提供IP地址的Common Name,或者服务的域名,否则certificate无法验证。
代码语言:javascript
复制
openssl req -new -key localhost.key -out localhost.csr
    1. 使用TLS CSR和密钥以及CA根密钥生成证书
代码语言:javascript
复制
openssl x509 -req -in localhost.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out localhost.crt -days 500 -sha256

最后,遵循同样步骤为每个客户端生成certificate

配置一个HTTPS Server

既然有private keycertificate文件,就可以对先前的go程序做修改了,这次用ListenAndServeTLS代替。

代码语言:javascript
复制
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),可以看到如下输出:

代码语言:javascript
复制
$ curl https://localhost:8443/ --cacert rootCA.crt --key client.key --cert client.crt

Running HTTPS Server!!

就是这样!这是大多数人启动HTTPS服务器必须做的事情。是Go管理TLS通信的默认行为和功能。

配置HTTPS服务以自动更新证书

当运行以上的HTTPS服务,你把certFilekeyFile传给了ListenAndServeTLS函数,然而,如果因为certificate过期certFilekeyFile发生变化,服务需要重启来使变化生效,为了克服这种中断导致的的短暂服务中断,可以使用net/http包的TLSConfig

cryto/tls包的TLSConfig结构会配置服务的TLS参数,包括服务证书等。所有TLSConfig的参数都是可选项,同时也要注意给TLSconfig的参数配置选项赋以空结构,就等同于赋个nil值给它。然而,配置GetCertificate字段却是相当有益的。

代码语言:javascript
复制
type Config struct{
GetCertificate func(*ClientHelloInfo) (*Certicate,error)
}

TLSConfigGetCertificate字段会基于ClientHelloInfo返回证书。

我需要实现GetCertificate闭包函数,该函数使用tls.LoadX509KeyPair(certFile string, keyFile string) 或者 tls.X509KeyPair(certFile []byte, keyFile []byte)函数来获取证书,举两个例子:

代码语言:javascript
复制
#这是使用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结构:

代码语言:javascript
复制
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及证收和之前创建的私有文件,返回了一个类型为Certificatecert对象。同时函数了一个error,方便调试和追踪。

由于我正在用TLS配置,为srv服务对象做预配置,我不需要给srv.ListenAndServeTLS函数调用提供certFilekeyFile。运行服务,它会像之前一样运行,但是区别点就在于,我从调用对象中抽象了所有的服务配置,因此这些配置即便更新,也会动态加载,而不必重启服务。

代码语言:javascript
复制
$ 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 模型,每一层都有各自的功能和特点,能够互相配合完成数据传输和网络通信的任务。

这两个图表,相信可以让你对服务传输有更深刻的理解。感谢阅读。