diff --git a/apis/externalsecrets/v1beta1/secretstore_conjur_types.go b/apis/externalsecrets/v1beta1/secretstore_conjur_types.go
index 213e21c1d..9ee55a133 100644
--- a/apis/externalsecrets/v1beta1/secretstore_conjur_types.go
+++ b/apis/externalsecrets/v1beta1/secretstore_conjur_types.go
@@ -17,13 +17,19 @@ package v1beta1
import esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
type ConjurProvider struct {
- URL string `json:"url"`
- CABundle string `json:"caBundle,omitempty"`
- Auth ConjurAuth `json:"auth"`
+ URL string `json:"url"`
+ // +optional
+ CABundle string `json:"caBundle,omitempty"`
+ // +optional
+ CAProvider *CAProvider `json:"caProvider,omitempty"`
+ Auth ConjurAuth `json:"auth"`
}
type ConjurAuth struct {
+ // +optional
Apikey *ConjurApikey `json:"apikey"`
+ // +optional
+ Jwt *ConjurJWT `json:"jwt"`
}
type ConjurApikey struct {
@@ -31,3 +37,20 @@ type ConjurApikey struct {
UserRef *esmeta.SecretKeySelector `json:"userRef"`
APIKeyRef *esmeta.SecretKeySelector `json:"apiKeyRef"`
}
+
+type ConjurJWT struct {
+ Account string `json:"account"`
+
+ // The conjur authn jwt webservice id
+ ServiceID string `json:"serviceID"`
+
+ // Optional SecretRef that refers to a key in a Secret resource containing JWT token to
+ // authenticate with Conjur using the JWT authentication method.
+ // +optional
+ SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"`
+
+ // Optional ServiceAccountRef specifies the Kubernetes service account for which to request
+ // a token for with the `TokenRequest` API.
+ // +optional
+ ServiceAccountRef *esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
+}
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index a0cb04157..a30dba500 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
@@ -672,6 +672,11 @@ func (in *ConjurAuth) DeepCopyInto(out *ConjurAuth) {
*out = new(ConjurApikey)
(*in).DeepCopyInto(*out)
}
+ if in.Jwt != nil {
+ in, out := &in.Jwt, &out.Jwt
+ *out = new(ConjurJWT)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConjurAuth.
@@ -684,9 +689,39 @@ 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 *ConjurJWT) DeepCopyInto(out *ConjurJWT) {
+ *out = *in
+ if in.SecretRef != nil {
+ in, out := &in.SecretRef, &out.SecretRef
+ *out = new(metav1.SecretKeySelector)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ServiceAccountRef != nil {
+ in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
+ *out = new(metav1.ServiceAccountSelector)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConjurJWT.
+func (in *ConjurJWT) DeepCopy() *ConjurJWT {
+ if in == nil {
+ return nil
+ }
+ out := new(ConjurJWT)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConjurProvider) DeepCopyInto(out *ConjurProvider) {
*out = *in
+ if in.CAProvider != nil {
+ in, out := &in.CAProvider, &out.CAProvider
+ *out = new(CAProvider)
+ (*in).DeepCopyInto(*out)
+ }
in.Auth.DeepCopyInto(&out.Auth)
}
diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
index 66cc882ab..31a3bb7c3 100644
--- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml
+++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
@@ -2251,11 +2251,98 @@ spec:
- apiKeyRef
- userRef
type: object
- required:
- - apikey
+ jwt:
+ properties:
+ account:
+ type: string
+ secretRef:
+ description: Optional SecretRef that refers to a key
+ in a Secret resource containing JWT token to authenticate
+ with Conjur using the JWT authentication method.
+ properties:
+ key:
+ description: The key of the entry in the Secret
+ resource's `data` field to be used. Some instances
+ of this field may be defaulted, in others it
+ may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being
+ referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped.
+ cluster-scoped defaults to the namespace of
+ the referent.
+ type: string
+ type: object
+ serviceAccountRef:
+ description: Optional ServiceAccountRef specifies
+ the Kubernetes service account for which to request
+ a token for with the `TokenRequest` API.
+ properties:
+ audiences:
+ description: Audience specifies the `aud` claim
+ for the service account token If the service
+ account uses a well-known annotation for e.g.
+ IRSA or GCP Workload Identity then this audiences
+ will be appended to the list
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the ServiceAccount resource
+ being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped.
+ cluster-scoped defaults to the namespace of
+ the referent.
+ type: string
+ required:
+ - name
+ type: object
+ serviceID:
+ description: The conjur authn jwt webservice id
+ type: string
+ required:
+ - account
+ - serviceID
+ type: object
type: object
caBundle:
type: string
+ caProvider:
+ description: Used to provide custom certificate authority
+ (CA) certificates for a secret store. The CAProvider points
+ to a Secret or ConfigMap resource that contains a PEM-encoded
+ certificate.
+ properties:
+ key:
+ description: The key where the CA certificate can be found
+ in the Secret or ConfigMap.
+ type: string
+ name:
+ description: The name of the object located at the provider
+ type.
+ type: string
+ namespace:
+ description: The namespace the Provider type is in. Can
+ only be defined when used in a ClusterSecretStore.
+ type: string
+ type:
+ description: The type of provider to use such as "Secret",
+ or "ConfigMap".
+ enum:
+ - Secret
+ - ConfigMap
+ type: string
+ required:
+ - name
+ - type
+ type: object
url:
type: string
required:
diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml
index 12bee9d36..036970081 100644
--- a/config/crds/bases/external-secrets.io_secretstores.yaml
+++ b/config/crds/bases/external-secrets.io_secretstores.yaml
@@ -2251,11 +2251,98 @@ spec:
- apiKeyRef
- userRef
type: object
- required:
- - apikey
+ jwt:
+ properties:
+ account:
+ type: string
+ secretRef:
+ description: Optional SecretRef that refers to a key
+ in a Secret resource containing JWT token to authenticate
+ with Conjur using the JWT authentication method.
+ properties:
+ key:
+ description: The key of the entry in the Secret
+ resource's `data` field to be used. Some instances
+ of this field may be defaulted, in others it
+ may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being
+ referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped.
+ cluster-scoped defaults to the namespace of
+ the referent.
+ type: string
+ type: object
+ serviceAccountRef:
+ description: Optional ServiceAccountRef specifies
+ the Kubernetes service account for which to request
+ a token for with the `TokenRequest` API.
+ properties:
+ audiences:
+ description: Audience specifies the `aud` claim
+ for the service account token If the service
+ account uses a well-known annotation for e.g.
+ IRSA or GCP Workload Identity then this audiences
+ will be appended to the list
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the ServiceAccount resource
+ being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped.
+ cluster-scoped defaults to the namespace of
+ the referent.
+ type: string
+ required:
+ - name
+ type: object
+ serviceID:
+ description: The conjur authn jwt webservice id
+ type: string
+ required:
+ - account
+ - serviceID
+ type: object
type: object
caBundle:
type: string
+ caProvider:
+ description: Used to provide custom certificate authority
+ (CA) certificates for a secret store. The CAProvider points
+ to a Secret or ConfigMap resource that contains a PEM-encoded
+ certificate.
+ properties:
+ key:
+ description: The key where the CA certificate can be found
+ in the Secret or ConfigMap.
+ type: string
+ name:
+ description: The name of the object located at the provider
+ type.
+ type: string
+ namespace:
+ description: The namespace the Provider type is in. Can
+ only be defined when used in a ClusterSecretStore.
+ type: string
+ type:
+ description: The type of provider to use such as "Secret",
+ or "ConfigMap".
+ enum:
+ - Secret
+ - ConfigMap
+ type: string
+ required:
+ - name
+ - type
+ type: object
url:
type: string
required:
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index ce500f846..3847ff7ac 100644
--- a/deploy/crds/bundle.yaml
+++ b/deploy/crds/bundle.yaml
@@ -2101,11 +2101,72 @@ spec:
- apiKeyRef
- userRef
type: object
- required:
- - apikey
+ jwt:
+ properties:
+ account:
+ type: string
+ secretRef:
+ description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Conjur using the JWT authentication method.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ type: object
+ serviceAccountRef:
+ description: Optional ServiceAccountRef specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API.
+ properties:
+ audiences:
+ description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the ServiceAccount resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ required:
+ - name
+ type: object
+ serviceID:
+ description: The conjur authn jwt webservice id
+ type: string
+ required:
+ - account
+ - serviceID
+ type: object
type: object
caBundle:
type: string
+ caProvider:
+ description: Used to provide custom certificate authority (CA) certificates for a secret store. The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+ properties:
+ key:
+ description: The key where the CA certificate can be found in the Secret or ConfigMap.
+ type: string
+ name:
+ description: The name of the object located at the provider type.
+ type: string
+ namespace:
+ description: The namespace the Provider type is in. Can only be defined when used in a ClusterSecretStore.
+ type: string
+ type:
+ description: The type of provider to use such as "Secret", or "ConfigMap".
+ enum:
+ - Secret
+ - ConfigMap
+ type: string
+ required:
+ - name
+ - type
+ type: object
url:
type: string
required:
@@ -5837,11 +5898,72 @@ spec:
- apiKeyRef
- userRef
type: object
- required:
- - apikey
+ jwt:
+ properties:
+ account:
+ type: string
+ secretRef:
+ description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Conjur using the JWT authentication method.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ type: object
+ serviceAccountRef:
+ description: Optional ServiceAccountRef specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API.
+ properties:
+ audiences:
+ description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the ServiceAccount resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ required:
+ - name
+ type: object
+ serviceID:
+ description: The conjur authn jwt webservice id
+ type: string
+ required:
+ - account
+ - serviceID
+ type: object
type: object
caBundle:
type: string
+ caProvider:
+ description: Used to provide custom certificate authority (CA) certificates for a secret store. The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+ properties:
+ key:
+ description: The key where the CA certificate can be found in the Secret or ConfigMap.
+ type: string
+ name:
+ description: The name of the object located at the provider type.
+ type: string
+ namespace:
+ description: The namespace the Provider type is in. Can only be defined when used in a ClusterSecretStore.
+ type: string
+ type:
+ description: The type of provider to use such as "Secret", or "ConfigMap".
+ enum:
+ - Secret
+ - ConfigMap
+ type: string
+ required:
+ - name
+ - type
+ type: object
url:
type: string
required:
diff --git a/docs/api/spec.md b/docs/api/spec.md
index 02d6557f9..4860c7781 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -964,6 +964,7 @@ string
(Appears on:
AkeylessProvider,
+ConjurProvider,
KubernetesServer,
VaultProvider)
@@ -1728,6 +1729,89 @@ ConjurApikey
+(Optional)
+ |
+
+
+
+jwt
+
+
+ConjurJWT
+
+
+ |
+
+(Optional)
+ |
+
+
+
+ConjurJWT
+
+
+(Appears on:
+ConjurAuth)
+
+
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+account
+
+string
+
+ |
+
+ |
+
+
+
+serviceID
+
+string
+
+ |
+
+ The conjur authn jwt webservice id
+ |
+
+
+
+secretRef
+
+
+External Secrets meta/v1.SecretKeySelector
+
+
+ |
+
+(Optional)
+ Optional SecretRef that refers to a key in a Secret resource containing JWT token to
+authenticate with Conjur using the JWT authentication method.
+ |
+
+
+
+serviceAccountRef
+
+
+External Secrets meta/v1.ServiceAccountSelector
+
+
+ |
+
+(Optional)
+ Optional ServiceAccountRef specifies the Kubernetes service account for which to request
+a token for with the TokenRequest API.
|
@@ -1766,6 +1850,20 @@ string
+(Optional)
+ |
+
+
+
+caProvider
+
+
+CAProvider
+
+
+ |
+
+(Optional)
|
diff --git a/docs/provider/conjur.md b/docs/provider/conjur.md
index c98b3cd22..39178b184 100644
--- a/docs/provider/conjur.md
+++ b/docs/provider/conjur.md
@@ -9,32 +9,32 @@ This section contains the list of the pre-requirements before installing the Con
* Running Conjur Server
- These items will be needed in order to configure the secret-store
+ Conjur endpoint - include the scheme but no trailing '/', ex: https://myapi.example.com
- + Conjur credentials (hostid, apikey)
+ + Conjur authentication info (hostid, apikey, jwt service id, etc)
+ + Conjur must be configured to support your authentication method (`apikey` is supported by default, `jwt` requires additional configuration)
+ Certificate for Conjur server is OPTIONAL -- But, **when using a self-signed cert when setting up your Conjur server, it is strongly recommended to populate "caBundle" with self-signed cert in the secret-store definition**
* Kubernetes cluster
- External Secrets Operator is installed
-### Create External Secret Store Definition
+### Certificate for Conjur server
+When using a self-signed cert when setting up your Conjur server, it is strongly recommended to populate "caBundle" with self-signed cert in the secret-store definition. The certificate CA must be referenced on the secret-store definition using either a `caBundle` or `caProvider` as below:
+
+```yaml
+{% include 'conjur-ca-bundle.yaml' %}
+```
+
+### External Secret Store Definition with ApiKey Authentication
+This method uses a combination of the Conjur `hostid` and `apikey` to authenticate to Conjur. This method is the simplest to setup and use as your Conjur instance requires no special setup.
+
+#### Create External Secret Store Definition
Recommend to save as filename: `conjur-secret-store.yaml`
```yaml
-{% include 'conjur-secret-store.yaml' %}
+{% include 'conjur-secret-store-apikey.yaml' %}
```
-### Create External Secret Definition
-
-Important note: **Creds must live in the same namespace as a SecretStore - the secret store may only reference secrets from the same namespace.** When using a ClusterSecretStore this limitation is lifted and the creds can live in any namespace.
-
-Recommend to save as filename: `conjur-external-secret.yaml`
-
-```yaml
-{% include 'conjur-external-secret.yaml' %}
-```
-
-### Create Kubernetes Secrets
-
-In order for the ESO **Conjur** provider to connect to the Conjur server, the creds should be stored as k8s secrets. Please refer to for various methods to create secrets. Here is one way to do it using `kubectl`
+#### Create Kubernetes Secrets
+In order for the ESO **Conjur** provider to connect to the Conjur server using the `apikey` creds, these creds should be stored as k8s secrets. Please refer to for various methods to create secrets. Here is one way to do it using `kubectl`
***NOTE***: "conjur-creds" is the "name" used in "userRef" and "apikeyRef" in the conjur-secret-store definition
@@ -46,6 +46,53 @@ kubectl -n external-secrets create secret generic conjur-creds --from-literal=ho
# kubectl -n external-secrets create secret generic conjur-creds --from-literal=hostid=host/data/app1/host001 --from-literal=apikey=321blahblah
```
+### External Secret Store with JWT Authentication
+This method uses JWT tokens to authenticate with Conjur. The following methods for retrieving the JWT token for authentication are supported:
+
+- JWT token from a referenced Kubernetes Service Account
+- JWT token stored in a Kubernetes secret
+
+#### Create External Secret Store Definition
+
+When using JWT authentication the following must be specified in the `SecretStore`:
+
+- `account` - The name of the Conjur account
+- `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that will be used to authenticate the JWT token
+
+You can then choose to either retrieve the JWT token using a Service Account reference or from a Kubernetes Secret.
+
+To use a JWT token from a referenced Kubernetes Service Account, the following secret store definition can be used:
+
+```yaml
+{% include 'conjur-secret-store-jwt-service-account-ref.yaml' %}
+```
+
+This is only supported in Kubernetes 1.22 and above as it uses the [TokenRequest API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-request-v1/) to get the JWT token from the referenced service account. Audiences can be set as required by the [Conjur JWT authenticator](https://docs.conjur.org/Latest/en/Content/Integrations/k8s-ocp/k8s-jwt-authn.htm).
+
+Alternatively, a secret containing a valid JWT token can be referenced as follows:
+
+```yaml
+{% include 'conjur-secret-store-jwt-secret-ref.yaml' %}
+```
+
+This secret must contain a JWT token that identifies your Conjur host. The secret must contain a JWT token consumable by a configured Conjur JWT authenticator and must satisfy all [Conjur JWT guidelines](https://docs.conjur.org/Latest/en/Content/Operations/Services/cjr-authn-jwt-guidelines.htm#Best). This can be a JWT created by an external JWT issuer or the Kubernetes api server itself. Such a with Kubernetes Service Account token can be created using the below command:
+
+```shell
+kubectl create token my-service-account --audience='https://conjur.company.com' --duration=3600s
+```
+
+Save the `SecretStore` definition as filename `conjur-secret-store.yaml` as referenced in later steps.
+
+### Create External Secret Definition
+
+Important note: **Creds must live in the same namespace as a SecretStore - the secret store may only reference secrets from the same namespace.** When using a ClusterSecretStore this limitation is lifted and the creds can live in any namespace.
+
+Recommend to save as filename: `conjur-external-secret.yaml`
+
+```yaml
+{% include 'conjur-external-secret.yaml' %}
+```
+
### Create the External Secrets Store
```shell
diff --git a/docs/snippets/conjur-ca-bundle.yaml b/docs/snippets/conjur-ca-bundle.yaml
new file mode 100644
index 000000000..b3e0b4a13
--- /dev/null
+++ b/docs/snippets/conjur-ca-bundle.yaml
@@ -0,0 +1,20 @@
+....
+spec:
+ provider:
+ conjur:
+ # Service URL
+ url: https://myapi.conjur.org
+
+ # [OPTIONAL] base64 encoded string of certificate
+ caBundle: ""
+
+ # [OPTIONAL] caProvider:
+ # Instead of caBundle you can also specify a caProvider
+ # this will retrieve the cert from a Secret or ConfigMap
+ caProvider:
+ type: "Secret" # Can be Secret or ConfigMap
+ name: ""
+ key: ""
+ # namespace is mandatory for ClusterSecretStore and not relevant for SecretStore
+ namespace: "my-cert-secret-namespace"
+ ....
diff --git a/docs/snippets/conjur-secret-store.yaml b/docs/snippets/conjur-secret-store-apikey.yaml
similarity index 100%
rename from docs/snippets/conjur-secret-store.yaml
rename to docs/snippets/conjur-secret-store-apikey.yaml
diff --git a/docs/snippets/conjur-secret-store-jwt-secret-ref.yaml b/docs/snippets/conjur-secret-store-jwt-secret-ref.yaml
new file mode 100644
index 000000000..bc745be48
--- /dev/null
+++ b/docs/snippets/conjur-secret-store-jwt-secret-ref.yaml
@@ -0,0 +1,19 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+ name: conjur
+spec:
+ provider:
+ conjur:
+ # Service URL
+ url: https://myapi.conjur.org
+ # [OPTIONAL] base64 encoded string of certificate
+ caBundle: OPTIONALxFIELDxxxBase64xCertxString==
+ auth:
+ jwt:
+ # conjur account
+ account: conjur
+ serviceID: my-jwt-auth-service # The authn-jwt service ID
+ secretRef: # Secret containing a valid JWT token
+ name: my-jwt-secret
+ key: token
diff --git a/docs/snippets/conjur-secret-store-jwt-service-account-ref.yaml b/docs/snippets/conjur-secret-store-jwt-service-account-ref.yaml
new file mode 100644
index 000000000..9e66bd23f
--- /dev/null
+++ b/docs/snippets/conjur-secret-store-jwt-service-account-ref.yaml
@@ -0,0 +1,21 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+ name: conjur
+spec:
+ provider:
+ conjur:
+ # Service URL
+ url: https://myapi.conjur.org
+ # [OPTIONAL] base64 encoded string of certificate
+ caBundle: OPTIONALxFIELDxxxBase64xCertxString==
+ auth:
+ jwt:
+ # conjur account
+ account: conjur
+ serviceID: my-jwt-auth-service # The authn-jwt service ID
+ serviceAccountRef: # Service account to retrieve JWT token for
+ name: my-service-account
+ audiences: # [OPTIONAL] audiences to include in JWT token
+ - https://conjur.company.com
+
diff --git a/pkg/provider/conjur/auth_jwt.go b/pkg/provider/conjur/auth_jwt.go
new file mode 100644
index 000000000..762cc377f
--- /dev/null
+++ b/pkg/provider/conjur/auth_jwt.go
@@ -0,0 +1,110 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package conjur
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/cyberark/conjur-api-go/conjurapi"
+ authenticationv1 "k8s.io/api/authentication/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+const JwtLifespan = 600 // 10 minutes
+
+// getJWTToken retrieves a JWT token either using the TokenRequest API for a specified service account, or from a jwt stored in a k8s secret.
+func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.ConjurJWT) (string, error) {
+ if conjurJWTConfig.ServiceAccountRef != nil {
+ // Should work for Kubernetes >=v1.22: fetch token via TokenRequest API
+ jwtToken, err := p.getJwtFromServiceAccountTokenRequest(ctx, *conjurJWTConfig.ServiceAccountRef, nil, JwtLifespan)
+ if err != nil {
+ return "", err
+ }
+ return jwtToken, nil
+ } else if conjurJWTConfig.SecretRef != nil {
+ tokenRef := conjurJWTConfig.SecretRef
+ if tokenRef.Key == "" {
+ tokenRef = conjurJWTConfig.SecretRef.DeepCopy()
+ tokenRef.Key = "token"
+ }
+ jwtToken, err := p.secretKeyRef(ctx, tokenRef)
+ if err != nil {
+ return "", err
+ }
+ return jwtToken, nil
+ }
+ return "", fmt.Errorf("missing ServiceAccountRef or SecretRef")
+}
+
+// getJwtFromServiceAccountTokenRequest uses the TokenRequest API to get a JWT token for the given service account.
+func (p *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, serviceAccountRef esmeta.ServiceAccountSelector, additionalAud []string, expirationSeconds int64) (string, error) {
+ audiences := serviceAccountRef.Audiences
+ if len(additionalAud) > 0 {
+ audiences = append(audiences, additionalAud...)
+ }
+ tokenRequest := &authenticationv1.TokenRequest{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: p.namespace,
+ },
+ Spec: authenticationv1.TokenRequestSpec{
+ Audiences: audiences,
+ ExpirationSeconds: &expirationSeconds,
+ },
+ }
+ if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
+ (serviceAccountRef.Namespace != nil) {
+ tokenRequest.Namespace = *serviceAccountRef.Namespace
+ }
+ tokenResponse, err := p.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
+ if err != nil {
+ return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err)
+ }
+ return tokenResponse.Status.Token, nil
+}
+
+// newClientFromJwt creates a new Conjur client using the given JWT Auth Config.
+func (p *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config, jwtAuth *esv1beta1.ConjurJWT) (SecretsClient, error) {
+ jwtToken, getJWTError := p.getJWTToken(ctx, jwtAuth)
+ if getJWTError != nil {
+ return nil, getJWTError
+ }
+
+ client, clientError := p.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID)
+ if clientError != nil {
+ return nil, clientError
+ }
+
+ return client, nil
+}
+
+// newHTTPSClient creates a new HTTPS client with the given cert.
+func newHTTPSClient(cert []byte) (*http.Client, error) {
+ pool := x509.NewCertPool()
+ ok := pool.AppendCertsFromPEM(cert)
+ if !ok {
+ return nil, fmt.Errorf("can't append Conjur SSL cert")
+ }
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12},
+ }
+ return &http.Client{Transport: tr, Timeout: time.Second * 10}, nil
+}
diff --git a/pkg/provider/conjur/conjur_api.go b/pkg/provider/conjur/conjur_api.go
new file mode 100644
index 000000000..eb437e8be
--- /dev/null
+++ b/pkg/provider/conjur/conjur_api.go
@@ -0,0 +1,85 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package conjur
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/cyberark/conjur-api-go/conjurapi"
+ "github.com/cyberark/conjur-api-go/conjurapi/authn"
+ "github.com/cyberark/conjur-api-go/conjurapi/response"
+)
+
+// SecretsClient is an interface for the Conjur client.
+type SecretsClient interface {
+ RetrieveSecret(secret string) (result []byte, err error)
+}
+
+// SecretsClientFactory is an interface for creating a Conjur client.
+type SecretsClientFactory interface {
+ NewClientFromKey(config conjurapi.Config, loginPair authn.LoginPair) (SecretsClient, error)
+ NewClientFromJWT(config conjurapi.Config, jwtToken string, jwtServiceID string) (SecretsClient, error)
+}
+
+// ClientAPIImpl is an implementation of the ClientAPI interface.
+type ClientAPIImpl struct{}
+
+func (c *ClientAPIImpl) NewClientFromKey(config conjurapi.Config, loginPair authn.LoginPair) (SecretsClient, error) {
+ return conjurapi.NewClientFromKey(config, loginPair)
+}
+
+// NewClientFromJWT creates a new Conjur client from a JWT token.
+// cannot use the built-in function "conjurapi.NewClientFromJwt" because it requires environment variables
+// see: https://github.com/cyberark/conjur-api-go/blob/b698692392a38e5d38b8440f32ab74206544848a/conjurapi/client.go#L130
+func (c *ClientAPIImpl) NewClientFromJWT(config conjurapi.Config, jwtToken, jwtServiceID string) (SecretsClient, error) {
+ jwtTokenString := fmt.Sprintf("jwt=%s", jwtToken)
+
+ var httpClient *http.Client
+ if config.IsHttps() {
+ cert, err := config.ReadSSLCert()
+ if err != nil {
+ return nil, err
+ }
+ httpClient, err = newHTTPSClient(cert)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ httpClient = &http.Client{Timeout: time.Second * 10}
+ }
+
+ authnJwtURL := strings.Join([]string{config.ApplianceURL, "authn-jwt", jwtServiceID, config.Account, "authenticate"}, "/")
+
+ req, err := http.NewRequest("POST", authnJwtURL, strings.NewReader(jwtTokenString))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ tokenBytes, err := response.DataResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return conjurapi.NewClientFromToken(config, string(tokenBytes))
+}
diff --git a/pkg/provider/conjur/provider.go b/pkg/provider/conjur/provider.go
index 04d566f8f..bd357372f 100644
--- a/pkg/provider/conjur/provider.go
+++ b/pkg/provider/conjur/provider.go
@@ -24,7 +24,10 @@ import (
"github.com/cyberark/conjur-api-go/conjurapi/authn"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ "k8s.io/client-go/kubernetes"
+ typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
+ ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
@@ -37,76 +40,130 @@ var (
errBadCertBundle = "caBundle failed to base64 decode: %w"
errBadServiceUser = "could not get Auth.Apikey.UserRef: %w"
errBadServiceAPIKey = "could not get Auth.Apikey.ApiKeyRef: %w"
+
+ errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
+
+ errUnableToFetchCAProviderCM = "unable to fetch Server.CAProvider ConfigMap: %w"
+ errUnableToFetchCAProviderSecret = "unable to fetch Server.CAProvider Secret: %w"
)
-// Provider is a provider for Conjur.
-type Provider struct {
- ConjurClient Client
- StoreKind string
- kube client.Client
- namespace string
+// Client is a provider for Conjur.
+type Client struct {
+ StoreKind string
+ kube client.Client
+ store esv1beta1.GenericStore
+ namespace string
+ corev1 typedcorev1.CoreV1Interface
+ clientAPI SecretsClientFactory
+ client SecretsClient
}
-// Client is an interface for the Conjur client.
-type Client interface {
- RetrieveSecret(secret string) (result []byte, err error)
+type Provider struct {
+ NewConjurProvider func(context context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientApi SecretsClientFactory) (esv1beta1.SecretsClient, error)
}
// NewClient creates a new Conjur client.
-func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
- prov, err := util.GetConjurProvider(store)
+func (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
+ // controller-runtime/client does not support TokenRequest or other subresource APIs
+ // so we need to construct our own client and use it to create a TokenRequest
+ restCfg, err := ctrlcfg.GetConfig()
if err != nil {
return nil, err
}
- p.StoreKind = store.GetObjectKind().GroupVersionKind().Kind
- p.kube = kube
- p.namespace = namespace
-
- certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(prov.CABundle))
- if decodeErr != nil {
- return nil, fmt.Errorf(errBadCertBundle, decodeErr)
+ clientset, err := kubernetes.NewForConfig(restCfg)
+ if err != nil {
+ return nil, err
+ }
+
+ return c.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
+}
+
+func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientAPI SecretsClientFactory) (esv1beta1.SecretsClient, error) {
+ return &Client{
+ StoreKind: store.GetObjectKind().GroupVersionKind().Kind,
+ store: store,
+ kube: kube,
+ namespace: namespace,
+ corev1: corev1,
+ clientAPI: clientAPI,
+ }, nil
+}
+
+func (p *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
+ // if the client is initialized already, return it
+ if p.client != nil {
+ return p.client, nil
+ }
+
+ prov, err := util.GetConjurProvider(p.store)
+ if err != nil {
+ return nil, err
+ }
+
+ cert, getCertErr := p.getCA(ctx, prov)
+ if getCertErr != nil {
+ return nil, getCertErr
}
- cert := string(certBytes)
config := conjurapi.Config{
- Account: prov.Auth.Apikey.Account,
ApplianceURL: prov.URL,
SSLCert: cert,
}
- conjUser, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.UserRef)
- if secErr != nil {
- return nil, fmt.Errorf(errBadServiceUser, secErr)
- }
- conjAPIKey, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.APIKeyRef)
- if secErr != nil {
- return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
- }
+ if prov.Auth.Apikey != nil {
+ config.Account = prov.Auth.Apikey.Account
+ conjUser, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.UserRef)
+ if secErr != nil {
+ return nil, fmt.Errorf(errBadServiceUser, secErr)
+ }
+ conjAPIKey, secErr := p.secretKeyRef(ctx, prov.Auth.Apikey.APIKeyRef)
+ if secErr != nil {
+ return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
+ }
- conjur, err := conjurapi.NewClientFromKey(config,
- authn.LoginPair{
- Login: conjUser,
- APIKey: conjAPIKey,
- },
- )
+ conjur, newClientFromKeyError := p.clientAPI.NewClientFromKey(config,
+ authn.LoginPair{
+ Login: conjUser,
+ APIKey: conjAPIKey,
+ },
+ )
- if err != nil {
- return nil, fmt.Errorf(errConjurClient, err)
+ if newClientFromKeyError != nil {
+ return nil, fmt.Errorf(errConjurClient, newClientFromKeyError)
+ }
+ p.client = conjur
+ return conjur, nil
+ } else if prov.Auth.Jwt != nil {
+ config.Account = prov.Auth.Jwt.Account
+
+ conjur, clientFromJwtError := p.newClientFromJwt(ctx, config, prov.Auth.Jwt)
+ if clientFromJwtError != nil {
+ return nil, fmt.Errorf(errConjurClient, clientFromJwtError)
+ }
+
+ p.client = conjur
+
+ return conjur, nil
+ } else {
+ // Should not happen because validate func should catch this
+ return nil, fmt.Errorf("no authentication method provided")
}
- p.ConjurClient = conjur
- return p, nil
}
// GetAllSecrets returns all secrets from the provider.
// NOT IMPLEMENTED.
-func (p *Provider) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+func (p *Client) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented
return nil, fmt.Errorf("GetAllSecrets not implemented")
}
// GetSecret returns a single secret from the provider.
-func (p *Provider) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
- secretValue, err := p.ConjurClient.RetrieveSecret(ref.Key)
+func (p *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+ conjurClient, getConjurClientError := p.GetConjurClient(ctx)
+ if getConjurClientError != nil {
+ return nil, getConjurClientError
+ }
+ secretValue, err := conjurClient.RetrieveSecret(ref.Key)
if err != nil {
return nil, err
}
@@ -115,18 +172,18 @@ func (p *Provider) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretData
}
// PushSecret will write a single secret into the provider.
-func (p *Provider) PushSecret(_ context.Context, _ []byte, _ *apiextensionsv1.JSON, _ esv1beta1.PushRemoteRef) error {
+func (p *Client) PushSecret(_ context.Context, _ []byte, _ *apiextensionsv1.JSON, _ esv1beta1.PushRemoteRef) error {
// NOT IMPLEMENTED
return nil
}
-func (p *Provider) DeleteSecret(_ context.Context, _ esv1beta1.PushRemoteRef) error {
+func (p *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushRemoteRef) error {
// NOT IMPLEMENTED
return nil
}
// GetSecretMap returns multiple k/v pairs from the provider.
-func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+func (p *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
// Gets a secret as normal, expecting secret value to be a json object
data, err := p.GetSecret(ctx, ref)
if err != nil {
@@ -149,17 +206,17 @@ func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecre
}
// Close closes the provider.
-func (p *Provider) Close(_ context.Context) error {
+func (p *Client) Close(_ context.Context) error {
return nil
}
// Validate validates the provider.
-func (p *Provider) Validate() (esv1beta1.ValidationResult, error) {
+func (p *Client) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}
// ValidateStore validates the store.
-func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+func (c *Provider) ValidateStore(store esv1beta1.GenericStore) error {
prov, err := util.GetConjurProvider(store)
if err != nil {
return err
@@ -186,8 +243,30 @@ func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
}
}
+ if prov.Auth.Jwt != nil {
+ if prov.Auth.Jwt.Account == "" {
+ return fmt.Errorf("missing Auth.Jwt.Account")
+ }
+ if prov.Auth.Jwt.ServiceID == "" {
+ return fmt.Errorf("missing Auth.Jwt.ServiceID")
+ }
+ if prov.Auth.Jwt.ServiceAccountRef == nil && prov.Auth.Jwt.SecretRef == nil {
+ return fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef")
+ }
+ if prov.Auth.Jwt.SecretRef != nil {
+ if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.Jwt.SecretRef); err != nil {
+ return fmt.Errorf("invalid Auth.Jwt.SecretRef: %w", err)
+ }
+ }
+ if prov.Auth.Jwt.ServiceAccountRef != nil {
+ if err := utils.ValidateReferentServiceAccountSelector(store, *prov.Auth.Jwt.ServiceAccountRef); err != nil {
+ return fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
+ }
+ }
+ }
+
// At least one auth must be configured
- if prov.Auth.Apikey == nil {
+ if prov.Auth.Apikey == nil && prov.Auth.Jwt == nil {
return fmt.Errorf("missing Auth.* configuration")
}
@@ -195,11 +274,11 @@ func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
}
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
-func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+func (c *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
-func (p *Provider) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
+func (p *Client) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
secret := &corev1.Secret{}
ref := client.ObjectKey{
Namespace: p.namespace,
@@ -224,8 +303,71 @@ func (p *Provider) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKey
return valueStr, nil
}
+// configMapKeyRef returns the value of a key in a configmap.
+func (p *Client) configMapKeyRef(ctx context.Context, cmRef *esmeta.SecretKeySelector) (string, error) {
+ configMap := &corev1.ConfigMap{}
+ ref := client.ObjectKey{
+ Namespace: p.namespace,
+ Name: cmRef.Name,
+ }
+ if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
+ (cmRef.Namespace != nil) {
+ ref.Namespace = *cmRef.Namespace
+ }
+ err := p.kube.Get(ctx, ref, configMap)
+ if err != nil {
+ return "", err
+ }
+
+ keyBytes, ok := configMap.Data[cmRef.Key]
+ if !ok {
+ return "", err
+ }
+
+ valueStr := strings.TrimSpace(keyBytes)
+ return valueStr, nil
+}
+
+// getCA try retrieve the CA bundle from the provider CABundle or from the CAProvider.
+func (p *Client) getCA(ctx context.Context, provider *esv1beta1.ConjurProvider) (string, error) {
+ if provider.CAProvider != nil {
+ var ca string
+ var err error
+ switch provider.CAProvider.Type {
+ case esv1beta1.CAProviderTypeConfigMap:
+ keySelector := esmeta.SecretKeySelector{
+ Name: provider.CAProvider.Name,
+ Namespace: provider.CAProvider.Namespace,
+ Key: provider.CAProvider.Key,
+ }
+ ca, err = p.configMapKeyRef(ctx, &keySelector)
+ if err != nil {
+ return "", fmt.Errorf(errUnableToFetchCAProviderCM, err)
+ }
+ case esv1beta1.CAProviderTypeSecret:
+ keySelector := esmeta.SecretKeySelector{
+ Name: provider.CAProvider.Name,
+ Namespace: provider.CAProvider.Namespace,
+ Key: provider.CAProvider.Key,
+ }
+ ca, err = p.secretKeyRef(ctx, &keySelector)
+ if err != nil {
+ return "", fmt.Errorf(errUnableToFetchCAProviderSecret, err)
+ }
+ }
+ return ca, nil
+ }
+ certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(provider.CABundle))
+ if decodeErr != nil {
+ return "", fmt.Errorf(errBadCertBundle, decodeErr)
+ }
+ return string(certBytes), nil
+}
+
func init() {
- esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+ esv1beta1.Register(&Provider{
+ NewConjurProvider: newConjurProvider,
+ }, &esv1beta1.SecretStoreProvider{
Conjur: &esv1beta1.ConjurProvider{},
})
}
diff --git a/pkg/provider/conjur/provider_test.go b/pkg/provider/conjur/provider_test.go
index 1c8ae4da3..1b49efb66 100644
--- a/pkg/provider/conjur/provider_test.go
+++ b/pkg/provider/conjur/provider_test.go
@@ -16,12 +16,26 @@ package conjur
import (
"context"
+ "errors"
"fmt"
+ "reflect"
"testing"
+ "time"
+
+ "github.com/cyberark/conjur-api-go/conjurapi"
+ "github.com/cyberark/conjur-api-go/conjurapi/authn"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/google/go-cmp/cmp"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+ kclient "sigs.k8s.io/controller-runtime/pkg/client"
+ clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
- fakeconjur "github.com/external-secrets/external-secrets/pkg/provider/conjur/fake"
+ "github.com/external-secrets/external-secrets/pkg/provider/conjur/fake"
+ utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
)
var (
@@ -31,39 +45,6 @@ var (
svcAccount = "account1"
)
-type secretManagerTestCase struct {
- err error
- refKey string
-}
-
-func TestConjurGetSecret(t *testing.T) {
- p := Provider{}
- p.ConjurClient = &fakeconjur.ConjurMockClient{}
-
- testCases := []*secretManagerTestCase{
- {
- err: nil,
- refKey: "secret",
- },
- {
- err: fmt.Errorf("error"),
- refKey: "error",
- },
- }
-
- for _, tc := range testCases {
- ref := makeValidRef(tc.refKey)
- _, err := p.GetSecret(context.Background(), *ref)
- if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
- t.Errorf("test failed! want %v, got %v", tc.err, err)
- } else if tc.err == nil && err != nil {
- t.Errorf("want nil got err %v", err)
- } else if tc.err != nil && err == nil {
- t.Errorf("want err %v got nil", tc.err)
- }
- }
-}
-
func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
return &esv1beta1.ExternalSecretDataRemoteRef{
Key: k,
@@ -79,29 +60,59 @@ type ValidateStoreTestCase struct {
func TestValidateStore(t *testing.T) {
testCases := []ValidateStoreTestCase{
{
- store: makeSecretStore(svcURL, svcUser, svcApikey, svcAccount),
+ store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
err: nil,
},
{
- store: makeSecretStore("", svcUser, svcApikey, svcAccount),
+ store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
err: fmt.Errorf("conjur URL cannot be empty"),
},
{
- store: makeSecretStore(svcURL, "", svcApikey, svcAccount),
+ store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
err: fmt.Errorf("missing Auth.Apikey.UserRef"),
},
{
- store: makeSecretStore(svcURL, svcUser, "", svcAccount),
+ store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
err: fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
},
{
- store: makeSecretStore(svcURL, svcUser, svcApikey, ""),
+ store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
err: fmt.Errorf("missing Auth.ApiKey.Account"),
},
+
+ {
+ store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount"),
+ err: nil,
+ },
+ {
+ store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-auth-service", "myconjuraccount"),
+ err: nil,
+ },
+ {
+ store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", ""),
+ err: fmt.Errorf("missing Auth.Jwt.Account"),
+ },
+ {
+ store: makeJWTSecretStore(svcURL, "conjur", "", "", "myconjuraccount"),
+ err: fmt.Errorf("missing Auth.Jwt.ServiceID"),
+ },
+ {
+ store: makeJWTSecretStore("", "conjur", "", "jwt-auth-service", "myconjuraccount"),
+ err: fmt.Errorf("conjur URL cannot be empty"),
+ },
+ {
+ store: makeJWTSecretStore(svcURL, "", "", "jwt-auth-service", "myconjuraccount"),
+ err: fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
+ },
+
+ {
+ store: makeNoAuthSecretStore(svcURL),
+ err: fmt.Errorf("missing Auth.* configuration"),
+ },
}
- p := Provider{}
+ c := Provider{}
for _, tc := range testCases {
- err := p.ValidateStore(tc.store)
+ err := c.ValidateStore(tc.store)
if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
t.Errorf("test failed! want %v, got %v", tc.err, err)
} else if tc.err == nil && err != nil {
@@ -112,7 +123,214 @@ func TestValidateStore(t *testing.T) {
}
}
-func makeSecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1beta1.SecretStore {
+func TestGetSecret(t *testing.T) {
+ type args struct {
+ store esv1beta1.GenericStore
+ kube kclient.Client
+ corev1 typedcorev1.CoreV1Interface
+ namespace string
+ secretPath string
+ }
+
+ type want struct {
+ err error
+ value string
+ }
+
+ type testCase struct {
+ reason string
+ args args
+ want want
+ }
+
+ cases := map[string]testCase{
+ "ApiKeyReadSecretSuccess": {
+ reason: "Should read a secret successfully using an ApiKey auth secret store.",
+ args: args{
+ store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+ kube: clientfake.NewClientBuilder().
+ WithObjects(makeFakeAPIKeySecrets()...).Build(),
+ namespace: "default",
+ secretPath: "path/to/secret",
+ },
+ want: want{
+ err: nil,
+ value: "secret",
+ },
+ },
+ "ApiKeyReadSecretFailure": {
+ reason: "Should fail to read secret using ApiKey auth secret store.",
+ args: args{
+ store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+ kube: clientfake.NewClientBuilder().
+ WithObjects(makeFakeAPIKeySecrets()...).Build(),
+ namespace: "default",
+ secretPath: "error",
+ },
+ want: want{
+ err: errors.New("error"),
+ value: "",
+ },
+ },
+ "JwtWithServiceAccountRefReadSecretSuccess": {
+ reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
+ args: args{
+ store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
+ kube: clientfake.NewClientBuilder().
+ WithObjects().Build(),
+ namespace: "default",
+ secretPath: "path/to/secret",
+ corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+ },
+ want: want{
+ err: nil,
+ value: "secret",
+ },
+ },
+ "JwtWithSecretRefReadSecretSuccess": {
+ reason: "Should read a secret successfully using an JWT auth secret store that references a k8s secret.",
+ args: args{
+ store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-authenticator", "myconjuraccount"),
+ kube: clientfake.NewClientBuilder().
+ WithObjects(&corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "jwt-secret",
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "token": []byte(createFakeJwtToken(true)),
+ },
+ }).Build(),
+ namespace: "default",
+ secretPath: "path/to/secret",
+ },
+ want: want{
+ err: nil,
+ value: "secret",
+ },
+ },
+ "JwtWithCABundleSuccess": {
+ reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
+ args: args{
+ store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
+ kube: clientfake.NewClientBuilder().
+ WithObjects().Build(),
+ namespace: "default",
+ secretPath: "path/to/secret",
+ corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+ },
+ want: want{
+ err: nil,
+ value: "secret",
+ },
+ },
+ }
+
+ runTest := func(t *testing.T, _ string, tc testCase) {
+ provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
+ ref := makeValidRef(tc.args.secretPath)
+ secret, err := provider.GetSecret(context.Background(), *ref)
+ if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nconjur.GetSecret(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ secretString := string(secret)
+ if secretString != tc.want.value {
+ t.Errorf("\n%s\nconjur.GetSecret(...): want value %v got %v", tc.reason, tc.want.value, secretString)
+ }
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ runTest(t, name, tc)
+ })
+ }
+}
+
+func TestGetCA(t *testing.T) {
+ type args struct {
+ store esv1beta1.GenericStore
+ kube kclient.Client
+ corev1 typedcorev1.CoreV1Interface
+ namespace string
+ }
+
+ type want struct {
+ err error
+ cert string
+ }
+
+ type testCase struct {
+ reason string
+ args args
+ want want
+ }
+
+ certData := "mycertdata"
+ certDataEncoded := "bXljZXJ0ZGF0YQo="
+
+ cases := map[string]testCase{
+ "UseCABundleSuccess": {
+ reason: "Should read a caBundle successfully.",
+ args: args{
+ store: makeStoreWithCA("cabundle", certDataEncoded),
+ kube: clientfake.NewClientBuilder().
+ WithObjects().Build(),
+ namespace: "default",
+ corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+ },
+ want: want{
+ err: nil,
+ cert: certDataEncoded,
+ },
+ },
+ "UseCAProviderConfigMapSuccess": {
+ reason: "Should read a ca from a ConfigMap successfully.",
+ args: args{
+ store: makeStoreWithCA("configmap", ""),
+ kube: clientfake.NewClientBuilder().
+ WithObjects(makeFakeCASource("configmap", certData)).Build(),
+ namespace: "default",
+ corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+ },
+ want: want{
+ err: nil,
+ cert: certDataEncoded,
+ },
+ },
+ "UseCAProviderSecretSuccess": {
+ reason: "Should read a ca from a Secret successfully.",
+ args: args{
+ store: makeStoreWithCA("secret", ""),
+ kube: clientfake.NewClientBuilder().
+ WithObjects(makeFakeCASource("secret", certData)).Build(),
+ namespace: "default",
+ corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
+ },
+ want: want{
+ err: nil,
+ cert: certDataEncoded,
+ },
+ },
+ }
+
+ runTest := func(t *testing.T, _ string, tc testCase) {
+ provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
+ _, err := provider.GetSecret(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "path/to/secret",
+ })
+ if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nconjur.GetCA(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ runTest(t, name, tc)
+ })
+ }
+}
+
+func makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1beta1.SecretStore {
uref := &esmeta.SecretKeySelector{
Name: "user",
Key: "conjur-hostid",
@@ -145,3 +363,169 @@ func makeSecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1beta1.S
}
return store
}
+
+func makeJWTSecretStore(svcURL, serviceAccountName, secretName, jwtServiceID, conjurAccount string) *esv1beta1.SecretStore {
+ serviceAccountRef := &esmeta.ServiceAccountSelector{
+ Name: serviceAccountName,
+ Audiences: []string{"conjur"},
+ }
+ if serviceAccountName == "" {
+ serviceAccountRef = nil
+ }
+
+ secretRef := &esmeta.SecretKeySelector{
+ Name: secretName,
+ Key: "token",
+ }
+ if secretName == "" {
+ secretRef = nil
+ }
+
+ store := &esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Conjur: &esv1beta1.ConjurProvider{
+ URL: svcURL,
+ Auth: esv1beta1.ConjurAuth{
+ Jwt: &esv1beta1.ConjurJWT{
+ Account: conjurAccount,
+ ServiceID: jwtServiceID,
+ ServiceAccountRef: serviceAccountRef,
+ SecretRef: secretRef,
+ },
+ },
+ },
+ },
+ },
+ }
+ return store
+}
+
+func makeStoreWithCA(caSource, caData string) *esv1beta1.SecretStore {
+ store := makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount")
+ if caSource == "secret" {
+ store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
+ Type: esv1beta1.CAProviderTypeSecret,
+ Name: "conjur-cert",
+ Key: "ca",
+ }
+ } else if caSource == "configmap" {
+ store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
+ Type: esv1beta1.CAProviderTypeConfigMap,
+ Name: "conjur-cert",
+ Key: "ca",
+ }
+ } else {
+ store.Spec.Provider.Conjur.CABundle = caData
+ }
+ return store
+}
+
+func makeNoAuthSecretStore(svcURL string) *esv1beta1.SecretStore {
+ store := &esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Conjur: &esv1beta1.ConjurProvider{
+ URL: svcURL,
+ },
+ },
+ },
+ }
+ return store
+}
+
+func makeFakeAPIKeySecrets() []kclient.Object {
+ return []kclient.Object{
+ &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "user",
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "conjur-hostid": []byte("myhostid"),
+ },
+ },
+ &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "apikey",
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "conjur-apikey": []byte("apikey"),
+ },
+ },
+ }
+}
+
+func makeFakeCASource(kind, caData string) kclient.Object {
+ if kind == "secret" {
+ return &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "conjur-cert",
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "conjur-cert": []byte(caData),
+ },
+ }
+ }
+ return &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "conjur-cert",
+ Namespace: "default",
+ },
+ Data: map[string]string{
+ "ca": caData,
+ },
+ }
+}
+
+func createFakeJwtToken(expires bool) string {
+ signingKey := []byte("fakekey")
+ token := jwt.New(jwt.SigningMethodHS256)
+ claims := token.Claims.(jwt.MapClaims)
+ if expires {
+ claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
+ }
+ jwtTokenString, err := token.SignedString(signingKey)
+ if err != nil {
+ panic(err)
+ }
+ return jwtTokenString
+}
+
+// ConjurMockAPIClient is a mock implementation of the ApiClient interface.
+type ConjurMockAPIClient struct {
+}
+
+func (c *ConjurMockAPIClient) NewClientFromKey(_ conjurapi.Config, _ authn.LoginPair) (SecretsClient, error) {
+ return &fake.ConjurMockClient{}, nil
+}
+
+func (c *ConjurMockAPIClient) NewClientFromJWT(_ conjurapi.Config, _, _ string) (SecretsClient, error) {
+ return &fake.ConjurMockClient{}, nil
+}
+
+// EquateErrors returns true if the supplied errors are of the same type and
+// produce identical strings. This mirrors the error comparison behavior of
+// https://github.com/go-test/deep, which most Crossplane tests targeted before
+// we switched to go-cmp.
+//
+// This differs from cmpopts.EquateErrors, which does not test for error strings
+// and instead returns whether one error 'is' (in the errors.Is sense) the
+// other.
+func EquateErrors() cmp.Option {
+ return cmp.Comparer(func(a, b error) bool {
+ if a == nil || b == nil {
+ return a == nil && b == nil
+ }
+
+ av := reflect.ValueOf(a)
+ bv := reflect.ValueOf(b)
+ if av.Type() != bv.Type() {
+ return false
+ }
+
+ return a.Error() == b.Error()
+ })
+}