How Istio works: MTLS

image

今天我们来瞧瞧 Istio 是如何实现 mTLS 的。

Istio 中的双向TLS(Mutual TLS,简称 mTLS)是一种保证服务间通信安全的机制,它确保通信的双方都可以验证对方的身份,从而防止未授权的信息访问或数据泄露。Istio 的 mTLS 流程大体如下:

  • 证书管理:当 Istio 部署在 Kubernetes 集群上时,Istio 的控制平面组件 Citadel(Istio 1.5之后变更为Istio CA)负责为每个服务的 sidecar 代理生成和管理 TLS 证书。
  • 证书分发:生成的证书和私钥通过安全通道分发到每个服务的 sidecar 代理(Envoy)。证书通常具有较短的有效期,Istio 会自动进行证书的轮换。
  • 服务发现:服务启动时,sidecar 代理会从 Pilot 获取服务的信息,包括其他服务的位置和是否要求 mTLS。
  • 建立连接:当一个服务(sidecar 代理)尝试与另一个服务通信时,客户端和服务器代理会通过 TLS 握手来相互验证对方的证书。
  • 密钥交换:在 TLS 握手过程中,将使用证书进行密钥交换,确立后续通信的加密方式。
  • 双向认证:由于是 mTLS,因此客户端和服务器都会验证对方的证书,并确认对方的合法性。
  • 加密通信:一旦握手成功,并且 TLS 通道建立起来,之后所有的通信都将是加密的。
  • 流量控制和管理:在 TLS 加密的基础上,Pilot 提供了一致性的流量管理规则,用于控制和管理服务间的交互。
    Istio 的 mTLS 实现确保服务间的通信安全并且透明,服务不需要修改自己的代码就能够享受这一安全机制。管理员可以通过Istio的策略定义和管理mTLS的启用范围和行为,可以在全局范围启用,或者细化到特定的命名空间或服务。随着Istio版本的更新,Istio中mTLS的配置和行为细节可能会有变化。

以上内容来自于 ChatGPT,不过看起来 ChatGPT 的理解还是有一些问题的,所以我们还是需要自己理解一下。

秘钥管理

服务端

这里分为服务端和客户端两部分的逻辑,服务端的逻辑在

startCAgithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// StartCA starts the CA or RA server if configured.
func (s *Server) startCA(caOpts *caOptions) {
if s.CA == nil && s.RA == nil {
return
}
s.addStartFunc("ca", func(stop <-chan struct{}) error {
grpcServer := s.secureGrpcServer
if s.secureGrpcServer == nil {
grpcServer = s.grpcServer
}
// Start the RA server if configured, else start the CA server
if s.RA != nil {
log.Infof("Starting RA")
s.RunCA(grpcServer, s.RA, caOpts)
} else if s.CA != nil {
log.Infof("Starting IstioD CA")
s.RunCA(grpcServer, s.CA, caOpts)
}
return nil
})
}

但是 Istio 的初始化绕来绕去的,我们直接定位 GRPC 的处理函数会比较快。

CreateCertificategithub
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
func (s *CAServer) CreateCertificate(ctx context.Context, request *pb.IstioCertificateRequest) (
*pb.IstioCertificateResponse, error,
) {
caServerLog.Infof("received CSR request")
if s.shouldReject() {
caServerLog.Info("force rejecting CSR request")
return nil, status.Error(codes.Unavailable, "CA server is not available")
}
if s.sendEmpty() {
caServerLog.Info("force sending empty cert chain in CSR response")
response := &pb.IstioCertificateResponse{
CertChain: []string{},
}
return response, nil
}
id := []string{"client-identity"}
if len(s.Authenticators) > 0 {
caller, err := security.Authenticate(ctx, s.Authenticators)
if caller == nil || err != nil {
return nil, status.Error(codes.Unauthenticated, "request authenticate failure")
}
id = caller.Identities
}
cert, err := s.sign([]byte(request.Csr), id, time.Duration(request.ValidityDuration)*time.Second, false)
if err != nil {
caServerLog.Errorf("failed to sign CSR: %+v", err)
return nil, status.Errorf(err.(*caerror.Error).HTTPErrorCode(), "CSR signing error: %+v", err.(*caerror.Error))
}
respCertChain := []string{string(cert)}
respCertChain = append(respCertChain, string(s.certPem))
response := &pb.IstioCertificateResponse{
CertChain: respCertChain,
}
caServerLog.Info("send back CSR success response")
return response, nil
}

对于服务端来说,处理起来是比较简单的,我们只需要关注 CreateCertificate 函数的逻辑即可。这里其实只有2个步骤

  1. 通过 Authenticators 来判断来源是否合法,以及其 Identities 是什么(这里实际上在运行环境中多是 K8s Token 的 Authenticator)
  2. 然后调用 sign 函数来生成数字证书,同时返回自己的公钥给客户端

