[istio源码分析][citadel] citadel之istio_ca

1. 前言

转载请说明原文出处, 尊重他人劳动成果!

源码位置: https://github.com/nicktming/istio
分支: tming-v1.3.6 (基于1.3.6版本)

本文将分析istio中的负责证书方面事情的组件citadel, 源码位置在istio/security, 具体将分析citadelistio_ca的作用.

对证书https不了解的可以参考 https://www.cnblogs.com/pzblog/p/9088286.html.

2. 初识istio_ca

[root@master istio_ca]# pwd
/root/go/src/istio.io/istio/security/cmd/istio_ca
[root@master istio_ca]# ls
istio_ca  main.go  run.sh
[root@master istio_ca]# kubectl get sa --all-namespaces
NAMESPACE         NAME                   SECRETS   AGE
default           bookinfo-details       1         14d
default           bookinfo-productpage   1         14d
default           bookinfo-ratings       1         14d
default           bookinfo-reviews       1         14d
default           default                1         39d
istio-system      default                1         15d
kube-node-lease   default                1         23d
kube-public       default                1         39d
kube-system       coredns                1         39d
kube-system       default                1         39d
tming             default                1         15d
[root@master istio_ca]# kubectl get cm -n istio-system
NAME                     DATA   AGE
istio                    2      15d
istio-sidecar-injector   2      15d
[root@master istio_ca]# cat run.sh 
#!/usr/bin/env bash
export CITADEL_ENABLE_NAMESPACES_BY_DEFAULT=true
config=""
config="$config --append-dns-names=true"
config="$config --grpc-port=8060"
config="$config --citadel-storage-namespace=istio-system"
config="$config --custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system"
config="$config --monitoring-port=15014"
config="$config --self-signed-ca=true"
config="$config --workload-cert-ttl=2160h"
./istio_ca $config
unset CITADEL_ENABLE_NAMESPACES_BY_DEFAULT 

[root@master istio_ca]# ./run.sh 
2020-02-04T08:02:23.012518Z     info    The custom-defined DNS name list is [istio-pilot-service-account.istio-system:istio-pilot.istio-system]
2020-02-04T08:02:23.018847Z     info    Use self-signed certificate as the CA certificate
2020-02-04T08:02:23.019753Z     info    ControlZ available at 127.0.0.1:9876
2020-02-04T08:02:23.027905Z     info    Failed to get secret (error: secrets "istio-ca-secret" not found), will create one
2020-02-04T08:02:23.495396Z     info    Using self-generated public key: -----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
2020-02-04T08:02:23.502515Z     info    The Citadel's public key is successfully written into configmap istio-security in namespace istio-system.
2020-02-04T08:02:23.502545Z     info    rootCertRotator Set up back off time 2m21s to start rotator.
2020-02-04T08:02:23.502558Z     info    Creating Kubernetes controller to write issued keys and certs into secret ...
2020-02-04T08:02:23.502619Z     info    rootCertRotator Jitter is enabled, wait 2m21s before starting root cert rotator.
2020-02-04T08:02:23.602779Z     info    adding registry entry "k8s.cluster.local" -> "k8s.cluster.local"
2020-02-04T08:02:23.602831Z     info    added client certificate authenticator
2020-02-04T08:02:23.603288Z     info    Citadel monitor has started.
2020-02-04T08:02:23.603298Z     info    Citadel has started
2020-02-04T08:02:23.603332Z     info    monitor Monitor server started.
2020-02-04T08:02:23.604169Z     info    Starting GRPC server on port 8060
2020-02-04T08:02:23.610077Z     info    adding registry entry "spiffe://cluster.local/ns/default/sa/bookinfo-details" -> "spiffe://cluster.local/ns/default/sa/bookinfo-details"
...
2020-02-04T08:02:23.786537Z     info    k8sController   Secret tming/istio.default is created successfully
...

查看所有secret, 可以每个serviceaccount都额外增加了一个类型为istio.io/key-and-certsecret, 名字就是istio.加上serviceaccount的名字.

[root@master ~]# kubectl get secret --all-namespaces
NAMESPACE         NAME                                           TYPE                                  DATA   AGE
...
default           istio.bookinfo-details                         istio.io/key-and-cert                 3      98s
default           istio.bookinfo-productpage                     istio.io/key-and-cert                 3      96s
default           istio.bookinfo-ratings                         istio.io/key-and-cert                 3      96s
default           istio.bookinfo-reviews                         istio.io/key-and-cert                 3      97s
default           istio.default                                  istio.io/key-and-cert                 3      39d
istio-system      istio.default                                  istio.io/key-and-cert                 3      95s
kube-node-lease   istio.default                                  istio.io/key-and-cert                 3      23d
kube-public       istio.default                                  istio.io/key-and-cert                 3      39d
kube-system       istio.coredns                                  istio.io/key-and-cert                 3      39d
kube-system       istio.default                                  istio.io/key-and-cert                 3      39d
tming             istio.default                                  istio.io/key-and-cert                 3      98s
...
[root@master ~]# 

3. pki

pki的地址在security/pki.

3.1 NewIstioCA

