conjur.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  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. "context"
  16. "crypto/rand"
  17. "crypto/rsa"
  18. "crypto/x509"
  19. "crypto/x509/pkix"
  20. "encoding/base64"
  21. "encoding/json"
  22. "encoding/pem"
  23. "errors"
  24. "fmt"
  25. "math/big"
  26. "net/url"
  27. "path/filepath"
  28. "strings"
  29. "time"
  30. corev1 "k8s.io/api/core/v1"
  31. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  32. "k8s.io/apimachinery/pkg/util/wait"
  33. "k8s.io/client-go/kubernetes"
  34. "k8s.io/client-go/rest"
  35. // nolint
  36. . "github.com/onsi/ginkgo/v2"
  37. "github.com/cyberark/conjur-api-go/conjurapi"
  38. "github.com/cyberark/conjur-api-go/conjurapi/authn"
  39. "github.com/external-secrets/external-secrets-e2e/framework/util"
  40. )
  41. const (
  42. // authenticatorServicePort is the port the authenticator-service binary listens on.
  43. // This matches the CONJUR_AUTHENTICATOR_SERVICE_URL default of http://localhost:5681
  44. // and must not collide with the conjur-oss container's own PORT (8080), since both
  45. // containers share the pod's network namespace.
  46. authenticatorServicePort = "5681"
  47. // authenticatorServiceContainerName is the name of the sidecar container running
  48. // the authenticator-service binary inside the conjur-oss pod.
  49. authenticatorServiceContainerName = "authenticator-service"
  50. // SpiffeTrustDomain is the SPIFFE trust domain used for the authn-cert authenticator
  51. // operating in 'spiffe' host mode.
  52. SpiffeTrustDomain = "eso-tests.example.org"
  53. // SpiffeWorkloadID is the SPIFFE ID's workload path, embedded as a URI SAN in the
  54. // SPIFFE client certificate (spiffe://<trust-domain>/<SpiffeWorkloadID>).
  55. SpiffeWorkloadID = "vm-spiffe"
  56. // SpiffeIdentityPath is the Conjur policy path that the authn-cert authenticator
  57. // prepends to the SPIFFE workload path to derive the full host identity. The resulting
  58. // host must exist at policy path SpiffeIdentityPath/SpiffeWorkloadID.
  59. SpiffeIdentityPath = "eso-tests/spiffe"
  60. )
  61. type Conjur struct {
  62. chart *HelmChart
  63. dataKey string
  64. Namespace string
  65. PodName string
  66. ConjurClient *conjurapi.Client
  67. ConjurURL string
  68. AdminApiKey string
  69. ConjurServerCA []byte
  70. ClientCert []byte
  71. ClientKey []byte
  72. SpiffeCert []byte
  73. SpiffeKey []byte
  74. portForwarder *PortForward
  75. }
  76. func NewConjur() *Conjur {
  77. repo := "conjur-conjur"
  78. dataKey := generateConjurDataKey()
  79. rootPem, rootKeyPEM, serverPem, serverKeyPem, err := genCertificates("conjur", "conjur-conjur-conjur-oss")
  80. if err != nil {
  81. Fail(err.Error())
  82. }
  83. // Generate client certificates for cert-based authentication.
  84. // The CN "vm-01" matches the host identity in the cert host policy.
  85. rootBlock, _ := pem.Decode(rootPem)
  86. if rootBlock == nil {
  87. Fail("unable to decode root cert PEM")
  88. }
  89. rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
  90. if err != nil {
  91. Fail(fmt.Sprintf("unable to parse root cert: %v", err))
  92. }
  93. rootKeyBlock, _ := pem.Decode(rootKeyPEM)
  94. if rootKeyBlock == nil {
  95. Fail("unable to decode root key PEM")
  96. }
  97. rootKey, err := x509.ParsePKCS1PrivateKey(rootKeyBlock.Bytes)
  98. if err != nil {
  99. Fail(fmt.Sprintf("unable to parse root key: %v", err))
  100. }
  101. clientCertPem, clientKeyRSA, err := genClientAuthCert(rootCert, rootKey, "vm-01", "")
  102. if err != nil {
  103. Fail(fmt.Sprintf("unable to generate client cert: %v", err))
  104. }
  105. clientKeyPem := pem.EncodeToMemory(&pem.Block{
  106. Type: privatePemType,
  107. Bytes: x509.MarshalPKCS1PrivateKey(clientKeyRSA),
  108. })
  109. // Generate a second client certificate carrying a SPIFFE URI SAN, used by the
  110. // authn-cert authenticator's 'spiffe' host mode, where the authenticated host's
  111. // identity is derived from the certificate itself rather than the request path.
  112. spiffeCertPem, spiffeKeyRSA, err := genClientAuthCert(rootCert, rootKey, "",
  113. fmt.Sprintf("spiffe://%s/%s", SpiffeTrustDomain, SpiffeWorkloadID))
  114. if err != nil {
  115. Fail(fmt.Sprintf("unable to generate spiffe client cert: %v", err))
  116. }
  117. spiffeKeyPem := pem.EncodeToMemory(&pem.Block{
  118. Type: privatePemType,
  119. Bytes: x509.MarshalPKCS1PrivateKey(spiffeKeyRSA),
  120. })
  121. return &Conjur{
  122. dataKey: dataKey,
  123. chart: &HelmChart{
  124. Namespace: "conjur",
  125. ReleaseName: "conjur-conjur",
  126. Chart: fmt.Sprintf("%s/conjur-oss", repo),
  127. // Pinned to the current latest conjur-oss chart release. Bump alongside conjurReleaseVersion.
  128. ChartVersion: "2.1.1",
  129. Repo: ChartRepo{
  130. Name: repo,
  131. URL: "https://cyberark.github.io/helm-charts",
  132. },
  133. Values: []string{filepath.Join(AssetDir(), "conjur.values.yaml")},
  134. Args: []string{
  135. "--create-namespace",
  136. "--set", "ssl.caCert=" + base64.StdEncoding.EncodeToString(rootPem),
  137. "--set", "ssl.caKey=" + base64.StdEncoding.EncodeToString(rootKeyPEM),
  138. "--set", "ssl.cert=" + base64.StdEncoding.EncodeToString(serverPem),
  139. "--set", "ssl.key=" + base64.StdEncoding.EncodeToString(serverKeyPem),
  140. },
  141. Vars: []StringTuple{
  142. {
  143. Key: "dataKey",
  144. Value: dataKey,
  145. },
  146. },
  147. },
  148. Namespace: "conjur",
  149. ClientCert: clientCertPem,
  150. ClientKey: clientKeyPem,
  151. SpiffeCert: spiffeCertPem,
  152. SpiffeKey: spiffeKeyPem,
  153. }
  154. }
  155. // Install sets up the conjur-oss chart, the authenticator-service sidecar, and configures
  156. // the authenticators. The install-then-patch-then-exec sequence spans several kind/kubelet
  157. // eventually-consistent steps (Deployment rollout, pod scheduling, kubelet exec/attach
  158. // readiness); under load these can intermittently race even with generous waits at each
  159. // step (e.g. a pod reported Ready via the API can still be replaced by a subsequent
  160. // reconcile before it is actually exec-attachable). Rather than chase every individual
  161. // timing window, retry the whole sequence a few times, tearing down between attempts.
  162. func (l *Conjur) Install() error {
  163. const maxAttempts = 3
  164. var lastErr error
  165. for attempt := 1; attempt <= maxAttempts; attempt++ {
  166. if attempt > 1 {
  167. By(fmt.Sprintf("retrying conjur install (attempt %d/%d) after: %v", attempt, maxAttempts, lastErr))
  168. if err := l.teardown(); err != nil {
  169. return fmt.Errorf("unable to tear down conjur before retry: %w", err)
  170. }
  171. }
  172. if lastErr = l.installOnce(); lastErr == nil {
  173. return nil
  174. }
  175. }
  176. return fmt.Errorf("conjur install failed after %d attempts: %w", maxAttempts, lastErr)
  177. }
  178. func (l *Conjur) installOnce() error {
  179. if err := l.chart.Install(); err != nil {
  180. return err
  181. }
  182. if err := l.patchAuthenticatorServiceSidecar(); err != nil {
  183. return err
  184. }
  185. if err := l.initConjur(); err != nil {
  186. return err
  187. }
  188. return l.configureConjur()
  189. }
  190. // teardown removes the conjur-oss release and namespace so installOnce can run again from
  191. // a clean slate. Errors are ignored: the namespace may not exist yet if chart.Install()
  192. // itself failed on a previous attempt.
  193. func (l *Conjur) teardown() error {
  194. if l.portForwarder != nil {
  195. l.portForwarder.Close()
  196. l.portForwarder = nil
  197. }
  198. _ = l.chart.Uninstall()
  199. err := l.chart.config.KubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), l.chart.Namespace, metav1.DeleteOptions{})
  200. if err != nil {
  201. return nil //nolint:nilerr // namespace may already be gone; nothing to clean up.
  202. }
  203. return util.WaitForKubeNamespaceNotExist(l.chart.Namespace, l.chart.config.KubeClientSet)
  204. }
  205. // retryExecCmd retries util.ExecCmdWithContainer briefly to absorb the window where a
  206. // freshly-created pod reports Ready via the API before the kubelet's exec/attach endpoint
  207. // for its containers is actually available.
  208. func retryExecCmd(client kubernetes.Interface, config *rest.Config, podName, containerName, namespace, command string) error {
  209. var lastErr error
  210. for i := 0; i < 10; i++ {
  211. if i > 0 {
  212. time.Sleep(1 * time.Second)
  213. }
  214. if _, lastErr = util.ExecCmdWithContainer(client, config, podName, containerName, namespace, command); lastErr == nil {
  215. return nil
  216. }
  217. }
  218. return lastErr
  219. }
  220. // waitForConjurServiceReady polls the Conjur Service URL from inside the cluster (via exec
  221. // into the conjur-oss pod) until it gets a real HTTP response, absorbing kube-proxy
  222. // Endpoints-sync lag after the sidecar rollout. curl's own TLS verification is disabled
  223. // (-k) because this only checks that the Service routes to a live, responsive nginx; actual
  224. // certificate validation is exercised by the real authentication flows later.
  225. func (l *Conjur) waitForConjurServiceReady() error {
  226. cmd := fmt.Sprintf("curl -sk -o /dev/null -w '%%{http_code}' %s/status", l.ConjurURL)
  227. return wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
  228. out, err := util.ExecCmdWithContainer(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
  229. l.PodName, "conjur-oss", l.Namespace, cmd)
  230. if err != nil {
  231. return false, nil
  232. }
  233. return strings.Contains(out, "200"), nil
  234. })
  235. }
  236. func (l *Conjur) initConjur() error {
  237. By("Waiting for conjur pods to be running")
  238. pl, err := util.WaitForPodsRunning(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
  239. LabelSelector: "app=conjur-oss",
  240. })
  241. if err != nil {
  242. return fmt.Errorf("error waiting for conjur to be running: %w", err)
  243. }
  244. l.PodName = pl.Items[0].Name
  245. By("Initializing conjur")
  246. // Get the auto generated certificates from the K8s secrets
  247. caCertSecret, err := util.GetKubeSecret(l.chart.config.KubeClientSet, l.Namespace, fmt.Sprintf("%s-conjur-ssl-ca-cert", l.chart.ReleaseName))
  248. if err != nil {
  249. return fmt.Errorf("error getting conjur ca cert: %w", err)
  250. }
  251. l.ConjurServerCA = caCertSecret.Data["tls.crt"]
  252. // Create "default" account. A freshly-created pod can report Ready in the API before
  253. // the kubelet's exec/attach endpoint for its containers is actually wired up, so the
  254. // first exec attempt against it can fail with "container not found" even though the
  255. // container is running; retry briefly to absorb that race.
  256. if err := retryExecCmd(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
  257. l.PodName, "conjur-oss", l.Namespace, "conjurctl account create default"); err != nil {
  258. return fmt.Errorf("error initializing conjur: %w", err)
  259. }
  260. // Retrieve the admin API key
  261. apiKey, err := util.ExecCmdWithContainer(
  262. l.chart.config.KubeClientSet,
  263. l.chart.config.KubeConfig,
  264. l.PodName, "conjur-oss", l.Namespace, "conjurctl role retrieve-key default:user:admin")
  265. if err != nil {
  266. return fmt.Errorf("error fetching admin API key: %w", err)
  267. }
  268. // Note: ExecCmdWithContainer includes the StdErr output with a warning about config directory.
  269. // Therefore we need to split the output and only use the first line.
  270. l.AdminApiKey = strings.Split(apiKey, "\n")[0]
  271. // This e2e test provider uses a local port-forwarded to talk to the vault API instead
  272. // of using the kubernetes service. This allows us to run the e2e test suite locally.
  273. l.portForwarder, err = NewPortForward(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
  274. "conjur-conjur-conjur-oss", l.chart.Namespace, 9443)
  275. if err != nil {
  276. return err
  277. }
  278. if err := l.portForwarder.Start(); err != nil {
  279. return err
  280. }
  281. l.ConjurURL = fmt.Sprintf("https://conjur-conjur-conjur-oss.%s.svc.cluster.local", l.Namespace)
  282. // The ExternalSecret controller reaches Conjur through this Service URL, which routes
  283. // through kube-proxy to whichever pod is currently in the Service's Endpoints. That
  284. // Endpoints list is reconciled asynchronously and can lag behind the pod-level readiness
  285. // checks in patchAuthenticatorServiceSidecar's rollout wait, especially right after the
  286. // sidecar-triggered rolling update. Probe through the Service URL itself (from inside the
  287. // cluster, exercising the same DNS/ClusterIP/kube-proxy path the controller will use)
  288. // before proceeding, so tests don't race a stale or not-yet-synced endpoint.
  289. if err := l.waitForConjurServiceReady(); err != nil {
  290. return fmt.Errorf("error waiting for conjur service to be reachable: %w", err)
  291. }
  292. cfg := conjurapi.Config{
  293. Account: "default",
  294. ApplianceURL: fmt.Sprintf("https://localhost:%d", l.portForwarder.localPort),
  295. SSLCert: string(l.ConjurServerCA),
  296. }
  297. l.ConjurClient, err = conjurapi.NewClientFromKey(cfg, authn.LoginPair{
  298. Login: "admin",
  299. APIKey: l.AdminApiKey,
  300. })
  301. if err != nil {
  302. return fmt.Errorf("unable to create conjur client: %w", err)
  303. }
  304. return nil
  305. }
  306. // conjurReleaseVersion is the pinned cyberark/conjur GitHub release used to fetch the
  307. // authenticator-service binary. Bump this when a newer release is needed.
  308. const conjurReleaseVersion = "v1.27.0"
  309. // authenticatorServiceInitScript downloads the authenticator-service binary matching the
  310. // node's architecture from the pinned cyberark/conjur GitHub release (see
  311. // conjurReleaseVersion), and writes its minimal config file, into a shared emptyDir volume
  312. // mounted by the sidecar container.
  313. //
  314. // The authenticator-service is not distributed as a container image, only as raw
  315. // per-architecture binaries attached to GitHub releases (see AUTHENTICATOR_SERVICE.md in
  316. // the cyberark/conjur repo), so it must be fetched at runtime rather than referenced by tag.
  317. const authenticatorServiceInitScript = `set -e
  318. ARCH=$(uname -m)
  319. case "$ARCH" in
  320. x86_64) BIN_ARCH=amd64 ;;
  321. aarch64|arm64) BIN_ARCH=arm64 ;;
  322. *) echo "unsupported architecture: $ARCH" >&2; exit 1 ;;
  323. esac
  324. DOWNLOAD_URL=$(curl -s https://api.github.com/repos/cyberark/conjur/releases/tags/` + conjurReleaseVersion + `\
  325. | grep -o "\"browser_download_url\": *\"[^\"]*authenticator_linux_[0-9]*_${BIN_ARCH}\"" \
  326. | head -1 | sed -E 's/.*"(https:[^"]+)".*/\1/')
  327. if [ -z "$DOWNLOAD_URL" ]; then
  328. echo "unable to determine authenticator-service download URL for arch ${BIN_ARCH}" >&2
  329. exit 1
  330. fi
  331. echo "downloading authenticator-service from ${DOWNLOAD_URL}"
  332. curl -sL -o /authsvc/authenticator-service "$DOWNLOAD_URL"
  333. chmod +x /authsvc/authenticator-service
  334. printf '{"port": "%s", "http_timeout": "10s"}' "` + authenticatorServicePort + `" > /authsvc/config.json`
  335. // patchAuthenticatorServiceSidecar adds an authenticator-service sidecar to the conjur-oss
  336. // deployment and enables the feature flag that lets Conjur delegate authn-cert credential
  337. // validation to it. Conjur OSS's authn-cert authenticator does not validate client
  338. // certificates itself; it calls out to this separate HTTP service (see
  339. // AUTHENTICATOR_SERVICE.md and CERTIFICATE_AUTH.md in the cyberark/conjur repo). The
  340. // conjur-oss Helm chart has no hook for adding sidecars, so the running Deployment is
  341. // patched directly after the initial chart install.
  342. func (l *Conjur) patchAuthenticatorServiceSidecar() error {
  343. By("patching conjur-oss deployment with authenticator-service sidecar")
  344. clientSet := l.chart.config.KubeClientSet
  345. deploymentName := fmt.Sprintf("%s-conjur-oss", l.chart.ReleaseName)
  346. deployment, err := clientSet.AppsV1().Deployments(l.Namespace).Get(GinkgoT().Context(), deploymentName, metav1.GetOptions{})
  347. if err != nil {
  348. return fmt.Errorf("unable to get conjur-oss deployment: %w", err)
  349. }
  350. binVolume := corev1.Volume{
  351. Name: "authsvc-bin",
  352. VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
  353. }
  354. binVolumeMount := corev1.VolumeMount{
  355. Name: binVolume.Name,
  356. MountPath: "/authsvc",
  357. }
  358. deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, binVolume)
  359. deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, corev1.Container{
  360. Name: "authenticator-service-fetch",
  361. Image: "curlimages/curl:8.11.0",
  362. Command: []string{"/bin/sh", "-c"},
  363. Args: []string{authenticatorServiceInitScript},
  364. VolumeMounts: []corev1.VolumeMount{binVolumeMount},
  365. })
  366. deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, corev1.Container{
  367. Name: authenticatorServiceContainerName,
  368. Image: "gcr.io/distroless/static-debian12:latest",
  369. Command: []string{"/authsvc/authenticator-service"},
  370. Args: []string{"-c", "/authsvc/config.json"},
  371. VolumeMounts: []corev1.VolumeMount{binVolumeMount},
  372. Ports: []corev1.ContainerPort{
  373. {Name: "authsvc", ContainerPort: 5681},
  374. },
  375. })
  376. for i := range deployment.Spec.Template.Spec.Containers {
  377. c := &deployment.Spec.Template.Spec.Containers[i]
  378. if c.Name != "conjur-oss" {
  379. continue
  380. }
  381. c.Env = append(c.Env,
  382. corev1.EnvVar{Name: "CONJUR_FEATURE_AUTHENTICATOR_SERVICE_ENABLED", Value: "true"},
  383. corev1.EnvVar{Name: "CONJUR_AUTHENTICATOR_SERVICE_URL", Value: "http://localhost:" + authenticatorServicePort},
  384. )
  385. }
  386. updated, err := clientSet.AppsV1().Deployments(l.Namespace).Update(GinkgoT().Context(), deployment, metav1.UpdateOptions{})
  387. if err != nil {
  388. return fmt.Errorf("unable to patch conjur-oss deployment with authenticator-service sidecar: %w", err)
  389. }
  390. // Wait for the rollout to fully complete (old ReplicaSet scaled to 0) rather than just
  391. // waiting for any pod matching the label to be ready. During the rolling update, the
  392. // old (pre-sidecar) pod can remain Ready while terminating; a label-only wait can race
  393. // and hand initConjur a pod whose conjur-oss container is already gone.
  394. if err := waitForDeploymentRollout(clientSet, l.Namespace, deploymentName, updated.Generation); err != nil {
  395. return fmt.Errorf("error waiting for conjur-oss rollout after sidecar patch: %w", err)
  396. }
  397. return nil
  398. }
  399. func waitForDeploymentRollout(clientSet kubernetes.Interface, namespace, name string, generation int64) error {
  400. if err := wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
  401. dep, err := clientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
  402. if err != nil {
  403. return false, nil
  404. }
  405. if dep.Status.ObservedGeneration < generation {
  406. return false, nil
  407. }
  408. replicas := int32(1)
  409. if dep.Spec.Replicas != nil {
  410. replicas = *dep.Spec.Replicas
  411. }
  412. return dep.Status.UpdatedReplicas == replicas &&
  413. dep.Status.Replicas == replicas &&
  414. dep.Status.AvailableReplicas == replicas, nil
  415. }); err != nil {
  416. return err
  417. }
  418. // Deployment status is computed asynchronously by the deployment controller and can
  419. // briefly report AvailableReplicas==1 while the old pre-sidecar pod is still
  420. // Running-but-terminating (Kubelet has not removed it yet). Cross-check pod state
  421. // directly: require exactly one pod for this Deployment that is not terminating and
  422. // has all 3 containers (conjur-oss, nginx, authenticator-service) ready, so
  423. // initConjur never execs into a pod that's mid-teardown.
  424. return wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
  425. pods, err := clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: "app=conjur-oss"})
  426. if err != nil {
  427. return false, nil
  428. }
  429. live := 0
  430. for i := range pods.Items {
  431. pod := &pods.Items[i]
  432. if pod.DeletionTimestamp != nil {
  433. continue
  434. }
  435. if pod.Status.Phase != corev1.PodRunning {
  436. continue
  437. }
  438. if len(pod.Status.ContainerStatuses) != 3 {
  439. continue
  440. }
  441. allReady := true
  442. for _, cs := range pod.Status.ContainerStatuses {
  443. allReady = allReady && cs.Ready
  444. }
  445. if allReady {
  446. live++
  447. }
  448. }
  449. return live == 1, nil
  450. })
  451. }
  452. func (l *Conjur) configureConjur() error {
  453. By("configuring conjur")
  454. // Construct Conjur policy for authn-jwt. This uses the token-app-property "sub" to
  455. // authenticate the host. This means that Conjur will determine which host is authenticating
  456. // based on the "sub" claim in the JWT token, which is provided by the Kubernetes service account.
  457. policy := `- !policy
  458. id: conjur/authn-jwt/eso-tests
  459. body:
  460. - !webservice
  461. - !variable public-keys
  462. - !variable issuer
  463. - !variable token-app-property
  464. - !variable audience`
  465. _, err := l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
  466. if err != nil {
  467. return fmt.Errorf("unable to load authn-jwt policy: %w", err)
  468. }
  469. // Construct Conjur policy for authn-jwt-hostid. This does not use the token-app-property variable
  470. // and instead uses the HostID passed in the authentication URL to determine which host is authenticating.
  471. // This is not the recommended way to authenticate, but it is needed for certain use cases where the
  472. // JWT token does not contain the "sub" claim.
  473. policy = `- !policy
  474. id: conjur/authn-jwt/eso-tests-hostid
  475. body:
  476. - !webservice
  477. - !variable public-keys
  478. - !variable issuer
  479. - !variable audience`
  480. _, err = l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
  481. if err != nil {
  482. return fmt.Errorf("unable to load authn-jwt policy: %w", err)
  483. }
  484. // Construct Conjur policy for two authn-cert authenticators. Both delegate client
  485. // certificate validation to the authenticator-service sidecar (see
  486. // patchAuthenticatorServiceSidecar). The 'ca-cert' variable name is fixed by the
  487. // authenticator; it is not a policy-defined path segment.
  488. //
  489. // 'eso-tests' operates in 'spiffe' host mode: the authenticated host's identity is
  490. // derived from the SPIFFE URI SAN in the client certificate (see genSpiffeCert), rather
  491. // than from a role identifier in the request path. This is the mode exercised when the
  492. // ExternalSecret store omits Auth.Cert.HostID.
  493. //
  494. // 'eso-tests-hostid' operates in the default 'request' host mode, mirroring the
  495. // authn-jwt/eso-tests-hostid authenticator: the host identity is supplied explicitly via
  496. // Auth.Cert.HostID. A single authenticator cannot serve both modes because host-mode is
  497. // a per-authenticator setting, not a per-request one.
  498. policy = `- !policy
  499. id: conjur/authn-cert/eso-tests
  500. body:
  501. - !webservice
  502. - !variable ca-cert
  503. - !variable host-mode
  504. - !variable trust-domain
  505. - !variable identity-path
  506. - !policy
  507. id: conjur/authn-cert/eso-tests-hostid
  508. body:
  509. - !webservice
  510. - !variable ca-cert`
  511. _, err = l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
  512. if err != nil {
  513. return fmt.Errorf("unable to load authn-cert policy: %w", err)
  514. }
  515. // Set the CA certificate and spiffe-mode variables for the authn-cert authenticators
  516. certSecrets := map[string]string{
  517. "conjur/authn-cert/eso-tests/ca-cert": string(l.ConjurServerCA),
  518. "conjur/authn-cert/eso-tests/host-mode": "spiffe",
  519. "conjur/authn-cert/eso-tests/trust-domain": SpiffeTrustDomain,
  520. "conjur/authn-cert/eso-tests/identity-path": SpiffeIdentityPath,
  521. "conjur/authn-cert/eso-tests-hostid/ca-cert": string(l.ConjurServerCA),
  522. }
  523. for secretPath, secretValue := range certSecrets {
  524. if err := l.ConjurClient.AddSecret(secretPath, secretValue); err != nil {
  525. return fmt.Errorf("unable to add secret %s: %w", secretPath, err)
  526. }
  527. }
  528. // Fetch the jwks info from the k8s cluster
  529. pubKeysJson, issuer, err := l.fetchJWKSandIssuer()
  530. if err != nil {
  531. return fmt.Errorf("unable to fetch jwks and issuer: %w", err)
  532. }
  533. // Set the variables for the authn-jwt policies
  534. secrets := map[string]string{
  535. "conjur/authn-jwt/eso-tests/audience": l.ConjurURL,
  536. "conjur/authn-jwt/eso-tests/issuer": issuer,
  537. "conjur/authn-jwt/eso-tests/public-keys": string(pubKeysJson),
  538. "conjur/authn-jwt/eso-tests/token-app-property": "sub",
  539. "conjur/authn-jwt/eso-tests-hostid/audience": l.ConjurURL,
  540. "conjur/authn-jwt/eso-tests-hostid/issuer": issuer,
  541. "conjur/authn-jwt/eso-tests-hostid/public-keys": string(pubKeysJson),
  542. }
  543. for secretPath, secretValue := range secrets {
  544. err := l.ConjurClient.AddSecret(secretPath, secretValue)
  545. if err != nil {
  546. return fmt.Errorf("unable to add secret %s: %w", secretPath, err)
  547. }
  548. }
  549. return nil
  550. }
  551. func (l *Conjur) fetchJWKSandIssuer() (pubKeysJson string, issuer string, err error) {
  552. kc := l.chart.config.KubeClientSet
  553. // Fetch the openid-configuration
  554. res, err := kc.CoreV1().RESTClient().Get().AbsPath("/.well-known/openid-configuration").DoRaw(GinkgoT().Context())
  555. if err != nil {
  556. return "", "", fmt.Errorf("unable to fetch openid-configuration: %w", err)
  557. }
  558. var openidConfig map[string]any
  559. json.Unmarshal(res, &openidConfig)
  560. issuer = openidConfig["issuer"].(string)
  561. // Fetch the jwks
  562. jwksJson, err := kc.CoreV1().RESTClient().Get().AbsPath("/openid/v1/jwks").DoRaw(GinkgoT().Context())
  563. if err != nil {
  564. return "", "", fmt.Errorf("unable to fetch jwks: %w", err)
  565. }
  566. var jwks map[string]any
  567. json.Unmarshal(jwksJson, &jwks)
  568. // Create a JSON object with the jwks that can be used by Conjur
  569. pubKeysObj := map[string]any{
  570. "type": "jwks",
  571. "value": jwks,
  572. }
  573. pubKeysJsonObj, err := json.Marshal(pubKeysObj)
  574. if err != nil {
  575. return "", "", fmt.Errorf("unable to marshal jwks: %w", err)
  576. }
  577. pubKeysJson = string(pubKeysJsonObj)
  578. return pubKeysJson, issuer, nil
  579. }
  580. // nolint:gocritic
  581. func genCertificates(namespace, serviceName string) ([]byte, []byte, []byte, []byte, error) {
  582. // gen server ca + certs
  583. rootCert, rootPem, rootKey, err := genCARoot()
  584. if err != nil {
  585. return nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
  586. }
  587. serverPem, serverKey, err := genPeerCert(rootCert, rootKey, "vault", []string{
  588. "localhost",
  589. serviceName,
  590. fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace)})
  591. if err != nil {
  592. return nil, nil, nil, nil, errors.New("unable to generate vault server cert")
  593. }
  594. serverKeyPem := pem.EncodeToMemory(&pem.Block{
  595. Type: privatePemType,
  596. Bytes: x509.MarshalPKCS1PrivateKey(serverKey)},
  597. )
  598. rootKeyPEM := pem.EncodeToMemory(&pem.Block{
  599. Type: privatePemType,
  600. Bytes: x509.MarshalPKCS1PrivateKey(rootKey),
  601. })
  602. return rootPem, rootKeyPEM, serverPem, serverKeyPem, err
  603. }
  604. // genSpiffeCert generates a client certificate carrying the given SPIFFE ID as a URI SAN,
  605. // signed by signingCert/signingKey. This is used by the authn-cert authenticator's 'spiffe'
  606. // host mode, which derives the authenticated host's identity from the certificate's URI SAN
  607. // rather than from a role identifier in the request path.
  608. // genClientAuthCert generates a client certificate for mutual TLS (authn-cert). cn sets the
  609. // Common Name (used by 'request' host mode's cn restriction annotation); spiffeID, if
  610. // non-empty, is embedded as a URI SAN (used by 'spiffe' host mode).
  611. func genClientAuthCert(signingCert *x509.Certificate, signingKey *rsa.PrivateKey, cn, spiffeID string) ([]byte, *rsa.PrivateKey, error) {
  612. var uris []*url.URL
  613. if spiffeID != "" {
  614. uri, err := url.Parse(spiffeID)
  615. if err != nil {
  616. return nil, nil, fmt.Errorf("unable to parse spiffe id: %w", err)
  617. }
  618. uris = []*url.URL{uri}
  619. }
  620. pkey, err := rsa.GenerateKey(rand.Reader, 2048)
  621. if err != nil {
  622. return nil, nil, err
  623. }
  624. tpl := x509.Certificate{
  625. Subject: pkix.Name{
  626. Country: []string{"/dev/null"},
  627. Organization: []string{"External Secrets ACME"},
  628. CommonName: cn,
  629. },
  630. SerialNumber: big.NewInt(1),
  631. NotBefore: time.Now(),
  632. NotAfter: time.Now().Add(time.Hour),
  633. // DigitalSignature is required for the client to sign the TLS handshake during
  634. // mutual TLS; CRLSign (used by the sibling genPeerCert for server certs) does not
  635. // cover that purpose and nginx's strict (critical) key usage check rejects the
  636. // handshake outright if it's missing.
  637. KeyUsage: x509.KeyUsageDigitalSignature,
  638. ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
  639. IsCA: false,
  640. MaxPathLenZero: true,
  641. URIs: uris,
  642. }
  643. _, certPEM, err := genCert(&tpl, signingCert, &pkey.PublicKey, signingKey)
  644. return certPEM, pkey, err
  645. }
  646. func (l *Conjur) Logs() error {
  647. return l.chart.Logs()
  648. }
  649. func (l *Conjur) Uninstall() error {
  650. if l.portForwarder != nil {
  651. l.portForwarder.Close()
  652. l.portForwarder = nil
  653. }
  654. if err := l.chart.Uninstall(); err != nil {
  655. return err
  656. }
  657. return l.chart.config.KubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), l.chart.Namespace, metav1.DeleteOptions{})
  658. }
  659. func (l *Conjur) Setup(cfg *Config) error {
  660. return l.chart.Setup(cfg)
  661. }
  662. func generateConjurDataKey() string {
  663. // Generate a 32 byte cryptographically secure random string.
  664. // Normally this is done by running `conjurctl data-key generate`
  665. // but for test purposes we can generate it programmatically.
  666. b := make([]byte, 32)
  667. _, err := rand.Read(b)
  668. if err != nil {
  669. panic(fmt.Errorf("unable to generate random string: %w", err))
  670. }
  671. // Encode the bytes as a base64 string
  672. return base64.StdEncoding.EncodeToString(b)
  673. }