istio ca控制器

使用Kubernetes CSR的自定义CA集成

使用Kubernetes CA部署Istio

使用Kubernetes CA部署Istio

  1. 使用istioctl以下配置在集群上部署Istio

    cat <<EOF > ./istio.yaml
    apiVersion: install.istio.io/v1alpha1
    kind: IstioOperator
    spec:
     pilot:
       k8s:
         env:
         # Indicate to Istiod that we use an Custom Certificate Authority
         - name: EXTERNAL_CA
           value: ISTIOD_RA_KUBERNETES_API
         # Tells Istiod to use the Kubernetes legacy CA Signer
         - name: K8S_SIGNER
           value: kubernetes.io/legacy-unknown
    EOF
    istioctl install --set profile=demo -f ./istio.yaml
  2. bookinfo在bookinfo名称空间中部署示例应用程序。确保在Istio根目录中执行以下命令

kubectl create ns bookinfo
kubectl apply -f <(istioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo.yaml) -n bookinfo

验证安装的证书是否正确

部署工作负载后,以上这些工作负载会将CSR请求发送给Istiod,然后将其转发给Kubernetes CA进行签名。如果一切顺利,则将已签名的证书发送回安装它们的工作负载。要验证它们是否已被Kubernetes CA签名,您需要首先提取签名的证书。

转储在名称空间中运行的所有Pod。

$ kubectl get pods -n bookinfo

选择任何一个正在运行的吊舱进行下一步。

获取Istio代理用于mTLS的证书链和CA根证书。

$ istioctl pc secret <pod-name> -o json > proxy_secret

proxy_secret json文件在trustedCA字段中包含mTLS的CA根证书。请注意,此证书是base64编码的。

Kubernetes CA使用的证书(特别是kubernetes.io/legacy-unknown签名者)被加载到与bookinfo命名空间中的每个服务帐户关联的机密上。

$ kubectl get secrets -n bookinfo

选择一个与任何服务帐户关联的秘密名称。它们的名称中有一个"令牌"。

$ kubectl get secrets -n bookinfo <secret-name> -o json

在ca.crt输出字段包含base64编码Kubernetes CA证书。

将ca.cert上一步中获得的结果与前一步中的TrustedCA字段内容进行比较。这两个应该是相同的。

(可选)按照bookinfo示例中的其余步骤进行操作,以确保服务之间的通信按预期进行。

使用自定义CA

假设定制CA实现的控制器具有读取和签名Kubernetes CSR请求的必要权限。有关更多详细信息,请参考Kubernetes CSR文档。请注意,以下步骤取决于外部来源,并且可能会更改。

在Kubernetes集群中部署自定义CA控制器

  1. 在此示例中,我们使用开源证书颁发机构实现。该代码构建了一个控制器,该控制器读取Kubernetes集群上的CSR资源,并使用本地密钥创建证书。请按照页面上的说明进行操作:

    1. 构建证书控制器docker 镜像

    2. 将镜像上传到Docker registry

    3. 生成Kubernetes manifest以部署它

  2. 将上一步中生成的Kubernetes清单部署在signer-ca-system名称空间中的本地集群上。

$ kubectl apply -f local-ca.yaml

确保所有服务都在运行。

$ kubectl get services -n signer-ca-system
  NAME                                           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
  signer-ca-controller-manager-metrics-service   ClusterIP   10.8.9.25    none        8443/TCP   72s
  1. 获取CA的公钥。这被编码在signer-ca-system名称空间中的秘密" signer-ca- *"中。

$ kubectl get secrets signer-ca-5hff5h74hm -o json

该tls.crt字段包含base64编码的公共密钥文件。记录下来以备将来使用。

将CA根证书加载到istiod可以访问的secret中

  1. 将机密加载到istiod名称空间中。

$ cat <<EOF > ./external-ca-secret.yaml
  apiVersion: v1
  kind: Secret
  metadata:
    name: external-ca-cert
    namespace: istio-system
  data:
  root-cert.pem: <tls.cert from the step above>
  EOF
$ kubectl apply -f external-ca-secret.yaml

Istio必须执行此步骤,以验证工作负载证书已由正确的证书颁发机构签名,并将根证书添加到信任捆绑中以使mTLS正常工作。