func NewSelfSignedIstioCAOptions(ctx context.Context,
    rootCertGracePeriodPercentile int, caCertTTL, rootCertCheckInverval, certTTL,
    maxCertTTL time.Duration, org string, dualUse bool, namespace string,
    readCertRetryInterval time.Duration, client corev1.CoreV1Interface, rootCertFile string,
    enableJitter bool) (caOpts *IstioCAOptions, err error) {
    // 查看istio-system这个namespaces中是否存在istio-ca-secret这个secret
    caSecret, scrtErr := client.Secrets(namespace).Get(CASecret, metav1.GetOptions{})
    if scrtErr != nil && readCertRetryInterval > time.Duration(0) {
        // 重试一次
    }
    caOpts = &IstioCAOptions{
        ...
    }
    if scrtErr != nil {
        // 如果k8s中不存在 就自己生成key和cert 
        options := util.CertOptions{
            ...
        }
        // 用x509生成key和证书cert
        pemCert, pemKey, ckErr := util.GenCertKeyFromOptions(options)
        if ckErr != nil {
            return nil, fmt.Errorf("unable to generate CA cert and key for self-signed CA (%v)", ckErr)
        }
        // 把证书cert添加到rootCerts
        rootCerts, err := util.AppendRootCerts(pemCert, rootCertFile)
        if err != nil {
            return nil, fmt.Errorf("failed to append root certificates (%v)", err)
        }
        // 利用pemCert, pemKey, rootCerts 填充caOpts.KeyCertBundle
        if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(pemCert, pemKey, nil, rootCerts); err != nil {
            return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
        }
        // 在istio-system namespace中生成一个类型为istio.io/ca-root的secret
        // secret里面的数据是 根证书和根key
        secret := k8ssecret.BuildSecret("", CASecret, namespace, nil, nil, nil, pemCert, pemKey, istioCASecretType)
        if _, err = client.Secrets(namespace).Create(secret); err != nil {
            log.Errorf("Failed to write secret to CA (error: %s). Abort.", err)
            return nil, fmt.Errorf("failed to create CA due to secret write error")
        }
        log.Infof("Using self-generated public key: %v", string(rootCerts))
    } else {
        // 从已有的secret中读取
        log.Infof("Load signing key and cert from existing secret %s:%s", caSecret.Namespace, caSecret.Name)
        rootCerts, err := util.AppendRootCerts(caSecret.Data[caCertID], rootCertFile)
        if err != nil {
            return nil, fmt.Errorf("failed to append root certificates (%v)", err)
        }
        if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(caSecret.Data[caCertID],
            caSecret.Data[caPrivateKeyID], nil, rootCerts); err != nil {
            return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
        }
        log.Infof("Using existing public key: %v", string(rootCerts))
    }
    // 将根证书保存到configmap中 名字为istio-system/istio-security
    if err = updateCertInConfigmap(namespace, client, caOpts.KeyCertBundle.GetRootCertPem()); err != nil {
        log.Errorf("Failed to write Citadel cert to configmap (%v). Node agents will not be able to connect.", err)
    } else {
        log.Infof("The Citadel's public key is successfully written into configmap istio-security in namespace %s.", namespace)
    }
    return caOpts, nil
}

1. 查看istio-system namespace中是否存在istio-ca-secret这个secret.
2. 如果不存在, 则会自己生成certkey, 该certkey会用于签名那些所有的secret的. 并且存到istio-ca-secret中.
3. 如果存在, 则直接从已有的secret中读取.
4. 无论存在不存在, 都需要根据pemCert, pemKey, rootCerts 填充caOpts.KeyCertBundle.
5. 将根证书保存到configmap中, 名字为istio-system/istio-security.

// security/pkg/pki/ca/ca.go
func updateCertInConfigmap(namespace string, client corev1.CoreV1Interface, cert []byte) error {
    certEncoded := base64.StdEncoding.EncodeToString(cert)
    cmc := configmap.NewController(namespace, client)
    return cmc.InsertCATLSRootCert(certEncoded)
}
// security/pkg/k8s/configmap/configmap.go
func (c *Controller) InsertCATLSRootCert(value string) error {
    configmap, err := c.core.ConfigMaps(c.namespace).Get(istioSecurityConfigMapName, metav1.GetOptions{})
    // 更新或生成该configmap
    return nil
}

查看istio-ca-secretistio-security.

