Skip to content
6 changes: 3 additions & 3 deletions internal/controller/datadogagent/component/agent/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,12 @@ func hostProfilerContainer(dda metav1.Object) corev1.Container {
"host-profiler",
"--core-config=" + agentCustomConfigVolumePath,
},
Env: commonEnvVars(dda),
Env: commonEnvVars(dda),
// host-profiler needs the same base mounts as otel-agent (logs, config, auth, tmp);
// the hostprofiler feature adds tracingfs on top via ManageNodeAgent.
VolumeMounts: volumeMountsForOtelAgent(),
Ports: []corev1.ContainerPort{},
SecurityContext: &corev1.SecurityContext{
ReadOnlyRootFilesystem: ptr.To(true),
Privileged: ptr.To(true),
},
}
}
Expand Down
58 changes: 35 additions & 23 deletions internal/controller/datadogagent/component/agent/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import (
"github.com/DataDog/datadog-operator/pkg/constants"
)

func findVolume(volumes []corev1.Volume, name string) *corev1.Volume {
for i := range volumes {
if volumes[i].Name == name {
return &volumes[i]
}
}
return nil
}

func TestVolumesForAgent(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -67,29 +76,13 @@ func TestVolumesForAgent(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
volumes := volumesForAgent(tt.dda, tt.requiredContainers)

// Check install-info volume
var installInfoVolume *corev1.Volume
for i := range volumes {
if volumes[i].Name == common.InstallInfoVolumeName {
installInfoVolume = &volumes[i]
break
}
}
assert.NotNil(t, installInfoVolume, "install-info volume should exist")
assert.Equal(t, tt.expectedInstallName, installInfoVolume.ConfigMap.Name)

// Check seccomp volume if system probe is required
if len(tt.requiredContainers) > 0 {
var seccompVolume *corev1.Volume
for i := range volumes {
if volumes[i].Name == common.SeccompSecurityVolumeName {
seccompVolume = &volumes[i]
break
}
}
assert.NotNil(t, seccompVolume, "seccomp security volume should exist")
assert.Equal(t, tt.expectedSeccompName, seccompVolume.ConfigMap.Name)
}
installVol := findVolume(volumes, common.InstallInfoVolumeName)
assert.NotNil(t, installVol, "install-info volume should exist")
assert.Equal(t, tt.expectedInstallName, installVol.ConfigMap.Name)

seccompVol := findVolume(volumes, common.SeccompSecurityVolumeName)
assert.NotNil(t, seccompVol, "seccomp security volume should exist")
assert.Equal(t, tt.expectedSeccompName, seccompVol.ConfigMap.Name)
})
}
}
Expand Down Expand Up @@ -197,6 +190,25 @@ func TestDefaultSyscallsForSystemProbe(t *testing.T) {
}
}

func TestHostProfilerContainer(t *testing.T) {
dda := &metav1.ObjectMeta{Name: "foo", Namespace: "default", Labels: map[string]string{}}

containers := agentOptimizedContainers(dda, []apicommon.AgentContainerName{
apicommon.CoreAgentContainerName,
apicommon.HostProfiler,
})
assert.Len(t, containers, 2)

c := containers[1]
assert.Equal(t, string(apicommon.HostProfiler), c.Name)
assert.NotNil(t, c.SecurityContext)
// The component layer only sets ReadOnlyRootFilesystem; the feature's ManageNodeAgent sets
// AllowPrivilegeEscalation, SeccompProfile, and Capabilities.
assert.Nil(t, c.SecurityContext.Privileged, "host-profiler should not run as privileged")
assert.NotNil(t, c.SecurityContext.ReadOnlyRootFilesystem)
assert.True(t, *c.SecurityContext.ReadOnlyRootFilesystem)
}