部署Istio

使用istioctl以下配置在集群上部署Istio 。

$ cat <<EOF > ./istio.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  components:
    base:
      k8s:
        overlays:
          # Amend ClusterRole to add permission for istiod to approve certificate signing by custom signer
          - kind: ClusterRole
            name: istiod-istio-system
            patches:
              - path: rules[-1]
                value: |
                  apiGroups:
                  - certificates.k8s.io
                  resourceNames:
                  # Name of k8s external Signer in this example
                  - example.com/foo
                  resources:
                  - signers
                  verbs:
                  - approve
    pilot:
      k8s:
        env:
          # Indicate to Istiod that we use an external signer
          - name: EXTERNAL_CA
            value: ISTIOD_RA_KUBERNETES_API
          # Indicate to Istiod the external k8s Signer Name
          - name: K8S_SIGNER
            value: example.com/foo
        overlays:
        - kind: Deployment
          name: istiod
          patches:
            - path: spec.template.spec.containers[0].volumeMounts[-1]
              value: |
                # Mount external CA certificate into Istiod
                name: external-ca-cert
                mountPath: /etc/external-ca-cert
                readOnly: true
            - path: spec.template.spec.volumes[-1]
              value: |
                name: external-ca-cert
                secret:
                  secretName: external-ca-cert
                  optional: true
EOF
$ istioctl install --set profile=demo -f ./istio.yaml
  1. bookinfo在bookinfo名称空间中部署示例应用程序。

$ kubectl create ns bookinfo
$ kubectl apply -f <(istioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo.yaml) -n bookinfo

验证安装的自定义CA证书是否正确

部署工作负载后,以上这些工作负载会将CSR请求发送给Istiod,然后将其转发给Kubernetes CA进行签名。如果一切顺利,则将已签名的证书发送回安装它们的工作负载。为了验证Kubernetes CA确实已对它们进行了签名,您需要首先提取已签名的证书。

转储在名称空间中运行的所有Pod。

$ kubectl get pods -n bookinfo

选择任何正在运行的吊舱进行下一步。

获取Istio代理用于mTLS的证书链和CA根证书。

$ istioctl pc secret <pod-name> -o json > proxy_secret

该proxy_secretJSON文件包含在MTLS CA根证书trustedCA领域。请注意,此证书是base64编码的。

将以上步骤中获得的CA根证书与external-ca-cert中的" root-cert.pem"值进行比较。这两个应该是相同的。

(可选)按照bookinfo示例中的其余步骤进行操作,以确保服务之间的通信按预期进行。

证书签发原理

type CertificateAuthority interface {
    // Sign根据给定的CSR和TTL为工作负载或CA生成证书。
    Sign(csrPEM []byte, subjectIDs []string, ttl time.Duration, forCA bool) ([]byte, error)
    // SignWithCertChain与Sign类似,但返回叶子证书和整个证书链。
    SignWithCertChain(csrPEM []byte, subjectIDs []string, ttl time.Duration, forCA bool) ([]byte, error)
    // GetCAKeyCertBundle返回CA使用的KeyCertBundle。
    GetCAKeyCertBundle() util.KeyCertBundle
}

判断使用哪种类型的CertificateAuthority

if s.CA, err = s.createIstioCA(corev1, caOpts); err != nil {
            return fmt.Errorf("failed to create CA: %v", err)
        }
        // 判断是否启用通过k8s csr 自定义CA
        if caOpts.ExternalCAType != "" {
            // 创建RA 中间机构
            if s.RA, err = s.createIstioRA(s.kubeClient, caOpts); err != nil {
                return fmt.Errorf("failed to create RA: %v", err)
            }
        }
        if err = s.initPublicKey(); err != nil {
            return fmt.Errorf("error initializing public key: %v", err)
        }

CreateCertificate处理传入的证书签名请求(CSR).它进行身份验证和授权.验证后,签署证书以证明:SAN是身份验证结果中呼叫者的身份.主题公钥是CSR中的公钥.有效期限是请求中的ValidityDuration,如果给定的期限无效,则为默认值.它由CA签名密钥签名。

