Skip to content

fix(rshell): run the privileged helper as one Agent-owned service gated on config - #58809

Open
astuyve wants to merge 26 commits into
mainfrom
ajstuyvenberg/rshell-privileged-root-marker
Open

astuyve wants to merge 26 commits into
mainfrom
ajstuyvenberg/rshell-privileged-root-marker

Conversation

@astuyve

@astuyve astuyve commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Reworks how the Linux installer ships the privileged rshell helper:

  • One systemd unit. datadog-agent-rshell-privileged.service is always written. datadog-agent.service pulls it in with Wants=, and it stops with the Agent through PartOf=. There's no .socket unit, no enablement check unit, and no [Install] section, so nothing gets enabled and no symlinks have to carry across upgrades. The installer no longer parses datadog.yaml.
  • The helper only runs when enabled. The unit runs privateactionrunner privileged-helper as root, which loads the Agent config without the secret backend. If private_action_runner.restricted_shell.privileged.enabled is false, or the kernel lacks Landlock ABI ≥ 3, it exits 0 without creating anything. Otherwise it execs rshell privileged-helper --socket=/run/datadog-agent-rshell/privileged.sock --idle-timeout=0. The helper creates the socket itself (dd-agent, 0600, in a root-owned RuntimeDirectory) once its policy and account are loaded, removes it when it exits, and keeps the service's PID, so systemd supervises it directly.
  • Depends on feat(privileged-helper): create and serve its own socket with --socket rshell#700, which adds --socket. go.mod pins rshell to that PR's commit for testing. It moves to the release once [tagger] cleanup pkg/tagger public api #700 lands, and this PR stays in draft until then.
  • privateactionrunner becomes root-owned, like rshell, since systemd runs it as root. A stable install or promote removes the legacy socket unit files and enablement symlinks that older versions left.
  • PAR uses the new default path and redirects a config that still names the old one. The socket now exists only while a helper serves it. If the setting is on but the socket is missing, EscalationAllowed tasks run in the ordinary interpreter with sudo rejected. A helper that accepts the connection and then fails is still not retried locally.

Motivation

The installer decided at install time and PAR decided at runtime, and the two could disagree:

  • Enabling the setting needed a reinstall.
  • Disabling it didn't stop the socket.
  • When the two disagreed, the shell broke.

Separately, the socket was bound to datadog-agent-action.service, so it stopped whenever the monolithic runner exited (split mode, or PAR disabled). It also kept the implicit Before=sockets.target while being ordered after the Agent, which created a boot ordering cycle.

This replaces closed #58241 and the systemd changes in #58769.

Tradeoffs:

  • The helper runs while the Agent runs, on hosts with the setting enabled, instead of being started per connection. It drops to dd-agent as its effective user, keeping root only as its real user ID.
  • Policy changes need a restart. The helper reads /etc/datadog-agent-rshell/policy.json when it starts, so a change takes effect at the next helper or Agent restart.
  • Disabled hosts run no helper and have no socket. Hosts that enabled the setting with an older version keep working after upgrading.

Note for the author of #57345: an enabled helper whose socket doesn't exist now falls back instead of erroring. TestEnabledPrivilegedHelperFailureDoesNotFallBack now tests a socket that accepts the connection and then fails. Please confirm that's acceptable.

Describe how you validated your changes

  • Unit tests:
    • Launcher: disabled config, unsupported kernel, socket creation and mode, fd handoff across exec.
    • Generated units.
    • Installer unit lists and legacy cleanup.
    • Socket path redirect.
    • Fallback cases.
  • Installer e2e in TestPackages/agent_*:
    • TestPrivilegedRshellHelperLifecycle: the helper exits while disabled; enabling and restarting starts it as rshell (real uid 0, effective uid dd-agent) with its socket; it survives datadog-agent-action stopping and stops with the Agent.
    • TestPrivilegedRshellSurvivesReboot (Ubuntu 24.04): no ordering cycle, and the helper is serving after a reboot.
  • PAR e2e: TestPrivilegedRshellEndToEnd runs signed elevated tasks through the long-running helper.
  • Manual QA on Ubuntu 24.04 (kernel 7.0, systemd 255), upgrading in place from 7.85.0-rc.8 with the setting enabled. Latest run: a5dcb66, pipeline 147972088, with rshell pinned to feat(privileged-helper): create and serve its own socket with --socket rshell#700 at b6318365, after both reviews and the runtime QA fixes.
    • On rc.8: the socket died when PAR exited, systemd-analyze verify reported the boot cycle, and after a reboot systemd deleted the socket's start job.
    • After upgrading: the legacy .socket unit and its symlinks were removed. The package root, embedded/bin, privateactionrunner, rshell and installer are root:root 755.
    • Helper: the service's main process is rshell privileged-helper --socket=… --idle-timeout=0 (real uid 0, effective uid dd-agent), with no shell in between, and it survives PAR exiting. The helper's directory checks accept /run/datadog-agent-rshell. The socket is dd-agent:dd-agent 600; dd-agent can connect and nobody is denied. The helper holds an exclusive flock on privileged.sock.lock (root-owned, 0600) for its lifetime. A second helper refuses to start ("another helper is serving"), and the original keeps serving. Stopping the helper makes systemd remove the socket, the lock file and the directory.
    • Disabled + Agent restart: the launcher logs "enabled is false", the unit exits successfully, and there's no socket and no runtime directory.
    • Re-enabled: the helper and socket come back. An Agent stop removes both, and an Agent start restores them.
    • Boot: no ordering cycle, and the helper serves after a reboot.

🤖 Generated with Claude Code

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI review by Codex (OpenAI) - workflow run

