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 Feb 7, 2022
1 parent 5df8ecc commit 50bb93e
Show file tree
Hide file tree
Showing 8 changed files with 594 additions and 126 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added
- Adds support for Secrets Provider secrets rotation feature, Community release.
[cyberark/secrets-provider-for-k8s#426](https://github.com/cyberark/secrets-provider-for-k8s/pull/426)

## [1.3.0] - 2022-01-03

### Added
Expand Down
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>`sidecar`</li></ul>Defaults to `init`.<br>Must be set (or default) to `init` or `sidecar`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\* | Defaults to `false` unless `conjur.org/secrets-rotation-interval` is explicitly set. Secrets Provider will exit with error if this is set to `false` and `conjur.org/secrets-rotation-interval` is set. |
| `conjur.org/secrets-refresh-interval` | Note\* | Set to a valid duration string as defined [here](https://pkg.go.dev/time#ParseDuration). Setting a time implicitly enables refresh. 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 default refresh interval is 5 minutes. 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
43 changes: 22 additions & 21 deletions cmd/secrets-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,15 @@ func main() {
}

// Gather secrets config and create a retryable Secrets Provider
provideSecrets, secretsConfig, err := retryableSecretsProvider(ctx, tracer, secretRetriever)
provideSecrets, _, err := retryableSecretsProvider(ctx, tracer, secretRetriever)
if err != nil {
logError(err.Error())
return
}

// Provide secrets
err = provideSecrets()
if err != nil {
errStr := fmt.Sprintf(messages.CSPFK039E, secretsConfig.StoreType, err.Error())
logError(errStr)
if err = provideSecrets(); err != nil {
logError(err.Error())
}
}

Expand Down Expand Up @@ -131,11 +129,6 @@ func secretRetriever(ctx context.Context,
log.Error(messages.CSPFK008E)
return nil, err
}
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 Down Expand Up @@ -187,6 +180,20 @@ func retryableSecretsProvider(
secretsConfig.RetryCountLimit,
provideSecrets,
)

// Create a channel to send a quit signal to the periodic secret provider.
// TODO: Currently, this is just used for testing, but in the future we
// may want to create a SIGTERM or SIGHUP handler to catch a signal from
// a user / external entity, and then send an (empty struct) quit signal
// on this channel to trigger a graceful shut down of the Secrets Provider.
providerQuit := make(chan struct{})

provideSecrets = secrets.PeriodicSecretProvider(
secretsConfig.SecretsRefreshInterval,
getContainerMode(),
provideSecrets,
providerQuit,
)
return provideSecrets, secretsConfig, nil
}

Expand Down Expand Up @@ -233,16 +240,10 @@ func logErrorsAndInfos(errLogs []error, infoLogs []error) error {
return nil
}

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

for _, validContainerModeType := range validContainerModes {
if containerMode == validContainerModeType {
return nil
}
func getContainerMode() string {
containerMode := "init"
if mode, exists := annotationsMap[secretsConfigProvider.ContainerModeKey]; exists {
containerMode = mode
}
return fmt.Errorf(messages.CSPFK007E, containerMode, validContainerModes)
return containerMode
}
3 changes: 2 additions & 1 deletion pkg/log/messages/error_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const CSPFK003E string = "CSPFK003E AccessToken failed to delete access token da
// Environment variables
const CSPFK004E string = "CSPFK004E Environment variable '%s' must be provided"
const CSPFK005E string = "CSPFK005E Provided incorrect value for environment variable %s"
const CSPFK007E string = "CSPFK007E CONTAINER_MODE '%s' is not supported. Supported values are: %v"

// Authenticator
const CSPFK008E string = "CSPFK008E Failed to instantiate authenticator configuration"
Expand Down Expand Up @@ -68,6 +67,8 @@ 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"
const CSPFK051E string = "CSPFK050E Invalid secrets refresh configuration: %s %s"

// Push to File
const CSPFK053E string = "CSPFK053E Unable to initialize Secrets Provider: unable to create secret group collection"
Expand Down
157 changes: 120 additions & 37 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 @@ -19,7 +20,11 @@ const (
DefaultRetryCountLimit = 5
DefaultRetryIntervalSec = 1
MinRetryValue = 0
MinRefreshInterval = time.Second
DefaultRefreshIntervalStr = "5m"

)
var DefaultRefreshInterval,_ = time.ParseDuration(DefaultRefreshIntervalStr)

// Config defines the configuration parameters
// for the authentication requests
Expand All @@ -29,6 +34,7 @@ type Config struct {
RetryCountLimit int
RetryIntervalSec int
StoreType string
SecretsRefreshInterval time.Duration
}

type annotationType int
Expand All @@ -46,18 +52,39 @@ type annotationRestraints struct {
allowedValues []string
}

const (
AuthnIdentityKey = "conjur.org/authn-identity"
JwtTokenPath = "conjur.org/jwt-token-path"
ContainerModeKey = "conjur.org/container-mode"
SecretsDestinationKey = "conjur.org/secrets-destination"
k8sSecretsKey = "conjur.org/k8s-secrets"
retryCountLimitKey = "conjur.org/retry-count-limit"
retryIntervalSecKey = "conjur.org/retry-interval-sec"
// SecretsRefreshIntervalKey is the Annotation key for setting the interval
// for retrieving Conjur secrets and updating Kubernetes Secrets or
// application secret files if necessary.
SecretsRefreshIntervalKey = "conjur.org/secrets-refresh-interval"
// SecretsRefreshEnabledKey is the Annotation key for enabling the refresh
SecretsRefreshEnabledKey = "conjur.org/secrets-refresh-enabled"
debugLoggingKey = "conjur.org/debug-logging"
logTracesKey = "conjur.org/log-traces"
jaegerCollectorUrl = "conjur.org/jaeger-collector-url"
)

// 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/jwt-token-path": {TYPESTRING, []string{}},
"conjur.org/container-mode": {TYPESTRING, []string{"init", "application"}},
"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{}},
"conjur.org/debug-logging": {TYPEBOOL, []string{}},
"conjur.org/log-traces": {TYPEBOOL, []string{}},
"conjur.org/jaeger-collector-url": {TYPESTRING, []string{}},
AuthnIdentityKey: {TYPESTRING, []string{}},
JwtTokenPath: {TYPESTRING, []string{}},
ContainerModeKey: {TYPESTRING, []string{"init", "application", "sidecar"}},
SecretsDestinationKey: {TYPESTRING, []string{"file", "k8s_secrets"}},
k8sSecretsKey: {TYPESTRING, []string{}},
retryCountLimitKey: {TYPEINT, []string{}},
retryIntervalSecKey: {TYPEINT, []string{}},
SecretsRefreshIntervalKey: {TYPESTRING, []string{}},
SecretsRefreshEnabledKey: {TYPEBOOL, []string{}},
debugLoggingKey: {TYPEBOOL, []string{}},
logTracesKey: {TYPEBOOL, []string{}},
jaegerCollectorUrl: {TYPESTRING, []string{}},
}

// Define supported annotation key prefixes for Push to File config, as well as value restraints for each.
Expand All @@ -70,7 +97,7 @@ var pushToFileAnnotationPrefixes = map[string]annotationRestraints{
"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-template.": {TYPESTRING, []string{}},
}

// Define environment variables used in Secrets Provider config
Expand Down Expand Up @@ -140,51 +167,53 @@ func ValidateSecretsProviderSettings(envAndAnnots map[string]string) ([]error, [
}

envStoreType := envAndAnnots["SECRETS_DESTINATION"]
annotStoreType := envAndAnnots["conjur.org/secrets-destination"]
annotStoreType := envAndAnnots[SecretsDestinationKey]
storeType := ""
var err error

if annotStoreType == "" {
switch envStoreType {
case K8s:
storeType = envStoreType
case File:
errorList = append(errorList, errors.New(messages.CSPFK047E))
case "":
errorList = append(errorList, errors.New(messages.CSPFK046E))
default:
errorList = append(errorList, fmt.Errorf(messages.CSPFK005E, "SECRETS_DESTINATION"))
switch {
case annotStoreType == "":
storeType, err = validateStore(envStoreType)
if err != nil {
errorList = append(errorList, err)
}
} else if validStoreType(annotStoreType) {
case validStoreType(annotStoreType):
if validStoreType(envStoreType) {
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "StoreType", "SECRETS_DESTINATION", "conjur.org/secrets-destination"))
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "StoreType", "SECRETS_DESTINATION", SecretsDestinationKey))
}
storeType = annotStoreType
} else {
errorList = append(errorList, fmt.Errorf(messages.CSPFK043E, "conjur.org/secrets-destination", annotStoreType, []string{File, K8s}))
default:
errorList = append(errorList, fmt.Errorf(messages.CSPFK043E, SecretsDestinationKey, annotStoreType, []string{File, K8s}))
}

