Browse Source

feat(conjur): Certificate Auth support (#6393)

* Conjur Certificate Auth support

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* Add max and min properties validations to ConjurAuth struct

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* Update docs/provider/conjur.md

Co-authored-by: Jean-Philippe Evrard <jean-philippe.evrard+rochepub@external.roche.com>
Signed-off-by: Hubert Dąbrowski <131349909+hdabrowski@users.noreply.github.com>
Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* Add missing required and optional statements to secretstore types

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* Add missing required and optional statements to secretstore types

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* Add more validations to ConjurAuth struct

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* fix e2e SPIFFE cert-auth scenarios

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* fix e2e local setup issues

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* pin the version of conjur to download authenticator service from

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

* pin the version of conjur charts

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>

---------

Signed-off-by: Hubert Dąbrowski <hubert.dabrowski@cyberark.com>
Signed-off-by: Hubert Dąbrowski <131349909+hdabrowski@users.noreply.github.com>
Co-authored-by: Jean-Philippe Evrard <jean-philippe.evrard+rochepub@external.roche.com>
Hubert Dąbrowski 1 week ago
parent
commit
8cd018b425

+ 38 - 0
apis/externalsecrets/v1/secretstore_conjur_types.go

@@ -21,6 +21,7 @@ import esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 // ConjurProvider provides access to a Conjur provider.
 type ConjurProvider struct {
 	// URL is the endpoint of the Conjur instance.
+	// +required
 	URL string `json:"url"`
 
 	// CABundle is a PEM encoded CA bundle that will be used to validate the Conjur server certificate.
@@ -34,10 +35,13 @@ type ConjurProvider struct {
 	CAProvider *CAProvider `json:"caProvider,omitempty"`
 
 	// Defines authentication settings for connecting to Conjur.
+	// +required
 	Auth ConjurAuth `json:"auth"`
 }
 
 // ConjurAuth is the way to provide authentication credentials to the ConjurProvider.
+// +kubebuilder:validation:MaxProperties=1
+// +kubebuilder:validation:MinProperties=1
 type ConjurAuth struct {
 	// Authenticates with Conjur using an API key.
 	// +optional
@@ -46,29 +50,38 @@ type ConjurAuth struct {
 	// Jwt enables JWT authentication using Kubernetes service account tokens.
 	// +optional
 	Jwt *ConjurJWT `json:"jwt,omitempty"`
+
+	// Cert enables certificate-based authentication using a client certificate and key.
+	// +optional
+	Cert *ConjurCert `json:"cert,omitempty"`
 }
 
 // ConjurAPIKey contains references to a Secret resource that holds
 // the Conjur username and API key.
 type ConjurAPIKey struct {
 	// Account is the Conjur organization account name.
+	// +required
 	Account string `json:"account"`
 
 	// A reference to a specific 'key' containing the Conjur username
 	// within a Secret resource. In some instances, `key` is a required field.
+	// +required
 	UserRef *esmeta.SecretKeySelector `json:"userRef"`
 
 	// A reference to a specific 'key' containing the Conjur API key
 	// within a Secret resource. In some instances, `key` is a required field.
+	// +required
 	APIKeyRef *esmeta.SecretKeySelector `json:"apiKeyRef"`
 }
 
 // ConjurJWT defines the JWT authentication configuration for Conjur provider.
 type ConjurJWT struct {
 	// Account is the Conjur organization account name.
+	// +required
 	Account string `json:"account"`
 
 	// The conjur authn jwt webservice id
+	// +required
 	ServiceID string `json:"serviceID"`
 
 	// Optional HostID for JWT authentication. This may be used depending
@@ -86,3 +99,28 @@ type ConjurJWT struct {
 	// +optional
 	ServiceAccountRef *esmeta.ServiceAccountSelector `json:"serviceAccountRef,omitempty"`
 }
+
+// ConjurCert defines the Cert authentication configuration for Conjur provider.
+type ConjurCert struct {
+	// Account is the Conjur organization account name.
+	// +required
+	Account string `json:"account"`
+
+	// The conjur authn cert webservice id
+	// +required
+	ServiceID string `json:"serviceID"`
+
+	// Optional HostID for cert authentication (can be omitted when using 'spiffe' mode).
+	// +optional
+	HostID string `json:"hostId,omitempty"`
+
+	// ClientCertRef is a reference to a specific 'key' containing the client certificate
+	// within a Secret resource. The certificate must be PEM-encoded.
+	// +required
+	ClientCertRef *esmeta.SecretKeySelector `json:"clientCertRef"`
+
+	// ClientKeyRef is a reference to a specific 'key' containing the private RSA client key
+	// within a Secret resource. The key must be PEM-encoded.
+	// +required
+	ClientKeyRef *esmeta.SecretKeySelector `json:"clientKeyRef"`
+}

+ 30 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -1244,6 +1244,11 @@ func (in *ConjurAuth) DeepCopyInto(out *ConjurAuth) {
 		*out = new(ConjurJWT)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Cert != nil {
+		in, out := &in.Cert, &out.Cert
+		*out = new(ConjurCert)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConjurAuth.
@@ -1256,6 +1261,31 @@ func (in *ConjurAuth) DeepCopy() *ConjurAuth {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ConjurCert) DeepCopyInto(out *ConjurCert) {
+	*out = *in
+	if in.ClientCertRef != nil {
+		in, out := &in.ClientCertRef, &out.ClientCertRef
+		*out = new(apismetav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.ClientKeyRef != nil {
+		in, out := &in.ClientKeyRef, &out.ClientKeyRef
+		*out = new(apismetav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConjurCert.
+func (in *ConjurCert) DeepCopy() *ConjurCert {
+	if in == nil {
+		return nil
+	}
+	out := new(ConjurCert)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // 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

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

@@ -1588,6 +1588,8 @@ spec:
                       auth:
                         description: Defines authentication settings for connecting
                           to Conjur.
+                        maxProperties: 1
+                        minProperties: 1
                         properties:
                           apikey:
                             description: Authenticates with Conjur using an API key.
@@ -1659,6 +1661,85 @@ spec:
                             - apiKeyRef
                             - userRef
                             type: object
+                          cert:
+                            description: Cert enables certificate-based authentication
+                              using a client certificate and key.
+                            properties:
+                              account:
+                                description: Account is the Conjur organization account
+                                  name.
+                                type: string
+                              clientCertRef:
+                                description: |-
+                                  ClientCertRef is a reference to a specific 'key' containing the client certificate
+                                  within a Secret resource. The certificate must be PEM-encoded.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              clientKeyRef:
+                                description: |-
+                                  ClientKeyRef is a reference to a specific 'key' containing the private RSA client key
+                                  within a Secret resource. The key must be PEM-encoded.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              hostId:
+                                description: Optional HostID for cert authentication
+                                  (can be omitted when using 'spiffe' mode).
+                                type: string
+                              serviceID:
+                                description: The conjur authn cert webservice id
+                                type: string
+                            required:
+                            - account
+                            - clientCertRef
+                            - clientKeyRef
+                            - serviceID
+                            type: object
                           jwt:
                             description: Jwt enables JWT authentication using Kubernetes
                               service account tokens.

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

@@ -1588,6 +1588,8 @@ spec:
                       auth:
                         description: Defines authentication settings for connecting
                           to Conjur.
+                        maxProperties: 1
+                        minProperties: 1
                         properties:
                           apikey:
                             description: Authenticates with Conjur using an API key.
@@ -1659,6 +1661,85 @@ spec:
                             - apiKeyRef
                             - userRef
                             type: object
+                          cert:
+                            description: Cert enables certificate-based authentication
+                              using a client certificate and key.
+                            properties:
+                              account:
+                                description: Account is the Conjur organization account
+                                  name.
+                                type: string
+                              clientCertRef:
+                                description: |-
+                                  ClientCertRef is a reference to a specific 'key' containing the client certificate
+                                  within a Secret resource. The certificate must be PEM-encoded.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              clientKeyRef:
+                                description: |-
+                                  ClientKeyRef is a reference to a specific 'key' containing the private RSA client key
+                                  within a Secret resource. The key must be PEM-encoded.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              hostId:
+                                description: Optional HostID for cert authentication
+                                  (can be omitted when using 'spiffe' mode).
+                                type: string
+                              serviceID:
+                                description: The conjur authn cert webservice id
+                                type: string
+                            required:
+                            - account
+                            - clientCertRef
+                            - clientKeyRef
+                            - serviceID
+                            type: object
                           jwt:
                             description: Jwt enables JWT authentication using Kubernetes
                               service account tokens.

+ 152 - 0
deploy/crds/bundle.yaml

@@ -3804,6 +3804,8 @@ spec:
                       properties:
                         auth:
                           description: Defines authentication settings for connecting to Conjur.
+                          maxProperties: 1
+                          minProperties: 1
                           properties:
                             apikey:
                               description: Authenticates with Conjur using an API key.
@@ -3872,6 +3874,80 @@ spec:
                                 - apiKeyRef
                                 - userRef
                               type: object
+                            cert:
+                              description: Cert enables certificate-based authentication using a client certificate and key.
+                              properties:
+                                account:
+                                  description: Account is the Conjur organization account name.
+                                  type: string
+                                clientCertRef:
+                                  description: |-
+                                    ClientCertRef is a reference to a specific 'key' containing the client certificate
+                                    within a Secret resource. The certificate must be PEM-encoded.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                clientKeyRef:
+                                  description: |-
+                                    ClientKeyRef is a reference to a specific 'key' containing the private RSA client key
+                                    within a Secret resource. The key must be PEM-encoded.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                hostId:
+                                  description: Optional HostID for cert authentication (can be omitted when using 'spiffe' mode).
+                                  type: string
+                                serviceID:
+                                  description: The conjur authn cert webservice id
+                                  type: string
+                              required:
+                                - account
+                                - clientCertRef
+                                - clientKeyRef
+                                - serviceID
+                              type: object
                             jwt:
                               description: Jwt enables JWT authentication using Kubernetes service account tokens.
                               properties:
@@ -16581,6 +16657,8 @@ spec:
                       properties:
                         auth:
                           description: Defines authentication settings for connecting to Conjur.
+                          maxProperties: 1
+                          minProperties: 1
                           properties:
                             apikey:
                               description: Authenticates with Conjur using an API key.
@@ -16649,6 +16727,80 @@ spec:
                                 - apiKeyRef
                                 - userRef
                               type: object
+                            cert:
+                              description: Cert enables certificate-based authentication using a client certificate and key.
+                              properties:
+                                account:
+                                  description: Account is the Conjur organization account name.
+                                  type: string
+                                clientCertRef:
+                                  description: |-
+                                    ClientCertRef is a reference to a specific 'key' containing the client certificate
+                                    within a Secret resource. The certificate must be PEM-encoded.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                clientKeyRef:
+                                  description: |-
+                                    ClientKeyRef is a reference to a specific 'key' containing the private RSA client key
+                                    within a Secret resource. The key must be PEM-encoded.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                hostId:
+                                  description: Optional HostID for cert authentication (can be omitted when using 'spiffe' mode).
+                                  type: string
+                                serviceID:
+                                  description: The conjur authn cert webservice id
+                                  type: string
+                              required:
+                                - account
+                                - clientCertRef
+                                - clientKeyRef
+                                - serviceID
+                              type: object
                             jwt:
                               description: Jwt enables JWT authentication using Kubernetes service account tokens.
                               properties:

+ 95 - 0
docs/api/spec.md

@@ -3268,6 +3268,101 @@ ConjurJWT
 <p>Jwt enables JWT authentication using Kubernetes service account tokens.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>cert</code></br>
+<em>
+<a href="#external-secrets.io/v1.ConjurCert">
+ConjurCert
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Cert enables certificate-based authentication using a client certificate and key.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.ConjurCert">ConjurCert
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.ConjurAuth">ConjurAuth</a>)
+</p>
+<p>
+<p>ConjurCert defines the Cert authentication configuration for Conjur provider.</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>
+<p>Account is the Conjur organization account name.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>serviceID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>The conjur authn cert webservice id</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>hostId</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Optional HostID for cert authentication (can be omitted when using &lsquo;spiffe&rsquo; mode).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>clientCertRef</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>
+<p>ClientCertRef is a reference to a specific &lsquo;key&rsquo; containing the client certificate
+within a Secret resource. The certificate must be PEM-encoded.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>clientKeyRef</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>
+<p>ClientKeyRef is a reference to a specific &lsquo;key&rsquo; containing the private RSA client key
+within a Secret resource. The key must be PEM-encoded.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.ConjurJWT">ConjurJWT

+ 56 - 3
docs/provider/conjur.md

@@ -8,8 +8,8 @@ Before installing the Secrets Manager provider, you need:
 
 * A running instance of [Conjur OSS](https://github.com/cyberark/conjur) or CyberArk Secrets Manager, with:
   * An accessible Secrets Manager endpoint (for example: `https://myapi.example.com`).
-  * Your configured Secrets Manager authentication info (such as `hostid`, `apikey`, or JWT service ID). For more information on configuring Secrets Manager, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
-  * Support for your authentication method (`apikey` is supported by default, `jwt` requires additional configuration).
+  * Your configured Secrets Manager authentication info (such as `hostid`, `apikey` or service ID of JWT or Cert authenticator). For more information on configuring Secrets Manager, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
+  * Support for your authentication method (`apikey` is supported by default, `jwt` and `cert` require additional configuration).
   * **Optional**: Secrets Manager server certificate (see [below](#conjur-server-certificate)).
 * A Kubernetes cluster with ESO installed.
 
@@ -23,10 +23,11 @@ If you set up your Secrets Manager server with a self-signed certificate, we rec
 
 ## External secret store
 
-The Secrets Manager provider is configured as an external secret store in ESO. The Secrets Manager provider supports these two methods to authenticate to Secrets Manager:
+To synchronise secrets with Secrets Manager, you must configure an external secret store with one of the following three methods to authenticate to the Secrets Manager API:
 
 * [`apikey`](#option-1-external-secret-store-with-apikey-authentication): uses a Secrets Manager `hostid` and `apikey` to authenticate with Secrets Manager
 * [`jwt`](#option-2-external-secret-store-with-jwt-authentication): uses a JWT to authenticate with Secrets Manager
+* [`cert`](#option-3-external-secret-store-with-cert-authentication): uses a client certificate and private key to authenticate with Secrets Manager
 
 ### Option 1: External secret store with apiKey authentication
 
@@ -132,6 +133,58 @@ kubectl apply -n external-secrets -f conjur-secret-store.yaml
 # kubectl delete secretstore -n external-secrets conjur
 ```
 
+### Option 3: External secret store with cert authentication
+
+This method uses a client X.509 certificate and private key to authenticate with Secrets Manager via the `authn-cert` authenticator. The certificate must be issued by a CA that is trusted by the Secrets Manager `authn-cert` service. Conjur validates the presented certificate using constraints configured in the authenticator policy (such as `cn`, `san-uri`, `san-dns`, `san-ip` variables), and optionally per-host annotations that scope constraints to individual workloads.
+
+For more information on configuring the certificate authenticator, see the [Secrets Manager Certificate authenticator documentation](https://docs.cyberark.com/conjur-open-source/latest/en/content/operations/authn/authn-cert/authn-cert.htm?TocPath=Integrations%7CCertificate%20authenticator%7C_____0).
+
+#### Step 1: Define an external secret store
+
+When you use cert authentication, the following must be specified in the `SecretStore`:
+
+* `account` - The name of the Secrets Manager account
+* `serviceID` - The ID of the `authn-cert` `WebService` configured in Secrets Manager that is used to authenticate the client certificate
+* `clientCertRef` - Reference to a Kubernetes secret containing the PEM-encoded client certificate
+* `clientKeyRef` - Reference to a Kubernetes secret containing the PEM-encoded client private key
+* `hostId` (optional) - The Secrets Manager host identity to authenticate as. Required in the default `request` mode. Leave empty when using `spiffe` mode, where the host identity is derived from the SPIFFE SAN URI (`spiffe://<trust-domain>/<workload-id>`) in the certificate.
+
+```yaml
+{% include 'conjur-secret-store-cert.yaml' %}
+```
+
+#### Step 2: Create Kubernetes secrets for the client certificate and key
+
+To connect to the Secrets Manager server using certificate authentication, the **ESO Secrets Manager provider** needs to retrieve the client certificate and private key from K8s secrets.
+
+You can use either a TLS-typed secret or a generic secret. The following example uses a generic secret:
+
+```shell
+# This is all one line
+kubectl -n external-secrets create secret generic conjur-client-cert \
+  --from-file=tls.crt=/path/to/client.crt \
+  --from-file=tls.key=/path/to/client.key
+```
+
+!!! Note
+    `conjur-client-cert` is the `name` defined in the `clientCertRef` and `clientKeyRef` fields of the `conjur-secret-store.yaml` file. Both the certificate and the key must be PEM-encoded.
+
+#### Step 3: Create the external secrets store
+
+!!! Important
+    Unless you are using a [ClusterSecretStore](../api/clustersecretstore.md), credentials must reside in the same namespace as the SecretStore.
+
+```shell
+# WARNING: creates the store in the "external-secrets" namespace, update the value as needed
+#
+kubectl apply -n external-secrets -f conjur-secret-store.yaml
+
+# WARNING: running the delete command will delete the secret store configuration
+#
+# If there is a need to delete the external secretstore
+# kubectl delete secretstore -n external-secrets conjur
+```
+
 ## Define an external secret
 
 After you have configured the Secrets Manager provider secret store, you can fetch secrets from Secrets Manager.

+ 28 - 0
docs/snippets/conjur-secret-store-cert.yaml

@@ -0,0 +1,28 @@
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: conjur
+spec:
+  provider:
+    conjur:
+      # Service URL
+      url: https://myapi.conjur.org
+      # [OPTIONAL] base64 encoded string of certificate
+      caBundle: OPTIONALxFIELDxxxBase64xCertxString==
+      auth:
+        cert:
+          # conjur account
+          account: conjur
+          # The authn-cert service ID
+          serviceID: my-cert-auth-service
+          # [OPTIONAL] HostID for cert authentication (leave empty for 'spiffe' mode).
+          # hostId: vm-01
+          # Reference to a Kubernetes secret containing the PEM-encoded client certificate
+          clientCertRef:
+            name: conjur-client-cert
+            key: tls.crt
+          # Reference to a Kubernetes secret containing the PEM-encoded client key
+          clientKeyRef:
+            name: conjur-client-cert
+            key: tls.key
+

+ 1 - 1
e2e/Makefile

@@ -68,7 +68,7 @@ test.managed: e2e-image ## Run e2e tests against current kube context
 
 
 e2e-bin: install-ginkgo
-	   CGO_ENABLED=0 ginkgo build ./suites/...
+	   CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ginkgo build ./suites/...
 
 e2e-image: e2e-bin
 	-rm -rf ./k8s/deploy

+ 432 - 16
e2e/framework/addon/conjur.go

@@ -17,17 +17,27 @@ limitations under the License.
 package addon
 
 import (
+	"context"
 	"crypto/rand"
+	"crypto/rsa"
 	"crypto/x509"
+	"crypto/x509/pkix"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/pem"
 	"errors"
 	"fmt"
+	"math/big"
+	"net/url"
 	"path/filepath"
 	"strings"
+	"time"
 
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
 
 	// nolint
 
@@ -38,6 +48,31 @@ import (
 	"github.com/external-secrets/external-secrets-e2e/framework/util"
 )
 
+const (
+	// authenticatorServicePort is the port the authenticator-service binary listens on.
+	// This matches the CONJUR_AUTHENTICATOR_SERVICE_URL default of http://localhost:5681
+	// and must not collide with the conjur-oss container's own PORT (8080), since both
+	// containers share the pod's network namespace.
+	authenticatorServicePort = "5681"
+
+	// authenticatorServiceContainerName is the name of the sidecar container running
+	// the authenticator-service binary inside the conjur-oss pod.
+	authenticatorServiceContainerName = "authenticator-service"
+
+	// SpiffeTrustDomain is the SPIFFE trust domain used for the authn-cert authenticator
+	// operating in 'spiffe' host mode.
+	SpiffeTrustDomain = "eso-tests.example.org"
+
+	// SpiffeWorkloadID is the SPIFFE ID's workload path, embedded as a URI SAN in the
+	// SPIFFE client certificate (spiffe://<trust-domain>/<SpiffeWorkloadID>).
+	SpiffeWorkloadID = "vm-spiffe"
+
+	// SpiffeIdentityPath is the Conjur policy path that the authn-cert authenticator
+	// prepends to the SPIFFE workload path to derive the full host identity. The resulting
+	// host must exist at policy path SpiffeIdentityPath/SpiffeWorkloadID.
+	SpiffeIdentityPath = "eso-tests/spiffe"
+)
+
 type Conjur struct {
 	chart        *HelmChart
 	dataKey      string
@@ -48,6 +83,10 @@ type Conjur struct {
 
 	AdminApiKey    string
 	ConjurServerCA []byte
+	ClientCert     []byte
+	ClientKey      []byte
+	SpiffeCert     []byte
+	SpiffeKey      []byte
 	portForwarder  *PortForward
 }
 
@@ -60,14 +99,54 @@ func NewConjur() *Conjur {
 		Fail(err.Error())
 	}
 
+	// Generate client certificates for cert-based authentication.
+	// The CN "vm-01" matches the host identity in the cert host policy.
+	rootBlock, _ := pem.Decode(rootPem)
+	if rootBlock == nil {
+		Fail("unable to decode root cert PEM")
+	}
+	rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
+	if err != nil {
+		Fail(fmt.Sprintf("unable to parse root cert: %v", err))
+	}
+	rootKeyBlock, _ := pem.Decode(rootKeyPEM)
+	if rootKeyBlock == nil {
+		Fail("unable to decode root key PEM")
+	}
+	rootKey, err := x509.ParsePKCS1PrivateKey(rootKeyBlock.Bytes)
+	if err != nil {
+		Fail(fmt.Sprintf("unable to parse root key: %v", err))
+	}
+	clientCertPem, clientKeyRSA, err := genClientAuthCert(rootCert, rootKey, "vm-01", "")
+	if err != nil {
+		Fail(fmt.Sprintf("unable to generate client cert: %v", err))
+	}
+	clientKeyPem := pem.EncodeToMemory(&pem.Block{
+		Type:  privatePemType,
+		Bytes: x509.MarshalPKCS1PrivateKey(clientKeyRSA),
+	})
+
+	// Generate a second client certificate carrying a SPIFFE URI SAN, used by the
+	// authn-cert authenticator's 'spiffe' host mode, where the authenticated host's
+	// identity is derived from the certificate itself rather than the request path.
+	spiffeCertPem, spiffeKeyRSA, err := genClientAuthCert(rootCert, rootKey, "",
+		fmt.Sprintf("spiffe://%s/%s", SpiffeTrustDomain, SpiffeWorkloadID))
+	if err != nil {
+		Fail(fmt.Sprintf("unable to generate spiffe client cert: %v", err))
+	}
+	spiffeKeyPem := pem.EncodeToMemory(&pem.Block{
+		Type:  privatePemType,
+		Bytes: x509.MarshalPKCS1PrivateKey(spiffeKeyRSA),
+	})
+
 	return &Conjur{
 		dataKey: dataKey,
 		chart: &HelmChart{
 			Namespace:   "conjur",
 			ReleaseName: "conjur-conjur",
 			Chart:       fmt.Sprintf("%s/conjur-oss", repo),
-			// Use latest version of Conjur OSS. To pin to a specific version, uncomment the following line.
-			// ChartVersion: "2.0.7",
+			// Pinned to the current latest conjur-oss chart release. Bump alongside conjurReleaseVersion.
+			ChartVersion: "2.1.1",
 			Repo: ChartRepo{
 				Name: repo,
 				URL:  "https://cyberark.github.io/helm-charts",
@@ -87,27 +166,102 @@ func NewConjur() *Conjur {
 				},
 			},
 		},
-		Namespace: "conjur",
+		Namespace:  "conjur",
+		ClientCert: clientCertPem,
+		ClientKey:  clientKeyPem,
+		SpiffeCert: spiffeCertPem,
+		SpiffeKey:  spiffeKeyPem,
 	}
 }
 
+// Install sets up the conjur-oss chart, the authenticator-service sidecar, and configures
+// the authenticators. The install-then-patch-then-exec sequence spans several kind/kubelet
+// eventually-consistent steps (Deployment rollout, pod scheduling, kubelet exec/attach
+// readiness); under load these can intermittently race even with generous waits at each
+// step (e.g. a pod reported Ready via the API can still be replaced by a subsequent
+// reconcile before it is actually exec-attachable). Rather than chase every individual
+// timing window, retry the whole sequence a few times, tearing down between attempts.
 func (l *Conjur) Install() error {
-	err := l.chart.Install()
-	if err != nil {
+	const maxAttempts = 3
+	var lastErr error
+	for attempt := 1; attempt <= maxAttempts; attempt++ {
+		if attempt > 1 {
+			By(fmt.Sprintf("retrying conjur install (attempt %d/%d) after: %v", attempt, maxAttempts, lastErr))
+			if err := l.teardown(); err != nil {
+				return fmt.Errorf("unable to tear down conjur before retry: %w", err)
+			}
+		}
+
+		if lastErr = l.installOnce(); lastErr == nil {
+			return nil
+		}
+	}
+	return fmt.Errorf("conjur install failed after %d attempts: %w", maxAttempts, lastErr)
+}
+
+func (l *Conjur) installOnce() error {
+	if err := l.chart.Install(); err != nil {
 		return err
 	}
 
-	err = l.initConjur()
-	if err != nil {
+	if err := l.patchAuthenticatorServiceSidecar(); err != nil {
 		return err
 	}
 
-	err = l.configureConjur()
-	if err != nil {
+	if err := l.initConjur(); err != nil {
 		return err
 	}
 
-	return nil
+	return l.configureConjur()
+}
+
+// teardown removes the conjur-oss release and namespace so installOnce can run again from
+// a clean slate. Errors are ignored: the namespace may not exist yet if chart.Install()
+// itself failed on a previous attempt.
+func (l *Conjur) teardown() error {
+	if l.portForwarder != nil {
+		l.portForwarder.Close()
+		l.portForwarder = nil
+	}
+	_ = l.chart.Uninstall()
+	err := l.chart.config.KubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), l.chart.Namespace, metav1.DeleteOptions{})
+	if err != nil {
+		return nil //nolint:nilerr // namespace may already be gone; nothing to clean up.
+	}
+	return util.WaitForKubeNamespaceNotExist(l.chart.Namespace, l.chart.config.KubeClientSet)
+}
+
+// retryExecCmd retries util.ExecCmdWithContainer briefly to absorb the window where a
+// freshly-created pod reports Ready via the API before the kubelet's exec/attach endpoint
+// for its containers is actually available.
+func retryExecCmd(client kubernetes.Interface, config *rest.Config, podName, containerName, namespace, command string) error {
+	var lastErr error
+	for i := 0; i < 10; i++ {
+		if i > 0 {
+			time.Sleep(1 * time.Second)
+		}
+		if _, lastErr = util.ExecCmdWithContainer(client, config, podName, containerName, namespace, command); lastErr == nil {
+			return nil
+		}
+	}
+	return lastErr
+}
+
+// waitForConjurServiceReady polls the Conjur Service URL from inside the cluster (via exec
+// into the conjur-oss pod) until it gets a real HTTP response, absorbing kube-proxy
+// Endpoints-sync lag after the sidecar rollout. curl's own TLS verification is disabled
+// (-k) because this only checks that the Service routes to a live, responsive nginx; actual
+// certificate validation is exercised by the real authentication flows later.
+func (l *Conjur) waitForConjurServiceReady() error {
+	cmd := fmt.Sprintf("curl -sk -o /dev/null -w '%%{http_code}' %s/status", l.ConjurURL)
+	return wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
+		out, err := util.ExecCmdWithContainer(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
+			l.PodName, "conjur-oss", l.Namespace, cmd)
+		if err != nil {
+			return false, nil
+		}
+		return strings.Contains(out, "200"), nil
+	})
 }
 
 func (l *Conjur) initConjur() error {
@@ -128,12 +282,12 @@ func (l *Conjur) initConjur() error {
 	}
 	l.ConjurServerCA = caCertSecret.Data["tls.crt"]
 
-	// Create "default" account
-	_, err = util.ExecCmdWithContainer(
-		l.chart.config.KubeClientSet,
-		l.chart.config.KubeConfig,
-		l.PodName, "conjur-oss", l.Namespace, "conjurctl account create default")
-	if err != nil {
+	// Create "default" account. A freshly-created pod can report Ready in the API before
+	// the kubelet's exec/attach endpoint for its containers is actually wired up, so the
+	// first exec attempt against it can fail with "container not found" even though the
+	// container is running; retry briefly to absorb that race.
+	if err := retryExecCmd(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
+		l.PodName, "conjur-oss", l.Namespace, "conjurctl account create default"); err != nil {
 		return fmt.Errorf("error initializing conjur: %w", err)
 	}
 
@@ -162,6 +316,18 @@ func (l *Conjur) initConjur() error {
 	}
 
 	l.ConjurURL = fmt.Sprintf("https://conjur-conjur-conjur-oss.%s.svc.cluster.local", l.Namespace)
+
+	// The ExternalSecret controller reaches Conjur through this Service URL, which routes
+	// through kube-proxy to whichever pod is currently in the Service's Endpoints. That
+	// Endpoints list is reconciled asynchronously and can lag behind the pod-level readiness
+	// checks in patchAuthenticatorServiceSidecar's rollout wait, especially right after the
+	// sidecar-triggered rolling update. Probe through the Service URL itself (from inside the
+	// cluster, exercising the same DNS/ClusterIP/kube-proxy path the controller will use)
+	// before proceeding, so tests don't race a stale or not-yet-synced endpoint.
+	if err := l.waitForConjurServiceReady(); err != nil {
+		return fmt.Errorf("error waiting for conjur service to be reachable: %w", err)
+	}
+
 	cfg := conjurapi.Config{
 		Account:      "default",
 		ApplianceURL: fmt.Sprintf("https://localhost:%d", l.portForwarder.localPort),
@@ -179,6 +345,164 @@ func (l *Conjur) initConjur() error {
 	return nil
 }
 
+// conjurReleaseVersion is the pinned cyberark/conjur GitHub release used to fetch the
+// authenticator-service binary. Bump this when a newer release is needed.
+const conjurReleaseVersion = "v1.27.0"
+
+// authenticatorServiceInitScript downloads the authenticator-service binary matching the
+// node's architecture from the pinned cyberark/conjur GitHub release (see
+// conjurReleaseVersion), and writes its minimal config file, into a shared emptyDir volume
+// mounted by the sidecar container.
+//
+// The authenticator-service is not distributed as a container image, only as raw
+// per-architecture binaries attached to GitHub releases (see AUTHENTICATOR_SERVICE.md in
+// the cyberark/conjur repo), so it must be fetched at runtime rather than referenced by tag.
+const authenticatorServiceInitScript = `set -e
+ARCH=$(uname -m)
+case "$ARCH" in
+  x86_64) BIN_ARCH=amd64 ;;
+  aarch64|arm64) BIN_ARCH=arm64 ;;
+  *) echo "unsupported architecture: $ARCH" >&2; exit 1 ;;
+esac
+DOWNLOAD_URL=$(curl -s https://api.github.com/repos/cyberark/conjur/releases/tags/` + conjurReleaseVersion + `\
+  | grep -o "\"browser_download_url\": *\"[^\"]*authenticator_linux_[0-9]*_${BIN_ARCH}\"" \
+  | head -1 | sed -E 's/.*"(https:[^"]+)".*/\1/')
+if [ -z "$DOWNLOAD_URL" ]; then
+  echo "unable to determine authenticator-service download URL for arch ${BIN_ARCH}" >&2
+  exit 1
+fi
+echo "downloading authenticator-service from ${DOWNLOAD_URL}"
+curl -sL -o /authsvc/authenticator-service "$DOWNLOAD_URL"
+chmod +x /authsvc/authenticator-service
+printf '{"port": "%s", "http_timeout": "10s"}' "` + authenticatorServicePort + `" > /authsvc/config.json`
+
+// patchAuthenticatorServiceSidecar adds an authenticator-service sidecar to the conjur-oss
+// deployment and enables the feature flag that lets Conjur delegate authn-cert credential
+// validation to it. Conjur OSS's authn-cert authenticator does not validate client
+// certificates itself; it calls out to this separate HTTP service (see
+// AUTHENTICATOR_SERVICE.md and CERTIFICATE_AUTH.md in the cyberark/conjur repo). The
+// conjur-oss Helm chart has no hook for adding sidecars, so the running Deployment is
+// patched directly after the initial chart install.
+func (l *Conjur) patchAuthenticatorServiceSidecar() error {
+	By("patching conjur-oss deployment with authenticator-service sidecar")
+	clientSet := l.chart.config.KubeClientSet
+	deploymentName := fmt.Sprintf("%s-conjur-oss", l.chart.ReleaseName)
+
+	deployment, err := clientSet.AppsV1().Deployments(l.Namespace).Get(GinkgoT().Context(), deploymentName, metav1.GetOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to get conjur-oss deployment: %w", err)
+	}
+
+	binVolume := corev1.Volume{
+		Name:         "authsvc-bin",
+		VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
+	}
+	binVolumeMount := corev1.VolumeMount{
+		Name:      binVolume.Name,
+		MountPath: "/authsvc",
+	}
+
+	deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, binVolume)
+	deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, corev1.Container{
+		Name:         "authenticator-service-fetch",
+		Image:        "curlimages/curl:8.11.0",
+		Command:      []string{"/bin/sh", "-c"},
+		Args:         []string{authenticatorServiceInitScript},
+		VolumeMounts: []corev1.VolumeMount{binVolumeMount},
+	})
+	deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, corev1.Container{
+		Name:         authenticatorServiceContainerName,
+		Image:        "gcr.io/distroless/static-debian12:latest",
+		Command:      []string{"/authsvc/authenticator-service"},
+		Args:         []string{"-c", "/authsvc/config.json"},
+		VolumeMounts: []corev1.VolumeMount{binVolumeMount},
+		Ports: []corev1.ContainerPort{
+			{Name: "authsvc", ContainerPort: 5681},
+		},
+	})
+
+	for i := range deployment.Spec.Template.Spec.Containers {
+		c := &deployment.Spec.Template.Spec.Containers[i]
+		if c.Name != "conjur-oss" {
+			continue
+		}
+		c.Env = append(c.Env,
+			corev1.EnvVar{Name: "CONJUR_FEATURE_AUTHENTICATOR_SERVICE_ENABLED", Value: "true"},
+			corev1.EnvVar{Name: "CONJUR_AUTHENTICATOR_SERVICE_URL", Value: "http://localhost:" + authenticatorServicePort},
+		)
+	}
+
+	updated, err := clientSet.AppsV1().Deployments(l.Namespace).Update(GinkgoT().Context(), deployment, metav1.UpdateOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to patch conjur-oss deployment with authenticator-service sidecar: %w", err)
+	}
+
+	// Wait for the rollout to fully complete (old ReplicaSet scaled to 0) rather than just
+	// waiting for any pod matching the label to be ready. During the rolling update, the
+	// old (pre-sidecar) pod can remain Ready while terminating; a label-only wait can race
+	// and hand initConjur a pod whose conjur-oss container is already gone.
+	if err := waitForDeploymentRollout(clientSet, l.Namespace, deploymentName, updated.Generation); err != nil {
+		return fmt.Errorf("error waiting for conjur-oss rollout after sidecar patch: %w", err)
+	}
+
+	return nil
+}
+
+func waitForDeploymentRollout(clientSet kubernetes.Interface, namespace, name string, generation int64) error {
+	if err := wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
+		dep, err := clientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return false, nil
+		}
+		if dep.Status.ObservedGeneration < generation {
+			return false, nil
+		}
+		replicas := int32(1)
+		if dep.Spec.Replicas != nil {
+			replicas = *dep.Spec.Replicas
+		}
+		return dep.Status.UpdatedReplicas == replicas &&
+			dep.Status.Replicas == replicas &&
+			dep.Status.AvailableReplicas == replicas, nil
+	}); err != nil {
+		return err
+	}
+
+	// Deployment status is computed asynchronously by the deployment controller and can
+	// briefly report AvailableReplicas==1 while the old pre-sidecar pod is still
+	// Running-but-terminating (Kubelet has not removed it yet). Cross-check pod state
+	// directly: require exactly one pod for this Deployment that is not terminating and
+	// has all 3 containers (conjur-oss, nginx, authenticator-service) ready, so
+	// initConjur never execs into a pod that's mid-teardown.
+	return wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
+		pods, err := clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: "app=conjur-oss"})
+		if err != nil {
+			return false, nil
+		}
+		live := 0
+		for i := range pods.Items {
+			pod := &pods.Items[i]
+			if pod.DeletionTimestamp != nil {
+				continue
+			}
+			if pod.Status.Phase != corev1.PodRunning {
+				continue
+			}
+			if len(pod.Status.ContainerStatuses) != 3 {
+				continue
+			}
+			allReady := true
+			for _, cs := range pod.Status.ContainerStatuses {
+				allReady = allReady && cs.Ready
+			}
+			if allReady {
+				live++
+			}
+		}
+		return live == 1, nil
+	})
+}
+
 func (l *Conjur) configureConjur() error {
 	By("configuring conjur")
 	// Construct Conjur policy for authn-jwt. This uses the token-app-property "sub" to
@@ -215,6 +539,54 @@ func (l *Conjur) configureConjur() error {
 		return fmt.Errorf("unable to load authn-jwt policy: %w", err)
 	}
 
+	// Construct Conjur policy for two authn-cert authenticators. Both delegate client
+	// certificate validation to the authenticator-service sidecar (see
+	// patchAuthenticatorServiceSidecar). The 'ca-cert' variable name is fixed by the
+	// authenticator; it is not a policy-defined path segment.
+	//
+	// 'eso-tests' operates in 'spiffe' host mode: the authenticated host's identity is
+	// derived from the SPIFFE URI SAN in the client certificate (see genSpiffeCert), rather
+	// than from a role identifier in the request path. This is the mode exercised when the
+	// ExternalSecret store omits Auth.Cert.HostID.
+	//
+	// 'eso-tests-hostid' operates in the default 'request' host mode, mirroring the
+	// authn-jwt/eso-tests-hostid authenticator: the host identity is supplied explicitly via
+	// Auth.Cert.HostID. A single authenticator cannot serve both modes because host-mode is
+	// a per-authenticator setting, not a per-request one.
+	policy = `- !policy
+  id: conjur/authn-cert/eso-tests
+  body:
+    - !webservice
+    - !variable ca-cert
+    - !variable host-mode
+    - !variable trust-domain
+    - !variable identity-path
+
+- !policy
+  id: conjur/authn-cert/eso-tests-hostid
+  body:
+    - !webservice
+    - !variable ca-cert`
+
+	_, err = l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
+	if err != nil {
+		return fmt.Errorf("unable to load authn-cert policy: %w", err)
+	}
+
+	// Set the CA certificate and spiffe-mode variables for the authn-cert authenticators
+	certSecrets := map[string]string{
+		"conjur/authn-cert/eso-tests/ca-cert":        string(l.ConjurServerCA),
+		"conjur/authn-cert/eso-tests/host-mode":      "spiffe",
+		"conjur/authn-cert/eso-tests/trust-domain":   SpiffeTrustDomain,
+		"conjur/authn-cert/eso-tests/identity-path":  SpiffeIdentityPath,
+		"conjur/authn-cert/eso-tests-hostid/ca-cert": string(l.ConjurServerCA),
+	}
+	for secretPath, secretValue := range certSecrets {
+		if err := l.ConjurClient.AddSecret(secretPath, secretValue); err != nil {
+			return fmt.Errorf("unable to add secret %s: %w", secretPath, err)
+		}
+	}
+
 	// Fetch the jwks info from the k8s cluster
 	pubKeysJson, issuer, err := l.fetchJWKSandIssuer()
 	if err != nil {
@@ -303,6 +675,50 @@ func genCertificates(namespace, serviceName string) ([]byte, []byte, []byte, []b
 	return rootPem, rootKeyPEM, serverPem, serverKeyPem, err
 }
 
+// genSpiffeCert generates a client certificate carrying the given SPIFFE ID as a URI SAN,
+// signed by signingCert/signingKey. This is used by the authn-cert authenticator's 'spiffe'
+// host mode, which derives the authenticated host's identity from the certificate's URI SAN
+// rather than from a role identifier in the request path.
+// genClientAuthCert generates a client certificate for mutual TLS (authn-cert). cn sets the
+// Common Name (used by 'request' host mode's cn restriction annotation); spiffeID, if
+// non-empty, is embedded as a URI SAN (used by 'spiffe' host mode).
+func genClientAuthCert(signingCert *x509.Certificate, signingKey *rsa.PrivateKey, cn, spiffeID string) ([]byte, *rsa.PrivateKey, error) {
+	var uris []*url.URL
+	if spiffeID != "" {
+		uri, err := url.Parse(spiffeID)
+		if err != nil {
+			return nil, nil, fmt.Errorf("unable to parse spiffe id: %w", err)
+		}
+		uris = []*url.URL{uri}
+	}
+
+	pkey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return nil, nil, err
+	}
+	tpl := x509.Certificate{
+		Subject: pkix.Name{
+			Country:      []string{"/dev/null"},
+			Organization: []string{"External Secrets ACME"},
+			CommonName:   cn,
+		},
+		SerialNumber: big.NewInt(1),
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().Add(time.Hour),
+		// DigitalSignature is required for the client to sign the TLS handshake during
+		// mutual TLS; CRLSign (used by the sibling genPeerCert for server certs) does not
+		// cover that purpose and nginx's strict (critical) key usage check rejects the
+		// handshake outright if it's missing.
+		KeyUsage:       x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:    []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+		IsCA:           false,
+		MaxPathLenZero: true,
+		URIs:           uris,
+	}
+	_, certPEM, err := genCert(&tpl, signingCert, &pkey.PublicKey, signingKey)
+	return certPEM, pkey, err
+}
+
 func (l *Conjur) Logs() error {
 	return l.chart.Logs()
 }

+ 1 - 1
e2e/go.mod

@@ -52,7 +52,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.30.13
 	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.9
 	github.com/aws/aws-sdk-go-v2/service/ssm v1.66.3
-	github.com/cyberark/conjur-api-go v0.13.8
+	github.com/cyberark/conjur-api-go v0.14.1
 	github.com/external-secrets/external-secrets/apis v0.0.0
 	github.com/external-secrets/external-secrets/providers/v1/azure v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/gcp v0.0.0-00010101000000-000000000000

+ 2 - 2
e2e/go.sum

@@ -129,8 +129,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
 github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
-github.com/cyberark/conjur-api-go v0.13.8 h1:woBEg+lvcoghpFkoGcS2JfPy+kJR59Ir7LeZcNA9wWE=
-github.com/cyberark/conjur-api-go v0.13.8/go.mod h1:xGi4RCulvsc+x/jYRrxUoEShznhlKP/4hJC/4+lueFg=
+github.com/cyberark/conjur-api-go v0.14.1 h1:6rF5smYVJ5wV/SpnJkVGQSYfYFNj4ltaWQXsfL1K2+c=
+github.com/cyberark/conjur-api-go v0.14.1/go.mod h1:POjKgJwnniONTnWySx6Yq87hYsdPD6Ohwm7x1UGio7E=
 github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
 github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

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

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

+ 34 - 0
e2e/suites/provider/cases/conjur/conjur.go

@@ -28,6 +28,8 @@ const (
 	withTokenAuth    = "with apikey auth"
 	withJWTK8s       = "with jwt k8s provider"
 	withJWTK8sHostID = "with jwt k8s hostid provider"
+	withCert         = "with cert provider"
+	withCertHostID   = "with cert hostid provider"
 )
 
 var _ = Describe("[conjur]", Label("conjur"), Ordered, func() {
@@ -78,6 +80,24 @@ var _ = Describe("[conjur]", Label("conjur"), Ordered, func() {
 		framework.Compose(withJWTK8sHostID, f, common.SyncWithoutTargetName, useJWTK8sHostIDProvider(prov)),
 		framework.Compose(withJWTK8sHostID, f, common.JSONDataFromSync, useJWTK8sHostIDProvider(prov)),
 		framework.Compose(withJWTK8sHostID, f, common.JSONDataFromRewrite, useJWTK8sHostIDProvider(prov)),
+
+		// use cert provider
+		framework.Compose(withCert, f, common.FindByName, useCertProvider(prov)),
+		framework.Compose(withCert, f, common.FindByNameAndRewrite, useCertProvider(prov)),
+		framework.Compose(withCert, f, common.FindByTag, useCertProvider(prov)),
+		framework.Compose(withCert, f, common.SimpleDataSync, useCertProvider(prov)),
+		framework.Compose(withCert, f, common.SyncWithoutTargetName, useCertProvider(prov)),
+		framework.Compose(withCert, f, common.JSONDataFromSync, useCertProvider(prov)),
+		framework.Compose(withCert, f, common.JSONDataFromRewrite, useCertProvider(prov)),
+
+		// use cert hostid provider
+		framework.Compose(withCertHostID, f, common.FindByName, useCertHostIDProvider(prov)),
+		framework.Compose(withCertHostID, f, common.FindByNameAndRewrite, useCertHostIDProvider(prov)),
+		framework.Compose(withCertHostID, f, common.FindByTag, useCertHostIDProvider(prov)),
+		framework.Compose(withCertHostID, f, common.SimpleDataSync, useCertHostIDProvider(prov)),
+		framework.Compose(withCertHostID, f, common.SyncWithoutTargetName, useCertHostIDProvider(prov)),
+		framework.Compose(withCertHostID, f, common.JSONDataFromSync, useCertHostIDProvider(prov)),
+		framework.Compose(withCertHostID, f, common.JSONDataFromRewrite, useCertHostIDProvider(prov)),
 	)
 })
 
@@ -101,3 +121,17 @@ func useJWTK8sHostIDProvider(prov *conjurProvider) func(tc *framework.TestCase)
 		tc.ExternalSecret.Spec.SecretStoreRef.Name = jwtK8sHostIDProviderName
 	}
 }
+
+func useCertProvider(prov *conjurProvider) func(tc *framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		prov.CreateCertStore()
+		tc.ExternalSecret.Spec.SecretStoreRef.Name = certProviderName
+	}
+}
+
+func useCertHostIDProvider(prov *conjurProvider) func(tc *framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		prov.CreateCertHostIDStore()
+		tc.ExternalSecret.Spec.SecretStoreRef.Name = certHostIDProviderName
+	}
+}

+ 60 - 3
e2e/suites/provider/cases/conjur/policy.go

@@ -17,7 +17,10 @@ package conjur
 
 import (
 	"bytes"
+	"fmt"
 	"text/template"
+
+	"github.com/external-secrets/external-secrets-e2e/framework/addon"
 )
 
 const createVariablePolicyTemplate = `- !variable
@@ -37,6 +40,16 @@ const createVariablePolicyTemplate = `- !variable
 - !permit
   role: !host system:serviceaccount:{{ .Namespace }}:test-app-hostid-sa
   privilege: [ read, execute ]
+  resource: !variable {{ .Key }}
+
+- !permit
+  role: !host vm-01
+  privilege: [ read, execute ]
+  resource: !variable {{ .Key }}
+
+- !permit
+  role: !host {{ .SpiffeHostID }}
+  privilege: [ read, execute ]
   resource: !variable {{ .Key }}`
 
 const deleteVariablePolicyTemplate = `- !delete
@@ -52,11 +65,41 @@ const jwtHostPolicyTemplate = `- !host
   privilege: [ read, authenticate ]
   resource: !webservice conjur/authn-jwt/{{ .ServiceID }}`
 
+// certHostIDPolicyTemplate creates the host authenticated by CreateCertHostIDStore, which
+// supplies an explicit HostID in the authentication request (request mode). Request mode
+// requires at least one certificate-matching restriction annotation; 'cn' is used here to
+// match the client certificate's Common Name.
+const certHostIDPolicyTemplate = `- !host
+  id: vm-01
+  annotations:
+    authn-cert/{{ .ServiceID }}/cn: "vm-01"
+
+- !permit
+  role: !host vm-01
+  privilege: [ read, authenticate ]
+  resource: !webservice conjur/authn-cert/{{ .ServiceID }}`
+
+// certSpiffePolicyTemplate creates the host authenticated by CreateCertStore, which omits
+// HostID so the authn-cert authenticator derives the role from the SPIFFE URI SAN in the
+// client certificate (spiffe mode). The host must exist at IdentityPath/WorkloadID to match
+// the identity the authenticator derives from the certificate; no restriction annotation is
+// required or checked in spiffe mode.
+const certSpiffePolicyTemplate = `- !policy
+  id: {{ .IdentityPath }}
+  body:
+    - !host {{ .WorkloadID }}
+
+- !permit
+  role: !host {{ .IdentityPath }}/{{ .WorkloadID }}
+  privilege: [ read, authenticate ]
+  resource: !webservice conjur/authn-cert/{{ .ServiceID }}`
+
 func createVariablePolicy(key, namespace string, tags map[string]string) string {
 	return renderTemplate(createVariablePolicyTemplate, map[string]interface{}{
-		"Key":       key,
-		"Namespace": namespace,
-		"Tags":      tags,
+		"Key":          key,
+		"Namespace":    namespace,
+		"Tags":         tags,
+		"SpiffeHostID": fmt.Sprintf("%s/%s", addon.SpiffeIdentityPath, addon.SpiffeWorkloadID),
 	})
 }
 
@@ -73,6 +116,20 @@ func createJwtHostPolicy(hostID, serviceID string) string {
 	})
 }
 
+func createCertHostIDPolicy(serviceID string) string {
+	return renderTemplate(certHostIDPolicyTemplate, map[string]interface{}{
+		"ServiceID": serviceID,
+	})
+}
+
+func createCertSpiffePolicy(serviceID, identityPath, workloadID string) string {
+	return renderTemplate(certSpiffePolicyTemplate, map[string]interface{}{
+		"ServiceID":    serviceID,
+		"IdentityPath": identityPath,
+		"WorkloadID":   workloadID,
+	})
+}
+
 func renderTemplate(templateText string, data map[string]interface{}) string {
 	// Use golang templates to render the policy
 	tmpl, err := template.New("policy").Parse(templateText)

+ 89 - 0
e2e/suites/provider/cases/conjur/provider.go

@@ -44,6 +44,10 @@ const (
 	secretName               = "conjur-creds"
 	jwtK8sProviderName       = "jwt-k8s-provider"
 	jwtK8sHostIDProviderName = "jwt-k8s-hostid-provider"
+	certProviderName         = "cert-provider"
+	certHostIDProviderName   = "cert-hostid-provider"
+	certSecretName           = "conjur-client-cert"
+	certHostIDSecretName     = "conjur-client-cert-hostid"
 	hostidServiceAccountName = "test-app-hostid-sa"
 	appServiceAccountName    = "test-app-sa"
 )
@@ -88,7 +92,18 @@ func (s *conjurProvider) BeforeEach() {
 	// setup policy
 	saName = "system:serviceaccount:" + s.framework.Namespace.Name + ":" + hostidServiceAccountName
 	policy = createJwtHostPolicy(saName, "eso-tests-hostid")
+	_, err = s.addon.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
+	Expect(err).ToNot(HaveOccurred())
+
+	// setup policy for the authn-cert authenticator's 'request' host mode
+	// (used by CreateCertHostIDStore, which supplies an explicit HostID)
+	policy = createCertHostIDPolicy("eso-tests-hostid")
+	_, err = s.addon.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
+	Expect(err).ToNot(HaveOccurred())
 
+	// setup policy for the authn-cert authenticator's 'spiffe' host mode
+	// (used by CreateCertStore, which omits HostID)
+	policy = createCertSpiffePolicy("eso-tests", addon.SpiffeIdentityPath, addon.SpiffeWorkloadID)
 	_, err = s.addon.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
 	Expect(err).ToNot(HaveOccurred())
 }
@@ -204,3 +219,77 @@ func (s conjurProvider) CreateJWTK8sHostIDStore() {
 	err = s.framework.CRClient.Create(GinkgoT().Context(), secretStore)
 	Expect(err).ToNot(HaveOccurred())
 }
+
+func (s conjurProvider) CreateCertStore() {
+	// This store omits HostID, so the authn-cert authenticator operates in 'spiffe' host
+	// mode and derives the authenticated host's identity from the SPIFFE URI SAN in the
+	// certificate rather than from a role identifier supplied by the client.
+	By("creating a conjur client cert secret")
+	certSecret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      certSecretName,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Data: map[string][]byte{
+			"clientCert": s.addon.SpiffeCert,
+			"clientKey":  s.addon.SpiffeKey,
+		},
+	}
+	err := s.framework.CRClient.Create(GinkgoT().Context(), certSecret)
+	Expect(err).ToNot(HaveOccurred())
+
+	By("creating a secret store for conjur with cert auth")
+	secretStore := makeStore(certProviderName, s.framework.Namespace.Name, s.addon)
+	secretStore.Spec.Provider.Conjur.Auth = esv1.ConjurAuth{
+		Cert: &esv1.ConjurCert{
+			Account:   "default",
+			ServiceID: "eso-tests",
+			ClientCertRef: &esmeta.SecretKeySelector{
+				Name: certSecretName,
+				Key:  "clientCert",
+			},
+			ClientKeyRef: &esmeta.SecretKeySelector{
+				Name: certSecretName,
+				Key:  "clientKey",
+			},
+		},
+	}
+	err = s.framework.CRClient.Create(GinkgoT().Context(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s conjurProvider) CreateCertHostIDStore() {
+	By("creating a conjur client cert secret for hostid")
+	certSecret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      certHostIDSecretName,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Data: map[string][]byte{
+			"clientCert": s.addon.ClientCert,
+			"clientKey":  s.addon.ClientKey,
+		},
+	}
+	err := s.framework.CRClient.Create(GinkgoT().Context(), certSecret)
+	Expect(err).ToNot(HaveOccurred())
+
+	By("creating a secret store for conjur with cert auth and host ID")
+	secretStore := makeStore(certHostIDProviderName, s.framework.Namespace.Name, s.addon)
+	secretStore.Spec.Provider.Conjur.Auth = esv1.ConjurAuth{
+		Cert: &esv1.ConjurCert{
+			Account:   "default",
+			HostID:    "vm-01",
+			ServiceID: "eso-tests-hostid",
+			ClientCertRef: &esmeta.SecretKeySelector{
+				Name: certHostIDSecretName,
+				Key:  "clientCert",
+			},
+			ClientKeyRef: &esmeta.SecretKeySelector{
+				Name: certHostIDSecretName,
+				Key:  "clientKey",
+			},
+		},
+	}
+	err = s.framework.CRClient.Create(GinkgoT().Context(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}

+ 1 - 1
go.mod

@@ -238,7 +238,7 @@ require (
 	github.com/cloudflare/circl v1.6.3 // indirect
 	github.com/cloudru-tech/iam-sdk v1.0.4 // indirect
 	github.com/cloudru-tech/secret-manager-sdk v1.1.1 // indirect
-	github.com/cyberark/conjur-api-go v0.13.8 // indirect
+	github.com/cyberark/conjur-api-go v0.14.1 // indirect
 	github.com/danieljoos/wincred v1.2.3 // indirect
 	github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect
 	github.com/extism/go-sdk v1.7.1 // indirect

+ 2 - 2
go.sum

@@ -289,8 +289,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/ctdk/goiardi v0.11.10 h1:IB/3Afl1pC2Q4KGwzmhHPAoJfe8VtU51wZ2V0QkvsL0=
 github.com/ctdk/goiardi v0.11.10/go.mod h1:Pr6Cj6Wsahw45myttaOEZeZ0LE7p1qzWmzgsBISkrNI=
-github.com/cyberark/conjur-api-go v0.13.8 h1:woBEg+lvcoghpFkoGcS2JfPy+kJR59Ir7LeZcNA9wWE=
-github.com/cyberark/conjur-api-go v0.13.8/go.mod h1:xGi4RCulvsc+x/jYRrxUoEShznhlKP/4hJC/4+lueFg=
+github.com/cyberark/conjur-api-go v0.14.1 h1:6rF5smYVJ5wV/SpnJkVGQSYfYFNj4ltaWQXsfL1K2+c=
+github.com/cyberark/conjur-api-go v0.14.1/go.mod h1:POjKgJwnniONTnWySx6Yq87hYsdPD6Ohwm7x1UGio7E=
 github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
 github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

+ 42 - 0
providers/v1/conjur/client.go

@@ -37,6 +37,8 @@ var (
 	errConjurClient          = "cannot setup new Conjur client: %w"
 	errBadServiceUser        = "could not get Auth.Apikey.UserRef: %w"
 	errBadServiceAPIKey      = "could not get Auth.Apikey.ApiKeyRef: %w"
+	errBadClientCert         = "could not get Auth.Cert.ClientCertRef: %w"
+	errBadClientKey          = "could not get Auth.Cert.ClientKeyRef: %w"
 	errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
 	errSecretKeyFmt          = "cannot find secret data for key: %q"
 )
@@ -91,6 +93,10 @@ func (c *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
 	if prov.Auth.Jwt != nil {
 		return c.conjurClientFromJWT(ctx, config, prov)
 	}
+	if prov.Auth.Cert != nil {
+		return c.conjurClientFromCert(ctx, config, prov)
+	}
+
 	// Should not happen because validate func should catch this
 	return nil, errors.New("no authentication method provided")
 }
@@ -176,3 +182,39 @@ func (c *Client) conjurClientFromJWT(ctx context.Context, config conjurapi.Confi
 	c.client = conjur
 	return conjur, nil
 }
+
+func (c *Client) conjurClientFromCert(ctx context.Context, config conjurapi.Config, prov *esv1.ConjurProvider) (SecretsClient, error) {
+	config.AuthnType = "cert"
+	config.Account = prov.Auth.Cert.Account
+	config.ServiceID = prov.Auth.Cert.ServiceID
+	config.CertHostID = prov.Auth.Cert.HostID
+
+	clientCert, secErr := resolvers.SecretKeyRef(
+		ctx,
+		c.kube,
+		c.StoreKind,
+		c.namespace, prov.Auth.Cert.ClientCertRef)
+	if secErr != nil {
+		return nil, fmt.Errorf(errBadClientCert, secErr)
+	}
+	config.ClientCert = clientCert
+
+	clientKey, secErr := resolvers.SecretKeyRef(
+		ctx,
+		c.kube,
+		c.StoreKind,
+		c.namespace,
+		prov.Auth.Cert.ClientKeyRef)
+	if secErr != nil {
+		return nil, fmt.Errorf(errBadClientKey, secErr)
+	}
+	config.ClientCertKey = clientKey
+
+	conjur, clientError := c.clientAPI.NewClientFromCert(config)
+	if clientError != nil {
+		return nil, fmt.Errorf(errConjurClient, clientError)
+	}
+
+	c.client = conjur
+	return conjur, nil
+}

+ 6 - 0
providers/v1/conjur/conjur_api.go

@@ -32,6 +32,7 @@ type SecretsClient interface {
 type SecretsClientFactory interface {
 	NewClientFromKey(config conjurapi.Config, loginPair authn.LoginPair) (SecretsClient, error)
 	NewClientFromJWT(config conjurapi.Config) (SecretsClient, error)
+	NewClientFromCert(config conjurapi.Config) (SecretsClient, error)
 }
 
 // ClientAPIImpl is an implementation of the ClientAPI interface.
@@ -46,3 +47,8 @@ func (c *ClientAPIImpl) NewClientFromKey(config conjurapi.Config, loginPair auth
 func (c *ClientAPIImpl) NewClientFromJWT(config conjurapi.Config) (SecretsClient, error) {
 	return conjurapi.NewClientFromJwt(config)
 }
+
+// NewClientFromCert creates a new Conjur client using certificate-based authentication.
+func (c *ClientAPIImpl) NewClientFromCert(config conjurapi.Config) (SecretsClient, error) {
+	return conjurapi.NewClientFromCertificate(config)
+}

+ 1 - 2
providers/v1/conjur/go.mod

@@ -3,7 +3,7 @@ module github.com/external-secrets/external-secrets/providers/v1/conjur
 go 1.26.5
 
 require (
-	github.com/cyberark/conjur-api-go v0.13.8
+	github.com/cyberark/conjur-api-go v0.14.1
 	github.com/external-secrets/external-secrets/apis v0.0.0
 	github.com/external-secrets/external-secrets/runtime v0.0.0
 	github.com/golang-jwt/jwt/v5 v5.3.0
@@ -104,7 +104,6 @@ require (
 	google.golang.org/protobuf v1.36.11 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
 	k8s.io/apiextensions-apiserver v0.35.2 // indirect
 	k8s.io/klog/v2 v2.140.0 // indirect
 	k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect

+ 2 - 4
providers/v1/conjur/go.sum

@@ -38,8 +38,8 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1U
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cyberark/conjur-api-go v0.13.8 h1:woBEg+lvcoghpFkoGcS2JfPy+kJR59Ir7LeZcNA9wWE=
-github.com/cyberark/conjur-api-go v0.13.8/go.mod h1:xGi4RCulvsc+x/jYRrxUoEShznhlKP/4hJC/4+lueFg=
+github.com/cyberark/conjur-api-go v0.14.1 h1:6rF5smYVJ5wV/SpnJkVGQSYfYFNj4ltaWQXsfL1K2+c=
+github.com/cyberark/conjur-api-go v0.14.1/go.mod h1:POjKgJwnniONTnWySx6Yq87hYsdPD6Ohwm7x1UGio7E=
 github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
 github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -257,8 +257,6 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
 gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 233 - 8
providers/v1/conjur/provider_test.go

@@ -41,13 +41,16 @@ import (
 )
 
 var (
-	svcURL           = "https://example.com"
-	svcUser          = "user"
-	svcApikey        = "apikey"
-	svcAccount       = "account1"
-	jwtAuthenticator = "jwt-authenticator"
-	jwtAuthnService  = "jwt-auth-service"
-	jwtSecretName    = "jwt-secret"
+	svcURL             = "https://example.com"
+	svcUser            = "user"
+	svcApikey          = "apikey"
+	svcAccount         = "account1"
+	jwtAuthenticator   = "jwt-authenticator"
+	jwtAuthnService    = "jwt-auth-service"
+	jwtSecretName      = "jwt-secret"
+	certServiceID      = "cert-auth-service"
+	certClientCertName = "conjur-client-cert"
+	certClientKeyName  = "conjur-client-key"
 )
 
 func makeValidRef(k string) *esv1.ExternalSecretDataRemoteRef {
@@ -186,6 +189,92 @@ func TestGetSecret(t *testing.T) {
 				value: "secret",
 			},
 		},
+		"CertReadSecretSuccess": {
+			reason: "Should read a secret successfully using a Cert auth secret store (spiffe).",
+			args: args{
+				store: makeCertSecretStore(svcURL, certServiceID, "", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeCertSecrets()...).Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+			},
+			want: want{
+				err:   nil,
+				value: "secret",
+			},
+		},
+		"CertWithHostIdReadSecretSuccess": {
+			reason: "Should read a secret successfully using a Cert auth secret store with a host ID.",
+			args: args{
+				store: makeCertSecretStore(svcURL, certServiceID, "myhostid", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeCertSecrets()...).Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+			},
+			want: want{
+				err:   nil,
+				value: "secret",
+			},
+		},
+		"CertReadSecretFailure": {
+			reason: "Should fail to read secret using Cert auth secret store.",
+			args: args{
+				store: makeCertSecretStore(svcURL, certServiceID, "", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeCertSecrets()...).Build(),
+				namespace:  "default",
+				secretPath: "error",
+			},
+			want: want{
+				err:   errors.New("error"),
+				value: "",
+			},
+		},
+		"CertMissingClientCertSecret": {
+			reason: "Should fail when the client certificate secret does not exist.",
+			args: args{
+				store: makeCertSecretStore(svcURL, certServiceID, "", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(&corev1.Secret{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      certClientKeyName,
+							Namespace: "default",
+						},
+						Data: map[string][]byte{
+							"tls.key": []byte("-----BEGIN RSA PRIVATE KEY-----\nfakekey\n-----END RSA PRIVATE KEY-----"),
+						},
+					}).Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+			},
+			want: want{
+				err:   fmt.Errorf(errBadClientCert, fmt.Errorf("cannot get Kubernetes secret \"%s\" from namespace \"default\": secrets \"%s\" not found", certClientCertName, certClientCertName)),
+				value: "",
+			},
+		},
+		"CertMissingClientKeySecret": {
+			reason: "Should fail when the client key secret does not exist.",
+			args: args{
+				store: makeCertSecretStore(svcURL, certServiceID, "", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(&corev1.Secret{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      certClientCertName,
+							Namespace: "default",
+						},
+						Data: map[string][]byte{
+							"tls.crt": []byte("-----BEGIN CERTIFICATE-----\nfakecert\n-----END CERTIFICATE-----"),
+						},
+					}).Build(),
+				namespace:  "default",
+				secretPath: "path/to/secret",
+			},
+			want: want{
+				err:   fmt.Errorf(errBadClientKey, fmt.Errorf("cannot get Kubernetes secret \"%s\" from namespace \"default\": secrets \"%s\" not found", certClientKeyName, certClientKeyName)),
+				value: "",
+			},
+		},
 	}
 
 	runTest := func(t *testing.T, _ string, tc testCase) {
@@ -630,12 +719,121 @@ func makeStoreWithCA(caSource, caData string) *esv1.SecretStore {
 	return store
 }
 
-func makeNoAuthSecretStore(svcURL string) *esv1.SecretStore {
+func makeCertSecretStore(svcURL, certServiceID, hostID, conjurAccount string) *esv1.SecretStore {
 	store := &esv1.SecretStore{
 		Spec: esv1.SecretStoreSpec{
 			Provider: &esv1.SecretStoreProvider{
 				Conjur: &esv1.ConjurProvider{
 					URL: svcURL,
+					Auth: esv1.ConjurAuth{
+						Cert: &esv1.ConjurCert{
+							Account:   conjurAccount,
+							ServiceID: certServiceID,
+							HostID:    hostID,
+							ClientCertRef: &esmeta.SecretKeySelector{
+								Name: certClientCertName,
+								Key:  "tls.crt",
+							},
+							ClientKeyRef: &esmeta.SecretKeySelector{
+								Name: certClientKeyName,
+								Key:  "tls.key",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	return store
+}
+
+func makeCertSecretStoreWithMissingRefs(svcURL, certServiceID, conjurAccount string, hasCert, hasKey bool) *esv1.SecretStore {
+	var certRef *esmeta.SecretKeySelector
+	var keyRef *esmeta.SecretKeySelector
+	if hasCert {
+		certRef = &esmeta.SecretKeySelector{
+			Name: certClientCertName,
+			Key:  "tls.crt",
+		}
+	}
+	if hasKey {
+		keyRef = &esmeta.SecretKeySelector{
+			Name: certClientKeyName,
+			Key:  "tls.key",
+		}
+	}
+	store := &esv1.SecretStore{
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				Conjur: &esv1.ConjurProvider{
+					URL: svcURL,
+					Auth: esv1.ConjurAuth{
+						Cert: &esv1.ConjurCert{
+							Account:       conjurAccount,
+							ServiceID:     certServiceID,
+							ClientCertRef: certRef,
+							ClientKeyRef:  keyRef,
+						},
+					},
+				},
+			},
+		},
+	}
+	return store
+}
+
+func makeMultiAuthSecretStore(svcURL string) *esv1.SecretStore {
+	return &esv1.SecretStore{
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				Conjur: &esv1.ConjurProvider{
+					URL: svcURL,
+					Auth: esv1.ConjurAuth{
+						APIKey: &esv1.ConjurAPIKey{
+							Account:   svcAccount,
+							UserRef:   &esmeta.SecretKeySelector{Name: svcUser, Key: "username"},
+							APIKeyRef: &esmeta.SecretKeySelector{Name: svcApikey, Key: "apikey"},
+						},
+						Jwt: &esv1.ConjurJWT{
+							Account:           "myconjuraccount",
+							ServiceID:         jwtAuthnService,
+							ServiceAccountRef: &esmeta.ServiceAccountSelector{Name: "conjur"},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func makeCertSecretStoreWithEmptyRefNames(svcURL, certServiceID, conjurAccount string, emptyCertName, emptyKeyName bool) *esv1.SecretStore {
+	certName := certClientCertName
+	if emptyCertName {
+		certName = ""
+	}
+	keyName := certClientKeyName
+	if emptyKeyName {
+		keyName = ""
+	}
+	store := &esv1.SecretStore{
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				Conjur: &esv1.ConjurProvider{
+					URL: svcURL,
+					Auth: esv1.ConjurAuth{
+						Cert: &esv1.ConjurCert{
+							Account:   conjurAccount,
+							ServiceID: certServiceID,
+							ClientCertRef: &esmeta.SecretKeySelector{
+								Name: certName,
+								Key:  "tls.crt",
+							},
+							ClientKeyRef: &esmeta.SecretKeySelector{
+								Name: keyName,
+								Key:  "tls.key",
+							},
+						},
+					},
 				},
 			},
 		},
@@ -666,6 +864,29 @@ func makeFakeAPIKeySecrets() []kclient.Object {
 	}
 }
 
+func makeFakeCertSecrets() []kclient.Object {
+	return []kclient.Object{
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      certClientCertName,
+				Namespace: "default",
+			},
+			Data: map[string][]byte{
+				"tls.crt": []byte("-----BEGIN CERTIFICATE-----\nfakecert\n-----END CERTIFICATE-----"),
+			},
+		},
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      certClientKeyName,
+				Namespace: "default",
+			},
+			Data: map[string][]byte{
+				"tls.key": []byte("-----BEGIN RSA PRIVATE KEY-----\nfakekey\n-----END RSA PRIVATE KEY-----"),
+			},
+		},
+	}
+}
+
 func makeFakeCASource(kind, caData string) kclient.Object {
 	if kind == "secret" {
 		return &corev1.Secret{
@@ -707,6 +928,10 @@ func createFakeJwtToken(expires bool) string {
 type ConjurMockAPIClient struct {
 }
 
+func (c *ConjurMockAPIClient) NewClientFromCert(_ conjurapi.Config) (SecretsClient, error) {
+	return &fake.ConjurMockClient{}, nil
+}
+
 func (c *ConjurMockAPIClient) NewClientFromKey(_ conjurapi.Config, _ authn.LoginPair) (SecretsClient, error) {
 	return &fake.ConjurMockClient{}, nil
 }

+ 56 - 3
providers/v1/conjur/validate.go

@@ -38,6 +38,11 @@ func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, e
 	if prov.URL == "" {
 		return nil, errors.New("conjur URL cannot be empty")
 	}
+
+	if err := validateAuthCount(prov.Auth); err != nil {
+		return nil, err
+	}
+
 	if prov.Auth.APIKey != nil {
 		err := validateAPIKeyStore(store, *prov.Auth.APIKey)
 		if err != nil {
@@ -52,14 +57,33 @@ func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, e
 		}
 	}
 
-	// At least one auth must be configured
-	if prov.Auth.APIKey == nil && prov.Auth.Jwt == nil {
-		return nil, errors.New("missing Auth.* configuration")
+	if prov.Auth.Cert != nil {
+		err := validateCertStore(store, *prov.Auth.Cert)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	return nil, nil
 }
 
+func validateAuthCount(auth esv1.ConjurAuth) error {
+	count := 0
+	if auth.APIKey != nil {
+		count++
+	}
+	if auth.Jwt != nil {
+		count++
+	}
+	if auth.Cert != nil {
+		count++
+	}
+	if count != 1 {
+		return errors.New("must specify exactly one Auth.* method")
+	}
+	return nil
+}
+
 func validateAPIKeyStore(store esv1.GenericStore, auth esv1.ConjurAPIKey) error {
 	if auth.Account == "" {
 		return errors.New("missing Auth.ApiKey.Account")
@@ -101,3 +125,32 @@ func validateJWTStore(store esv1.GenericStore, auth esv1.ConjurJWT) error {
 	}
 	return nil
 }
+
+func validateCertStore(store esv1.GenericStore, auth esv1.ConjurCert) error {
+	if auth.Account == "" {
+		return errors.New("missing Auth.Cert.Account")
+	}
+	if auth.ServiceID == "" {
+		return errors.New("missing Auth.Cert.ServiceID")
+	}
+	if auth.ClientCertRef == nil {
+		return errors.New("missing Auth.Cert.ClientCertRef")
+	}
+	if auth.ClientCertRef.Name == "" {
+		return errors.New("missing Auth.Cert.ClientCertRef.Name")
+	}
+	if err := esutils.ValidateReferentSecretSelector(store, *auth.ClientCertRef); err != nil {
+		return fmt.Errorf("invalid Auth.Cert.ClientCertRef: %w", err)
+	}
+	if auth.ClientKeyRef == nil {
+		return errors.New("missing Auth.Cert.ClientKeyRef")
+	}
+	if auth.ClientKeyRef.Name == "" {
+		return errors.New("missing Auth.Cert.ClientKeyRef.Name")
+	}
+	if err := esutils.ValidateReferentSecretSelector(store, *auth.ClientKeyRef); err != nil {
+		return fmt.Errorf("invalid Auth.Cert.ClientKeyRef: %w", err)
+	}
+
+	return nil
+}

+ 51 - 2
providers/v1/conjur/validate_test.go

@@ -77,8 +77,57 @@ func TestValidateStore(t *testing.T) {
 		},
 
 		{
-			store: makeNoAuthSecretStore(svcURL),
-			err:   errors.New("missing Auth.* configuration"),
+			store: makeCertSecretStore(svcURL, certServiceID, "", svcAccount),
+			err:   nil,
+		},
+		{
+			store: makeCertSecretStore(svcURL, certServiceID, "myhostid", svcAccount),
+			err:   nil,
+		},
+		{
+			store: makeCertSecretStore("", certServiceID, "", svcAccount),
+			err:   errors.New("conjur URL cannot be empty"),
+		},
+		{
+			store: makeCertSecretStore(svcURL, "", "", svcAccount),
+			err:   errors.New("missing Auth.Cert.ServiceID"),
+		},
+		{
+			store: makeCertSecretStore(svcURL, certServiceID, "", ""),
+			err:   errors.New("missing Auth.Cert.Account"),
+		},
+		{
+			store: makeCertSecretStoreWithMissingRefs(svcURL, certServiceID, svcAccount, true, false),
+			err:   errors.New("missing Auth.Cert.ClientKeyRef"),
+		},
+		{
+			store: makeCertSecretStoreWithMissingRefs(svcURL, certServiceID, svcAccount, false, true),
+			err:   errors.New("missing Auth.Cert.ClientCertRef"),
+		},
+		{
+			store: makeCertSecretStoreWithEmptyRefNames(svcURL, certServiceID, svcAccount, true, false),
+			err:   errors.New("missing Auth.Cert.ClientCertRef.Name"),
+		},
+		{
+			store: makeCertSecretStoreWithEmptyRefNames(svcURL, certServiceID, svcAccount, false, true),
+			err:   errors.New("missing Auth.Cert.ClientKeyRef.Name"),
+		},
+		{
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						Conjur: &esv1.ConjurProvider{
+							URL:  svcURL,
+							Auth: esv1.ConjurAuth{},
+						},
+					},
+				},
+			},
+			err: errors.New("must specify exactly one Auth.* method"),
+		},
+		{
+			store: makeMultiAuthSecretStore(svcURL),
+			err:   errors.New("must specify exactly one Auth.* method"),
 		},
 	}
 	p := Provider{}

+ 12 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -244,6 +244,18 @@ spec:
             key: string
             name: string
             namespace: string
+        cert:
+          account: string
+          clientCertRef:
+            key: string
+            name: string
+            namespace: string
+          clientKeyRef:
+            key: string
+            name: string
+            namespace: string
+          hostId: string
+          serviceID: string
         jwt:
           account: string
           hostId: string

+ 12 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -244,6 +244,18 @@ spec:
             key: string
             name: string
             namespace: string
+        cert:
+          account: string
+          clientCertRef:
+            key: string
+            name: string
+            namespace: string
+          clientKeyRef:
+            key: string
+            name: string
+            namespace: string
+          hostId: string
+          serviceID: string
         jwt:
           account: string
           hostId: string