Browse Source

Conjur E2E Tests for K8s JWT Authentication (#3217)

Signed-off-by: Shlomo Heigh <shlomo.heigh@cyberark.com>
Shlomo Zalman Heigh 2 years ago
parent
commit
1d3209da59

+ 5 - 0
apis/externalsecrets/v1beta1/secretstore_conjur_types.go

@@ -44,6 +44,11 @@ type ConjurJWT struct {
 	// The conjur authn jwt webservice id
 	// The conjur authn jwt webservice id
 	ServiceID string `json:"serviceID"`
 	ServiceID string `json:"serviceID"`
 
 
+	// Optional HostID for JWT authentication. This may be used depending
+	// on how the Conjur JWT authenticator policy is configured.
+	// +optional
+	HostID string `json:"hostId"`
+
 	// Optional SecretRef that refers to a key in a Secret resource containing JWT token to
 	// Optional SecretRef that refers to a key in a Secret resource containing JWT token to
 	// authenticate with Conjur using the JWT authentication method.
 	// authenticate with Conjur using the JWT authentication method.
 	// +optional
 	// +optional

+ 5 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2311,6 +2311,11 @@ spec:
                             properties:
                             properties:
                               account:
                               account:
                                 type: string
                                 type: string
+                              hostId:
+                                description: |-
+                                  Optional HostID for JWT authentication. This may be used depending
+                                  on how the Conjur JWT authenticator policy is configured.
+                                type: string
                               secretRef:
                               secretRef:
                                 description: |-
                                 description: |-
                                   Optional SecretRef that refers to a key in a Secret resource containing JWT token to
                                   Optional SecretRef that refers to a key in a Secret resource containing JWT token to

+ 5 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2311,6 +2311,11 @@ spec:
                             properties:
                             properties:
                               account:
                               account:
                                 type: string
                                 type: string
+                              hostId:
+                                description: |-
+                                  Optional HostID for JWT authentication. This may be used depending
+                                  on how the Conjur JWT authenticator policy is configured.
+                                type: string
                               secretRef:
                               secretRef:
                                 description: |-
                                 description: |-
                                   Optional SecretRef that refers to a key in a Secret resource containing JWT token to
                                   Optional SecretRef that refers to a key in a Secret resource containing JWT token to

+ 10 - 0
deploy/crds/bundle.yaml

@@ -2764,6 +2764,11 @@ spec:
                               properties:
                               properties:
                                 account:
                                 account:
                                   type: string
                                   type: string
+                                hostId:
+                                  description: |-
+                                    Optional HostID for JWT authentication. This may be used depending
+                                    on how the Conjur JWT authenticator policy is configured.
+                                  type: string
                                 secretRef:
                                 secretRef:
                                   description: |-
                                   description: |-
                                     Optional SecretRef that refers to a key in a Secret resource containing JWT token to
                                     Optional SecretRef that refers to a key in a Secret resource containing JWT token to
@@ -7918,6 +7923,11 @@ spec:
                               properties:
                               properties:
                                 account:
                                 account:
                                   type: string
                                   type: string
+                                hostId:
+                                  description: |-
+                                    Optional HostID for JWT authentication. This may be used depending
+                                    on how the Conjur JWT authenticator policy is configured.
+                                  type: string
                                 secretRef:
                                 secretRef:
                                   description: |-
                                   description: |-
                                     Optional SecretRef that refers to a key in a Secret resource containing JWT token to
                                     Optional SecretRef that refers to a key in a Secret resource containing JWT token to

+ 13 - 0
docs/api/spec.md

@@ -1943,6 +1943,19 @@ string
 </tr>
 </tr>
 <tr>
 <tr>
 <td>
 <td>
+<code>hostId</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Optional HostID for JWT authentication. This may be used depending
+on how the Conjur JWT authenticator policy is configured.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>secretRef</code></br>
 <code>secretRef</code></br>
 <em>
 <em>
 <a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
 <a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">

+ 96 - 2
e2e/framework/addon/conjur.go

@@ -14,8 +14,10 @@ limitations under the License.
 package addon
 package addon
 
 
 import (
 import (
+	"context"
 	"crypto/rand"
 	"crypto/rand"
 	"encoding/base64"
 	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
 
 
@@ -51,7 +53,8 @@ func NewConjur(namespace string) *Conjur {
 			Namespace:    namespace,
 			Namespace:    namespace,
 			ReleaseName:  fmt.Sprintf("conjur-%s", namespace), // avoid cluster role collision
 			ReleaseName:  fmt.Sprintf("conjur-%s", namespace), // avoid cluster role collision
 			Chart:        fmt.Sprintf("%s/conjur-oss", repo),
 			Chart:        fmt.Sprintf("%s/conjur-oss", repo),
-			ChartVersion: "2.0.7",
+			// Use latest version of Conjur OSS. To pin to a specific version, uncomment the following line.
+			// ChartVersion: "2.0.7",
 			Repo: ChartRepo{
 			Repo: ChartRepo{
 				Name: repo,
 				Name: repo,
 				URL:  "https://cyberark.github.io/helm-charts",
 				URL:  "https://cyberark.github.io/helm-charts",
@@ -148,10 +151,101 @@ func (l *Conjur) initConjur() error {
 
 
 func (l *Conjur) configureConjur() error {
 func (l *Conjur) configureConjur() error {
 	ginkgo.By("configuring conjur")
 	ginkgo.By("configuring conjur")
-	// TODO: This will be used for the JWT tests
+	// Construct Conjur policy for authn-jwt. This uses the token-app-property "sub" to
+	// authenticate the host. This means that Conjur will determine which host is authenticating
+	// based on the "sub" claim in the JWT token, which is provided by the Kubernetes service account.
+	policy := `- !policy
+  id: conjur/authn-jwt/eso-tests
+  body:
+    - !webservice
+    - !variable public-keys
+    - !variable issuer
+    - !variable token-app-property
+    - !variable audience`
+
+	_, err := l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
+	if err != nil {
+		return fmt.Errorf("unable to load authn-jwt policy: %w", err)
+	}
+
+	// Construct Conjur policy for authn-jwt-hostid. This does not use the token-app-property variable
+	// and instead uses the HostID passed in the authentication URL to determine which host is authenticating.
+	// This is not the recommended way to authenticate, but it is needed for certain use cases where the
+	// JWT token does not contain the "sub" claim.
+	policy = `- !policy
+  id: conjur/authn-jwt/eso-tests-hostid
+  body:
+    - !webservice
+    - !variable public-keys
+    - !variable issuer
+    - !variable audience`
+
+	_, err = l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
+	if err != nil {
+		return fmt.Errorf("unable to load authn-jwt policy: %w", err)
+	}
+
+	// Fetch the jwks info from the k8s cluster
+	pubKeysJson, issuer, err := l.fetchJWKSandIssuer()
+	if err != nil {
+		return fmt.Errorf("unable to fetch jwks and issuer: %w", err)
+	}
+
+	// Set the variables for the authn-jwt policies
+	secrets := map[string]string{
+		"conjur/authn-jwt/eso-tests/audience":           l.ConjurURL,
+		"conjur/authn-jwt/eso-tests/issuer":             issuer,
+		"conjur/authn-jwt/eso-tests/public-keys":        string(pubKeysJson),
+		"conjur/authn-jwt/eso-tests/token-app-property": "sub",
+		"conjur/authn-jwt/eso-tests-hostid/audience":    l.ConjurURL,
+		"conjur/authn-jwt/eso-tests-hostid/issuer":      issuer,
+		"conjur/authn-jwt/eso-tests-hostid/public-keys": string(pubKeysJson),
+	}
+
+	for secretPath, secretValue := range secrets {
+		err := l.ConjurClient.AddSecret(secretPath, secretValue)
+		if err != nil {
+			return fmt.Errorf("unable to add secret %s: %w", secretPath, err)
+		}
+	}
+
 	return nil
 	return nil
 }
 }
 
 
+func (l *Conjur) fetchJWKSandIssuer() (pubKeysJson string, issuer string, err error) {
+	kc := l.chart.config.KubeClientSet
+
+	// Fetch the openid-configuration
+	res, err := kc.CoreV1().RESTClient().Get().AbsPath("/.well-known/openid-configuration").DoRaw(context.Background())
+	if err != nil {
+		return "", "", fmt.Errorf("unable to fetch openid-configuration: %w", err)
+	}
+	var openidConfig map[string]interface{}
+	json.Unmarshal(res, &openidConfig)
+	issuer = openidConfig["issuer"].(string)
+
+	// Fetch the jwks
+	jwksJson, err := kc.CoreV1().RESTClient().Get().AbsPath("/openid/v1/jwks").DoRaw(context.Background())
+	if err != nil {
+		return "", "", fmt.Errorf("unable to fetch jwks: %w", err)
+	}
+	var jwks map[string]interface{}
+	json.Unmarshal(jwksJson, &jwks)
+
+	// Create a JSON object with the jwks that can be used by Conjur
+	pubKeysObj := map[string]interface{}{
+		"type":  "jwks",
+		"value": jwks,
+	}
+	pubKeysJsonObj, err := json.Marshal(pubKeysObj)
+	if err != nil {
+		return "", "", fmt.Errorf("unable to marshal jwks: %w", err)
+	}
+
+	pubKeysJson = string(pubKeysJsonObj)
+	return pubKeysJson, issuer, nil
+}
+
 func (l *Conjur) Logs() error {
 func (l *Conjur) Logs() error {
 	return l.chart.Logs()
 	return l.chart.Logs()
 }
 }

+ 1 - 1
e2e/k8s/conjur.values.yaml

@@ -1,4 +1,4 @@
-authenticators: authn,authn-jwt/eso-tests
+authenticators: authn,authn-jwt/eso-tests,authn-jwt/eso-tests-hostid
 logLevel: "debug"
 logLevel: "debug"
 service:
 service:
   external:
   external:

+ 21 - 8
e2e/suites/provider/cases/conjur/conjur.go

@@ -21,8 +21,9 @@ import (
 )
 )
 
 
 const (
 const (
-	withTokenAuth = "with apikey auth"
-	withJWTK8s    = "with jwt k8s provider"
+	withTokenAuth    = "with apikey auth"
+	withJWTK8s       = "with jwt k8s provider"
+	withJWTK8sHostID = "with jwt k8s hostid provider"
 )
 )
 
 
 var _ = Describe("[conjur]", Label("conjur"), func() {
 var _ = Describe("[conjur]", Label("conjur"), func() {
@@ -38,9 +39,17 @@ var _ = Describe("[conjur]", Label("conjur"), func() {
 		framework.Compose(withTokenAuth, f, common.JSONDataFromRewrite, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataFromRewrite, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.SyncV1Alpha1, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.SyncV1Alpha1, useApiKeyAuth),
 
 
-		// // use jwt k8s provider
-		// framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
-		// framework.Compose(withJWTK8s, f, common.JSONDataFromRewrite, useJWTK8sProvider),
+		// use jwt k8s provider
+		framework.Compose(withJWTK8s, f, common.SimpleDataSync, useJWTK8sProvider),
+		framework.Compose(withJWTK8s, f, common.SyncWithoutTargetName, useJWTK8sProvider),
+		framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
+		framework.Compose(withJWTK8s, f, common.JSONDataFromRewrite, useJWTK8sProvider),
+
+		// use jwt k8s hostid provider
+		framework.Compose(withJWTK8sHostID, f, common.SimpleDataSync, useJWTK8sHostIDProvider),
+		framework.Compose(withJWTK8sHostID, f, common.SyncWithoutTargetName, useJWTK8sHostIDProvider),
+		framework.Compose(withJWTK8sHostID, f, common.JSONDataFromSync, useJWTK8sHostIDProvider),
+		framework.Compose(withJWTK8sHostID, f, common.JSONDataFromRewrite, useJWTK8sHostIDProvider),
 	)
 	)
 })
 })
 
 
@@ -48,6 +57,10 @@ func useApiKeyAuth(tc *framework.TestCase) {
 	tc.ExternalSecret.Spec.SecretStoreRef.Name = tc.Framework.Namespace.Name
 	tc.ExternalSecret.Spec.SecretStoreRef.Name = tc.Framework.Namespace.Name
 }
 }
 
 
-// func useJWTK8sProvider(tc *framework.TestCase) {
-// 	tc.ExternalSecret.Spec.SecretStoreRef.Name = jwtK8sProviderName
-// }
+func useJWTK8sProvider(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = jwtK8sProviderName
+}
+
+func useJWTK8sHostIDProvider(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = jwtK8sHostIDProviderName
+}

+ 81 - 0
e2e/suites/provider/cases/conjur/policy.go

@@ -0,0 +1,81 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package conjur
+
+import (
+	"bytes"
+	"text/template"
+)
+
+const createVariablePolicyTemplate = `- !variable
+  id: {{ .Key }}
+
+- !permit
+  role: !host system:serviceaccount:{{ .Namespace }}:test-app-sa
+  privilege: [ read, execute ]
+  resource: !variable {{ .Key }}
+
+- !permit
+  role: !host system:serviceaccount:{{ .Namespace }}:test-app-hostid-sa
+  privilege: [ read, execute ]
+  resource: !variable {{ .Key }}`
+
+const deleteVariablePolicyTemplate = `- !delete
+  record: !variable {{ .Key }}`
+
+const jwtHostPolicyTemplate = `- !host
+  id: {{ .HostID }}
+  annotations:
+    authn-jwt/{{ .ServiceID }}/sub: "{{ .HostID }}"
+
+- !permit
+  role: !host {{ .HostID }}
+  privilege: [ read, authenticate ]
+  resource: !webservice conjur/authn-jwt/{{ .ServiceID }}`
+
+func createVariablePolicy(key, namespace string) string {
+	return renderTemplate(createVariablePolicyTemplate, map[string]string{
+		"Key":       key,
+		"Namespace": namespace,
+	})
+}
+
+func deleteVariablePolicy(key string) string {
+	return renderTemplate(deleteVariablePolicyTemplate, map[string]string{
+		"Key": key,
+	})
+}
+
+func createJwtHostPolicy(hostID, serviceID string) string {
+	return renderTemplate(jwtHostPolicyTemplate, map[string]string{
+		"HostID":    hostID,
+		"ServiceID": serviceID,
+	})
+}
+
+func renderTemplate(templateText string, data map[string]string) string {
+	// Use golang templates to render the policy
+	tmpl, err := template.New("policy").Parse(templateText)
+	if err != nil {
+		// The templates are hardcoded, so this should never happen
+		panic(err)
+	}
+	output := new(bytes.Buffer)
+	err = tmpl.Execute(output, data)
+	if err != nil {
+		// The templates are hardcoded, so this should never happen
+		panic(err)
+	}
+	return output.String()
+}

+ 107 - 27
e2e/suites/provider/cases/conjur/provider.go

@@ -40,8 +40,8 @@ type conjurProvider struct {
 }
 }
 
 
 const (
 const (
-	apiKeyAuthProviderName = "api-key-auth-provider"
-	jwtK8sProviderName     = "jwt-k8s-provider"
+	jwtK8sProviderName       = "jwt-k8s-provider"
+	jwtK8sHostIDProviderName = "jwt-k8s-hostid-provider"
 )
 )
 
 
 func newConjurProvider(f *framework.Framework) *conjurProvider {
 func newConjurProvider(f *framework.Framework) *conjurProvider {
@@ -49,12 +49,14 @@ func newConjurProvider(f *framework.Framework) *conjurProvider {
 		framework: f,
 		framework: f,
 	}
 	}
 	BeforeEach(prov.BeforeEach)
 	BeforeEach(prov.BeforeEach)
+	AfterEach(prov.AfterEach)
 	return prov
 	return prov
 }
 }
 
 
 func (s *conjurProvider) CreateSecret(key string, val framework.SecretEntry) {
 func (s *conjurProvider) CreateSecret(key string, val framework.SecretEntry) {
 	// Generate a policy file for the secret key
 	// Generate a policy file for the secret key
-	policy := "- !variable " + key
+	policy := createVariablePolicy(key, s.framework.Namespace.Name)
+
 	_, err := s.client.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
 	_, err := s.client.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 
 
@@ -64,8 +66,7 @@ func (s *conjurProvider) CreateSecret(key string, val framework.SecretEntry) {
 }
 }
 
 
 func (s *conjurProvider) DeleteSecret(key string) {
 func (s *conjurProvider) DeleteSecret(key string) {
-	policy := `- !delete
-  record: !variable ` + key
+	policy := deleteVariablePolicy(key)
 	_, err := s.client.LoadPolicy(conjurapi.PolicyModePatch, "root", strings.NewReader(policy))
 	_, err := s.client.LoadPolicy(conjurapi.PolicyModePatch, "root", strings.NewReader(policy))
 
 
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
@@ -79,7 +80,33 @@ func (s *conjurProvider) BeforeEach() {
 	s.url = c.ConjurURL
 	s.url = c.ConjurURL
 
 
 	s.CreateApiKeyStore(c, ns)
 	s.CreateApiKeyStore(c, ns)
-	// s.CreateJWTK8sStore(c, ns)
+	s.CreateJWTK8sStore(c, ns)
+	s.CreateJWTK8sHostIDStore(c, ns)
+}
+
+func (s *conjurProvider) AfterEach() {
+	// Print Conjur logs if the test failed
+	if !CurrentGinkgoTestDescription().Failed {
+		return
+	}
+
+	// Get logs from Conjur pod
+	ns := s.framework.Namespace.Name
+	pods, err := s.framework.KubeClientSet.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{})
+	if err != nil {
+		GinkgoWriter.Printf("Error getting pods: %s\n", err)
+		return
+	}
+
+	for _, pod := range pods.Items {
+		if strings.Contains(pod.Name, "conjur-oss") {
+			logs, err := s.framework.KubeClientSet.CoreV1().Pods(ns).GetLogs(pod.Name, &v1.PodLogOptions{Container: "conjur-oss"}).DoRaw(context.Background())
+			if err != nil {
+				GinkgoWriter.Printf("Error getting logs from Conjur pod: %s\n", err)
+			}
+			GinkgoWriter.Printf("Conjur logs:\n%s\n", logs)
+		}
+	}
 }
 }
 
 
 func makeStore(name, ns string, c *addon.Conjur) *esv1beta1.SecretStore {
 func makeStore(name, ns string, c *addon.Conjur) *esv1beta1.SecretStore {
@@ -103,7 +130,6 @@ func (s *conjurProvider) CreateApiKeyStore(c *addon.Conjur, ns string) {
 	By("creating a conjur secret")
 	By("creating a conjur secret")
 	conjurCreds := &v1.Secret{
 	conjurCreds := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 		ObjectMeta: metav1.ObjectMeta{
-			// Name:      apiKeyAuthProviderName,
 			Name:      ns,
 			Name:      ns,
 			Namespace: ns,
 			Namespace: ns,
 		},
 		},
@@ -116,18 +142,15 @@ func (s *conjurProvider) CreateApiKeyStore(c *addon.Conjur, ns string) {
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 
 
 	By("creating an secret store for conjur")
 	By("creating an secret store for conjur")
-	// secretStore := makeStore(apiKeyAuthProviderName, ns, c)
 	secretStore := makeStore(ns, ns, c)
 	secretStore := makeStore(ns, ns, c)
 	secretStore.Spec.Provider.Conjur.Auth = esv1beta1.ConjurAuth{
 	secretStore.Spec.Provider.Conjur.Auth = esv1beta1.ConjurAuth{
 		APIKey: &esv1beta1.ConjurAPIKey{
 		APIKey: &esv1beta1.ConjurAPIKey{
 			Account: "default",
 			Account: "default",
 			UserRef: &esmeta.SecretKeySelector{
 			UserRef: &esmeta.SecretKeySelector{
-				// Name: apiKeyAuthProviderName,
 				Name: ns,
 				Name: ns,
 				Key:  "username",
 				Key:  "username",
 			},
 			},
 			APIKeyRef: &esmeta.SecretKeySelector{
 			APIKeyRef: &esmeta.SecretKeySelector{
-				// Name: apiKeyAuthProviderName,
 				Name: ns,
 				Name: ns,
 				Key:  "apikey",
 				Key:  "apikey",
 			},
 			},
@@ -137,20 +160,77 @@ func (s *conjurProvider) CreateApiKeyStore(c *addon.Conjur, ns string) {
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 }
 }
 
 
-// func (s conjurProvider) CreateJWTK8sStore(c *addon.Conjur, ns string) {
-// 	secretStore := makeStore(jwtK8sProviderName, ns, c)
-// 	secretStore.Spec.Provider.Conjur.Auth = esv1beta1.ConjurAuth{
-// 		Jwt: &esv1beta1.ConjurJWT{
-// 			Account:   "default",
-// 			ServiceID: "eso-tests",
-// 			ServiceAccountRef: &esmeta.ServiceAccountSelector{
-// 				Name: "default",
-// 				Audiences: []string{
-// 					c.ConjurURL,
-// 				},
-// 			},
-// 		},
-// 	}
-// 	err := s.framework.CRClient.Create(context.Background(), secretStore)
-// 	Expect(err).ToNot(HaveOccurred())
-// }
+func (s conjurProvider) CreateJWTK8sStore(c *addon.Conjur, ns string) {
+	// Create a service account
+	sa := &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-app-sa",
+			Namespace: ns,
+		},
+	}
+	err := s.framework.CRClient.Create(context.Background(), sa)
+	Expect(err).ToNot(HaveOccurred())
+
+	// Add the service account to the Conjur policy with permissions to
+	// authenticate with authn-jwt
+	saName := "system:serviceaccount:" + ns + ":test-app-sa"
+	policy := createJwtHostPolicy(saName, "eso-tests")
+
+	_, err = s.client.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
+	Expect(err).ToNot(HaveOccurred())
+
+	// Now create a secret store that uses the service account to authenticate
+	secretStore := makeStore(jwtK8sProviderName, ns, c)
+	secretStore.Spec.Provider.Conjur.Auth = esv1beta1.ConjurAuth{
+		Jwt: &esv1beta1.ConjurJWT{
+			Account:   "default",
+			ServiceID: "eso-tests",
+			ServiceAccountRef: &esmeta.ServiceAccountSelector{
+				Name: "test-app-sa",
+				Audiences: []string{
+					c.ConjurURL,
+				},
+			},
+		},
+	}
+	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s conjurProvider) CreateJWTK8sHostIDStore(c *addon.Conjur, ns string) {
+	// Create a service account
+	sa := &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-app-hostid-sa",
+			Namespace: ns,
+		},
+	}
+	err := s.framework.CRClient.Create(context.Background(), sa)
+	Expect(err).ToNot(HaveOccurred())
+
+	// Add the service account to the Conjur policy with permissions to
+	// authenticate with authn-jwt
+	saName := "system:serviceaccount:" + ns + ":test-app-hostid-sa"
+	policy := createJwtHostPolicy(saName, "eso-tests-hostid")
+
+	_, err = s.client.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
+	Expect(err).ToNot(HaveOccurred())
+
+	// Now create a secret store that uses the service account to authenticate
+	secretStore := makeStore(jwtK8sHostIDProviderName, ns, c)
+	secretStore.Spec.Provider.Conjur.Auth = esv1beta1.ConjurAuth{
+		Jwt: &esv1beta1.ConjurJWT{
+			Account:   "default",
+			HostID:    "host/" + saName,
+			ServiceID: "eso-tests-hostid",
+			ServiceAccountRef: &esmeta.ServiceAccountSelector{
+				Name: "test-app-hostid-sa",
+				Audiences: []string{
+					c.ConjurURL,
+				},
+			},
+		},
+	}
+	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}

+ 1 - 1
pkg/provider/conjur/auth_jwt.go

@@ -95,7 +95,7 @@ func (p *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config,
 		return nil, getJWTError
 		return nil, getJWTError
 	}
 	}
 
 
-	client, clientError := p.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID)
+	client, clientError := p.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID, jwtAuth.HostID)
 	if clientError != nil {
 	if clientError != nil {
 		return nil, clientError
 		return nil, clientError
 	}
 	}

+ 10 - 3
pkg/provider/conjur/conjur_api.go

@@ -17,6 +17,7 @@ package conjur
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"net/url"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -33,7 +34,7 @@ type SecretsClient interface {
 // SecretsClientFactory is an interface for creating a Conjur client.
 // SecretsClientFactory is an interface for creating a Conjur client.
 type SecretsClientFactory interface {
 type SecretsClientFactory interface {
 	NewClientFromKey(config conjurapi.Config, loginPair authn.LoginPair) (SecretsClient, error)
 	NewClientFromKey(config conjurapi.Config, loginPair authn.LoginPair) (SecretsClient, error)
-	NewClientFromJWT(config conjurapi.Config, jwtToken string, jwtServiceID string) (SecretsClient, error)
+	NewClientFromJWT(config conjurapi.Config, jwtToken string, jwtServiceID, jwtHostID string) (SecretsClient, error)
 }
 }
 
 
 // ClientAPIImpl is an implementation of the ClientAPI interface.
 // ClientAPIImpl is an implementation of the ClientAPI interface.
@@ -46,7 +47,7 @@ func (c *ClientAPIImpl) NewClientFromKey(config conjurapi.Config, loginPair auth
 // NewClientFromJWT creates a new Conjur client from a JWT token.
 // NewClientFromJWT creates a new Conjur client from a JWT token.
 // cannot use the built-in function "conjurapi.NewClientFromJwt" because it requires environment variables
 // cannot use the built-in function "conjurapi.NewClientFromJwt" because it requires environment variables
 // see: https://github.com/cyberark/conjur-api-go/blob/b698692392a38e5d38b8440f32ab74206544848a/conjurapi/client.go#L130
 // see: https://github.com/cyberark/conjur-api-go/blob/b698692392a38e5d38b8440f32ab74206544848a/conjurapi/client.go#L130
-func (c *ClientAPIImpl) NewClientFromJWT(config conjurapi.Config, jwtToken, jwtServiceID string) (SecretsClient, error) {
+func (c *ClientAPIImpl) NewClientFromJWT(config conjurapi.Config, jwtToken, jwtServiceID, jwtHostID string) (SecretsClient, error) {
 	jwtTokenString := fmt.Sprintf("jwt=%s", jwtToken)
 	jwtTokenString := fmt.Sprintf("jwt=%s", jwtToken)
 
 
 	var httpClient *http.Client
 	var httpClient *http.Client
@@ -63,7 +64,13 @@ func (c *ClientAPIImpl) NewClientFromJWT(config conjurapi.Config, jwtToken, jwtS
 		httpClient = &http.Client{Timeout: time.Second * 10}
 		httpClient = &http.Client{Timeout: time.Second * 10}
 	}
 	}
 
 
-	authnJwtURL := strings.Join([]string{config.ApplianceURL, "authn-jwt", jwtServiceID, config.Account, "authenticate"}, "/")
+	var authnJwtURL string
+	// If a hostID is provided, it must be included in the URL
+	if jwtHostID != "" {
+		authnJwtURL = strings.Join([]string{config.ApplianceURL, "authn-jwt", jwtServiceID, config.Account, url.PathEscape(jwtHostID), "authenticate"}, "/")
+	} else {
+		authnJwtURL = strings.Join([]string{config.ApplianceURL, "authn-jwt", jwtServiceID, config.Account, "authenticate"}, "/")
+	}
 
 
 	req, err := http.NewRequest("POST", authnJwtURL, strings.NewReader(jwtTokenString))
 	req, err := http.NewRequest("POST", authnJwtURL, strings.NewReader(jwtTokenString))
 	if err != nil {
 	if err != nil {

+ 36 - 17
pkg/provider/conjur/provider_test.go

@@ -39,10 +39,13 @@ import (
 )
 )
 
 
 var (
 var (
-	svcURL     = "https://example.com"
-	svcUser    = "user"
-	svcApikey  = "apikey"
-	svcAccount = "account1"
+	svcURL           = "https://example.com"
+	svcUser          = "user"
+	svcApikey        = "apikey"
+	svcAccount       = "account1"
+	jwtAuthenticator = "jwt-authenticator"
+	jwtAuthnService  = "jwt-auth-service"
+	jwtSecretName    = "jwt-secret"
 )
 )
 
 
 func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
 func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
@@ -81,27 +84,27 @@ func TestValidateStore(t *testing.T) {
 		},
 		},
 
 
 		{
 		{
-			store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount"),
+			store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount"),
 			err:   nil,
 			err:   nil,
 		},
 		},
 		{
 		{
-			store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-auth-service", "myconjuraccount"),
+			store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthnService, "", "myconjuraccount"),
 			err:   nil,
 			err:   nil,
 		},
 		},
 		{
 		{
-			store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", ""),
+			store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", ""),
 			err:   fmt.Errorf("missing Auth.Jwt.Account"),
 			err:   fmt.Errorf("missing Auth.Jwt.Account"),
 		},
 		},
 		{
 		{
-			store: makeJWTSecretStore(svcURL, "conjur", "", "", "myconjuraccount"),
+			store: makeJWTSecretStore(svcURL, "conjur", "", "", "", "myconjuraccount"),
 			err:   fmt.Errorf("missing Auth.Jwt.ServiceID"),
 			err:   fmt.Errorf("missing Auth.Jwt.ServiceID"),
 		},
 		},
 		{
 		{
-			store: makeJWTSecretStore("", "conjur", "", "jwt-auth-service", "myconjuraccount"),
+			store: makeJWTSecretStore("", "conjur", "", jwtAuthnService, "", "myconjuraccount"),
 			err:   fmt.Errorf("conjur URL cannot be empty"),
 			err:   fmt.Errorf("conjur URL cannot be empty"),
 		},
 		},
 		{
 		{
-			store: makeJWTSecretStore(svcURL, "", "", "jwt-auth-service", "myconjuraccount"),
+			store: makeJWTSecretStore(svcURL, "", "", jwtAuthnService, "", "myconjuraccount"),
 			err:   fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
 			err:   fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
 		},
 		},
 
 
@@ -175,7 +178,22 @@ func TestGetSecret(t *testing.T) {
 		"JwtWithServiceAccountRefReadSecretSuccess": {
 		"JwtWithServiceAccountRefReadSecretSuccess": {
 			reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
 			reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
 			args: args{
 			args: args{
-				store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
+				store: makeJWTSecretStore(svcURL, svcAccount, "", jwtAuthenticator, "", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects().Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+				corev1:     utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+			},
+			want: want{
+				err:   nil,
+				value: "secret",
+			},
+		},
+		"JwtWithServiceAccountRefWithHostIdReadSecretSuccess": {
+			reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account and uses a host ID.",
+			args: args{
+				store: makeJWTSecretStore(svcURL, svcAccount, "", jwtAuthenticator, "myhostid", "myconjuraccount"),
 				kube: clientfake.NewClientBuilder().
 				kube: clientfake.NewClientBuilder().
 					WithObjects().Build(),
 					WithObjects().Build(),
 				namespace:  "default",
 				namespace:  "default",
@@ -190,11 +208,11 @@ func TestGetSecret(t *testing.T) {
 		"JwtWithSecretRefReadSecretSuccess": {
 		"JwtWithSecretRefReadSecretSuccess": {
 			reason: "Should read a secret successfully using an JWT auth secret store that references a k8s secret.",
 			reason: "Should read a secret successfully using an JWT auth secret store that references a k8s secret.",
 			args: args{
 			args: args{
-				store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-authenticator", "myconjuraccount"),
+				store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthenticator, "", "myconjuraccount"),
 				kube: clientfake.NewClientBuilder().
 				kube: clientfake.NewClientBuilder().
 					WithObjects(&corev1.Secret{
 					WithObjects(&corev1.Secret{
 						ObjectMeta: metav1.ObjectMeta{
 						ObjectMeta: metav1.ObjectMeta{
-							Name:      "jwt-secret",
+							Name:      jwtSecretName,
 							Namespace: "default",
 							Namespace: "default",
 						},
 						},
 						Data: map[string][]byte{
 						Data: map[string][]byte{
@@ -212,7 +230,7 @@ func TestGetSecret(t *testing.T) {
 		"JwtWithCABundleSuccess": {
 		"JwtWithCABundleSuccess": {
 			reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
 			reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
 			args: args{
 			args: args{
-				store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
+				store: makeJWTSecretStore(svcURL, svcAccount, "", jwtAuthenticator, "", "myconjuraccount"),
 				kube: clientfake.NewClientBuilder().
 				kube: clientfake.NewClientBuilder().
 					WithObjects().Build(),
 					WithObjects().Build(),
 				namespace:  "default",
 				namespace:  "default",
@@ -364,7 +382,7 @@ func makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1b
 	return store
 	return store
 }
 }
 
 
-func makeJWTSecretStore(svcURL, serviceAccountName, secretName, jwtServiceID, conjurAccount string) *esv1beta1.SecretStore {
+func makeJWTSecretStore(svcURL, serviceAccountName, secretName, jwtServiceID, jwtHostID, conjurAccount string) *esv1beta1.SecretStore {
 	serviceAccountRef := &esmeta.ServiceAccountSelector{
 	serviceAccountRef := &esmeta.ServiceAccountSelector{
 		Name:      serviceAccountName,
 		Name:      serviceAccountName,
 		Audiences: []string{"conjur"},
 		Audiences: []string{"conjur"},
@@ -392,6 +410,7 @@ func makeJWTSecretStore(svcURL, serviceAccountName, secretName, jwtServiceID, co
 							ServiceID:         jwtServiceID,
 							ServiceID:         jwtServiceID,
 							ServiceAccountRef: serviceAccountRef,
 							ServiceAccountRef: serviceAccountRef,
 							SecretRef:         secretRef,
 							SecretRef:         secretRef,
+							HostID:            jwtHostID,
 						},
 						},
 					},
 					},
 				},
 				},
@@ -402,7 +421,7 @@ func makeJWTSecretStore(svcURL, serviceAccountName, secretName, jwtServiceID, co
 }
 }
 
 
 func makeStoreWithCA(caSource, caData string) *esv1beta1.SecretStore {
 func makeStoreWithCA(caSource, caData string) *esv1beta1.SecretStore {
-	store := makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount")
+	store := makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount")
 	if caSource == "secret" {
 	if caSource == "secret" {
 		store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
 		store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
 			Type: esv1beta1.CAProviderTypeSecret,
 			Type: esv1beta1.CAProviderTypeSecret,
@@ -502,7 +521,7 @@ func (c *ConjurMockAPIClient) NewClientFromKey(_ conjurapi.Config, _ authn.Login
 	return &fake.ConjurMockClient{}, nil
 	return &fake.ConjurMockClient{}, nil
 }
 }
 
 
-func (c *ConjurMockAPIClient) NewClientFromJWT(_ conjurapi.Config, _, _ string) (SecretsClient, error) {
+func (c *ConjurMockAPIClient) NewClientFromJWT(_ conjurapi.Config, _, _, _ string) (SecretsClient, error) {
 	return &fake.ConjurMockClient{}, nil
 	return &fake.ConjurMockClient{}, nil
 }
 }