[root@master ~]# kubectl get secret istio-ca-secret -o yaml -n istio-system
apiVersion: v1
data:
  ca-cert.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMzVENDQWNXZ0F3SUJBZ0lRY005Q2w4OCttYThzTERnWFNFaGF3akFOQmdrcWhraUc5dzBCQVFzRkFEQVkKTVJZd0ZBWURWUVFLRXcxamJIVnpkR1Z5TG14dlkyRnNNQjRYRFRJd01ESXdOREE0TURJeU0xb1hEVE13TURJdwpNVEE0TURJeU0xb3dHREVXTUJRR0ExVUVDaE1OWTJ4MWMzUmxjaTVzYjJOaGJEQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKOVZrLythRXV1bHhvb1F1bzNSUnlrWUpuME9udlViZ1JyRHJXWEYKekxKYVZDVUthY1lxTE5vSjBXKzFES1dyZ05weFBKcE8vUUtQeGxOdnJtK004cllVOHc0NWI5VGFLdTNxSGwvdgp1UmkrSnJZT3ZWcGlZY3A4SEc2cnI5UWtIcGJjcm1OT2VOQ1lVZGdXQ2VkMW5tWjZjZGcrT0RIYkFzVkloTmRiCjBHNDc4RU1RM3FOaWFTdGVIRXJMSDRlU045Z2VVbkhmY2hCTVFnQXN4cjJza01wUEw2VWJSMHl2dzVXM3dpWngKbWpaN2ZUYURBcy9vZ3hTV1N6LzE2eG1lVzYvbEx4N1JQNVd6ZTlkWnBPOE9xUW5BV3dlL2puTTNrQ0lNbksrUgpPTVlBRjBiU1VLKzZiT004bmZVanM4RUVJdEJvdUluYmNwNFplZkhvSFlaNk10RUNBd0VBQWFNak1DRXdEZ1lEClZSMFBBUUgvQkFRREFnSUVNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUIKQUcxdEU3UnFCTHpzd3ovTStuYndoeFR2VE14eHJ2RDYxVDc1eTVPRUMxUnk4RmQxbnYyT09vR0UybjUxV0N4VAp0YzFERzAvWUNrbko1a3grWEo5TElDbkdkcHFxR2VnQkNrR1RlWmpPMlk0Zko5dEZ3SWdhbVQwYmNUVjJ3NmUwClVRdUxuR1hxaytVdUFiOHFHWUZObGVlMk1LOFV5aEM0SGlEN1Z2c2xXcUdmazdTOHNtWmR0cGUyMkY2RThrbDQKSFlYNHh4RWQyRTZxVlYxbFZsMTV1TGo2V3R3Z1V0dmZMR3RvMnlyOHN3cExETHZ4SVprdWd6azc1OE1LbUljNwowOEV3bUdNVnVvUUhEcnA4NDU1NkNwZGl2NDY0dUdWeW9PaDU1WWlPcGxidWtWQzZTRE9QbTlueTRRTThRV1ZKCmYyYkRXQkpoSmRXb1FXVElPWm1wek5vPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
  ca-key.pem: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBbjFXVC81b1M2NlhHaWhDNmpkRkhLUmdtZlE2ZTlSdUJHc090WmNYTXNscFVKUXBwCnhpb3MyZ25SYjdVTXBhdUEybkU4bWs3OUFvL0dVMit1YjR6eXRoVHpEamx2MU5vcTdlb2VYKys1R0w0bXRnNjkKV21KaHlud2NicXV2MUNRZWx0eXVZMDU0MEpoUjJCWUo1M1dlWm5weDJENDRNZHNDeFVpRTExdlFianZ3UXhEZQpvMkpwSzE0Y1Nzc2ZoNUkzMkI1U2NkOXlFRXhDQUN6R3ZheVF5azh2cFJ0SFRLL0RsYmZDSm5HYU5udDlOb01DCnoraURGSlpMUC9YckdaNWJyK1V2SHRFL2xiTjcxMW1rN3c2cENjQmJCNytPY3plUUlneWNyNUU0eGdBWFJ0SlEKcjdwczR6eWQ5U096d1FRaTBHaTRpZHR5bmhsNThlZ2Robm95MFFJREFRQUJBb0lCQUdHYzloeURjYy80TVpmbwpBOEphVWZRMUhXOUVBOUk1MVhCbUxOYkt4VXNHMThJUmpSZWdRdllaU2J2YitUR056bFVGUnBGcWpzcUE5b21yClEveUhKekt4eHU0UjloYzZ5VTRVUGlPY0k1T3ErdUJTUzJNU0hzTUVJZzhURTVjdHdhZSs3djliMWR4RlZPN0QKSWJJeGRxZGxvRlZRV1BFQ01jSlhXVHJ1dnRTbzRFeWQ5NlFzV0w5RFBvRkRiU0xULzJKM3lQTFJlUEhQK25mbQpiTEJZNStGcFI0TElGSHpjZ1VtdWhHdFA2QkQyTnM5VERLSWJINjd1OUZ5S05Ec0laRmZEZkY5NDJIbzNYUnhkCkpoaHpKdy9Kbkl2M0pGbzM2ekwxWHVjd0hkNnVIYlJoMGw5eER3Y3Z6MlFmYm9BN2hlSlAvUllRb3VLVGl1UHAKMVVWbXVaa0NnWUVBeElQYUNTRjQzMUNjQjlYNHpJRDhCaFBQY3E3K2cvbjZEMThiY1NWK2M1eHhGYytKU1FvbwpIRC9MZnJUWlNEa3lCbmV6eU5zbXRzY1pXTGpGblRZWFFaZDZVUkl4dUw3anU1emI3N2YwMWVMS1RWT3IwUk81CjlZMEF1UkEyaEp4Q0RObVdmYm5JcUxzTUJBOUhUNGE5T3BGME1KVi9DaGpPcElBSEdKMHV3L2NDZ1lFQXo1Q04KWWFQbzNwaGlFZnFyUElEMlRtUjdFNnZveGkxc0VmVjMvRk1EMEVUTkpidE5xL09KWElyOGFEV1lDeThMVDZteApXU3FDVG1tdDEvNTREa3hzVnM2NVJGUVVIc1pKSktkVVNTNXI0MkUxZElvcHBWMWZQcDdqTXFUb1l2QjQ3eWpMCnR2LzRvdnlzajdqYWJIZTZ4ZFF1bUhmZ1B6dndaaEllUmVuKy9YY0NnWUVBd2xqYjAxZmxJSVdxS2gyMU54c0IKSkVtSFNoWkM0K2JmSlVDYjlTUnRrSXpSVWc2ejZTWkFVTi9Pc3ZyTVFKOUFHQ0ZlRG5DZU12bG8yZE95ckM0SQpoZmYzSWlKcVJobVRROEozeVBZWEQwaUJaa1F2a2xCK0FwaDJkSS9TT3dnR1Vvc0dTRVRxYStUQWwyczh6U3VtCkRUdzR1cXUrdmcrV25oMTM1eUhjVkRVQ2dZQWVtM0k5czYzakpSVlJFV1d1eGxXTHVjVnNZUzB5REFDanBVN00KSm5HcGhIdTcxS0xmZUVvSkczV2M2RXBEVyt5UEVyYlVMekNIMzQyRERFTW4ySjBoTDlxaCtNUCt4YjZEYVNsRQpvVlBIYzg0dUlUR0M4UUhhZWhPR3BFcWdURldJN3JzYmdTTm81Vm1NMS9WZGlVcEJZY2g5TXpBZUl2aVFGSnZpCk0xOG1nUUtCZ0N0QVFNcytIK1RGcnFHaDZZN3RFNXBoM3pwZ2tsclp4ZEN2MDR4M2JDcy9EZDEyZDZKUElRSk0KQ0JGYXZibmgvNDhwZUJxR0NDRkxIZ1FVL0VUOTdRdFRPU3h3SWxHck9rR1BhUkR3eVc2M0hXWGV1RHBiRXhrbQpXUW9CcHBZYlpWSGlTcVVWaVR1NG5tNjRPQ0loNWVoa3dVS05YY0cvakFxSkdnUDhPYzEzCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
  cert-chain.pem: ""
  key.pem: ""
  root-cert.pem: ""
