vault.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package addon
  14. import (
  15. "crypto/rand"
  16. "crypto/rsa"
  17. "crypto/x509"
  18. "crypto/x509/pkix"
  19. "encoding/json"
  20. "encoding/pem"
  21. "errors"
  22. "fmt"
  23. "math/big"
  24. "net"
  25. "net/http"
  26. "os"
  27. "path/filepath"
  28. "time"
  29. rbacv1 "k8s.io/api/rbac/v1"
  30. "k8s.io/apimachinery/pkg/types"
  31. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  32. "github.com/golang-jwt/jwt/v4"
  33. vault "github.com/hashicorp/vault/api"
  34. . "github.com/onsi/ginkgo/v2"
  35. v1 "k8s.io/api/core/v1"
  36. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  37. "github.com/external-secrets/external-secrets-e2e/framework/util"
  38. )
  39. type Vault struct {
  40. chart *HelmChart
  41. Namespace string
  42. PodName string
  43. VaultClient *vault.Client
  44. VaultURL string
  45. VaultMtlsURL string
  46. portForwarder *PortForward
  47. RootToken string
  48. VaultServerCA []byte
  49. ServerCert []byte
  50. ServerKey []byte
  51. VaultClientCA []byte
  52. ClientCert []byte
  53. ClientKey []byte
  54. JWTPubkey []byte
  55. JWTPrivKey []byte
  56. JWTToken string
  57. JWTRole string
  58. JWTPath string
  59. JWTK8sPath string
  60. KubernetesAuthPath string
  61. KubernetesAuthRole string
  62. AppRoleSecret string
  63. AppRoleID string
  64. AppRolePath string
  65. }
  66. const privatePemType = "RSA PRIVATE KEY"
  67. func NewVault() *Vault {
  68. repo := "hashicorp-vault"
  69. return &Vault{
  70. chart: &HelmChart{
  71. Namespace: "vault",
  72. ReleaseName: "vault",
  73. Chart: fmt.Sprintf("%s/vault", repo),
  74. ChartVersion: "0.30.1",
  75. Repo: ChartRepo{
  76. Name: repo,
  77. URL: "https://helm.releases.hashicorp.com",
  78. },
  79. Args: []string{
  80. "--create-namespace",
  81. },
  82. Values: []string{filepath.Join(AssetDir(), "vault.values.yaml")},
  83. },
  84. Namespace: "vault",
  85. }
  86. }
  87. type OperatorInitResponse struct {
  88. UnsealKeysB64 []string `json:"unseal_keys_b64"`
  89. RootToken string `json:"root_token"`
  90. }
  91. func (l *Vault) Install() error {
  92. // From Kubernetes 1.32+ on the oidc endpoint is not available to unauthenticated clients.
  93. // We create this clusterrole to allow vault to access the oidc endpoint.
  94. // see: https://github.com/ansible-collections/kubernetes.core/issues/868
  95. crb := &rbacv1.ClusterRoleBinding{
  96. ObjectMeta: metav1.ObjectMeta{
  97. Name: "allow-anon-oidc",
  98. },
  99. }
  100. _, err := controllerutil.CreateOrUpdate(GinkgoT().Context(), l.chart.config.CRClient, crb, func() error {
  101. crb.Subjects = []rbacv1.Subject{
  102. {
  103. APIGroup: "rbac.authorization.k8s.io",
  104. Kind: "User",
  105. Name: "system:anonymous",
  106. },
  107. }
  108. crb.RoleRef = rbacv1.RoleRef{
  109. APIGroup: "rbac.authorization.k8s.io",
  110. Kind: "ClusterRole",
  111. Name: "system:service-account-issuer-discovery",
  112. }
  113. return nil
  114. })
  115. if err != nil {
  116. return err
  117. }
  118. if err = l.chart.Install(); err != nil {
  119. return err
  120. }
  121. if err = l.patchVaultService(); err != nil {
  122. return err
  123. }
  124. if err = l.initVault(); err != nil {
  125. return err
  126. }
  127. if err = l.configureVault(); err != nil {
  128. return err
  129. }
  130. return nil
  131. }
  132. func (l *Vault) patchVaultService() error {
  133. serviceName := l.chart.ReleaseName
  134. servicePatch := []byte(`[{"op": "add", "path": "/spec/ports/-", "value": { "name": "https-mtls", "port": 8210, "protocol": "TCP", "targetPort": 8210 }}]`)
  135. clientSet := l.chart.config.KubeClientSet
  136. _, err := clientSet.CoreV1().Services(l.Namespace).
  137. Patch(GinkgoT().Context(), serviceName, types.JSONPatchType, servicePatch, metav1.PatchOptions{})
  138. return err
  139. }
  140. func (l *Vault) initVault() error {
  141. // gen certificates and put them into the secret
  142. serverRootPem, serverPem, serverKeyPem, clientRootPem, clientPem, clientKeyPem, err := genVaultCertificates(l.Namespace, l.chart.ReleaseName)
  143. if err != nil {
  144. return fmt.Errorf("unable to gen vault certs: %w", err)
  145. }
  146. jwtPrivkey, jwtPubkey, jwtToken, err := genVaultJWTKeys()
  147. if err != nil {
  148. return fmt.Errorf("unable to generate vault jwt keys: %w", err)
  149. }
  150. // make certs available to the struct
  151. // so it can be used by the provider
  152. l.VaultServerCA = serverRootPem
  153. l.ServerCert = serverPem
  154. l.ServerKey = serverKeyPem
  155. l.VaultClientCA = clientRootPem
  156. l.ClientCert = clientPem
  157. l.ClientKey = clientKeyPem
  158. l.JWTPrivKey = jwtPrivkey
  159. l.JWTPubkey = jwtPubkey
  160. l.JWTToken = jwtToken
  161. l.JWTPath = "myjwt" // see configure-vault.sh
  162. l.JWTK8sPath = "myjwtk8s" // see configure-vault.sh
  163. l.JWTRole = "external-secrets-operator" // see configure-vault.sh
  164. l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh
  165. l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh
  166. // vault-config contains vault init config and policies
  167. files, err := os.ReadDir(fmt.Sprintf("%s/vault-config", AssetDir()))
  168. if err != nil {
  169. return err
  170. }
  171. sec := &v1.Secret{
  172. ObjectMeta: metav1.ObjectMeta{
  173. Name: "vault-tls-config",
  174. Namespace: l.Namespace,
  175. },
  176. Data: map[string][]byte{},
  177. }
  178. _, err = controllerutil.CreateOrUpdate(GinkgoT().Context(), l.chart.config.CRClient, sec, func() error {
  179. sec.Data = map[string][]byte{}
  180. for _, f := range files {
  181. name := f.Name()
  182. data := mustReadFile(fmt.Sprintf("%s/vault-config/%s", AssetDir(), name))
  183. sec.Data[name] = data
  184. }
  185. sec.Data["vault-server-ca.pem"] = serverRootPem
  186. sec.Data["server-cert.pem"] = serverPem
  187. sec.Data["server-cert-key.pem"] = serverKeyPem
  188. sec.Data["vault-client-ca.pem"] = clientRootPem
  189. sec.Data["es-client.pem"] = clientPem
  190. sec.Data["es-client-key.pem"] = clientKeyPem
  191. sec.Data["jwt-pubkey.pem"] = jwtPubkey
  192. return nil
  193. })
  194. if err != nil {
  195. return err
  196. }
  197. pl, err := util.WaitForPodsRunning(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
  198. LabelSelector: "app.kubernetes.io/name=vault",
  199. })
  200. if err != nil {
  201. return fmt.Errorf("error waiting for vault to be running: %w", err)
  202. }
  203. l.PodName = pl.Items[0].Name
  204. out, err := util.ExecCmd(
  205. l.chart.config.KubeClientSet,
  206. l.chart.config.KubeConfig,
  207. l.PodName, l.Namespace, "vault operator init --format=json")
  208. if err != nil {
  209. return fmt.Errorf("error initializing vault: %w", err)
  210. }
  211. var res OperatorInitResponse
  212. err = json.Unmarshal([]byte(out), &res)
  213. if err != nil {
  214. return err
  215. }
  216. l.RootToken = res.RootToken
  217. for _, k := range res.UnsealKeysB64 {
  218. _, err = util.ExecCmd(
  219. l.chart.config.KubeClientSet,
  220. l.chart.config.KubeConfig,
  221. l.PodName, l.Namespace, "vault operator unseal "+k)
  222. if err != nil {
  223. return fmt.Errorf("unable to unseal vault: %w", err)
  224. }
  225. }
  226. // vault becomes ready after it has been unsealed
  227. err = util.WaitForPodsReady(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
  228. LabelSelector: "app.kubernetes.io/name=vault",
  229. })
  230. if err != nil {
  231. return fmt.Errorf("error waiting for vault to be ready: %w", err)
  232. }
  233. // This e2e test provider uses a local port-forwarded to talk to the vault API instead
  234. // of using the kubernetes service. This allows us to run the e2e test suite locally.
  235. l.portForwarder, err = NewPortForward(l.chart.config.KubeClientSet, l.chart.config.KubeConfig, "vault", l.chart.Namespace, 8200)
  236. if err != nil {
  237. return err
  238. }
  239. if err := l.portForwarder.Start(); err != nil {
  240. return err
  241. }
  242. serverCA := l.VaultServerCA
  243. caCertPool := x509.NewCertPool()
  244. ok := caCertPool.AppendCertsFromPEM(serverCA)
  245. if !ok {
  246. panic("unable to append server ca cert")
  247. }
  248. cfg := vault.DefaultConfig()
  249. l.VaultURL = fmt.Sprintf("https://%s.%s.svc.cluster.local:8200", l.chart.ReleaseName, l.Namespace)
  250. l.VaultMtlsURL = fmt.Sprintf("https://%s.%s.svc.cluster.local:8210", l.chart.ReleaseName, l.Namespace)
  251. cfg.Address = fmt.Sprintf("https://localhost:%d", l.portForwarder.localPort)
  252. cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = caCertPool
  253. l.VaultClient, err = vault.NewClient(cfg)
  254. if err != nil {
  255. return fmt.Errorf("unable to create vault client: %w", err)
  256. }
  257. l.VaultClient.SetToken(l.RootToken)
  258. return nil
  259. }
  260. func (l *Vault) configureVault() error {
  261. cmd := `sh /etc/vault-config/configure-vault.sh %s`
  262. _, err := util.ExecCmd(
  263. l.chart.config.KubeClientSet,
  264. l.chart.config.KubeConfig,
  265. l.PodName, l.Namespace, fmt.Sprintf(cmd, l.RootToken))
  266. if err != nil {
  267. return fmt.Errorf("unable to configure vault: %w", err)
  268. }
  269. // configure appRole
  270. l.AppRolePath = "myapprole"
  271. req := l.VaultClient.NewRequest(http.MethodGet, fmt.Sprintf("/v1/auth/%s/role/eso-e2e-role/role-id", l.AppRolePath))
  272. res, err := l.VaultClient.RawRequest(req) //nolint:staticcheck
  273. if err != nil {
  274. return err
  275. }
  276. defer func() {
  277. _ = res.Body.Close()
  278. }()
  279. sec, err := vault.ParseSecret(res.Body)
  280. if err != nil {
  281. return err
  282. }
  283. l.AppRoleID = sec.Data["role_id"].(string)
  284. // parse role id
  285. req = l.VaultClient.NewRequest(http.MethodPost, fmt.Sprintf("/v1/auth/%s/role/eso-e2e-role/secret-id", l.AppRolePath))
  286. res, err = l.VaultClient.RawRequest(req) //nolint:staticcheck
  287. if err != nil {
  288. return err
  289. }
  290. defer func() {
  291. _ = res.Body.Close()
  292. }()
  293. sec, err = vault.ParseSecret(res.Body)
  294. if err != nil {
  295. return err
  296. }
  297. l.AppRoleSecret = sec.Data["secret_id"].(string)
  298. return nil
  299. }
  300. func (l *Vault) Logs() error {
  301. return l.chart.Logs()
  302. }
  303. func (l *Vault) Uninstall() error {
  304. if l.portForwarder != nil {
  305. l.portForwarder.Close()
  306. l.portForwarder = nil
  307. }
  308. if err := l.chart.Uninstall(); err != nil {
  309. return err
  310. }
  311. return l.chart.config.KubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), l.chart.Namespace, metav1.DeleteOptions{})
  312. }
  313. func (l *Vault) Setup(cfg *Config) error {
  314. return l.chart.Setup(cfg)
  315. }
  316. // nolint:gocritic
  317. func genVaultCertificates(namespace, serviceName string) ([]byte, []byte, []byte, []byte, []byte, []byte, error) {
  318. // gen server ca + certs
  319. serverRootCert, serverRootPem, serverRootKey, err := genCARoot()
  320. if err != nil {
  321. return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
  322. }
  323. serverPem, serverKey, err := genPeerCert(serverRootCert, serverRootKey, "vault", []string{
  324. "localhost",
  325. serviceName,
  326. fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace)})
  327. if err != nil {
  328. return nil, nil, nil, nil, nil, nil, errors.New("unable to generate vault server cert")
  329. }
  330. serverKeyPem := pem.EncodeToMemory(&pem.Block{
  331. Type: privatePemType,
  332. Bytes: x509.MarshalPKCS1PrivateKey(serverKey)},
  333. )
  334. // gen client ca + certs
  335. clientRootCert, clientRootPem, clientRootKey, err := genCARoot()
  336. if err != nil {
  337. return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
  338. }
  339. clientPem, clientKey, err := genPeerCert(clientRootCert, clientRootKey, "vault-client", nil)
  340. if err != nil {
  341. return nil, nil, nil, nil, nil, nil, errors.New("unable to generate vault server cert")
  342. }
  343. clientKeyPem := pem.EncodeToMemory(&pem.Block{
  344. Type: privatePemType,
  345. Bytes: x509.MarshalPKCS1PrivateKey(clientKey)},
  346. )
  347. return serverRootPem, serverPem, serverKeyPem, clientRootPem, clientPem, clientKeyPem, err
  348. }
  349. func genVaultJWTKeys() ([]byte, []byte, string, error) {
  350. key, err := rsa.GenerateKey(rand.Reader, 2048)
  351. if err != nil {
  352. return nil, nil, "", err
  353. }
  354. privPem := pem.EncodeToMemory(&pem.Block{
  355. Type: privatePemType,
  356. Bytes: x509.MarshalPKCS1PrivateKey(key),
  357. })
  358. pk, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
  359. if err != nil {
  360. return nil, nil, "", err
  361. }
  362. pubPem := pem.EncodeToMemory(&pem.Block{
  363. Type: "RSA PUBLIC KEY",
  364. Bytes: pk,
  365. })
  366. token := jwt.NewWithClaims(jwt.SigningMethodPS256, jwt.MapClaims{
  367. "aud": "vault.client",
  368. "sub": "vault@example",
  369. "iss": "example.iss",
  370. "user": "eso",
  371. "exp": time.Now().Add(time.Hour).Unix(),
  372. "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
  373. })
  374. // Sign and get the complete encoded token as a string using the secret
  375. tokenString, err := token.SignedString(key)
  376. if err != nil {
  377. return nil, nil, "", err
  378. }
  379. return privPem, pubPem, tokenString, nil
  380. }
  381. func genCARoot() (*x509.Certificate, []byte, *rsa.PrivateKey, error) {
  382. tpl := x509.Certificate{
  383. SerialNumber: big.NewInt(1),
  384. Subject: pkix.Name{
  385. Country: []string{"/dev/null"},
  386. Organization: []string{"External Secrets ACME"},
  387. CommonName: "External Secrets Vault CA",
  388. },
  389. NotBefore: time.Now(),
  390. NotAfter: time.Now().Add(time.Hour),
  391. KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
  392. ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
  393. BasicConstraintsValid: true,
  394. IsCA: true,
  395. MaxPathLen: 2,
  396. }
  397. pkey, err := rsa.GenerateKey(rand.Reader, 2048)
  398. if err != nil {
  399. return nil, nil, nil, err
  400. }
  401. rootCert, rootPEM, err := genCert(&tpl, &tpl, &pkey.PublicKey, pkey)
  402. return rootCert, rootPEM, pkey, err
  403. }
  404. func genCert(template, parent *x509.Certificate, publicKey *rsa.PublicKey, privateKey *rsa.PrivateKey) (*x509.Certificate, []byte, error) {
  405. certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, privateKey)
  406. if err != nil {
  407. return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
  408. }
  409. cert, err := x509.ParseCertificate(certBytes)
  410. if err != nil {
  411. return nil, nil, fmt.Errorf("failed to parse certificate: %w", err)
  412. }
  413. b := pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
  414. certPEM := pem.EncodeToMemory(&b)
  415. return cert, certPEM, err
  416. }
  417. func genPeerCert(signingCert *x509.Certificate, signingKey *rsa.PrivateKey, cn string, dnsNames []string) ([]byte, *rsa.PrivateKey, error) {
  418. pkey, err := rsa.GenerateKey(rand.Reader, 2048)
  419. if err != nil {
  420. return nil, nil, err
  421. }
  422. tpl := x509.Certificate{
  423. Subject: pkix.Name{
  424. Country: []string{"/dev/null"},
  425. Organization: []string{"External Secrets ACME"},
  426. CommonName: cn,
  427. },
  428. SerialNumber: big.NewInt(1),
  429. NotBefore: time.Now(),
  430. NotAfter: time.Now().Add(time.Hour),
  431. KeyUsage: x509.KeyUsageCRLSign,
  432. ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
  433. IsCA: false,
  434. MaxPathLenZero: true,
  435. IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
  436. DNSNames: dnsNames,
  437. }
  438. _, serverPEM, err := genCert(&tpl, signingCert, &pkey.PublicKey, signingKey)
  439. return serverPEM, pkey, err
  440. }
  441. func mustReadFile(path string) []byte {
  442. b, err := os.ReadFile(path)
  443. if err != nil {
  444. panic(err)
  445. }
  446. return b
  447. }