Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

retrieve k8s secrets based on label #550

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion PUSH_TO_FILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,6 @@ To display the Secrets Provider logs in Kubernetes.
| Annotation file line 'x' is malformed: | `CSPFK045E` | The annotation described in the message is malformed. Check the annotation has a valid key and value |
| Secret Store Type needs to be configured | `CSPFK046E` | Either the conjur.org/secrets-destination annotation has not been set, or the downward API volumes have not been configured as required. See the [Configuring Pod Volumes and Container Volume Mounts](#configuring-pod-volumes-and-container-volume-mounts-for-push-to-file) section above for more information.|
| Secrets Provider in Push-to-File mode can only be configured with Pod annotations | `CSPFK047E`| To configure push-to-file the Secrets store type must have the `conjur.org/secrets-destination` with pod annotations and cannot be set with the `SECRETS_DESTINATION` environment variable. If the secrets-destination is set via annotations verify that the pod volumes and volumeMounts are configured correctly. See the [Configuring Pod Volumes and Container Volume Mounts](#configuring-pod-volumes-and-container-volume-mounts-for-push-to-file) section above for more information. |
| Secrets Provider in K8s Secrets mode requires either the 'K8S_SECRETS' environment variable or 'conjur.org/k8s-secrets' | `CSPFK048E ` | If the `secrets-destination` is set to `k8s_secrets` then the `K8S_SECRETS` environment variable or `conjur.org/k8s-secrets` needs to be configured. This does not apply to push-to-file. |
| Failed to validate Pod annotations | `CSPFK049E` | The service provider was unable to successfully parse the annotations. This could be due to a previous error. Check the logs for a specific error before this. |
| Unable to initialize Secrets Provider: unable to create secret group collection |`CSPFK053E` | Secrets provider could be initialized. Check futher back in the log file for any specific configuration errors. |
| Unable to initialize Secrets Provider: unrecognized Store Type | `CSPFK054E` | The `secrets-destination` value, either defined by the `SECRETS_DESTINATION` environment variable or the `conjur.org/secrets-destination` is an invalid value. `conjur.org/secrets-destination` should be `files` or `k8s-secrets`. |
Expand Down
1 change: 0 additions & 1 deletion pkg/log/messages/error_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ const CSPFK045E string = "CSPFK045E Annotation file line %d is malformed: expect

const CSPFK046E string = "CSPFK046E Secret Store Type needs to be configured, either with 'SECRETS_DESTINATION' environment variable or 'conjur.org/secrets-destination' Pod annotation"
const CSPFK047E string = "CSPFK047E Secrets Provider in Push-to-File mode can only be configured with Pod annotations"
const CSPFK048E string = "CSPFK048E Secrets Provider in K8s Secrets mode requires either the 'K8S_SECRETS' environment variable or 'conjur.org/k8s-secrets' Pod annotation"
const CSPFK049E string = "CSPFK049E Failed to validate Pod annotations"
const CSPFK050E string = "CSPFK050E Invalid secrets refresh interval annotation: %s %s"
const CSPFK051E string = "CSPFK051E Invalid secrets refresh configuration: %s %s"
Expand Down
1 change: 1 addition & 0 deletions pkg/log/messages/info_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ const CSPFK020I string = "CSPFK020I No change in Kubernetes secret, no secrets u
const CSPFK021I string = "CSPFK021I Error fetching Conjur secrets, clearing Kubernetes secrets"
const CSPFK022I string = "CSPFK022I Storing secret with base64 content-type '%s' in destination '%s'"
const CSPFK023I string = "CSPFK023I Retrieving all available secrets from Conjur"
const CSPFK023J string = "CSPFK023J Secrets Provider setting '%s' set to retrieve Kubernetes secrets by label"
8 changes: 8 additions & 0 deletions pkg/secrets/clients/k8s/k8s_secrets_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package k8s

import (
"context"
"github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/config"

"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
v1 "k8s.io/api/core/v1"
Expand All @@ -14,6 +15,7 @@ import (

type RetrieveK8sSecretFunc func(namespace string, secretName string) (*v1.Secret, error)
type UpdateK8sSecretFunc func(namespace string, secretName string, originalK8sSecret *v1.Secret, stringDataEntriesMap map[string][]byte) error
type RetrieveK8sSecretListFunc func(namespace string) (*v1.SecretList, error)

func RetrieveK8sSecret(namespace string, secretName string) (*v1.Secret, error) {
// get K8s client object
Expand Down Expand Up @@ -51,6 +53,12 @@ func UpdateK8sSecret(namespace string, secretName string, originalK8sSecret *v1.
return nil
}

func RetrieveK8sSecretList(namespace string) (*v1.SecretList, error) {
kubeClient, _ := configK8sClient()
log.Info("Retrieving labeled Kubernetes secrets from namespace '%s'", namespace)
return kubeClient.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: config.ManagedByProviderKey + "=true"})
}

func configK8sClient() (*kubernetes.Clientset, error) {
// Create the Kubernetes client
log.Info(messages.CSPFK004I)
Expand Down
3 changes: 2 additions & 1 deletion pkg/secrets/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const (
logLevelKey = "conjur.org/log-level"
logTracesKey = "conjur.org/log-traces"
jaegerCollectorUrl = "conjur.org/jaeger-collector-url"
ManagedByProviderKey = "conjur.org/managed-by-provider"
)

// Define supported annotation keys for Secrets Provider config, as well as value restraints for each
Expand Down Expand Up @@ -198,7 +199,7 @@ func ValidateSecretsProviderSettings(envAndAnnots map[string]string) ([]error, [
annotK8sSecretsStr := envAndAnnots[k8sSecretsKey]
if storeType == "k8s_secrets" {
if envK8sSecretsStr == "" && annotK8sSecretsStr == "" {
errorList = append(errorList, errors.New(messages.CSPFK048E))
infoList = append(infoList, fmt.Errorf(messages.CSPFK023J, "RequiredK8sSecrets"))
} else if envK8sSecretsStr != "" && annotK8sSecretsStr != "" {
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RequiredK8sSecrets", "K8S_SECRETS", k8sSecretsKey))
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/secrets/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,12 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
assert: assertErrorInList(fmt.Errorf(messages.CSPFK043E, SecretsDestinationKey, "invalid", []string{File, K8s})),
},
{
description: "if RequiredK8sSecrets is not configured in K8s Secrets mode, an error is returned",
description: "if RequiredK8sSecrets is not configured in K8s Secrets mode, all labeled secret should be used",
envAndAnnots: map[string]string{
"MY_POD_NAMESPACE": "test-namespace",
SecretsDestinationKey: "k8s_secrets",
},
assert: assertErrorInList(errors.New(messages.CSPFK048E)),
assert: assertInfoInList(fmt.Errorf(messages.CSPFK023J, "RequiredK8sSecrets")),
},
{
description: "if RequiredK8sSecrets is set to a null string in K8s Secrets mode, an error is returned",
Expand All @@ -314,7 +314,7 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
"SECRETS_DESTINATION": "k8s_secrets",
"K8S_SECRETS": "",
},
assert: assertErrorInList(errors.New(messages.CSPFK048E)),
assert: assertInfoInList(fmt.Errorf(messages.CSPFK023J, "RequiredK8sSecrets")),
},
{
description: "if envVar 'SECRETS_DESTINATION' is malformed in the absence of annotation 'conjur.org/secrets-destination', an error is returned",
Expand Down Expand Up @@ -429,7 +429,7 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
"SECRETS_DESTINATION": "k8s_secrets",
"K8S_SECRETS": "another-secret-1,another-secret-2",
},
assert: assertEmptyErrorList(),
assert: assertEmptyErrorList(),
},
}

Expand Down
27 changes: 26 additions & 1 deletion pkg/secrets/k8s_secrets_storage/mocks/k8s_secrets_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package mocks

import (
"errors"

"gopkg.in/yaml.v3"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// K8sSecrets represents a collection of Kubernetes Secrets to be populated
Expand Down Expand Up @@ -79,6 +79,9 @@ func (c *KubeSecretsClient) RetrieveSecret(_ string, secretName string) (*v1.Sec
}

return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
},
Data: secretData,
}, nil
}
Expand All @@ -102,6 +105,28 @@ func (c *KubeSecretsClient) UpdateSecret(
return nil
}

// RetrieveSecretList retrieves a Kubernetes Secrets list from the mock Kubernetes
// Secrets client's database.
func (c *KubeSecretsClient) RetrieveSecretList(_ string) (*v1.SecretList, error) {

if c.ErrOnRetrieve != nil {
return nil, c.ErrOnRetrieve
}

secretList := &v1.SecretList{}
// Return all secret objects from the mock K8s DB
for secretName, secretData := range c.database {
secretList.Items = append(secretList.Items, v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
},
Data: secretData,
})
}

return secretList, nil
}