kind: Secret
metadata:
  name: istio-ca-secret
  ...
type: istio.io/ca-root
[root@master ~]# kubectl get cm istio-security -o yaml -n istio-system
apiVersion: v1
data:
  caTLSRootCert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMzVENDQWNXZ0F3SUJBZ0lRY005Q2w4OCttYThzTERnWFNFaGF3akFOQmdrcWhraUc5dzBCQVFzRkFEQVkKTVJZd0ZBWURWUVFLRXcxamJIVnpkR1Z5TG14dlkyRnNNQjRYRFRJd01ESXdOREE0TURJeU0xb1hEVE13TURJdwpNVEE0TURJeU0xb3dHREVXTUJRR0ExVUVDaE1OWTJ4MWMzUmxjaTVzYjJOaGJEQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKOVZrLythRXV1bHhvb1F1bzNSUnlrWUpuME9udlViZ1JyRHJXWEYKekxKYVZDVUthY1lxTE5vSjBXKzFES1dyZ05weFBKcE8vUUtQeGxOdnJtK004cllVOHc0NWI5VGFLdTNxSGwvdgp1UmkrSnJZT3ZWcGlZY3A4SEc2cnI5UWtIcGJjcm1OT2VOQ1lVZGdXQ2VkMW5tWjZjZGcrT0RIYkFzVkloTmRiCjBHNDc4RU1RM3FOaWFTdGVIRXJMSDRlU045Z2VVbkhmY2hCTVFnQXN4cjJza01wUEw2VWJSMHl2dzVXM3dpWngKbWpaN2ZUYURBcy9vZ3hTV1N6LzE2eG1lVzYvbEx4N1JQNVd6ZTlkWnBPOE9xUW5BV3dlL2puTTNrQ0lNbksrUgpPTVlBRjBiU1VLKzZiT004bmZVanM4RUVJdEJvdUluYmNwNFplZkhvSFlaNk10RUNBd0VBQWFNak1DRXdEZ1lEClZSMFBBUUgvQkFRREFnSUVNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUIKQUcxdEU3UnFCTHpzd3ovTStuYndoeFR2VE14eHJ2RDYxVDc1eTVPRUMxUnk4RmQxbnYyT09vR0UybjUxV0N4VAp0YzFERzAvWUNrbko1a3grWEo5TElDbkdkcHFxR2VnQkNrR1RlWmpPMlk0Zko5dEZ3SWdhbVQwYmNUVjJ3NmUwClVRdUxuR1hxaytVdUFiOHFHWUZObGVlMk1LOFV5aEM0SGlEN1Z2c2xXcUdmazdTOHNtWmR0cGUyMkY2RThrbDQKSFlYNHh4RWQyRTZxVlYxbFZsMTV1TGo2V3R3Z1V0dmZMR3RvMnlyOHN3cExETHZ4SVprdWd6azc1OE1LbUljNwowOEV3bUdNVnVvUUhEcnA4NDU1NkNwZGl2NDY0dUdWeW9PaDU1WWlPcGxidWtWQzZTRE9QbTlueTRRTThRV1ZKCmYyYkRXQkpoSmRXb1FXVElPWm1wek5vPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
kind: ConfigMap
metadata:
  name: istio-security
  ...
[root@master ~]# 

总结一下, 就是用x509生成了根cert 和 根key, 保存到了istio-ca-secret中, 一个可以叠加的root cert保存到了istio-security.

3.2 Sign

func (ca *IstioCA) Sign(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, forCA bool) ([]byte, error) {
    // 用于签名的证书和key 就是在NewSelfSignedIstioCAOptions中生成的key和cert
    signingCert, signingKey, _, _ := ca.keyCertBundle.GetAll()
    if signingCert == nil {
        return nil, caerror.NewError(caerror.CANotReady, fmt.Errorf("Istio CA is not ready")) // nolint
    }
    // 生成csr
    csr, err := util.ParsePemEncodedCSR(csrPEM)
    if err != nil {
        return nil, caerror.NewError(caerror.CSRError, err)
    }
    ...
    // 生成签名证书
    certBytes, err := util.GenCertFromCSR(csr, signingCert, csr.PublicKey, *signingKey, subjectIDs, lifetime, forCA)
    ...
    block := &pem.Block{
        Type:  "CERTIFICATE",
        Bytes: certBytes,
    }
    cert := pem.EncodeToMemory(block)
    return cert, nil
}