忘记的同学可以看这里

image

服务端的工作就到这里了,我们来看看客户端的逻辑。

客户端

客户端的逻辑,对 istio 架构有一定了解的话,很容易了解到就是在 agent 里面,我们通过调用链就很容易定位到如下位置

GenerateSecretgithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (sc *SecretManagerClient) GenerateSecret(resourceName string) (secret *security.SecretItem, err error) {
cacheLog.Debugf("generate secret %q", resourceName)
// Setup the call to store generated secret to disk
defer func() {
// 生成成功之后,需要写成 secret
}()

// 中间忽略一些内容

// 发送 CSR 给 CA
// send request to CA to get new workload certificate
ns, err = sc.generateNewSecret(resourceName)
if err != nil {
return nil, fmt.Errorf("failed to generate workload certificate: %v", err)
}

// Store the new secret in the secretCache and trigger the periodic rotation for workload certificate
// 将申请的到的储存起来
sc.registerSecret(*ns)

return ns, nil
}

那么申请的逻辑在 generateNewSecret 函数中

generateNewSecretgithub
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
func (sc *SecretManagerClient) generateNewSecret(resourceName string) (*security.SecretItem, error) {
trustBundlePEM := []string{}
var rootCertPEM []byte

if sc.caClient == nil {
return nil, fmt.Errorf("attempted to fetch secret, but ca client is nil")
}
t0 := time.Now()
logPrefix := cacheLogPrefix(resourceName)

// 生成 TLS 中的 信息
csrHostName := &spiffe.Identity{
TrustDomain: sc.configOptions.TrustDomain,
Namespace: sc.configOptions.WorkloadNamespace,
ServiceAccount: sc.configOptions.ServiceAccount,
}

cacheLog.Debugf("constructed host name for CSR: %s", csrHostName.String())
options := pkiutil.CertOptions{
Host: csrHostName.String(),
RSAKeySize: sc.configOptions.WorkloadRSAKeySize,
PKCS8Key: sc.configOptions.Pkcs8Keys,
ECSigAlg: pkiutil.SupportedECSignatureAlgorithms(sc.configOptions.ECCSigAlg),
ECCCurve: pkiutil.SupportedEllipticCurves(sc.configOptions.ECCCurve),
}

// Generate the cert/key, send CSR to CA.
// 生成 CSR 请求
csrPEM, keyPEM, err := pkiutil.GenCSR(options)
if err != nil {
cacheLog.Errorf("%s failed to generate key and certificate for CSR: %v", logPrefix, err)
return nil, err
}

numOutgoingRequests.With(RequestType.Value(monitoring.CSR)).Increment()
timeBeforeCSR := time.Now()

// 发送 CSR 请求给上面的 CA 服务器,从而获取到证书
certChainPEM, err := sc.caClient.CSRSign(csrPEM, int64(sc.configOptions.SecretTTL.Seconds()))
if err == nil {
trustBundlePEM, err = sc.caClient.GetRootCertBundle()
}
csrLatency := float64(time.Since(timeBeforeCSR).Nanoseconds()) / float64(time.Millisecond)
outgoingLatency.With(RequestType.Value(monitoring.CSR)).Record(csrLatency)
if err != nil {
numFailedOutgoingRequests.With(RequestType.Value(monitoring.CSR)).Increment()
return nil, err
}

// 生成证书链
certChain := concatCerts(certChainPEM)

cacheLog.WithLabels("latency", time.Since(t0), "ttl", time.Until(expireTime)).Info("generated new workload certificate")

if len(trustBundlePEM) > 0 {
rootCertPEM = concatCerts(trustBundlePEM)
} else {
// If CA Client has no explicit mechanism to retrieve CA root, infer it from the root of the certChain
rootCertPEM = []byte(certChainPEM[len(certChainPEM)-1])
}

return &security.SecretItem{
CertificateChain: certChain,
PrivateKey: keyPEM,
ResourceName: resourceName,
CreatedTime: time.Now(),
ExpireTime: expireTime,
RootCert: rootCertPEM,
}, nil
}

SDS 服务

对于 agent 组件来说,还有一个重要的职能就是承担对于 Envoy 作为一个 SDS 服务提供方。这部分在最终的 MTLS 通讯中需要使用到。

起始点在

NewServergithub
1
2
3
4
5
6
7
8
9
10
11
// NewServer creates and starts the Grpc server for SDS.
func NewServer(options *security.Options, workloadSecretCache security.SecretManager, pkpConf *mesh.PrivateKeyProvider) *Server {
s := &Server{stopped: atomic.NewBool(false)}
s.workloadSds = newSDSService(workloadSecretCache, options, pkpConf)
s.initWorkloadSdsService()
return s
}


// 监听了一个 UDS 的 Socket
s.grpcWorkloadListener, err = uds.NewListener(security.WorkloadIdentitySocketPath)

