| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- /*
- 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
- http://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 (
- "context"
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/json"
- "encoding/pem"
- "fmt"
- "math/big"
- "net"
- "net/http"
- "os"
- "time"
- "github.com/golang-jwt/jwt"
- vault "github.com/hashicorp/vault/api"
- // nolint
- . "github.com/onsi/ginkgo"
- 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
- RootToken string
- VaultServerCA []byte
- ServerCert []byte
- ServerKey []byte
- VaultClientCA []byte
- ClientCert []byte
- ClientKey []byte
- JWTPubkey []byte
- JWTPrivKey []byte
- JWTToken string
- JWTRole string
- KubernetesAuthPath string
- KubernetesAuthRole string
- AppRoleSecret string
- AppRoleID string
- AppRolePath string
- }
- const privatePemType = "RSA PRIVATE KEY"
- func NewVault(namespace string) *Vault {
- repo := "hashicorp-" + namespace
- return &Vault{
- chart: &HelmChart{
- Namespace: namespace,
- ReleaseName: fmt.Sprintf("vault-%s", namespace), // avoid cluster role collision
- Chart: fmt.Sprintf("%s/vault", repo),
- ChartVersion: "0.11.0",
- Repo: ChartRepo{
- Name: repo,
- URL: "https://helm.releases.hashicorp.com",
- },
- Values: []string{"/k8s/vault.values.yaml"},
- },
- Namespace: namespace,
- }
- }
- type OperatorInitResponse struct {
- UnsealKeysB64 []string `json:"unseal_keys_b64"`
- RootToken string `json:"root_token"`
- }
- func (l *Vault) Install() error {
- By("Installing vault in " + l.Namespace)
- err := l.chart.Install()
- if err != nil {
- return err
- }
- err = l.initVault()
- if err != nil {
- return err
- }
- err = l.configureVault()
- if err != nil {
- return err
- }
- return nil
- }
- func (l *Vault) initVault() error {
- sec := &v1.Secret{
- ObjectMeta: metav1.ObjectMeta{
- Name: "vault-tls-config",
- Namespace: l.Namespace,
- },
- Data: map[string][]byte{},
- }
- // vault-config contains vault init config and policies
- files, err := os.ReadDir("/k8s/vault-config")
- if err != nil {
- return err
- }
- for _, f := range files {
- name := f.Name()
- data := mustReadFile(fmt.Sprintf("/k8s/vault-config/%s", name))
- sec.Data[name] = data
- }
- // gen certificates and put them into the secret
- serverRootPem, serverPem, serverKeyPem, clientRootPem, clientPem, clientKeyPem, err := genVaultCertificates(l.Namespace)
- 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)
- }
- // pass certs to secret
- 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
- // 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.JWTRole = "external-secrets-operator" // see configure-vault.sh
- l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh
- l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh
- By("Creating vault TLS secret")
- err = l.chart.config.CRClient.Create(context.Background(), sec)
- if err != nil {
- return err
- }
- By("Waiting for vault pods to be running")
- 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
- By("Initializing vault")
- 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)
- }
- By("Parsing init response")
- var res OperatorInitResponse
- err = json.Unmarshal([]byte(out), &res)
- if err != nil {
- return err
- }
- l.RootToken = res.RootToken
- By("Unsealing vault")
- 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)
- }
- 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://vault-%s.%s.svc.cluster.local:8200", l.Namespace, l.Namespace)
- cfg.Address = l.VaultURL
- 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 {
- By("configuring vault")
- 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)
- if err != nil {
- return err
- }
- defer 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)
- if err != nil {
- return err
- }
- defer 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 {
- return l.chart.Uninstall()
- }
- func (l *Vault) Setup(cfg *Config) error {
- return l.chart.Setup(cfg)
- }
- // nolint:gocritic
- func genVaultCertificates(namespace 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",
- "vault-" + namespace,
- fmt.Sprintf("vault-%s.%s.svc.cluster.local", namespace, namespace)})
- if err != nil {
- return nil, nil, nil, nil, nil, nil, fmt.Errorf("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, fmt.Errorf("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
- }
|