可以看到签名是用之前生成保存在ca.keyCertBundle的根cert的.

4. workloadsecret

该组件是专门为所有的serviceaccount生成和维护一个istio.io/key-and-cert类型的secret. 所有生成的这种类型的secret中保存着自己的key and cert, 并且它的证书都是通过保存在istio-ca-secret的根证书来进行签名的.

func NewSecretController(ca certificateAuthority, enableNamespacesByDefault bool,
    certTTL time.Duration, gracePeriodRatio float32, minGracePeriod time.Duration,
    dualUse bool, core corev1.CoreV1Interface, forCA bool, pkcs8Key bool, namespaces []string,
    dnsNames map[string]*DNSNameEntry, istioCaStorageNamespace, rootCertFile string,
    selfSignedCa bool) (*SecretController, error) {
    ...
    c.saStore, c.saController =
        cache.NewInformer(saLW, &v1.ServiceAccount{}, time.Minute, cache.ResourceEventHandlerFuncs{
            AddFunc:    c.saAdded,
            DeleteFunc: c.saDeleted,
        })
    istioSecretSelector := fields.SelectorFromSet(map[string]string{"type": IstioSecretType}).String()
    scrtLW := listwatch.MultiNamespaceListerWatcher(namespaces, func(namespace string) cache.ListerWatcher {
        return &cache.ListWatch{
            ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
                options.FieldSelector = istioSecretSelector
                return core.Secrets(namespace).List(options)
            },
            WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
                options.FieldSelector = istioSecretSelector
                return core.Secrets(namespace).Watch(options)
            },
        }
    })
    c.scrtStore, c.scrtController =
        cache.NewInformer(scrtLW, &v1.Secret{}, secretResyncPeriod, cache.ResourceEventHandlerFuncs{
            DeleteFunc: c.scrtDeleted,
            UpdateFunc: c.scrtUpdated,
        })
    ...
    c.namespaceStore, c.namespaceController =
        cache.NewInformer(namespaceLW, &v1.Namespace{}, namespaceResyncPeriod, cache.ResourceEventHandlerFuncs{
            UpdateFunc: c.namespaceUpdated,
        })

    return c, nil
}
func (sc *SecretController) Run(stopCh chan struct{}) {
    go sc.scrtController.Run(stopCh)
    cache.WaitForCacheSync(stopCh, sc.scrtController.HasSynced)
    go sc.saController.Run(stopCh)
    go sc.namespaceController.Run(stopCh)
}

1. 可以看到有serviceaccount informer, secret informernamespace informer. 另外需要注意的是istioSecretSelector过滤的是类型为istio.io/key-and-certsecret.
2. ca certificateAuthority需要实现以下三个方法, IstioCA就是其中一个实现体.

type certificateAuthority interface {
    Sign(csrPEM []byte, subjectIDs []string, ttl time.Duration, forCA bool) ([]byte, error)
    SignWithCertChain(csrPEM []byte, subjectIDs []string, ttl time.Duration, forCA bool) ([]byte, error)
    GetCAKeyCertBundle() util.KeyCertBundle
}

3. 另外需要注意的是dnsNames, 这个在生成证书的时候会用到, 支持自定义域名, 可以看到运行中的参数有--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system.

// security/pkg/k8s/controller/customdnsname.go
func ConstructCustomDNSNames(serviceAccounts []string, serviceNames []string,
    namespace string, customDNSNames string) map[string]*DNSNameEntry {
    result := make(map[string]*DNSNameEntry)
    for i, svcAccount := range serviceAccounts {
        result[svcAccount] = &DNSNameEntry{
            ServiceName: serviceNames[i],
            Namespace:   namespace,
        }
    }
    if len(customDNSNames) > 0 {
        customNames := strings.Split(customDNSNames, ",")
        log.Infof("The custom-defined DNS name list is %v", customNames)
        for _, customName := range customNames {
            nameDomain := strings.Split(customName, ":")
            if len(nameDomain) == 2 {
                override, ok := result[nameDomain[0]]
                if ok {
                    override.CustomDomains = append(override.CustomDomains, nameDomain[1])
                } else {
                    result[nameDomain[0]] = &DNSNameEntry{
                        ServiceName:   nameDomain[0],
                        CustomDomains: []string{nameDomain[1]},
                    }
                }
            } else {
                ...
            }
        }
    }
    return result
}

所以SecretController中的dnsName就是这么来的.

4.1 upsertSecret

作用: 为对应的serviceaccount创建istio.io/key-and-certsecret.

func (sc *SecretController) upsertSecret(saName, saNamespace string) {
    // 根据serviceaccount name和namespace 构造一个istio.io/key-and-cert类型的secret
    secret := k8ssecret.BuildSecret(saName, GetSecretName(saName), saNamespace, nil,
        nil, nil, nil, nil, IstioSecretType)
    // 查看该secret是否已经存在于k8s中
    _, exists, err := sc.scrtStore.Get(secret)
    ...
    // 如果已经存在 直接返回
    if exists {
        return
    }
    // k8s中不存在 则生成自己的key和证书 签名是用根证书签的
    chain, key, err := sc.generateKeyAndCert(saName, saNamespace)
    ...
    // 根证书
    rootCert := sc.ca.GetCAKeyCertBundle().GetRootCertPem()
    secret.Data = map[string][]byte{
        CertChainID:  chain,
        PrivateKeyID: key,
        RootCertID:   rootCert,
    }
    for i := 0; i < secretCreationRetry; i++ {
        // 保存到k8s中
        _, err = sc.core.Secrets(saNamespace).Create(secret)
        ...
    }
}