这个 SDS 服务的主体逻辑如下

Generategithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Generate implements the XDS Generator interface. This allows the XDS server to dispatch requests
// for SecretTypeV3 to our server to generate the Envoy response.
func (s *sdsservice) Generate(proxy *model.Proxy, w *model.WatchedResource, updates *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
// updates.Full indicates we should do a complete push of all updated resources
// In practice, all pushes should be incremental (ie, if the `default` cert changes we won't push
// all file certs).
if updates.Full {
resp, err := s.generate(w.ResourceNames)
return resp, pushLog(w.ResourceNames), err
}
names := []string{}
watched := sets.New(w.ResourceNames...)
for i := range updates.ConfigsUpdated {
if i.Kind == kind.Secret && watched.Contains(i.Name) {
names = append(names, i.Name)
}
}
resp, err := s.generate(names)
return resp, pushLog(names), err
}

这里的逻辑也非常的简单易懂,就是根据来源的 ResourceName 生成证书并且返回。这里调用的就是 SecretManagerClientGenerateSecret 函数。不过有一些特殊的处理,从 Envoy 来的 SDS 请求只可能请求

  • ROOTCA: 验证 upstream 的证书是否合法
  • default: 自身的证书

所以需要额外通过 spiffe 来判断身份。

MTLS

目标匹配

在生成的 CDS 信息中

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
{
"transport_socket_matches":
[
{
"name": "tlsMode-istio",
"match":
{
"tlsMode": "istio"
},
"transport_socket":
{
"name": "envoy.transport_sockets.tls",
"typed_config":
{
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"common_tls_context":
{
"tls_params":
{
"tls_minimum_protocol_version": "TLSv1_2",
"tls_maximum_protocol_version": "TLSv1_3"
},
"alpn_protocols":
[
"istio-peer-exchange",
"istio"
], // 自身的 TLS 配置,从 SDS 中来,使用 default 来请求
"tls_certificate_sds_secret_configs":
[
{
"name": "default",
"sds_config":
{
"api_config_source":
{
"api_type": "GRPC",
"grpc_services":
[
{
"envoy_grpc":
{
"cluster_name": "sds-grpc"
}
}
],
"set_node_on_first_message_only": true,
"transport_api_version": "V3"
},
"initial_fetch_timeout": "0s",
"resource_api_version": "V3"
}
}
],
"combined_validation_context":
{
"default_validation_context":
{
"match_subject_alt_names":
[
{
"exact": "spiffe://cluster.local/ns/default/sa/default"
}
]
},
// 对端的 TLS 验证信息,从 SDS 中来,使用 ROOTCA 来请求,其实就是拿 CA 公钥
"validation_context_sds_secret_config":
{
"name": "ROOTCA",
"sds_config":
{
"api_config_source":
{
"api_type": "GRPC",
"grpc_services":
[
{
"envoy_grpc":
{
"cluster_name": "sds-grpc"
}
}
],
"set_node_on_first_message_only": true,
"transport_api_version": "V3"
},
"initial_fetch_timeout": "0s",
"resource_api_version": "V3"
}
}
}
},
"sni": "outbound_.80_._.echo-web.default.svc.cluster.local"
}
}
},
{
"name": "tlsMode-disabled",
"match":
{},
"transport_socket":
{
"name": "envoy.transport_sockets.raw_buffer",
"typed_config":
{
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer"
}
}
}
]
}

当访问的对端能够匹配 “tlsMode”: “istio” 的时候,就会通过 TLS 的路径进行访问。

证书匹配

在配置中还包含了如下内容

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
"combined_validation_context":
{
"default_validation_context":
{
"match_subject_alt_names":
[
{
"exact": "spiffe://cluster.local/ns/default/sa/default"
}
]
},
"validation_context_sds_secret_config":
{
"name": "ROOTCA",
"sds_config":
{
"api_config_source":
{
"api_type": "GRPC",
"grpc_services":
[
{
"envoy_grpc":
{
"cluster_name": "sds-grpc"
}
}
],
"set_node_on_first_message_only": true,
"transport_api_version": "V3"
},
"initial_fetch_timeout": "0s",
"resource_api_version": "V3"
}
}
}

这里包含了2个部分

  1. match_subject_alt_names: 验证对端的 spiffe 名称
  2. validation_context_sds_secret_config: 证书

对于下游的 TLS 请求的证书校验是委托给 sds-grpc 这个服务执行的

这就是上面我们所谈及的 UDS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"name": "sds-grpc",
"type": "STATIC",
"connect_timeout": "1s",
"load_assignment": {
"cluster_name": "sds-grpc",
"endpoints": [
{
"lb_endpoints": [
{
"endpoint": {
"address": {
"pipe": {
"path": "./var/run/secrets/workload-spiffe-uds/socket"
}
}
}
}
]
}
]
}

总结一下

mtls