conjur.go 10 KB

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