1. 根据serviceaccount namenamespace构造一个istio.io/key-and-cert类型的secret.
2. 查看该secret是否已经存在于k8s中.
3. 如果已经存在, 直接返回.
4. 如果k8s中不存在, 则生成自己的key和证书, 签名是用根证书签的.

func (sc *SecretController) generateKeyAndCert(saName string, saNamespace string) ([]byte, []byte, error) {
    // 生成可以识别的名字 
    id := spiffe.MustGenSpiffeURI(saNamespace, saName)
    // 如果有自定义域名 则加入到id中
    if sc.dnsNames != nil {
        // Control plane components in same namespace.
        if e, ok := sc.dnsNames[saName]; ok {
            if e.Namespace == saNamespace {
                // Example: istio-pilot.istio-system.svc, istio-pilot.istio-system
                id += "," + fmt.Sprintf("%s.%s.svc", e.ServiceName, e.Namespace)
                id += "," + fmt.Sprintf("%s.%s", e.ServiceName, e.Namespace)
            }
        }
        if e, ok := sc.dnsNames[saName+"."+saNamespace]; ok {
            for _, d := range e.CustomDomains {
                id += "," + d
            }
        }
    }
    options := util.CertOptions{
        Host:       id,
        RSAKeySize: keySize,
        IsDualUse:  sc.dualUse,
        PKCS8Key:   sc.pkcs8Key,
    }
    // 生成key 和csr
    csrPEM, keyPEM, err := util.GenCSR(options)
    ...
    certChainPEM := sc.ca.GetCAKeyCertBundle().GetCertChainPem()
    // 获得签名后的证书
    certPEM, signErr := sc.ca.Sign(csrPEM, strings.Split(id, ","), sc.certTTL, sc.forCA)
    ...
    certPEM = append(certPEM, certChainPEM...)
    return certPEM, keyPEM, nil
}

5. 获得根证书, 组装信息把该secret保存到k8s中.

func (sc *SecretController) refreshSecret(scrt *v1.Secret) error {
    namespace := scrt.GetNamespace()
    saName := scrt.Annotations[ServiceAccountNameAnnotationKey]
    chain, key, err := sc.generateKeyAndCert(saName, namespace)
    if err != nil {
        return err
    }
    scrt.Data[CertChainID] = chain
    scrt.Data[PrivateKeyID] = key
    scrt.Data[RootCertID] = sc.ca.GetCAKeyCertBundle().GetRootCertPem()
    _, err = sc.core.Secrets(namespace).Update(scrt)
    return err
}

更新secret.

4.2 各informer的行为

informers.png

总体是上图所示, 具体细节参考详细代码即可. 所以查看一个istio.io/key-and-cert类型的secret, data属性有cert-chain.pem, key.pemroot-cert.pem.

