docker swarm 集群:网络安全 TLS 分析
TLS 基础
docker swarm 集群间为了保证通信安全,使用TLS进行安全加固。分析TLS安全加固时需要一些网络安全背景知识。
数字签名:《数字证书及CA的扫盲介绍》、《数字签名是什么》
CA和证书:《数字证书(Digital Certificate)与数字签名(Digital Signature)》、《OpenSSL 与 SSL 数字证书概念贴》、《基于OpenSSL自建CA和颁发SSL证书》
SSL/TLS:《SSL/TLS原理详解》、《grpc tls认证传输》以及很全的关于TLS 证书的文档(英文)《Survival guides - TLS/SSL and SSL (X.509) Certificates》
数字签名过程
发送人将发送内容使用HASH算法生成一个内容摘要(digest) —> 内容摘要(digest)使用私钥进行加密—>生成数字签名(signature) —> 数字签名(signature)附在信件下面 —> 将信件发送给收件人 —-> 收件人收到信件,并使用发送人的公钥进行解密:证明信件来自于发件人 —> 收件人使用同种Hash算法计算发送内容的摘要(digest),并和解密得到的digest进行对比:一致则证明信件内容没有别修改
一次数字签名涉及到一个哈希函数、发送者的公钥、发送者的私钥(不在签名里)。
数字签名使用私钥对内容摘要用私钥进行加密;加密通信过程中,使用公钥加密,私钥进行解密
数字中心(CA)
验证公钥的正确性,为公钥做认证,使用自己的私钥为其他公钥和一些信息进行加密; —> 生成”数字证书”Digital Certificate。
签名证书
CA进行对公钥和其他一些内容进行数字签名的信件或消息。
证书常用扩展名
- .crt 证书文件 ,可以是DER(二进制)编码的,也可以是PEM( ASCII (Base64) )编码的 ,在类unix系统中比较常见
- .cer 也是证书 常见于Windows系统 编码类型同样可以是DER或者PEM的,windows 下有工具可以转换crt到cer
- .csr 证书签名请求 一般是生成请求以后发送给CA,然后CA会给你签名并发回证书.key 一般公钥或者密钥都会用这种扩展名,可以是DER编码的或者是PEM编码的 查看DER编码的(公钥或者密钥)的文件的命令为 openssl rsa -inform DER -noout -text -in xxx
- .key 查看PEM编码的(公钥或者密钥)的文件的命令为 openssl rsa -inform PEM -noout -text -in xxx.key
swarmkit TLS加固分析
概述
swarm 集群间个节点通信,先进行身份验证,再进行加密通信。
执行命令:
docker swarm init --advertise-addr
在 ${docker-root}/ /swarm/certificates/ 目录下生成四个文件
[root@localhost certificates]# ls
swarm-node.crt swarm-node.key swarm-root-ca.crt swarm-root-ca.key
文件说明:
swarm-root-ca.crt:CA的自签名证书(执行docker swarm init 节点会启动CA(证书中心)线程,为其他节点颁发签名证书);
swarm-root-ca.key:CA的私钥;
swarm-node.crt:这个文件不是一个证书而是一个证书集合,包含:当前节点证书和证书中心CA的证书;包含CA的证书的目的是为了;
swarm-node.key:当前节点的私钥;
docker swarm (LINUX下)的证书使用pem格式,查看证书信息
[root@localhost certificates]# openssl x509 -in swarm-node.crt -inform pem -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: 24:45:68:8a:07:b7:e2:e1:2c:51:c0:be:43:da:1f:54:fe:09:4e:47 Signature Algorithm: ecdsa-with-SHA256 Issuer: CN=swarm-ca //证书颁发结构 Validity Not Before: Feb 11 00:24:00 2018 GMT Not After : May 12 01:24:00 2018 GMT Subject: O=cvp2afdvt625g5ddhds5t9ldq, OU=swarm-manager, CN=83lp2qgjndo09mgpf96k4jkom //拥有者信息 Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: 04:9e:27:3c:81:2c:ea:99:f4:0c:45:c8:96:a4:7d: 94:5f:58:14:a9:98:61:a3:31:0a:ab:16:b5:7c:31: 31:a1:57:57:60:47:71:77:64:e6:99:53:25:63:02: c0:b7:7f:1a:eb:78:92:9b:4b:94:a8:50:a2:f5:c2: 8a:1b:81:d1:77 ASN1 OID: prime256v1
由上信息,我们可以看出签名证书中保护多种信息,比如证书持有者、证书颁发者、证书的公钥的散列值等,具体证书格式和内容参考文章《OpenSSL 与 SSL 数字证书概念贴》;
一个swarm证书包含内容
//issueRenewCertificate 函数 CSR: csr, CN: node.ID, Role: node.Spec.Role, Status: api.IssuanceStatus{ State: api.IssuanceStateRenew,}
node.ID在创建证书前被 nodeID = identity.NewID() 创建并被包含在证书里面,所以docker swarm 集群里节点的nodeID是由leader节点创建。
docker swarm .key文件保留着当前节点的私钥,查看私钥
[root@localhost certificates]# cat swarm-node.key -----BEGIN EC PRIVATE KEY----- MHcCAQEEIEyyW3De4ix3KOdfv/DtxwtvykdTT9Wla7uUxlus4DjEoAoGCCqGSM49 AwEHoUQDQgAEnic8gSzqmfQMRciWpH2UX1gUqZhhozEKqxa1fDExoVdXYEdxd2Tm mVMlYwLAt38a63iSm0uUqFCi9cKKG4HRdw== -----END EC PRIVATE KEY-----
docker 加入swarm 集群,执行命令:
docker swarm join --token ${token-string} host-ip:port
命令执行成功后,在当前节点的 ${docker-root}/ /swarm/certificates/ 目录下出现三个文件
[root@localhost certificates]# ls
swarm-node.crt swarm-node.key swarm-root-ca.crt
文件说明:
swarm-root-ca.crt:从执行swarm init命令的节点下载的CA证书文件,使用来验证其他节点的签名证书的完整性、安全性
swarm-node.key:当前节点的x509私钥
swarm-node.crt:当前节点的公钥签名证书,颁发机构为 swarm-root-ca -→ 执行swarm init 的节点
docker swarm certificates 生成
执行docker swarm init
的节点证书
swarm-root-ca.crt swarm-root-ca.key
证书中心(CA)的自签名证书文件,由leader节点生成。
生成过程:
1. 执行swarm init 时,会在leader节点启动ca.server协程,响应其他节点的证书请求,颁发证书;
2. 若不适用外部CA,生成的swarm-root-ca.key会保留在本地,而swarm-root-ca.crt会分发给其他节点(其他节点join的时候),所以集群的每个node节点上的swarm-root-ca.crt都是一样的;
swarm-node.crt swarm-node.key
生成流程:
执行swarm join 节点证书
swarm-root-ca.crt
swarm-root-ca.crt文件不是当前节点生成的,从CA.server下载(默认leader节点会启动CA.server进程服务)。由于是第一次连接,没有TLS安全机制进行检测,为了保证根CA证书的安全、完整性,使用 JoinToken 散列验证证书;
swarm-node.crt swarm-node.key
这两个证书文件和leader节点的同名的两个证书生成过程类似,只不过此节点的证书请求CSR通过TLS加密通道发送个leader节点(通信前,会验证leader节点的身份),由leader节点的 Ca.server 使用私钥(swarm-root-ca.key)签名生成签名证书,再有leader节点发送给新加入节点;
swarm TLS安全加固源码分析
swarm JoinToken源码分析
Swarm JoinToken产生
JoinToken产生调用链(JoinToken由Leader产生)
/manager/manager.go: Run /ca/certificates.go: GenerateJoinToken JoinToken查询: docker swarm join-token work/manager
JoinToken使用
当我们将一个节点加入一个集群时,需执行命令:
docker swarm join --token ${token-string} host-ip:port
swarm token的作用:在加入集群前,当前节点没有swarm集群的根证书中心(root-ca)的证书,无法根据TLS安全认证验证数据的root-ca的证书完整、安全性,token的作用就是使用散列来验证根CA证书的完整、安全性;
token-string -> Digest(split(token)[2]) -> digest.NewDigestVerifier() 获取verifer算法:获取相应的hash算法;
SHA256 Algorithm = "sha256" // sha256 with hex encoding SHA384 Algorithm = "sha384" // sha384 with hex encoding SHA512 Algorithm = "sha512" // sha512 with hex encoding
根据相应的 hash 算法,获取token中的hash值于证书从新计算出的值进行对比验证。
参考代码: ca/certificates.go GetRemoteCA 函数
CA server颁发证书流程分析
client和CA server间通信
实用protobuf实现客户端和服务端通信,CA相关protobuf定义文件:/swarmkit/api/ca.proto protoc生成文件:ca.pb.go ;
CA server处理 client端证书请求
数据流:
底层grpc server收到来自客户端的protobuf格式的序列化流 -→ ca.pb.go:NodeCAServer -→ api.(*authenticatedWrapperNodeCAServer).IssueNodeCertificate -→ ca.(*Server).IssueNodeCertificate
帖上整个调用链堆栈
Feb 13 21:33:52 localhost dockerd: github.com/docker/swarmkit/ca.(*Server).IssueNodeCertificate(0xc42112a000, 0x20fde60, 0xc4217d3bc0, 0xc4217d3c20, 0x0, 0x0, 0x0) Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/ca/server.go:186 +0x409 Feb 13 21:33:52 localhost dockerd: github.com/docker/swarmkit/api.(*authenticatedWrapperNodeCAServer).IssueNodeCertificate(0xc421713da0, 0x20fde60, 0xc4217d3bc0, 0xc4217d3c20, 0x421137, 0x1550200, 0x155ab80) Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/api/ca.pb.go:131 +0x51 Feb 13 21:33:52 localhost dockerd: github.com/docker/swarmkit/api.(*raftProxyNodeCAServer).IssueNodeCertificate(0xc42112a140, 0x20fde60, 0xc4217d3bc0, 0xc4217d3c20, 0x0, 0x0, 0x0) Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/api/ca.pb.go:791 +0x9f Feb 13 21:33:52 localhost dockerd: github.com/docker/swarmkit/api._NodeCA_IssueNodeCertificate_Handler(0x155ab80, 0xc42112a140, 0x20fde60, 0xc4217d3bc0, 0xc42158a440, 0x0, 0x0, 0xc420a2268c, 0x10, 0x10) Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/api/ca.pb.go:427 +0x27d Feb 13 21:33:52 localhost dockerd: google.golang.org/grpc.(*Server).processUnaryRPC(0xc4217d6a20, 0x2101540, 0xc4210f4090, 0xc421771180, 0xc421713e40, 0x20c9e00, 0xc4217d3b90, 0x0, 0x0) Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/google.golang.org/grpc/server.go:522 +0xa14 Feb 13 21:33:52 localhost dockerd: google.golang.org/grpc.(*Server).handleStream(0xc4217d6a20, 0x2101540, 0xc4210f4090, 0xc421771180, 0xc4217d3b90) Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/google.golang.org/grpc/server.go:682 +0x6ad Feb 13 21:33:52 localhost dockerd: google.golang.org/grpc.(*Server).serveStreams.func1.1(0xc42186b0a0, 0xc4217d6a20, 0x2101540, 0xc4210f4090, 0xc421771180) Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/google.golang.org/grpc/server.go:348 +0xab Feb 13 21:33:52 localhost dockerd: created by google.golang.org/grpc.(*Server).serveStreams.func1 Feb 13 21:33:52 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/google.golang.org/grpc/server.go:349 +0xa3
CA server 在 /swarmkit/ca/server.go 的 IssueNodeCertificate 函数中真正处理证书请求
IssueNodeCertificate 函数:
1. 查看申请节点是否为集群中节点,并做相应处理
2. 若是新加入节点,为新节点创建NodeId等信息,并生成cert证书
//IssueNodeCertificate 函数片段 ...... nodeID = identity.NewID() // Create a new node err := s.store.Update(func(tx store.Tx) error { node := &api.Node { ID: nodeID, Certificate: api.Certificate { CSR: request.CSR, CN: nodeID, Role: role, Status: api.IssuanceStatus { State: api.IssuanceStatePending, }, }, Spec: api.NodeSpec{ Role: role, Membership: api.NodeMembershipAccepted, }, } return store.CreateNode(tx, node) }) ...
创建证书时,将证书状态置为api.IssuanceStatePending,并调用store.CreateNode创建节点数据库信息,创建完成后产生state.EventCreateNode事件,到现在证书还没有CA签名,当manager被选举为leader时,会创建CA server服务协程,为其他节点开启CA签名服务。(具体代码参见manager.go run函数)
CA server 主要服务代码:
//ca/server.go run ... for { select { case event := <-updates: switch v := event.(type) { case state.EventCreateNode: s.evaluateAndSignNodeCert(ctx, v.Node) case state.EventUpdateNode: // If this certificate is already at a final state // no need to evaluate and sign it. if !isFinalState(v.Node.Certificate.Status) { s.evaluateAndSignNodeCert(ctx, v.Node) } case state.EventUpdateCluster: s.updateCluster(ctx, v.Cluster) } case <-ctx.Done(): return ctx.Err() case <-s.ctx.Done(): return nil } }
CA server服务检查三个事件:
state.EventCreateNode state.EventUpdateNode state.EventUpdateCluster
在 IssueNodeCertificate 函数,node信息创建完成,发送 state.EventCreateNode 事件,被CA server检测到,调用evaluateAndSignNodeCert函数完成证书签名;
新节点加入集群证书申请整体流程:
CA rpc server 收到新节点CSR(证书请求,保护公钥) -→ 处理请求为新节点生成NODE信息,并附加到证书 -→ CA server检测到新节点的创建,使用根CA的私钥为证书签名;
新节点加入swarm集群时证书申请
签名证书申请主要函数ca/certificates.go GetRemoteSignedCertificate,首先看看此函数的调用链
Feb 14 15:14:00 localhost dockerd: github.com/docker/swarmkit/ca.GetRemoteSignedCertificate(0x20fde60, 0xc420f5d200, 0xc4209fd500, 0x146, 0x1c0, 0xc420222ae0, 0x55, 0xc420f709f0, 0xc42016fd40, 0x0, ...) Feb 14 15:14:00 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/ca/certificates.go:604 +0x95 Feb 14 15:14:00 localhost dockerd: github.com/docker/swarmkit/ca.(*RootCA).RequestAndSaveNewCertificates(0xc420059020, 0x20fde60, 0xc420f5d200, 0xc420f5d3b0, 0x2e, 0xc420f5d410, 0x2e, 0xc420222ae0, 0x55, 0xc42016fd40, ...) Feb 14 15:14:00 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/ca/certificates.go:174 +0x1d4 Feb 14 15:14:00 localhost dockerd: github.com/docker/swarmkit/ca.LoadOrCreateSecurityConfig(0x20fde60, 0xc420f5d200, 0xc420192100, 0x1f, 0xc420222ae0, 0x55, 0x1720611, 0xd, 0xc42016fd40, 0xc420058fc0, ...) Feb 14 15:14:00 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/ca/config.go:270 +0x80f Feb 14 15:14:00 localhost dockerd: github.com/docker/swarmkit/agent.(*Node).run(0xc420a9de00, 0x20fdde0, 0xc42000c668, 0x0, 0x0) Feb 14 15:14:00 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/agent/node.go:216 +0x56a Feb 14 15:14:00 localhost dockerd: created by github.com/docker/swarmkit/agent.(*Node).Start Feb 14 15:14:00 localhost dockerd: /root/dockerbuild/BUILD/docker-engine/vendor/src/github.com/docker/swarmkit/agent/node.go:158 +0x3d8
ca.LoadOrCreateSecurityConfig函数首先在本地找签名证书,如果在本地没有签名证书调用RequestAndSaveNewCertificates函数生成、获取证书。调用GetRemoteSignedCertificate函数前,先生成包含公钥的证书请求(CSR)和私钥。
ca/certificates.go GetRemoteSignedCertificate 函数将CSR封装于api.IssueNodeCertificateRequest请求,使用api.NewNodeCAClien(protobuf相关)和Ca.Server通信,并在未超时时间内等待签名证书返回。(具体细节参考GetRemoteSignedCertificate 函数)
docker swarm 节点间MutableTLS认证、通信
在SSL/TSL协议中,有两种认证通信方式:单向认证(TLS)和双向认证(Mutual TLS),具体协议认证过程参考《An Introduction to Mutual SSL Authentication》。docker swarm 集群两节点间需互相验证身份,所以使用Mutual TLS方式认证身份。
docker swarm MTLS认证、通信流图图:
SSL/TLS 在TCP/UDP上应用层下,SSL/TLS协议代码被封装于golang的crypto包中,所以只分析在swarm代码中,如何使用MTLS进行加密通信,不涉及SSL/TLS协议代码。
几个验证过程注意:
1. 在新节点建立从CA server获取根CA的自签名证书时,由于没有CA证书无法进行认证,所以这次通信是没有使用TLS认证授权的,下载文件swarm-root-ca.cert使用token散列验证;
2. 新加入节点,向CA server 请求证书,此时新节点没有CA签名证书,无法和server进行双向认证,这个过程使用单向认证 –→ 客户端验证CA server 身份;
3. 上述情况的外,都使用双向认证(MTLS);
获取根CA证书代码
//GetRemoteCA insecureCreds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}) //不使用TLS验证 opts := []grpc.DialOption { grpc.WithTransportCredentials(insecureCreds), grpc.WithBackoffMaxDelay(10 * time.Second), grpc.WithPicker(picker) } firstAddr, err := picker.PickAddr() if err != nil { return RootCA{}, err } conn, err := grpc.Dial(firstAddr, opts...)
获取节点签名证书
//GetRemoteSignedCertificate if creds == nil { //请求签名证书时,此为nil creds = credentials.NewTLS(&tls.Config{ServerName: CARole, RootCAs: rootCAPool})//只验证CARole身份 } opts := []grpc.DialOption{ grpc.WithTransportCredentials(creds), grpc.WithBackoffMaxDelay(10 * time.Second), grpc.WithPicker(picker) } firstAddr, err := picker.PickAddr() if err != nil { return nil, err } conn, err := grpc.Dial(firstAddr, opts...)
其他情况双向认证
LoadOrCreateSecurityConfig函数:
//LoadOrCreateSecurityConfig clientTLSCreds, serverTLSCreds, err = LoadTLSCreds(rootCA, paths.Node) //加载磁盘中证书和私钥 if err != nil { //磁盘没有证书包错(一般节点第一次加入或者创建集群时),加载失败 log.Debugf("no valid local TLS credentials found: %v", err) var ( tlsKeyPair *tls.Certificate err error ) //Leader节点,自己为自己创建节点信息,并使用为自己创建证书并用CA签名 if rootCA.CanSign() { //Leader节点才可以启动签名证书服务,所以Leader节点此处为真 // Create a new random ID for this certificate cn := identity.NewID() org := identity.NewID() if nodeInfo != nil { nodeInfo <- api.IssueNodeCertificateResponse{ NodeID: cn, NodeMembership: api.NodeMembershipAccepted, } } tlsKeyPair, err = rootCA.IssueAndSaveNewCertificates(paths.Node, cn, proposedRole, org) //创建并签名证书 if err != nil { return nil, err } } else { //非Leader节点,创建证书请求并向Leader 或CA 服务请求签名证书 // There was an error loading our Credentials, let's get a new certificate issued // Last argument is nil because at this point we don't have any valid TLS creds tlsKeyPair, err = rootCA.RequestAndSaveNewCertificates(ctx, paths.Node, token, picker, nil, nodeInfo) if err != nil { return nil, err } } // Create the Server TLS Credentials for this node. These will not be used by agents. serverTLSCreds, err = rootCA.NewServerTLSCredentials(tlsKeyPair) //客户端TLS连接配置 if err != nil { return nil, err } // Create a TLSConfig to be used when this node connects as a client to another remote node. // We're using ManagerRole as remote serverName for TLS host verification clientTLSCreds, err = rootCA.NewClientTLSCredentials(tlsKeyPair, ManagerRole) //服务端TLS连接配置 if err != nil { return nil, err } log.Debugf("new TLS credentials generated: %s.", paths.Node.Cert) } else { if nodeInfo != nil { nodeInfo <- api.IssueNodeCertificateResponse{ NodeID: clientTLSCreds.NodeID(), NodeMembership: api.NodeMembershipAccepted, } } log.Debugf("loaded local TLS credentials: %s.", paths.Node.Cert) } return NewSecurityConfig(&rootCA, clientTLSCreds, serverTLSCreds), nil //LTS连接配置
LoadOrCreateSecurityConfig函数返回了TLS安全双向认证的连接配置,此后使用安全连接配置进行安全通行;
例如
//agent/node.go run函数 ... securityConfig, err := ca.LoadOrCreateSecurityConfig(ctx, certDir, n.config.JoinToken, ca.ManagerRole, picker.NewPicker(n.remotes), issueResponseChan) ... managerErr = n.runManager(ctx, securityConfig, managerReady) // 使用securityConfig安全配置 ... agentErr = n.runAgent(ctx, db, securityConfig.ClientTLSCreds, agentReady) //使用securityConfig安全配置
由于manager既要做服务端又要做客户端,所以服务端和客户端的双向安全配置;
NewClientTLSCredentials、NewServerTLSCredentials 底层调用 golang crypto 包里面tls安全认证类。