Browse Source

Conjur JWT support (#2591)

* Add JWT Auth to Conjur Provider

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

* Update docs for Cyberark Conjur Provider

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

* Update test suite to cover new functionality

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

* Run make reviewable

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

* Set MinVersion for tls.Config to satisfy linting

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

* Move ca bundle config example to a yaml snippet

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

* fix: consolidate naming

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* fix: consolidate naming

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* docs: make it a working example

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* Remove JWT expiration handling logic

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

* Run make fmt

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>

---------

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
Kieran Bristow 2 years ago
parent
commit
d9eaeb40dc

+ 26 - 3
apis/externalsecrets/v1beta1/secretstore_conjur_types.go

@@ -17,13 +17,19 @@ package v1beta1
 import esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 
 type ConjurProvider struct {
-	URL      string     `json:"url"`
-	CABundle string     `json:"caBundle,omitempty"`
-	Auth     ConjurAuth `json:"auth"`
+	URL string `json:"url"`
+	// +optional
+	CABundle string `json:"caBundle,omitempty"`
+	// +optional
+	CAProvider *CAProvider `json:"caProvider,omitempty"`
+	Auth       ConjurAuth  `json:"auth"`
 }
 
 type ConjurAuth struct {
+	// +optional
 	Apikey *ConjurApikey `json:"apikey"`
+	// +optional
+	Jwt *ConjurJWT `json:"jwt"`
 }
 
 type ConjurApikey struct {
@@ -31,3 +37,20 @@ type ConjurApikey struct {
 	UserRef   *esmeta.SecretKeySelector `json:"userRef"`
 	APIKeyRef *esmeta.SecretKeySelector `json:"apiKeyRef"`
 }
+
+type ConjurJWT struct {
+	Account string `json:"account"`
+
+	// The conjur authn jwt webservice id
+	ServiceID string `json:"serviceID"`
+
+	// Optional SecretRef that refers to a key in a Secret resource containing JWT token to
+	// authenticate with Conjur using the JWT authentication method.
+	// +optional
+	SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"`
+
+	// Optional ServiceAccountRef specifies the Kubernetes service account for which to request
+	// a token for with the `TokenRequest` API.
+	// +optional
+	ServiceAccountRef *esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
+}

+ 35 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -672,6 +672,11 @@ func (in *ConjurAuth) DeepCopyInto(out *ConjurAuth) {
 		*out = new(ConjurApikey)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Jwt != nil {
+		in, out := &in.Jwt, &out.Jwt
+		*out = new(ConjurJWT)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConjurAuth.
@@ -685,8 +690,38 @@ func (in *ConjurAuth) DeepCopy() *ConjurAuth {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ConjurJWT) DeepCopyInto(out *ConjurJWT) {
+	*out = *in
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(metav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.ServiceAccountRef != nil {
+		in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
+		*out = new(metav1.ServiceAccountSelector)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConjurJWT.
+func (in *ConjurJWT) DeepCopy() *ConjurJWT {
+	if in == nil {
+		return nil
+	}
+	out := new(ConjurJWT)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ConjurProvider) DeepCopyInto(out *ConjurProvider) {
 	*out = *in
+	if in.CAProvider != nil {
+		in, out := &in.CAProvider, &out.CAProvider
+		*out = new(CAProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	in.Auth.DeepCopyInto(&out.Auth)
 }
 

+ 89 - 2
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2251,11 +2251,98 @@ spec:
                             - apiKeyRef
                             - userRef
                             type: object
-                        required:
-                        - apikey
+                          jwt:
+                            properties:
+                              account:
+                                type: string
+                              secretRef:
+                                description: Optional SecretRef that refers to a key
+                                  in a Secret resource containing JWT token to authenticate
+                                  with Conjur using the JWT authentication method.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                              serviceAccountRef:
+                                description: Optional ServiceAccountRef specifies
+                                  the Kubernetes service account for which to request
+                                  a token for with the `TokenRequest` API.
+                                properties:
+                                  audiences:
+                                    description: Audience specifies the `aud` claim
+                                      for the service account token If the service
+                                      account uses a well-known annotation for e.g.
+                                      IRSA or GCP Workload Identity then this audiences
+                                      will be appended to the list
+                                    items:
+                                      type: string
+                                    type: array
+                                  name:
+                                    description: The name of the ServiceAccount resource
+                                      being referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                required:
+                                - name
+                                type: object
+                              serviceID:
+                                description: The conjur authn jwt webservice id
+                                type: string
+                            required:
+                            - account
+                            - serviceID
+                            type: object
                         type: object
                       caBundle:
                         type: string
+                      caProvider:
+                        description: Used to provide custom certificate authority
+                          (CA) certificates for a secret store. The CAProvider points
+                          to a Secret or ConfigMap resource that contains a PEM-encoded
+                          certificate.
+                        properties:
+                          key:
+                            description: The key where the CA certificate can be found
+                              in the Secret or ConfigMap.
+                            type: string
+                          name:
+                            description: The name of the object located at the provider
+                              type.
+                            type: string
+                          namespace:
+                            description: The namespace the Provider type is in. Can
+                              only be defined when used in a ClusterSecretStore.
+                            type: string
+                          type:
+                            description: The type of provider to use such as "Secret",
+                              or "ConfigMap".
+                            enum:
+                            - Secret
+                            - ConfigMap
+                            type: string
+                        required:
+                        - name
+                        - type
+                        type: object
                       url:
                         type: string
                     required:

+ 89 - 2
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2251,11 +2251,98 @@ spec:
                             - apiKeyRef
                             - userRef
                             type: object
-                        required:
-                        - apikey
+                          jwt:
+                            properties:
+                              account:
+                                type: string
+                              secretRef:
+                                description: Optional SecretRef that refers to a key
+                                  in a Secret resource containing JWT token to authenticate
+                                  with Conjur using the JWT authentication method.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                              serviceAccountRef:
+                                description: Optional ServiceAccountRef specifies
+                                  the Kubernetes service account for which to request
+                                  a token for with the `TokenRequest` API.
+                                properties:
+                                  audiences:
+                                    description: Audience specifies the `aud` claim
+                                      for the service account token If the service
+                                      account uses a well-known annotation for e.g.
+                                      IRSA or GCP Workload Identity then this audiences
+                                      will be appended to the list
+                                    items:
+                                      type: string
+                                    type: array
+                                  name:
+                                    description: The name of the ServiceAccount resource
+                                      being referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                required:
+                                - name
+                                type: object
+                              serviceID:
+                                description: The conjur authn jwt webservice id
+                                type: string
+                            required:
+                            - account
+                            - serviceID
+                            type: object
                         type: object
                       caBundle:
                         type: string
+                      caProvider:
+                        description: Used to provide custom certificate authority
+                          (CA) certificates for a secret store. The CAProvider points
+                          to a Secret or ConfigMap resource that contains a PEM-encoded
+                          certificate.
+                        properties:
+                          key:
+                            description: The key where the CA certificate can be found
+                              in the Secret or ConfigMap.
+                            type: string
+                          name:
+                            description: The name of the object located at the provider
+                              type.
+                            type: string
+                          namespace:
+                            description: The namespace the Provider type is in. Can
+                              only be defined when used in a ClusterSecretStore.
+                            type: string
+                          type:
+                            description: The type of provider to use such as "Secret",
+                              or "ConfigMap".
+                            enum:
+                            - Secret
+                            - ConfigMap
+                            type: string
+                        required:
+                        - name
+                        - type
+                        type: object
                       url:
                         type: string
                     required:

+ 126 - 4
deploy/crds/bundle.yaml

@@ -2101,11 +2101,72 @@ spec:
                                 - apiKeyRef
                                 - userRef
                               type: object
-                          required:
-                            - apikey
+                            jwt:
+                              properties:
+                                account:
+                                  type: string
+                                secretRef:
+                                  description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Conjur using the JWT authentication method.
+                                  properties:
+                                    key:
+                                      description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      type: string
+                                    namespace:
+                                      description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                      type: string
+                                  type: object
+                                serviceAccountRef:
+                                  description: Optional ServiceAccountRef specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API.
+                                  properties:
+                                    audiences:
+                                      description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list
+                                      items:
+                                        type: string
+                                      type: array
+                                    name:
+                                      description: The name of the ServiceAccount resource being referred to.
+                                      type: string
+                                    namespace:
+                                      description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                      type: string
+                                  required:
+                                    - name
+                                  type: object
+                                serviceID:
+                                  description: The conjur authn jwt webservice id
+                                  type: string
+                              required:
+                                - account
+                                - serviceID
+                              type: object
                           type: object
                         caBundle:
                           type: string
+                        caProvider:
+                          description: Used to provide custom certificate authority (CA) certificates for a secret store. The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+                          properties:
+                            key:
+                              description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                              type: string
+                            name:
+                              description: The name of the object located at the provider type.
+                              type: string
+                            namespace:
+                              description: The namespace the Provider type is in. Can only be defined when used in a ClusterSecretStore.
+                              type: string
+                            type:
+                              description: The type of provider to use such as "Secret", or "ConfigMap".
+                              enum:
+                                - Secret
+                                - ConfigMap
+                              type: string
+                          required:
+                            - name
+                            - type
+                          type: object
                         url:
                           type: string
                       required:
@@ -5837,11 +5898,72 @@ spec:
                                 - apiKeyRef
                                 - userRef
                               type: object
-                          required:
-                            - apikey
+                            jwt:
+                              properties:
+                                account:
+                                  type: string
+                                secretRef:
+                                  description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Conjur using the JWT authentication method.
+                                  properties:
+                                    key:
+                                      description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      type: string
+                                    namespace:
+                                      description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                      type: string
+                                  type: object
+                                serviceAccountRef:
+                                  description: Optional ServiceAccountRef specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API.
+                                  properties:
+                                    audiences:
+                                      description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list
+                                      items:
+                                        type: string
+                                      type: array
+                                    name:
+                                      description: The name of the ServiceAccount resource being referred to.
+                                      type: string
+                                    namespace:
+                                      description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                      type: string
+                                  required:
+                                    - name
+                                  type: object
+                                serviceID:
+                                  description: The conjur authn jwt webservice id
+                                  type: string
+                              required:
+                                - account
+                                - serviceID
+                              type: object
                           type: object
                         caBundle:
                           type: string
+                        caProvider:
+                          description: Used to provide custom certificate authority (CA) certificates for a secret store. The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+                          properties:
+                            key:
+                              description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                              type: string
+                            name:
+                              description: The name of the object located at the provider type.
+                              type: string
+                            namespace:
+                              description: The namespace the Provider type is in. Can only be defined when used in a ClusterSecretStore.
+                              type: string
+                            type:
+                              description: The type of provider to use such as "Secret", or "ConfigMap".
+                              enum:
+                                - Secret
+                                - ConfigMap
+                              type: string
+                          required:
+                            - name
+                            - type
+                          type: object
                         url:
                           type: string
                       required:

+ 98 - 0
docs/api/spec.md

@@ -964,6 +964,7 @@ string
 <p>
 (<em>Appears on:</em>
 <a href="#external-secrets.io/v1beta1.AkeylessProvider">AkeylessProvider</a>, 
+<a href="#external-secrets.io/v1beta1.ConjurProvider">ConjurProvider</a>, 
 <a href="#external-secrets.io/v1beta1.KubernetesServer">KubernetesServer</a>, 
 <a href="#external-secrets.io/v1beta1.VaultProvider">VaultProvider</a>)
 </p>
@@ -1728,6 +1729,89 @@ ConjurApikey
 </em>
 </td>
 <td>
+<em>(Optional)</em>
+</td>
+</tr>
+<tr>
+<td>
+<code>jwt</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ConjurJWT">
+ConjurJWT
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.ConjurJWT">ConjurJWT
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.ConjurAuth">ConjurAuth</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>account</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>serviceID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>The conjur authn jwt webservice id</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Optional SecretRef that refers to a key in a Secret resource containing JWT token to
+authenticate with Conjur using the JWT authentication method.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>serviceAccountRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#ServiceAccountSelector">
+External Secrets meta/v1.ServiceAccountSelector
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Optional ServiceAccountRef specifies the Kubernetes service account for which to request
+a token for with the <code>TokenRequest</code> API.</p>
 </td>
 </tr>
 </tbody>
@@ -1766,6 +1850,20 @@ string
 </em>
 </td>
 <td>
+<em>(Optional)</em>
+</td>
+</tr>
+<tr>
+<td>
+<code>caProvider</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.CAProvider">
+CAProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
 </td>
 </tr>
 <tr>

+ 59 - 12
docs/provider/conjur.md

@@ -9,32 +9,32 @@ This section contains the list of the pre-requirements before installing the Con
 *   Running Conjur Server
     -   These items will be needed in order to configure the secret-store
         +   Conjur endpoint - include the scheme but no trailing '/', ex: https://myapi.example.com
-        +   Conjur credentials (hostid, apikey)
+        +   Conjur authentication info (hostid, apikey, jwt service id, etc)
+        +   Conjur must be configured to support your authentication method (`apikey` is supported by default, `jwt` requires additional configuration)
         +   Certificate for Conjur server is OPTIONAL -- But, **when using a self-signed cert when setting up your Conjur server, it is strongly recommended to populate "caBundle" with self-signed cert in the secret-store definition**
 *   Kubernetes cluster
     -   External Secrets Operator is installed
 
-### Create External Secret Store Definition
+### Certificate for Conjur server
 
-Recommend to save as filename: `conjur-secret-store.yaml`
+When using a self-signed cert when setting up your Conjur server, it is strongly recommended to populate "caBundle" with self-signed cert in the secret-store definition. The certificate CA must be referenced on the secret-store definition using either a `caBundle` or `caProvider` as below:
 
 ```yaml
-{% include 'conjur-secret-store.yaml' %}
+{% include 'conjur-ca-bundle.yaml' %}
 ```
 
-### Create External Secret Definition
+### External Secret Store Definition with ApiKey Authentication
+This method uses a combination of the Conjur `hostid` and `apikey` to authenticate to Conjur. This method is the simplest to setup and use as your Conjur instance requires no special setup.
 
-Important note: **Creds must live in the same namespace as a SecretStore  - the secret store may only reference secrets from the same namespace.**  When using a ClusterSecretStore this limitation is lifted and the creds can live in any namespace.
-
-Recommend to save as filename: `conjur-external-secret.yaml`
+#### Create External Secret Store Definition
+Recommend to save as filename: `conjur-secret-store.yaml`
 
 ```yaml
-{% include 'conjur-external-secret.yaml' %}
+{% include 'conjur-secret-store-apikey.yaml' %}
 ```
 
-### Create Kubernetes Secrets
-
-In order for the ESO **Conjur** provider to connect to the Conjur server, the creds should be stored as k8s secrets.  Please refer to <https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret> for various methods to create secrets.  Here is one way to do it using `kubectl`
+#### Create Kubernetes Secrets
+In order for the ESO **Conjur** provider to connect to the Conjur server using the `apikey` creds, these creds should be stored as k8s secrets.  Please refer to <https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret> for various methods to create secrets.  Here is one way to do it using `kubectl`
 
 ***NOTE***: "conjur-creds" is the "name" used in "userRef" and "apikeyRef" in the conjur-secret-store definition
 
@@ -46,6 +46,53 @@ kubectl -n external-secrets create secret generic conjur-creds --from-literal=ho
 # kubectl -n external-secrets create secret generic conjur-creds --from-literal=hostid=host/data/app1/host001 --from-literal=apikey=321blahblah
 ```
 
+### External Secret Store with JWT Authentication
+This method uses JWT tokens to authenticate with Conjur. The following methods for retrieving the JWT token for authentication are supported:
+
+-  JWT token from a referenced Kubernetes Service Account
+-  JWT token stored in a Kubernetes secret
+
+#### Create External Secret Store Definition
+
+When using JWT authentication the following must be specified in the `SecretStore`:
+
+- `account` -  The name of the Conjur account
+- `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that will be used to authenticate the JWT token
+
+You can then choose to either retrieve the JWT token using a Service Account reference or from a Kubernetes Secret.
+
+To use a JWT token from a referenced Kubernetes Service Account, the following secret store definition can be used:
+
+```yaml
+{% include 'conjur-secret-store-jwt-service-account-ref.yaml' %}
+```
+
+This is only supported in Kubernetes 1.22 and above as it uses the [TokenRequest API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-request-v1/) to get the JWT token from the referenced service account. Audiences can be set as required by the [Conjur JWT authenticator](https://docs.conjur.org/Latest/en/Content/Integrations/k8s-ocp/k8s-jwt-authn.htm).
+
+Alternatively, a secret containing a valid JWT token can be referenced as follows:
+
+```yaml
+{% include 'conjur-secret-store-jwt-secret-ref.yaml' %}
+```
+
+This secret must contain a JWT token that identifies your Conjur host. The secret must contain a JWT token consumable by a configured Conjur JWT authenticator and must satisfy all [Conjur JWT guidelines](https://docs.conjur.org/Latest/en/Content/Operations/Services/cjr-authn-jwt-guidelines.htm#Best). This can be a JWT created by an external JWT issuer or the Kubernetes api server itself. Such a with Kubernetes Service Account token can be created using the below command:
+
+```shell
+kubectl create token my-service-account --audience='https://conjur.company.com' --duration=3600s
+```
+
+Save the `SecretStore` definition as filename `conjur-secret-store.yaml` as referenced in later steps.
+
+### Create External Secret Definition
+
+Important note: **Creds must live in the same namespace as a SecretStore  - the secret store may only reference secrets from the same namespace.**  When using a ClusterSecretStore this limitation is lifted and the creds can live in any namespace.
+
+Recommend to save as filename: `conjur-external-secret.yaml`
+
+```yaml
+{% include 'conjur-external-secret.yaml' %}
+```
+
 ### Create the External Secrets Store
 
 ```shell

+ 20 - 0
docs/snippets/conjur-ca-bundle.yaml

@@ -0,0 +1,20 @@
+....
+spec:
+  provider:
+    conjur:
+      # Service URL
+      url: https://myapi.conjur.org
+
+      # [OPTIONAL] base64 encoded string of certificate
+      caBundle: "<base64 encoded cabundle>"
+
+      # [OPTIONAL] caProvider:
+      # Instead of caBundle you can also specify a caProvider
+      # this will retrieve the cert from a Secret or ConfigMap
+      caProvider:
+        type: "Secret" # Can be Secret or ConfigMap
+        name: "<name of secret or configmap>"
+        key: "<key inside secret or configmap>"
+        # namespace is mandatory for ClusterSecretStore and not relevant for SecretStore
+        namespace: "my-cert-secret-namespace"
+  ....

docs/snippets/conjur-secret-store.yaml → docs/snippets/conjur-secret-store-apikey.yaml


+ 19 - 0
docs/snippets/conjur-secret-store-jwt-secret-ref.yaml

@@ -0,0 +1,19 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: conjur
+spec:
+  provider:
+    conjur:
+      # Service URL
+      url: https://myapi.conjur.org
+      # [OPTIONAL] base64 encoded string of certificate
+      caBundle: OPTIONALxFIELDxxxBase64xCertxString==
+      auth:
+        jwt:
+          # conjur account
+          account: conjur
+          serviceID: my-jwt-auth-service # The authn-jwt service ID
+          secretRef: # Secret containing a valid JWT token
+            name: my-jwt-secret
+            key: token

+ 21 - 0
docs/snippets/conjur-secret-store-jwt-service-account-ref.yaml

@@ -0,0 +1,21 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: conjur
+spec:
+  provider:
+    conjur:
+      # Service URL
+      url: https://myapi.conjur.org
+      # [OPTIONAL] base64 encoded string of certificate
+      caBundle: OPTIONALxFIELDxxxBase64xCertxString==
+      auth:
+        jwt:
+          # conjur account
+          account: conjur
+          serviceID: my-jwt-auth-service # The authn-jwt service ID
+          serviceAccountRef: # Service account to retrieve JWT token for
+            name: my-service-account
+            audiences:  # [OPTIONAL] audiences to include in JWT token
+              - https://conjur.company.com
+

+ 110 - 0
pkg/provider/conjur/auth_jwt.go

@@ -0,0 +1,110 @@
+/*
+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 (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/cyberark/conjur-api-go/conjurapi"
+	authenticationv1 "k8s.io/api/authentication/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+const JwtLifespan = 600 // 10 minutes
+
+// getJWTToken retrieves a JWT token either using the TokenRequest API for a specified service account, or from a jwt stored in a k8s secret.
+func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.ConjurJWT) (string, error) {
+	if conjurJWTConfig.ServiceAccountRef != nil {
+		// Should work for Kubernetes >=v1.22: fetch token via TokenRequest API
+		jwtToken, err := p.getJwtFromServiceAccountTokenRequest(ctx, *conjurJWTConfig.ServiceAccountRef, nil, JwtLifespan)
+		if err != nil {
+			return "", err
+		}
+		return jwtToken, nil
+	} else if conjurJWTConfig.SecretRef != nil {
+		tokenRef := conjurJWTConfig.SecretRef
+		if tokenRef.Key == "" {
+			tokenRef = conjurJWTConfig.SecretRef.DeepCopy()
+			tokenRef.Key = "token"
+		}
+		jwtToken, err := p.secretKeyRef(ctx, tokenRef)
+		if err != nil {
+			return "", err
+		}
+		return jwtToken, nil
+	}
+	return "", fmt.Errorf("missing ServiceAccountRef or SecretRef")
+}
+
+// getJwtFromServiceAccountTokenRequest uses the TokenRequest API to get a JWT token for the given service account.
+func (p *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, serviceAccountRef esmeta.ServiceAccountSelector, additionalAud []string, expirationSeconds int64) (string, error) {
+	audiences := serviceAccountRef.Audiences
+	if len(additionalAud) > 0 {
+		audiences = append(audiences, additionalAud...)
+	}
+	tokenRequest := &authenticationv1.TokenRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: p.namespace,
+		},
+		Spec: authenticationv1.TokenRequestSpec{
+			Audiences:         audiences,
+			ExpirationSeconds: &expirationSeconds,
+		},
+	}
+	if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
+		(serviceAccountRef.Namespace != nil) {
+		tokenRequest.Namespace = *serviceAccountRef.Namespace
+	}
+	tokenResponse, err := p.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
+	if err != nil {
+		return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err)
+	}
+	return tokenResponse.Status.Token, nil
+}
+
+// newClientFromJwt creates a new Conjur client using the given JWT Auth Config.
+func (p *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config, jwtAuth *esv1beta1.ConjurJWT) (SecretsClient, error) {
+	jwtToken, getJWTError := p.getJWTToken(ctx, jwtAuth)
+	if getJWTError != nil {
+		return nil, getJWTError
+	}
+
+	client, clientError := p.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID)
+	if clientError != nil {
+		return nil, clientError
+	}
+
+	return client, nil
+}
+
+// newHTTPSClient creates a new HTTPS client with the given cert.
+func newHTTPSClient(cert []byte) (*http.Client, error) {
+	pool := x509.NewCertPool()
+	ok := pool.AppendCertsFromPEM(cert)
+	if !ok {
+		return nil, fmt.Errorf("can't append Conjur SSL cert")
+	}
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12},
+	}
+	return &http.Client{Transport: tr, Timeout: time.Second * 10}, nil
+}

+ 85 - 0
pkg/provider/conjur/conjur_api.go

@@ -0,0 +1,85 @@
+/*
+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 (
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/cyberark/conjur-api-go/conjurapi"
+	"github.com/cyberark/conjur-api-go/conjurapi/authn"
+	"github.com/cyberark/conjur-api-go/conjurapi/response"
+)
+
+// SecretsClient is an interface for the Conjur client.
+type SecretsClient interface {
+	RetrieveSecret(secret string) (result []byte, err error)
+}
+
+// SecretsClientFactory is an interface for creating a Conjur client.
+type SecretsClientFactory interface {
+	NewClientFromKey(config conjurapi.Config, loginPair authn.LoginPair) (SecretsClient, error)
+	NewClientFromJWT(config conjurapi.Config, jwtToken string, jwtServiceID string) (SecretsClient, error)
+}
+
+// ClientAPIImpl is an implementation of the ClientAPI interface.
+type ClientAPIImpl struct{}
+
+func (c *ClientAPIImpl) NewClientFromKey(config conjurapi.Config, loginPair authn.LoginPair) (SecretsClient, error) {
+	return conjurapi.NewClientFromKey(config, loginPair)
+}
+
+// NewClientFromJWT creates a new Conjur client from a JWT token.
+// 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
+func (c *ClientAPIImpl) NewClientFromJWT(config conjurapi.Config, jwtToken, jwtServiceID string) (SecretsClient, error) {
+	jwtTokenString := fmt.Sprintf("jwt=%s", jwtToken)
+
+	var httpClient *http.Client
+	if config.IsHttps() {
+		cert, err := config.ReadSSLCert()
+		if err != nil {
+			return nil, err
+		}
+		httpClient, err = newHTTPSClient(cert)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		httpClient = &http.Client{Timeout: time.Second * 10}
+	}
+
+	authnJwtURL := strings.Join([]string{config.ApplianceURL, "authn-jwt", jwtServiceID, config.Account, "authenticate"}, "/")
+
+	req, err := http.NewRequest("POST", authnJwtURL, strings.NewReader(jwtTokenString))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	resp, err := httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	tokenBytes, err := response.DataResponse(resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return conjurapi.NewClientFromToken(config, string(tokenBytes))
+}

+ 192 - 50
pkg/provider/conjur/provider.go

@@ -24,7 +24,10 @@ import (
 	"github.com/cyberark/conjur-api-go/conjurapi/authn"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/client-go/kubernetes"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
@@ -37,76 +40,130 @@ var (
 	errBadCertBundle    = "caBundle failed to base64 decode: %w"
 	errBadServiceUser   = "could not get Auth.Apikey.UserRef: %w"
 	errBadServiceAPIKey = "could not get Auth.Apikey.ApiKeyRef: %w"
+
+	errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
+
+	errUnableToFetchCAProviderCM     = "unable to fetch Server.CAProvider ConfigMap: %w"
+	errUnableToFetchCAProviderSecret = "unable to fetch Server.CAProvider Secret: %w"
 )
 
-// Provider is a provider for Conjur.
-type Provider struct {
-	ConjurClient Client
-	StoreKind    string
-	kube         client.Client
-	namespace    string
+// Client is a provider for Conjur.
+type Client struct {
+	StoreKind string
+	kube      client.Client
+	store     esv1beta1.GenericStore
+	namespace string
+	corev1    typedcorev1.CoreV1Interface
+	clientAPI SecretsClientFactory
+	client    SecretsClient
 }
 
-// Client is an interface for the Conjur client.
-type Client interface {
-	RetrieveSecret(secret string) (result []byte, err error)
+type Provider struct {
+	NewConjurProvider func(context context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientApi SecretsClientFactory) (esv1beta1.SecretsClient, error)
 }
 
 // NewClient creates a new Conjur client.
-func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
-	prov, err := util.GetConjurProvider(store)
+func (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	// controller-runtime/client does not support TokenRequest or other subresource APIs
+	// so we need to construct our own client and use it to create a TokenRequest
+	restCfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	clientset, err := kubernetes.NewForConfig(restCfg)
 	if err != nil {
 		return nil, err
 	}
-	p.StoreKind = store.GetObjectKind().GroupVersionKind().Kind
-	p.kube = kube
-	p.namespace = namespace
 
-	certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(prov.CABundle))
-	if decodeErr != nil {
-		return nil, fmt.Errorf(errBadCertBundle, decodeErr)
+	return c.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
+}
+
+func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientAPI SecretsClientFactory) (esv1beta1.SecretsClient, error) {
+	return &Client{
+		StoreKind: store.GetObjectKind().GroupVersionKind().Kind,
+		store:     store,
+		kube:      kube,
+		namespace: namespace,
+		corev1:    corev1,
+		clientAPI: clientAPI,
+	}, nil
+}
+
+func (p *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
+	// if the client is initialized already, return it
+	if p.client != nil {
+		return p.client, nil
+	}
+
+	prov, err := util.GetConjurProvider(p.store)
+	if err != nil {
+		return nil, err
+	}
+
+	cert, getCertErr := p.getCA(ctx, prov)
+	if getCertErr != nil {
+		return nil, getCertErr
 	}
-	cert := string(certBytes)
 
 	config := conjurapi.Config{
-		Account:      prov.Auth.Apikey.Account,
 		ApplianceURL: prov.URL,
 		SSLCert:      cert,
 	}
 
-	conjUser, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.UserRef)
-	if secErr != nil {
-		return nil, fmt.Errorf(errBadServiceUser, secErr)
-	}
-	conjAPIKey, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.APIKeyRef)
-	if secErr != nil {
-		return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
-	}
+	if prov.Auth.Apikey != nil {
+		config.Account = prov.Auth.Apikey.Account
+		conjUser, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.UserRef)
+		if secErr != nil {
+			return nil, fmt.Errorf(errBadServiceUser, secErr)
+		}
+		conjAPIKey, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.APIKeyRef)
+		if secErr != nil {
+			return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
+		}
 
-	conjur, err := conjurapi.NewClientFromKey(config,
-		authn.LoginPair{
-			Login:  conjUser,
-			APIKey: conjAPIKey,
-		},
-	)
+		conjur, newClientFromKeyError := p.clientAPI.NewClientFromKey(config,
+			authn.LoginPair{
+				Login:  conjUser,
+				APIKey: conjAPIKey,
+			},
+		)
 
-	if err != nil {
-		return nil, fmt.Errorf(errConjurClient, err)
+		if newClientFromKeyError != nil {
+			return nil, fmt.Errorf(errConjurClient, newClientFromKeyError)
+		}
+		p.client = conjur
+		return conjur, nil
+	} else if prov.Auth.Jwt != nil {
+		config.Account = prov.Auth.Jwt.Account
+
+		conjur, clientFromJwtError := p.newClientFromJwt(ctx, config, prov.Auth.Jwt)
+		if clientFromJwtError != nil {
+			return nil, fmt.Errorf(errConjurClient, clientFromJwtError)
+		}
+
+		p.client = conjur
+
+		return conjur, nil
+	} else {
+		// Should not happen because validate func should catch this
+		return nil, fmt.Errorf("no authentication method provided")
 	}
-	p.ConjurClient = conjur
-	return p, nil
 }
 
 // GetAllSecrets returns all secrets from the provider.
 // NOT IMPLEMENTED.
-func (p *Provider) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+func (p *Client) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented
 	return nil, fmt.Errorf("GetAllSecrets not implemented")
 }
 
 // GetSecret returns a single secret from the provider.
-func (p *Provider) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	secretValue, err := p.ConjurClient.RetrieveSecret(ref.Key)
+func (p *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	conjurClient, getConjurClientError := p.GetConjurClient(ctx)
+	if getConjurClientError != nil {
+		return nil, getConjurClientError
+	}
+	secretValue, err := conjurClient.RetrieveSecret(ref.Key)
 	if err != nil {
 		return nil, err
 	}
@@ -115,18 +172,18 @@ func (p *Provider) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretData
 }
 
 // PushSecret will write a single secret into the provider.
-func (p *Provider) PushSecret(_ context.Context, _ []byte, _ *apiextensionsv1.JSON, _ esv1beta1.PushRemoteRef) error {
+func (p *Client) PushSecret(_ context.Context, _ []byte, _ *apiextensionsv1.JSON, _ esv1beta1.PushRemoteRef) error {
 	// NOT IMPLEMENTED
 	return nil
 }
 
-func (p *Provider) DeleteSecret(_ context.Context, _ esv1beta1.PushRemoteRef) error {
+func (p *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushRemoteRef) error {
 	// NOT IMPLEMENTED
 	return nil
 }
 
 // GetSecretMap returns multiple k/v pairs from the provider.
-func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+func (p *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	// Gets a secret as normal, expecting secret value to be a json object
 	data, err := p.GetSecret(ctx, ref)
 	if err != nil {
@@ -149,17 +206,17 @@ func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecre
 }
 
 // Close closes the provider.
-func (p *Provider) Close(_ context.Context) error {
+func (p *Client) Close(_ context.Context) error {
 	return nil
 }
 
 // Validate validates the provider.
-func (p *Provider) Validate() (esv1beta1.ValidationResult, error) {
+func (p *Client) Validate() (esv1beta1.ValidationResult, error) {
 	return esv1beta1.ValidationResultReady, nil
 }
 
 // ValidateStore validates the store.
-func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+func (c *Provider) ValidateStore(store esv1beta1.GenericStore) error {
 	prov, err := util.GetConjurProvider(store)
 	if err != nil {
 		return err
@@ -186,8 +243,30 @@ func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
 		}
 	}
 
+	if prov.Auth.Jwt != nil {
+		if prov.Auth.Jwt.Account == "" {
+			return fmt.Errorf("missing Auth.Jwt.Account")
+		}
+		if prov.Auth.Jwt.ServiceID == "" {
+			return fmt.Errorf("missing Auth.Jwt.ServiceID")
+		}
+		if prov.Auth.Jwt.ServiceAccountRef == nil && prov.Auth.Jwt.SecretRef == nil {
+			return fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef")
+		}
+		if prov.Auth.Jwt.SecretRef != nil {
+			if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.Jwt.SecretRef); err != nil {
+				return fmt.Errorf("invalid Auth.Jwt.SecretRef: %w", err)
+			}
+		}
+		if prov.Auth.Jwt.ServiceAccountRef != nil {
+			if err := utils.ValidateReferentServiceAccountSelector(store, *prov.Auth.Jwt.ServiceAccountRef); err != nil {
+				return fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
+			}
+		}
+	}
+
 	// At least one auth must be configured
-	if prov.Auth.Apikey == nil {
+	if prov.Auth.Apikey == nil && prov.Auth.Jwt == nil {
 		return fmt.Errorf("missing Auth.* configuration")
 	}
 
@@ -195,11 +274,11 @@ func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
 }
 
 // Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
-func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+func (c *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
 	return esv1beta1.SecretStoreReadOnly
 }
 
-func (p *Provider) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
+func (p *Client) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
 	secret := &corev1.Secret{}
 	ref := client.ObjectKey{
 		Namespace: p.namespace,
@@ -224,8 +303,71 @@ func (p *Provider) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKey
 	return valueStr, nil
 }
 
+// configMapKeyRef returns the value of a key in a configmap.
+func (p *Client) configMapKeyRef(ctx context.Context, cmRef *esmeta.SecretKeySelector) (string, error) {
+	configMap := &corev1.ConfigMap{}
+	ref := client.ObjectKey{
+		Namespace: p.namespace,
+		Name:      cmRef.Name,
+	}
+	if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
+		(cmRef.Namespace != nil) {
+		ref.Namespace = *cmRef.Namespace
+	}
+	err := p.kube.Get(ctx, ref, configMap)
+	if err != nil {
+		return "", err
+	}
+
+	keyBytes, ok := configMap.Data[cmRef.Key]
+	if !ok {
+		return "", err
+	}
+
+	valueStr := strings.TrimSpace(keyBytes)
+	return valueStr, nil
+}
+
+// getCA try retrieve the CA bundle from the provider CABundle or from the CAProvider.
+func (p *Client) getCA(ctx context.Context, provider *esv1beta1.ConjurProvider) (string, error) {
+	if provider.CAProvider != nil {
+		var ca string
+		var err error
+		switch provider.CAProvider.Type {
+		case esv1beta1.CAProviderTypeConfigMap:
+			keySelector := esmeta.SecretKeySelector{
+				Name:      provider.CAProvider.Name,
+				Namespace: provider.CAProvider.Namespace,
+				Key:       provider.CAProvider.Key,
+			}
+			ca, err = p.configMapKeyRef(ctx, &keySelector)
+			if err != nil {
+				return "", fmt.Errorf(errUnableToFetchCAProviderCM, err)
+			}
+		case esv1beta1.CAProviderTypeSecret:
+			keySelector := esmeta.SecretKeySelector{
+				Name:      provider.CAProvider.Name,
+				Namespace: provider.CAProvider.Namespace,
+				Key:       provider.CAProvider.Key,
+			}
+			ca, err = p.secretKeyRef(ctx, &keySelector)
+			if err != nil {
+				return "", fmt.Errorf(errUnableToFetchCAProviderSecret, err)
+			}
+		}
+		return ca, nil
+	}
+	certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(provider.CABundle))
+	if decodeErr != nil {
+		return "", fmt.Errorf(errBadCertBundle, decodeErr)
+	}
+	return string(certBytes), nil
+}
+
 func init() {
-	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&Provider{
+		NewConjurProvider: newConjurProvider,
+	}, &esv1beta1.SecretStoreProvider{
 		Conjur: &esv1beta1.ConjurProvider{},
 	})
 }

+ 426 - 42
pkg/provider/conjur/provider_test.go

@@ -16,12 +16,26 @@ package conjur
 
 import (
 	"context"
+	"errors"
 	"fmt"
+	"reflect"
 	"testing"
+	"time"
+
+	"github.com/cyberark/conjur-api-go/conjurapi"
+	"github.com/cyberark/conjur-api-go/conjurapi/authn"
+	"github.com/golang-jwt/jwt/v5"
+	"github.com/google/go-cmp/cmp"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	fakeconjur "github.com/external-secrets/external-secrets/pkg/provider/conjur/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/conjur/fake"
+	utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
 )
 
 var (
@@ -31,39 +45,6 @@ var (
 	svcAccount = "account1"
 )
 
-type secretManagerTestCase struct {
-	err    error
-	refKey string
-}
-
-func TestConjurGetSecret(t *testing.T) {
-	p := Provider{}
-	p.ConjurClient = &fakeconjur.ConjurMockClient{}
-
-	testCases := []*secretManagerTestCase{
-		{
-			err:    nil,
-			refKey: "secret",
-		},
-		{
-			err:    fmt.Errorf("error"),
-			refKey: "error",
-		},
-	}
-
-	for _, tc := range testCases {
-		ref := makeValidRef(tc.refKey)
-		_, err := p.GetSecret(context.Background(), *ref)
-		if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
-			t.Errorf("test failed! want %v, got %v", tc.err, err)
-		} else if tc.err == nil && err != nil {
-			t.Errorf("want nil got err %v", err)
-		} else if tc.err != nil && err == nil {
-			t.Errorf("want err %v got nil", tc.err)
-		}
-	}
-}
-
 func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
 	return &esv1beta1.ExternalSecretDataRemoteRef{
 		Key:     k,
@@ -79,29 +60,59 @@ type ValidateStoreTestCase struct {
 func TestValidateStore(t *testing.T) {
 	testCases := []ValidateStoreTestCase{
 		{
-			store: makeSecretStore(svcURL, svcUser, svcApikey, svcAccount),
+			store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
 			err:   nil,
 		},
 		{
-			store: makeSecretStore("", svcUser, svcApikey, svcAccount),
+			store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
 			err:   fmt.Errorf("conjur URL cannot be empty"),
 		},
 		{
-			store: makeSecretStore(svcURL, "", svcApikey, svcAccount),
+			store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
 			err:   fmt.Errorf("missing Auth.Apikey.UserRef"),
 		},
 		{
-			store: makeSecretStore(svcURL, svcUser, "", svcAccount),
+			store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
 			err:   fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
 		},
 		{
-			store: makeSecretStore(svcURL, svcUser, svcApikey, ""),
+			store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
 			err:   fmt.Errorf("missing Auth.ApiKey.Account"),
 		},
+
+		{
+			store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount"),
+			err:   nil,
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-auth-service", "myconjuraccount"),
+			err:   nil,
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", ""),
+			err:   fmt.Errorf("missing Auth.Jwt.Account"),
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "conjur", "", "", "myconjuraccount"),
+			err:   fmt.Errorf("missing Auth.Jwt.ServiceID"),
+		},
+		{
+			store: makeJWTSecretStore("", "conjur", "", "jwt-auth-service", "myconjuraccount"),
+			err:   fmt.Errorf("conjur URL cannot be empty"),
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "", "", "jwt-auth-service", "myconjuraccount"),
+			err:   fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
+		},
+
+		{
+			store: makeNoAuthSecretStore(svcURL),
+			err:   fmt.Errorf("missing Auth.* configuration"),
+		},
 	}
-	p := Provider{}
+	c := Provider{}
 	for _, tc := range testCases {
-		err := p.ValidateStore(tc.store)
+		err := c.ValidateStore(tc.store)
 		if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
 			t.Errorf("test failed! want %v, got %v", tc.err, err)
 		} else if tc.err == nil && err != nil {
@@ -112,7 +123,214 @@ func TestValidateStore(t *testing.T) {
 	}
 }
 
-func makeSecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1beta1.SecretStore {
+func TestGetSecret(t *testing.T) {
+	type args struct {
+		store      esv1beta1.GenericStore
+		kube       kclient.Client
+		corev1     typedcorev1.CoreV1Interface
+		namespace  string
+		secretPath string
+	}
+
+	type want struct {
+		err   error
+		value string
+	}
+
+	type testCase struct {
+		reason string
+		args   args
+		want   want
+	}
+
+	cases := map[string]testCase{
+		"ApiKeyReadSecretSuccess": {
+			reason: "Should read a secret successfully using an ApiKey auth secret store.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+			},
+			want: want{
+				err:   nil,
+				value: "secret",
+			},
+		},
+		"ApiKeyReadSecretFailure": {
+			reason: "Should fail to read secret using ApiKey auth secret store.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace:  "default",
+				secretPath: "error",
+			},
+			want: want{
+				err:   errors.New("error"),
+				value: "",
+			},
+		},
+		"JwtWithServiceAccountRefReadSecretSuccess": {
+			reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
+			args: args{
+				store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects().Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+				corev1:     utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+			},
+			want: want{
+				err:   nil,
+				value: "secret",
+			},
+		},
+		"JwtWithSecretRefReadSecretSuccess": {
+			reason: "Should read a secret successfully using an JWT auth secret store that references a k8s secret.",
+			args: args{
+				store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-authenticator", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(&corev1.Secret{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "jwt-secret",
+							Namespace: "default",
+						},
+						Data: map[string][]byte{
+							"token": []byte(createFakeJwtToken(true)),
+						},
+					}).Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+			},
+			want: want{
+				err:   nil,
+				value: "secret",
+			},
+		},
+		"JwtWithCABundleSuccess": {
+			reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
+			args: args{
+				store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects().Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+				corev1:     utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+			},
+			want: want{
+				err:   nil,
+				value: "secret",
+			},
+		},
+	}
+
+	runTest := func(t *testing.T, _ string, tc testCase) {
+		provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
+		ref := makeValidRef(tc.args.secretPath)
+		secret, err := provider.GetSecret(context.Background(), *ref)
+		if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+			t.Errorf("\n%s\nconjur.GetSecret(...): -want error, +got error:\n%s", tc.reason, diff)
+		}
+		secretString := string(secret)
+		if secretString != tc.want.value {
+			t.Errorf("\n%s\nconjur.GetSecret(...): want value %v got %v", tc.reason, tc.want.value, secretString)
+		}
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			runTest(t, name, tc)
+		})
+	}
+}
+
+func TestGetCA(t *testing.T) {
+	type args struct {
+		store     esv1beta1.GenericStore
+		kube      kclient.Client
+		corev1    typedcorev1.CoreV1Interface
+		namespace string
+	}
+
+	type want struct {
+		err  error
+		cert string
+	}
+
+	type testCase struct {
+		reason string
+		args   args
+		want   want
+	}
+
+	certData := "mycertdata"
+	certDataEncoded := "bXljZXJ0ZGF0YQo="
+
+	cases := map[string]testCase{
+		"UseCABundleSuccess": {
+			reason: "Should read a caBundle successfully.",
+			args: args{
+				store: makeStoreWithCA("cabundle", certDataEncoded),
+				kube: clientfake.NewClientBuilder().
+					WithObjects().Build(),
+				namespace: "default",
+				corev1:    utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+			},
+			want: want{
+				err:  nil,
+				cert: certDataEncoded,
+			},
+		},
+		"UseCAProviderConfigMapSuccess": {
+			reason: "Should read a ca from a ConfigMap successfully.",
+			args: args{
+				store: makeStoreWithCA("configmap", ""),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeCASource("configmap", certData)).Build(),
+				namespace: "default",
+				corev1:    utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+			},
+			want: want{
+				err:  nil,
+				cert: certDataEncoded,
+			},
+		},
+		"UseCAProviderSecretSuccess": {
+			reason: "Should read a ca from a Secret successfully.",
+			args: args{
+				store: makeStoreWithCA("secret", ""),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeCASource("secret", certData)).Build(),
+				namespace: "default",
+				corev1:    utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+			},
+			want: want{
+				err:  nil,
+				cert: certDataEncoded,
+			},
+		},
+	}
+
+	runTest := func(t *testing.T, _ string, tc testCase) {
+		provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
+		_, err := provider.GetSecret(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
+			Key: "path/to/secret",
+		})
+		if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+			t.Errorf("\n%s\nconjur.GetCA(...): -want error, +got error:\n%s", tc.reason, diff)
+		}
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			runTest(t, name, tc)
+		})
+	}
+}
+
+func makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1beta1.SecretStore {
 	uref := &esmeta.SecretKeySelector{
 		Name: "user",
 		Key:  "conjur-hostid",
@@ -145,3 +363,169 @@ func makeSecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1beta1.S
 	}
 	return store
 }
+
+func makeJWTSecretStore(svcURL, serviceAccountName, secretName, jwtServiceID, conjurAccount string) *esv1beta1.SecretStore {
+	serviceAccountRef := &esmeta.ServiceAccountSelector{
+		Name:      serviceAccountName,
+		Audiences: []string{"conjur"},
+	}
+	if serviceAccountName == "" {
+		serviceAccountRef = nil
+	}
+
+	secretRef := &esmeta.SecretKeySelector{
+		Name: secretName,
+		Key:  "token",
+	}
+	if secretName == "" {
+		secretRef = nil
+	}
+
+	store := &esv1beta1.SecretStore{
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				Conjur: &esv1beta1.ConjurProvider{
+					URL: svcURL,
+					Auth: esv1beta1.ConjurAuth{
+						Jwt: &esv1beta1.ConjurJWT{
+							Account:           conjurAccount,
+							ServiceID:         jwtServiceID,
+							ServiceAccountRef: serviceAccountRef,
+							SecretRef:         secretRef,
+						},
+					},
+				},
+			},
+		},
+	}
+	return store
+}
+
+func makeStoreWithCA(caSource, caData string) *esv1beta1.SecretStore {
+	store := makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount")
+	if caSource == "secret" {
+		store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
+			Type: esv1beta1.CAProviderTypeSecret,
+			Name: "conjur-cert",
+			Key:  "ca",
+		}
+	} else if caSource == "configmap" {
+		store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
+			Type: esv1beta1.CAProviderTypeConfigMap,
+			Name: "conjur-cert",
+			Key:  "ca",
+		}
+	} else {
+		store.Spec.Provider.Conjur.CABundle = caData
+	}
+	return store
+}
+
+func makeNoAuthSecretStore(svcURL string) *esv1beta1.SecretStore {
+	store := &esv1beta1.SecretStore{
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				Conjur: &esv1beta1.ConjurProvider{
+					URL: svcURL,
+				},
+			},
+		},
+	}
+	return store
+}
+
+func makeFakeAPIKeySecrets() []kclient.Object {
+	return []kclient.Object{
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "user",
+				Namespace: "default",
+			},
+			Data: map[string][]byte{
+				"conjur-hostid": []byte("myhostid"),
+			},
+		},
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "apikey",
+				Namespace: "default",
+			},
+			Data: map[string][]byte{
+				"conjur-apikey": []byte("apikey"),
+			},
+		},
+	}
+}
+
+func makeFakeCASource(kind, caData string) kclient.Object {
+	if kind == "secret" {
+		return &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "conjur-cert",
+				Namespace: "default",
+			},
+			Data: map[string][]byte{
+				"conjur-cert": []byte(caData),
+			},
+		}
+	}
+	return &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "conjur-cert",
+			Namespace: "default",
+		},
+		Data: map[string]string{
+			"ca": caData,
+		},
+	}
+}
+
+func createFakeJwtToken(expires bool) string {
+	signingKey := []byte("fakekey")
+	token := jwt.New(jwt.SigningMethodHS256)
+	claims := token.Claims.(jwt.MapClaims)
+	if expires {
+		claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
+	}
+	jwtTokenString, err := token.SignedString(signingKey)
+	if err != nil {
+		panic(err)
+	}
+	return jwtTokenString
+}
+
+// ConjurMockAPIClient is a mock implementation of the ApiClient interface.
+type ConjurMockAPIClient struct {
+}
+
+func (c *ConjurMockAPIClient) NewClientFromKey(_ conjurapi.Config, _ authn.LoginPair) (SecretsClient, error) {
+	return &fake.ConjurMockClient{}, nil
+}
+
+func (c *ConjurMockAPIClient) NewClientFromJWT(_ conjurapi.Config, _, _ string) (SecretsClient, error) {
+	return &fake.ConjurMockClient{}, nil
+}
+
+// EquateErrors returns true if the supplied errors are of the same type and
+// produce identical strings. This mirrors the error comparison behavior of
+// https://github.com/go-test/deep, which most Crossplane tests targeted before
+// we switched to go-cmp.
+//
+// This differs from cmpopts.EquateErrors, which does not test for error strings
+// and instead returns whether one error 'is' (in the errors.Is sense) the
+// other.
+func EquateErrors() cmp.Option {
+	return cmp.Comparer(func(a, b error) bool {
+		if a == nil || b == nil {
+			return a == nil && b == nil
+		}
+
+		av := reflect.ValueOf(a)
+		bv := reflect.ValueOf(b)
+		if av.Type() != bv.Type() {
+			return false
+		}
+
+		return a.Error() == b.Error()
+	})
+}