// InspectSecret provides a way for unit tests to view the 'Data' field
// content of a Kubernetes Secret by reading this content directly from
// the mock client's database.
Expand Down
70 changes: 47 additions & 23 deletions pkg/secrets/k8s_secrets_storage/provide_conjur_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type k8sSecretsState struct {
type k8sAccessDeps struct {
retrieveSecret k8sClient.RetrieveK8sSecretFunc
updateSecret k8sClient.UpdateK8sSecretFunc
listSecret k8sClient.RetrieveK8sSecretListFunc
}

type conjurAccessDeps struct {
Expand Down Expand Up @@ -108,6 +109,7 @@ func NewProvider(
k8s: k8sAccessDeps{
k8sClient.RetrieveK8sSecret,
k8sClient.UpdateK8sSecret,
k8sClient.RetrieveK8sSecretList,
},
conjur: conjurAccessDeps{
retrieveConjurSecrets,
Expand Down Expand Up @@ -219,51 +221,73 @@ func (p *K8sProvider) retrieveRequiredK8sSecrets(tracer trace.Tracer) error {
spanCtx, span := tracer.Start(p.traceContext, "Gather required K8s Secrets")
defer span.End()

for _, k8sSecretName := range p.requiredK8sSecrets {
_, childSpan := tracer.Start(spanCtx, "Retrieve K8s Secret")
defer childSpan.End()
if err := p.retrieveRequiredK8sSecret(k8sSecretName); err != nil {
childSpan.RecordErrorAndSetStatus(err)
span.RecordErrorAndSetStatus(err)
return err
// If strict list of secrets is provided from config, processed them.
// Otherwise, try to retrieve and processed all labeled secrets.
if len(p.requiredK8sSecrets) > 0 {
for _, k8sSecretName := range p.requiredK8sSecrets {
_, childSpan := tracer.Start(spanCtx, "Retrieve K8s Secret")
defer childSpan.End()
k8sSecret, err := p.k8s.retrieveSecret(p.podNamespace, k8sSecretName)
if err != nil {
// Error messages returned from K8s should be printed only in debug mode
p.log.debug(messages.CSPFK004D, err.Error())
return p.log.recordedError(messages.CSPFK020E)
}
if err := p.retrieveRequiredK8sSecret(k8sSecret); err != nil {
childSpan.RecordErrorAndSetStatus(err)
span.RecordErrorAndSetStatus(err)
return err
}
}
} else {
k8sSecrets, err := p.k8s.listSecret(p.podNamespace)
if err != nil {
p.log.logError(err.Error())
return p.log.recordedError("CSPFK020E Failed to retrieve Kubernetes Secrets from %s namespace", p.podNamespace)
}

if k8sSecrets.Items != nil {
for _, k8sSecret := range k8sSecrets.Items {
_, childSpan := tracer.Start(spanCtx, "Process K8s Secret")
defer childSpan.End()
if err := p.retrieveRequiredK8sSecret(&k8sSecret); err != nil {
childSpan.RecordErrorAndSetStatus(err)
span.RecordErrorAndSetStatus(err)
return err
}
}
} else {
p.log.info("No secrets to retrieve")
}
}
return nil
}

// retrieveRequiredK8sSecret retrieves an individual K8s Secrets that needs
// to be managed/updated by the Secrets Provider.
func (p *K8sProvider) retrieveRequiredK8sSecret(k8sSecretName string) error {

// Retrieve the K8s Secret
k8sSecret, err := p.k8s.retrieveSecret(p.podNamespace, k8sSecretName)
if err != nil {
// Error messages returned from K8s should be printed only in debug mode
p.log.debug(messages.CSPFK004D, err.Error())
return p.log.recordedError(messages.CSPFK020E)
}
func (p *K8sProvider) retrieveRequiredK8sSecret(k8sSecret *v1.Secret) error {

// Record the K8s Secret API object
p.secretsState.originalK8sSecrets[k8sSecretName] = k8sSecret
p.secretsState.originalK8sSecrets[k8sSecret.Name] = k8sSecret

// Read the value of the "conjur-map" entry in the K8s Secret's Data
// field, if it exists. If the entry does not exist or has a null
// value, return an error.
conjurMapKey := config.ConjurMapKey
conjurSecretsYAML, entryExists := k8sSecret.Data[conjurMapKey]
if !entryExists {
p.log.debug(messages.CSPFK008D, k8sSecretName, conjurMapKey)
return p.log.recordedError(messages.CSPFK028E, k8sSecretName)
p.log.debug(messages.CSPFK008D, k8sSecret.Name, conjurMapKey)
return p.log.recordedError(messages.CSPFK028E, k8sSecret.Name)
}
if len(conjurSecretsYAML) == 0 {
p.log.debug(messages.CSPFK006D, k8sSecretName, conjurMapKey)
return p.log.recordedError(messages.CSPFK028E, k8sSecretName)
p.log.debug(messages.CSPFK006D, k8sSecret.Name, conjurMapKey)
return p.log.recordedError(messages.CSPFK028E, k8sSecret.Name)
}

// Parse the YAML-formatted Conjur secrets mapping that has been
// retrieved from this K8s Secret.
p.log.debug(messages.CSPFK009D, conjurMapKey, k8sSecretName)
return p.parseConjurSecretsYAML(conjurSecretsYAML, k8sSecretName)
p.log.debug(messages.CSPFK009D, conjurMapKey, k8sSecret.Name)
return p.parseConjurSecretsYAML(conjurSecretsYAML, k8sSecret.Name)
}

// parseConjurSecretsYAML parses the YAML-formatted Conjur secrets mapping
Expand Down
37 changes: 37 additions & 0 deletions pkg/secrets/k8s_secrets_storage/provide_conjur_secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func (m testMocks) newProvider(requiredSecrets []string) K8sProvider {
k8s: k8sAccessDeps{
m.kubeClient.RetrieveSecret,
m.kubeClient.UpdateSecret,
m.kubeClient.RetrieveSecretList,
},
conjur: conjurAccessDeps{
m.conjurClient.RetrieveSecrets,
Expand Down Expand Up @@ -232,6 +233,42 @@ func TestProvide(t *testing.T) {
),
},
},
{
desc: "Happy path without requiredSecrets configured, 2 existing k8s Secrets with 2 existing Conjur secrets",
k8sSecrets: k8sStorageMocks.K8sSecrets{
"k8s-labeled-secret1": {
"conjur-map": {
"secret1": "conjur/var/path1",
"secret2": "conjur/var/path2",
},
},
"k8s-labeled-secret2": {
"conjur-map": {
"secret3": "conjur/var/path3",
"secret4": "conjur/var/path4",
},
},
},
asserts: []assertFunc{
assertSecretsUpdated(
expectedK8sSecrets{
"k8s-labeled-secret1": {
"secret1": "secret-value1",
"secret2": "secret-value2",
},
"k8s-labeled-secret2": {
"secret3": "secret-value3",
"secret4": "secret-value4",
},
},
expectedMissingValues{
"k8s-labeled-secret1": {"secret-value3", "secret-value4"},
"k8s-labeled-secret2": {"secret-value1", "secret-value2"},
},
false,
),
},
},
{
desc: "Happy path, 2 k8s Secrets use the same Conjur secret with different names",
k8sSecrets: k8sStorageMocks.K8sSecrets{
Expand Down