func TestPrivateActionRunnerContainer(t *testing.T) {
dda := &metav1.ObjectMeta{
Name: "test-dda",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,15 @@ func egressAgentDatadogIntake(podSelector metav1.LabelSelector, site string, ddU
{
MatchName: fmt.Sprintf("orchestrator.%s", site),
},
{
MatchName: fmt.Sprintf("intake.profile.%s", site),
},
{
MatchName: fmt.Sprintf("sourcemap-intake.%s", site),
},
{
MatchName: fmt.Sprintf("otlp.%s", site),
},
}...),
ToPorts: []cilium.PortRule{
{
Expand Down
98 changes: 82 additions & 16 deletions internal/controller/datadogagent/feature/hostprofiler/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package hostprofiler

import (
"errors"
"fmt"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
Expand All @@ -13,12 +14,13 @@ import (
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/common"
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature"
featureutils "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/utils"
"github.com/DataDog/datadog-operator/pkg/constants"
)

var errHostPIDDisabledManually = errors.New("Host PID is required for host profiler")

type hostProfilerFeature struct {
hostProfilerEnabled bool
owner metav1.Object
hostPIDDisabledManually bool

logger logr.Logger
Expand Down Expand Up @@ -46,28 +48,36 @@ func (o *hostProfilerFeature) ID() feature.IDType {
}

func (o *hostProfilerFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, _ *v2alpha1.RemoteConfigConfiguration) feature.RequiredComponents {
o.hostProfilerEnabled = featureutils.HasFeatureEnableAnnotation(dda, featureutils.EnableHostProfilerAnnotation)

var reqComp feature.RequiredComponents
if o.hostProfilerEnabled {
reqComp = feature.RequiredComponents{
Agent: feature.RequiredComponent{
IsRequired: ptr.To(true),
Containers: []apicommon.AgentContainerName{
apicommon.CoreAgentContainerName,
apicommon.HostProfiler,
},
o.owner = dda

if !featureutils.HasFeatureEnableAnnotation(dda, featureutils.EnableHostProfilerAnnotation) {
return feature.RequiredComponents{}
}

return feature.RequiredComponents{
Agent: feature.RequiredComponent{
IsRequired: ptr.To(true),
Containers: []apicommon.AgentContainerName{
apicommon.CoreAgentContainerName,
apicommon.HostProfiler,
},
}
},
}
return reqComp
}

func (o *hostProfilerFeature) seccompConfigMapName() string {
return fmt.Sprintf("%s-%s", constants.GetDDAName(o.owner), agentSecurityConfigMapSuffixName)
}

func (o *hostProfilerFeature) ManageDependencies(managers feature.ResourceManagers, provider string) error {
if o.hostPIDDisabledManually {
return errHostPIDDisabledManually
}
return nil
return managers.ConfigMapManager().AddConfigMap(
o.seccompConfigMapName(),
o.owner.GetNamespace(),
defaultSeccompConfigData(),
)
}

func (o *hostProfilerFeature) ManageClusterAgent(managers feature.PodTemplateManagers, provider string) error {
Expand All @@ -80,7 +90,63 @@ func (o *hostProfilerFeature) ManageNodeAgent(managers feature.PodTemplateManage
}

// Host PID
managers.PodTemplateSpec().Spec.HostPID = *ptr.To(true)
managers.PodTemplateSpec().Spec.HostPID = true

// Security context: drop all caps, add only what host-profiler needs, lock down privilege escalation,
// and apply a localhost seccomp profile. AllowPrivilegeEscalation must be explicitly false so that
// runc applies the seccomp filter before its own setuid/setgid/capset calls during container setup.
var hostProfilerContainer *corev1.Container
for i := range managers.PodTemplateSpec().Spec.Containers {
if managers.PodTemplateSpec().Spec.Containers[i].Name == string(apicommon.HostProfiler) {
hostProfilerContainer = &managers.PodTemplateSpec().Spec.Containers[i]
break
}
}

if hostProfilerContainer == nil {
return fmt.Errorf("host-profiler container not found in pod template spec")
}

if hostProfilerContainer.SecurityContext == nil {
hostProfilerContainer.SecurityContext = &corev1.SecurityContext{}
}

sc := hostProfilerContainer.SecurityContext
sc.AllowPrivilegeEscalation = ptr.To(false)
sc.SeccompProfile = &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeLocalhost,
LocalhostProfile: ptr.To(seccompProfileName),
}
sc.Capabilities = &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
Add: defaultCapabilities(),
}

// AppArmor: unconfined so the default containerd profile doesn't block ptrace cross-profile,
// which host-profiler requires to read /proc/<pid>/map_files for process profiling.
managers.Annotation().AddAnnotation(common.AppArmorAnnotationKey+"/"+string(apicommon.HostProfiler), "unconfined")
Comment thread
theomagellan marked this conversation as resolved.
Comment thread
theomagellan marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppArmor: Do we want to expose a setting to override the "unconfined" ? I'm not familiar with how someone would override this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can change it by changing the datadog agent's manifest and override the container.apparmor.security.beta.kubernetes.io/host-profiler annotation

As discussed on Thursday though, the profile provisioning is left to the user; neither the helm chart or the operator allow us to provision one automatically, that's why it's unconfined by default


// host-profiler-security ConfigMap volume (contains the seccomp profile JSON)
secVol := corev1.Volume{
Name: securityVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: o.seccompConfigMapName(),
},
},
},
}
managers.Volume().AddVolume(&secVol)

// seccomp-root EmptyDir volume (shared with system-probe when both are enabled; VolumeManager deduplicates)
seccompRootVol := common.GetVolumeForSeccomp()
managers.Volume().AddVolume(&seccompRootVol)

// Init container: copy seccomp profile JSON to the kubelet seccomp directory on the host.
// Appended after the base init containers (init-volume, init-config) added by default.go.
initContainer := buildSeccompSetupInitContainer(hostProfilerContainer.Image)
managers.PodTemplateSpec().Spec.InitContainers = append(managers.PodTemplateSpec().Spec.InitContainers, initContainer)

// Tracingfs volume
volumeTracingfs := corev1.Volume{
Expand Down
Loading
Loading