envK8sSecretsStr := envAndAnnots["K8S_SECRETS"]
annotK8sSecretsStr := envAndAnnots["conjur.org/k8s-secrets"]
annotK8sSecretsStr := envAndAnnots[k8sSecretsKey]
if storeType == "k8s_secrets" {
if envK8sSecretsStr == "" && annotK8sSecretsStr == "" {
errorList = append(errorList, errors.New(messages.CSPFK048E))
} else if envK8sSecretsStr != "" && annotK8sSecretsStr != "" {
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RequiredK8sSecrets", "K8S_SECRETS", "conjur.org/k8s-secrets"))
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RequiredK8sSecrets", "K8S_SECRETS", k8sSecretsKey))
}
}

annotRetryCountLimit := envAndAnnots["conjur.org/retry-count-limit"]
annotRetryCountLimit := envAndAnnots[retryCountLimitKey]
envRetryCountLimit := envAndAnnots["RETRY_COUNT_LIMIT"]
if annotRetryCountLimit != "" && envRetryCountLimit != "" {
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryCountLimit", "RETRY_COUNT_LIMIT", "conjur.org/retry-count-limit"))
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryCountLimit", "RETRY_COUNT_LIMIT", retryCountLimitKey))
}

annotRetryIntervalSec := envAndAnnots["conjur.org/retry-interval-sec"]
annotRetryIntervalSec := envAndAnnots[retryIntervalSecKey]
envRetryIntervalSec := envAndAnnots["RETRY_INTERVAL_SEC"]
if annotRetryIntervalSec != "" && envRetryIntervalSec != "" {
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryIntervalSec", "RETRY_INTERVAL_SEC", "conjur.org/retry-interval-sec"))
infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryIntervalSec", "RETRY_INTERVAL_SEC", retryIntervalSecKey))
}

