vault.go 13 KB

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