The patch is correct. I found no actionable regressions in the installer changes, systemd dependencies, socket migration, or fallback behavior. The new E2E tests appropriately exercise deployed permissions and service lifecycle boundaries. Tests were not run in the read-only environment.

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
• Patch Coverage: 54.44%
• Overall Coverage: 60.60% (+0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 096c3ef | Docs | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Static quality checks

✅ Please find below the results from static quality gates
Comparison made with ancestor d6711c7
📊 Static Quality Gates Dashboard
🔗 SQG Job

Successful checks

Info

Quality gate Change Size (prev → curr → max)
✅ agent_deb_amd64 +111.61 KiB (0.01% increase, -1.15% of buffer) 755.412 → 755.521 → 764.850
✅ agent_deb_amd64_fips -7.82 KiB (0.00% reduction, +0.07% of buffer) 686.573 → 686.566 → 696.770
✅ agent_heroku_amd64 -7.8 KiB (0.00% reduction, +0.10% of buffer) 312.807 → 312.800 → 320.390
✅ agent_msi +59.18 KiB (0.01% increase, -1.86% of buffer) 663.924 → 663.982 → 667.030
✅ agent_rpm_amd64 +111.61 KiB (0.01% increase, -1.16% of buffer) 755.396 → 755.505 → 764.830
✅ agent_rpm_amd64_fips -7.82 KiB (0.00% reduction, +0.07% of buffer) 686.557 → 686.549 → 696.750
✅ agent_rpm_arm64 -11.74 KiB (0.00% reduction, +0.12% of buffer) 725.126 → 725.115 → 735.060
✅ agent_suse_amd64 +111.61 KiB (0.01% increase, -1.16% of buffer) 755.396 → 755.505 → 764.830
✅ agent_suse_amd64_fips -7.82 KiB (0.00% reduction, +0.07% of buffer) 686.557 → 686.549 → 696.750
✅ agent_suse_arm64 -11.74 KiB (0.00% reduction, +0.12% of buffer) 725.126 → 725.115 → 735.060
✅ docker_agent_amd64 +119.61 KiB (0.01% increase, -1.22% of buffer) 811.474 → 811.591 → 821.040
✅ docker_agent_arm64 +52.26 KiB (0.01% increase, -0.50% of buffer) 805.808 → 805.859 → 815.930
✅ docker_agent_jmx_amd64 +119.61 KiB (0.01% increase, -1.22% of buffer) 1002.338 → 1002.455 → 1011.910
✅ docker_agent_jmx_arm64 +52.27 KiB (0.01% increase, -0.50% of buffer) 985.456 → 985.507 → 995.580
19 successful checks with minimal change (< 2 KiB)
Quality gate Current Size
✅ agent_rpm_arm64_fips 662.031 MiB
✅ agent_suse_arm64_fips 662.031 MiB
✅ docker_cws_instrumentation_amd64 7.451 MiB
✅ docker_cws_instrumentation_arm64 6.877 MiB
✅ docker_dogstatsd_amd64 40.143 MiB
✅ docker_dogstatsd_arm64 38.131 MiB
✅ docker_host_profiler_amd64 313.631 MiB
✅ docker_host_profiler_arm64 324.410 MiB
✅ dogstatsd_deb_amd64 30.894 MiB
✅ dogstatsd_deb_arm64 28.832 MiB
✅ dogstatsd_rpm_amd64 30.894 MiB
✅ dogstatsd_suse_amd64 30.894 MiB
✅ iot_agent_deb_amd64 47.902 MiB
✅ iot_agent_deb_arm64 44.318 MiB
✅ iot_agent_deb_armhf 45.131 MiB
✅ iot_agent_rpm_amd64 47.903 MiB
✅ iot_agent_suse_amd64 47.902 MiB
✅ docker_cluster_agent_amd64 216.411 MiB
✅ docker_cluster_agent_arm64 229.056 MiB

@astuyve astuyve changed the title fix(rshell): gate the privileged helper on a root-owned marker fix(rshell): always install the privileged helper units and gate them at runtime Oct 23, 2026
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Regression Detector

Regression Detector Results

Metrics dashboard
Target profiles
Job ID: 6d6c2032-f3c3-44d1-9fde-0c9fdf31c888

Baseline: d6711c7
Comparison: 096c3ef
Diff

Optimization Goals: ✅ No significant changes detected

Fine details of change detection per experiment

perf experiment goal Δ mean % Δ mean % CI trials links
➖ quality_gate_metrics_logs memory utilization +0.48 [+0.26, +0.70] 1 Logs bounds checks dashboard
➖ dsd_uds_10mb_3k_timestamped_contexts_cpu % cpu utilization +0.20 [-0.06, +0.46] 1 Logs
➖ python_openmetrics % cpu utilization +0.19 [-0.47, +0.84] 1 Logs bounds checks dashboard
➖ quality_gate_idle_all_features memory utilization +0.06 [-0.02, +0.13] 1 Logs bounds checks dashboard
➖ quality_gate_security_mean_fs_load memory utilization +0.03 [-0.01, +0.06] 1 Logs bounds checks dashboard
➖ quality_gate_security_no_fs_load memory utilization -0.04 [-0.10, +0.02] 1 Logs bounds checks dashboard
➖ quality_gate_idle memory utilization -0.22 [-0.26, -0.18] 1 Logs bounds checks dashboard
➖ quality_gate_private_action_runner memory utilization -0.22 [-0.35, -0.09] 1 Logs bounds checks dashboard
➖ quality_gate_security_idle memory utilization -0.25 [-0.28, -0.22] 1 Logs bounds checks dashboard
➖ dsd_uds_10mb_3k_timestamped_contexts_memory memory utilization -0.35 [-0.56, -0.14] 1 Logs
➖ dsd_uds_client_drop_detector_cpu % cpu utilization -1.22 [-1.75, -0.68] 1 Logs
➖ quality_gate_logs % cpu utilization -2.83 [-3.73, -1.92] 1 Logs bounds checks dashboard

Bounds Checks: ✅ Passed

perf experiment bounds_check_name replicates_passed observed_value links
✅ python_openmetrics checks_execution_time 10/10 79.59 ≤ 100 bounds checks dashboard
✅ python_openmetrics cpu_usage 10/10 1360.06 ≤ 1500 bounds checks dashboard
✅ python_openmetrics memory_usage 10/10 4.31GiB ≤ 4.75GiB bounds checks dashboard
✅ quality_gate_idle intake_connections 10/10 4 ≤ 5 bounds checks dashboard
✅ quality_gate_idle memory_usage 10/10 179.42MiB ≤ 181MiB bounds checks dashboard
✅ quality_gate_idle total_bytes_received 10/10 781.40KiB ≤ 819.20KiB bounds checks dashboard
✅ quality_gate_idle_all_features intake_connections 10/10 2 ≤ 5 bounds checks dashboard
✅ quality_gate_idle_all_features memory_usage 10/10 473.53MiB ≤ 542MiB bounds checks dashboard
✅ quality_gate_idle_all_features total_bytes_received 10/10 1.16MiB ≤ 1.25MiB bounds checks dashboard
✅ quality_gate_logs intake_connections 10/10 16 ≤ 40 bounds checks dashboard
✅ quality_gate_logs memory_usage 10/10 216.63MiB ≤ 228MiB bounds checks dashboard
✅ quality_gate_logs missed_bytes 10/10 0B = 0B bounds checks dashboard
✅ quality_gate_logs total_bytes_received 10/10 264.33MiB ≤ 292MiB bounds checks dashboard
✅ quality_gate_metrics_logs cpu_usage 10/10 371.90 ≤ 2000 bounds checks dashboard
✅ quality_gate_metrics_logs intake_connections 10/10 18 ≤ 40 bounds checks dashboard
✅ quality_gate_metrics_logs memory_usage 10/10 433.82MiB ≤ 455MiB bounds checks dashboard
✅ quality_gate_metrics_logs missed_bytes 10/10 0B = 0B bounds checks dashboard
✅ quality_gate_metrics_logs total_bytes_received 10/10 0.95GiB ≤ 1.04GiB bounds checks dashboard
✅ quality_gate_private_action_runner memory_usage 10/10 75.12MiB ≤ 77MiB bounds checks dashboard
✅ quality_gate_security_idle cpu_usage 10/10 29.65 ≤ 100 bounds checks dashboard
✅ quality_gate_security_idle memory_usage 10/10 330.12MiB ≤ 357MiB bounds checks dashboard
✅ quality_gate_security_mean_fs_load cpu_usage 10/10 42.70 ≤ 200 bounds checks dashboard
✅ quality_gate_security_mean_fs_load memory_usage 10/10 308.24MiB ≤ 337MiB bounds checks dashboard
✅ quality_gate_security_no_fs_load cpu_usage 10/10 23.09 ≤ 100 bounds checks dashboard
✅ quality_gate_security_no_fs_load memory_usage 10/10 334.29MiB ≤ 348MiB bounds checks dashboard

Explanation

Confidence level: 90.00%
Effect size tolerance: |Δ mean %| ≥ 5.00%

Performance changes are noted in the perf column of each table:

  • ✅ = significantly better comparison variant performance
  • ❌ = significantly worse comparison variant performance
  • ➖ = no significant change in performance

A regression test is an A/B test of target performance in a repeatable rig, where "performance" is measured as "comparison variant minus baseline variant" for an optimization goal (e.g., ingress throughput). Due to intrinsic variability in measuring that goal, we can only estimate its mean value for each experiment; we report uncertainty in that value as a 90.00% confidence interval denoted "Δ mean % CI".

For each experiment, we decide whether a change in performance is a "regression" -- a change worth investigating further -- if all of the following criteria are true:

  1. Its estimated |Δ mean %| ≥ 5.00%, indicating the change is big enough to merit a closer look.

  2. Its 90.00% confidence interval "Δ mean % CI" does not contain zero, indicating that if our statistical model is accurate, there is at least a 90.00% chance there is a difference in performance between baseline and comparison variants.

  3. Its configuration does not mark it "erratic".

CI Pass/Fail Decision

✅ Passed. All Quality Gates passed.

  • quality_gate_idle_all_features, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_idle_all_features, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_idle_all_features, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
  • quality_gate_security_mean_fs_load, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_security_mean_fs_load, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_idle, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_idle, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_idle, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
  • quality_gate_private_action_runner, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_security_idle, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_security_idle, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_logs, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_logs, bounds check missed_bytes: 10/10 replicas passed. Gate passed.
  • quality_gate_logs, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_logs, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
  • quality_gate_security_no_fs_load, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_security_no_fs_load, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check missed_bytes: 10/10 replicas passed. Gate passed.

@dd-octo-sts

dd-octo-sts Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Go Package Import Differences

Baseline: d6711c7
Comparison: 096c3ef

binaryosarchchange
privateactionrunnerlinuxamd64
+1, -0
+github.com/DataDog/datadog-agent/cmd/privateactionrunner/subcommands/privilegedhelper
privateactionrunnerlinuxarm64
+1, -0
+github.com/DataDog/datadog-agent/cmd/privateactionrunner/subcommands/privilegedhelper
privateactionrunnerwindowsamd64
+1, -0
+github.com/DataDog/datadog-agent/cmd/privateactionrunner/subcommands/privilegedhelper
privateactionrunnerdarwinamd64
+1, -0
+github.com/DataDog/datadog-agent/cmd/privateactionrunner/subcommands/privilegedhelper
privateactionrunnerdarwinarm64
+1, -0
+github.com/DataDog/datadog-agent/cmd/privateactionrunner/subcommands/privilegedhelper

@astuyve astuyve added the qa/done QA done before merge and regressions are covered by tests label Oct 23, 2026 — with ddtool CLI
@astuyve
astuyve marked this pull request as ready for review October 23, 2026 20:03
@astuyve
astuyve requested review from a team as code owners October 23, 2026 20:03
@astuyve
astuyve requested review from julesmcrt, maddiefranke and sar-shah and removed request for a team October 23, 2026 20:03
@astuyve astuyve added the follow-pr Followed by the `/follow-pr` skill label Oct 23, 2026 — with ddtool CLI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI review by Codex (OpenAI) - workflow run

The patch is correct. I found no actionable defects in the runtime gate, socket lifecycle changes, or fallback behavior. The new E2E tests appropriately exercise deployed packaging and systemd boundaries. Validation was limited to static review; tests were not run.

@matt-dz matt-dz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Review by Matthew's agent (Dogpark), on their behalf:

Review of the dd-agent-mcp files only: pkg/privateactionrunner/bundles/remoteaction/rshell/{BUILD.bazel,privileged_support_linux.go,privileged_support_other.go,run_command.go,run_command_test.go}. I didn't review the installer or systemd parts.

Verdict: looks good, a few nits. The runtime gate in Run only ever moves in the safe direction. When the helper is off, unsupported or has no socket, EscalationAllowed tasks run in the ordinary interpreter, and sudo is still rejected (the new test checks the 126). Once PAR has dialed, a failure is returned and the task is not retried locally. I don't see a way to escalate when the setting is disabled. The package tests pass locally (dda inv test --targets=./pkg/privateactionrunner/bundles/remoteaction/rshell).

Nits, most useful first:

  1. The fallback is easy to miss. The warning is logged once per handler for the life of the process, and the task output never says it ran without elevation. Here is how that bites. On an enabled host where the helper keeps crashing (for example a bad policy.json), the socket reaches its trigger limit, and RemoveOnStop=yes deletes the file. From then on every elevated task runs unprivileged and sudo exits 126. The operator gets nothing beyond a warning that may have been logged hours earlier. I'd suggest adding a SandboxWarnings entry on every fallback, so the backend and UI can show why elevation didn't happen.
  2. The Landlock ABI is duplicated. privilegedHelperMinLandlockABI = 3 copies requireKernelSupport in rshell's internal/sandbox/landlock. rshell doesn't export it, so if the helper's minimum changes (or it is ever built with landlocktsync, which needs ABI 8), PAR will dial helpers that accept the connection and then fail. Either export the constant from rshell, or add a comment that ties the two together.
  3. The no-fallback test is weaker. It now only matches "privileged rshell helper". Counting accepted connections in the test listener and asserting the count is at least 1 would show the task actually reached the helper and wasn't retried.

// warnPrivilegedFallback logs the first fallback only, since every elevated
// task would otherwise repeat it.
func (h *RunCommandHandler) warnPrivilegedFallback(format string, args ...interface{}) {
if h.privilegedFallbackWarned.CompareAndSwap(false, true) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Matthew's agent (Dogpark):

Nit: this logs once per handler for the life of the process. A host can go from "socket present" to "socket removed", for example when the helper fails repeatedly, the socket reaches its trigger limit and RemoveOnStop=yes deletes it. If an earlier fallback already used up the warning, that later change is never logged. Could each fallback also add an entry to the task's SandboxWarnings, such as "ran without elevation: "? Then the operator can see why sudo returned 126.

h.warnPrivilegedFallback("this host cannot run the rshell privileged helper (%v); running without elevation", err)
return false
}
if _, err := os.Stat(h.privilegedSocket); errors.Is(err, os.ErrNotExist) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Matthew's agent (Dogpark):

This is fine, and it fails closed. Any Stat error other than ErrNotExist, or a stale file nothing listens on, still dials and returns the error. It might be worth saying in the comment that a socket unit that failed at its trigger limit also lands here, because RemoveOnStop deletes the file, and the task then falls back silently instead of erroring.


// privilegedHelperMinLandlockABI is the Landlock ABI the privileged helper
// sandbox requires.
const privilegedHelperMinLandlockABI = 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Matthew's agent (Dogpark):

This copies the version < 3 check in rshell's internal/sandbox/landlock.requireKernelSupport, and nothing keeps the two in sync. If rshell raises its minimum, or the helper is built with landlocktsync (which needs ABI 8), PAR will dial a helper that accepts the connection and then fails, and the task is not retried. Could rshell export the constant from privilegedhelper, or at least have a comment here pointing at the rshell code?

out, err := action.handler.Run(context.Background(), task, nil)

require.ErrorContains(t, err, "privileged rshell helper: connect to privileged helper")
require.ErrorContains(t, err, "privileged rshell helper")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Matthew's agent (Dogpark):

Nit: this assertion is looser than before. Counting Accepts in the listener goroutine and asserting the count is at least 1 would prove the task reached the helper, which is the point of the test. It would also catch a future change that returns a different privileged rshell helper error before dialing.

@astuyve astuyve changed the title fix(rshell): always install the privileged helper units and gate them at runtime fix(rshell): run the privileged helper as one Agent-owned service gated on config Oct 26, 2026

astuyve commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Moving this back to draft. We're going to move socket creation into the rshell privileged helper itself, to clean up the implementation: privateactionrunner privileged-helper will still check the config and Landlock and exit when the helper is disabled, then exec the helper directly with --socket. That removes the /bin/sh trampoline and the descriptor handoff from this PR.

That needs an rshell change first. Until it's released, this branch pins rshell to the PR's commit for testing. We'll update this PR when the rshell PR lands.

🤖 Generated with Claude Code

astuyve and others added 5 commits October 27, 2026 11:55
Pin github.com/DataDog/rshell to the DataDog/rshell#700 branch commit
(v0.0.31-0.20261027154721-1f4107c16f20) for testing; it adds
`privileged-helper --socket=PATH`, which creates and serves the socket
itself before dropping privileges. Move to the release once it lands.

The privileged-helper launcher now only checks the configuration and
Landlock, then execs the helper with --socket. That removes the /bin/sh
trampoline, the descriptor handoff and the launcher's socket creation.
The socket now appears only once the helper has loaded its policy and
account, and the helper removes it when it exits.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the testing pin to DataDog/rshell#700's 6e02cbc8
(v0.0.31-0.20261027164856-6e02cbc8081f), which addresses review: the
helper no longer tries to unlink its socket after dropping privileges
(the unit's RuntimeDirectory= removes it), checks every ancestor
directory, refuses a socket that is still served, and forces
--idle-timeout=0 with --socket.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the testing pin to DataDog/rshell#700's d57cc35c
(v0.0.31-0.20261027170324-d57cc35c5040), which also rejects --socket
paths that are not clean. The Agent's path,
/run/datadog-agent-rshell/privileged.sock, is clean and has no symlinked
components.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
641df17 ("root-own the files root executes on Linux installs") makes
/opt/datadog-agent root:root so dd-agent cannot replace binaries root
runs, but the installer e2e assertions still expected dd-agent. They
failed in TestPackageAgentFapolicyd and would in every
TestPackages/agent_*/TestInstall. processes.d stays dd-agent.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the testing pin to DataDog/rshell#700's b6318365
(v0.0.31-0.20261027191057-b63183659b81): the helper now holds a lock on
privileged.sock.lock for its lifetime so only one serves the socket,
removes a socket whose chown failed, and requires dd-agent to be able to
reach it. The lock file lives in the unit's RuntimeDirectory, which
systemd removes on stop.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Contributor

I completed package-level QA on Ubuntu 24.04 x86_64 and arm64, including direct DEB downgrade/upgrade and Fleet experiment rollback/promotion. I found two lifecycle issues:

  1. The privileged helper survives an unrecoverable Agent failure.

The unit uses PartOf=datadog-agent.service. After repeatedly killing the Agent until datadog-agent.service exhausted its restart limit and entered failed, datadog-agent-rshell-privileged.service remained active and /run/datadog-agent-rshell/privileged.sock remained reachable. I
reproduced this with installed pipeline packages on both x86_64 and arm64.

Using BindsTo=datadog-agent.service instead stopped the helper and removed its RuntimeDirectory when the Agent failed. systemd-analyze verify reported no ordering cycle.

  1. DEB-to-OCI promotion leaves the legacy socket unit file behind.

I started from official Agent 7.84.6 with the legacy socket active, installed this PR’s package as an experiment, tested rollback, then reinstalled and promoted it. Promotion correctly removed the old socket and enablement links and started the new helper, but
/usr/lib/systemd/system/datadog-agent-rshell-privileged.socket remained loaded and disabled.

removeLegacyPrivilegedRshellSocket removes unit files only from unitsPath(ctx). During OCI promotion that is /etc/systemd/system, so it does not remove a unit installed by the previous DEB under /lib/systemd/system (which resolves to /usr/lib/systemd/system on Ubuntu). Could it
remove the legacy stable and experiment socket units from every entry in unitInstallPaths, similar to retireLegacyProcmgrUnits? A DEB-to-OCI test would catch this; the current test covers enablement links only.

Everything else I exercised passed: x86_64 and arm64 package installation, config gating, environment overrides, permissions, helper UIDs, concurrent startup, stale/live socket handling, reboot/systemd behavior, direct DEB downgrade/upgrade, experiment rollback, and experiment
promotion.

astuyve and others added 2 commits October 28, 2026 09:05
DataDog/rshell#700 merged as 6d758c7e; pin to it
(v0.0.31-0.20261028124019-6d758c7edecf). The helper code is identical to
the reviewed tip. Re-pin to the v0.0.31 tag once it is cut.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DataDog/rshell#700 shipped in v0.0.31 (tag on 6d758c7e, the commit this
branch was pinned to by pseudo-version). Switch to the release tag. The
privileged helper code is unchanged; verified end to end against the
staging Action Platform on this build.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@astuyve
astuyve marked this pull request as ready for review October 28, 2026 13:45

astuyve commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

End-to-end verification on the staging Action Platform (org 2 / datad0g.com), on a VM running this branch's build with rshell v0.0.31 (the merged #700):

  • Enrollment: PAR self-enrolled (API-key-only) as runner-VjE4NKLfUpBd8eASBWsUSR; remote-config manager ready.
  • Helper: datadog-agent-rshell-privileged.service runs rshell privileged-helper --socket=/run/datadog-agent-rshell/privileged.sock --idle-timeout=0 directly (real uid 0, effective uid dd-agent); socket dd-agent:dd-agent 0600.
  • Non-privileged dispatch: runCommand: echo READY → SUCCEEDED, stdout READY, exit 0.
  • Privilege boundary: with a scoped execution policy (read-only /var/log/datadog, elevation rshell:cat) and a root-only fixture /var/log/datadog/priv-proof.txt:
    • non-elevated cat → exit 1, permission denied (dd-agent cannot read it);
    • sudo cat → exit 0, returns the file contents, via the privileged helper.
  • The helper's authorization log shows orgId:2, effectivePermissions:EscalationAllowed, a signed trusted key, and execution_completed exitCode:0; PAR logged runPrivileged … elevatableCommands=[rshell:cat].

The VM, the execution policy, and the keys were torn down after the run. Now pinned to the v0.0.31 release tag; out of draft.

🤖 Generated with Claude Code

@julesmcrt

Copy link
Copy Markdown
Contributor

[P1] Do not fall back to the ordinary interpreter when the enabled helper socket is absent

privilegedHelperAvailable() treats a missing socket as a reason to run an EscalationAllowed task unprivileged. That changes the policy boundary: runPrivileged() forwards the local privileged-helper policy, but the ordinary interpreter path does not.

I reproduced this with a valid signed runRemediationCommand:

  1. The backend task allowed a fixture directory as :rw.

  2. The local privileged-helper policy allowed that same directory only as :ro.

  3. With the helper socket present, echo MUTATED > <fixture>/target was rejected and the file stayed unchanged.

  4. After removing only the socket pathname—without changing the local policy—the same signed task logged:

    rshell privileged helper is enabled but its socket ... does not exist; running without elevation

    It then ran through the ordinary interpreter and successfully changed the file.

This can happen during helper startup/restart/cleanup races or after an unexpected helper failure. It does not require a forged task; it only requires a valid task whose backend policy is broader than the host’s local helper policy.

Please fail the task explicitly when privileged execution is enabled and an EscalationAllowed task cannot reach the helper, rather than locally retrying it without the helper. A regression test should cover a backend :rw grant plus stricter local :ro policy and assert that
a missing socket cannot permit the write.

Reproduction

Prerequisites:

  • Privileged rshell enabled.
  • A valid signed runRemediationCommand with effectivePermissions=EscalationAllowed.
  • Backend allowed_paths: <fixture>:rw.
  • Local helper policy: <fixture>:ro.
  • <fixture>/target writable by dd-agent.
  1. Start the helper with that local policy and submit:

    echo MUTATED > /target

    Expected: denied; target remains unchanged.

  2. Remove only the helper socket path. Leave the policy unchanged.

  3. Submit the exact same signed task.

    Actual: RunCommandHandler falls back to the ordinary interpreter and the write succeeds.
    Expected: helper-unavailable error; target remains unchanged.

…r after the Agent

Addresses review findings on #58809:

- [P1] When privileged mode is enabled, an EscalationAllowed task whose
  helper socket is unreachable now fails instead of falling back to the
  ordinary interpreter. The local helper policy gates elevation and the
  ordinary path does not apply it, so falling back let a backend grant
  broader than the local policy through (e.g. a :rw task under a :ro
  local policy) during a socket race or helper failure. Removed
  privilegedHelperAvailable; enabled => always route to the helper.

- The helper is now BindsTo= (not PartOf=) its Agent, so it stops and
  its RuntimeDirectory is removed when the Agent fails (restart limit
  exhausted), not only on a clean stop.

- Fix the ordering direction: the Agent no longer orders itself After
  the helper (an inverted leftover from socket activation); the helper
  orders After the Agent it is bound to. Pairs correctly with BindsTo.

- removeLegacyPrivilegedRshellSocket now removes the legacy socket unit
  from every unit install path, so a DEB-to-OCI promotion no longer
  leaves the old unit under /usr/lib/systemd/system.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

astuyve commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @julesmcrt — thorough QA, all three fixed in 5cef453c, plus the ordering inversion that fix 1 exposed.

[P1] Missing socket now fails closed. privilegedHelperAvailable() is gone. When privileged.enabled is true, an EscalationAllowed task always routes to the helper and fails if it is unreachable — it no longer falls back to the ordinary interpreter, which doesn't apply the local helper policy. The privileged.enabled=false path is unchanged (no local policy to enforce, so the ordinary interpreter runs it and rejects sudo, per #57345). New unit test asserts a command that would succeed unprivileged instead errors when enabled + socket absent, for both read-only and remediation handlers. Your :rw backend / :ro local repro is the motivating case; the host's local policy can no longer be bypassed by removing the socket.

Helper now BindsTo= the Agent. Swapped PartOf= → BindsTo=datadog-agent[-exp].service, so the helper stops and its RuntimeDirectory is removed when the Agent fails (restart-limit-exhausted), not only on a clean stop — matching what you saw. systemd-analyze verify is clean.

Ordering fixed (caught in parallel). The Agent was ordered After= the helper — an inverted leftover from the socket-activation design. The Agent now only Wants= it; the helper orders After= the Agent it's bound to, which is the correct direction and pairs with BindsTo for clean teardown. No cycle.

DEB-to-OCI promotion. removeLegacyPrivilegedRshellSocket now removes the legacy socket unit from every entry in unitInstallPaths (like retireLegacyProcmgrUnits), not just the current package type's dir, so the stale unit under /usr/lib/systemd/system after a DEB→OCI promotion is cleaned. Added a unit test over all install paths; a DEB→OCI e2e remains a good follow-up.

🤖 Generated with Claude Code

astuyve and others added 2 commits October 28, 2026 11:27
The rshell v0.0.31 bump pulls in a new subpackage,
github.com/DataDog/rshell/builtins/internal/boundedreplace, which
lint_licenses flagged as missing. Regenerated with
`dda inv generate-licenses`.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cy socket cleanup

Addresses review feedback on the privileged rshell helper units:

- Unit wiring parity: the stable and experiment helper units now mirror
  datadog-agent-action's wiring. Each BindsTo its matching core Agent,
  orders After both core Agents, and Conflicts with the opposite core
  Agent and helper, so stable and experiment never run the helper at the
  same time against the shared socket.
- Consolidate permissions: the privateactionrunner launcher and the rshell
  helper binary are now root:root entries in agentPackagePermissions,
  replacing the separate permissions table and the bespoke ensure step.
  systemd runs both as root, so dd-agent must not be able to replace them.
- Finish legacy socket retirement: removeLegacyPrivilegedRshellSocket now
  stops and disables the old socket units (guarded by systemd.IsRunning)
  before deleting their files, mirroring retireLegacyProcmgrUnits, so a
  stale still-loaded socket on an upgraded host is cleared rather than left
  active in systemd.
- e2e: cover the failure-containment transition. A helper killed until it
  exhausts its restart limit enters failed, drops its socket, and leaves
  the core Agent running (BindsTo binds the helper to the Agent, not the
  reverse); restarting the Agent clears it and brings it back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

astuyve commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressed all four in fc2c77d:

  1. Unit wiring parity. Both helper units now mirror datadog-agent-action: each BindsTo its matching core Agent, orders After both core Agents, and Conflicts with the opposite core Agent and helper, so stable and experiment never run the helper at once against the shared socket. (Dropped the literal datadog-agent-action.service from a comment so the "no PAR-frontend dependency" test guard still holds.)
  2. Consolidate permissions. privateactionrunner and rshell are now root:root entries in agentPackagePermissions; removed the separate permissions table, the bespoke ensure step, and the rel-path const. TestAgentPackagePermissionsRootOwnRootRunPaths now asserts both binaries.
  3. Finish legacy socket retirement. removeLegacyPrivilegedRshellSocket now stops+disables the old socket units (guarded by systemd.IsRunning) before deleting their files, mirroring retireLegacyProcmgrUnits, so a stale still-loaded socket on an upgraded host is cleared rather than left active.
  4. Lifecycle on a deployed host. Validated manually on a staging VM (enable→start, socket dd-agent:dd-agent 0600, BindsTo teardown on Agent stop, no boot ordering cycle). Also encoded the failure-containment transition in TestPrivilegedRshellHelperLifecycle: a helper killed until it exhausts its restart limit enters failed, drops its socket, and leaves the core Agent running; restarting the Agent recovers it.

gofmt/linter clean on both modules, installer + template unit tests green.

…g it in e2e

The failure-containment case in TestPrivilegedRshellHelperLifecycle drives the
helper to `failed` by exhausting its systemd start limit, then expected a plain
`systemctl restart datadog-agent` to bring it back. It does not: a unit that hit
its start limit stays failed, and the Agent's Wants= does not reset that counter.
On Ubuntu 24.04 (the only matrix distro with Landlock support, so the only one
that runs this block) requireRshellHelperSupported then timed out with "the
enabled helper neither started nor declined the kernel".

Clear the helper's start-limit counter with `systemctl reset-failed` before
restarting the Agent, and fix the comment that wrongly claimed Wants= resets it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@julesmcrt

julesmcrt commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Follow-ups:

  1. The release note still promises fallback when the helper is missing. Please clarify: with privileged mode enabled, EscalationAllowed tasks fail if the helper is unavailable—even without sudo—to preserve the helper’s local policy. Ordinary execution still works when privileged mode is disabled.

  2. Permission consolidation removed explicit 0755 enforcement and the rshell regular-file check. Is relying on the package’s modes/file types intentional? This is a clarification, not a confirmed regression.

Have the previously reproduced Agent-failure and DEB→OCI cleanup cases been rerun against the final build? If so, please link the results.
Otherwise, those are the targeted checks left—not the whole QA matrix. The new helper-crash test covers the reverse direction: helper failure must leave the Agent running.

@julesmcrt

Copy link
Copy Markdown
Contributor

Potential security regression in the permission consolidation: the removed ensurePrivilegedRshellPermissions rejected a symlinked rshell, but Permission.Ensure calls filepath.EvalSymlinks before Lchown, so it follows and accepts the symlink instead.

If its target is in a dd-agent-writable directory, making the target root-owned does not prevent dd-agent from replacing it afterward. The root launcher then executes whatever that helper path resolves to.

Please preserve the regular-file validation for rshell and restore the deleted symlink-rejection test. The root-executed privateactionrunner launcher should receive equivalent validation. This can coexist with the consolidated permission list; no need to restore the separate permission table.

I confirmed the changed acceptance behavior with a focused Linux test against Permission.Ensure. I have not reproduced planting the symlink during a real installation, so this is a confirmed validation regression, not a demonstrated P1 privilege escalation.

astuyve and others added 2 commits October 29, 2026 11:51
The fixes note said restricted shell tasks "run without elevation instead of
failing" when the helper is unavailable, which reads as a blanket fallback. With
privileged mode enabled, EscalationAllowed tasks fail closed if the helper is
unreachable (to preserve its host-local policy); only ordinary, non-elevated
commands keep running. Spell both out, plus the privileged-mode-disabled case.

Reported by Jules in review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ar file

The permission consolidation removed ensurePrivilegedRshellPermissions, whose
Lstat+IsRegular check rejected a symlinked rshell. agentPackagePermissions.Ensure
instead resolves symlinks with filepath.EvalSymlinks before Lchown, so it would
follow a symlinked binary and chown its target to root rather than reject it. If
that target sat in a dd-agent-writable directory, dd-agent could replace it
afterward and the root launcher would exec whatever the path resolved to.

Restore the validation in two places, keeping the consolidated ownership list:

- The privateactionrunner launcher, which runs as root, now refuses to exec a
  helper that is not a regular file, is not root-owned, or is writable by group
  or other, before execve. This is the runtime gate and catches a tampered
  binary regardless of how it got there.
- The installer rejects a symlinked (or non-regular) rshell and
  privateactionrunner before applying ownership, restoring the removed
  install-time check for both root-executed binaries.

Tests: restored the installer symlink-rejection test over both binaries, added
launcher unit tests (symlink, non-root owner, group/other-writable), and an e2e
assertion that the service refuses to start a symlinked helper binary.

Also clarify the release note: with privileged mode enabled, EscalationAllowed
tasks fail closed when the helper is unreachable (only ordinary commands keep
running), rather than the blanket "run without elevation instead of failing".

Both reported by Jules in review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Contributor

1. The root launcher inherits a dd-agent-writable environment

The new helper unit imports /etc/datadog-agent/environment before starting privateactionrunner as root. The installer makes /etc/datadog-agent writable by dd-agent, so that account can create or replace the environment file.

I checked the Omnibus package from this PR’s earlier commit a5dcb66, pipeline 147972088: privateactionrunner is dynamically linked, and the relevant build recipes are unchanged at the current head. On that packaging path, an LD_PRELOAD entry can load code as root when the service
next starts, before the configuration and Landlock checks. The clean environment passed to the later rshell exec does not protect the launcher itself.

Could we remove the shared EnvironmentFile= input from this root unit, or preserve the required overrides through a root-controlled or narrowly filtered mechanism? Changing only the environment file’s ownership would not suffice while its parent directory remains writable by
dd-agent.

2. The shared Lchown change can break symlinked configuration updates

This is distinct from the removed regular-file check for executables. The Fleet config writer in pkg/fleet/installer/config/config_nix.go calls file.Chown directly, without going through Permission.Ensure. It still chmods a symlink’s target, but changing file.Chown to use Lchown
means it now chowns only the link.

For example, given a relative link datadog.yaml -> real-datadog.yaml whose target is root:root 0644, a Fleet config patch changes the target’s mode to 0640 while leaving its ownership as root:root. Without an additional ACL, the experiment Agent running as dd-agent can no longer
read it. Previously, the target became dd-agent:dd-agent.

Could we preserve target-ownership updates in the config writer, preferably through the confined opened file, while keeping installer permission walks non-following? A regression test should check the target’s ownership and readability, not just its mode.

Validation was source/caller review plus inspection of the earlier PR package. I have not run a live exploit or reproduced the config-update failure end to end.

…helper

The privileged helper unit runs privateactionrunner as root and sourced
EnvironmentFile=-/etc/datadog-agent/environment. That directory is
dd-agent:dd-agent, so dd-agent can replace the environment file (it owns the
directory even though the file itself is root-owned) and inject variables such
as LD_PRELOAD into the root launcher. The dynamic loader acts before main(), so
this runs attacker code as root before the config, Landlock, and helper-binary
checks run - a dd-agent to root escalation, reproduced on a staging VM.

Remove EnvironmentFile= from the helper unit. The launcher needs nothing from
it; the only variable it receives, DD_FLEET_POLICIES_DIR, stays a
root-controlled literal baked into the unit. Invert the template test to assert
the unit has no EnvironmentFile directive.

Reported by Jules in review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
}
}
if p.Mode != 0 {
// Skip symlinks: there is no portable lchmod and os.Chmod follows them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's the goal here ?

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.

based on this feedback from jules #58809 (comment)

we're trying to ensure the binary can't be swapped by a rogue dd-agent process and then started


// Chown changes the ownership of a file to the specified owner and group.
// Chown changes the ownership of a file to the specified owner and group. It
// does not follow symlinks.

@arbll arbll Oct 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why ? Not sure how safe this is, there could be symlinks somewhere we use this function no ?

// enablement symlinks left by older versions. It only runs when installing or
// promoting the stable package: during an experiment, the previous stable
// version may still rely on them to start its helper after a rollback.
func removeLegacyPrivilegedRshellSocket(ctx HookContext) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need that migration if it was never shipped ?

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.

yeah it did ship in 7.84 although it likely has very very very few installs

julesmcrt and others added 2 commits October 29, 2026 18:04
…ged helper

dd-agent is a trusted, elevated user that can chown the install directory and
binaries at any time, so hardening the installer and launcher against a hostile
dd-agent adds complexity without a real security boundary. Back it out and keep
only creating the items:

- Launcher: remove the pre-exec regular-file / root-owner / not-writable check;
  it execs the configured helper directly again.
- Installer: remove ensureRootExecutablesAreRegularFiles, and drop the
  root-ownership pinning of embedded/bin/rshell and embedded/bin/privateactionrunner.
- Unit: restore EnvironmentFile=-<etc>/environment on the helper unit.
- Tests: drop the launcher trust tests, the installer symlink-rejection tests,
  and the e2e symlinked-helper assertion.

Kept intact: the legacy socket cleanup and the general root-trust-boundary
(VULN-4673), which are unrelated to this helper's trust model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

follow-pr Followed by the `/follow-pr` skill internal Identify a non-fork PR long review PR is complex, plan time to review it qa/done QA done before merge and regressions are covered by tests team/action-platform team/agent-devx team/fleet-automation team/fleet-remediation team/windows-products

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants