Skip to content

Commit

Permalink
Add Secrets Provider refresh interval
Browse files Browse the repository at this point in the history
  • Loading branch information
rpothier committed Jan 25, 2022
1 parent 54ce163 commit 26f25ca
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 29 deletions.
4 changes: 3 additions & 1 deletion PUSH_TO_FILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,14 @@ for a description of each environment variable setting:
| K8s Annotation | Equivalent<br>Environment Variable | Description, Notes |
|-----------------------------------------|---------------------|----------------------------------|
| `conjur.org/authn-identity` | `CONJUR_AUTHN_LOGIN` | Required value. Example: `host/conjur/authn-k8s/cluster/apps/inventory-api` |
| `conjur.org/container-mode` | `CONTAINER_MODE` | Allowed values: <ul><li>`init`</li><li>`application`</li></ul>Defaults to `init`.<br>Must be set (or default) to `init` for Push to File mode.|
| `conjur.org/container-mode` | `CONTAINER_MODE` | Allowed values: <ul><li>`init`</li><li>`application`</li><li>`side-car`</li></ul>Defaults to `init`.<br>Must be set (or default) to `init` or `side-car`for Push to File mode.|
| `conjur.org/secrets-destination` | `SECRETS_DESTINATION` | Allowed values: <ul><li>`file`</li><li>`k8s_secrets`</li></ul> |
| `conjur.org/k8s-secrets` | `K8S_SECRETS` | This list is ignored when `conjur.org/secrets-destination` annotation is set to **`file`** |
| `conjur.org/retry-count-limit` | `RETRY_COUNT_LIMIT` | Defaults to 5
| `conjur.org/retry-interval-sec` | `RETRY_INTERVAL_SEC` | Defaults to 1 (sec) |
| `conjur.org/debug-logging` | `DEBUG` | Defaults to `false` |
| `conjur.org/secrets-refresh-enabled`| Note\* | Can be set to `true` or `false`. Secrets Provider will exit with error if this is explicitly set to `false` and `conjur.org/secrets-rotation-interval` is explicitly set. |
| `conjur.org/secrets-refresh-interval` | Note\* | Set to a valid duration string as defined [here](https://pkg.go.dev/time#ParseDuration). Valid time units are `s`, `m`, and `h` (for seconds, minutes, and hours, respectively). Some examples of valid duration strings:<ul><li>`5m`</li><li>`2h30m`</li><li>`48h`</li></ul>The minimum refresh interval is 1 second. A refresh interval of 0 seconds is treated as a fatal configuration error. The maximum refresh interval is approximately 290 years. |
| `conjur.org/conjur-secrets.{secret-group}` | Note\* | List of secrets to be retrieved from Conjur. Each entry can be either:<ul><li>A Conjur variable path</li><li> A key/value pairs of the form `<alias>:<Conjur variable path>` where the `alias` represents the name of the secret to be written to the secrets file |
| `conjur.org/conjur-secrets-policy-path.{secret-group}` | Note\* | Defines a common Conjur policy path, assumed to be relative to the root policy.<br><br>When this annotation is set, the policy paths defined by `conjur.org/conjur-secrets.{secret-group}` are relative to this common path.<br><br>When this annotation is not set, the policy paths defined by `conjur.org/conjur-secrets.{secret-group}` are themselves relative to the root policy.<br><br>(See [Example Common Policy Path](#example-common-policy-path) for an explicit example of this relationship.)|
| `conjur.org/secret-file-path.{secret-group}` | Note\* | Relative path for secret file or directory to be written. This path is assumed to be relative to the respective mount path for the shared secrets volume for each container.<br><br>If the `conjur.org/secret-file-template.{secret-group}` is set, then this secret file path defaults to `{secret-group}.out`. For example, if the secret group name is `my-app`, the the secret file path defaults to `my-app.out`.<br><br>If the `conjur.org/secret-file-template.{secret-group}` is not set, then this secret file path defaults to `{secret-group}.{secret-group-file-format}`. For example, if the secret group name is `my-app`, and the secret file format is set for YAML, the the secret file path defaults to `my-app.yaml`.
Expand Down
77 changes: 64 additions & 13 deletions cmd/secrets-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ func main() {
}

// Process Pod Annotations
if err := processAnnotations(ctx, tracer); err != nil {
//var refreshInterval time.Duration
if /*refreshInterval*/_, err = processAnnotations(ctx, tracer); err != nil {
logError(err.Error())
return
}
Expand All @@ -85,37 +86,50 @@ func main() {
logError(err.Error())
return
}
fmt.Printf("***TEMP*** begin refresh loop\n")

// Provide secrets
err = provideSecrets()
if err != nil {
errStr := fmt.Sprintf(messages.CSPFK039E, secretsConfig.StoreType, err.Error())
logError(errStr)
}
fmt.Printf("***TEMP*** Secrets Provider complete!\n")
}

func processAnnotations(ctx context.Context, tracer trace.Tracer) error {
func processAnnotations(ctx context.Context, tracer trace.Tracer) (time.Duration, error) {
// Only attempt to populate from annotations if the annotations file exists
// TODO: Figure out strategy for dealing with explicit annotation file path
// set by user. In that case we can't just ignore that the file is missing.
refreshInterval := time.Duration(0)
if _, err := os.Stat(annotationsFilePath); err == nil {
_, span := tracer.Start(ctx, "Process Annotations")
defer span.End()

defer func() {
if err != nil {
log.Error(err.Error())
span.RecordErrorAndSetStatus(err)
}
}()

annotationsMap, err = annotations.NewAnnotationsFromFile(annotationsFilePath)
if err != nil {
log.Error(err.Error())
span.RecordErrorAndSetStatus(err)
return err
return 0, err
}

errLogs, infoLogs := secretsConfigProvider.ValidateAnnotations(annotationsMap)
if err := logErrorsAndInfos(errLogs, infoLogs); err != nil {
log.Error(messages.CSPFK049E)
span.RecordErrorAndSetStatus(errors.New(messages.CSPFK049E))
return err
err := errors.New(messages.CSPFK049E)
return 0, err
}

refreshInterval, err = parseSecretsRefreshInterval()
if err != nil {
return 0, err
}
}
return nil
return refreshInterval, nil
}

func secretRetriever(ctx context.Context,
Expand All @@ -130,11 +144,12 @@ func secretRetriever(ctx context.Context,
log.Error(messages.CSPFK008E)
return nil, err
}
if err = validateContainerMode(authnConfig.GetContainerMode()); err != nil {
// Do we need to check this? Container mode is checked in ValidateAnnotations
/*if err = validateContainerMode(authnConfig.GetContainerMode()); err != nil {
span.RecordErrorAndSetStatus(err)
log.Error(err.Error())
return nil, err
}
}*/

// Initialize a Conjur secret retriever
secretRetriever, err := conjur.NewSecretRetriever(authnConfig)
Expand All @@ -160,6 +175,7 @@ func retryableSecretsProvider(
span.RecordErrorAndSetStatus(err)
return nil, nil, err
}

providerConfig := &secrets.ProviderConfig{
StoreType: secretsConfig.StoreType,
PodNamespace: secretsConfig.PodNamespace,
Expand All @@ -186,6 +202,15 @@ func retryableSecretsProvider(
secretsConfig.RetryCountLimit,
provideSecrets,
)

// add a new retryable secrets
provideSecrets = secrets.PeriodicSecretProvider(
secretsConfig.SecretsRefreshInterval,
/*secretsConfig.secretRefreshEnabled,*/
getContainerMode(),
provideSecrets,
time.Duration(100 * 365 * 24)*time.Hour,
)
return provideSecrets, secretsConfig, nil
}

Expand Down Expand Up @@ -232,16 +257,42 @@ func logErrorsAndInfos(errLogs []error, infoLogs []error) error {
return nil
}

func validateContainerMode(containerMode string) error {

/*func validateContainerMode(containerMode string) error {
validContainerModes := []string{
"init",
"application",
}

validContainerModes := secretsConfigProvider.
for _, validContainerModeType := range validContainerModes {
if containerMode == validContainerModeType {
return nil
}
}
return fmt.Errorf(messages.CSPFK007E, containerMode, validContainerModes)
}*/

func parseSecretsRefreshInterval() (time.Duration, error) {
intervalKey := secretsConfigProvider.SecretsRefreshIntervalKey
duration := time.Duration(0)
if intervalStr, exists := annotationsMap[intervalKey]; exists {
var err error
duration, err = time.ParseDuration(intervalStr)
if err != nil {
return 0, fmt.Errorf(messages.CSPFK050E, intervalStr)
}
}
return duration, nil
}

func getContainerMode() string {
containerMode := "init"

if mode, exists := annotationsMap[secretsConfigProvider.ContainerModeKey]; exists {
fmt.Printf("***TEMP*** found container mode %s\n", mode)
containerMode = mode
}
//return containerMode == "init"
//return secrets.Init
return containerMode
}
1 change: 1 addition & 0 deletions pkg/log/messages/error_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const CSPFK046E string = "CSPFK046E Secret Store Type needs to be configured, ei
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"

// Push to File
const CSPFK053E string = "CSPFK053E Unable to initialize Secrets Provider: unable to create secret group collection"
Expand Down
44 changes: 40 additions & 4 deletions pkg/secrets/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"strconv"
"strings"
"time"

"github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages"
)
Expand All @@ -29,6 +30,7 @@ type Config struct {
RetryCountLimit int
RetryIntervalSec int
StoreType string
SecretsRefreshInterval time.Duration
}

type annotationType int
Expand All @@ -46,14 +48,24 @@ type annotationRestraints struct {
allowedValues []string
}

// Annotations used to support Conjur secrets rotation.
// SecretsRefreshIntervalKey is the Annotation key for setting the interval
// for retrieving Conjur secrets and updating Kubernetes Secrets or
// application secret files if necessary.
const (
ContainerModeKey = "conjur.org/container-mode"
SecretsRefreshIntervalKey = "conjur.org/secrets-refresh-interval"
)

// Define supported annotation keys for Secrets Provider config, as well as value restraints for each
var secretsProviderAnnotations = map[string]annotationRestraints{
"conjur.org/authn-identity": {TYPESTRING, []string{}},
"conjur.org/container-mode": {TYPESTRING, []string{"init", "application"}},
ContainerModeKey: {TYPESTRING, []string{"init", "application", "side-car"}},
"conjur.org/secrets-destination": {TYPESTRING, []string{"file", "k8s_secrets"}},
"conjur.org/k8s-secrets": {TYPESTRING, []string{}},
"conjur.org/retry-count-limit": {TYPEINT, []string{}},
"conjur.org/retry-interval-sec": {TYPEINT, []string{}},
SecretsRefreshIntervalKey: {TYPESTRING, []string{}},
"conjur.org/debug-logging": {TYPEBOOL, []string{}},
"conjur.org/log-traces": {TYPEBOOL, []string{}},
"conjur.org/jaeger-collector-url": {TYPESTRING, []string{}},
Expand All @@ -68,8 +80,8 @@ var pushToFileAnnotationPrefixes = map[string]annotationRestraints{
"conjur.org/conjur-secrets-policy-path.": {TYPESTRING, []string{}},
"conjur.org/secret-file-path.": {TYPESTRING, []string{}},
"conjur.org/secret-file-format.": {TYPESTRING, []string{"yaml", "json", "dotenv", "bash", "template"}},
"conjur.org/secret-file-permissions.": {TYPESTRING, []string{}},
"conjur.org/secret-file-template": {TYPESTRING, []string{}},
"conjur.org/secret-file-permissions.": {TYPESTRING, []string{}},
"conjur.org/secret-file-template.": {TYPESTRING, []string{}},
}

// Define environment variables used in Secrets Provider config
Expand All @@ -82,7 +94,7 @@ var validEnvVars = []string{
}

// ValidateAnnotations confirms that the provided annotations are properly
// formated, have the proper value type, and if the annotation in question
// formatted, have the proper value type, and if the annotation in question
// had a defined set of accepted values, the provided value is confirmed.
// Function returns a list of Error logs, and a list of Info logs.
func ValidateAnnotations(annotations map[string]string) ([]error, []error) {
Expand Down Expand Up @@ -183,6 +195,14 @@ func ValidateSecretsProviderSettings(envAndAnnots map[string]string) ([]error, [
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryIntervalSec", "RETRY_INTERVAL_SEC", "conjur.org/retry-interval-sec"))
}

annotSecretsRefreshInterval := envAndAnnots["conjur.org/secrets-refresh-interval"]
if annotSecretsRefreshInterval != "" {
valid, reason := validRefreshInterval(annotSecretsRefreshInterval)
if !valid {
errorList = append(errorList, fmt.Errorf(messages.CSPFK050E, annotSecretsRefreshInterval, reason))
}
}

return errorList, infoList
}

Expand Down Expand Up @@ -222,12 +242,17 @@ func NewConfig(settings map[string]string) *Config {
}
retryIntervalSec := parseIntFromStringOrDefault(retryIntervalSecStr, DefaultRetryIntervalSec, MinRetryValue)

refreshIntervalStr := settings["conjur.org/secrets-refresh-interval"]
fmt.Printf("***TEMP*** secrets-refresh-interval=%s\n", refreshIntervalStr)

refreshInterval, _ := time.ParseDuration(refreshIntervalStr)
return &Config{
PodNamespace: podNamespace,
RequiredK8sSecrets: k8sSecretsArr,
RetryCountLimit: retryCountLimit,
RetryIntervalSec: retryIntervalSec,
StoreType: storeType,
SecretsRefreshInterval: refreshInterval,
}
}

Expand Down Expand Up @@ -304,3 +329,14 @@ func validStoreType(storeType string) bool {
}
return false
}

func validRefreshInterval(interval string) (bool, string) {
duration , err := time.ParseDuration(interval)
if err != nil {
return false, err.Error()
}
if duration < 0 {
return false, "Time cannot be negative"
}
return true, ""
}
18 changes: 18 additions & 0 deletions pkg/secrets/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var validateAnnotationsTestCases = []validateAnnotationsTestCase{
"conjur.org/k8s-secrets": "- secret-1\n- secret-2\n- secret-3\n",
"conjur.org/retry-count-limit": "12",
"conjur.org/retry-interval-sec": "2",
"conjur.org/secrets-refresh-interval": "5s",
"conjur.org/conjur-secrets.this-group": "- test/url\n- test-password: test/password\n- test-username: test/username\n",
"conjur.org/secret-file-path.this-group": "this-relative-path",
"conjur.org/secret-file-format.this-group": "yaml",
Expand Down Expand Up @@ -194,6 +195,7 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
"conjur.org/retry-count-limit": "10",
"conjur.org/retry-interval-sec": "20",
"conjur.org/k8s-secrets": "- secret-1\n- secret-2\n- secret-3\n",
"conjur.org/secrets-refresh-interval": "5s",
},
assert: assertEmptyErrorList(),
},
Expand Down Expand Up @@ -311,6 +313,22 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
},
assert: assertErrorInList(fmt.Errorf(messages.CSPFK005E, "SECRETS_DESTINATION")),
},
{
description: "if refresh interval is malformed, an error is returned",
envAndAnnots: map[string]string{
"MY_POD_NAMESPACE": "test-namespace",
"conjur.org/secrets-refresh-interval": "5",
},
assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "5", "time: missing unit in duration \"5\"")),
},
{
description: "if refresh interval is malformed, an error is returned",
envAndAnnots: map[string]string{
"MY_POD_NAMESPACE": "test-namespace",
"conjur.org/secrets-refresh-interval": "-5s",
},
assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "-5s", "Time cannot be negative")),
},
}

type newConfigTestCase struct {
Expand Down
Loading

0 comments on commit 26f25ca

Please sign in to comment.