[root@master istio_ca]# kubectl get secret istio.bookinfo-details -o yaml 
apiVersion: v1
data:
  cert-chain.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lSQUlzNzlyd0IvaUdISXppUFV4MUFPTEF3RFFZSktvWklodmNOQVFFTEJRQXcKR0RFV01CUUdBMVVFQ2hNTlkyeDFjM1JsY2k1c2IyTmhiREFlRncweU1EQXlNRFF3T0RBeU1qTmFGdzB5TURBMQpNRFF3T0RBeU1qTmFNQUF3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGhET3hXCndXUVNMeDNjR0hsOHI1M3NRd1dGQTZBcC9PZVJ6YlpvSU5ySUVucithVVVmWHU3UGNtQ0xhNzVScy9UcENsTXgKeGpDTFU0aGZIWkk2cHBCS09xS2piZ3pIaldRbVpxTHdseWtEMWltMzNkTW9IekFaMHV3OFV0UDhpQS9GWUUzZAoxYnYxM3Q1eHlhaksxd2ZGUTF6bmt1YUhzNkdsMjZxcllzUHpSQUd4RHBVRk54T2l2cUJzUzZqQXJKWFplSWNBClFqMkl6cHArK1MvK2Z6alRNS2k4R3FiQ0M1VzZTUkpRMUxWbkIwT0RKd0JiSFYxUW1ML2FrbTNKaE9vQ1lFMzYKZG8wVzA0aWpERlZhM2E1c1NPZjdLcXNqcGZESHFZRDhXZFhhZThObmVxTVJ1aGpIdGF2cGRuQXR3YStMQXpKTApYU2drcTJ6UldBME5uRjB4QWdNQkFBR2pnWVV3Z1lJd0RnWURWUjBQQVFIL0JBUURBZ1dnTUIwR0ExVWRKUVFXCk1CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOVkhSTUJBZjhFQWpBQU1FTUdBMVVkRVFFQi93UTUKTURlR05YTndhV1ptWlRvdkwyTnNkWE4wWlhJdWJHOWpZV3d2Ym5NdlpHVm1ZWFZzZEM5ellTOWliMjlyYVc1bQpieTFrWlhSaGFXeHpNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJmaDRxY1F0NUFJRzlGcWF5dXB2SExsUTVYCnJDYmNFSlRGQWFjeUZVQVJuelk4aDBWV21hYTB4WnNJVXpGaklZd2ZLVU5DRWszUHhOQ25laTlEdVV2U2hOdlYKQlZnSnBaWUR0NS91ZnVlc3M5Q2FSMTdYMTNDTFJuRFJBTExWWXNaOVpFKzB6bWRLb1NkdmdSTUZVQ0N0QXgvYwpEaFVsU3F5eFpaSk8wU1hUT1JCNGFWK3dUZDdub0UrcEl6eEF3Q1VHc2U0TjNJK1B6WWVaQkd2Zmd6cXFLOGJ2CkdRWFlPcHdKTTdjc1pwWHlDRGtxSEt0UG81dlM2cXVXMnBPUWdrYkdnV2FaYWZ4aWtvWVBJZklQMFF1NVNjWjgKNXVDRXROQnY3c2xOcmV4amJhZnJBUzFpaFZ5c3R3bGdDc0NBM0FRNExqQnlzNjQyUUo4Mko0SDFEVkUzCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
  key.pem: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNFF6c1ZzRmtFaThkM0JoNWZLK2Q3RU1GaFFPZ0tmem5rYzIyYUNEYXlCSjYvbWxGCkgxN3V6M0pnaTJ1K1ViUDA2UXBUTWNZd2kxT0lYeDJTT3FhUVNqcWlvMjRNeDQxa0ptYWk4SmNwQTlZcHQ5M1QKS0I4d0dkTHNQRkxUL0lnUHhXQk4zZFc3OWQ3ZWNjbW95dGNIeFVOYzU1TG1oN09ocGR1cXEyTEQ4MFFCc1E2VgpCVGNUb3I2Z2JFdW93S3lWMlhpSEFFSTlpTTZhZnZrdi9uODQwekNvdkJxbXdndVZ1a2tTVU5TMVp3ZERneWNBCld4MWRVSmkvMnBKdHlZVHFBbUJOK25hTkZ0T0lvd3hWV3QydWJFam4reXFySTZYd3g2bUEvRm5WMm52RFozcWoKRWJvWXg3V3I2WFp3TGNHdml3TXlTMTBvSkt0czBWZ05EWnhkTVFJREFRQUJBb0lCQVFEYS9qVlk4cFZMY0pmdwozY3dTUGQ1QjBySWpUblRqaHR0Y01UNkhzempTS2RHUGthYVdzVTFYaG1oV0kwRXV4aHZUVFozSk9KOXlaSlcyCllOTXp5WE50R0FPOVh0Q0d1cHF6RjNzRS9VTUhIYmE0MmQycEZEZzlXTmRTbUJMNmtQZ3Z5OWZadnl0SlJWZFcKaUxKOHQ2UXpHNnJqR1RyRTRGS3pUNytUU09kKzVtSDNGSGtiR05QUUpoZkJZNnRCR2k4bmFUNnhtUEpadjlhaQpXbVJpNUNjVkkrc2R4ajNOYXorcXNFRHJ3TTlLckcyTnMyQWxQejVrRndXQUEyY3U2N3BDUy9ZZFRtQkEzVGhICjIrQmp1dlBrL2pNRHhsU2IwNzZSa2JtZkZxcmY3OW9CMmxDWW42Z0Rybk5mNDFLSSs1ekJKN2w2OGo1aThqUnAKdmZBclExd3hBb0dCQU9iOFh4TnhMVFpGRGRablRUcGUwbmw5Vy9wZFpnQ2NQaE5FSzBNTHJGaWlaSzl2bS9TSAoydGEzTU1KbVlnSGpEa0NGWnh2cGRiY3BsY3BnNEIyTGh5UmtqcjA1NDZZSUplTkJhQTgzdElkdllRMm90OTMyCmQyOXlYUFdKRkowNnFBT0JZNEVOcUlzQlZuNFpJc1RpVzAwTGdNeHNHcyt5RE9zaGRueWhPdEhkQW9HQkFQbHMKQVl0ZDRQS3NkRTZYZi9PMlNTVEJWVHZuQnd2SWNwelRWbG02cnJVakFQd2RNSStOdmhoYmMrTDBEQk82RWRmRQo5dVpycE4xV3V5Mk0xVEpHU2dGdndpUWVLLzdOK3BzOXVzMEdmVnhIK2I2SEQ4bnJtWUhVWjVlR25pdysrZ3FwClFOZlFIWU9BU0FWRnl1MHB0anl1VGpDeVlwWW1Xck5lRWN5eVEwVmxBb0dBZWVSRDUrYVpqVUh6ZzJrUC8vVjUKN0ZLUzd4UEtlQmY3U2Y5M05QSThDS25wcUFxbHFlem5OdGVGQzVFcnR3TXl5aW1idDhjREw1enFSdG5JYXM3dQppZTNteFVSL05XYW5WNzEwUkZPSkdNOVZ6L2R3ejlqNFBmK0R2cTZRQ2tpaDBQZStvcU9xaFhBM1RHUEhUVTZHCkQ2bzZWYVhXb0RPOVRYZGpKM0dOc0tFQ2dZQUhBcVFnZUtrRDZSanp4SXBTSFVOOUJ2b1FUdlFCdnNhSjVkNjkKY3VQS0w0dXRpbHg5REd6VlhteXBhbGFVajF4RkJrSXlROEJFZ0ZXT2VERGQwdC90bm1pRWYxeVpNc3ppWkIvRgo5M0s2ajBOVHVaUVdCc3N2dlBxVVpiSTRhQ1M2Ky9yWWxmN2VYVktvNDBkSzF5dEtGVlFaUmtwREVoem1nYlJhClhvQkppUUtCZ1FDdDlSZll6cURRKzJ2eVFRUkhoRVhIaXpQbVRXZEN6M00xVERPSUpZcEI5VENUVE81cGwvYWcKbmVMamNjTng0czFPWm9qVDM4MXRFMndPNFRPQlRIMlJBTnBHcDBUbDRyL2RqSmdKVmpUa0Z4S0o0WG1OWEdiUApnWHBTdERWTTVrbDI1dUlKbXpVdVJSSTdIbEZmcnV4VnlYR3BUMTNUY2RHL01ib05vOE1Ianc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
  root-cert.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMzVENDQWNXZ0F3SUJBZ0lRY005Q2w4OCttYThzTERnWFNFaGF3akFOQmdrcWhraUc5dzBCQVFzRkFEQVkKTVJZd0ZBWURWUVFLRXcxamJIVnpkR1Z5TG14dlkyRnNNQjRYRFRJd01ESXdOREE0TURJeU0xb1hEVE13TURJdwpNVEE0TURJeU0xb3dHREVXTUJRR0ExVUVDaE1OWTJ4MWMzUmxjaTVzYjJOaGJEQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKOVZrLythRXV1bHhvb1F1bzNSUnlrWUpuME9udlViZ1JyRHJXWEYKekxKYVZDVUthY1lxTE5vSjBXKzFES1dyZ05weFBKcE8vUUtQeGxOdnJtK004cllVOHc0NWI5VGFLdTNxSGwvdgp1UmkrSnJZT3ZWcGlZY3A4SEc2cnI5UWtIcGJjcm1OT2VOQ1lVZGdXQ2VkMW5tWjZjZGcrT0RIYkFzVkloTmRiCjBHNDc4RU1RM3FOaWFTdGVIRXJMSDRlU045Z2VVbkhmY2hCTVFnQXN4cjJza01wUEw2VWJSMHl2dzVXM3dpWngKbWpaN2ZUYURBcy9vZ3hTV1N6LzE2eG1lVzYvbEx4N1JQNVd6ZTlkWnBPOE9xUW5BV3dlL2puTTNrQ0lNbksrUgpPTVlBRjBiU1VLKzZiT004bmZVanM4RUVJdEJvdUluYmNwNFplZkhvSFlaNk10RUNBd0VBQWFNak1DRXdEZ1lEClZSMFBBUUgvQkFRREFnSUVNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUIKQUcxdEU3UnFCTHpzd3ovTStuYndoeFR2VE14eHJ2RDYxVDc1eTVPRUMxUnk4RmQxbnYyT09vR0UybjUxV0N4VAp0YzFERzAvWUNrbko1a3grWEo5TElDbkdkcHFxR2VnQkNrR1RlWmpPMlk0Zko5dEZ3SWdhbVQwYmNUVjJ3NmUwClVRdUxuR1hxaytVdUFiOHFHWUZObGVlMk1LOFV5aEM0SGlEN1Z2c2xXcUdmazdTOHNtWmR0cGUyMkY2RThrbDQKSFlYNHh4RWQyRTZxVlYxbFZsMTV1TGo2V3R3Z1V0dmZMR3RvMnlyOHN3cExETHZ4SVprdWd6azc1OE1LbUljNwowOEV3bUdNVnVvUUhEcnA4NDU1NkNwZGl2NDY0dUdWeW9PaDU1WWlPcGxidWtWQzZTRE9QbTlueTRRTThRV1ZKCmYyYkRXQkpoSmRXb1FXVElPWm1wek5vPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
