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

Add Conjur Support for FindByName, FindByTag (#3364)

This commit is contained in:
Shlomo Zalman Heigh 2024-04-28 13:01:00 -04:00 committed by GitHub
parent a85de0d1da
commit 02c6f625bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1121 additions and 454 deletions

View file

@ -51,7 +51,7 @@ The following table describes the stability level of each provider and who's res
| [Doppler SecretOps Platform](https://external-secrets.io/latest/provider/doppler) | alpha | [@ryan-blunden](https://github.com/ryan-blunden/) [@nmanoogian](https://github.com/nmanoogian/) |
| [Keeper Security](https://www.keepersecurity.com/) | alpha | [@ppodevlab](https://github.com/ppodevlab) |
| [Scaleway](https://external-secrets.io/latest/provider/scaleway) | alpha | [@azert9](https://github.com/azert9/) |
| [Conjur](https://external-secrets.io/latest/provider/conjur) | alpha | [@davidh-cyberark](https://github.com/davidh-cyberark/) |
| [Conjur](https://external-secrets.io/latest/provider/conjur) | stable | [@davidh-cyberark](https://github.com/davidh-cyberark/) [@szh](https://github.com/szh) |
| [Delinea](https://external-secrets.io/latest/provider/delinea) | alpha | [@michaelsauter](https://github.com/michaelsauter/) |
| [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi) | alpha | [@dirien](https://github.com/dirien) |
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
@ -80,7 +80,7 @@ The following table show the support for features across different providers.
| Doppler | x | | | | x | | |
| Keeper Security | x | | | | x | x | |
| Scaleway | x | x | | | x | x | x |
| Conjur | | | | | x | | |
| Conjur | x | x | | | x | | |
| Delinea | x | | | | x | | |
| Pulumi ESC | x | | | | x | | |
| Passbolt | x | | | | x | | |

View file

@ -6,12 +6,12 @@ This section describes how to set up the Conjur provider for External Secrets Op
Before installing the Conjur provider, you need:
* A running Conjur Server, with:
* An accessible Conjur endpoint (for example: `https://myapi.example.com`).
* Your configured Conjur authentication info (such as `hostid`, `apikey`, or JWT service ID). For more information on configuring Conjur, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
* Support for your authentication method (`apikey` is supported by default, `jwt` requires additional configuration).
* **Optional**: Conjur server certificate (see [below](#conjur-server-certificate)).
* A Kubernetes cluster with ESO installed.
* A running Conjur Server, with:
* An accessible Conjur endpoint (for example: `https://myapi.example.com`).
* Your configured Conjur authentication info (such as `hostid`, `apikey`, or JWT service ID). For more information on configuring Conjur, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
* Support for your authentication method (`apikey` is supported by default, `jwt` requires additional configuration).
* **Optional**: Conjur server certificate (see [below](#conjur-server-certificate)).
* A Kubernetes cluster with ESO installed.
### Conjur server certificate
@ -21,11 +21,18 @@ If you set up your Conjur server with a self-signed certificate, we recommend th
{% include 'conjur-ca-bundle.yaml' %}
```
### External secret store with apiKey authentication
### External secret store
The Conjur provider is configured as an external secret store in ESO. The Conjur provider supports these two methods to authenticate to Conjur:
* [`apikey`](#option-1-external-secret-store-with-apikey-authentication): uses a Conjur `hostid` and `apikey` to authenticate with Conjur
* [`jwt`](#option-2-external-secret-store-with-jwt-authentication): uses a JWT to authenticate with Conjur
#### Option 1: External secret store with apiKey authentication
This method uses a Conjur `hostid` and `apikey` to authenticate with Conjur. It is the simplest method to set up and use because your Conjur instance requires no additional configuration.
#### Step 1: Create an external secret store
##### Step 1: Define an external secret store
!!! Tip
Save as the file as: `conjur-secret-store.yaml`
@ -34,7 +41,7 @@ This method uses a Conjur `hostid` and `apikey` to authenticate with Conjur. It
{% include 'conjur-secret-store-apikey.yaml' %}
```
#### Step 2: Create Kubernetes secrets
##### Step 2: Create Kubernetes secrets for Conjur credentials
To connect to the Conjur server, the **ESO Conjur provider** needs to retrieve the `apikey` credentials from K8s secrets.
@ -54,19 +61,36 @@ kubectl -n external-secrets create secret generic conjur-creds --from-literal=ho
!!! Note
`conjur-creds` is the `name` defined in the `userRef` and `apikeyRef` fields of the `conjur-secret-store.yml` file.
### External secret store with JWT authentication
##### Step 3: Create the external secrets store
!!! Important
Unless you are using a [ClusterSecretStore](../api/clustersecretstore.md), credentials must reside in the same namespace as the SecretStore.
```shell
# WARNING: creates the store in the "external-secrets" namespace, update the value as needed
#
kubectl apply -n external-secrets -f conjur-secret-store.yaml
# WARNING: running the delete command will delete the secret store configuration
#
# If there is a need to delete the external secretstore
# kubectl delete secretstore -n external-secrets conjur
```
#### Option 2: External secret store with JWT authentication
This method uses JWT tokens to authenticate with Conjur. You can use the following methods to retrieve a JWT token for authentication:
- JWT token from a referenced Kubernetes service account
- JWT token stored in a Kubernetes secret
* JWT token from a referenced Kubernetes service account
* JWT token stored in a Kubernetes secret
#### Step 1: Define an external secret store
##### Step 1: Define an external secret store
When you use 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 is used to authenticate the JWT token
* `account` - The name of the Conjur account
* `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that is used to authenticate the JWT token
You can retrieve the JWT token from either a referenced service account or a Kubernetes secret.
@ -93,20 +117,9 @@ You can use an external JWT issuer or the Kubernetes API server to create the to
kubectl create token my-service-account --audience='https://conjur.company.com' --duration=3600s
```
Save the secret store file as `conjur-secret-store.yaml` (the filename used in subsequent steps).
Save the secret store file as `conjur-secret-store.yaml`.
#### Step 2: Define an external secret
Save the external secret file as: `conjur-external-secret.yaml`
```yaml
{% include 'conjur-external-secret.yaml' %}
```
!!!Important
Unless you are using a [ClusterSecretStore](../api/clustersecretstore.md), credentials must reside in the same namespace as the SecretStore.
#### Step 3: Create the external secrets store
##### Step 2: Create the external secrets store
```shell
# WARNING: creates the store in the "external-secrets" namespace, update the value as needed
@ -119,7 +132,32 @@ kubectl apply -n external-secrets -f conjur-secret-store.yaml
# kubectl delete secretstore -n external-secrets conjur
```
#### Step 4: Create the external secret
### Define an external secret
After you have configured the Conjur provider secret store, you can fetch secrets from Conjur.
Here is an example of how to fetch a single secret from Conjur:
```yaml
{% include 'conjur-external-secret.yaml' %}
```
Save the external secret file as `conjur-external-secret.yaml`.
#### Find by Name and Find by Tag
The Conjur provider also supports the Find by Name and Find by Tag ESO features. This means that
you can use a regular expression or tags to dynamically fetch multiple secrets from Conjur.
```yaml
{% include 'conjur-external-secret-find.yaml' %}
```
If you use these features, we strongly recommend that you limit the permissions of the Conjur host
to only the secrets that it needs to access. This is more secure and it reduces the load on
both the Conjur server and ESO.
### Create the external secret
```shell
# WARNING: creates the external-secret in the "external-secrets" namespace, update the value as needed
@ -132,7 +170,7 @@ kubectl apply -n external-secrets -f conjur-external-secret.yaml
# kubectl delete externalsecret -n external-secrets conjur
```
#### Step 5: Get the K8s secret
### Get the K8s secret
* Log in to your Conjur server and verify that your secret exists
* Review the value of your Kubernetes secret to verify that it contains the same value as the Conjur server
@ -149,10 +187,9 @@ kubectl get secret -n external-secrets conjur -o jsonpath="{.data.secret00}" |
* [Accelerator-K8s-External-Secrets repo](https://github.com/conjurdemos/Accelerator-K8s-External-Secrets)
* [Configure Conjur JWT authentication](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Services/cjr-authn-jwt-guidelines.htm)
### License
Copyright (c) 2023 CyberArk Software Ltd. All rights reserved.
Copyright (c) 2023-2024 CyberArk Software Ltd. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -0,0 +1,22 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: conjur-find-by-name
spec:
refreshInterval: 10s
secretStoreRef:
# This name must match the metadata.name in the `SecretStore`
name: conjur
kind: SecretStore
target:
name: k8s-secret-to-be-created
dataFrom:
- find:
# You can use *either* `name` or `tags` to filter the secrets. Here are basic examples of both:
name:
# Match all secrets in the app1 namespace (e.g., `app1/secret00`, `app1/secret01`, etc.)
regexp: "^app1\/.+$"
tags:
# Only fetch Conjur secrets with the following annotations
environment: "prod"
application: "app1"

View file

@ -1,8 +1,6 @@
module github.com/external-secrets/external-secrets-e2e
go 1.22
toolchain go1.22.1
go 1.22.1
replace github.com/external-secrets/external-secrets => ../

View file

@ -32,20 +32,40 @@ var _ = Describe("[conjur]", Label("conjur"), func() {
DescribeTable("sync secrets",
framework.TableFuncWithExternalSecret(f, prov),
// uses token auth
// use api key auth
framework.Compose(withTokenAuth, f, common.FindByName, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.FindByNameAndRewrite, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.FindByTag, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.SimpleDataSync, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.SyncWithoutTargetName, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.JSONDataFromRewrite, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.JSONDataWithTemplateFromLiteral, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.TemplateFromConfigmaps, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.SSHKeySync, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.SSHKeySyncDataProperty, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.DockerJSONConfig, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.NestedJSONWithGJSON, useApiKeyAuth),
framework.Compose(withTokenAuth, f, common.SyncV1Alpha1, useApiKeyAuth),
// use jwt k8s provider
framework.Compose(withJWTK8s, f, common.FindByName, useJWTK8sProvider),
framework.Compose(withJWTK8s, f, common.FindByNameAndRewrite, useJWTK8sProvider),
framework.Compose(withJWTK8s, f, common.FindByTag, useJWTK8sProvider),
framework.Compose(withJWTK8s, f, common.SimpleDataSync, useJWTK8sProvider),
framework.Compose(withJWTK8s, f, common.SyncWithoutTargetName, useJWTK8sProvider),
framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
framework.Compose(withJWTK8s, f, common.JSONDataFromRewrite, useJWTK8sProvider),
// use jwt k8s hostid provider
framework.Compose(withJWTK8sHostID, f, common.FindByName, useJWTK8sHostIDProvider),
framework.Compose(withJWTK8sHostID, f, common.FindByNameAndRewrite, useJWTK8sHostIDProvider),
framework.Compose(withJWTK8sHostID, f, common.FindByTag, useJWTK8sHostIDProvider),
framework.Compose(withJWTK8sHostID, f, common.SimpleDataSync, useJWTK8sHostIDProvider),
framework.Compose(withJWTK8sHostID, f, common.SyncWithoutTargetName, useJWTK8sHostIDProvider),
framework.Compose(withJWTK8sHostID, f, common.JSONDataFromSync, useJWTK8sHostIDProvider),

View file

@ -20,6 +20,12 @@ import (
const createVariablePolicyTemplate = `- !variable
id: {{ .Key }}
{{ if .Tags }}
annotations:
{{- range $key, $value := .Tags }}
{{ $key }}: "{{ $value }}"
{{- end }}
{{ end }}
- !permit
role: !host system:serviceaccount:{{ .Namespace }}:test-app-sa
@ -44,27 +50,28 @@ const jwtHostPolicyTemplate = `- !host
privilege: [ read, authenticate ]
resource: !webservice conjur/authn-jwt/{{ .ServiceID }}`
func createVariablePolicy(key, namespace string) string {
return renderTemplate(createVariablePolicyTemplate, map[string]string{
func createVariablePolicy(key, namespace string, tags map[string]string) string {
return renderTemplate(createVariablePolicyTemplate, map[string]interface{}{
"Key": key,
"Namespace": namespace,
"Tags": tags,
})
}
func deleteVariablePolicy(key string) string {
return renderTemplate(deleteVariablePolicyTemplate, map[string]string{
return renderTemplate(deleteVariablePolicyTemplate, map[string]interface{}{
"Key": key,
})
}
func createJwtHostPolicy(hostID, serviceID string) string {
return renderTemplate(jwtHostPolicyTemplate, map[string]string{
return renderTemplate(jwtHostPolicyTemplate, map[string]interface{}{
"HostID": hostID,
"ServiceID": serviceID,
})
}
func renderTemplate(templateText string, data map[string]string) string {
func renderTemplate(templateText string, data map[string]interface{}) string {
// Use golang templates to render the policy
tmpl, err := template.New("policy").Parse(templateText)
if err != nil {

View file

@ -49,13 +49,12 @@ func newConjurProvider(f *framework.Framework) *conjurProvider {
framework: f,
}
BeforeEach(prov.BeforeEach)
AfterEach(prov.AfterEach)
return prov
}
func (s *conjurProvider) CreateSecret(key string, val framework.SecretEntry) {
// Generate a policy file for the secret key
policy := createVariablePolicy(key, s.framework.Namespace.Name)
policy := createVariablePolicy(key, s.framework.Namespace.Name, val.Tags)
_, err := s.client.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
Expect(err).ToNot(HaveOccurred())
@ -84,31 +83,6 @@ func (s *conjurProvider) BeforeEach() {
s.CreateJWTK8sHostIDStore(c, ns)
}
func (s *conjurProvider) AfterEach() {
// Print Conjur logs if the test failed
if !CurrentGinkgoTestDescription().Failed {
return
}
// Get logs from Conjur pod
ns := s.framework.Namespace.Name
pods, err := s.framework.KubeClientSet.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{})
if err != nil {
GinkgoWriter.Printf("Error getting pods: %s\n", err)
return
}
for _, pod := range pods.Items {
if strings.Contains(pod.Name, "conjur-oss") {
logs, err := s.framework.KubeClientSet.CoreV1().Pods(ns).GetLogs(pod.Name, &v1.PodLogOptions{Container: "conjur-oss"}).DoRaw(context.Background())
if err != nil {
GinkgoWriter.Printf("Error getting logs from Conjur pod: %s\n", err)
}
GinkgoWriter.Printf("Conjur logs:\n%s\n", logs)
}
}
}
func makeStore(name, ns string, c *addon.Conjur) *esv1beta1.SecretStore {
return &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{

2
go.mod
View file

@ -1,6 +1,6 @@
module github.com/external-secrets/external-secrets
go 1.22
go 1.22.1
require (
cloud.google.com/go/iam v1.1.7

View file

@ -34,10 +34,10 @@ import (
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) {
func (c *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)
jwtToken, err := c.getJwtFromServiceAccountTokenRequest(ctx, *conjurJWTConfig.ServiceAccountRef, nil, JwtLifespan)
if err != nil {
return "", err
}
@ -50,9 +50,9 @@ func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.Con
}
jwtToken, err := resolvers.SecretKeyRef(
ctx,
p.kube,
p.StoreKind,
p.namespace,
c.kube,
c.StoreKind,
c.namespace,
tokenRef)
if err != nil {
return "", err
@ -63,25 +63,25 @@ func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.Con
}
// 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) {
func (c *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,
Namespace: c.namespace,
},
Spec: authenticationv1.TokenRequestSpec{
Audiences: audiences,
ExpirationSeconds: &expirationSeconds,
},
}
if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
if (c.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
(serviceAccountRef.Namespace != nil) {
tokenRequest.Namespace = *serviceAccountRef.Namespace
}
tokenResponse, err := p.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
tokenResponse, err := c.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
if err != nil {
return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err)
}
@ -89,13 +89,13 @@ func (p *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, servi
}
// 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)
func (c *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config, jwtAuth *esv1beta1.ConjurJWT) (SecretsClient, error) {
jwtToken, getJWTError := c.getJWTToken(ctx, jwtAuth)
if getJWTError != nil {
return nil, getJWTError
}
client, clientError := p.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID, jwtAuth.HostID)
client, clientError := c.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID, jwtAuth.HostID)
if clientError != nil {
return nil, clientError
}

View file

@ -0,0 +1,219 @@
/*
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"
"fmt"
"strings"
"github.com/cyberark/conjur-api-go/conjurapi"
"github.com/cyberark/conjur-api-go/conjurapi/authn"
corev1 "k8s.io/api/core/v1"
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/conjur/util"
"github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
)
var (
errConjurClient = "cannot setup new Conjur client: %w"
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"
errSecretKeyFmt = "cannot find secret data for key: %q"
)
// 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
}
func (c *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
// if the client is initialized already, return it
if c.client != nil {
return c.client, nil
}
prov, err := util.GetConjurProvider(c.store)
if err != nil {
return nil, err
}
cert, getCertErr := c.getCA(ctx, prov)
if getCertErr != nil {
return nil, getCertErr
}
config := conjurapi.Config{
ApplianceURL: prov.URL,
SSLCert: cert,
}
if prov.Auth.APIKey != nil {
config.Account = prov.Auth.APIKey.Account
conjUser, secErr := resolvers.SecretKeyRef(
ctx,
c.kube,
c.StoreKind,
c.namespace, prov.Auth.APIKey.UserRef)
if secErr != nil {
return nil, fmt.Errorf(errBadServiceUser, secErr)
}
conjAPIKey, secErr := resolvers.SecretKeyRef(
ctx,
c.kube,
c.StoreKind,
c.namespace,
prov.Auth.APIKey.APIKeyRef)
if secErr != nil {
return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
}
conjur, newClientFromKeyError := c.clientAPI.NewClientFromKey(config,
authn.LoginPair{
Login: conjUser,
APIKey: conjAPIKey,
},
)
if newClientFromKeyError != nil {
return nil, fmt.Errorf(errConjurClient, newClientFromKeyError)
}
c.client = conjur
return conjur, nil
} else if prov.Auth.Jwt != nil {
config.Account = prov.Auth.Jwt.Account
conjur, clientFromJwtError := c.newClientFromJwt(ctx, config, prov.Auth.Jwt)
if clientFromJwtError != nil {
return nil, fmt.Errorf(errConjurClient, clientFromJwtError)
}
c.client = conjur
return conjur, nil
} else {
// Should not happen because validate func should catch this
return nil, fmt.Errorf("no authentication method provided")
}
}
// PushSecret will write a single secret into the provider.
func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
// NOT IMPLEMENTED
return nil
}
func (c *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
// NOT IMPLEMENTED
return nil
}
func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
return false, fmt.Errorf("not implemented")
}
// Validate validates the provider.
func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}
// Close closes the provider.
func (c *Client) Close(_ context.Context) error {
return nil
}
// configMapKeyRef returns the value of a key in a ConfigMap.
func (c *Client) configMapKeyRef(ctx context.Context, cmRef *esmeta.SecretKeySelector) (string, error) {
configMap := &corev1.ConfigMap{}
ref := client.ObjectKey{
Namespace: c.namespace,
Name: cmRef.Name,
}
if (c.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
(cmRef.Namespace != nil) {
ref.Namespace = *cmRef.Namespace
}
err := c.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 (c *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 = c.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 = resolvers.SecretKeyRef(
ctx,
c.kube,
c.StoreKind,
c.namespace,
&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
}

View file

@ -0,0 +1,223 @@
/*
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"
"encoding/json"
"fmt"
"strings"
"github.com/cyberark/conjur-api-go/conjurapi"
"github.com/tidwall/gjson"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/find"
)
type conjurResource map[string]interface{}
// resourceFilterFunc is a function that filters resources.
// It takes a resource as input and returns the name of the resource if it should be included.
// If the resource should not be included, it returns an empty string.
// If an error occurs, it returns an empty string and the error.
type resourceFilterFunc func(candidate conjurResource) (name string, err error)
// GetSecret returns a single secret from the provider.
func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
conjurClient, getConjurClientError := c.GetConjurClient(ctx)
if getConjurClientError != nil {
return nil, getConjurClientError
}
secretValue, err := conjurClient.RetrieveSecret(ref.Key)
if err != nil {
return nil, err
}
// If no property is specified, return the secret value as is
if ref.Property == "" {
return secretValue, nil
}
// If a property is specified, parse the secret value as JSON and return the property value
val := gjson.Get(string(secretValue), ref.Property)
if !val.Exists() {
return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
}
return []byte(val.String()), nil
}
// GetSecretMap returns multiple k/v pairs from the provider.
func (c *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 := c.GetSecret(ctx, ref)
if err != nil {
return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
}
// Maps the json data to a string:string map
kv := make(map[string]string)
err = json.Unmarshal(data, &kv)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
}
// Converts values in K:V pairs into bytes, while leaving keys as strings
secretData := make(map[string][]byte)
for k, v := range kv {
secretData[k] = []byte(v)
}
return secretData, nil
}
// GetAllSecrets gets multiple secrets from the provider and loads into a kubernetes secret.
// First load all secrets from secretStore path configuration
// Then, gets secrets from a matching name or matching custom_metadata.
func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Name != nil {
return c.findSecretsFromName(ctx, *ref.Name)
}
return c.findSecretsFromTags(ctx, ref.Tags)
}
func (c *Client) findSecretsFromName(ctx context.Context, ref esv1beta1.FindName) (map[string][]byte, error) {
matcher, err := find.New(ref)
if err != nil {
return nil, err
}
var resourceFilterFunc = func(candidate conjurResource) (string, error) {
name := trimConjurResourceName(candidate["id"].(string))
isMatch := matcher.MatchName(name)
if !isMatch {
return "", nil
}
return name, nil
}
return c.listSecrets(ctx, resourceFilterFunc)
}
func (c *Client) findSecretsFromTags(ctx context.Context, tags map[string]string) (map[string][]byte, error) {
var resourceFilterFunc = func(candidate conjurResource) (string, error) {
name := trimConjurResourceName(candidate["id"].(string))
annotations, ok := candidate["annotations"].([]interface{})
if !ok {
// No annotations, skip
return "", nil
}
formattedAnnotations, err := formatAnnotations(annotations)
if err != nil {
return "", err
}
// Check if all tags match
for tk, tv := range tags {
p, ok := formattedAnnotations[tk]
if !ok || p != tv {
return "", nil
}
}
return name, nil
}
return c.listSecrets(ctx, resourceFilterFunc)
}
func (c *Client) listSecrets(ctx context.Context, filterFunc resourceFilterFunc) (map[string][]byte, error) {
conjurClient, getConjurClientError := c.GetConjurClient(ctx)
if getConjurClientError != nil {
return nil, getConjurClientError
}
filteredResourceNames := []string{}
// Loop through all secrets in the Conjur account.
// Ideally this will be only a small list, but we need to handle pagination in the
// case that there are a lot of secrets. To limit load on Conjur and memory usage
// in ESO, we will only load 100 secrets at a time. We will then filter these secrets,
// discarding any that do not match the filterFunc. We will then repeat this process
// until we have loaded all secrets.
for offset := 0; ; offset += 100 {
resFilter := &conjurapi.ResourceFilter{
Kind: "variable",
Limit: 100,
Offset: offset,
}
resources, err := conjurClient.Resources(resFilter)
if err != nil {
return nil, err
}
for _, candidate := range resources {
name, err := filterFunc(candidate)
if err != nil {
return nil, err
}
if name != "" {
filteredResourceNames = append(filteredResourceNames, name)
}
}
// If we have less than 100 resources, we reached the last page
if len(resources) < 100 {
break
}
}
filteredResources, err := c.client.RetrieveBatchSecrets(filteredResourceNames)
if err != nil {
return nil, err
}
// Trim the resource names to just the last part of the ID
return trimConjurResourceNames(filteredResources), nil
}
// trimConjurResourceNames trims the Conjur resource names to the last part of the ID.
// It iterates over a map of secrets and returns a new map with the trimmed names.
func trimConjurResourceNames(resources map[string][]byte) map[string][]byte {
trimmedResources := make(map[string][]byte)
for k, v := range resources {
trimmedResources[trimConjurResourceName(k)] = v
}
return trimmedResources
}
// trimConjurResourceName trims the Conjur resource name to the last part of the ID.
// For example, if the ID is "account:variable:secret", the function will return
// "secret".
func trimConjurResourceName(id string) string {
tokens := strings.SplitN(id, ":", 3)
return tokens[len(tokens)-1]
}
// Convert annotations from objects with "name", "policy", "value" keys (as returned by the Conjur API)
// to a key/value map for easier comparison in code.
func formatAnnotations(annotations []interface{}) (map[string]string, error) {
formattedAnnotations := make(map[string]string)
for _, annot := range annotations {
annot, ok := annot.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("could not parse annotation: %v", annot)
}
name := annot["name"].(string)
value := annot["value"].(string)
formattedAnnotations[name] = value
}
return formattedAnnotations, nil
}

View file

@ -29,6 +29,8 @@ import (
// SecretsClient is an interface for the Conjur client.
type SecretsClient interface {
RetrieveSecret(secret string) (result []byte, err error)
RetrieveBatchSecrets(variableIDs []string) (map[string][]byte, error)
Resources(filter *conjurapi.ResourceFilter) (resources []map[string]interface{}, err error)
}
// SecretsClientFactory is an interface for creating a Conjur client.

View file

@ -16,6 +16,10 @@ package fake
import (
"errors"
"fmt"
"math/rand"
"github.com/cyberark/conjur-api-go/conjurapi"
)
type ConjurMockClient struct {
@ -26,5 +30,95 @@ func (mc *ConjurMockClient) RetrieveSecret(secret string) (result []byte, err er
err = errors.New("error")
return nil, err
}
if secret == "json_map" {
return []byte(`{"key1":"value1","key2":"value2"}`), nil
}
if secret == "json_nested" {
return []byte(`{"key1":"value1","key2":{"key3":"value3","key4":"value4"}}`), nil
}
return []byte("secret"), nil
}
func (mc *ConjurMockClient) RetrieveBatchSecrets(variableIDs []string) (map[string][]byte, error) {
secrets := make(map[string][]byte)
for _, id := range variableIDs {
if id == "error" {
return nil, errors.New("error")
}
fullID := fmt.Sprintf("conjur:variable:%s", id)
secrets[fullID] = []byte("secret")
}
return secrets, nil
}
func (mc *ConjurMockClient) Resources(filter *conjurapi.ResourceFilter) (resources []map[string]interface{}, err error) {
policyID := "conjur:policy:root"
if filter.Offset == 0 {
// First "page" of secrets: 2 static ones and 98 random ones
secrets := []map[string]interface{}{
{
"id": "conjur:variable:secret1",
"annotations": []interface{}{
map[string]interface{}{
"name": "conjur/kind",
"value": "dummy",
},
},
},
{
"id": "conjur:variable:secret2",
"owner": "conjur:policy:admin1",
"annotations": []interface{}{
map[string]interface{}{
"name": "Description",
"policy": policyID,
"value": "Lorem ipsum dolor sit amet",
},
map[string]interface{}{
"name": "conjur/kind",
"policy": policyID,
"value": "password",
},
},
"permissions": map[string]string{
"policy": policyID,
"privilege": "update",
"role": "conjur:group:admins",
},
"policy": policyID,
},
}
// Add 98 random secrets so we can simulate a full "page" of 100 secrets
secrets = append(secrets, generateRandomSecrets(98)...)
return secrets, nil
} else if filter.Offset == 100 {
// Second "page" of secrets: 100 random ones
return generateRandomSecrets(100), nil
}
// Add 50 random secrets so we can simulate a partial "page" of 50 secrets
return generateRandomSecrets(50), nil
}
func generateRandomSecrets(count int) []map[string]interface{} {
var secrets []map[string]interface{}
for i := 0; i < count; i++ {
//nolint:gosec
randomNumber := rand.Intn(10000)
secrets = append(secrets, generateRandomSecret(randomNumber))
}
return secrets
}
func generateRandomSecret(num int) map[string]interface{} {
return map[string]interface{}{
"id": fmt.Sprintf("conjur:variable:random/var_%d", num),
"annotations": []map[string]interface{}{
{
"name": "random_number",
"value": fmt.Sprintf("%d", num),
},
},
"policy": "conjur:policy:random",
}
}

View file

@ -17,55 +17,21 @@ package conjur
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cyberark/conjur-api-go/conjurapi"
"github.com/cyberark/conjur-api-go/conjurapi/authn"
corev1 "k8s.io/api/core/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"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/conjur/util"
"github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
)
var (
errConjurClient = "cannot setup new Conjur client: %w"
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"
)
// 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
}
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 (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
func (p *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()
@ -77,7 +43,12 @@ func (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore,
return nil, err
}
return c.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
return p.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
}
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientAPI SecretsClientFactory) (esv1beta1.SecretsClient, error) {
@ -91,274 +62,6 @@ func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube cli
}, 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
}
config := conjurapi.Config{
ApplianceURL: prov.URL,
SSLCert: cert,
}
if prov.Auth.APIKey != nil {
config.Account = prov.Auth.APIKey.Account
conjUser, secErr := resolvers.SecretKeyRef(
ctx,
p.kube,
p.StoreKind,
p.namespace, prov.Auth.APIKey.UserRef)
if secErr != nil {
return nil, fmt.Errorf(errBadServiceUser, secErr)
}
conjAPIKey, secErr := resolvers.SecretKeyRef(
ctx,
p.kube,
p.StoreKind,
p.namespace,
prov.Auth.APIKey.APIKeyRef)
if secErr != nil {
return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
}
conjur, newClientFromKeyError := p.clientAPI.NewClientFromKey(config,
authn.LoginPair{
Login: conjUser,
APIKey: conjAPIKey,
},
)
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")
}
}
// GetAllSecrets returns all secrets from the provider.
// NOT IMPLEMENTED.
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 *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
}
return secretValue, nil
}
// PushSecret will write a single secret into the provider.
func (p *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
// NOT IMPLEMENTED
return nil
}
func (p *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
// NOT IMPLEMENTED
return nil
}
func (p *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
return false, fmt.Errorf("not implemented")
}
// GetSecretMap returns multiple k/v pairs from the provider.
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 {
return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
}
// Maps the json data to a string:string map
kv := make(map[string]string)
err = json.Unmarshal(data, &kv)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
}
// Converts values in K:V pairs into bytes, while leaving keys as strings
secretData := make(map[string][]byte)
for k, v := range kv {
secretData[k] = []byte(v)
}
return secretData, nil
}
// Close closes the provider.
func (p *Client) Close(_ context.Context) error {
return nil
}
// Validate validates the provider.
func (p *Client) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}
// ValidateStore validates the store.
func (c *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
prov, err := util.GetConjurProvider(store)
if err != nil {
return nil, err
}
if prov.URL == "" {
return nil, fmt.Errorf("conjur URL cannot be empty")
}
if prov.Auth.APIKey != nil {
if prov.Auth.APIKey.Account == "" {
return nil, fmt.Errorf("missing Auth.ApiKey.Account")
}
if prov.Auth.APIKey.UserRef == nil {
return nil, fmt.Errorf("missing Auth.Apikey.UserRef")
}
if prov.Auth.APIKey.APIKeyRef == nil {
return nil, fmt.Errorf("missing Auth.Apikey.ApiKeyRef")
}
if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.APIKey.UserRef); err != nil {
return nil, fmt.Errorf("invalid Auth.Apikey.UserRef: %w", err)
}
if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.APIKey.APIKeyRef); err != nil {
return nil, fmt.Errorf("invalid Auth.Apikey.ApiKeyRef: %w", err)
}
}
if prov.Auth.Jwt != nil {
if prov.Auth.Jwt.Account == "" {
return nil, fmt.Errorf("missing Auth.Jwt.Account")
}
if prov.Auth.Jwt.ServiceID == "" {
return nil, fmt.Errorf("missing Auth.Jwt.ServiceID")
}
if prov.Auth.Jwt.ServiceAccountRef == nil && prov.Auth.Jwt.SecretRef == nil {
return nil, 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 nil, 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 nil, fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
}
}
}
// At least one auth must be configured
if prov.Auth.APIKey == nil && prov.Auth.Jwt == nil {
return nil, fmt.Errorf("missing Auth.* configuration")
}
return nil, nil
}
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
func (c *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// 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 = resolvers.SecretKeyRef(
ctx,
p.kube,
p.StoreKind,
p.namespace,
&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{
NewConjurProvider: newConjurProvider,

View file

@ -55,75 +55,17 @@ func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
}
}
type ValidateStoreTestCase struct {
store *esv1beta1.SecretStore
err error
}
func TestValidateStore(t *testing.T) {
testCases := []ValidateStoreTestCase{
{
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
err: nil,
},
{
store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
err: fmt.Errorf("conjur URL cannot be empty"),
},
{
store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
err: fmt.Errorf("missing Auth.Apikey.UserRef"),
},
{
store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
err: fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
},
{
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
err: fmt.Errorf("missing Auth.ApiKey.Account"),
},
{
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount"),
err: nil,
},
{
store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthnService, "", "myconjuraccount"),
err: nil,
},
{
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", ""),
err: fmt.Errorf("missing Auth.Jwt.Account"),
},
{
store: makeJWTSecretStore(svcURL, "conjur", "", "", "", "myconjuraccount"),
err: fmt.Errorf("missing Auth.Jwt.ServiceID"),
},
{
store: makeJWTSecretStore("", "conjur", "", jwtAuthnService, "", "myconjuraccount"),
err: fmt.Errorf("conjur URL cannot be empty"),
},
{
store: makeJWTSecretStore(svcURL, "", "", jwtAuthnService, "", "myconjuraccount"),
err: fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
},
{
store: makeNoAuthSecretStore(svcURL),
err: fmt.Errorf("missing Auth.* configuration"),
},
}
c := Provider{}
for _, tc := range testCases {
_, 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 {
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 makeValidFindRef(search string, tags map[string]string) *esv1beta1.ExternalSecretFind {
var name *esv1beta1.FindName
if search != "" {
name = &esv1beta1.FindName{
RegExp: search,
}
}
return &esv1beta1.ExternalSecretFind{
Name: name,
Tags: tags,
}
}
func TestGetSecret(t *testing.T) {
@ -264,6 +206,239 @@ func TestGetSecret(t *testing.T) {
}
}
func TestGetAllSecrets(t *testing.T) {
type args struct {
store esv1beta1.GenericStore
kube kclient.Client
corev1 typedcorev1.CoreV1Interface
namespace string
search string
tags map[string]string
}
type want struct {
err error
values map[string][]byte
}
type testCase struct {
reason string
args args
want want
}
cases := map[string]testCase{
"SimpleSearchSingleResultSuccess": {
reason: "Should search for secrets successfully using a simple string.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
search: "secret1",
},
want: want{
err: nil,
values: map[string][]byte{
"secret1": []byte("secret"),
},
},
},
"RegexSearchMultipleResultsSuccess": {
reason: "Should search for secrets successfully using a regex and return multiple results.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
search: "^secret[1,2]$",
},
want: want{
err: nil,
values: map[string][]byte{
"secret1": []byte("secret"),
"secret2": []byte("secret"),
},
},
},
"RegexSearchInvalidRegexFailure": {
reason: "Should fail to search for secrets using an invalid regex.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
search: "^secret[1,2", // Missing `]`
},
want: want{
err: fmt.Errorf("could not compile find.name.regexp [%s]: %w", "^secret[1,2", fmt.Errorf("error parsing regexp: missing closing ]: `[1,2`")),
values: nil,
},
},
"SimpleSearchNoResultsSuccess": {
reason: "Should search for secrets successfully using a simple string and return no results.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
search: "nonexistent",
},
want: want{
err: nil,
values: map[string][]byte{},
},
},
"TagSearchSingleResultSuccess": {
reason: "Should search for secrets successfully using a tag.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
tags: map[string]string{
"conjur/kind": "password",
},
},
want: want{
err: nil,
values: map[string][]byte{
"secret2": []byte("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 := makeValidFindRef(tc.args.search, tc.args.tags)
secrets, err := provider.GetAllSecrets(context.Background(), *ref)
if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
t.Errorf("\n%s\nconjur.GetAllSecrets(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.values, secrets); diff != "" {
t.Errorf("\n%s\nconjur.GetAllSecrets(...): -want, +got:\n%s", tc.reason, diff)
}
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
runTest(t, name, tc)
})
}
}
func TestGetSecretMap(t *testing.T) {
type args struct {
store esv1beta1.GenericStore
kube kclient.Client
corev1 typedcorev1.CoreV1Interface
namespace string
ref *esv1beta1.ExternalSecretDataRemoteRef
}
type want struct {
err error
val map[string][]byte
}
type testCase struct {
reason string
args args
want want
}
cases := map[string]testCase{
"ReadJsonSecret": {
reason: "Should read a JSON key value secret.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
ref: makeValidRef("json_map"),
},
want: want{
err: nil,
val: map[string][]byte{
"key1": []byte("value1"),
"key2": []byte("value2"),
},
},
},
"ReadJsonSecretFailure": {
reason: "Should fail to read a non JSON secret",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
ref: makeValidRef("secret1"),
},
want: want{
err: fmt.Errorf("%w", fmt.Errorf("unable to unmarshal secret secret1: invalid character 's' looking for beginning of value")),
val: nil,
},
},
"ReadJsonSecretSpecificKey": {
reason: "Should read a specific key from a JSON secret.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
ref: &esv1beta1.ExternalSecretDataRemoteRef{
Key: "json_nested",
Version: "default",
Property: "key2",
},
},
want: want{
err: nil,
val: map[string][]byte{
"key3": []byte("value3"),
"key4": []byte("value4"),
},
},
},
"ReadJsonSecretSpecificKeyNotFound": {
reason: "Should fail to read a nonexistent key from a JSON secret.",
args: args{
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
kube: clientfake.NewClientBuilder().
WithObjects(makeFakeAPIKeySecrets()...).Build(),
namespace: "default",
ref: &esv1beta1.ExternalSecretDataRemoteRef{
Key: "json_map",
Version: "default",
Property: "key3",
},
},
want: want{
err: fmt.Errorf("%w", fmt.Errorf("error getting secret json_map: cannot find secret data for key: \"key3\"")),
val: nil,
},
},
}
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{})
val, err := provider.GetSecretMap(context.Background(), *tc.args.ref)
if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
t.Errorf("\n%s\nconjur.GetSecretMap(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.val, val); diff != "" {
t.Errorf("\n%s\nconjur.GetSecretMap(...): -want val, +got val:\n%s", tc.reason, diff)
}
}
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

View file

@ -0,0 +1,100 @@
/*
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 provides a Conjur provider for External Secrets.
package conjur
import (
"fmt"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider/conjur/util"
"github.com/external-secrets/external-secrets/pkg/utils"
)
// ValidateStore validates the store.
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
prov, err := util.GetConjurProvider(store)
if err != nil {
return nil, err
}
if prov.URL == "" {
return nil, fmt.Errorf("conjur URL cannot be empty")
}
if prov.Auth.APIKey != nil {
err := validateAPIKeyStore(store, *prov.Auth.APIKey)
if err != nil {
return nil, err
}
}
if prov.Auth.Jwt != nil {
err := validateJWTStore(store, *prov.Auth.Jwt)
if err != nil {
return nil, err
}
}
// At least one auth must be configured
if prov.Auth.APIKey == nil && prov.Auth.Jwt == nil {
return nil, fmt.Errorf("missing Auth.* configuration")
}
return nil, nil
}
func validateAPIKeyStore(store esv1beta1.GenericStore, auth esv1beta1.ConjurAPIKey) error {
if auth.Account == "" {
return fmt.Errorf("missing Auth.ApiKey.Account")
}
if auth.UserRef == nil {
return fmt.Errorf("missing Auth.Apikey.UserRef")
}
if auth.APIKeyRef == nil {
return fmt.Errorf("missing Auth.Apikey.ApiKeyRef")
}
if err := utils.ValidateReferentSecretSelector(store, *auth.UserRef); err != nil {
return fmt.Errorf("invalid Auth.Apikey.UserRef: %w", err)
}
if err := utils.ValidateReferentSecretSelector(store, *auth.APIKeyRef); err != nil {
return fmt.Errorf("invalid Auth.Apikey.ApiKeyRef: %w", err)
}
return nil
}
func validateJWTStore(store esv1beta1.GenericStore, auth esv1beta1.ConjurJWT) error {
if auth.Account == "" {
return fmt.Errorf("missing Auth.Jwt.Account")
}
if auth.ServiceID == "" {
return fmt.Errorf("missing Auth.Jwt.ServiceID")
}
if auth.ServiceAccountRef == nil && auth.SecretRef == nil {
return fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef")
}
if auth.SecretRef != nil {
if err := utils.ValidateReferentSecretSelector(store, *auth.SecretRef); err != nil {
return fmt.Errorf("invalid Auth.Jwt.SecretRef: %w", err)
}
}
if auth.ServiceAccountRef != nil {
if err := utils.ValidateReferentServiceAccountSelector(store, *auth.ServiceAccountRef); err != nil {
return fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
}
}
return nil
}

View file

@ -0,0 +1,93 @@
/*
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"
"testing"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
type ValidateStoreTestCase struct {
store *esv1beta1.SecretStore
err error
}
func TestValidateStore(t *testing.T) {
testCases := []ValidateStoreTestCase{
{
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
err: nil,
},
{
store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
err: fmt.Errorf("conjur URL cannot be empty"),
},
{
store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
err: fmt.Errorf("missing Auth.Apikey.UserRef"),
},
{
store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
err: fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
},
{
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
err: fmt.Errorf("missing Auth.ApiKey.Account"),
},
{
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount"),
err: nil,
},
{
store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthnService, "", "myconjuraccount"),
err: nil,
},
{
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", ""),
err: fmt.Errorf("missing Auth.Jwt.Account"),
},
{
store: makeJWTSecretStore(svcURL, "conjur", "", "", "", "myconjuraccount"),
err: fmt.Errorf("missing Auth.Jwt.ServiceID"),
},
{
store: makeJWTSecretStore("", "conjur", "", jwtAuthnService, "", "myconjuraccount"),
err: fmt.Errorf("conjur URL cannot be empty"),
},
{
store: makeJWTSecretStore(svcURL, "", "", jwtAuthnService, "", "myconjuraccount"),
err: fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
},
{
store: makeNoAuthSecretStore(svcURL),
err: fmt.Errorf("missing Auth.* configuration"),
},
}
p := Provider{}
for _, tc := range testCases {
_, err := p.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 {
t.Errorf("want nil got err %v", err)
} else if tc.err != nil && err == nil {
t.Errorf("want err %v got nil", tc.err)
}
}
}