conjur.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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/x509"
  17. "encoding/base64"
  18. "encoding/json"
  19. "encoding/pem"
  20. "errors"
  21. "fmt"
  22. "path/filepath"
  23. "strings"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. // nolint
  26. . "github.com/onsi/ginkgo/v2"
  27. "github.com/cyberark/conjur-api-go/conjurapi"
  28. "github.com/cyberark/conjur-api-go/conjurapi/authn"
  29. "github.com/external-secrets/external-secrets-e2e/framework/util"
  30. )
  31. type Conjur struct {
  32. chart *HelmChart
  33. dataKey string
  34. Namespace string
  35. PodName string
  36. ConjurClient *conjurapi.Client
  37. ConjurURL string
  38. AdminApiKey string
  39. ConjurServerCA []byte
  40. portForwarder *PortForward
  41. }
  42. func NewConjur() *Conjur {
  43. repo := "conjur-conjur"
  44. dataKey := generateConjurDataKey()
  45. rootPem, rootKeyPEM, serverPem, serverKeyPem, err := genCertificates("conjur", "conjur-conjur-conjur-oss")
  46. if err != nil {
  47. Fail(err.Error())
  48. }
  49. return &Conjur{
  50. dataKey: dataKey,
  51. chart: &HelmChart{
  52. Namespace: "conjur",
  53. ReleaseName: "conjur-conjur",
  54. Chart: fmt.Sprintf("%s/conjur-oss", repo),
  55. // Use latest version of Conjur OSS. To pin to a specific version, uncomment the following line.
  56. // ChartVersion: "2.0.7",
  57. Repo: ChartRepo{
  58. Name: repo,
  59. URL: "https://cyberark.github.io/helm-charts",
  60. },
  61. Values: []string{filepath.Join(AssetDir(), "conjur.values.yaml")},
  62. Args: []string{
  63. "--create-namespace",
  64. "--set", "ssl.caCert=" + base64.StdEncoding.EncodeToString(rootPem),
  65. "--set", "ssl.caKey=" + base64.StdEncoding.EncodeToString(rootKeyPEM),
  66. "--set", "ssl.cert=" + base64.StdEncoding.EncodeToString(serverPem),
  67. "--set", "ssl.key=" + base64.StdEncoding.EncodeToString(serverKeyPem),
  68. },
  69. Vars: []StringTuple{
  70. {
  71. Key: "dataKey",
  72. Value: dataKey,
  73. },
  74. },
  75. },
  76. Namespace: "conjur",
  77. }
  78. }
  79. func (l *Conjur) Install() error {
  80. err := l.chart.Install()
  81. if err != nil {
  82. return err
  83. }
  84. err = l.initConjur()
  85. if err != nil {
  86. return err
  87. }
  88. err = l.configureConjur()
  89. if err != nil {
  90. return err
  91. }
  92. return nil
  93. }
  94. func (l *Conjur) initConjur() error {
  95. By("Waiting for conjur pods to be running")
  96. pl, err := util.WaitForPodsRunning(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
  97. LabelSelector: "app=conjur-oss",
  98. })
  99. if err != nil {
  100. return fmt.Errorf("error waiting for conjur to be running: %w", err)
  101. }
  102. l.PodName = pl.Items[0].Name
  103. By("Initializing conjur")
  104. // Get the auto generated certificates from the K8s secrets
  105. caCertSecret, err := util.GetKubeSecret(l.chart.config.KubeClientSet, l.Namespace, fmt.Sprintf("%s-conjur-ssl-ca-cert", l.chart.ReleaseName))
  106. if err != nil {
  107. return fmt.Errorf("error getting conjur ca cert: %w", err)
  108. }
  109. l.ConjurServerCA = caCertSecret.Data["tls.crt"]
  110. // Create "default" account
  111. _, err = util.ExecCmdWithContainer(
  112. l.chart.config.KubeClientSet,
  113. l.chart.config.KubeConfig,
  114. l.PodName, "conjur-oss", l.Namespace, "conjurctl account create default")
  115. if err != nil {
  116. return fmt.Errorf("error initializing conjur: %w", err)
  117. }
  118. // Retrieve the admin API key
  119. apiKey, err := util.ExecCmdWithContainer(
  120. l.chart.config.KubeClientSet,
  121. l.chart.config.KubeConfig,
  122. l.PodName, "conjur-oss", l.Namespace, "conjurctl role retrieve-key default:user:admin")
  123. if err != nil {
  124. return fmt.Errorf("error fetching admin API key: %w", err)
  125. }
  126. // Note: ExecCmdWithContainer includes the StdErr output with a warning about config directory.
  127. // Therefore we need to split the output and only use the first line.
  128. l.AdminApiKey = strings.Split(apiKey, "\n")[0]
  129. // This e2e test provider uses a local port-forwarded to talk to the vault API instead
  130. // of using the kubernetes service. This allows us to run the e2e test suite locally.
  131. l.portForwarder, err = NewPortForward(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
  132. "conjur-conjur-conjur-oss", l.chart.Namespace, 9443)
  133. if err != nil {
  134. return err
  135. }
  136. if err := l.portForwarder.Start(); err != nil {
  137. return err
  138. }
  139. l.ConjurURL = fmt.Sprintf("https://conjur-conjur-conjur-oss.%s.svc.cluster.local", l.Namespace)
  140. cfg := conjurapi.Config{
  141. Account: "default",
  142. ApplianceURL: fmt.Sprintf("https://localhost:%d", l.portForwarder.localPort),
  143. SSLCert: string(l.ConjurServerCA),
  144. }
  145. l.ConjurClient, err = conjurapi.NewClientFromKey(cfg, authn.LoginPair{
  146. Login: "admin",
  147. APIKey: l.AdminApiKey,
  148. })
  149. if err != nil {
  150. return fmt.Errorf("unable to create conjur client: %w", err)
  151. }
  152. return nil
  153. }
  154. func (l *Conjur) configureConjur() error {
  155. By("configuring conjur")
  156. // Construct Conjur policy for authn-jwt. This uses the token-app-property "sub" to
  157. // authenticate the host. This means that Conjur will determine which host is authenticating
  158. // based on the "sub" claim in the JWT token, which is provided by the Kubernetes service account.
  159. policy := `- !policy
  160. id: conjur/authn-jwt/eso-tests
  161. body:
  162. - !webservice
  163. - !variable public-keys
  164. - !variable issuer
  165. - !variable token-app-property
  166. - !variable audience`
  167. _, err := l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
  168. if err != nil {
  169. return fmt.Errorf("unable to load authn-jwt policy: %w", err)
  170. }
  171. // Construct Conjur policy for authn-jwt-hostid. This does not use the token-app-property variable
  172. // and instead uses the HostID passed in the authentication URL to determine which host is authenticating.
  173. // This is not the recommended way to authenticate, but it is needed for certain use cases where the
  174. // JWT token does not contain the "sub" claim.
  175. policy = `- !policy
  176. id: conjur/authn-jwt/eso-tests-hostid
  177. body:
  178. - !webservice
  179. - !variable public-keys
  180. - !variable issuer
  181. - !variable audience`
  182. _, err = l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
  183. if err != nil {
  184. return fmt.Errorf("unable to load authn-jwt policy: %w", err)
  185. }
  186. // Fetch the jwks info from the k8s cluster
  187. pubKeysJson, issuer, err := l.fetchJWKSandIssuer()
  188. if err != nil {
  189. return fmt.Errorf("unable to fetch jwks and issuer: %w", err)
  190. }
  191. // Set the variables for the authn-jwt policies
  192. secrets := map[string]string{
  193. "conjur/authn-jwt/eso-tests/audience": l.ConjurURL,
  194. "conjur/authn-jwt/eso-tests/issuer": issuer,
  195. "conjur/authn-jwt/eso-tests/public-keys": string(pubKeysJson),
  196. "conjur/authn-jwt/eso-tests/token-app-property": "sub",
  197. "conjur/authn-jwt/eso-tests-hostid/audience": l.ConjurURL,
  198. "conjur/authn-jwt/eso-tests-hostid/issuer": issuer,
  199. "conjur/authn-jwt/eso-tests-hostid/public-keys": string(pubKeysJson),
  200. }
  201. for secretPath, secretValue := range secrets {
  202. err := l.ConjurClient.AddSecret(secretPath, secretValue)
  203. if err != nil {
  204. return fmt.Errorf("unable to add secret %s: %w", secretPath, err)
  205. }
  206. }
  207. return nil
  208. }
  209. func (l *Conjur) fetchJWKSandIssuer() (pubKeysJson string, issuer string, err error) {
  210. kc := l.chart.config.KubeClientSet
  211. // Fetch the openid-configuration
  212. res, err := kc.CoreV1().RESTClient().Get().AbsPath("/.well-known/openid-configuration").DoRaw(GinkgoT().Context())
  213. if err != nil {
  214. return "", "", fmt.Errorf("unable to fetch openid-configuration: %w", err)
  215. }
  216. var openidConfig map[string]any
  217. json.Unmarshal(res, &openidConfig)
  218. issuer = openidConfig["issuer"].(string)
  219. // Fetch the jwks
  220. jwksJson, err := kc.CoreV1().RESTClient().Get().AbsPath("/openid/v1/jwks").DoRaw(GinkgoT().Context())
  221. if err != nil {
  222. return "", "", fmt.Errorf("unable to fetch jwks: %w", err)
  223. }
  224. var jwks map[string]any
  225. json.Unmarshal(jwksJson, &jwks)
  226. // Create a JSON object with the jwks that can be used by Conjur
  227. pubKeysObj := map[string]any{
  228. "type": "jwks",
  229. "value": jwks,
  230. }
  231. pubKeysJsonObj, err := json.Marshal(pubKeysObj)
  232. if err != nil {
  233. return "", "", fmt.Errorf("unable to marshal jwks: %w", err)
  234. }
  235. pubKeysJson = string(pubKeysJsonObj)
  236. return pubKeysJson, issuer, nil
  237. }
  238. // nolint:gocritic
  239. func genCertificates(namespace, serviceName string) ([]byte, []byte, []byte, []byte, error) {
  240. // gen server ca + certs
  241. rootCert, rootPem, rootKey, err := genCARoot()
  242. if err != nil {
  243. return nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
  244. }
  245. serverPem, serverKey, err := genPeerCert(rootCert, rootKey, "vault", []string{
  246. "localhost",
  247. serviceName,
  248. fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace)})
  249. if err != nil {
  250. return nil, nil, nil, nil, errors.New("unable to generate vault server cert")
  251. }
  252. serverKeyPem := pem.EncodeToMemory(&pem.Block{
  253. Type: privatePemType,
  254. Bytes: x509.MarshalPKCS1PrivateKey(serverKey)},
  255. )
  256. rootKeyPEM := pem.EncodeToMemory(&pem.Block{
  257. Type: privatePemType,
  258. Bytes: x509.MarshalPKCS1PrivateKey(rootKey),
  259. })
  260. return rootPem, rootKeyPEM, serverPem, serverKeyPem, err
  261. }
  262. func (l *Conjur) Logs() error {
  263. return l.chart.Logs()
  264. }
  265. func (l *Conjur) Uninstall() error {
  266. if l.portForwarder != nil {
  267. l.portForwarder.Close()
  268. l.portForwarder = nil
  269. }
  270. if err := l.chart.Uninstall(); err != nil {
  271. return err
  272. }
  273. return l.chart.config.KubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), l.chart.Namespace, metav1.DeleteOptions{})
  274. }
  275. func (l *Conjur) Setup(cfg *Config) error {
  276. return l.chart.Setup(cfg)
  277. }
  278. func generateConjurDataKey() string {
  279. // Generate a 32 byte cryptographically secure random string.
  280. // Normally this is done by running `conjurctl data-key generate`
  281. // but for test purposes we can generate it programmatically.
  282. b := make([]byte, 32)
  283. _, err := rand.Read(b)
  284. if err != nil {
  285. panic(fmt.Errorf("unable to generate random string: %w", err))
  286. }
  287. // Encode the bytes as a base64 string
  288. return base64.StdEncoding.EncodeToString(b)
  289. }