| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package addon
- import (
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/json"
- "encoding/pem"
- "errors"
- "fmt"
- "math/big"
- "net"
- "net/http"
- "os"
- "path/filepath"
- "time"
- rbacv1 "k8s.io/api/rbac/v1"
- "k8s.io/apimachinery/pkg/types"
- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
- "github.com/golang-jwt/jwt/v4"
- vault "github.com/hashicorp/vault/api"
- . "github.com/onsi/ginkgo/v2"
- v1 "k8s.io/api/core/v1"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "github.com/external-secrets/external-secrets-e2e/framework/util"
- )
- type Vault struct {
- chart *HelmChart
- Namespace string
- PodName string
- VaultClient *vault.Client
- VaultURL string
- VaultMtlsURL string
- portForwarder *PortForward
- RootToken string
- VaultServerCA []byte
- ServerCert []byte
- ServerKey []byte
- VaultClientCA []byte
- ClientCert []byte
- ClientKey []byte
- JWTPubkey []byte
- JWTPrivKey []byte
- JWTToken string
- JWTRole string
- JWTPath string
- JWTK8sPath string
- KubernetesAuthPath string
- KubernetesAuthRole string
- AppRoleSecret string
- AppRoleID string
- AppRolePath string
- }
- const privatePemType = "RSA PRIVATE KEY"
- func NewVault() *Vault {
- repo := "hashicorp-vault"
- return &Vault{
- chart: &HelmChart{
- Namespace: "vault",
- ReleaseName: "vault",
- Chart: fmt.Sprintf("%s/vault", repo),
- ChartVersion: "0.30.1",
- Repo: ChartRepo{
- Name: repo,
- URL: "https://helm.releases.hashicorp.com",
- },
- Args: []string{
- "--create-namespace",
- },
- Values: []string{filepath.Join(AssetDir(), "vault.values.yaml")},
- },
- Namespace: "vault",
- }
- }
- type OperatorInitResponse struct {
- UnsealKeysB64 []string `json:"unseal_keys_b64"`
- RootToken string `json:"root_token"`
- }
- func (l *Vault) Install() error {
- // From Kubernetes 1.32+ on the oidc endpoint is not available to unauthenticated clients.
- // We create this clusterrole to allow vault to access the oidc endpoint.
- // see: https://github.com/ansible-collections/kubernetes.core/issues/868
- crb := &rbacv1.ClusterRoleBinding{
- ObjectMeta: metav1.ObjectMeta{
- Name: "allow-anon-oidc",
- },
- }
- _, err := controllerutil.CreateOrUpdate(GinkgoT().Context(), l.chart.config.CRClient, crb, func() error {
- crb.Subjects = []rbacv1.Subject{
- {
- APIGroup: "rbac.authorization.k8s.io",
- Kind: "User",
- Name: "system:anonymous",
- },
- }
- crb.RoleRef = rbacv1.RoleRef{
- APIGroup: "rbac.authorization.k8s.io",
- Kind: "ClusterRole",
- Name: "system:service-account-issuer-discovery",
- }
- return nil
- })
- if err != nil {
- return err
- }
- if err = l.chart.Install(); err != nil {
- return err
- }
- if err = l.patchVaultService(); err != nil {
- return err
- }
- if err = l.initVault(); err != nil {
- return err
- }
- if err = l.configureVault(); err != nil {
- return err
- }
- return nil
- }
- func (l *Vault) patchVaultService() error {
- serviceName := l.chart.ReleaseName
- servicePatch := []byte(`[{"op": "add", "path": "/spec/ports/-", "value": { "name": "https-mtls", "port": 8210, "protocol": "TCP", "targetPort": 8210 }}]`)
- clientSet := l.chart.config.KubeClientSet
- _, err := clientSet.CoreV1().Services(l.Namespace).
- Patch(GinkgoT().Context(), serviceName, types.JSONPatchType, servicePatch, metav1.PatchOptions{})
- return err
- }
- func (l *Vault) initVault() error {
- // gen certificates and put them into the secret
- serverRootPem, serverPem, serverKeyPem, clientRootPem, clientPem, clientKeyPem, err := genVaultCertificates(l.Namespace, l.chart.ReleaseName)
- if err != nil {
- return fmt.Errorf("unable to gen vault certs: %w", err)
- }
- jwtPrivkey, jwtPubkey, jwtToken, err := genVaultJWTKeys()
- if err != nil {
- return fmt.Errorf("unable to generate vault jwt keys: %w", err)
- }
- // make certs available to the struct
- // so it can be used by the provider
- l.VaultServerCA = serverRootPem
- l.ServerCert = serverPem
- l.ServerKey = serverKeyPem
- l.VaultClientCA = clientRootPem
- l.ClientCert = clientPem
- l.ClientKey = clientKeyPem
- l.JWTPrivKey = jwtPrivkey
- l.JWTPubkey = jwtPubkey
- l.JWTToken = jwtToken
- l.JWTPath = "myjwt" // see configure-vault.sh
- l.JWTK8sPath = "myjwtk8s" // see configure-vault.sh
- l.JWTRole = "external-secrets-operator" // see configure-vault.sh
- l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh
- l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh
- // vault-config contains vault init config and policies
- files, err := os.ReadDir(fmt.Sprintf("%s/vault-config", AssetDir()))
- if err != nil {
- return err
- }
- sec := &v1.Secret{
- ObjectMeta: metav1.ObjectMeta{
- Name: "vault-tls-config",
- Namespace: l.Namespace,
- },
- Data: map[string][]byte{},
- }
- _, err = controllerutil.CreateOrUpdate(GinkgoT().Context(), l.chart.config.CRClient, sec, func() error {
- sec.Data = map[string][]byte{}
- for _, f := range files {
- name := f.Name()
- data := mustReadFile(fmt.Sprintf("%s/vault-config/%s", AssetDir(), name))
- sec.Data[name] = data
- }
- sec.Data["vault-server-ca.pem"] = serverRootPem
- sec.Data["server-cert.pem"] = serverPem
- sec.Data["server-cert-key.pem"] = serverKeyPem
- sec.Data["vault-client-ca.pem"] = clientRootPem
- sec.Data["es-client.pem"] = clientPem
- sec.Data["es-client-key.pem"] = clientKeyPem
- sec.Data["jwt-pubkey.pem"] = jwtPubkey
- return nil
- })
- if err != nil {
- return err
- }
- pl, err := util.WaitForPodsRunning(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
- LabelSelector: "app.kubernetes.io/name=vault",
- })
- if err != nil {
- return fmt.Errorf("error waiting for vault to be running: %w", err)
- }
- l.PodName = pl.Items[0].Name
- out, err := util.ExecCmd(
- l.chart.config.KubeClientSet,
- l.chart.config.KubeConfig,
- l.PodName, l.Namespace, "vault operator init --format=json")
- if err != nil {
- return fmt.Errorf("error initializing vault: %w", err)
- }
- var res OperatorInitResponse
- err = json.Unmarshal([]byte(out), &res)
- if err != nil {
- return err
- }
- l.RootToken = res.RootToken
- for _, k := range res.UnsealKeysB64 {
- _, err = util.ExecCmd(
- l.chart.config.KubeClientSet,
- l.chart.config.KubeConfig,
- l.PodName, l.Namespace, "vault operator unseal "+k)
- if err != nil {
- return fmt.Errorf("unable to unseal vault: %w", err)
- }
- }
- // vault becomes ready after it has been unsealed
- err = util.WaitForPodsReady(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
- LabelSelector: "app.kubernetes.io/name=vault",
- })
- if err != nil {
- return fmt.Errorf("error waiting for vault to be ready: %w", err)
- }
- // This e2e test provider uses a local port-forwarded to talk to the vault API instead
- // of using the kubernetes service. This allows us to run the e2e test suite locally.
- l.portForwarder, err = NewPortForward(l.chart.config.KubeClientSet, l.chart.config.KubeConfig, "vault", l.chart.Namespace, 8200)
- if err != nil {
- return err
- }
- if err := l.portForwarder.Start(); err != nil {
- return err
- }
- serverCA := l.VaultServerCA
- caCertPool := x509.NewCertPool()
- ok := caCertPool.AppendCertsFromPEM(serverCA)
- if !ok {
- panic("unable to append server ca cert")
- }
- cfg := vault.DefaultConfig()
- l.VaultURL = fmt.Sprintf("https://%s.%s.svc.cluster.local:8200", l.chart.ReleaseName, l.Namespace)
- l.VaultMtlsURL = fmt.Sprintf("https://%s.%s.svc.cluster.local:8210", l.chart.ReleaseName, l.Namespace)
- cfg.Address = fmt.Sprintf("https://localhost:%d", l.portForwarder.localPort)
- cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = caCertPool
- l.VaultClient, err = vault.NewClient(cfg)
- if err != nil {
- return fmt.Errorf("unable to create vault client: %w", err)
- }
- l.VaultClient.SetToken(l.RootToken)
- return nil
- }
- func (l *Vault) configureVault() error {
- cmd := `sh /etc/vault-config/configure-vault.sh %s`
- _, err := util.ExecCmd(
- l.chart.config.KubeClientSet,
- l.chart.config.KubeConfig,
- l.PodName, l.Namespace, fmt.Sprintf(cmd, l.RootToken))
- if err != nil {
- return fmt.Errorf("unable to configure vault: %w", err)
- }
- // configure appRole
- l.AppRolePath = "myapprole"
- req := l.VaultClient.NewRequest(http.MethodGet, fmt.Sprintf("/v1/auth/%s/role/eso-e2e-role/role-id", l.AppRolePath))
- res, err := l.VaultClient.RawRequest(req) //nolint:staticcheck
- if err != nil {
- return err
- }
- defer func() {
- _ = res.Body.Close()
- }()
- sec, err := vault.ParseSecret(res.Body)
- if err != nil {
- return err
- }
- l.AppRoleID = sec.Data["role_id"].(string)
- // parse role id
- req = l.VaultClient.NewRequest(http.MethodPost, fmt.Sprintf("/v1/auth/%s/role/eso-e2e-role/secret-id", l.AppRolePath))
- res, err = l.VaultClient.RawRequest(req) //nolint:staticcheck
- if err != nil {
- return err
- }
- defer func() {
- _ = res.Body.Close()
- }()
- sec, err = vault.ParseSecret(res.Body)
- if err != nil {
- return err
- }
- l.AppRoleSecret = sec.Data["secret_id"].(string)
- return nil
- }
- func (l *Vault) Logs() error {
- return l.chart.Logs()
- }
- func (l *Vault) Uninstall() error {
- if l.portForwarder != nil {
- l.portForwarder.Close()
- l.portForwarder = nil
- }
- if err := l.chart.Uninstall(); err != nil {
- return err
- }
- return l.chart.config.KubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), l.chart.Namespace, metav1.DeleteOptions{})
- }
- func (l *Vault) Setup(cfg *Config) error {
- return l.chart.Setup(cfg)
- }
- // nolint:gocritic
- func genVaultCertificates(namespace, serviceName string) ([]byte, []byte, []byte, []byte, []byte, []byte, error) {
- // gen server ca + certs
- serverRootCert, serverRootPem, serverRootKey, err := genCARoot()
- if err != nil {
- return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
- }
- serverPem, serverKey, err := genPeerCert(serverRootCert, serverRootKey, "vault", []string{
- "localhost",
- serviceName,
- fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace)})
- if err != nil {
- return nil, nil, nil, nil, nil, nil, errors.New("unable to generate vault server cert")
- }
- serverKeyPem := pem.EncodeToMemory(&pem.Block{
- Type: privatePemType,
- Bytes: x509.MarshalPKCS1PrivateKey(serverKey)},
- )
- // gen client ca + certs
- clientRootCert, clientRootPem, clientRootKey, err := genCARoot()
- if err != nil {
- return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
- }
- clientPem, clientKey, err := genPeerCert(clientRootCert, clientRootKey, "vault-client", nil)
- if err != nil {
- return nil, nil, nil, nil, nil, nil, errors.New("unable to generate vault server cert")
- }
- clientKeyPem := pem.EncodeToMemory(&pem.Block{
- Type: privatePemType,
- Bytes: x509.MarshalPKCS1PrivateKey(clientKey)},
- )
- return serverRootPem, serverPem, serverKeyPem, clientRootPem, clientPem, clientKeyPem, err
- }
- func genVaultJWTKeys() ([]byte, []byte, string, error) {
- key, err := rsa.GenerateKey(rand.Reader, 2048)
- if err != nil {
- return nil, nil, "", err
- }
- privPem := pem.EncodeToMemory(&pem.Block{
- Type: privatePemType,
- Bytes: x509.MarshalPKCS1PrivateKey(key),
- })
- pk, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
- if err != nil {
- return nil, nil, "", err
- }
- pubPem := pem.EncodeToMemory(&pem.Block{
- Type: "RSA PUBLIC KEY",
- Bytes: pk,
- })
- token := jwt.NewWithClaims(jwt.SigningMethodPS256, jwt.MapClaims{
- "aud": "vault.client",
- "sub": "vault@example",
- "iss": "example.iss",
- "user": "eso",
- "exp": time.Now().Add(time.Hour).Unix(),
- "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
- })
- // Sign and get the complete encoded token as a string using the secret
- tokenString, err := token.SignedString(key)
- if err != nil {
- return nil, nil, "", err
- }
- return privPem, pubPem, tokenString, nil
- }
- func genCARoot() (*x509.Certificate, []byte, *rsa.PrivateKey, error) {
- tpl := x509.Certificate{
- SerialNumber: big.NewInt(1),
- Subject: pkix.Name{
- Country: []string{"/dev/null"},
- Organization: []string{"External Secrets ACME"},
- CommonName: "External Secrets Vault CA",
- },
- NotBefore: time.Now(),
- NotAfter: time.Now().Add(time.Hour),
- KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
- BasicConstraintsValid: true,
- IsCA: true,
- MaxPathLen: 2,
- }
- pkey, err := rsa.GenerateKey(rand.Reader, 2048)
- if err != nil {
- return nil, nil, nil, err
- }
- rootCert, rootPEM, err := genCert(&tpl, &tpl, &pkey.PublicKey, pkey)
- return rootCert, rootPEM, pkey, err
- }
- func genCert(template, parent *x509.Certificate, publicKey *rsa.PublicKey, privateKey *rsa.PrivateKey) (*x509.Certificate, []byte, error) {
- certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, privateKey)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
- }
- cert, err := x509.ParseCertificate(certBytes)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to parse certificate: %w", err)
- }
- b := pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
- certPEM := pem.EncodeToMemory(&b)
- return cert, certPEM, err
- }
- func genPeerCert(signingCert *x509.Certificate, signingKey *rsa.PrivateKey, cn string, dnsNames []string) ([]byte, *rsa.PrivateKey, error) {
- pkey, err := rsa.GenerateKey(rand.Reader, 2048)
- if err != nil {
- return nil, nil, err
- }
- tpl := x509.Certificate{
- Subject: pkix.Name{
- Country: []string{"/dev/null"},
- Organization: []string{"External Secrets ACME"},
- CommonName: cn,
- },
- SerialNumber: big.NewInt(1),
- NotBefore: time.Now(),
- NotAfter: time.Now().Add(time.Hour),
- KeyUsage: x509.KeyUsageCRLSign,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
- IsCA: false,
- MaxPathLenZero: true,
- IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
- DNSNames: dnsNames,
- }
- _, serverPEM, err := genCert(&tpl, signingCert, &pkey.PublicKey, signingKey)
- return serverPEM, pkey, err
- }
- func mustReadFile(path string) []byte {
- b, err := os.ReadFile(path)
- if err != nil {
- panic(err)
- }
- return b
- }
|