vault.go 13 KB

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