diff --git a/charts/ratify/templates/store.yaml b/charts/ratify/templates/store.yaml index 6cece47f3..814e370a4 100644 --- a/charts/ratify/templates/store.yaml +++ b/charts/ratify/templates/store.yaml @@ -33,6 +33,7 @@ spec: authProvider: name: k8Secrets serviceAccountName: {{ include "ratify.serviceAccountName" . }} + secretTimeout: {{ .Values.oras.authProviders.k8secretsSecretTimeout }} {{- end }} {{- if .Values.oras.authProviders.awsEcrBasicEnabled }} authProvider: diff --git a/charts/ratify/values.yaml b/charts/ratify/values.yaml index 87afc984a..b377e0d04 100644 --- a/charts/ratify/values.yaml +++ b/charts/ratify/values.yaml @@ -100,6 +100,7 @@ oras: azureManagedIdentityEnabled: false azureContainerRegistryEndpoints: [] k8secretsEnabled: false + k8secretsSecretTimeout: 43200 awsEcrBasicEnabled: false awsApiOverride: enabled: false diff --git a/docs/design/K8s Secrets AuthProvider.md b/docs/design/K8s Secrets AuthProvider.md index 1ba9f65cf..8352348f3 100644 --- a/docs/design/K8s Secrets AuthProvider.md +++ b/docs/design/K8s Secrets AuthProvider.md @@ -69,7 +69,8 @@ roleRef: "secretName": "test2.ghcr.io", "namespace": "test" } - ] + ], + "secretTimeout": 43200 // in seconds - 43200s or 12h used by default if not specified } } ] @@ -106,6 +107,7 @@ type k8SecretAuthProviderConf struct { Name string `json:"name"` ServiceAccountName string `json:"serviceAccountName,omitempty"` Secrets []secretConfig `json:"secrets,omitempty"` + SecretTimeout uint32 `json:"secretTimeout,omitempty"` } func init() // init calls Register for our k8s-secrets provider @@ -141,6 +143,7 @@ func (d *k8SecretAuthProvider) Provide(artifact string) (AuthConfig, error) { // if .dockercfg secret, deserialize .dockercfg data, see if matching registry host credential exists, extract auth configs and return matching AuthConfig if it exists // if config.json secret do same as above except using non legacy format + // set the cache TTL to the "SecretTimeout" property (defaults to 12h) } ``` diff --git a/pkg/common/oras/authprovider/k8secret_authprovider.go b/pkg/common/oras/authprovider/k8secret_authprovider.go index 50fa03416..a302bc445 100644 --- a/pkg/common/oras/authprovider/k8secret_authprovider.go +++ b/pkg/common/oras/authprovider/k8secret_authprovider.go @@ -52,10 +52,11 @@ type k8SecretAuthProviderConf struct { Name string `json:"name"` ServiceAccountName string `json:"serviceAccountName,omitempty"` Secrets []secretConfig `json:"secrets,omitempty"` + SecretTimeout *uint32 `json:"secretTimeout,omitempty"` } const defaultName = "default" -const secretTimeout = time.Hour * 12 +const defaultSecretTimeout = 3600 * 12 * time.Second // init calls Register for our k8Secrets provider func init() { @@ -65,14 +66,9 @@ func init() { // Create returns a k8AuthProvider instance after parsing auth config and resolving // named K8s secrets func (s *k8SecretProviderFactory) Create(authProviderConfig AuthProviderConfig) (AuthProvider, error) { - conf := k8SecretAuthProviderConf{} - authProviderConfigBytes, err := json.Marshal(authProviderConfig) + conf, err := parseAuthProviderConfig(authProviderConfig) if err != nil { - return nil, re.ErrorCodeConfigInvalid.NewError(re.AuthProvider, "", re.EmptyLink, err, "failed to marshal authentication provider config", re.HideStackTrace) - } - - if err := json.Unmarshal(authProviderConfigBytes, &conf); err != nil { - return nil, re.ErrorCodeConfigInvalid.NewError(re.AuthProvider, "", re.EmptyLink, err, "failed to parse authentication provider configuration", re.HideStackTrace) + return nil, re.ErrorCodeConfigInvalid.NewError(re.AuthProvider, "", re.EmptyLink, err, "failed to deserialize auth provider config", re.HideStackTrace) } clusterConfig, err := rest.InClusterConfig() @@ -102,6 +98,20 @@ func (s *k8SecretProviderFactory) Create(authProviderConfig AuthProviderConfig) }, nil } +func parseAuthProviderConfig(authProviderConfig AuthProviderConfig) (k8SecretAuthProviderConf, error) { + conf := k8SecretAuthProviderConf{} + authProviderConfigBytes, err := json.Marshal(authProviderConfig) + if err != nil { + return conf, re.ErrorCodeConfigInvalid.NewError(re.AuthProvider, "", re.EmptyLink, err, "failed to marshal authentication provider config", re.HideStackTrace) + } + + if err := json.Unmarshal(authProviderConfigBytes, &conf); err != nil { + return conf, re.ErrorCodeConfigInvalid.NewError(re.AuthProvider, "", re.EmptyLink, err, "failed to parse authentication provider configuration", re.HideStackTrace) + } + + return conf, nil +} + // Enabled checks if ratify namespace, config, or cluster client set is nil func (d *k8SecretAuthProvider) Enabled(_ context.Context) bool { if d.ratifyNamespace == "" || d.clusterClientSet == nil { @@ -215,6 +225,14 @@ func (d *k8SecretAuthProvider) resolveCredentialFromSecret(ctx context.Context, Password: authConfig.Password, IdentityToken: authConfig.IdentityToken, Provider: d, - ExpiresOn: time.Now().Add(secretTimeout), + ExpiresOn: time.Now().Add(d.getSecretTimeout()), }, nil } + +func (d *k8SecretAuthProvider) getSecretTimeout() time.Duration { + if d.config.SecretTimeout == nil { + return defaultSecretTimeout + } + + return time.Second * time.Duration(*d.config.SecretTimeout) +} diff --git a/pkg/common/oras/authprovider/k8secret_authprovider_test.go b/pkg/common/oras/authprovider/k8secret_authprovider_test.go index 8dcc3f142..f97550869 100644 --- a/pkg/common/oras/authprovider/k8secret_authprovider_test.go +++ b/pkg/common/oras/authprovider/k8secret_authprovider_test.go @@ -19,6 +19,7 @@ import ( "context" "errors" "testing" + "time" ratifyerrors "github.com/ratify-project/ratify/errors" core "k8s.io/api/core/v1" @@ -329,6 +330,7 @@ func TestProvider_SecretFound_ReturnsSuccess(t *testing.T) { // TestProvide_ServiceAccountSecretFound_ReturnsSuccess tests that the Provide method // returns auth config when a matching credential is found for a service account image pull secret func TestProvider_ServiceAccountSecretFound_ReturnsSuccess(t *testing.T) { + secretTimeout := uint32(7200) k8secretprovider := k8SecretAuthProvider{ ratifyNamespace: "gatekeeper-system", clusterClientSet: fake.NewSimpleClientset(&core.ServiceAccount{ @@ -353,10 +355,65 @@ func TestProvider_ServiceAccountSecretFound_ReturnsSuccess(t *testing.T) { }), config: k8SecretAuthProviderConf{ ServiceAccountName: "ratify-admin", + SecretTimeout: &secretTimeout, }, } - if _, err := k8secretprovider.Provide(context.Background(), "index.docker.io/artifact:v1"); err != nil { + if k8secretprovider.getSecretTimeout() != 7200*time.Second { + t.Fatalf("time passed in config but could not verify the correct secret expiration time") + } + + authConfig, err := k8secretprovider.Provide(context.Background(), "index.docker.io/artifact:v1") + if err != nil { t.Fatalf("Provide failed to get credential with err %v", err) } + + timeDifference := time.Now().Add(time.Duration(secretTimeout) * time.Second).Sub(authConfig.ExpiresOn).Abs() + epsilon := 5 * time.Second + if timeDifference > epsilon { // using 5 seconds as epsilon value for comparison to account for execution delays + t.Fatalf("auth config ExpiresOn is not set properly - expected to be set to 2 hours from current time: %v", err) + } +} + +func TestProvider_NilSecretTimeoutReturnsDefault(t *testing.T) { + authConfig := AuthProviderConfig{ + "name": "k8Secrets", + "serviceAccountName": "ratify-admin", + } + parsedAuthConf, err := parseAuthProviderConfig(authConfig) + if err != nil { + t.Fatalf("could not parse auth config properly: %v", err) + } + + if parsedAuthConf.SecretTimeout != nil { + t.Fatalf("expected SecretTimeout to be nil due to no secretTimeout config in auth provider config") + } + + authProvider := &k8SecretAuthProvider{ + config: parsedAuthConf, + } + if authProvider.getSecretTimeout() != defaultSecretTimeout { + t.Fatalf("expected secret timeout to be defaulted to 12h due to nil secret timeout in auth config") + } +} + +func TestProvider_Deserialization_Success(t *testing.T) { + authProviderConfig := AuthProviderConfig{ + "name": "k8Secrets", + "serviceAccountName": "ratify-admin", + "secretTimeout": 7200, + } + + conf, err := parseAuthProviderConfig(authProviderConfig) + if err != nil { + t.Fatalf("Unable to parse auth provider config: %v", err) + } + + if conf.Name != "k8Secrets" || + conf.ServiceAccountName != "ratify-admin" || + conf.SecretTimeout == nil || + *conf.SecretTimeout != 7200 || + conf.Secrets != nil { + t.Fatalf("Parsed auth config does not match the input: %v", err) + } } diff --git a/pkg/controllers/clusterresource/store_controller_test.go b/pkg/controllers/clusterresource/store_controller_test.go index faa0242fa..8770e8fb0 100644 --- a/pkg/controllers/clusterresource/store_controller_test.go +++ b/pkg/controllers/clusterresource/store_controller_test.go @@ -291,7 +291,7 @@ func resetStoreMap() { } func getOrasStoreSpec(pluginName, pluginPath string) configv1beta1.StoreSpec { - var parametersString = "{\"authProvider\":{\"name\":\"k8Secrets\",\"secrets\":[{\"secretName\":\"myregistrykey\"}]},\"cosignEnabled\":false,\"useHttp\":false}" + var parametersString = "{\"authProvider\":{\"name\":\"k8Secrets\",\"secrets\":[{\"secretName\":\"myregistrykey\"}],\"secretTimeout\":3600},\"cosignEnabled\":false,\"useHttp\":false}" var storeParameters = []byte(parametersString) return configv1beta1.StoreSpec{