istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/istio/ca.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package istio 16 17 import ( 18 "context" 19 "crypto/tls" 20 "crypto/x509" 21 "fmt" 22 "sync" 23 "time" 24 25 "google.golang.org/grpc" 26 "google.golang.org/grpc/credentials" 27 "google.golang.org/grpc/metadata" 28 authenticationv1 "k8s.io/api/authentication/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/client-go/kubernetes" 31 32 pb "istio.io/api/security/v1alpha1" 33 "istio.io/istio/pkg/config/constants" 34 "istio.io/istio/pkg/test/framework" 35 pkiutil "istio.io/istio/security/pkg/pki/util" 36 ) 37 38 type Cert struct { 39 ClientCert, Key, RootCert []byte 40 } 41 42 func CreateCertificate(t framework.TestContext, i Instance, serviceAccount, namespace string) (Cert, error) { 43 c := t.Clusters().Default() 44 rootCert, err := FetchRootCert(c.Kube()) 45 if err != nil { 46 return Cert{}, fmt.Errorf("failed to fetch root cert: %v", err) 47 } 48 49 token, err := GetServiceAccountToken(c.Kube(), "istio-ca", namespace, serviceAccount) 50 if err != nil { 51 return Cert{}, err 52 } 53 54 san := fmt.Sprintf("spiffe://%s/ns/%s/sa/%s", "cluster.local", namespace, serviceAccount) 55 options := pkiutil.CertOptions{ 56 Host: san, 57 RSAKeySize: 2048, 58 } 59 // Generate the cert/key, send CSR to CA. 60 csrPEM, keyPEM, err := pkiutil.GenCSR(options) 61 if err != nil { 62 return Cert{}, err 63 } 64 a, err := i.InternalDiscoveryAddressFor(c) 65 if err != nil { 66 return Cert{}, err 67 } 68 client, err := newCitadelClient(a, []byte(rootCert)) 69 if err != nil { 70 return Cert{}, fmt.Errorf("creating citadel client: %v", err) 71 } 72 req := &pb.IstioCertificateRequest{ 73 Csr: string(csrPEM), 74 ValidityDuration: int64((time.Hour * 24 * 7).Seconds()), 75 } 76 rctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("Authorization", "Bearer "+token, "ClusterID", constants.DefaultClusterName)) 77 resp, err := client.CreateCertificate(rctx, req) 78 if err != nil { 79 return Cert{}, fmt.Errorf("send CSR: %v", err) 80 } 81 certChain := []byte{} 82 for _, c := range resp.CertChain { 83 certChain = append(certChain, []byte(c)...) 84 } 85 return Cert{certChain, keyPEM, []byte(rootCert)}, nil 86 } 87 88 // 7 days 89 var saTokenExpiration int64 = 60 * 60 * 24 * 7 90 91 func GetServiceAccountToken(c kubernetes.Interface, aud, ns, sa string) (string, error) { 92 san := san(ns, sa) 93 94 if got, f := cachedTokens.Load(san); f { 95 t := got.(token) 96 if t.expiration.After(time.Now().Add(time.Minute)) { 97 return t.token, nil 98 } 99 // Otherwise, its expired, load a new one 100 } 101 rt, err := c.CoreV1().ServiceAccounts(ns).CreateToken(context.Background(), sa, 102 &authenticationv1.TokenRequest{ 103 Spec: authenticationv1.TokenRequestSpec{ 104 Audiences: []string{aud}, 105 ExpirationSeconds: &saTokenExpiration, 106 }, 107 }, metav1.CreateOptions{}) 108 if err != nil { 109 return "", err 110 } 111 exp := rt.Status.ExpirationTimestamp.Time 112 cachedTokens.Store(san, token{rt.Status.Token, exp}) 113 return rt.Status.Token, nil 114 } 115 116 // map of SAN to jwt token. Used to avoid repetitive calls 117 var cachedTokens sync.Map 118 119 type token struct { 120 token string 121 expiration time.Time 122 } 123 124 // NewCitadelClient create a CA client for Citadel. 125 func newCitadelClient(endpoint string, rootCert []byte) (pb.IstioCertificateServiceClient, error) { 126 certPool := x509.NewCertPool() 127 ok := certPool.AppendCertsFromPEM(rootCert) 128 if !ok { 129 return nil, fmt.Errorf("failed to append certificates") 130 } 131 config := tls.Config{ 132 RootCAs: certPool, 133 InsecureSkipVerify: true, // nolint: gosec // test only code 134 } 135 transportCreds := credentials.NewTLS(&config) 136 137 conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(transportCreds)) 138 if err != nil { 139 return nil, fmt.Errorf("failed to connect to endpoint %s", endpoint) 140 } 141 142 client := pb.NewIstioCertificateServiceClient(conn) 143 return client, nil 144 } 145 146 func san(ns, sa string) string { 147 return fmt.Sprintf("spiffe://%s/ns/%s/sa/%s", "cluster.local", ns, sa) 148 } 149 150 func FetchRootCert(c kubernetes.Interface) (string, error) { 151 cm, err := c.CoreV1().ConfigMaps("istio-system").Get(context.TODO(), "istio-ca-root-cert", metav1.GetOptions{}) 152 if err != nil { 153 return "", err 154 } 155 return cm.Data["root-cert.pem"], nil 156 }