kind: Secret
metadata:
  annotations:
    istio.io/service-account.name: bookinfo-details
  name: istio.bookinfo-details
  namespace: default
 ...
type: istio.io/key-and-cert
[root@master istio_ca]# 

5. server

rootCmd = &cobra.Command{
        Use:   "istio_ca",
        Short: "Istio Certificate Authority (CA).",
        Args:  cobra.ExactArgs(0),
        Run: func(cmd *cobra.Command, args []string) {
            runCA()
        },
    }
func main() {
    if err := rootCmd.Execute(); err != nil {
        log.Errora(err)
        os.Exit(-1)
    }
}
func runCA() {
    ...
    var webhooks map[string]*controller.DNSNameEntry
    if opts.appendDNSNames {
        // 生成自定义的dnsname
        webhooks = controller.ConstructCustomDNSNames(webhookServiceAccounts,
            webhookServiceNames, opts.istioCaStorageNamespace, opts.customDNSNames)
    }
    // 创建k8s client
    cs, err := kubelib.CreateClientset(opts.kubeConfigFile, "")
    ...
    // 创建IstioCA
    ca := createCA(cs.CoreV1())
    stopCh := make(chan struct{})
    if !opts.serverOnly {
        // 创建SecretController
        sc, err := controller.NewSecretController(ca, opts.enableNamespacesByDefault,
            opts.workloadCertTTL, opts.workloadCertGracePeriodRatio, opts.workloadCertMinGracePeriod,
            opts.dualUse, cs.CoreV1(), opts.signCACerts, opts.pkcs8Keys, listenedNamespaces, webhooks,
            opts.istioCaStorageNamespace, opts.rootCertFile, opts.selfSignedCA)
        ...
        // 运行SecretController
        sc.Run(stopCh)
    } else {
        ...
    }
    if opts.grpcPort > 0 {
        ...
       }
    ...
}

1. 生成自定义的dnsname.
2. 创建IstioCA对象ca.
3. 创建SecretController对象sc并运行.

6. 参考

  1. istio 1.3.6源码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容