1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

Conjur JWT support (#2591)

* Add JWT Auth to Conjur Provider

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

* Update docs for Cyberark Conjur Provider

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

* Update test suite to cover new functionality

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

* Run make reviewable

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

* Set MinVersion for tls.Config to satisfy linting

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

* Move ca bundle config example to a yaml snippet

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

* fix: consolidate naming

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

* fix: consolidate naming

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

* docs: make it a working example

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

* Remove JWT expiration handling logic

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

* Run make fmt

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

---------

Signed-off-by: Kieran Bristow <kieran.bristow@absa.africa>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
This commit is contained in:
Kieran Bristow 2023-09-25 10:05:17 +02:00 committed by GitHub
parent 719e8b1c82
commit d9eaeb40dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1400 additions and 120 deletions

View file

@ -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"`
}

View file

@ -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)
}

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

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

View file

@ -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 <https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret> for various methods to create secrets. Here is one way to do it using `kubectl`
#### Create Kubernetes Secrets
In order for the ESO **Conjur** provider to connect to the Conjur server using the `apikey` creds, these creds should be stored as k8s secrets. Please refer to <https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret> for various methods to create secrets. Here is one way to do it using `kubectl`
***NOTE***: "conjur-creds" is the "name" used in "userRef" and "apikeyRef" in the conjur-secret-store definition
@ -46,6 +46,53 @@ kubectl -n external-secrets create secret generic conjur-creds --from-literal=ho
# kubectl -n external-secrets create secret generic conjur-creds --from-literal=hostid=host/data/app1/host001 --from-literal=apikey=321blahblah
```
### External Secret Store with JWT Authentication
This method uses JWT tokens to authenticate with Conjur. The following methods for retrieving the JWT token for authentication are supported:
- JWT token from a referenced Kubernetes Service Account
- JWT token stored in a Kubernetes secret
#### Create External Secret Store Definition
When using JWT authentication the following must be specified in the `SecretStore`:
- `account` - The name of the Conjur account
- `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that will be used to authenticate the JWT token
You can then choose to either retrieve the JWT token using a Service Account reference or from a Kubernetes Secret.
To use a JWT token from a referenced Kubernetes Service Account, the following secret store definition can be used:
```yaml
{% include 'conjur-secret-store-jwt-service-account-ref.yaml' %}
```
This is only supported in Kubernetes 1.22 and above as it uses the [TokenRequest API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-request-v1/) to get the JWT token from the referenced service account. Audiences can be set as required by the [Conjur JWT authenticator](https://docs.conjur.org/Latest/en/Content/Integrations/k8s-ocp/k8s-jwt-authn.htm).
Alternatively, a secret containing a valid JWT token can be referenced as follows:
```yaml
{% include 'conjur-secret-store-jwt-secret-ref.yaml' %}
```
This secret must contain a JWT token that identifies your Conjur host. The secret must contain a JWT token consumable by a configured Conjur JWT authenticator and must satisfy all [Conjur JWT guidelines](https://docs.conjur.org/Latest/en/Content/Operations/Services/cjr-authn-jwt-guidelines.htm#Best). This can be a JWT created by an external JWT issuer or the Kubernetes api server itself. Such a with Kubernetes Service Account token can be created using the below command:
```shell
kubectl create token my-service-account --audience='https://conjur.company.com' --duration=3600s
```
Save the `SecretStore` definition as filename `conjur-secret-store.yaml` as referenced in later steps.
### Create External Secret Definition
Important note: **Creds must live in the same namespace as a SecretStore - the secret store may only reference secrets from the same namespace.** When using a ClusterSecretStore this limitation is lifted and the creds can live in any namespace.
Recommend to save as filename: `conjur-external-secret.yaml`
```yaml
{% include 'conjur-external-secret.yaml' %}
```
### Create the External Secrets Store
```shell

View file

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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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))
}

View file

@ -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{},
})
}

View file

@ -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()
})
}