func (s *Server) CreateCertificate(ctx context.Context, request *pb.IstioCertificateRequest) (
    *pb.IstioCertificateResponse, error) {
    s.monitoring.CSR.Increment()
    caller := s.authenticate(ctx)
    if caller == nil {
        s.monitoring.AuthnError.Increment()
        return nil, status.Error(codes.Unauthenticated, "request authenticate failure")
    }

    // TODO: Call authorizer.

    _, _, certChainBytes, rootCertBytes := s.ca.GetCAKeyCertBundle().GetAll()
    cert, signErr := s.ca.Sign(
        []byte(request.Csr), caller.Identities, time.Duration(request.ValidityDuration)*time.Second, false)
    if signErr != nil {
        serverCaLog.Errorf("CSR signing error (%v)", signErr.Error())
        s.monitoring.GetCertSignError(signErr.(*caerror.Error).ErrorType()).Increment()
        return nil, status.Errorf(signErr.(*caerror.Error).HTTPErrorCode(), "CSR signing error (%v)", signErr.(*caerror.Error))
    }
    respCertChain := []string{string(cert)}
    if len(certChainBytes) != 0 {
        respCertChain = append(respCertChain, string(certChainBytes))
    }
    respCertChain = append(respCertChain, string(rootCertBytes))
    response := &pb.IstioCertificateResponse{
        CertChain: respCertChain,
    }
    s.monitoring.Success.Increment()
    serverCaLog.Debug("CSR successfully signed.")
    return response, nil
}

k8s ra

k8s ra 的sign法法最终调用k8sSign

ra.k8sSign(ra.csrInterface.CertificateSigningRequests(), csrPEM, csrName, ra.raOpts.caCertFile)

k8sSign 使用chiron签发证书,发出csrrequest

func (ra *IstioRA) k8sSign(k8sCsrInterface certclient.CertificateSigningRequestInterface,
    csrPEM []byte, csrName string, caCertFile string) ([]byte, error) {
    csrSpec := &cert.CertificateSigningRequestSpec{
        SignerName: &ra.raOpts.caSigner,
        Request:    csrPEM,
        Groups:     []string{"system:authenticated"},
        Usages: []cert.KeyUsage{
            cert.UsageDigitalSignature,
            cert.UsageKeyEncipherment,
            cert.UsageServerAuth,
            cert.UsageClientAuth,
        },
    }
    certChain, _, err := chiron.SignCSRK8s(k8sCsrInterface, csrName, csrSpec, "", caCertFile, false)
    if err != nil {
        return nil, caerror.NewError(caerror.CertGenError, err)
    }
    return certChain, err
}

ca

Sign接受PEM编码的CSR,主题ID和生存期,并返回已签名的证书.如果forCA为true,则签名证书为CA证书,否则为工作负载证书。

func (ca *IstioCA) Sign(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, forCA bool) ([]byte, error) {
    signingCert, signingKey, _, _ := ca.keyCertBundle.GetAll()
    if signingCert == nil {
        return nil, caerror.NewError(caerror.CANotReady, fmt.Errorf("Istio CA is not ready")) // nolint
    }

    csr, err := util.ParsePemEncodedCSR(csrPEM)
    if err != nil {
        return nil, caerror.NewError(caerror.CSRError, err)
    }

    lifetime := requestedLifetime
    // If the requested requestedLifetime is non-positive, apply the default TTL.
    if requestedLifetime.Seconds() <= 0 {
        lifetime = ca.defaultCertTTL
    }
    // If the requested TTL is greater than maxCertTTL, return an error
    if requestedLifetime.Seconds() > ca.maxCertTTL.Seconds() {
        return nil, caerror.NewError(caerror.TTLError, fmt.Errorf(
            "requested TTL %s is greater than the max allowed TTL %s", requestedLifetime, ca.maxCertTTL))
    }

    certBytes, err := util.GenCertFromCSR(csr, signingCert, csr.PublicKey, *signingKey, subjectIDs, lifetime, forCA)
    if err != nil {
        return nil, caerror.NewError(caerror.CertGenError, err)
    }

    block := &pem.Block{
        Type:  "CERTIFICATE",
        Bytes: certBytes,
    }
    cert := pem.EncodeToMemory(block)

    return cert, nil
}

Last updated