From 3692dba3822d624f3a58b5823d30c2991b6b32ef Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Wed, 18 Mar 2026 15:45:17 -0500 Subject: [PATCH 01/11] Add CSI registry allow list check to admission webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using CSI-based library injection, the admission webhook now checks the registry allow list before adding CSI volumes to pods. If a library's registry is not in the allow list, the webhook skips adding the CSI volume entirely. This provides defense in depth alongside the CSI driver's own registry check — the webhook gives clean UX (no unnecessary volumes), while the CSI driver acts as a security backstop. The config key admission_controller.auto_instrumentation.csi_registry_allow_list is set via the Helm chart from the same value used for the CSI driver's DD_REGISTRY_ALLOW_LIST. --- .../mutate/autoinstrumentation/config.go | 7 ++++++ .../libraryinjection/csi.go | 23 +++++++++++++++++++ .../libraryinjection/provider.go | 5 ++++ .../autoinstrumentation/namespace_mutator.go | 1 + pkg/config/setup/common_settings.go | 1 + 5 files changed, 37 insertions(+) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go index 7be211b96c210d..49c3921cc69c3e 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go @@ -46,6 +46,11 @@ type staticConfig struct { // containerRegistry is the container registry to use for the autoinstrumentation logic containerRegistry string + // registryAllowList restricts which registries can be used for CSI-based library injection. + // When non-empty, CSI volumes will only be added for libraries from these registries. + // An empty list allows all registries (default). + registryAllowList []string + // mutateUnlabelled is used to control if we require workloads to have a label when using Local Lib Injection. mutateUnlabelled bool @@ -126,6 +131,7 @@ func NewConfig(datadogConfig config.Component) (*Config, error) { } containerRegistry := mutatecommon.ContainerRegistry(datadogConfig, "admission_controller.auto_instrumentation.container_registry") + registryAllowList := datadogConfig.GetStringSlice("admission_controller.auto_instrumentation.csi_registry_allow_list") mutateUnlabelled := datadogConfig.GetBool("admission_controller.mutate_unlabelled") return &Config{ @@ -134,6 +140,7 @@ func NewConfig(datadogConfig config.Component) (*Config, error) { LanguageDetection: NewLanguageDetectionConfig(datadogConfig), Instrumentation: instrumentationConfig, containerRegistry: containerRegistry, + registryAllowList: registryAllowList, mutateUnlabelled: mutateUnlabelled, initResources: initResources, initSecurityContext: initSecurityContext, diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go index 66f03dcd086ed8..a924a1acd03778 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go @@ -8,8 +8,12 @@ package libraryinjection import ( + "slices" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" + + "github.com/DataDog/datadog-agent/pkg/util/log" ) // CSI driver constants. @@ -50,8 +54,22 @@ func NewCSIProvider(cfg LibraryInjectionConfig) *CSIProvider { } } +// registryAllowed checks if a registry is permitted by the allow list. +// An empty allow list permits all registries. +func (p *CSIProvider) registryAllowed(registry string) bool { + if len(p.cfg.RegistryAllowList) == 0 { + return true + } + return slices.Contains(p.cfg.RegistryAllowList, registry) +} + // InjectInjector mutates the pod to add the APM injector using CSI volumes. func (p *CSIProvider) InjectInjector(pod *corev1.Pod, cfg InjectorConfig) MutationResult { + if !p.registryAllowed(cfg.Package.Registry) { + log.Warnf("Skipping CSI injector injection: registry %q is not in the allow list", cfg.Package.Registry) + return MutationResult{Status: MutationStatusSkipped} + } + patcher := NewPodPatcher(pod, p.cfg.ContainerFilter) // CSI volume for the injector image contents @@ -102,6 +120,11 @@ func (p *CSIProvider) InjectInjector(pod *corev1.Pod, cfg InjectorConfig) Mutati // InjectLibrary mutates the pod to add a language-specific tracing library using CSI volumes. func (p *CSIProvider) InjectLibrary(pod *corev1.Pod, cfg LibraryConfig) MutationResult { + if !p.registryAllowed(cfg.Package.Registry) { + log.Warnf("Skipping CSI library injection for %s: registry %q is not in the allow list", cfg.Language, cfg.Package.Registry) + return MutationResult{Status: MutationStatusSkipped} + } + patcher := NewPodPatcher(pod, p.cfg.ContainerFilter) // CSI volume for the library (uses DatadogLibrary type to mount OCI image contents) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go index 1ab7f80ec37a2a..434e19ef59392c 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go @@ -110,6 +110,11 @@ type LibraryInjectionConfig struct { // InjectionType identifies the type of injection, e.g. "single step" or "lib injection" (for metrics). InjectionType string + + // RegistryAllowList is an optional list of allowed container registries for CSI-based injection. + // When non-empty and using CSI injection mode, only libraries from these registries will be injected. + // An empty list allows all registries (default). + RegistryAllowList []string } // LibraryInjectionProvider defines the strategy for injecting APM libraries into pods. diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/namespace_mutator.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/namespace_mutator.go index 62808663a81bfc..ec015cd78d3894 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/namespace_mutator.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/namespace_mutator.go @@ -111,6 +111,7 @@ func (m *mutatorCore) apmInjectionMutator(config extractedPodLibInfo, autoDetect Debug: m.isDebugEnabled(pod), AutoDetected: autoDetected, InjectionType: injectionType, + RegistryAllowList: m.config.registryAllowList, Injector: libraryinjection.InjectorConfig{ Package: m.resolveInjectorImage(pod), }, diff --git a/pkg/config/setup/common_settings.go b/pkg/config/setup/common_settings.go index 6d03f000540fb4..600e22e71f979b 100644 --- a/pkg/config/setup/common_settings.go +++ b/pkg/config/setup/common_settings.go @@ -664,6 +664,7 @@ func initCoreAgentFull(config pkgconfigmodel.Setup) { config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.patcher.fallback_to_file_provider", false) // to be enabled only in e2e tests config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.patcher.file_provider_path", "/etc/datadog-agent/patch/auto-instru.json") // to be used only in e2e tests config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.inject_auto_detected_libraries", true) // allows injecting libraries for languages detected by automatic language detection feature + config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.csi_registry_allow_list", []string{}) // restricts which registries can be used for CSI-based library injection config.BindEnv("admission_controller.auto_instrumentation.init_resources.cpu") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' config.BindEnv("admission_controller.auto_instrumentation.init_resources.memory") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' config.BindEnv("admission_controller.auto_instrumentation.init_security_context") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' From f46dad48853803181634dbfbac461470dc412c5d Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Wed, 18 Mar 2026 16:23:24 -0500 Subject: [PATCH 02/11] Fix Viper GetStringSlice for CSI registry allow list Use ParseEnvAsStringSlice (the established pattern in the codebase) to register an env var transformer that splits comma-separated values. This replaces the ad-hoc splitStringSlice wrapper and is consistent with how other string slice configs handle the Viper limitation (e.g. apm_config.features, apm_config.ignore_resources). --- pkg/config/setup/common_settings.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/config/setup/common_settings.go b/pkg/config/setup/common_settings.go index 600e22e71f979b..55c28a40d8becf 100644 --- a/pkg/config/setup/common_settings.go +++ b/pkg/config/setup/common_settings.go @@ -665,6 +665,16 @@ func initCoreAgentFull(config pkgconfigmodel.Setup) { config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.patcher.file_provider_path", "/etc/datadog-agent/patch/auto-instru.json") // to be used only in e2e tests config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.inject_auto_detected_libraries", true) // allows injecting libraries for languages detected by automatic language detection feature config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.csi_registry_allow_list", []string{}) // restricts which registries can be used for CSI-based library injection + config.ParseEnvAsStringSlice("admission_controller.auto_instrumentation.csi_registry_allow_list", func(s string) []string { + var result []string + for _, r := range strings.Split(s, ",") { + r = strings.TrimSpace(r) + if r != "" { + result = append(result, r) + } + } + return result + }) config.BindEnv("admission_controller.auto_instrumentation.init_resources.cpu") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' config.BindEnv("admission_controller.auto_instrumentation.init_resources.memory") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' config.BindEnv("admission_controller.auto_instrumentation.init_security_context") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' From 67f3f1669b304ed17d334c08a138638e3900f904 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Mon, 23 Mar 2026 11:38:28 -0500 Subject: [PATCH 03/11] Rename csi_registry_allow_list to container_registry_allow_list The allow-list check now lives in the admission webhook (mutator.go) and covers all injection modes (init-container, image-volume, CSI), not just CSI. Rename the config key accordingly. --- .../mutate/autoinstrumentation/config.go | 6 ++--- .../libraryinjection/csi.go | 23 ------------------- .../libraryinjection/mutator.go | 10 ++++++++ .../libraryinjection/provider.go | 4 ++-- pkg/config/setup/common_settings.go | 4 ++-- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go index 49c3921cc69c3e..5250620120caea 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/config.go @@ -46,8 +46,8 @@ type staticConfig struct { // containerRegistry is the container registry to use for the autoinstrumentation logic containerRegistry string - // registryAllowList restricts which registries can be used for CSI-based library injection. - // When non-empty, CSI volumes will only be added for libraries from these registries. + // registryAllowList restricts which registries can be used for library injection. + // When non-empty, libraries from registries not in this list will not be injected. // An empty list allows all registries (default). registryAllowList []string @@ -131,7 +131,7 @@ func NewConfig(datadogConfig config.Component) (*Config, error) { } containerRegistry := mutatecommon.ContainerRegistry(datadogConfig, "admission_controller.auto_instrumentation.container_registry") - registryAllowList := datadogConfig.GetStringSlice("admission_controller.auto_instrumentation.csi_registry_allow_list") + registryAllowList := datadogConfig.GetStringSlice("admission_controller.auto_instrumentation.container_registry_allow_list") mutateUnlabelled := datadogConfig.GetBool("admission_controller.mutate_unlabelled") return &Config{ diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go index a924a1acd03778..66f03dcd086ed8 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/csi.go @@ -8,12 +8,8 @@ package libraryinjection import ( - "slices" - corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" - - "github.com/DataDog/datadog-agent/pkg/util/log" ) // CSI driver constants. @@ -54,22 +50,8 @@ func NewCSIProvider(cfg LibraryInjectionConfig) *CSIProvider { } } -// registryAllowed checks if a registry is permitted by the allow list. -// An empty allow list permits all registries. -func (p *CSIProvider) registryAllowed(registry string) bool { - if len(p.cfg.RegistryAllowList) == 0 { - return true - } - return slices.Contains(p.cfg.RegistryAllowList, registry) -} - // InjectInjector mutates the pod to add the APM injector using CSI volumes. func (p *CSIProvider) InjectInjector(pod *corev1.Pod, cfg InjectorConfig) MutationResult { - if !p.registryAllowed(cfg.Package.Registry) { - log.Warnf("Skipping CSI injector injection: registry %q is not in the allow list", cfg.Package.Registry) - return MutationResult{Status: MutationStatusSkipped} - } - patcher := NewPodPatcher(pod, p.cfg.ContainerFilter) // CSI volume for the injector image contents @@ -120,11 +102,6 @@ func (p *CSIProvider) InjectInjector(pod *corev1.Pod, cfg InjectorConfig) Mutati // InjectLibrary mutates the pod to add a language-specific tracing library using CSI volumes. func (p *CSIProvider) InjectLibrary(pod *corev1.Pod, cfg LibraryConfig) MutationResult { - if !p.registryAllowed(cfg.Package.Registry) { - log.Warnf("Skipping CSI library injection for %s: registry %q is not in the allow list", cfg.Language, cfg.Package.Registry) - return MutationResult{Status: MutationStatusSkipped} - } - patcher := NewPodPatcher(pod, p.cfg.ContainerFilter) // CSI volume for the library (uses DatadogLibrary type to mount OCI image contents) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go index f4712b7834c0fd..aad70684bac983 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go @@ -9,6 +9,7 @@ package libraryinjection import ( "fmt" + "slices" "strconv" "time" @@ -28,6 +29,15 @@ import ( // // Returns an error if the injection fails. func InjectAPMLibraries(pod *corev1.Pod, cfg LibraryInjectionConfig) error { + // Check the registry allow list before any injection. All images use the same registry, + // so checking the injector registry is sufficient. + if len(cfg.RegistryAllowList) > 0 && !slices.Contains(cfg.RegistryAllowList, cfg.Injector.Package.Registry) { + msg := fmt.Sprintf("registry %q is not in the allow list", cfg.Injector.Package.Registry) + log.Warnf("Skipping APM library injection for pod %s: %s", mutatecommon.PodString(pod), msg) + annotation.Set(pod, annotation.InjectionError, msg) + return nil + } + // Select the provider based on the injection mode (annotation or default) factory := NewProviderFactory(InjectionMode(cfg.InjectionMode)) provider := factory.GetProviderForPod(pod, cfg) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go index 434e19ef59392c..55546317008d44 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/provider.go @@ -111,8 +111,8 @@ type LibraryInjectionConfig struct { // InjectionType identifies the type of injection, e.g. "single step" or "lib injection" (for metrics). InjectionType string - // RegistryAllowList is an optional list of allowed container registries for CSI-based injection. - // When non-empty and using CSI injection mode, only libraries from these registries will be injected. + // RegistryAllowList is an optional list of allowed container registries for library injection. + // When non-empty, only libraries from these registries will be injected. // An empty list allows all registries (default). RegistryAllowList []string } diff --git a/pkg/config/setup/common_settings.go b/pkg/config/setup/common_settings.go index 55c28a40d8becf..f18d369449c97f 100644 --- a/pkg/config/setup/common_settings.go +++ b/pkg/config/setup/common_settings.go @@ -664,8 +664,8 @@ func initCoreAgentFull(config pkgconfigmodel.Setup) { config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.patcher.fallback_to_file_provider", false) // to be enabled only in e2e tests config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.patcher.file_provider_path", "/etc/datadog-agent/patch/auto-instru.json") // to be used only in e2e tests config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.inject_auto_detected_libraries", true) // allows injecting libraries for languages detected by automatic language detection feature - config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.csi_registry_allow_list", []string{}) // restricts which registries can be used for CSI-based library injection - config.ParseEnvAsStringSlice("admission_controller.auto_instrumentation.csi_registry_allow_list", func(s string) []string { + config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.container_registry_allow_list", []string{}) // restricts which registries can be used for library injection + config.ParseEnvAsStringSlice("admission_controller.auto_instrumentation.container_registry_allow_list", func(s string) []string { var result []string for _, r := range strings.Split(s, ",") { r = strings.TrimSpace(r) From 89c6ff50c3d895ea33f09080ebd995d3dba6c4d4 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Thu, 26 Mar 2026 13:48:07 -0500 Subject: [PATCH 04/11] Add unit tests for registry allow list enforcement in InjectAPMLibraries Test three cases: - Empty allow list permits injection from any registry (backward compat) - Registry in allow list permits injection - Registry not in allow list blocks injection and sets error annotation --- .../libraryinjection/mutator_test.go | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go index df82e07358e87f..8383201eace468 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go @@ -19,6 +19,80 @@ import ( "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection" ) +func TestInjectAPMLibraries_RegistryAllowList(t *testing.T) { + tests := []struct { + name string + registryAllowList []string + injectorRegistry string + expectInjected bool + expectErrorAnnot bool + }{ + { + name: "empty allow list permits any registry", + registryAllowList: []string{}, + injectorRegistry: "registry.datadoghq.com", + expectInjected: true, + expectErrorAnnot: false, + }, + { + name: "registry in allow list permits injection", + registryAllowList: []string{"gcr.io/datadoghq", "public.ecr.aws/datadog"}, + injectorRegistry: "gcr.io/datadoghq", + expectInjected: true, + expectErrorAnnot: false, + }, + { + name: "registry not in allow list blocks injection", + registryAllowList: []string{"fake.registry.invalid"}, + injectorRegistry: "registry.datadoghq.com", + expectInjected: false, + expectErrorAnnot: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "app"}}, + }, + } + + err := libraryinjection.InjectAPMLibraries(pod, libraryinjection.LibraryInjectionConfig{ + InjectionMode: "auto", + KubeServerVersion: &version.Info{GitVersion: "v1.30.9"}, + RegistryAllowList: tt.registryAllowList, + Injector: libraryinjection.InjectorConfig{ + Package: libraryinjection.NewLibraryImageFromFullRef(tt.injectorRegistry+"/apm-inject:0.52.0", "0.52.0"), + }, + }) + require.NoError(t, err) + + _, hasErrorAnnot := annotation.Get(pod, annotation.InjectionError) + require.Equal(t, tt.expectErrorAnnot, hasErrorAnnot) + + if tt.expectErrorAnnot { + val, _ := annotation.Get(pod, annotation.InjectionError) + require.Contains(t, val, "not in the allow list") + } + + // Verify injection state: check whether LD_PRELOAD was set on the container. + injected := false + for _, env := range pod.Spec.Containers[0].Env { + if env.Name == "LD_PRELOAD" { + injected = true + break + } + } + require.Equal(t, tt.expectInjected, injected) + }) + } +} + func TestInjectAPMLibraries_StopsGracefullyWhenProviderUnavailable(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ From acd837a89506142b265d8448f7ef95e13178fb1e Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Thu, 26 Mar 2026 14:00:39 -0500 Subject: [PATCH 05/11] Add allowed + blocked registry allow list e2e tests Replace global.containerRegistryAllowList approach with setting DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_CONTAINER_REGISTRY_ALLOW_LIST directly via clusterAgent.env, so the test works with the current helm chart version without needing the helm-charts PR to merge first. Two test cases: - TestRegistryAllowListBlocked: allow list = fake.registry.invalid, injection is blocked and error annotation is set - TestRegistryAllowListAllowed: allow list = registry.datadoghq.com (the injector's registry), injection proceeds and traces arrive --- test/new-e2e/tests/ssi/ssi_test.go | 83 +++++++++++++++++++ .../testdata/registry_allow_list_allowed.yaml | 32 +++++++ .../testdata/registry_allow_list_blocked.yaml | 32 +++++++ 3 files changed, 147 insertions(+) create mode 100644 test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml create mode 100644 test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml diff --git a/test/new-e2e/tests/ssi/ssi_test.go b/test/new-e2e/tests/ssi/ssi_test.go index 3002926ac1082a..4f84e6218a310c 100644 --- a/test/new-e2e/tests/ssi/ssi_test.go +++ b/test/new-e2e/tests/ssi/ssi_test.go @@ -42,6 +42,12 @@ var namespaceSelectionHelmValues string //go:embed testdata/workload_selection.yaml var workloadSelectionHelmValues string +//go:embed testdata/registry_allow_list_blocked.yaml +var registryAllowListBlockedHelmValues string + +//go:embed testdata/registry_allow_list_allowed.yaml +var registryAllowListAllowedHelmValues string + // ssiSuite runs all SSI test groups on a single cluster, calling UpdateEnv at the start of // each group to update the env (workloads, helm values). type ssiSuite struct { @@ -327,3 +333,80 @@ func (v *ssiSuite) TestWorkloadSelection() { podValidator.RequireNoInjection(v.T()) }) } + +func (v *ssiSuite) TestRegistryAllowListBlocked() { + v.UpdateEnv(Provisioner(ProvisionerOptions{ + AgentOptions: []kubernetesagentparams.Option{ + kubernetesagentparams.WithHelmValues(registryAllowListBlockedHelmValues), + }, + AgentDependentWorkloadAppFunc: func(e config.Env, kubeProvider *kubernetes.Provider, dependsOnAgent pulumi.ResourceOption) (*compkube.Workload, error) { + return singlestep.Scenario(e, kubeProvider, "registry-allow-list", []singlestep.Namespace{ + { + Name: "registry-allow-list", + Apps: []singlestep.App{ + { + Name: "registry-allow-list-app", + Image: "registry.datadoghq.com/injector-dev/python", + Version: "16ad9d4b", + Port: 8080, + }, + }, + }, + }, dependsOnAgent) + }, + })) + + v.Run("InjectionBlockedByAllowList", func() { + k8s := v.Env().KubernetesCluster.Client() + pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-app") + + // The injector image comes from registry.datadoghq.com, which is not in the + // allow list (fake.registry.invalid). Injection should be skipped entirely. + podValidator := testutils.NewPodValidator(pod, testutils.InjectionModeAuto) + podValidator.RequireNoInjection(v.T()) + + // The webhook sets an error annotation explaining why injection was skipped. + errAnnotation := pod.Annotations["internal.apm.datadoghq.com/injection-error"] + require.NotEmpty(v.T(), errAnnotation, "expected injection-error annotation to be set") + require.Contains(v.T(), errAnnotation, "not in the allow list") + }) +} + +func (v *ssiSuite) TestRegistryAllowListAllowed() { + v.UpdateEnv(Provisioner(ProvisionerOptions{ + AgentOptions: []kubernetesagentparams.Option{ + kubernetesagentparams.WithHelmValues(registryAllowListAllowedHelmValues), + }, + AgentDependentWorkloadAppFunc: func(e config.Env, kubeProvider *kubernetes.Provider, dependsOnAgent pulumi.ResourceOption) (*compkube.Workload, error) { + return singlestep.Scenario(e, kubeProvider, "registry-allow-list", []singlestep.Namespace{ + { + Name: "registry-allow-list", + Apps: []singlestep.App{ + { + Name: "registry-allow-list-app", + Image: "registry.datadoghq.com/injector-dev/python", + Version: "16ad9d4b", + Port: 8080, + }, + }, + }, + }, dependsOnAgent) + }, + })) + + v.Run("InjectionAllowedByAllowList", func() { + intake := v.Env().FakeIntake.Client() + k8s := v.Env().KubernetesCluster.Client() + + pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-app") + podValidator := testutils.NewPodValidator(pod, testutils.InjectionModeAuto) + podValidator.RequireInjection(v.T(), []string{"registry-allow-list-app"}) + podValidator.RequireInjectorVersion(v.T(), "0.54.0") + podValidator.RequireLibraryVersions(v.T(), map[string]string{"python": "v3.18.1"}) + + require.Eventually(v.T(), func() bool { + traces := FindTracesForService(v.T(), intake, "registry-allow-list-app") + return len(traces) != 0 + }, 1*time.Minute, 10*time.Second, "did not find any traces at intake for DD_SERVICE %s", "registry-allow-list-app") + }) +} diff --git a/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml b/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml new file mode 100644 index 00000000000000..cc2b7a630bd20b --- /dev/null +++ b/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml @@ -0,0 +1,32 @@ +--- +clusterAgent: + admissionController: + configMode: "hostip" + env: + - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_CONTAINER_REGISTRY_ALLOW_LIST + value: "registry.datadoghq.com" + - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_GRADUAL_ROLLOUT_ENABLED + value: "false" + +# Temporary until the Datadog CSI driver is released +datadog-csi-driver: + image: + repository: "gcr.io/datadoghq/csi-driver" + tag: "1.2.0" + +datadog: + csi: + enabled: true + apm: + instrumentation: + enabled: true + injector: + imageTag: "0.54.0" + enabledNamespaces: [] + targets: + - name: "apps" + namespaceSelector: + matchNames: + - "registry-allow-list" + ddTraceVersions: + python: "v3.18.1" diff --git a/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml b/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml new file mode 100644 index 00000000000000..8ef797b5f31ba5 --- /dev/null +++ b/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml @@ -0,0 +1,32 @@ +--- +clusterAgent: + admissionController: + configMode: "hostip" + env: + - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_CONTAINER_REGISTRY_ALLOW_LIST + value: "fake.registry.invalid" + - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_GRADUAL_ROLLOUT_ENABLED + value: "false" + +# Temporary until the Datadog CSI driver is released +datadog-csi-driver: + image: + repository: "gcr.io/datadoghq/csi-driver" + tag: "1.2.0" + +datadog: + csi: + enabled: true + apm: + instrumentation: + enabled: true + injector: + imageTag: "0.54.0" + enabledNamespaces: [] + targets: + - name: "apps" + namespaceSelector: + matchNames: + - "registry-allow-list" + ddTraceVersions: + python: "v3.18.1" From 46b9454978d6b1a7c93e363915a6acb012eac0a9 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Thu, 26 Mar 2026 15:28:12 -0500 Subject: [PATCH 06/11] Add release note for SSI registry allow list feature --- ...i-registry-allow-list-1fb5c3991073fbf8.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 releasenotes/notes/ssi-registry-allow-list-1fb5c3991073fbf8.yaml diff --git a/releasenotes/notes/ssi-registry-allow-list-1fb5c3991073fbf8.yaml b/releasenotes/notes/ssi-registry-allow-list-1fb5c3991073fbf8.yaml new file mode 100644 index 00000000000000..0fd148fb6e238b --- /dev/null +++ b/releasenotes/notes/ssi-registry-allow-list-1fb5c3991073fbf8.yaml @@ -0,0 +1,18 @@ +# Each section from every release note are combined when the +# CHANGELOG.rst is rendered. So the text needs to be worded so that +# it does not depend on any information only available in another +# section. This may mean repeating some details, but each section +# must be readable independently of the other. +# +# Each section note must be formatted as reStructuredText. +--- +features: + - | + Add ``admission_controller.auto_instrumentation.container_registry_allow_list`` + configuration option (env var ``DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_CONTAINER_REGISTRY_ALLOW_LIST``) + to restrict which container registries can be used as sources for APM library + injection via Single Step Instrumentation. When set to a non-empty + comma-separated list, the admission controller will skip injection for any pod + whose injector image registry is not in the list, and will set the + ``internal.apm.datadoghq.com/injection-error`` annotation with the reason. + An empty list (the default) allows injection from any registry. From c70ef79b51ff2708e5237159b5672c1cdcb67fdc Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Thu, 26 Mar 2026 15:34:37 -0500 Subject: [PATCH 07/11] Remove unnecessary GRADUAL_ROLLOUT_ENABLED override --- .../new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml | 2 -- .../new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml | 2 -- 2 files changed, 4 deletions(-) diff --git a/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml b/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml index cc2b7a630bd20b..278ebd56aa364e 100644 --- a/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml +++ b/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml @@ -5,8 +5,6 @@ clusterAgent: env: - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_CONTAINER_REGISTRY_ALLOW_LIST value: "registry.datadoghq.com" - - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_GRADUAL_ROLLOUT_ENABLED - value: "false" # Temporary until the Datadog CSI driver is released datadog-csi-driver: diff --git a/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml b/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml index 8ef797b5f31ba5..58873b14430f7b 100644 --- a/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml +++ b/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml @@ -5,8 +5,6 @@ clusterAgent: env: - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_CONTAINER_REGISTRY_ALLOW_LIST value: "fake.registry.invalid" - - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_GRADUAL_ROLLOUT_ENABLED - value: "false" # Temporary until the Datadog CSI driver is released datadog-csi-driver: From 10e5a41d6d8a208500def9007d9850de164c54f0 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Thu, 26 Mar 2026 20:46:24 -0500 Subject: [PATCH 08/11] Fix gofmt formatting --- .../libraryinjection/mutator_test.go | 6 +++--- pkg/config/setup/common_settings.go | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go index 8383201eace468..ea10ca4137e9ba 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go @@ -28,11 +28,11 @@ func TestInjectAPMLibraries_RegistryAllowList(t *testing.T) { expectErrorAnnot bool }{ { - name: "empty allow list permits any registry", + name: "empty allow list permits any registry", registryAllowList: []string{}, injectorRegistry: "registry.datadoghq.com", - expectInjected: true, - expectErrorAnnot: false, + expectInjected: true, + expectErrorAnnot: false, }, { name: "registry in allow list permits injection", diff --git a/pkg/config/setup/common_settings.go b/pkg/config/setup/common_settings.go index 6f5a56dd40d45e..4eb2ca6e88917d 100644 --- a/pkg/config/setup/common_settings.go +++ b/pkg/config/setup/common_settings.go @@ -665,7 +665,7 @@ func initCoreAgentFull(config pkgconfigmodel.Setup) { config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.patcher.fallback_to_file_provider", false) // to be enabled only in e2e tests config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.patcher.file_provider_path", "/etc/datadog-agent/patch/auto-instru.json") // to be used only in e2e tests config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.inject_auto_detected_libraries", true) // allows injecting libraries for languages detected by automatic language detection feature - config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.container_registry_allow_list", []string{}) // restricts which registries can be used for library injection + config.BindEnvAndSetDefault("admission_controller.auto_instrumentation.container_registry_allow_list", []string{}) // restricts which registries can be used for library injection config.ParseEnvAsStringSlice("admission_controller.auto_instrumentation.container_registry_allow_list", func(s string) []string { var result []string for _, r := range strings.Split(s, ",") { @@ -676,13 +676,13 @@ func initCoreAgentFull(config pkgconfigmodel.Setup) { } return result }) - config.BindEnv("admission_controller.auto_instrumentation.init_resources.cpu") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' - config.BindEnv("admission_controller.auto_instrumentation.init_resources.memory") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' - config.BindEnv("admission_controller.auto_instrumentation.init_security_context") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' - config.BindEnv("admission_controller.auto_instrumentation.asm.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_APPSEC_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for ASM which is implemented in the client libraries - config.BindEnv("admission_controller.auto_instrumentation.iast.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_IAST_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for IAST which is implemented in the client libraries - config.BindEnv("admission_controller.auto_instrumentation.asm_sca.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_APPSEC_SCA_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for SCA - config.BindEnv("admission_controller.auto_instrumentation.profiling.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_PROFILING_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for profiling + config.BindEnv("admission_controller.auto_instrumentation.init_resources.cpu") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' + config.BindEnv("admission_controller.auto_instrumentation.init_resources.memory") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' + config.BindEnv("admission_controller.auto_instrumentation.init_security_context") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' + config.BindEnv("admission_controller.auto_instrumentation.asm.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_APPSEC_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for ASM which is implemented in the client libraries + config.BindEnv("admission_controller.auto_instrumentation.iast.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_IAST_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for IAST which is implemented in the client libraries + config.BindEnv("admission_controller.auto_instrumentation.asm_sca.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_APPSEC_SCA_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for SCA + config.BindEnv("admission_controller.auto_instrumentation.profiling.enabled", "DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_PROFILING_ENABLED") //nolint:forbidigo // TODO: replace by 'SetDefaultAndBindEnv' // config for profiling config.BindEnvAndSetDefault("admission_controller.cws_instrumentation.enabled", false) config.BindEnvAndSetDefault("admission_controller.cws_instrumentation.pod_endpoint", "/inject-pod-cws") config.BindEnvAndSetDefault("admission_controller.cws_instrumentation.command_endpoint", "/inject-command-cws") From 0fde2bd67e788635d4f89cd157b5a7b6603ee0d1 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Fri, 27 Mar 2026 12:57:22 -0500 Subject: [PATCH 09/11] Merge registry allow list test scenarios into a single test --- test/new-e2e/tests/ssi/ssi_test.go | 37 ++++++++++-------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/test/new-e2e/tests/ssi/ssi_test.go b/test/new-e2e/tests/ssi/ssi_test.go index 4f84e6218a310c..2099dd6313b473 100644 --- a/test/new-e2e/tests/ssi/ssi_test.go +++ b/test/new-e2e/tests/ssi/ssi_test.go @@ -334,24 +334,22 @@ func (v *ssiSuite) TestWorkloadSelection() { }) } -func (v *ssiSuite) TestRegistryAllowListBlocked() { +func (v *ssiSuite) TestRegistryAllowList() { + registryAllowListApp := singlestep.App{ + Name: "registry-allow-list-app", + Image: "registry.datadoghq.com/injector-dev/python", + Version: "16ad9d4b", + Port: 8080, + } + + // Scenario 1: registry not in allow list — injection should be blocked. v.UpdateEnv(Provisioner(ProvisionerOptions{ AgentOptions: []kubernetesagentparams.Option{ kubernetesagentparams.WithHelmValues(registryAllowListBlockedHelmValues), }, AgentDependentWorkloadAppFunc: func(e config.Env, kubeProvider *kubernetes.Provider, dependsOnAgent pulumi.ResourceOption) (*compkube.Workload, error) { return singlestep.Scenario(e, kubeProvider, "registry-allow-list", []singlestep.Namespace{ - { - Name: "registry-allow-list", - Apps: []singlestep.App{ - { - Name: "registry-allow-list-app", - Image: "registry.datadoghq.com/injector-dev/python", - Version: "16ad9d4b", - Port: 8080, - }, - }, - }, + {Name: "registry-allow-list", Apps: []singlestep.App{registryAllowListApp}}, }, dependsOnAgent) }, })) @@ -370,26 +368,15 @@ func (v *ssiSuite) TestRegistryAllowListBlocked() { require.NotEmpty(v.T(), errAnnotation, "expected injection-error annotation to be set") require.Contains(v.T(), errAnnotation, "not in the allow list") }) -} -func (v *ssiSuite) TestRegistryAllowListAllowed() { + // Scenario 2: registry in allow list — injection should proceed. v.UpdateEnv(Provisioner(ProvisionerOptions{ AgentOptions: []kubernetesagentparams.Option{ kubernetesagentparams.WithHelmValues(registryAllowListAllowedHelmValues), }, AgentDependentWorkloadAppFunc: func(e config.Env, kubeProvider *kubernetes.Provider, dependsOnAgent pulumi.ResourceOption) (*compkube.Workload, error) { return singlestep.Scenario(e, kubeProvider, "registry-allow-list", []singlestep.Namespace{ - { - Name: "registry-allow-list", - Apps: []singlestep.App{ - { - Name: "registry-allow-list-app", - Image: "registry.datadoghq.com/injector-dev/python", - Version: "16ad9d4b", - Port: 8080, - }, - }, - }, + {Name: "registry-allow-list", Apps: []singlestep.App{registryAllowListApp}}, }, dependsOnAgent) }, })) From 305909b630e7983cecbd72dac86f1cab43ae73c0 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Fri, 27 Mar 2026 13:18:17 -0500 Subject: [PATCH 10/11] Test both allow list scenarios in a single UpdateEnv Deploy two apps in the same cluster with allow list = registry.datadoghq.com: - registry-allow-list-allowed: uses default injector, injection proceeds - registry-allow-list-blocked: pod annotation overrides injector image to fake.registry.invalid (not in allow list), injection is blocked This avoids a second UpdateEnv call (and second cluster setup) by using the admission.datadoghq.com/apm-inject.custom-image annotation to point one pod at an injector registry that is not in the allow list. --- test/new-e2e/tests/ssi/ssi_test.go | 92 +++++++++---------- ..._allowed.yaml => registry_allow_list.yaml} | 0 .../testdata/registry_allow_list_blocked.yaml | 30 ------ 3 files changed, 46 insertions(+), 76 deletions(-) rename test/new-e2e/tests/ssi/testdata/{registry_allow_list_allowed.yaml => registry_allow_list.yaml} (100%) delete mode 100644 test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml diff --git a/test/new-e2e/tests/ssi/ssi_test.go b/test/new-e2e/tests/ssi/ssi_test.go index 2099dd6313b473..7a2643225a9f2e 100644 --- a/test/new-e2e/tests/ssi/ssi_test.go +++ b/test/new-e2e/tests/ssi/ssi_test.go @@ -42,11 +42,8 @@ var namespaceSelectionHelmValues string //go:embed testdata/workload_selection.yaml var workloadSelectionHelmValues string -//go:embed testdata/registry_allow_list_blocked.yaml -var registryAllowListBlockedHelmValues string - -//go:embed testdata/registry_allow_list_allowed.yaml -var registryAllowListAllowedHelmValues string +//go:embed testdata/registry_allow_list.yaml +var registryAllowListHelmValues string // ssiSuite runs all SSI test groups on a single cluster, calling UpdateEnv at the start of // each group to update the env (workloads, helm values). @@ -335,48 +332,37 @@ func (v *ssiSuite) TestWorkloadSelection() { } func (v *ssiSuite) TestRegistryAllowList() { - registryAllowListApp := singlestep.App{ - Name: "registry-allow-list-app", - Image: "registry.datadoghq.com/injector-dev/python", - Version: "16ad9d4b", - Port: 8080, - } - - // Scenario 1: registry not in allow list — injection should be blocked. + // Both apps run in the same cluster with allow list = registry.datadoghq.com. + // The "allowed" app uses the default injector (registry.datadoghq.com) — injection proceeds. + // The "blocked" app overrides the injector image to fake.registry.invalid via pod + // annotation, which is not in the allow list — injection is skipped. v.UpdateEnv(Provisioner(ProvisionerOptions{ AgentOptions: []kubernetesagentparams.Option{ - kubernetesagentparams.WithHelmValues(registryAllowListBlockedHelmValues), + kubernetesagentparams.WithHelmValues(registryAllowListHelmValues), }, AgentDependentWorkloadAppFunc: func(e config.Env, kubeProvider *kubernetes.Provider, dependsOnAgent pulumi.ResourceOption) (*compkube.Workload, error) { return singlestep.Scenario(e, kubeProvider, "registry-allow-list", []singlestep.Namespace{ - {Name: "registry-allow-list", Apps: []singlestep.App{registryAllowListApp}}, - }, dependsOnAgent) - }, - })) - - v.Run("InjectionBlockedByAllowList", func() { - k8s := v.Env().KubernetesCluster.Client() - pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-app") - - // The injector image comes from registry.datadoghq.com, which is not in the - // allow list (fake.registry.invalid). Injection should be skipped entirely. - podValidator := testutils.NewPodValidator(pod, testutils.InjectionModeAuto) - podValidator.RequireNoInjection(v.T()) - - // The webhook sets an error annotation explaining why injection was skipped. - errAnnotation := pod.Annotations["internal.apm.datadoghq.com/injection-error"] - require.NotEmpty(v.T(), errAnnotation, "expected injection-error annotation to be set") - require.Contains(v.T(), errAnnotation, "not in the allow list") - }) - - // Scenario 2: registry in allow list — injection should proceed. - v.UpdateEnv(Provisioner(ProvisionerOptions{ - AgentOptions: []kubernetesagentparams.Option{ - kubernetesagentparams.WithHelmValues(registryAllowListAllowedHelmValues), - }, - AgentDependentWorkloadAppFunc: func(e config.Env, kubeProvider *kubernetes.Provider, dependsOnAgent pulumi.ResourceOption) (*compkube.Workload, error) { - return singlestep.Scenario(e, kubeProvider, "registry-allow-list", []singlestep.Namespace{ - {Name: "registry-allow-list", Apps: []singlestep.App{registryAllowListApp}}, + { + Name: "registry-allow-list", + Apps: []singlestep.App{ + { + Name: "registry-allow-list-allowed", + Image: "registry.datadoghq.com/injector-dev/python", + Version: "16ad9d4b", + Port: 8080, + }, + { + Name: "registry-allow-list-blocked", + Image: "registry.datadoghq.com/injector-dev/python", + Version: "16ad9d4b", + Port: 8080, + PodAnnotations: map[string]string{ + // Override injector to a registry not in the allow list. + "admission.datadoghq.com/apm-inject.custom-image": "fake.registry.invalid/apm-inject:0.54.0", + }, + }, + }, + }, }, dependsOnAgent) }, })) @@ -385,15 +371,29 @@ func (v *ssiSuite) TestRegistryAllowList() { intake := v.Env().FakeIntake.Client() k8s := v.Env().KubernetesCluster.Client() - pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-app") + pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-allowed") podValidator := testutils.NewPodValidator(pod, testutils.InjectionModeAuto) - podValidator.RequireInjection(v.T(), []string{"registry-allow-list-app"}) + podValidator.RequireInjection(v.T(), []string{"registry-allow-list-allowed"}) podValidator.RequireInjectorVersion(v.T(), "0.54.0") podValidator.RequireLibraryVersions(v.T(), map[string]string{"python": "v3.18.1"}) require.Eventually(v.T(), func() bool { - traces := FindTracesForService(v.T(), intake, "registry-allow-list-app") + traces := FindTracesForService(v.T(), intake, "registry-allow-list-allowed") return len(traces) != 0 - }, 1*time.Minute, 10*time.Second, "did not find any traces at intake for DD_SERVICE %s", "registry-allow-list-app") + }, 1*time.Minute, 10*time.Second, "did not find any traces at intake for DD_SERVICE %s", "registry-allow-list-allowed") + }) + + v.Run("InjectionBlockedByAllowList", func() { + k8s := v.Env().KubernetesCluster.Client() + pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-blocked") + + // The injector image is overridden to fake.registry.invalid via pod annotation, + // which is not in the allow list. Injection should be skipped entirely. + podValidator := testutils.NewPodValidator(pod, testutils.InjectionModeAuto) + podValidator.RequireNoInjection(v.T()) + + errAnnotation := pod.Annotations["internal.apm.datadoghq.com/injection-error"] + require.NotEmpty(v.T(), errAnnotation, "expected injection-error annotation to be set") + require.Contains(v.T(), errAnnotation, "not in the allow list") }) } diff --git a/test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml b/test/new-e2e/tests/ssi/testdata/registry_allow_list.yaml similarity index 100% rename from test/new-e2e/tests/ssi/testdata/registry_allow_list_allowed.yaml rename to test/new-e2e/tests/ssi/testdata/registry_allow_list.yaml diff --git a/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml b/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml deleted file mode 100644 index 58873b14430f7b..00000000000000 --- a/test/new-e2e/tests/ssi/testdata/registry_allow_list_blocked.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -clusterAgent: - admissionController: - configMode: "hostip" - env: - - name: DD_ADMISSION_CONTROLLER_AUTO_INSTRUMENTATION_CONTAINER_REGISTRY_ALLOW_LIST - value: "fake.registry.invalid" - -# Temporary until the Datadog CSI driver is released -datadog-csi-driver: - image: - repository: "gcr.io/datadoghq/csi-driver" - tag: "1.2.0" - -datadog: - csi: - enabled: true - apm: - instrumentation: - enabled: true - injector: - imageTag: "0.54.0" - enabledNamespaces: [] - targets: - - name: "apps" - namespaceSelector: - matchNames: - - "registry-allow-list" - ddTraceVersions: - python: "v3.18.1" From 4ff8d7b7fa107f7ac54bbeb3ad22d65c3f8640c6 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Fri, 27 Mar 2026 13:34:08 -0500 Subject: [PATCH 11/11] Also check library image registries against the allow list The previous check only validated the injector image registry. A user could bypass the allow list by annotating a pod with admission.datadoghq.com/python-lib.custom-image pointing to an arbitrary registry. Now InjectAPMLibraries checks every library's registry against the allow list in addition to the injector's registry. Adds a unit test for the library registry case and a third e2e scenario (LibraryRegistryBlockedByAllowList) that uses a python-lib.custom-image annotation pointing to fake.registry.invalid to verify the check fires. --- .../libraryinjection/mutator.go | 25 ++++++++---- .../libraryinjection/mutator_test.go | 40 +++++++++++++++++++ test/new-e2e/tests/ssi/ssi_test.go | 39 ++++++++++++++---- 3 files changed, 90 insertions(+), 14 deletions(-) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go index aad70684bac983..ab5bd6ea7daa97 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator.go @@ -29,13 +29,24 @@ import ( // // Returns an error if the injection fails. func InjectAPMLibraries(pod *corev1.Pod, cfg LibraryInjectionConfig) error { - // Check the registry allow list before any injection. All images use the same registry, - // so checking the injector registry is sufficient. - if len(cfg.RegistryAllowList) > 0 && !slices.Contains(cfg.RegistryAllowList, cfg.Injector.Package.Registry) { - msg := fmt.Sprintf("registry %q is not in the allow list", cfg.Injector.Package.Registry) - log.Warnf("Skipping APM library injection for pod %s: %s", mutatecommon.PodString(pod), msg) - annotation.Set(pod, annotation.InjectionError, msg) - return nil + // Check the registry allow list before any injection. Both the injector image and + // any library images (which may come from custom-image annotations pointing to + // arbitrary registries) must be in the allow list. + if len(cfg.RegistryAllowList) > 0 { + if !slices.Contains(cfg.RegistryAllowList, cfg.Injector.Package.Registry) { + msg := fmt.Sprintf("registry %q is not in the allow list", cfg.Injector.Package.Registry) + log.Warnf("Skipping APM library injection for pod %s: %s", mutatecommon.PodString(pod), msg) + annotation.Set(pod, annotation.InjectionError, msg) + return nil + } + for _, lib := range cfg.Libraries { + if lib.Package.Registry != "" && !slices.Contains(cfg.RegistryAllowList, lib.Package.Registry) { + msg := fmt.Sprintf("registry %q is not in the allow list", lib.Package.Registry) + log.Warnf("Skipping APM library injection for pod %s: %s", mutatecommon.PodString(pod), msg) + annotation.Set(pod, annotation.InjectionError, msg) + return nil + } + } } // Select the provider based on the injection mode (annotation or default) diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go index ea10ca4137e9ba..0480d3e9861c04 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/libraryinjection/mutator_test.go @@ -70,6 +70,7 @@ func TestInjectAPMLibraries_RegistryAllowList(t *testing.T) { Package: libraryinjection.NewLibraryImageFromFullRef(tt.injectorRegistry+"/apm-inject:0.52.0", "0.52.0"), }, }) + require.NoError(t, err) _, hasErrorAnnot := annotation.Get(pod, annotation.InjectionError) @@ -93,6 +94,45 @@ func TestInjectAPMLibraries_RegistryAllowList(t *testing.T) { } } +func TestInjectAPMLibraries_RegistryAllowListBlocksLibraryRegistry(t *testing.T) { + // A library specified via custom-image annotation may come from a different registry + // than the injector. The allow list must also cover library registries. + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "app"}}, + }, + } + + err := libraryinjection.InjectAPMLibraries(pod, libraryinjection.LibraryInjectionConfig{ + InjectionMode: "auto", + KubeServerVersion: &version.Info{GitVersion: "v1.30.9"}, + RegistryAllowList: []string{"registry.datadoghq.com"}, + Injector: libraryinjection.InjectorConfig{ + Package: libraryinjection.NewLibraryImageFromFullRef("registry.datadoghq.com/apm-inject:0.52.0", "0.52.0"), + }, + Libraries: []libraryinjection.LibraryConfig{ + { + Language: "python", + Package: libraryinjection.NewLibraryImageFromFullRef("evil.registry.invalid/dd-lib-python-init:v3.18.1", "v3.18.1"), + }, + }, + }) + require.NoError(t, err) + + val, ok := annotation.Get(pod, annotation.InjectionError) + require.True(t, ok, "expected injection-error annotation to be set") + require.Contains(t, val, "not in the allow list") + + // Injection should be blocked — LD_PRELOAD should not be set. + for _, env := range pod.Spec.Containers[0].Env { + require.NotEqual(t, "LD_PRELOAD", env.Name, "expected LD_PRELOAD to not be set") + } +} + func TestInjectAPMLibraries_StopsGracefullyWhenProviderUnavailable(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/new-e2e/tests/ssi/ssi_test.go b/test/new-e2e/tests/ssi/ssi_test.go index 7a2643225a9f2e..e6526148eed115 100644 --- a/test/new-e2e/tests/ssi/ssi_test.go +++ b/test/new-e2e/tests/ssi/ssi_test.go @@ -332,10 +332,11 @@ func (v *ssiSuite) TestWorkloadSelection() { } func (v *ssiSuite) TestRegistryAllowList() { - // Both apps run in the same cluster with allow list = registry.datadoghq.com. - // The "allowed" app uses the default injector (registry.datadoghq.com) — injection proceeds. - // The "blocked" app overrides the injector image to fake.registry.invalid via pod - // annotation, which is not in the allow list — injection is skipped. + // All three apps run in the same cluster with allow list = registry.datadoghq.com. + // - "allowed": default injector and library, both from registry.datadoghq.com — injection proceeds. + // - "injector-blocked": injector image overridden to fake.registry.invalid — injection blocked. + // - "library-blocked": injector is allowed, but python-lib.custom-image points to + // fake.registry.invalid — injection blocked by library registry check. v.UpdateEnv(Provisioner(ProvisionerOptions{ AgentOptions: []kubernetesagentparams.Option{ kubernetesagentparams.WithHelmValues(registryAllowListHelmValues), @@ -352,7 +353,7 @@ func (v *ssiSuite) TestRegistryAllowList() { Port: 8080, }, { - Name: "registry-allow-list-blocked", + Name: "registry-allow-list-injector-blocked", Image: "registry.datadoghq.com/injector-dev/python", Version: "16ad9d4b", Port: 8080, @@ -361,6 +362,16 @@ func (v *ssiSuite) TestRegistryAllowList() { "admission.datadoghq.com/apm-inject.custom-image": "fake.registry.invalid/apm-inject:0.54.0", }, }, + { + Name: "registry-allow-list-library-blocked", + Image: "registry.datadoghq.com/injector-dev/python", + Version: "16ad9d4b", + Port: 8080, + PodAnnotations: map[string]string{ + // Override python library to a registry not in the allow list. + "admission.datadoghq.com/python-lib.custom-image": "fake.registry.invalid/dd-lib-python-init:v3.18.1", + }, + }, }, }, }, dependsOnAgent) @@ -383,9 +394,9 @@ func (v *ssiSuite) TestRegistryAllowList() { }, 1*time.Minute, 10*time.Second, "did not find any traces at intake for DD_SERVICE %s", "registry-allow-list-allowed") }) - v.Run("InjectionBlockedByAllowList", func() { + v.Run("InjectorRegistryBlockedByAllowList", func() { k8s := v.Env().KubernetesCluster.Client() - pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-blocked") + pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-injector-blocked") // The injector image is overridden to fake.registry.invalid via pod annotation, // which is not in the allow list. Injection should be skipped entirely. @@ -396,4 +407,18 @@ func (v *ssiSuite) TestRegistryAllowList() { require.NotEmpty(v.T(), errAnnotation, "expected injection-error annotation to be set") require.Contains(v.T(), errAnnotation, "not in the allow list") }) + + v.Run("LibraryRegistryBlockedByAllowList", func() { + k8s := v.Env().KubernetesCluster.Client() + pod := FindPodInNamespace(v.T(), k8s, "registry-allow-list", "registry-allow-list-library-blocked") + + // The injector is from the allowed registry, but the python library is overridden + // to fake.registry.invalid via annotation. Injection should be skipped entirely. + podValidator := testutils.NewPodValidator(pod, testutils.InjectionModeAuto) + podValidator.RequireNoInjection(v.T()) + + errAnnotation := pod.Annotations["internal.apm.datadoghq.com/injection-error"] + require.NotEmpty(v.T(), errAnnotation, "expected injection-error annotation to be set") + require.Contains(v.T(), errAnnotation, "not in the allow list") + }) }