conjur.go 29 KB

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