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进行对公钥和其他一些内容进行数字签名的信件或消息。

证书常用扩展名

  1. .crt 证书文件 ,可以是DER(二进制)编码的,也可以是PEM( ASCII (Base64) )编码的 ,在类unix系统中比较常见
  2. .cer 也是证书 常见于Windows系统 编码类型同样可以是DER或者PEM的,windows 下有工具可以转换crt到cer
  3. .csr 证书签名请求 一般是生成请求以后发送给CA,然后CA会给你签名并发回证书.key 一般公钥或者密钥都会用这种扩展名,可以是DER编码的或者是PEM编码的 查看DER编码的(公钥或者密钥)的文件的命令为 openssl rsa -inform DER -noout -text -in xxx
  4. .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安全认证类;

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页