From e64756afbc3b61b1bcfcb6d2df7f9dca1f61de25 Mon Sep 17 00:00:00 2001 From: Marc Sensenich Date: Sat, 18 Apr 2026 12:24:41 -0400 Subject: [PATCH 1/6] feat: AWS secret engine config and role types --- PROJECT | 26 ++ api/v1alpha1/awssecretengineconfig_test.go | 276 +++++++++++++ api/v1alpha1/awssecretengineconfig_types.go | 369 ++++++++++++++++++ api/v1alpha1/awssecretengineconfig_webhook.go | 124 ++++++ api/v1alpha1/awssecretenginerole_test.go | 274 +++++++++++++ api/v1alpha1/awssecretenginerole_types.go | 258 ++++++++++++ api/v1alpha1/awssecretenginerole_webhook.go | 167 ++++++++ api/v1alpha1/utils/vaultobject_test.go | 14 +- api/v1alpha1/webhook_suite_test.go | 6 + api/v1alpha1/zz_generated.deepcopy.go | 272 +++++++++++++ ...tcop.redhat.io_awssecretengineconfigs.yaml | 352 +++++++++++++++++ ...hatcop.redhat.io_awssecretengineroles.yaml | 307 +++++++++++++++ config/crd/kustomization.yaml | 6 + ...cainjection_in_awssecretengineconfigs.yaml | 7 + .../cainjection_in_awssecretengineroles.yaml | 7 + .../webhook_in_awssecretengineconfigs.yaml | 16 + .../webhook_in_awssecretengineroles.yaml | 16 + .../awssecretengineconfig_editor_role.yaml | 31 ++ .../awssecretengineconfig_viewer_role.yaml | 27 ++ .../rbac/awssecretenginerole_editor_role.yaml | 31 ++ .../rbac/awssecretenginerole_viewer_role.yaml | 27 ++ config/rbac/role.yaml | 52 +++ config/samples/kustomization.yaml | 2 + ...hatcop_v1alpha1_awssecretengineconfig.yaml | 12 + ...edhatcop_v1alpha1_awssecretenginerole.yaml | 12 + config/webhook/manifests.yaml | 80 ++++ .../awssecretengineconfig_controller.go | 77 ++++ controllers/awssecretenginerole_controller.go | 75 ++++ go.mod | 2 +- go.sum | 2 - main.go | 22 +- 31 files changed, 2938 insertions(+), 11 deletions(-) create mode 100644 api/v1alpha1/awssecretengineconfig_test.go create mode 100644 api/v1alpha1/awssecretengineconfig_types.go create mode 100644 api/v1alpha1/awssecretengineconfig_webhook.go create mode 100644 api/v1alpha1/awssecretenginerole_test.go create mode 100644 api/v1alpha1/awssecretenginerole_types.go create mode 100644 api/v1alpha1/awssecretenginerole_webhook.go create mode 100644 config/crd/bases/redhatcop.redhat.io_awssecretengineconfigs.yaml create mode 100644 config/crd/bases/redhatcop.redhat.io_awssecretengineroles.yaml create mode 100644 config/crd/patches/cainjection_in_awssecretengineconfigs.yaml create mode 100644 config/crd/patches/cainjection_in_awssecretengineroles.yaml create mode 100644 config/crd/patches/webhook_in_awssecretengineconfigs.yaml create mode 100644 config/crd/patches/webhook_in_awssecretengineroles.yaml create mode 100644 config/rbac/awssecretengineconfig_editor_role.yaml create mode 100644 config/rbac/awssecretengineconfig_viewer_role.yaml create mode 100644 config/rbac/awssecretenginerole_editor_role.yaml create mode 100644 config/rbac/awssecretenginerole_viewer_role.yaml create mode 100644 config/samples/redhatcop_v1alpha1_awssecretengineconfig.yaml create mode 100644 config/samples/redhatcop_v1alpha1_awssecretenginerole.yaml create mode 100644 controllers/awssecretengineconfig_controller.go create mode 100644 controllers/awssecretenginerole_controller.go diff --git a/PROJECT b/PROJECT index f9c26997..597a63c2 100644 --- a/PROJECT +++ b/PROJECT @@ -573,4 +573,30 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: redhat.io + group: redhatcop + kind: AWSSecretEngineRole + path: github.com/redhat-cop/vault-config-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: redhat.io + group: redhatcop + kind: AWSSecretEngineConfig + path: github.com/redhat-cop/vault-config-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1alpha1/awssecretengineconfig_test.go b/api/v1alpha1/awssecretengineconfig_test.go new file mode 100644 index 00000000..6e319706 --- /dev/null +++ b/api/v1alpha1/awssecretengineconfig_test.go @@ -0,0 +1,276 @@ +package v1alpha1 + +import ( + "testing" + + vaultutils "github.com/redhat-cop/vault-config-operator/api/v1alpha1/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAWSSecretEngineConfigGetPath(t *testing.T) { + tests := []struct { + name string + config *AWSSecretEngineConfig + expectedPath string + }{ + { + name: "basic path", + config: &AWSSecretEngineConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "aws-config"}, + Spec: AWSSecretEngineConfigSpec{ + Path: "aws", + }, + }, + expectedPath: vaultutils.CleansePath("aws/config/root"), + }, + { + name: "custom path", + config: &AWSSecretEngineConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "custom-config"}, + Spec: AWSSecretEngineConfigSpec{ + Path: "custom/aws/path", + }, + }, + expectedPath: vaultutils.CleansePath("custom/aws/path/config/root"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.config.GetPath() + if result != tt.expectedPath { + t.Errorf("GetPath() = %v, expected %v", result, tt.expectedPath) + } + }) + } +} + +func TestAWSSEConfigToMap(t *testing.T) { + config := AWSSEConfig{ + MaxRetries: 3, + Region: "us-west-2", + IAMEndpoint: "https://iam.example.com", + STSEndpoint: "https://sts.example.com", + STSRegion: "us-west-2", + STSFallbackEndpoints: []string{"https://sts1.example.com", "https://sts2.example.com"}, + STSFallbackRegions: []string{"us-east-1", "eu-west-1"}, + UsernameTemplate: "vault-{{.RoleName}}-{{unix_time}}", + retrievedAccessKey: "AKIAIOSFODNN7EXAMPLE", + retrievedSecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + } + + result := config.toMap() + + if result["max_retries"] != 3 { + t.Errorf("max_retries = %v, expected 3", result["max_retries"]) + } + + if result["region"] != "us-west-2" { + t.Errorf("region = %v, expected 'us-west-2'", result["region"]) + } + + if result["iam_endpoint"] != "https://iam.example.com" { + t.Errorf("iam_endpoint = %v", result["iam_endpoint"]) + } + + if result["sts_endpoint"] != "https://sts.example.com" { + t.Errorf("sts_endpoint = %v", result["sts_endpoint"]) + } + + if result["sts_region"] != "us-west-2" { + t.Errorf("sts_region = %v", result["sts_region"]) + } + + stsFallbackEndpoints, ok := result["sts_fallback_endpoints"].([]string) + if !ok { + t.Fatalf("sts_fallback_endpoints should be []string, got %T", result["sts_fallback_endpoints"]) + } + if len(stsFallbackEndpoints) != 2 { + t.Errorf("expected 2 sts_fallback_endpoints, got %d", len(stsFallbackEndpoints)) + } + + stsFallbackRegions, ok := result["sts_fallback_regions"].([]string) + if !ok { + t.Fatalf("sts_fallback_regions should be []string, got %T", result["sts_fallback_regions"]) + } + if len(stsFallbackRegions) != 2 { + t.Errorf("expected 2 sts_fallback_regions, got %d", len(stsFallbackRegions)) + } + + if result["username_template"] != "vault-{{.RoleName}}-{{unix_time}}" { + t.Errorf("username_template = %v", result["username_template"]) + } + + if result["access_key"] != "AKIAIOSFODNN7EXAMPLE" { + t.Errorf("access_key = %v", result["access_key"]) + } + + if result["secret_key"] != "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" { + t.Errorf("secret_key = %v", result["secret_key"]) + } + + // Fields that should not be in the map when not set + if _, exists := result["identity_token_audience"]; exists { + t.Error("identity_token_audience should not be in map when empty") + } + if _, exists := result["role_arn"]; exists { + t.Error("role_arn should not be in map when empty") + } + if _, exists := result["rotation_period"]; exists { + t.Error("rotation_period should not be in map when 0") + } + if _, exists := result["rotation_schedule"]; exists { + t.Error("rotation_schedule should not be in map when empty") + } +} + +func TestAWSSEConfigToMapWithRotation(t *testing.T) { + config := AWSSEConfig{ + MaxRetries: -1, + RotationPeriod: 86400, + RotationWindow: 3600, + DisableAutomatedRotation: true, + retrievedAccessKey: "AKIAIOSFODNN7EXAMPLE", + retrievedSecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + } + + result := config.toMap() + + if result["max_retries"] != -1 { + t.Errorf("max_retries = %v, expected -1", result["max_retries"]) + } + + if result["rotation_period"] != 86400 { + t.Errorf("rotation_period = %v, expected 86400", result["rotation_period"]) + } + + if result["rotation_window"] != 3600 { + t.Errorf("rotation_window = %v, expected 3600", result["rotation_window"]) + } + + disableRotation, ok := result["disable_automated_rotation"].(bool) + if !ok { + t.Fatalf("disable_automated_rotation should be bool, got %T", result["disable_automated_rotation"]) + } + if !disableRotation { + t.Errorf("disable_automated_rotation = %v, expected true", disableRotation) + } +} + +func TestAWSSEConfigToMapWithIdentityToken(t *testing.T) { + config := AWSSEConfig{ + MaxRetries: -1, + RoleARN: "arn:aws:iam::123456789012:role/VaultRole", + IdentityTokenAudience: "vault.example.com", + IdentityTokenTTL: "7200", + } + + result := config.toMap() + + if result["role_arn"] != "arn:aws:iam::123456789012:role/VaultRole" { + t.Errorf("role_arn = %v", result["role_arn"]) + } + + if result["identity_token_audience"] != "vault.example.com" { + t.Errorf("identity_token_audience = %v", result["identity_token_audience"]) + } + + if result["identity_token_ttl"] != "7200" { + t.Errorf("identity_token_ttl = %v", result["identity_token_ttl"]) + } + + // Should not include access/secret keys when using identity token + if _, exists := result["access_key"]; exists { + t.Error("access_key should not be in map when using identity token") + } + if _, exists := result["secret_key"]; exists { + t.Error("secret_key should not be in map when using identity token") + } +} + +func TestAWSSecretEngineConfigIsEquivalentMatching(t *testing.T) { + config := &AWSSecretEngineConfig{ + Spec: AWSSecretEngineConfigSpec{ + Path: "aws", + AWSSEConfig: AWSSEConfig{ + MaxRetries: 3, + Region: "us-west-2", + }, + }, + } + + payload := config.Spec.AWSSEConfig.toMap() + + if !config.IsEquivalentToDesiredState(payload) { + t.Error("expected matching payload to be equivalent") + } +} + +func TestAWSSecretEngineConfigIsEquivalentNonMatching(t *testing.T) { + config := &AWSSecretEngineConfig{ + Spec: AWSSecretEngineConfigSpec{ + Path: "aws", + AWSSEConfig: AWSSEConfig{ + MaxRetries: 3, + RotationPeriod: 86400, + }, + }, + } + + payload := config.Spec.AWSSEConfig.toMap() + payload["rotation_period"] = 172800 + + if config.IsEquivalentToDesiredState(payload) { + t.Error("expected non-matching payload (different rotation_period) to NOT be equivalent") + } +} + +func TestAWSSecretEngineConfigIsEquivalentExtraFields(t *testing.T) { + config := &AWSSecretEngineConfig{ + Spec: AWSSecretEngineConfigSpec{ + Path: "aws", + AWSSEConfig: AWSSEConfig{ + MaxRetries: -1, + Region: "us-east-1", + }, + }, + } + + payload := config.Spec.AWSSEConfig.toMap() + payload["extra_vault_field"] = "some-value" + + if config.IsEquivalentToDesiredState(payload) { + t.Error("expected payload with extra fields to NOT be equivalent (bare reflect.DeepEqual)") + } +} + +func TestAWSSecretEngineConfigIsDeletable(t *testing.T) { + config := &AWSSecretEngineConfig{} + if !config.IsDeletable() { + t.Error("expected AWSSecretEngineConfig to be deletable") + } +} + +func TestAWSSecretEngineConfigConditions(t *testing.T) { + config := &AWSSecretEngineConfig{} + + conditions := []metav1.Condition{ + { + Type: "ReconcileSuccessful", + Status: metav1.ConditionTrue, + }, + } + + config.SetConditions(conditions) + got := config.GetConditions() + + if len(got) != 1 { + t.Fatalf("expected 1 condition, got %d", len(got)) + } + if got[0].Type != "ReconcileSuccessful" { + t.Errorf("expected condition type 'ReconcileSuccessful', got %v", got[0].Type) + } + if got[0].Status != metav1.ConditionTrue { + t.Errorf("expected condition status True, got %v", got[0].Status) + } +} diff --git a/api/v1alpha1/awssecretengineconfig_types.go b/api/v1alpha1/awssecretengineconfig_types.go new file mode 100644 index 00000000..d9b25af1 --- /dev/null +++ b/api/v1alpha1/awssecretengineconfig_types.go @@ -0,0 +1,369 @@ +/* +Copyright 2021. + +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 v1alpha1 + +import ( + "context" + "errors" + "reflect" + + vaultutils "github.com/redhat-cop/vault-config-operator/api/v1alpha1/utils" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// AWSSecretEngineConfigSpec defines the desired state of AWSSecretEngineConfig +type AWSSecretEngineConfigSpec struct { + // Connection represents the information needed to connect to Vault. This operator uses the standard Vault environment variables to connect to Vault. If you need to override those settings and for example connect to a different Vault instance, you can do with this section of the CR. + // +kubebuilder:validation:Optional + Connection *vaultutils.VaultConnection `json:"connection,omitempty"` + + // Authentication is the kube auth configuraiton to be used to execute this request + // +kubebuilder:validation:Required + Authentication vaultutils.KubeAuthConfiguration `json:"authentication,omitempty"` + + // Path at which to make the configuration. + // The final path in Vault will be {[spec.authentication.namespace]}/{spec.path}/config/root. + // The authentication role must have the following capabilities = [ "create", "read", "update", "delete"] on that path. + // +kubebuilder:validation:Required + Path vaultutils.Path `json:"path,omitempty"` + + // AWSCredentials consists of access_key and secret_key, which can be created as Kubernetes Secret, VaultSecret or RandomSecret + // +kubebuilder:validation:Optional + AWSCredentials vaultutils.RootCredentialConfig `json:"awsCredentials,omitempty"` + + // +kubebuilder:validation:Required + AWSSEConfig `json:",inline"` +} + +// AWSSecretEngineConfigStatus defines the observed state of AWSSecretEngineConfig +type AWSSecretEngineConfigStatus struct { + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// AWSSecretEngineConfig is the Schema for the awssecretengineconfigs API +type AWSSecretEngineConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AWSSecretEngineConfigSpec `json:"spec,omitempty"` + Status AWSSecretEngineConfigStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AWSSecretEngineConfigList contains a list of AWSSecretEngineConfig +type AWSSecretEngineConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AWSSecretEngineConfig `json:"items"` +} + +type AWSSEConfig struct { + // Number of max retries the client should use for recoverable errors. The default (-1) falls back to the AWS SDK's default behavior. + // +kubebuilder:validation:Optional + // +kubebuilder:default=-1 + MaxRetries int `json:"maxRetries,omitempty"` + + // Role ARN to assume for plugin workload identity federation (Enterprise). Required with identity_token_audience. + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + RoleARN string `json:"roleArn,omitempty"` + + // The audience claim value for plugin identity tokens (Enterprise). Must match an allowed audience configured for the target IAM OIDC identity provider. + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + IdentityTokenAudience string `json:"identityTokenAudience,omitempty"` + + // The TTL of generated tokens (Enterprise). Defaults to 1 hour. Uses duration format strings. + // +kubebuilder:validation:Optional + // +kubebuilder:default="3600" + IdentityTokenTTL string `json:"identityTokenTtl,omitempty"` + + // Specifies the AWS region. If not set it will use the AWS_REGION env var, AWS_DEFAULT_REGION env var, or us-east-1 in that order. + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + Region string `json:"region,omitempty"` + + // Specifies a custom HTTP IAM endpoint to use. + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + IAMEndpoint string `json:"iamEndpoint,omitempty"` + + // Specifies a custom HTTP STS endpoint to use. + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + STSEndpoint string `json:"stsEndpoint,omitempty"` + + // Specifies a custom STS region to use (should match sts_endpoint). + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + STSRegion string `json:"stsRegion,omitempty"` + + // Specifies an ordered list of fallback STS endpoints to use. + // +kubebuilder:validation:Optional + STSFallbackEndpoints []string `json:"stsFallbackEndpoints,omitempty"` + + // Specifies an ordered list of fallback STS regions to use (should match fallback endpoints). + // +kubebuilder:validation:Optional + STSFallbackRegions []string `json:"stsFallbackRegions,omitempty"` + + // Template describing how dynamic usernames are generated. + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + UsernameTemplate string `json:"usernameTemplate,omitempty"` + + // The amount of time, in seconds, Vault should wait before rotating the root credential (Enterprise). A zero value tells Vault not to rotate the root credential. + // +kubebuilder:validation:Optional + // +kubebuilder:default=0 + RotationPeriod int `json:"rotationPeriod,omitempty"` + + // The schedule, in cron-style time format, defining the schedule on which Vault should rotate the root token (Enterprise). + // +kubebuilder:validation:Optional + // +kubebuilder:default="" + RotationSchedule string `json:"rotationSchedule,omitempty"` + + // The maximum amount of time, in seconds, allowed to complete a rotation when a scheduled token rotation occurs (Enterprise). + // +kubebuilder:validation:Optional + // +kubebuilder:default=0 + RotationWindow int `json:"rotationWindow,omitempty"` + + // Cancels all upcoming rotations of the root credential until unset (Enterprise). + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + DisableAutomatedRotation bool `json:"disableAutomatedRotation,omitempty"` + + retrievedAccessKey string `json:"-"` + + retrievedSecretKey string `json:"-"` +} + +var _ vaultutils.VaultObject = &AWSSecretEngineConfig{} +var _ vaultutils.ConditionsAware = &AWSSecretEngineConfig{} + +func init() { + SchemeBuilder.Register(&AWSSecretEngineConfig{}, &AWSSecretEngineConfigList{}) +} + +func (d *AWSSecretEngineConfig) IsDeletable() bool { + return true +} + +func (r *AWSSecretEngineConfig) SetConditions(conditions []metav1.Condition) { + r.Status.Conditions = conditions +} + +func (d *AWSSecretEngineConfig) GetVaultConnection() *vaultutils.VaultConnection { + return d.Spec.Connection +} + +func (r *AWSSecretEngineConfig) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *AWSSecretEngineConfig) GetKubeAuthConfiguration() *vaultutils.KubeAuthConfiguration { + return &r.Spec.Authentication +} + +func (d *AWSSecretEngineConfig) GetPath() string { + return string(d.Spec.Path) + "/" + "config/root" +} + +func (d *AWSSecretEngineConfig) GetPayload() map[string]interface{} { + return d.Spec.toMap() +} + +func (r *AWSSecretEngineConfig) IsEquivalentToDesiredState(payload map[string]interface{}) bool { + desiredState := r.Spec.AWSSEConfig.toMap() + return reflect.DeepEqual(desiredState, payload) +} + +func (r *AWSSecretEngineConfig) IsInitialized() bool { + return true +} + +func (r *AWSSecretEngineConfig) IsValid() (bool, error) { + err := r.isValid() + return err == nil, err +} + +func (r *AWSSecretEngineConfig) isValid() error { + return r.Spec.AWSCredentials.ValidateEitherFromVaultSecretOrFromSecretOrFromRandomSecret() +} + +func (r *AWSSecretEngineConfig) PrepareInternalValues(context context.Context, object client.Object) error { + if reflect.DeepEqual(r.Spec.AWSCredentials, vaultutils.RootCredentialConfig{PasswordKey: "secret_key", UsernameKey: "access_key"}) { + return nil + } + + return r.setInternalCredentials(context) +} + +func (d *AWSSecretEngineConfig) PrepareTLSConfig(context context.Context, object client.Object) error { + return nil +} + +func (r *AWSSecretEngineConfig) setInternalCredentials(context context.Context) error { + log := log.FromContext(context) + kubeClient := context.Value("kubeClient").(client.Client) + if r.Spec.AWSCredentials.RandomSecret != nil { + randomSecret := &RandomSecret{} + err := kubeClient.Get(context, types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.AWSCredentials.RandomSecret.Name, + }, randomSecret) + if err != nil { + log.Error(err, "unable to retrieve RandomSecret", "instance", r) + return err + } + secret, exists, err := vaultutils.ReadSecret(context, randomSecret.GetPath()) + if err != nil { + return err + } + if !exists { + err = errors.New("secret not found") + log.Error(err, "unable to retrieve vault secret", "instance", r) + return err + } + accessKey := secret.Data[r.Spec.AWSCredentials.UsernameKey].(string) + secretKey := secret.Data[r.Spec.AWSCredentials.PasswordKey].(string) + r.setAccessKeyAndSecretKey(accessKey+":"+secretKey, accessKey, secretKey) + return nil + } + if r.Spec.AWSCredentials.Secret != nil { + secret := &corev1.Secret{} + err := kubeClient.Get(context, types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.AWSCredentials.Secret.Name, + }, secret) + if err != nil { + log.Error(err, "unable to retrieve Secret", "instance", r) + return err + } + accessKey, ok := secret.Data[r.Spec.AWSCredentials.UsernameKey] + if !ok { + err := errors.New("unable to find access_key key in secret") + log.Error(err, "unable to retrieve field for Secret", "instance", r) + return err + } + secretKey, ok := secret.Data[r.Spec.AWSCredentials.PasswordKey] + if !ok { + err := errors.New("unable to find secret_key key in secret") + log.Error(err, "unable to retrieve field for Secret", "instance", r) + return err + } + r.setAccessKeyAndSecretKey(string(accessKey)+":"+string(secretKey), string(accessKey), string(secretKey)) + return nil + } + if r.Spec.AWSCredentials.VaultSecret != nil { + secret, exists, err := vaultutils.ReadSecret(context, string(r.Spec.AWSCredentials.VaultSecret.Path)) + if err != nil { + return err + } + if !exists { + err = errors.New("secret not found") + log.Error(err, "unable to retrieve vault secret", "instance", r) + return err + } + accessKey := secret.Data[r.Spec.AWSCredentials.UsernameKey].(string) + secretKey := secret.Data[r.Spec.AWSCredentials.PasswordKey].(string) + r.setAccessKeyAndSecretKey(accessKey+":"+secretKey, accessKey, secretKey) + log.V(1).Info("", "accessKey", accessKey, "secretKey", secretKey) + return nil + } + return errors.New("no aws credentials source specified") +} + +func (r *AWSSecretEngineConfig) setAccessKeyAndSecretKey(secretName string, accessKey string, secretKey string) { + r.Spec.AWSSEConfig.retrievedAccessKey = accessKey + r.Spec.AWSSEConfig.retrievedSecretKey = secretKey +} + +func (i *AWSSEConfig) toMap() map[string]interface{} { + payload := map[string]interface{}{} + + if i.retrievedAccessKey != "" { + payload["access_key"] = i.retrievedAccessKey + } + if i.retrievedSecretKey != "" { + payload["secret_key"] = i.retrievedSecretKey + } + + payload["max_retries"] = i.MaxRetries + + if i.Region != "" { + payload["region"] = i.Region + } + if i.IAMEndpoint != "" { + payload["iam_endpoint"] = i.IAMEndpoint + } + if i.STSEndpoint != "" { + payload["sts_endpoint"] = i.STSEndpoint + } + if i.STSRegion != "" { + payload["sts_region"] = i.STSRegion + } + if len(i.STSFallbackEndpoints) > 0 { + payload["sts_fallback_endpoints"] = i.STSFallbackEndpoints + } + if len(i.STSFallbackRegions) > 0 { + payload["sts_fallback_regions"] = i.STSFallbackRegions + } + if i.UsernameTemplate != "" { + payload["username_template"] = i.UsernameTemplate + } + if i.RoleARN != "" { + payload["role_arn"] = i.RoleARN + } + if i.IdentityTokenAudience != "" { + payload["identity_token_audience"] = i.IdentityTokenAudience + } + if i.IdentityTokenTTL != "" && i.IdentityTokenTTL != "3600" { + payload["identity_token_ttl"] = i.IdentityTokenTTL + } + if i.RotationPeriod != 0 { + payload["rotation_period"] = i.RotationPeriod + } + if i.RotationSchedule != "" { + payload["rotation_schedule"] = i.RotationSchedule + } + if i.RotationWindow != 0 { + payload["rotation_window"] = i.RotationWindow + } + if i.DisableAutomatedRotation { + payload["disable_automated_rotation"] = i.DisableAutomatedRotation + } + + return payload +} + +func (r *AWSSecretEngineConfigSpec) toMap() map[string]interface{} { + return r.AWSSEConfig.toMap() +} diff --git a/api/v1alpha1/awssecretengineconfig_webhook.go b/api/v1alpha1/awssecretengineconfig_webhook.go new file mode 100644 index 00000000..008324b5 --- /dev/null +++ b/api/v1alpha1/awssecretengineconfig_webhook.go @@ -0,0 +1,124 @@ +/* +Copyright 2021. + +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 v1alpha1 + +import ( + "errors" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var awssecretengineconfiglog = logf.Log.WithName("awssecretengineconfig-resource") + +func (r *AWSSecretEngineConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig,mutating=true,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=AWSSecretEngineConfigs,verbs=create;update,versions=v1alpha1,name=mAWSSecretEngineConfig.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &AWSSecretEngineConfig{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *AWSSecretEngineConfig) Default() { + awssecretengineconfiglog.Info("default", "name", r.Name) + + // TODO(user): fill in your defaulting logic. +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig,mutating=false,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=AWSSecretEngineConfigs,verbs=create;update,versions=v1alpha1,name=vAWSSecretEngineConfig.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &AWSSecretEngineConfig{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *AWSSecretEngineConfig) ValidateCreate() (admission.Warnings, error) { + awssecretengineconfiglog.Info("validate create", "name", r.Name) + + return nil, r.validateAWSSecretEngineConfig() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *AWSSecretEngineConfig) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + awssecretengineconfiglog.Info("validate update", "name", r.Name) + + if r.Spec.Path != old.(*AWSSecretEngineConfig).Spec.Path { + return nil, errors.New("spec.path cannot be updated") + } + + return nil, r.validateAWSSecretEngineConfig() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *AWSSecretEngineConfig) ValidateDelete() (admission.Warnings, error) { + awssecretengineconfiglog.Info("validate delete", "name", r.Name) + + return nil, nil +} + +// validateAWSSecretEngineConfig validates the AWS Secret Engine Config spec +func (r *AWSSecretEngineConfig) validateAWSSecretEngineConfig() error { + spec := &r.Spec.AWSSEConfig + + // Validate mutually exclusive credential options + hasAccessKey := r.Spec.AWSCredentials.Secret != nil || r.Spec.AWSCredentials.RandomSecret != nil || r.Spec.AWSCredentials.VaultSecret != nil + hasIdentityTokenAudience := spec.IdentityTokenAudience != "" + + if hasAccessKey && hasIdentityTokenAudience { + return errors.New("spec.awsCredentials and spec.identityTokenAudience are mutually exclusive") + } + + // If using identity token federation, role_arn is required + if hasIdentityTokenAudience && spec.RoleARN == "" { + return errors.New("spec.roleArn is required when spec.identityTokenAudience is specified") + } + + // Validate rotation settings + hasRotationPeriod := spec.RotationPeriod != 0 + hasRotationSchedule := spec.RotationSchedule != "" + + if hasRotationPeriod && hasRotationSchedule { + return errors.New("cannot set both spec.rotationPeriod and spec.rotationSchedule") + } + + if hasRotationPeriod && spec.RotationPeriod < 10 { + return errors.New("spec.rotationPeriod must be at least 10 seconds") + } + + if spec.RotationWindow != 0 && spec.RotationWindow < 3600 { + return errors.New("spec.rotationWindow must be at least 1 hour (3600 seconds)") + } + + if spec.RotationWindow != 0 && hasRotationPeriod { + return errors.New("cannot set spec.rotationWindow when using spec.rotationPeriod") + } + + // Validate STS fallback endpoints and regions match in length + if len(spec.STSFallbackEndpoints) != len(spec.STSFallbackRegions) { + return errors.New("spec.stsFallbackEndpoints and spec.stsFallbackRegions must have the same length") + } + + return nil +} diff --git a/api/v1alpha1/awssecretenginerole_test.go b/api/v1alpha1/awssecretenginerole_test.go new file mode 100644 index 00000000..70172ce3 --- /dev/null +++ b/api/v1alpha1/awssecretenginerole_test.go @@ -0,0 +1,274 @@ +package v1alpha1 + +import ( + "testing" + + vaultutils "github.com/redhat-cop/vault-config-operator/api/v1alpha1/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAWSSecretEngineRoleGetPath(t *testing.T) { + tests := []struct { + name string + role *AWSSecretEngineRole + expectedPath string + }{ + { + name: "with spec.name specified", + role: &AWSSecretEngineRole{ + ObjectMeta: metav1.ObjectMeta{Name: "meta-name"}, + Spec: AWSSecretEngineRoleSpec{ + Path: "aws", + Name: "custom-name", + }, + }, + expectedPath: vaultutils.CleansePath("aws/roles/custom-name"), + }, + { + name: "without spec.name falls back to metadata.name", + role: &AWSSecretEngineRole{ + ObjectMeta: metav1.ObjectMeta{Name: "meta-name"}, + Spec: AWSSecretEngineRoleSpec{ + Path: "aws", + }, + }, + expectedPath: vaultutils.CleansePath("aws/roles/meta-name"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.role.GetPath() + if result != tt.expectedPath { + t.Errorf("GetPath() = %v, expected %v", result, tt.expectedPath) + } + }) + } +} + +func TestAWSSERoleToMap(t *testing.T) { + role := AWSSERole{ + CredentialType: "iam_user", + PolicyARNs: []string{"arn:aws:iam::aws:policy/ReadOnlyAccess"}, + PolicyDocument: `{"Version": "2012-10-17"}`, + IAMGroups: []string{"group1", "group2"}, + IAMTags: []string{"env=prod", "team=platform"}, + UserPath: "/custom/path/", + PermissionsBoundaryARN: "arn:aws:iam::123456789012:policy/boundary", + MFASerialNumber: "arn:aws:iam::123456789012:mfa/user", + } + + result := role.toMap() + + if result["credential_type"] != "iam_user" { + t.Errorf("credential_type = %v, expected 'iam_user'", result["credential_type"]) + } + + policyArns, ok := result["policy_arns"].([]string) + if !ok { + t.Fatalf("policy_arns should be []string, got %T", result["policy_arns"]) + } + if len(policyArns) != 1 || policyArns[0] != "arn:aws:iam::aws:policy/ReadOnlyAccess" { + t.Errorf("policy_arns = %v", policyArns) + } + + if result["policy_document"] != `{"Version": "2012-10-17"}` { + t.Errorf("policy_document = %v", result["policy_document"]) + } + + iamGroups, ok := result["iam_groups"].([]string) + if !ok { + t.Fatalf("iam_groups should be []string, got %T", result["iam_groups"]) + } + if len(iamGroups) != 2 { + t.Errorf("expected 2 iam_groups, got %d", len(iamGroups)) + } + + iamTags, ok := result["iam_tags"].([]string) + if !ok { + t.Fatalf("iam_tags should be []string, got %T", result["iam_tags"]) + } + if len(iamTags) != 2 { + t.Errorf("expected 2 iam_tags, got %d", len(iamTags)) + } + + if result["user_path"] != "/custom/path/" { + t.Errorf("user_path = %v", result["user_path"]) + } + + if result["permissions_boundary_arn"] != "arn:aws:iam::123456789012:policy/boundary" { + t.Errorf("permissions_boundary_arn = %v", result["permissions_boundary_arn"]) + } + + if result["mfa_serial_number"] != "arn:aws:iam::123456789012:mfa/user" { + t.Errorf("mfa_serial_number = %v", result["mfa_serial_number"]) + } + + // Fields that should not be in the map when not set + if _, exists := result["role_arns"]; exists { + t.Error("role_arns should not be in map when empty") + } + if _, exists := result["default_sts_ttl"]; exists { + t.Error("default_sts_ttl should not be in map when empty") + } + if _, exists := result["max_sts_ttl"]; exists { + t.Error("max_sts_ttl should not be in map when empty") + } + if _, exists := result["session_tags"]; exists { + t.Error("session_tags should not be in map when empty") + } + if _, exists := result["external_id"]; exists { + t.Error("external_id should not be in map when empty") + } +} + +func TestAWSSERoleToMapAssumedRole(t *testing.T) { + role := AWSSERole{ + CredentialType: "assumed_role", + RoleARNs: []string{"arn:aws:iam::123456789012:role/MyRole"}, + PolicyARNs: []string{"arn:aws:iam::aws:policy/ReadOnlyAccess"}, + SessionTags: []string{"project=foo", "dept=engineering"}, + DefaultSTSTTL: "1h", + MaxSTSTTL: "12h", + ExternalID: "external-id-123", + } + + result := role.toMap() + + if result["credential_type"] != "assumed_role" { + t.Errorf("credential_type = %v", result["credential_type"]) + } + + roleArns, ok := result["role_arns"].([]string) + if !ok { + t.Fatalf("role_arns should be []string, got %T", result["role_arns"]) + } + if len(roleArns) != 1 || roleArns[0] != "arn:aws:iam::123456789012:role/MyRole" { + t.Errorf("role_arns = %v", roleArns) + } + + sessionTags, ok := result["session_tags"].([]string) + if !ok { + t.Fatalf("session_tags should be []string, got %T", result["session_tags"]) + } + if len(sessionTags) != 2 { + t.Errorf("expected 2 session_tags, got %d", len(sessionTags)) + } + + if result["default_sts_ttl"] != "1h" { + t.Errorf("default_sts_ttl = %v", result["default_sts_ttl"]) + } + + if result["max_sts_ttl"] != "12h" { + t.Errorf("max_sts_ttl = %v", result["max_sts_ttl"]) + } + + if result["external_id"] != "external-id-123" { + t.Errorf("external_id = %v", result["external_id"]) + } + + // Fields that should not be in the map for assumed_role + if _, exists := result["user_path"]; exists { + t.Error("user_path should not be in map for assumed_role") + } + if _, exists := result["permissions_boundary_arn"]; exists { + t.Error("permissions_boundary_arn should not be in map for assumed_role") + } + if _, exists := result["iam_tags"]; exists { + t.Error("iam_tags should not be in map for assumed_role") + } + if _, exists := result["mfa_serial_number"]; exists { + t.Error("mfa_serial_number should not be in map for assumed_role") + } +} + +func TestAWSSecretEngineRoleIsEquivalentMatching(t *testing.T) { + role := &AWSSecretEngineRole{ + Spec: AWSSecretEngineRoleSpec{ + Path: "aws", + AWSSERole: AWSSERole{ + CredentialType: "iam_user", + PolicyARNs: []string{"arn:aws:iam::aws:policy/ReadOnlyAccess"}, + PolicyDocument: `{"Version": "2012-10-17"}`, + IAMGroups: []string{"group1"}, + UserPath: "/users/", + }, + }, + } + + payload := role.Spec.AWSSERole.toMap() + + if !role.IsEquivalentToDesiredState(payload) { + t.Error("expected matching payload to be equivalent") + } +} + +func TestAWSSecretEngineRoleIsEquivalentNonMatching(t *testing.T) { + role := &AWSSecretEngineRole{ + Spec: AWSSecretEngineRoleSpec{ + Path: "aws", + AWSSERole: AWSSERole{ + CredentialType: "assumed_role", + RoleARNs: []string{"arn:aws:iam::123456789012:role/MyRole"}, + DefaultSTSTTL: "1h", + }, + }, + } + + payload := role.Spec.AWSSERole.toMap() + payload["default_sts_ttl"] = "2h" + + if role.IsEquivalentToDesiredState(payload) { + t.Error("expected non-matching payload (different default_sts_ttl) to NOT be equivalent") + } +} + +func TestAWSSecretEngineRoleIsEquivalentExtraFields(t *testing.T) { + role := &AWSSecretEngineRole{ + Spec: AWSSecretEngineRoleSpec{ + Path: "aws", + AWSSERole: AWSSERole{ + CredentialType: "iam_user", + PolicyDocument: `{"Version": "2012-10-17"}`, + }, + }, + } + + payload := role.Spec.AWSSERole.toMap() + payload["extra_vault_field"] = "some-value" + + if role.IsEquivalentToDesiredState(payload) { + t.Error("expected payload with extra fields to NOT be equivalent (bare reflect.DeepEqual)") + } +} + +func TestAWSSecretEngineRoleIsDeletable(t *testing.T) { + role := &AWSSecretEngineRole{} + if !role.IsDeletable() { + t.Error("expected AWSSecretEngineRole to be deletable") + } +} + +func TestAWSSecretEngineRoleConditions(t *testing.T) { + role := &AWSSecretEngineRole{} + + conditions := []metav1.Condition{ + { + Type: "ReconcileSuccessful", + Status: metav1.ConditionTrue, + }, + } + + role.SetConditions(conditions) + got := role.GetConditions() + + if len(got) != 1 { + t.Fatalf("expected 1 condition, got %d", len(got)) + } + if got[0].Type != "ReconcileSuccessful" { + t.Errorf("expected condition type 'ReconcileSuccessful', got %v", got[0].Type) + } + if got[0].Status != metav1.ConditionTrue { + t.Errorf("expected condition status True, got %v", got[0].Status) + } +} diff --git a/api/v1alpha1/awssecretenginerole_types.go b/api/v1alpha1/awssecretenginerole_types.go new file mode 100644 index 00000000..53df1a92 --- /dev/null +++ b/api/v1alpha1/awssecretenginerole_types.go @@ -0,0 +1,258 @@ +/* +Copyright 2021. + +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 v1alpha1 + +import ( + "context" + "reflect" + + vaultutils "github.com/redhat-cop/vault-config-operator/api/v1alpha1/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// AWSSecretEngineRoleSpec defines the desired state of AWSSecretEngineRole +type AWSSecretEngineRoleSpec struct { + // Connection represents the information needed to connect to Vault. This operator uses the standard Vault environment variables to connect to Vault. If you need to override those settings and for example connect to a different Vault instance, you can do with this section of the CR. + // +kubebuilder:validation:Optional + Connection *vaultutils.VaultConnection `json:"connection,omitempty"` + + // Authentication is the kube auth configuraiton to be used to execute this request + // +kubebuilder:validation:Required + Authentication vaultutils.KubeAuthConfiguration `json:"authentication,omitempty"` + + // Path at which to make the configuration. + // The final path in Vault will be {[spec.authentication.namespace]}/auth/{spec.path}/groups/{metadata.name}. + // The authentication role must have the following capabilities = [ "create", "read", "update", "delete"] on that path. + // +kubebuilder:validation:Required + Path vaultutils.Path `json:"path,omitempty"` + + AWSSERole `json:",inline"` + + // The name of the object created in Vault. If this is specified it takes precedence over {metatada.name} + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern:=`[a-z0-9]([-a-z0-9]*[a-z0-9])?` + Name string `json:"name,omitempty"` +} + +// AWSSecretEngineRoleStatus defines the observed state of AWSSecretEngineRole +type AWSSecretEngineRoleStatus struct { + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// AWSSecretEngineRole is the Schema for the awssecretengineroles API +type AWSSecretEngineRole struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AWSSecretEngineRoleSpec `json:"spec,omitempty"` + Status AWSSecretEngineRoleStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AWSSecretEngineRoleList contains a list of AWSSecretEngineRole +type AWSSecretEngineRoleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AWSSecretEngineRole `json:"items"` +} + +type AWSSERole struct { + // Specifies the type of credential to be used when retrieving credentials from the role. + // Must be one of iam_user, assumed_role, federation_token, or session_token. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=iam_user;assumed_role;federation_token;session_token + CredentialType string `json:"credentialType,omitempty"` + + // Specifies the ARNs of the AWS roles this Vault role is allowed to assume. + // Required when credential_type is assumed_role and prohibited otherwise. + // +kubebuilder:validation:Optional + RoleARNs []string `json:"roleArns,omitempty"` + + // Specifies a list of AWS managed policy ARNs. The behavior depends on the credential type. + // With iam_user, the policies will be attached to IAM users when they are requested. + // With assumed_role and federation_token, the policy ARNs will act as a filter on what the credentials can do. + // +kubebuilder:validation:Optional + PolicyARNs []string `json:"policyArns,omitempty"` + + // The IAM policy document for the role. The behavior depends on the credential type. + // With iam_user, the policy document will be attached to the IAM user generated. + // With assumed_role and federation_token, the policy document will act as a filter on what the credentials can do. + // +kubebuilder:validation:Optional + PolicyDocument string `json:"policyDocument,omitempty"` + + // A list of IAM group names. IAM users generated against this vault role will be added to these IAM Groups. + // For a credential type of assumed_role or federation_token, the policies sent to the corresponding AWS call + // will be the policies from each group in iam_groups combined with the policy_document and policy_arns parameters. + // +kubebuilder:validation:Optional + IAMGroups []string `json:"iamGroups,omitempty"` + + // A list of strings representing a key/value pair to be used as a tag for any iam_user user that is created by this role. + // Format is a key and value separated by an = (e.g. test_key=value). + // +kubebuilder:validation:Optional + IAMTags []string `json:"iamTags,omitempty"` + + // The default TTL for STS credentials. When a TTL is not specified when STS credentials are requested, + // and a default TTL is specified on the role, then this default TTL will be used. + // Valid only when credential_type is one of assumed_role or federation_token. + // +kubebuilder:validation:Optional + DefaultSTSTTL string `json:"defaultStsTtl,omitempty"` + + // The max allowed TTL for STS credentials (credentials TTL are capped to max_sts_ttl). + // Valid only when credential_type is one of assumed_role or federation_token. + // +kubebuilder:validation:Optional + MaxSTSTTL string `json:"maxStsTtl,omitempty"` + + // The set of key-value pairs to be included as tags for the STS session. + // Format is key=value. + // Valid only when credential_type is set to assumed_role. + // +kubebuilder:validation:Optional + SessionTags []string `json:"sessionTags,omitempty"` + + // The external ID to use when assuming the role. + // Valid only when credential_type is set to assumed_role. + // +kubebuilder:validation:Optional + ExternalID string `json:"externalId,omitempty"` + + // The path for the user name. Valid only when credential_type is iam_user. Default is / + // +kubebuilder:validation:Optional + UserPath string `json:"userPath,omitempty"` + + // The ARN of the AWS Permissions Boundary to attach to IAM users created in the role. + // Valid only when credential_type is iam_user. If not specified, then no permissions boundary policy will be attached. + // +kubebuilder:validation:Optional + PermissionsBoundaryARN string `json:"permissionsBoundaryArn,omitempty"` + + // The ARN or hardware device number of the device configured to the IAM user for multi-factor authentication. + // Only required if the IAM user has an MFA device set up in AWS. + // +kubebuilder:validation:Optional + MFASerialNumber string `json:"mfaSerialNumber,omitempty"` +} + +var _ vaultutils.VaultObject = &AWSSecretEngineRole{} +var _ vaultutils.ConditionsAware = &AWSSecretEngineRole{} + +func init() { + SchemeBuilder.Register(&AWSSecretEngineRole{}, &AWSSecretEngineRoleList{}) +} + +func (r *AWSSecretEngineRole) GetKubeAuthConfiguration() *vaultutils.KubeAuthConfiguration { + return &r.Spec.Authentication +} + +func (d *AWSSecretEngineRole) GetPath() string { + if d.Spec.Name != "" { + return vaultutils.CleansePath(string(d.Spec.Path) + "/" + "roles" + "/" + d.Spec.Name) + } + return vaultutils.CleansePath(string(d.Spec.Path) + "/" + "roles" + "/" + d.Name) +} + +func (d *AWSSecretEngineRole) GetPayload() map[string]interface{} { + return d.Spec.toMap() +} + +func (d *AWSSecretEngineRole) IsDeletable() bool { + return true +} + +func (d *AWSSecretEngineRole) GetVaultConnection() *vaultutils.VaultConnection { + return d.Spec.Connection +} + +func (d *AWSSecretEngineRole) IsEquivalentToDesiredState(payload map[string]interface{}) bool { + desiredState := d.Spec.AWSSERole.toMap() + return reflect.DeepEqual(desiredState, payload) +} + +func (d *AWSSecretEngineRole) IsInitialized() bool { + return true +} + +func (r *AWSSecretEngineRole) IsValid() (bool, error) { + return true, nil +} + +func (d *AWSSecretEngineRole) PrepareInternalValues(context context.Context, object client.Object) error { + return nil +} + +func (d *AWSSecretEngineRole) PrepareTLSConfig(context context.Context, object client.Object) error { + return nil +} + +func (r *AWSSecretEngineRole) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *AWSSecretEngineRole) SetConditions(conditions []metav1.Condition) { + r.Status.Conditions = conditions +} + +func (i *AWSSERole) toMap() map[string]interface{} { + payload := map[string]interface{}{} + payload["credential_type"] = i.CredentialType + + if len(i.RoleARNs) > 0 { + payload["role_arns"] = i.RoleARNs + } + if len(i.PolicyARNs) > 0 { + payload["policy_arns"] = i.PolicyARNs + } + if i.PolicyDocument != "" { + payload["policy_document"] = i.PolicyDocument + } + if len(i.IAMGroups) > 0 { + payload["iam_groups"] = i.IAMGroups + } + if len(i.IAMTags) > 0 { + payload["iam_tags"] = i.IAMTags + } + if i.DefaultSTSTTL != "" { + payload["default_sts_ttl"] = i.DefaultSTSTTL + } + if i.MaxSTSTTL != "" { + payload["max_sts_ttl"] = i.MaxSTSTTL + } + if len(i.SessionTags) > 0 { + payload["session_tags"] = i.SessionTags + } + if i.ExternalID != "" { + payload["external_id"] = i.ExternalID + } + if i.UserPath != "" { + payload["user_path"] = i.UserPath + } + if i.PermissionsBoundaryARN != "" { + payload["permissions_boundary_arn"] = i.PermissionsBoundaryARN + } + if i.MFASerialNumber != "" { + payload["mfa_serial_number"] = i.MFASerialNumber + } + + return payload +} diff --git a/api/v1alpha1/awssecretenginerole_webhook.go b/api/v1alpha1/awssecretenginerole_webhook.go new file mode 100644 index 00000000..624148db --- /dev/null +++ b/api/v1alpha1/awssecretenginerole_webhook.go @@ -0,0 +1,167 @@ +/* +Copyright 2021. + +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 v1alpha1 + +import ( + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var awssecretenginerolelog = logf.Log.WithName("awssecretenginerole-resource") + +func (r *AWSSecretEngineRole) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-redhatcop-redhat-io-v1alpha1-awssecretenginerole,mutating=true,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=awssecretengineroles,verbs=create;update,versions=v1alpha1,name=mawssecretenginerole.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &AWSSecretEngineRole{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *AWSSecretEngineRole) Default() { + awssecretenginerolelog.Info("default", "name", r.Name) + + // TODO(user): fill in your defaulting logic. +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-redhatcop-redhat-io-v1alpha1-awssecretenginerole,mutating=false,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=awssecretengineroles,verbs=create;update,versions=v1alpha1,name=vawssecretenginerole.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &AWSSecretEngineRole{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *AWSSecretEngineRole) ValidateCreate() (admission.Warnings, error) { + awssecretenginerolelog.Info("validate create", "name", r.Name) + + return nil, r.validateAWSSecretEngineRole() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *AWSSecretEngineRole) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + awssecretenginerolelog.Info("validate update", "name", r.Name) + + if r.Spec.Path != old.(*AWSSecretEngineRole).Spec.Path { + return nil, errors.New("spec.path cannot be updated") + } + + return nil, r.validateAWSSecretEngineRole() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *AWSSecretEngineRole) ValidateDelete() (admission.Warnings, error) { + awssecretenginerolelog.Info("validate delete", "name", r.Name) + + return nil, nil +} + +// validateAWSSecretEngineRole validates the AWS Secret Engine Role spec +func (r *AWSSecretEngineRole) validateAWSSecretEngineRole() error { + spec := &r.Spec.AWSSERole + credType := spec.CredentialType + + // Validate credential_type is provided + if credType == "" { + return errors.New("spec.credentialType is required") + } + + // Validate role_arns + if credType == "assumed_role" { + if len(spec.RoleARNs) == 0 { + return errors.New("spec.roleArns is required when credentialType is 'assumed_role'") + } + } else { + if len(spec.RoleARNs) > 0 { + return fmt.Errorf("spec.roleArns is not allowed when credentialType is '%s'", credType) + } + } + + // Validate policy_arns and policy_document for iam_user and federation_token + if credType == "iam_user" || credType == "federation_token" { + if len(spec.PolicyARNs) == 0 && spec.PolicyDocument == "" { + return fmt.Errorf("at least one of spec.policyArns or spec.policyDocument must be specified when credentialType is '%s'", credType) + } + } + + // Validate policy_arns, policy_document, and iam_groups are not used with session_token + if credType == "session_token" { + if len(spec.PolicyARNs) > 0 { + return errors.New("spec.policyArns is not allowed when credentialType is 'session_token'") + } + if spec.PolicyDocument != "" { + return errors.New("spec.policyDocument is not allowed when credentialType is 'session_token'") + } + if len(spec.IAMGroups) > 0 { + return errors.New("spec.iamGroups is not allowed when credentialType is 'session_token'") + } + } + + // Validate session_tags + if len(spec.SessionTags) > 0 && credType != "assumed_role" { + return fmt.Errorf("spec.sessionTags is only valid when credentialType is 'assumed_role', got '%s'", credType) + } + + // Validate default_sts_ttl and max_sts_ttl + if credType != "assumed_role" && credType != "federation_token" { + if spec.DefaultSTSTTL != "" { + return fmt.Errorf("spec.defaultStsTtl is only valid when credentialType is 'assumed_role' or 'federation_token', got '%s'", credType) + } + if spec.MaxSTSTTL != "" { + return fmt.Errorf("spec.maxStsTtl is only valid when credentialType is 'assumed_role' or 'federation_token', got '%s'", credType) + } + } + + // Validate external_id + if spec.ExternalID != "" && credType != "assumed_role" { + return fmt.Errorf("spec.externalId is only valid when credentialType is 'assumed_role', got '%s'", credType) + } + + // Validate user_path, permissions_boundary_arn, iam_tags, and mfa_serial_number for non-iam_user credential types + if credType != "iam_user" { + // Validate user_path + if spec.UserPath != "" { + return fmt.Errorf("spec.userPath is only valid when credentialType is 'iam_user', got '%s'", credType) + } + + // Validate permissions_boundary_arn + if spec.PermissionsBoundaryARN != "" { + return fmt.Errorf("spec.permissionsBoundaryArn is only valid when credentialType is 'iam_user', got '%s'", credType) + } + + // Validate iam_tags + if len(spec.IAMTags) > 0 { + return fmt.Errorf("spec.iamTags is only valid when credentialType is 'iam_user', got '%s'", credType) + } + + // Validate mfa_serial_number + if spec.MFASerialNumber != "" { + return fmt.Errorf("spec.mfaSerialNumber is only valid when credentialType is 'iam_user', got '%s'", credType) + } + } + + return nil +} diff --git a/api/v1alpha1/utils/vaultobject_test.go b/api/v1alpha1/utils/vaultobject_test.go index 4b59d1e2..2839eaa9 100644 --- a/api/v1alpha1/utils/vaultobject_test.go +++ b/api/v1alpha1/utils/vaultobject_test.go @@ -18,18 +18,18 @@ type mockVaultObject struct { payload map[string]interface{} } -func (m *mockVaultObject) GetPath() string { return m.path } -func (m *mockVaultObject) GetPayload() map[string]interface{} { return m.payload } +func (m *mockVaultObject) GetPath() string { return m.path } +func (m *mockVaultObject) GetPayload() map[string]interface{} { return m.payload } func (m *mockVaultObject) IsEquivalentToDesiredState(_ map[string]interface{}) bool { return false } -func (m *mockVaultObject) IsInitialized() bool { return true } -func (m *mockVaultObject) IsValid() (bool, error) { return true, nil } -func (m *mockVaultObject) IsDeletable() bool { return true } +func (m *mockVaultObject) IsInitialized() bool { return true } +func (m *mockVaultObject) IsValid() (bool, error) { return true, nil } +func (m *mockVaultObject) IsDeletable() bool { return true } func (m *mockVaultObject) PrepareInternalValues(_ context.Context, _ client.Object) error { return nil } func (m *mockVaultObject) PrepareTLSConfig(_ context.Context, _ client.Object) error { return nil } -func (m *mockVaultObject) GetKubeAuthConfiguration() *KubeAuthConfiguration { return nil } -func (m *mockVaultObject) GetVaultConnection() *VaultConnection { return nil } +func (m *mockVaultObject) GetKubeAuthConfiguration() *KubeAuthConfiguration { return nil } +func (m *mockVaultObject) GetVaultConnection() *VaultConnection { return nil } // fakeVaultStore holds in-memory KV data and serves Vault-compatible HTTP responses. type fakeVaultStore struct { diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go index 53372797..f438ab0e 100644 --- a/api/v1alpha1/webhook_suite_test.go +++ b/api/v1alpha1/webhook_suite_test.go @@ -240,6 +240,12 @@ var _ = BeforeSuite(func() { err = (&IdentityTokenRole{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = (&AWSSecretEngineRole{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = (&AWSSecretEngineConfig{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:webhook go func() { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0384f23d..0fb3efb6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,278 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSEConfig) DeepCopyInto(out *AWSSEConfig) { + *out = *in + if in.STSFallbackEndpoints != nil { + in, out := &in.STSFallbackEndpoints, &out.STSFallbackEndpoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.STSFallbackRegions != nil { + in, out := &in.STSFallbackRegions, &out.STSFallbackRegions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSEConfig. +func (in *AWSSEConfig) DeepCopy() *AWSSEConfig { + if in == nil { + return nil + } + out := new(AWSSEConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSERole) DeepCopyInto(out *AWSSERole) { + *out = *in + if in.RoleARNs != nil { + in, out := &in.RoleARNs, &out.RoleARNs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PolicyARNs != nil { + in, out := &in.PolicyARNs, &out.PolicyARNs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IAMGroups != nil { + in, out := &in.IAMGroups, &out.IAMGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IAMTags != nil { + in, out := &in.IAMTags, &out.IAMTags + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SessionTags != nil { + in, out := &in.SessionTags, &out.SessionTags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSERole. +func (in *AWSSERole) DeepCopy() *AWSSERole { + if in == nil { + return nil + } + out := new(AWSSERole) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineConfig) DeepCopyInto(out *AWSSecretEngineConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineConfig. +func (in *AWSSecretEngineConfig) DeepCopy() *AWSSecretEngineConfig { + if in == nil { + return nil + } + out := new(AWSSecretEngineConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSSecretEngineConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineConfigList) DeepCopyInto(out *AWSSecretEngineConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AWSSecretEngineConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineConfigList. +func (in *AWSSecretEngineConfigList) DeepCopy() *AWSSecretEngineConfigList { + if in == nil { + return nil + } + out := new(AWSSecretEngineConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSSecretEngineConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineConfigSpec) DeepCopyInto(out *AWSSecretEngineConfigSpec) { + *out = *in + if in.Connection != nil { + in, out := &in.Connection, &out.Connection + *out = new(utils.VaultConnection) + (*in).DeepCopyInto(*out) + } + in.Authentication.DeepCopyInto(&out.Authentication) + in.AWSCredentials.DeepCopyInto(&out.AWSCredentials) + in.AWSSEConfig.DeepCopyInto(&out.AWSSEConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineConfigSpec. +func (in *AWSSecretEngineConfigSpec) DeepCopy() *AWSSecretEngineConfigSpec { + if in == nil { + return nil + } + out := new(AWSSecretEngineConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineConfigStatus) DeepCopyInto(out *AWSSecretEngineConfigStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineConfigStatus. +func (in *AWSSecretEngineConfigStatus) DeepCopy() *AWSSecretEngineConfigStatus { + if in == nil { + return nil + } + out := new(AWSSecretEngineConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineRole) DeepCopyInto(out *AWSSecretEngineRole) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineRole. +func (in *AWSSecretEngineRole) DeepCopy() *AWSSecretEngineRole { + if in == nil { + return nil + } + out := new(AWSSecretEngineRole) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSSecretEngineRole) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineRoleList) DeepCopyInto(out *AWSSecretEngineRoleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AWSSecretEngineRole, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineRoleList. +func (in *AWSSecretEngineRoleList) DeepCopy() *AWSSecretEngineRoleList { + if in == nil { + return nil + } + out := new(AWSSecretEngineRoleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSSecretEngineRoleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineRoleSpec) DeepCopyInto(out *AWSSecretEngineRoleSpec) { + *out = *in + if in.Connection != nil { + in, out := &in.Connection, &out.Connection + *out = new(utils.VaultConnection) + (*in).DeepCopyInto(*out) + } + in.Authentication.DeepCopyInto(&out.Authentication) + in.AWSSERole.DeepCopyInto(&out.AWSSERole) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineRoleSpec. +func (in *AWSSecretEngineRoleSpec) DeepCopy() *AWSSecretEngineRoleSpec { + if in == nil { + return nil + } + out := new(AWSSecretEngineRoleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSSecretEngineRoleStatus) DeepCopyInto(out *AWSSecretEngineRoleStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSecretEngineRoleStatus. +func (in *AWSSecretEngineRoleStatus) DeepCopy() *AWSSecretEngineRoleStatus { + if in == nil { + return nil + } + out := new(AWSSecretEngineRoleStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Audit) DeepCopyInto(out *Audit) { *out = *in diff --git a/config/crd/bases/redhatcop.redhat.io_awssecretengineconfigs.yaml b/config/crd/bases/redhatcop.redhat.io_awssecretengineconfigs.yaml new file mode 100644 index 00000000..7defe554 --- /dev/null +++ b/config/crd/bases/redhatcop.redhat.io_awssecretengineconfigs.yaml @@ -0,0 +1,352 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: awssecretengineconfigs.redhatcop.redhat.io +spec: + group: redhatcop.redhat.io + names: + kind: AWSSecretEngineConfig + listKind: AWSSecretEngineConfigList + plural: awssecretengineconfigs + singular: awssecretengineconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: AWSSecretEngineConfig is the Schema for the awssecretengineconfigs + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: AWSSecretEngineConfigSpec defines the desired state of AWSSecretEngineConfig + properties: + authentication: + description: Authentication is the kube auth configuraiton to be used + to execute this request + properties: + namespace: + description: Namespace is the Vault namespace to be used in all + the operations withing this connection/authentication. Only + available in Vault Enterprise. + type: string + path: + default: kubernetes + description: Path is the path of the role used for this kube auth + authentication. The operator will try to authenticate at {[namespace/]}auth/{spec.path} + pattern: ^(?:/?[\w;:@&=\$-\.\+]*)+/? + type: string + role: + description: Role the role to be used during authentication + type: string + serviceAccount: + default: + name: default + description: ServiceAccount is the service account used for the + kube auth authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + awsCredentials: + description: AWSCredentials consists of access_key and secret_key, + which can be created as Kubernetes Secret, VaultSecret or RandomSecret + properties: + passwordKey: + default: password + description: PasswordKey key to be used when retrieving the password, + required with VaultSecrets and Kubernetes secrets, ignored with + RandomSecret + type: string + randomSecret: + description: |- + RandomSecret retrieves the credentials from the Vault secret corresponding to this RandomSecret. This will map the "username" and "password" keys of the secret to the username and password of this config. All other keys will be ignored. If the RandomSecret is refreshed the operator retrieves the new secret from Vault and updates this configuration. Only one of RootCredentialsFromVaultSecret or RootCredentialsFromSecret or RootCredentialsFromRandomSecret can be specified. + When using randomSecret a username must be specified in the spec.username + password: Specifies the password to use when connecting with the username. This value will not be returned by Vault when performing a read upon the configuration. This is typically used in the connection_url field via the templating directive "{{"password"}}"". + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + secret: + description: |- + Secret retrieves the credentials from a Kubernetes secret. The secret must be of basicauth type (https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret). This will map the "username" and "password" keys of the secret to the username and password of this config. If the kubernetes secret is updated, this configuration will also be updated. All other keys will be ignored. Only one of RootCredentialsFromVaultSecret or RootCredentialsFromSecret or RootCredentialsFromRandomSecret can be specified. + username: Specifies the name of the user to use as the "root" user when connecting to the database. This "root" user is used to create/update/delete users managed by these plugins, so you will need to ensure that this user has permissions to manipulate users appropriate to the database. This is typically used in the connection_url field via the templating directive "{{"username"}}" or "{{"name"}}". + password: Specifies the password to use when connecting with the username. This value will not be returned by Vault when performing a read upon the configuration. This is typically used in the connection_url field via the templating directive "{{"password"}}". + If username is provided as spec.username, it takes precedence over the username retrieved from the referenced secret + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + usernameKey: + default: username + description: UsernameKey key to be used when retrieving the username, + optional with VaultSecrets and Kubernetes secrets, ignored with + RandomSecret + type: string + vaultSecret: + description: |- + VaultSecret retrieves the credentials from a Vault secret. This will map the "username" and "password" keys of the secret to the username and password of this config. All other keys will be ignored. Only one of RootCredentialsFromVaultSecret or RootCredentialsFromSecret or RootCredentialsFromRandomSecret can be specified. + username: Specifies the name of the user to use as the "root" user when connecting to the database. This "root" user is used to create/update/delete users managed by these plugins, so you will need to ensure that this user has permissions to manipulate users appropriate to the database. This is typically used in the connection_url field via the templating directive "{{"username"}}" or "{{"name"}}". + password: Specifies the password to use when connecting with the username. This value will not be returned by Vault when performing a read upon the configuration. This is typically used in the connection_url field via the templating directive "{{"password"}}". + If username is provided as spec.username, it takes precedence over the username retrieved from the referenced secret + properties: + path: + description: Path is the path to the secret + type: string + type: object + type: object + connection: + description: Connection represents the information needed to connect + to Vault. This operator uses the standard Vault environment variables + to connect to Vault. If you need to override those settings and + for example connect to a different Vault instance, you can do with + this section of the CR. + properties: + address: + description: 'Address Address of the Vault server expressed as + a URL and port, for example: https://127.0.0.1:8200/' + type: string + maxRetries: + description: MaxRetries Maximum number of retries when certain + error codes are encountered. The default is 2, for three total + attempts. Set this to 0 or less to disable retrying. Error codes + that are retried are 412 (client consistency requirement not + satisfied) and all 5xx except for 501 (not implemented). + type: integer + tLSConfig: + properties: + cacert: + description: Cacert Path to a PEM-encoded CA certificate file + on the local disk. This file is used to verify the Vault + server's SSL certificate. This environment variable takes + precedence over a cert passed via the secret. + type: string + skipVerify: + description: SkipVerify Do not verify Vault's presented certificate + before communicating with it. Setting this variable is not + recommended and voids Vault's security model. + type: boolean + tlsSecret: + description: 'TLSSecret namespace-local secret containing + the tls material for the connection. the expected keys for + the secret are: ca bundle -> "ca.crt", certificate -> "tls.crt", + key -> "tls.key"' + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + tlsServerName: + description: TLSServerName Name to use as the SNI host when + connecting via TLS. + type: string + type: object + timeOut: + description: Timeout Timeout variable. The default value is 60s. + type: string + type: object + disableAutomatedRotation: + default: false + description: Cancels all upcoming rotations of the root credential + until unset (Enterprise). + type: boolean + iamEndpoint: + default: "" + description: Specifies a custom HTTP IAM endpoint to use. + type: string + identityTokenAudience: + default: "" + description: The audience claim value for plugin identity tokens (Enterprise). + Must match an allowed audience configured for the target IAM OIDC + identity provider. + type: string + identityTokenTtl: + default: "3600" + description: The TTL of generated tokens (Enterprise). Defaults to + 1 hour. Uses duration format strings. + type: string + maxRetries: + default: -1 + description: Number of max retries the client should use for recoverable + errors. The default (-1) falls back to the AWS SDK's default behavior. + type: integer + path: + description: |- + Path at which to make the configuration. + The final path in Vault will be {[spec.authentication.namespace]}/{spec.path}/config/root. + The authentication role must have the following capabilities = [ "create", "read", "update", "delete"] on that path. + pattern: ^(?:/?[\w;:@&=\$-\.\+]*)+/? + type: string + region: + default: "" + description: Specifies the AWS region. If not set it will use the + AWS_REGION env var, AWS_DEFAULT_REGION env var, or us-east-1 in + that order. + type: string + roleArn: + default: "" + description: Role ARN to assume for plugin workload identity federation + (Enterprise). Required with identity_token_audience. + type: string + rotationPeriod: + default: 0 + description: The amount of time, in seconds, Vault should wait before + rotating the root credential (Enterprise). A zero value tells Vault + not to rotate the root credential. + type: integer + rotationSchedule: + default: "" + description: The schedule, in cron-style time format, defining the + schedule on which Vault should rotate the root token (Enterprise). + type: string + rotationWindow: + default: 0 + description: The maximum amount of time, in seconds, allowed to complete + a rotation when a scheduled token rotation occurs (Enterprise). + type: integer + stsEndpoint: + default: "" + description: Specifies a custom HTTP STS endpoint to use. + type: string + stsFallbackEndpoints: + description: Specifies an ordered list of fallback STS endpoints to + use. + items: + type: string + type: array + stsFallbackRegions: + description: Specifies an ordered list of fallback STS regions to + use (should match fallback endpoints). + items: + type: string + type: array + stsRegion: + default: "" + description: Specifies a custom STS region to use (should match sts_endpoint). + type: string + usernameTemplate: + default: "" + description: Template describing how dynamic usernames are generated. + type: string + type: object + status: + description: AWSSecretEngineConfigStatus defines the observed state of + AWSSecretEngineConfig + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/redhatcop.redhat.io_awssecretengineroles.yaml b/config/crd/bases/redhatcop.redhat.io_awssecretengineroles.yaml new file mode 100644 index 00000000..efb86b9f --- /dev/null +++ b/config/crd/bases/redhatcop.redhat.io_awssecretengineroles.yaml @@ -0,0 +1,307 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: awssecretengineroles.redhatcop.redhat.io +spec: + group: redhatcop.redhat.io + names: + kind: AWSSecretEngineRole + listKind: AWSSecretEngineRoleList + plural: awssecretengineroles + singular: awssecretenginerole + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: AWSSecretEngineRole is the Schema for the awssecretengineroles + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: AWSSecretEngineRoleSpec defines the desired state of AWSSecretEngineRole + properties: + authentication: + description: Authentication is the kube auth configuraiton to be used + to execute this request + properties: + namespace: + description: Namespace is the Vault namespace to be used in all + the operations withing this connection/authentication. Only + available in Vault Enterprise. + type: string + path: + default: kubernetes + description: Path is the path of the role used for this kube auth + authentication. The operator will try to authenticate at {[namespace/]}auth/{spec.path} + pattern: ^(?:/?[\w;:@&=\$-\.\+]*)+/? + type: string + role: + description: Role the role to be used during authentication + type: string + serviceAccount: + default: + name: default + description: ServiceAccount is the service account used for the + kube auth authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + connection: + description: Connection represents the information needed to connect + to Vault. This operator uses the standard Vault environment variables + to connect to Vault. If you need to override those settings and + for example connect to a different Vault instance, you can do with + this section of the CR. + properties: + address: + description: 'Address Address of the Vault server expressed as + a URL and port, for example: https://127.0.0.1:8200/' + type: string + maxRetries: + description: MaxRetries Maximum number of retries when certain + error codes are encountered. The default is 2, for three total + attempts. Set this to 0 or less to disable retrying. Error codes + that are retried are 412 (client consistency requirement not + satisfied) and all 5xx except for 501 (not implemented). + type: integer + tLSConfig: + properties: + cacert: + description: Cacert Path to a PEM-encoded CA certificate file + on the local disk. This file is used to verify the Vault + server's SSL certificate. This environment variable takes + precedence over a cert passed via the secret. + type: string + skipVerify: + description: SkipVerify Do not verify Vault's presented certificate + before communicating with it. Setting this variable is not + recommended and voids Vault's security model. + type: boolean + tlsSecret: + description: 'TLSSecret namespace-local secret containing + the tls material for the connection. the expected keys for + the secret are: ca bundle -> "ca.crt", certificate -> "tls.crt", + key -> "tls.key"' + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + tlsServerName: + description: TLSServerName Name to use as the SNI host when + connecting via TLS. + type: string + type: object + timeOut: + description: Timeout Timeout variable. The default value is 60s. + type: string + type: object + credentialType: + description: |- + Specifies the type of credential to be used when retrieving credentials from the role. + Must be one of iam_user, assumed_role, federation_token, or session_token. + enum: + - iam_user + - assumed_role + - federation_token + - session_token + type: string + defaultStsTtl: + description: |- + The default TTL for STS credentials. When a TTL is not specified when STS credentials are requested, + and a default TTL is specified on the role, then this default TTL will be used. + Valid only when credential_type is one of assumed_role or federation_token. + type: string + externalId: + description: |- + The external ID to use when assuming the role. + Valid only when credential_type is set to assumed_role. + type: string + iamGroups: + description: |- + A list of IAM group names. IAM users generated against this vault role will be added to these IAM Groups. + For a credential type of assumed_role or federation_token, the policies sent to the corresponding AWS call + will be the policies from each group in iam_groups combined with the policy_document and policy_arns parameters. + items: + type: string + type: array + iamTags: + description: |- + A list of strings representing a key/value pair to be used as a tag for any iam_user user that is created by this role. + Format is a key and value separated by an = (e.g. test_key=value). + items: + type: string + type: array + maxStsTtl: + description: |- + The max allowed TTL for STS credentials (credentials TTL are capped to max_sts_ttl). + Valid only when credential_type is one of assumed_role or federation_token. + type: string + mfaSerialNumber: + description: |- + The ARN or hardware device number of the device configured to the IAM user for multi-factor authentication. + Only required if the IAM user has an MFA device set up in AWS. + type: string + name: + description: The name of the object created in Vault. If this is specified + it takes precedence over {metatada.name} + pattern: '[a-z0-9]([-a-z0-9]*[a-z0-9])?' + type: string + path: + description: |- + Path at which to make the configuration. + The final path in Vault will be {[spec.authentication.namespace]}/auth/{spec.path}/groups/{metadata.name}. + The authentication role must have the following capabilities = [ "create", "read", "update", "delete"] on that path. + pattern: ^(?:/?[\w;:@&=\$-\.\+]*)+/? + type: string + permissionsBoundaryArn: + description: |- + The ARN of the AWS Permissions Boundary to attach to IAM users created in the role. + Valid only when credential_type is iam_user. If not specified, then no permissions boundary policy will be attached. + type: string + policyArns: + description: |- + Specifies a list of AWS managed policy ARNs. The behavior depends on the credential type. + With iam_user, the policies will be attached to IAM users when they are requested. + With assumed_role and federation_token, the policy ARNs will act as a filter on what the credentials can do. + items: + type: string + type: array + policyDocument: + description: |- + The IAM policy document for the role. The behavior depends on the credential type. + With iam_user, the policy document will be attached to the IAM user generated. + With assumed_role and federation_token, the policy document will act as a filter on what the credentials can do. + type: string + roleArns: + description: |- + Specifies the ARNs of the AWS roles this Vault role is allowed to assume. + Required when credential_type is assumed_role and prohibited otherwise. + items: + type: string + type: array + sessionTags: + description: |- + The set of key-value pairs to be included as tags for the STS session. + Format is key=value. + Valid only when credential_type is set to assumed_role. + items: + type: string + type: array + userPath: + description: The path for the user name. Valid only when credential_type + is iam_user. Default is / + type: string + type: object + status: + description: AWSSecretEngineRoleStatus defines the observed state of AWSSecretEngineRole + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 15c0be46..bede3774 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -49,6 +49,8 @@ resources: - bases/redhatcop.redhat.io_identitytokenconfigs.yaml - bases/redhatcop.redhat.io_identitytokenkeys.yaml - bases/redhatcop.redhat.io_identitytokenroles.yaml +- bases/redhatcop.redhat.io_awssecretengineroles.yaml +- bases/redhatcop.redhat.io_awssecretengineconfigs.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: [] @@ -97,6 +99,8 @@ patchesStrategicMerge: [] #- patches/webhook_in_identitytokenconfigs.yaml #- patches/webhook_in_identitytokenkeys.yaml #- patches/webhook_in_identitytokenroles.yaml +#- patches/webhook_in_awssecretengineroles.yaml +#- patches/webhook_in_awssecretengineconfigs.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -146,6 +150,8 @@ patchesStrategicMerge: [] #- patches/cainjection_in_identitytokenconfigs.yaml #- patches/cainjection_in_identitytokenkeys.yaml #- patches/cainjection_in_identitytokenroles.yaml +#- patches/cainjection_in_awssecretengineroles.yaml +#- patches/cainjection_in_awssecretengineconfigs.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_awssecretengineconfigs.yaml b/config/crd/patches/cainjection_in_awssecretengineconfigs.yaml new file mode 100644 index 00000000..ee887ebf --- /dev/null +++ b/config/crd/patches/cainjection_in_awssecretengineconfigs.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: awssecretengineconfigs.redhatcop.redhat.io diff --git a/config/crd/patches/cainjection_in_awssecretengineroles.yaml b/config/crd/patches/cainjection_in_awssecretengineroles.yaml new file mode 100644 index 00000000..f9829c78 --- /dev/null +++ b/config/crd/patches/cainjection_in_awssecretengineroles.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: awssecretengineroles.redhatcop.redhat.io diff --git a/config/crd/patches/webhook_in_awssecretengineconfigs.yaml b/config/crd/patches/webhook_in_awssecretengineconfigs.yaml new file mode 100644 index 00000000..aa1d12be --- /dev/null +++ b/config/crd/patches/webhook_in_awssecretengineconfigs.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: awssecretengineconfigs.redhatcop.redhat.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/crd/patches/webhook_in_awssecretengineroles.yaml b/config/crd/patches/webhook_in_awssecretengineroles.yaml new file mode 100644 index 00000000..24ed812e --- /dev/null +++ b/config/crd/patches/webhook_in_awssecretengineroles.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: awssecretengineroles.redhatcop.redhat.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/awssecretengineconfig_editor_role.yaml b/config/rbac/awssecretengineconfig_editor_role.yaml new file mode 100644 index 00000000..20cfc5bd --- /dev/null +++ b/config/rbac/awssecretengineconfig_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit awssecretengineconfigs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: awssecretengineconfig-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: vault-config-operator + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + name: awssecretengineconfig-editor-role +rules: +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineconfigs/status + verbs: + - get diff --git a/config/rbac/awssecretengineconfig_viewer_role.yaml b/config/rbac/awssecretengineconfig_viewer_role.yaml new file mode 100644 index 00000000..1d62d14e --- /dev/null +++ b/config/rbac/awssecretengineconfig_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view awssecretengineconfigs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: awssecretengineconfig-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: vault-config-operator + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + name: awssecretengineconfig-viewer-role +rules: +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineconfigs + verbs: + - get + - list + - watch +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineconfigs/status + verbs: + - get diff --git a/config/rbac/awssecretenginerole_editor_role.yaml b/config/rbac/awssecretenginerole_editor_role.yaml new file mode 100644 index 00000000..bceab8c2 --- /dev/null +++ b/config/rbac/awssecretenginerole_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit awssecretengineroles. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: awssecretenginerole-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: vault-config-operator + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + name: awssecretenginerole-editor-role +rules: +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineroles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineroles/status + verbs: + - get diff --git a/config/rbac/awssecretenginerole_viewer_role.yaml b/config/rbac/awssecretenginerole_viewer_role.yaml new file mode 100644 index 00000000..9ebb9f5f --- /dev/null +++ b/config/rbac/awssecretenginerole_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view awssecretengineroles. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: awssecretenginerole-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: vault-config-operator + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + name: awssecretenginerole-viewer-role +rules: +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineroles + verbs: + - get + - list + - watch +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineroles/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 58ab2d36..b9aa7fac 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -121,6 +121,58 @@ rules: - get - patch - update +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineconfigs/finalizers + verbs: + - update +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineconfigs/status + verbs: + - get + - patch + - update +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineroles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineroles/finalizers + verbs: + - update +- apiGroups: + - redhatcop.redhat.io + resources: + - awssecretengineroles/status + verbs: + - get + - patch + - update - apiGroups: - redhatcop.redhat.io resources: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index fc7487c5..29ce40d3 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -45,5 +45,7 @@ resources: - redhatcop_v1alpha1_identitytokenconfig.yaml - redhatcop_v1alpha1_identitytokenkey.yaml - redhatcop_v1alpha1_identitytokenrole.yaml +- redhatcop_v1alpha1_awssecretenginerole.yaml +- redhatcop_v1alpha1_awssecretengineconfig.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/redhatcop_v1alpha1_awssecretengineconfig.yaml b/config/samples/redhatcop_v1alpha1_awssecretengineconfig.yaml new file mode 100644 index 00000000..e53f49de --- /dev/null +++ b/config/samples/redhatcop_v1alpha1_awssecretengineconfig.yaml @@ -0,0 +1,12 @@ +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: AWSSecretEngineConfig +metadata: + labels: + app.kubernetes.io/name: awssecretengineconfig + app.kubernetes.io/instance: awssecretengineconfig-sample + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-config-operator + name: awssecretengineconfig-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/redhatcop_v1alpha1_awssecretenginerole.yaml b/config/samples/redhatcop_v1alpha1_awssecretenginerole.yaml new file mode 100644 index 00000000..91b3ea0e --- /dev/null +++ b/config/samples/redhatcop_v1alpha1_awssecretenginerole.yaml @@ -0,0 +1,12 @@ +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: AWSSecretEngineRole +metadata: + labels: + app.kubernetes.io/name: awssecretenginerole + app.kubernetes.io/instance: awssecretenginerole-sample + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-config-operator + name: awssecretenginerole-sample +spec: + # TODO(user): Add fields here diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index b2813fae..7b354e21 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -4,6 +4,26 @@ kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig + failurePolicy: Fail + name: mAWSSecretEngineConfig.kb.io + rules: + - apiGroups: + - redhatcop.redhat.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - AWSSecretEngineConfigs + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -24,6 +44,26 @@ webhooks: resources: - authenginemounts sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-redhatcop-redhat-io-v1alpha1-awssecretenginerole + failurePolicy: Fail + name: mawssecretenginerole.kb.io + rules: + - apiGroups: + - redhatcop.redhat.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - awssecretengineroles + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -833,6 +873,26 @@ kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig + failurePolicy: Fail + name: vAWSSecretEngineConfig.kb.io + rules: + - apiGroups: + - redhatcop.redhat.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - AWSSecretEngineConfigs + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -853,6 +913,26 @@ webhooks: resources: - authenginemounts sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-redhatcop-redhat-io-v1alpha1-awssecretenginerole + failurePolicy: Fail + name: vawssecretenginerole.kb.io + rules: + - apiGroups: + - redhatcop.redhat.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - awssecretengineroles + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/controllers/awssecretengineconfig_controller.go b/controllers/awssecretengineconfig_controller.go new file mode 100644 index 00000000..80e98cd0 --- /dev/null +++ b/controllers/awssecretengineconfig_controller.go @@ -0,0 +1,77 @@ +/* +Copyright 2021. + +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 controllers + +import ( + "context" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + redhatcopv1alpha1 "github.com/redhat-cop/vault-config-operator/api/v1alpha1" + "github.com/redhat-cop/vault-config-operator/controllers/vaultresourcecontroller" +) + +// AWSSecretEngineConfigReconciler reconciles a AWSSecretEngineConfig object +type AWSSecretEngineConfigReconciler struct { + vaultresourcecontroller.ReconcilerBase +} + +//+kubebuilder:rbac:groups=redhatcop.redhat.io,resources=awssecretengineconfigs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=redhatcop.redhat.io,resources=awssecretengineconfigs/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=redhatcop.redhat.io,resources=awssecretengineconfigs/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the AWSSecretEngineConfig object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *AWSSecretEngineConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + instance := &redhatcopv1alpha1.AWSSecretEngineConfig{} + err := r.GetClient().Get(ctx, req.NamespacedName, instance) + if err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, err + } + + ctx1, err := prepareContext(ctx, r.ReconcilerBase, instance) + if err != nil { + r.Log.Error(err, "unable to prepare context", "instance", instance) + return vaultresourcecontroller.ManageOutcome(ctx, r.ReconcilerBase, instance, err) + } + vaultResource := vaultresourcecontroller.NewVaultResource(&r.ReconcilerBase, instance) + + return vaultResource.Reconcile(ctx1, instance) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AWSSecretEngineConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&redhatcopv1alpha1.AWSSecretEngineConfig{}, builder.WithPredicates(vaultresourcecontroller.NewDefaultPeriodicReconcilePredicate())). + Complete(r) +} diff --git a/controllers/awssecretenginerole_controller.go b/controllers/awssecretenginerole_controller.go new file mode 100644 index 00000000..42b0d648 --- /dev/null +++ b/controllers/awssecretenginerole_controller.go @@ -0,0 +1,75 @@ +/* +Copyright 2021. + +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 controllers + +import ( + "context" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + redhatcopv1alpha1 "github.com/redhat-cop/vault-config-operator/api/v1alpha1" + "github.com/redhat-cop/vault-config-operator/controllers/vaultresourcecontroller" +) + +// AWSSecretEngineRoleReconciler reconciles a AWSSecretEngineRole object +type AWSSecretEngineRoleReconciler struct { + vaultresourcecontroller.ReconcilerBase +} + +//+kubebuilder:rbac:groups=redhatcop.redhat.io,resources=awssecretengineroles,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=redhatcop.redhat.io,resources=awssecretengineroles/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=redhatcop.redhat.io,resources=awssecretengineroles/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create +//+kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;patch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +func (r *AWSSecretEngineRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + instance := &redhatcopv1alpha1.AWSSecretEngineRole{} + err := r.GetClient().Get(ctx, req.NamespacedName, instance) + if err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, err + } + + ctx1, err := prepareContext(ctx, r.ReconcilerBase, instance) + if err != nil { + r.Log.Error(err, "unable to prepare context", "instance", instance) + return vaultresourcecontroller.ManageOutcome(ctx, r.ReconcilerBase, instance, err) + } + vaultResource := vaultresourcecontroller.NewVaultResource(&r.ReconcilerBase, instance) + + return vaultResource.Reconcile(ctx1, instance) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AWSSecretEngineRoleReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&redhatcopv1alpha1.AWSSecretEngineRole{}, builder.WithPredicates(vaultresourcecontroller.NewDefaultPeriodicReconcilePredicate())). + Complete(r) +} diff --git a/go.mod b/go.mod index 66ddaf2f..15a467f7 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/onsi/gomega v1.33.1 github.com/pkg/errors v0.9.1 github.com/scylladb/go-set v1.0.2 + github.com/stretchr/testify v1.11.1 k8s.io/api v0.29.2 k8s.io/apiextensions-apiserver v0.29.2 k8s.io/apimachinery v0.29.2 @@ -81,7 +82,6 @@ require ( github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/zclconf/go-cty v1.13.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect diff --git a/go.sum b/go.sum index 76647fb5..e54e7902 100644 --- a/go.sum +++ b/go.sum @@ -192,8 +192,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/main.go b/main.go index 17396be4..94e2eca9 100644 --- a/main.go +++ b/main.go @@ -37,12 +37,12 @@ import ( //"github.com/redhat-cop/operator-utils/pkg/util" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/webhook" redhatcopv1alpha1 "github.com/redhat-cop/vault-config-operator/api/v1alpha1" "github.com/redhat-cop/vault-config-operator/controllers" "github.com/redhat-cop/vault-config-operator/controllers/vaultresourcecontroller" - "sigs.k8s.io/controller-runtime/pkg/cache" //+kubebuilder:scaffold:imports ) @@ -264,6 +264,16 @@ func main() { os.Exit(1) } + if err = (&controllers.AWSSecretEngineConfigReconciler{ReconcilerBase: vaultresourcecontroller.NewFromManager(mgr, "AWSSecretEngineConfig")}).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AWSSecretEngineConfig") + os.Exit(1) + } + + if err = (&controllers.AWSSecretEngineRoleReconciler{ReconcilerBase: vaultresourcecontroller.NewFromManager(mgr, "AWSSecretEngineRole")}).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AWSSecretEngineRole") + os.Exit(1) + } + if err = (&controllers.AzureSecretEngineConfigReconciler{ReconcilerBase: vaultresourcecontroller.NewFromManager(mgr, "AzureSecretEngineConfig")}).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AzureSecretEngineConfig") os.Exit(1) @@ -467,6 +477,16 @@ func main() { os.Exit(1) } + if err = (&redhatcopv1alpha1.AWSSecretEngineConfig{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AWSSecretEngineConfig") + os.Exit(1) + } + + if err = (&redhatcopv1alpha1.AWSSecretEngineRole{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AWSSecretEngineRole") + os.Exit(1) + } + if err = (&redhatcopv1alpha1.PKISecretEngineConfig{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "PKISecretEngineConfig") os.Exit(1) From 051e0474a7cf0962379d2b39d0c84dc3fe32cf5f Mon Sep 17 00:00:00 2001 From: Marc Sensenich Date: Wed, 22 Apr 2026 08:29:03 -0400 Subject: [PATCH 2/6] feat: AWS secret engine config allow for Plugin Workload Identity Federation --- api/v1alpha1/awssecretengineconfig_types.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/awssecretengineconfig_types.go b/api/v1alpha1/awssecretengineconfig_types.go index d9b25af1..4b56aed7 100644 --- a/api/v1alpha1/awssecretengineconfig_types.go +++ b/api/v1alpha1/awssecretengineconfig_types.go @@ -255,6 +255,7 @@ func (r *AWSSecretEngineConfig) setInternalCredentials(context context.Context) accessKey := secret.Data[r.Spec.AWSCredentials.UsernameKey].(string) secretKey := secret.Data[r.Spec.AWSCredentials.PasswordKey].(string) r.setAccessKeyAndSecretKey(accessKey+":"+secretKey, accessKey, secretKey) + log.V(1).Info("Using AWS credentials from random secret", "randomSecretName", randomSecret.Name) return nil } if r.Spec.AWSCredentials.Secret != nil { @@ -280,6 +281,7 @@ func (r *AWSSecretEngineConfig) setInternalCredentials(context context.Context) return err } r.setAccessKeyAndSecretKey(string(accessKey)+":"+string(secretKey), string(accessKey), string(secretKey)) + log.V(1).Info("Using AWS credentials from secret", "secretName", r.Spec.AWSCredentials.Secret.Name) return nil } if r.Spec.AWSCredentials.VaultSecret != nil { @@ -295,7 +297,13 @@ func (r *AWSSecretEngineConfig) setInternalCredentials(context context.Context) accessKey := secret.Data[r.Spec.AWSCredentials.UsernameKey].(string) secretKey := secret.Data[r.Spec.AWSCredentials.PasswordKey].(string) r.setAccessKeyAndSecretKey(accessKey+":"+secretKey, accessKey, secretKey) - log.V(1).Info("", "accessKey", accessKey, "secretKey", secretKey) + log.V(1).Info("Using AWS credentials from Vault Secret", "vaultSecretPath", r.Spec.AWSCredentials.VaultSecret.Path) + return nil + } + // Plugin Workload Identity Federation (WIF) does not use access key and secret key. + // So, we can consider it valid as long as the identity token audience is set. + if r.Spec.IdentityTokenAudience != "" { + log.V(1).Info("Using Plugin Workload Identity Federation authentication", "identityTokenAudience", r.Spec.IdentityTokenAudience) return nil } return errors.New("no aws credentials source specified") From 996f75572e82a4321f528dce998e7fea36c8d8c5 Mon Sep 17 00:00:00 2001 From: Marc Sensenich Date: Wed, 22 Apr 2026 08:49:00 -0400 Subject: [PATCH 3/6] feat: AWS secret engine config and role type documentation --- docs/secret-engines.md | 268 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 267 insertions(+), 1 deletion(-) diff --git a/docs/secret-engines.md b/docs/secret-engines.md index be4b4ee7..876929e6 100644 --- a/docs/secret-engines.md +++ b/docs/secret-engines.md @@ -18,6 +18,8 @@ - [KubernetesSecretEngineRole](#kubernetessecretenginerole) - [AzureSecretEngineConfig] (#azuresecretengineconfig) - [AzureSecretEngineRole] (#azuresecretenginerole) + - [AWSSecretEngineConfig] (#awssecretengineconfig) + - [AWSSecretEngineRole] (#awssecretenginerole) ## SecretEngineMount @@ -738,4 +740,268 @@ spec: The `signInAudience` field - Specifies the security principal types that are allowed to sign in to the application. Valid values are: AzureADMyOrg, AzureADMultipleOrgs, AzureADandPersonalMicrosoftAccount, PersonalMicrosoftAccount. - The `tags` field - A comma-separated string of Azure tags to attach to an application. \ No newline at end of file + The `tags` field - A comma-separated string of Azure tags to attach to an application. + +## AWSSecretEngineConfig + +The `AWSSecretEngineConfig` CRD allows a user to create an [AWS Secret Engine configuration](https://developer.hashicorp.com/vault/api-docs/secret/aws#configure-root-credentials). + +```yaml +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: AWSSecretEngineConfig +metadata: + labels: + app.kubernetes.io/name: awssecretengineconfig + app.kubernetes.io/instance: awssecretengineconfig-sample + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-config-operator + name: awssecretengineconfig-sample +spec: + authentication: + path: vault-admin + role: vault-admin + connection: + address: 'https://vault.example.com' + path: aws + awsCredentials: + secret: + name: aws-credentials + usernameKey: accessKey + passwordKey: secretKey +``` + +The previous example is equivalent to + +```sh +vault write aws/config/root \ + access_key=xxx \ + secret_key=xxx \ + region=us-east-1 +``` + +The `maxRetries` field - (Optional) Number of max retries the client should use for recoverable errors. The default (-1) falls back to the AWS SDK's default behavior. + +The `roleArn` field - (Optional) Role ARN to assume for plugin workload identity federation (Enterprise). Required with identity_token_audience. + +The `identityTokenAudience` field - (Optional) The audience claim value for plugin identity tokens (Enterprise). Must match an allowed audience configured for the target IAM OIDC identity provider. + +The `IdentityTokenTTL` field - (Optional) The TTL of generated tokens (Enterprise). Defaults to 1 hour. Uses duration format strings. + +The `region` field - (Optional) Specifies the AWS region. If not set it will use the AWS_REGION env var, AWS_DEFAULT_REGION env var, or us-east-1 in that order. + +The `iamEndpoint` field - (Optional) Specifies a custom HTTP IAM endpoint to use. + +The `stsEndpoint` field - (Optional) Specifies a custom HTTP STS endpoint to use. + +The `stsRegion` field - (Optional) Specifies a custom STS region to use (should match sts_endpoint). + +The `STSFallbackEndpoints` field - (Optional) Specifies an ordered list of fallback STS endpoints to use. + +The `stsFallbackRegions` field - (Optional) Specifies an ordered list of fallback STS regions to use (should match fallback endpoints). + +The `usernameTemplate` field - (Optional) Template describing how dynamic usernames are generated. + +The `rotationPeriod` field - (Optional) The amount of time, in seconds, Vault should wait before rotating the root credential (Enterprise). A zero value tells Vault not to rotate the root credential. + +The `rotationSchedule` field - (Optional) The schedule, in cron-style time format, defining the schedule on which Vault should rotate the root token (Enterprise). + +The `rotationWindow` field - (Optional) The maximum amount of time, in seconds, allowed to complete a rotation when a scheduled token rotation occurs (Enterprise). + +The `disableAutomatedRotation` field - (Optional) Cancels all upcoming rotations of the root credential until unset (Enterprise). + +The `awsCredentials` field - The OAuth Client Secret from the provider for OIDC roles. +The access key and secret can be retrived a three different ways: + +1. From a Kubernetes secret, specifying the `azureCredentials` field as follows: +```yaml + awsCredentials: + secret: + name: aws-credentials + usernameKey: accessKey + passwordKey: secretKey +``` +The secret must be of [basic auth type](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret). + +Example Secret : +```bash +kubectl create secret generic aad-credentials --from-literal=clientid="123456-1234-1234-1234-123456789" --from-literal=clientsecret="saffsfsdfsfsdgsdgsdgsdgghdfhdhdgsjgjgjfj" -n vault-admin +``` +If the secret is updated this connection will also be updated. + +2. From a [Vault secret](https://developer.hashicorp.com/vault/docs/secrets/kv), specifying the `azureCredentials` field as follows: +```yaml + awsCredentials: + vaultSecret: + path: secret/foo + usernameKey: accessKey + passwordKey: secretKey +``` +3. From a [RandomSecret](secret-management.md#RandomSecret), specifying the `awsCredentials` field as follows: +```yaml + awsCredentials: + randomSecret: + name: aws-credentials + usernameKey: accessKey + passwordKey: secretKey +``` +When the RandomSecret generates a new secret, this connection will also be updated. + +Alternatively, the `identityTokenAudience` and `roleArn` can be set to enable [Plugin Workload Identity Federation](https://developer.hashicorp.com/vault/docs/secrets/aws#plugin-workload-identity-federation-wif) + +## AWSSecretEngineRole + +The `AWSSecretEngineRole` CRD allows a user to create a [AWS Secret Engine Role](https://developer.hashicorp.com/vault/api-docs/secret/aws#create-update-role) + +The `credentialType` field - Specifies the type of credential to be used when retrieving credentials from the role. Must be one of iam_user, assumed_role, federation_token, or session_token. + +The `roleArns` field - Specifies the ARNs of the AWS roles this Vault role is allowed to assume. Required when credential_type is `assumed_role` and prohibited otherwise. + +The `policyArns` field - Specifies a list of AWS managed policy ARNs. The behavior depends on the credential type. With `iam_user`, the policies will be attached to IAM users when they are requested. With `assumed_role` and `federation_token`, the policy ARNs will act as a filter on what the credentials can do. + +The `policyDocument` field - The IAM policy document for the role. The behavior depends on the credential type. With `iam_user`, the policy document will be attached to the IAM user generated. + +The `iamGroups` field - A list of IAM group names. IAM users generated against this vault role will be added to these IAM Groups. For a credential type of `assumed_role` or `federation_token`, the policies sent to the corresponding AWS call will be the policies from each group in `iam_groups` combined with the `policy_document` and `policy_arns` parameters. + +The `iamTags` field - A list of strings representing a key/value pair to be used as a tag for any `iam_user` user that is created by this role. Format is a key and value separated by an = (e.g. test_key=value). + +The `defaultStsTtl` field - The default TTL for STS credentials. When a TTL is not specified when STS credentials are requested, and a default TTL is specified on the role, then this default TTL will be used. Valid only when `credentialType` is one of `assumed_role` or `federation_token`. + +The `maxStsTtl` field - The max allowed TTL for STS credentials (credentials TTL are capped to max_sts_ttl). Valid only when `credentialType` is one of `assumed_role` or `federation_token`. + +The `sessionTags` field - The set of key-value pairs to be included as tags for the STS session. Format is key=value. Valid only when `credentialType` is set to `assumed_role`. + +The `externalId` field - The external ID to use when assuming the role. Valid only when `credentialType` is set to `assumed_role`. + +The `userPath` field - The path for the user name. Valid only when `credentialType` is `iam_user`. Default is `/` + +The `permissionsBoundaryArn` field - The ARN of the AWS Permissions Boundary to attach to IAM users created in the role. Valid only when `credentialType` is `iam_user`. If not specified, then no permissions boundary policy will be attached. + +The `mfaSerialNumber` field - The ARN or hardware device number of the device configured to the IAM user for multi-factor authentication. Only required if the IAM user has an MFA device set up in AWS. + +### IAM User + +```yaml +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: AWSSecretEngineRole +metadata: + labels: + app.kubernetes.io/name: awssecretenginerole + app.kubernetes.io/instance: awssecretenginerole-sample + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-config-operator + name: awssecretenginerole-sample-iam-user +spec: + authentication: + path: vault-admin + role: vault-admin + connection: + address: 'https://vault.example.com' + path: aws + name: "iam-user-role" + credentialType: iam_user + policyArns: + - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess + - arn:aws:iam::aws:policy/IAMReadOnlyAccess + iamGroups: + - group1 + - group2 + policyDocument: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:*", + "Resource": "*" + } + ] + } +``` + +### Federation Token + +```yaml +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: AWSSecretEngineRole +metadata: + labels: + app.kubernetes.io/name: awssecretenginerole + app.kubernetes.io/instance: awssecretenginerole-sample + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-config-operator + name: awssecretenginerole-sample-federation-token +spec: + authentication: + path: vault-admin + role: vault-admin + connection: + address: 'https://vault.example.com' + path: aws + name: "federation-token-role" + credentialType: federation_token + policyDocument: | + { + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Action": [ + "ec2:*", + "sts:GetFederationToken" + ], + "Resource": "*" + } + } +``` + +### Session Token + +```yaml +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: AWSSecretEngineRole +metadata: + labels: + app.kubernetes.io/name: awssecretenginerole + app.kubernetes.io/instance: awssecretenginerole-sample + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-config-operator + name: awssecretenginerole-sample-session-token +spec: + authentication: + path: vault-admin + role: vault-admin + connection: + address: 'https://vault.example.com' + path: aws + name: "session-token-role" + credentialType: session_token +``` + +### Assume Role + +```yaml +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: AWSSecretEngineRole +metadata: + labels: + app.kubernetes.io/name: awssecretenginerole + app.kubernetes.io/instance: awssecretenginerole-sample + app.kubernetes.io/part-of: vault-config-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-config-operator + name: awssecretenginerole-sample-assume-role +spec: + authentication: + path: vault-admin + role: vault-admin + connection: + address: 'https://vault.example.com' + path: aws + name: "assume-role" + credentialType: assumed_role + roleArns: + - arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:role/RoleNameToAssume +``` From f5c3d2ed3f3b6acd359732bdcd9b30f111d2df2a Mon Sep 17 00:00:00 2001 From: Marc Sensenich Date: Wed, 22 Apr 2026 08:52:10 -0400 Subject: [PATCH 4/6] fix: lowercase for AWS webhook configs --- api/v1alpha1/awssecretengineconfig_webhook.go | 4 +-- config/webhook/manifests.yaml | 32 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/api/v1alpha1/awssecretengineconfig_webhook.go b/api/v1alpha1/awssecretengineconfig_webhook.go index 008324b5..64424780 100644 --- a/api/v1alpha1/awssecretengineconfig_webhook.go +++ b/api/v1alpha1/awssecretengineconfig_webhook.go @@ -37,7 +37,7 @@ func (r *AWSSecretEngineConfig) SetupWebhookWithManager(mgr ctrl.Manager) error // TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -//+kubebuilder:webhook:path=/mutate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig,mutating=true,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=AWSSecretEngineConfigs,verbs=create;update,versions=v1alpha1,name=mAWSSecretEngineConfig.kb.io,admissionReviewVersions=v1 +//+kubebuilder:webhook:path=/mutate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig,mutating=true,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=AWSSecretEngineConfigs,verbs=create;update,versions=v1alpha1,name=mawssecretengineconfig.kb.io,admissionReviewVersions=v1 var _ webhook.Defaulter = &AWSSecretEngineConfig{} @@ -49,7 +49,7 @@ func (r *AWSSecretEngineConfig) Default() { } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -//+kubebuilder:webhook:path=/validate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig,mutating=false,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=AWSSecretEngineConfigs,verbs=create;update,versions=v1alpha1,name=vAWSSecretEngineConfig.kb.io,admissionReviewVersions=v1 +//+kubebuilder:webhook:path=/validate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig,mutating=false,failurePolicy=fail,sideEffects=None,groups=redhatcop.redhat.io,resources=AWSSecretEngineConfigs,verbs=create;update,versions=v1alpha1,name=vawssecretengineconfig.kb.io,admissionReviewVersions=v1 var _ webhook.Validator = &AWSSecretEngineConfig{} diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 7b354e21..61009e8d 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -6,13 +6,14 @@ metadata: webhooks: - admissionReviewVersions: - v1 + - v1beta1 clientConfig: service: name: webhook-service namespace: system - path: /mutate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig + path: /mutate-redhatcop-redhat-io-v1alpha1-authenginemount failurePolicy: Fail - name: mAWSSecretEngineConfig.kb.io + name: mauthenginemount.kb.io rules: - apiGroups: - redhatcop.redhat.io @@ -20,20 +21,18 @@ webhooks: - v1alpha1 operations: - CREATE - - UPDATE resources: - - AWSSecretEngineConfigs + - authenginemounts sideEffects: None - admissionReviewVersions: - v1 - - v1beta1 clientConfig: service: name: webhook-service namespace: system - path: /mutate-redhatcop-redhat-io-v1alpha1-authenginemount + path: /mutate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig failurePolicy: Fail - name: mauthenginemount.kb.io + name: mawssecretengineconfig.kb.io rules: - apiGroups: - redhatcop.redhat.io @@ -41,8 +40,9 @@ webhooks: - v1alpha1 operations: - CREATE + - UPDATE resources: - - authenginemounts + - AWSSecretEngineConfigs sideEffects: None - admissionReviewVersions: - v1 @@ -875,43 +875,43 @@ metadata: webhooks: - admissionReviewVersions: - v1 + - v1beta1 clientConfig: service: name: webhook-service namespace: system - path: /validate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig + path: /validate-redhatcop-redhat-io-v1alpha1-authenginemount failurePolicy: Fail - name: vAWSSecretEngineConfig.kb.io + name: vauthenginemount.kb.io rules: - apiGroups: - redhatcop.redhat.io apiVersions: - v1alpha1 operations: - - CREATE - UPDATE resources: - - AWSSecretEngineConfigs + - authenginemounts sideEffects: None - admissionReviewVersions: - v1 - - v1beta1 clientConfig: service: name: webhook-service namespace: system - path: /validate-redhatcop-redhat-io-v1alpha1-authenginemount + path: /validate-redhatcop-redhat-io-v1alpha1-AWSSecretEngineConfig failurePolicy: Fail - name: vauthenginemount.kb.io + name: vawssecretengineconfig.kb.io rules: - apiGroups: - redhatcop.redhat.io apiVersions: - v1alpha1 operations: + - CREATE - UPDATE resources: - - authenginemounts + - AWSSecretEngineConfigs sideEffects: None - admissionReviewVersions: - v1 From 41de760ec79ac83cdae4509050e3e4d6e4a66343 Mon Sep 17 00:00:00 2001 From: Marc Sensenich Date: Wed, 22 Apr 2026 08:59:54 -0400 Subject: [PATCH 5/6] chore: revert changes to api/v1alpha1/utils/vaultobject_test.go --- api/v1alpha1/utils/vaultobject_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/utils/vaultobject_test.go b/api/v1alpha1/utils/vaultobject_test.go index 2839eaa9..4b59d1e2 100644 --- a/api/v1alpha1/utils/vaultobject_test.go +++ b/api/v1alpha1/utils/vaultobject_test.go @@ -18,18 +18,18 @@ type mockVaultObject struct { payload map[string]interface{} } -func (m *mockVaultObject) GetPath() string { return m.path } -func (m *mockVaultObject) GetPayload() map[string]interface{} { return m.payload } +func (m *mockVaultObject) GetPath() string { return m.path } +func (m *mockVaultObject) GetPayload() map[string]interface{} { return m.payload } func (m *mockVaultObject) IsEquivalentToDesiredState(_ map[string]interface{}) bool { return false } -func (m *mockVaultObject) IsInitialized() bool { return true } -func (m *mockVaultObject) IsValid() (bool, error) { return true, nil } -func (m *mockVaultObject) IsDeletable() bool { return true } +func (m *mockVaultObject) IsInitialized() bool { return true } +func (m *mockVaultObject) IsValid() (bool, error) { return true, nil } +func (m *mockVaultObject) IsDeletable() bool { return true } func (m *mockVaultObject) PrepareInternalValues(_ context.Context, _ client.Object) error { return nil } func (m *mockVaultObject) PrepareTLSConfig(_ context.Context, _ client.Object) error { return nil } -func (m *mockVaultObject) GetKubeAuthConfiguration() *KubeAuthConfiguration { return nil } -func (m *mockVaultObject) GetVaultConnection() *VaultConnection { return nil } +func (m *mockVaultObject) GetKubeAuthConfiguration() *KubeAuthConfiguration { return nil } +func (m *mockVaultObject) GetVaultConnection() *VaultConnection { return nil } // fakeVaultStore holds in-memory KV data and serves Vault-compatible HTTP responses. type fakeVaultStore struct { From 8927c7825891584c04625fcc6b0b14e3c60d1da2 Mon Sep 17 00:00:00 2001 From: Marc Sensenich Date: Wed, 22 Apr 2026 09:04:49 -0400 Subject: [PATCH 6/6] chore: add AWS secret engine config and role to readme.md --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index c29e662f..86d17f6b 100644 --- a/readme.md +++ b/readme.md @@ -80,6 +80,8 @@ Currently this operator covers the following Vault APIs: 10. [QuaySecretEngineStaticRole](./docs/secret-engines.md#QuaySecretEngineStaticRole) Configures a Quay server to produce credentials for a Robot account using a fixed username and generated credentials, see the also the [vault-plugin-secrets-quay](https://github.com/redhat-cop/vault-plugin-secrets-quay) 11. [RabbitMQSecretEngineConfig](./docs/secret-engines.md#rabbitmqsecretengineconfig) Configures a [RabbitMQ Secret Engine](https://www.vaultproject.io/docs/secrets/rabbitmq#rabbitmq-secrets-engine) 12. [RabbitMQSecretEngineRole](./docs/secret-engines.md#rabbitmqsecretenginerole) Configures a [RabbitMQ Secret Engine Role](https://www.vaultproject.io/docs/secrets/rabbitmq#rabbitmq-secrets-engine) +13. [AWSSecretEngineConfig](./docs/secret-engines.md#awssecretengineconfig) Configures an [AWS Secret Engine](https://developer.hashicorp.com/vault/docs/secrets/aws) +14. [AWSSecretEngineRole](./docs/secret-engines.md#awssecretenginerole) Configures an [AWS Secret Engine Role](https://developer.hashicorp.com/vault/docs/secrets/aws) ## Secret Management