diff --git a/api/datadoghq/common/envvar.go b/api/datadoghq/common/envvar.go index 94c26e8a7..cecab2524 100644 --- a/api/datadoghq/common/envvar.go +++ b/api/datadoghq/common/envvar.go @@ -150,6 +150,8 @@ const ( DDSBOMHostEnabled = "DD_SBOM_HOST_ENABLED" DDSBOMHostAnalyzers = "DD_SBOM_HOST_ANALYZERS" DDSecretBackendCommand = "DD_SECRET_BACKEND_COMMAND" + DDSecretBackendArguments = "DD_SECRET_BACKEND_ARGUMENTS" + DDSecretBackendTimeout = "DD_SECRET_BACKEND_TIMEOUT" DDSite = "DD_SITE" DDSystemProbeAgentEnabled = "DD_SYSTEM_PROBE_ENABLED" DDSystemProbeBPFDebugEnabled = DDSystemProbeEnvPrefix + "BPF_DEBUG" diff --git a/api/datadoghq/v2alpha1/datadogagent_types.go b/api/datadoghq/v2alpha1/datadogagent_types.go index 7f0d998aa..a12e52ac1 100644 --- a/api/datadoghq/v2alpha1/datadogagent_types.go +++ b/api/datadoghq/v2alpha1/datadogagent_types.go @@ -1245,6 +1245,10 @@ type GlobalConfig struct { // FIPS contains configuration used to customize the FIPS proxy sidecar. FIPS *FIPSConfig `json:"fips,omitempty"` + + // Configure the secret backend feature https://docs.datadoghq.com/agent/guide/secrets-management + // See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md + SecretBackend *SecretBackendConfig `json:"secretBackend,omitempty"` } // DatadogCredentials is a generic structure that holds credentials to access Datadog. @@ -1271,13 +1275,47 @@ type DatadogCredentials struct { AppSecret *SecretConfig `json:"appSecret,omitempty"` } +// SecretBackendRolesConfig provides configuration of the secrets Datadog agents can read for the SecretBackend feature +// +k8s:openapi-gen=true +type SecretBackendRolesConfig struct { + // Namespace defines the namespace in which the secrets reside. + // +required + Namespace *string `json:"namespace,omitempty"` + + // Secrets defines the list of secrets for which a role should be created. + // +required + // +listType=set + Secrets []string `json:"secrets,omitempty"` +} + // SecretBackendConfig provides configuration for the secret backend. +// +k8s:openapi-gen=true type SecretBackendConfig struct { - // Command defines the secret backend command to use + // The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. + // Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers. Command *string `json:"command,omitempty"` - // Args defines the list of arguments to pass to the command - Args []string `json:"args,omitempty"` + // List of arguments to pass to the command (space-separated strings). + // +optional + Args *string `json:"args,omitempty"` + + // The command timeout in seconds. + // Default: `30`. + // +optional + Timeout *int32 `json:"timeout,omitempty"` + + // Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. + // Default: `false`. + // +optional + EnableGlobalPermissions *bool `json:"enableGlobalPermissions,omitempty"` + + // Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. + // They are defined as a list of namespace/secrets. + // Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. + // See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent. + // +optional + // +listType=atomic + Roles []*SecretBackendRolesConfig `json:"roles,omitempty"` } // NetworkPolicyFlavor specifies which flavor of Network Policy to use. diff --git a/api/datadoghq/v2alpha1/test/builder.go b/api/datadoghq/v2alpha1/test/builder.go index 9b4f0087e..de457beb9 100644 --- a/api/datadoghq/v2alpha1/test/builder.go +++ b/api/datadoghq/v2alpha1/test/builder.go @@ -783,6 +783,34 @@ func (builder *DatadogAgentBuilder) WithRegistry(registry string) *DatadogAgentB return builder } +// Global SecretBackend + +func (builder *DatadogAgentBuilder) WithGlobalSecretBackendGlobalPerms(command string, args string, timeout int32) *DatadogAgentBuilder { + builder.datadogAgent.Spec.Global.SecretBackend = &v2alpha1.SecretBackendConfig{ + Command: apiutils.NewStringPointer(command), + Args: apiutils.NewStringPointer(args), + Timeout: apiutils.NewInt32Pointer(timeout), + EnableGlobalPermissions: apiutils.NewBoolPointer(true), + } + return builder +} + +func (builder *DatadogAgentBuilder) WithGlobalSecretBackendSpecificRoles(command string, args string, timeout int32, secretNs string, secretNames []string) *DatadogAgentBuilder { + builder.datadogAgent.Spec.Global.SecretBackend = &v2alpha1.SecretBackendConfig{ + Command: apiutils.NewStringPointer(command), + Args: apiutils.NewStringPointer(args), + Timeout: apiutils.NewInt32Pointer(timeout), + EnableGlobalPermissions: apiutils.NewBoolPointer(false), + Roles: []*v2alpha1.SecretBackendRolesConfig{ + { + Namespace: apiutils.NewStringPointer(secretNs), + Secrets: secretNames, + }, + }, + } + return builder +} + // Override func (builder *DatadogAgentBuilder) WithComponentOverride(componentName v2alpha1.ComponentName, override v2alpha1.DatadogAgentComponentOverride) *DatadogAgentBuilder { diff --git a/api/datadoghq/v2alpha1/zz_generated.deepcopy.go b/api/datadoghq/v2alpha1/zz_generated.deepcopy.go index 2bb8608e4..509b7c399 100644 --- a/api/datadoghq/v2alpha1/zz_generated.deepcopy.go +++ b/api/datadoghq/v2alpha1/zz_generated.deepcopy.go @@ -1563,6 +1563,11 @@ func (in *GlobalConfig) DeepCopyInto(out *GlobalConfig) { *out = new(FIPSConfig) (*in).DeepCopyInto(*out) } + if in.SecretBackend != nil { + in, out := &in.SecretBackend, &out.SecretBackend + *out = new(SecretBackendConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalConfig. @@ -2345,8 +2350,29 @@ func (in *SecretBackendConfig) DeepCopyInto(out *SecretBackendConfig) { } if in.Args != nil { in, out := &in.Args, &out.Args - *out = make([]string, len(*in)) - copy(*out, *in) + *out = new(string) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int32) + **out = **in + } + if in.EnableGlobalPermissions != nil { + in, out := &in.EnableGlobalPermissions, &out.EnableGlobalPermissions + *out = new(bool) + **out = **in + } + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]*SecretBackendRolesConfig, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(SecretBackendRolesConfig) + (*in).DeepCopyInto(*out) + } + } } } @@ -2360,6 +2386,31 @@ func (in *SecretBackendConfig) DeepCopy() *SecretBackendConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretBackendRolesConfig) DeepCopyInto(out *SecretBackendRolesConfig) { + *out = *in + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretBackendRolesConfig. +func (in *SecretBackendRolesConfig) DeepCopy() *SecretBackendRolesConfig { + if in == nil { + return nil + } + out := new(SecretBackendRolesConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretConfig) DeepCopyInto(out *SecretConfig) { *out = *in diff --git a/api/datadoghq/v2alpha1/zz_generated.openapi.go b/api/datadoghq/v2alpha1/zz_generated.openapi.go index d6c6a295e..f48719612 100644 --- a/api/datadoghq/v2alpha1/zz_generated.openapi.go +++ b/api/datadoghq/v2alpha1/zz_generated.openapi.go @@ -45,6 +45,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "./api/datadoghq/v2alpha1.PrometheusScrapeFeatureConfig": schema__api_datadoghq_v2alpha1_PrometheusScrapeFeatureConfig(ref), "./api/datadoghq/v2alpha1.RemoteConfigConfiguration": schema__api_datadoghq_v2alpha1_RemoteConfigConfiguration(ref), "./api/datadoghq/v2alpha1.SeccompConfig": schema__api_datadoghq_v2alpha1_SeccompConfig(ref), + "./api/datadoghq/v2alpha1.SecretBackendConfig": schema__api_datadoghq_v2alpha1_SecretBackendConfig(ref), + "./api/datadoghq/v2alpha1.SecretBackendRolesConfig": schema__api_datadoghq_v2alpha1_SecretBackendRolesConfig(ref), "./api/datadoghq/v2alpha1.UnixDomainSocketConfig": schema__api_datadoghq_v2alpha1_UnixDomainSocketConfig(ref), } } @@ -1416,6 +1418,107 @@ func schema__api_datadoghq_v2alpha1_SeccompConfig(ref common.ReferenceCallback) } } +func schema__api_datadoghq_v2alpha1_SecretBackendConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SecretBackendConfig provides configuration for the secret backend.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "command": { + SchemaProps: spec.SchemaProps{ + Description: "The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers.", + Type: []string{"string"}, + Format: "", + }, + }, + "args": { + SchemaProps: spec.SchemaProps{ + Description: "List of arguments to pass to the command (space-separated strings).", + Type: []string{"string"}, + Format: "", + }, + }, + "timeout": { + SchemaProps: spec.SchemaProps{ + Description: "The command timeout in seconds. Default: `30`.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "enableGlobalPermissions": { + SchemaProps: spec.SchemaProps{ + Description: "Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. Default: `false`.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "roles": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. They are defined as a list of namespace/secrets. Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("./api/datadoghq/v2alpha1.SecretBackendRolesConfig"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "./api/datadoghq/v2alpha1.SecretBackendRolesConfig"}, + } +} + +func schema__api_datadoghq_v2alpha1_SecretBackendRolesConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SecretBackendRolesConfig provides configuration of the secrets Datadog agents can read for the SecretBackend feature", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace defines the namespace in which the secrets reside.", + Type: []string{"string"}, + Format: "", + }, + }, + "secrets": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Secrets defines the list of secrets for which a role should be created.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func schema__api_datadoghq_v2alpha1_UnixDomainSocketConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/config/crd/bases/v1/datadoghq.com_datadogagents.yaml b/config/crd/bases/v1/datadoghq.com_datadogagents.yaml index 74a7420aa..51771dfb5 100644 --- a/config/crd/bases/v1/datadoghq.com_datadogagents.yaml +++ b/config/crd/bases/v1/datadoghq.com_datadogagents.yaml @@ -2057,6 +2057,52 @@ spec: Use 'docker.io/datadog' for DockerHub. Default: 'gcr.io/datadoghq' type: string + secretBackend: + description: |- + Configure the secret backend feature https://docs.datadoghq.com/agent/guide/secrets-management + See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md + properties: + args: + description: List of arguments to pass to the command (space-separated strings). + type: string + command: + description: |- + The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. + Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers. + type: string + enableGlobalPermissions: + description: |- + Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. + Default: `false`. + type: boolean + roles: + description: |- + Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. + They are defined as a list of namespace/secrets. + Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. + See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent. + items: + description: SecretBackendRolesConfig provides configuration of the secrets Datadog agents can read for the SecretBackend feature + properties: + namespace: + description: Namespace defines the namespace in which the secrets reside. + type: string + secrets: + description: Secrets defines the list of secrets for which a role should be created. + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: array + x-kubernetes-list-type: atomic + timeout: + description: |- + The command timeout in seconds. + Default: `30`. + format: int32 + type: integer + type: object site: description: |- Site is the Datadog intake site Agent data are sent to. diff --git a/docs/configuration.v2alpha1.md b/docs/configuration.v2alpha1.md index ff3f6a3ec..1ff16ed0a 100644 --- a/docs/configuration.v2alpha1.md +++ b/docs/configuration.v2alpha1.md @@ -223,6 +223,11 @@ spec: | global.podAnnotationsAsTags | Provide a mapping of Kubernetes Annotations to Datadog Tags. : | | global.podLabelsAsTags | Provide a mapping of Kubernetes Labels to Datadog Tags. : | | global.registry | Registry is the image registry to use for all Agent images. Use 'public.ecr.aws/datadog' for AWS ECR. Use 'datadoghq.azurecr.io' for Azure Container Registry. Use 'gcr.io/datadoghq' for Google Container Registry. Use 'eu.gcr.io/datadoghq' for Google Container Registry in the EU region. Use 'asia.gcr.io/datadoghq' for Google Container Registry in the Asia region. Use 'docker.io/datadog' for DockerHub. Default: 'gcr.io/datadoghq' | +| global.secretBackend.args | List of arguments to pass to the command (space-separated strings). | +| global.secretBackend.command | The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers. | +| global.secretBackend.enableGlobalPermissions | Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. Default: `false`. | +| global.secretBackend.roles | Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. They are defined as a list of namespace/secrets. Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent. | +| global.secretBackend.timeout | The command timeout in seconds. Default: `30`. | | global.site | Site is the Datadog intake site Agent data are sent to. Set to 'datadoghq.com' to send data to the US1 site (default). Set to 'datadoghq.eu' to send data to the EU site. Set to 'us3.datadoghq.com' to send data to the US3 site. Set to 'us5.datadoghq.com' to send data to the US5 site. Set to 'ddog-gov.com' to send data to the US1-FED site. Set to 'ap1.datadoghq.com' to send data to the AP1 site. Default: 'datadoghq.com' | | global.tags | Tags contains a list of tags to attach to every metric, event and service check collected. Learn more about tagging: https://docs.datadoghq.com/tagging/ | | override | Override the default configurations of the agents | diff --git a/internal/controller/datadogagent/merger/rbac.go b/internal/controller/datadogagent/merger/rbac.go index 4f47724ba..139a85aab 100644 --- a/internal/controller/datadogagent/merger/rbac.go +++ b/internal/controller/datadogagent/merger/rbac.go @@ -21,7 +21,7 @@ import ( type RBACManager interface { AddServiceAccount(namespace string, name string) error AddServiceAccountByComponent(namespace, name, component string) error - AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule) error + AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule, saNamespace ...string) error AddPolicyRulesByComponent(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule, component string) error AddRoleBinding(roleNamespace, roleName, saNamespace, saName string, roleRef rbacv1.RoleRef) error AddClusterPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule) error @@ -87,7 +87,7 @@ func (m *rbacManagerImpl) DeleteServiceAccountByComponent(component, namespace s } // AddPolicyRules is used to add PolicyRules to a Role. It also creates the RoleBinding. -func (m *rbacManagerImpl) AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule) error { +func (m *rbacManagerImpl) AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule, saNamespace ...string) error { obj, _ := m.store.GetOrCreate(kubernetes.RolesKind, namespace, roleName) role, ok := obj.(*rbacv1.Role) if !ok { @@ -106,7 +106,13 @@ func (m *rbacManagerImpl) AddPolicyRules(namespace string, roleName string, saNa Name: roleName, } - return m.AddRoleBinding(namespace, roleName, namespace, saName, roleRef) + // If saNamespace is not provided, defaults to using role namespace. + targetSaNamespace := namespace + if len(saNamespace) > 0 { + targetSaNamespace = saNamespace[0] + } + + return m.AddRoleBinding(namespace, roleName, targetSaNamespace, saName, roleRef) } // AddPolicyRulesByComponent is used to add PolicyRules to a Role, create a RoleBinding, and associate them with a component diff --git a/internal/controller/datadogagent/override/global.go b/internal/controller/datadogagent/override/global.go index 2ed3bf94e..675bc41a2 100644 --- a/internal/controller/datadogagent/override/global.go +++ b/internal/controller/datadogagent/override/global.go @@ -10,6 +10,8 @@ import ( "fmt" "path/filepath" + "strconv" + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" apiutils "github.com/DataDog/datadog-operator/api/utils" @@ -17,6 +19,8 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object/volume" "github.com/DataDog/datadog-operator/pkg/defaulting" + "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" + rbacv1 "k8s.io/api/rbac/v1" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -313,6 +317,80 @@ func applyGlobalSettings(logger logr.Logger, manager feature.PodTemplateManagers } } + // Apply SecretBackend config + if config.SecretBackend != nil { + // Set secret backend command + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: apicommon.DDSecretBackendCommand, + Value: apiutils.StringValue(config.SecretBackend.Command), + }) + + // Set secret backend arguments + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: apicommon.DDSecretBackendArguments, + Value: apiutils.StringValue(config.SecretBackend.Args), + }) + + // Set secret backend timeout + if config.SecretBackend.Timeout != nil { + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: apicommon.DDSecretBackendTimeout, + Value: strconv.FormatInt(int64(*config.SecretBackend.Timeout), 10), + }) + } + + var componentSaName string + switch componentName { + case v2alpha1.ClusterAgentComponentName: + componentSaName = v2alpha1.GetClusterAgentServiceAccount(dda) + case v2alpha1.NodeAgentComponentName: + componentSaName = v2alpha1.GetAgentServiceAccount(dda) + case v2alpha1.ClusterChecksRunnerComponentName: + componentSaName = v2alpha1.GetClusterChecksRunnerServiceAccount(dda) + } + + agentName := dda.GetName() + agentNs := dda.GetNamespace() + rbacSuffix := "secret-backend" + + // Set global RBAC config (only if specific roles are not defined) + if apiutils.BoolValue(config.SecretBackend.EnableGlobalPermissions) && config.SecretBackend.Roles == nil { + + var secretBackendGlobalRBACPolicyRules = []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + Verbs: []string{rbac.GetVerb}, + }, + } + + roleName := fmt.Sprintf("%s-%s-%s", agentNs, agentName, rbacSuffix) + + if err := resourcesManager.RBACManager().AddClusterPolicyRules(agentNs, roleName, componentSaName, secretBackendGlobalRBACPolicyRules); err != nil { + logger.Error(err, "Error adding cluster-wide secrets RBAC policy") + } + } + + // Set specific roles for the secret backend + if config.SecretBackend.Roles != nil { + for _, role := range config.SecretBackend.Roles { + secretNs := apiutils.StringValue(role.Namespace) + roleName := fmt.Sprintf("%s-%s-%s", secretNs, agentName, rbacSuffix) + policyRule := []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + ResourceNames: role.Secrets, + Verbs: []string{rbac.GetVerb}, + }, + } + if err := resourcesManager.RBACManager().AddPolicyRules(secretNs, roleName, componentSaName, policyRule, agentNs); err != nil { + logger.Error(err, "Error adding secrets RBAC policy") + } + } + } + } + // Apply FIPS config if config.FIPS != nil && apiutils.BoolValue(config.FIPS.Enabled) { applyFIPSConfig(logger, manager, dda, resourcesManager) diff --git a/internal/controller/datadogagent/override/global_test.go b/internal/controller/datadogagent/override/global_test.go index 95e8e94f0..cc153baa3 100644 --- a/internal/controller/datadogagent/override/global_test.go +++ b/internal/controller/datadogagent/override/global_test.go @@ -6,8 +6,12 @@ package override import ( + "fmt" "testing" + "github.com/DataDog/datadog-operator/pkg/kubernetes" + "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" v2alpha1test "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1/test" @@ -19,16 +23,25 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/fake" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/store" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" ) const ( - hostCAPath = "/host/ca/path/ca.crt" - agentCAPath = "/agent/ca/path/ca.crt" - dockerSocketPath = "/docker/socket/path/docker.sock" + hostCAPath = "/host/ca/path/ca.crt" + agentCAPath = "/agent/ca/path/ca.crt" + dockerSocketPath = "/docker/socket/path/docker.sock" + secretBackendCommand = "foo.sh" + secretBackendArgs = "bar baz" + secretBackendTimeout = 60 + ddaName = "datadog" + ddaNamespace = "system" + secretNamespace = "postgres" ) +var secretNames = []string{"db-username", "db-password"} + func TestNodeAgentComponenGlobalSettings(t *testing.T) { logger := logf.Log.WithName("TestRequiredComponents") @@ -49,6 +62,7 @@ func TestNodeAgentComponenGlobalSettings(t *testing.T) { wantVolumes []*corev1.Volume wantEnvVars []*corev1.EnvVar want func(t testing.TB, mgrInterface feature.PodTemplateManagers, expectedEnvVars []*corev1.EnvVar, expectedVolumes []*corev1.Volume, expectedVolumeMounts []*corev1.VolumeMount) + wantDependency func(t testing.TB, resourcesManager feature.ResourceManagers) }{ { name: "Kubelet volume configured", @@ -143,6 +157,64 @@ func TestNodeAgentComponenGlobalSettings(t *testing.T) { wantVolumes: emptyVolumes, want: assertAll, }, + { + name: "Secret backend - global permissions", + singleContainerStrategyEnabled: false, + dda: addNameNamespaceToDDA( + ddaName, + ddaNamespace, + v2alpha1test.NewDatadogAgentBuilder(). + WithGlobalSecretBackendGlobalPerms(secretBackendCommand, secretBackendArgs, secretBackendTimeout). + BuildWithDefaults(), + ), + wantEnvVars: getExpectedEnvVars([]*corev1.EnvVar{ + { + Name: apicommon.DDSecretBackendCommand, + Value: secretBackendCommand, + }, + { + Name: apicommon.DDSecretBackendArguments, + Value: secretBackendArgs, + }, + { + Name: apicommon.DDSecretBackendTimeout, + Value: "60", + }, + }...), + wantVolumeMounts: emptyVolumeMounts, + wantVolumes: emptyVolumes, + want: assertAll, + wantDependency: assertSecretBackendGlobalPerms, + }, + { + name: "Secret backend - specific secret permissions", + singleContainerStrategyEnabled: false, + dda: addNameNamespaceToDDA( + ddaName, + ddaNamespace, + v2alpha1test.NewDatadogAgentBuilder(). + WithGlobalSecretBackendSpecificRoles(secretBackendCommand, secretBackendArgs, secretBackendTimeout, secretNamespace, secretNames). + BuildWithDefaults(), + ), + wantEnvVars: getExpectedEnvVars([]*corev1.EnvVar{ + { + Name: apicommon.DDSecretBackendCommand, + Value: secretBackendCommand, + }, + { + Name: apicommon.DDSecretBackendArguments, + Value: secretBackendArgs, + }, + { + Name: apicommon.DDSecretBackendTimeout, + Value: "60", + }, + }...), + wantVolumeMounts: emptyVolumeMounts, + wantVolumes: emptyVolumes, + want: assertAll, + wantDependency: assertSecretBackendSpecificPerms, + }, } for _, tt := range tests { @@ -154,6 +226,10 @@ func TestNodeAgentComponenGlobalSettings(t *testing.T) { ApplyGlobalSettingsNodeAgent(logger, podTemplateManager, tt.dda, resourcesManager, tt.singleContainerStrategyEnabled) tt.want(t, podTemplateManager, tt.wantEnvVars, tt.wantVolumes, tt.wantVolumeMounts) + // Assert dependencies if and only if a dependency is expected + if tt.wantDependency != nil { + tt.wantDependency(t, resourcesManager) + } }) } } @@ -240,3 +316,125 @@ func getExpectedVolumeMounts() []*corev1.VolumeMount { }, } } + +func addNameNamespaceToDDA(name string, namespace string, dda *v2alpha1.DatadogAgent) *v2alpha1.DatadogAgent { + dda.Name = name + dda.Namespace = namespace + return dda +} + +func assertSecretBackendGlobalPerms(t testing.TB, resourcesManager feature.ResourceManagers) { + store := resourcesManager.Store() + // ClusterRole and ClusterRoleBinding use the same name + expectedName := fmt.Sprintf("%s-%s-%s", ddaNamespace, ddaName, "secret-backend") + expectedPolicyRules := []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + Verbs: []string{rbac.GetVerb}, + }, + } + crObj, found := store.Get(kubernetes.ClusterRolesKind, "", expectedName) + if !found { + t.Error("Should have created ClusterRole") + } else { + cr := crObj.(*rbacv1.ClusterRole) + assert.True( + t, + apiutils.IsEqualStruct(cr.Rules, expectedPolicyRules), + "ClusterRole Policy Rules \ndiff = %s", cmp.Diff(cr.Rules, expectedPolicyRules), + ) + } + + expectedRoleRef := rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: rbac.ClusterRoleKind, + Name: expectedName, + } + + expectedSubject := []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: ddaName + "-" + v2alpha1.DefaultAgentResourceSuffix, + Namespace: ddaNamespace, + }, + } + + crbObj, found := store.Get(kubernetes.ClusterRoleBindingKind, "", expectedName) + if !found { + t.Error("Should have created ClusterRoleBinding") + } else { + crb := crbObj.(*rbacv1.ClusterRoleBinding) + // Validate ClusterRoleBinding roleRef name + assert.True( + t, + apiutils.IsEqualStruct(crb.RoleRef, expectedRoleRef), + "ClusterRoleBinding Role Ref \ndiff = %s", cmp.Diff(crb.RoleRef, expectedRoleRef), + ) + // Validate ClusterRoleBinding subject + assert.True( + t, + apiutils.IsEqualStruct(crb.Subjects, expectedSubject), + "ClusterRoleBinding Subject \ndiff = %s", cmp.Diff(crb.Subjects, expectedSubject), + ) + } +} + +func assertSecretBackendSpecificPerms(t testing.TB, resourcesManager feature.ResourceManagers) { + store := resourcesManager.Store() + + // Role and RoleBinding use the same name + expectedName := fmt.Sprintf("%s-%s-%s", secretNamespace, ddaName, "secret-backend") + expectedPolicyRules := []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + ResourceNames: secretNames, + Verbs: []string{rbac.GetVerb}, + }, + } + rObj, found := store.Get(kubernetes.RolesKind, secretNamespace, expectedName) + if !found { + t.Error("Should have created Role") + } else { + r := rObj.(*rbacv1.Role) + assert.True( + t, + apiutils.IsEqualStruct(r.Rules, expectedPolicyRules), + "Role Policy Rules \ndiff = %s", cmp.Diff(r.Rules, expectedPolicyRules), + ) + } + + expectedRoleRef := rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: rbac.RoleKind, + Name: expectedName, + } + + expectedSubject := []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: ddaName + "-" + v2alpha1.DefaultAgentResourceSuffix, + Namespace: ddaNamespace, + }, + } + + rbObj, found := store.Get(kubernetes.RoleBindingKind, secretNamespace, expectedName) + if !found { + t.Error("Should have created RoleBinding") + } else { + rb := rbObj.(*rbacv1.RoleBinding) + // Validate RoleBinding roleRef name + assert.True( + t, + apiutils.IsEqualStruct(rb.RoleRef, expectedRoleRef), + "RoleBinding Role Ref \ndiff = %s", cmp.Diff(rb.RoleRef, expectedRoleRef), + ) + // Validate RoleBinding subject + assert.True( + t, + apiutils.IsEqualStruct(rb.Subjects, expectedSubject), + "RoleBinding Subject \ndiff = %s", cmp.Diff(rb.Subjects, expectedSubject), + ) + } +}