vault.go 13 KB

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