annotSecretsRefreshEnable := envAndAnnots[SecretsRefreshEnabledKey]
annotSecretsRefreshInterval := envAndAnnots[SecretsRefreshIntervalKey]
annotContainerMode := envAndAnnots[ContainerModeKey]
err = validRefreshInterval(annotSecretsRefreshInterval, annotSecretsRefreshEnable, annotContainerMode)
if err != nil {
errorList = append(errorList, err)
}
return errorList, infoList
}

Expand All @@ -193,14 +222,14 @@ func ValidateSecretsProviderSettings(envAndAnnots map[string]string) ([]error, [
func NewConfig(settings map[string]string) *Config {
podNamespace := settings["MY_POD_NAMESPACE"]

storeType := settings["conjur.org/secrets-destination"]
storeType := settings[SecretsDestinationKey]
if storeType == "" {
storeType = settings["SECRETS_DESTINATION"]
}

k8sSecretsArr := []string{}
if storeType != "file" {
k8sSecretsStr := settings["conjur.org/k8s-secrets"]
k8sSecretsStr := settings[k8sSecretsKey]
if k8sSecretsStr != "" {
k8sSecretsStr := strings.ReplaceAll(k8sSecretsStr, "- ", "")
k8sSecretsArr = strings.Split(k8sSecretsStr, "\n")
Expand All @@ -212,24 +241,34 @@ func NewConfig(settings map[string]string) *Config {
}
}

retryCountLimitStr := settings["conjur.org/retry-count-limit"]
retryCountLimitStr := settings[retryCountLimitKey]
if retryCountLimitStr == "" {
retryCountLimitStr = settings["RETRY_COUNT_LIMIT"]
}
retryCountLimit := parseIntFromStringOrDefault(retryCountLimitStr, DefaultRetryCountLimit, MinRetryValue)

retryIntervalSecStr := settings["conjur.org/retry-interval-sec"]
retryIntervalSecStr := settings[retryIntervalSecKey]
if retryIntervalSecStr == "" {
retryIntervalSecStr = settings["RETRY_INTERVAL_SEC"]
}
retryIntervalSec := parseIntFromStringOrDefault(retryIntervalSecStr, DefaultRetryIntervalSec, MinRetryValue)

refreshIntervalStr := settings[SecretsRefreshIntervalKey]
refreshEnableStr := settings[SecretsRefreshEnabledKey]

if refreshIntervalStr == "" && refreshEnableStr == "true" {
refreshIntervalStr = DefaultRefreshIntervalStr
}
// ignore errors here, if the interval string is null, zero is returned
refreshInterval, _ := time.ParseDuration(refreshIntervalStr)

return &Config{
PodNamespace: podNamespace,
RequiredK8sSecrets: k8sSecretsArr,
RetryCountLimit: retryCountLimit,
RetryIntervalSec: retryIntervalSec,
StoreType: storeType,
SecretsRefreshInterval: refreshInterval,
}
}

Expand Down Expand Up @@ -306,3 +345,47 @@ func validStoreType(storeType string) bool {
}
return false
}

func validateStore(envStoreType string) (string,error) {
var err error
storeType := ""
switch envStoreType {
case K8s:
storeType = envStoreType
case File:
err = errors.New(messages.CSPFK047E)
case "":
err = errors.New(messages.CSPFK046E)
default:
err = fmt.Errorf(messages.CSPFK005E, "SECRETS_DESTINATION")
}
return storeType, err
}

func validRefreshInterval(intervalStr string, enableStr string, containerMode string) error {

var err error
if intervalStr != "" || enableStr != "" {
if containerMode != "sidecar" {
return fmt.Errorf(messages.CSPFK051E, "Secrets refresh is enabled while container mode is set to", containerMode)
}
enabled, _ := strconv.ParseBool(enableStr)
duration, e := time.ParseDuration(intervalStr)
switch {
// the user set enabled to true and did not set the interval
case intervalStr == "" && enableStr != "":
err = nil
// duration can't be parsed
case e != nil:
err = fmt.Errorf(messages.CSPFK050E, intervalStr, e.Error())
// check if the user explicitly set enable to false
case enabled == false && enableStr != "" && intervalStr != "":
err = fmt.Errorf(messages.CSPFK050E, intervalStr, "Secrets refresh interval set to value while enable is false")
// duration too small
case duration < MinRefreshInterval:
err = fmt.Errorf(messages.CSPFK050E, intervalStr, "Secrets refresh interval must be at least one second")
}
}
return err
}

Loading

0 comments on commit 50bb93e

Please sign in to comment.