conjur.go 8.0 KB

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