diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 473ce1f48..af63724e5 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -1983,6 +1983,18 @@ the intentionally sparse public response. `NoNewPrivileges=false` and `RestrictSUIDSGID=false`. That exception is limited to the PVE command-agent path; it must not become the default for Docker / Podman, host-only, PBS-only, or ordinary non-command agents. + The narrower `AmbientCapabilities=CAP_SETUID CAP_SETGID` grant is not bound + to the install-time flag and must be written for every `--enable-proxmox` + agent of type `pve` or `all`, with or without `--enable-commands`. Command + execution is also enabled from the server at runtime, where + `applyRemoteConfig` starts the command client without regenerating the unit, + so an agent provisioned only for the install-time case would accept commands + it cannot use: `lxc-attach` needs `CAP_SETUID` to write `/proc//uid_map` + for unprivileged guests, and without it the Docker socket probe fails with + `write_id_mapping: 61 Operation not permitted` for every unprivileged LXC. + Because that probe failure is debug-level and retried each poll, the + resulting surface is silently incomplete rather than visibly broken, so the + capability must be provisioned up front rather than diagnosed later. Persistence-sensitive NAS targets must keep one canonical continuity model here: installer-owned bootstraps may use flash-backed or immutable-root launch hooks only as thin trampolines, while the durable wrapper, state, and reboot-surviving binary copy stay in the governed persistent state directory that updater continuity also refreshes. Unix `--update` re-entry must also preserve lifecycle identity for legacy v5.1.x agents that do not yet have v6 `connection.env` state. When a diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b8f910684..2fa7661ba 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -467,6 +467,20 @@ upgrade, update, release, or artifact-selection behavior. `RestrictSUIDSGID=false`. That exception is deployment-owned operator truth for the Proxmox LXC Docker inventory path and must not leak into non-PVE or non-command agent installs. + Independently of the install-time command flag, the unit for **any** PVE + agent must grant `AmbientCapabilities=CAP_SETUID CAP_SETGID`. `lxc-attach` + into an unprivileged guest writes `/proc//uid_map`, which requires + `CAP_SETUID` in the parent user namespace; `NoNewPrivileges` removes it from + the effective set and simultaneously blocks the setuid + `newuidmap`/`newgidmap` fallback, so the socket probe fails with + `write_id_mapping: 61 Operation not permitted`. Gating this grant on the + install-time flag is not sufficient, because command execution is also + enabled from the server at runtime (`applyRemoteConfig`) without rewriting + the unit; an agent switched on that way could run commands but never attach + to unprivileged guests, so Docker inside them silently vanished from the + Proxmox page. The grant is deliberately narrower than the + `NoNewPrivileges=false` exception above: it restores only the privilege + `lxc-attach` needs and leaves the rest of the sandbox intact. ## Extension Points diff --git a/scripts/install.sh b/scripts/install.sh index 2d7a11fff..ef2e8093f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1012,10 +1012,27 @@ render_systemd_agent_unit() { if [[ -n "$log_target" ]]; then log_lines=$'\n'"StandardOutput=append:${log_target}"$'\n'"StandardError=append:${log_target}" fi + local ambient_line="" if systemd_agent_requires_lxc_attach; then no_new_privileges="false" restrict_suidsgid="false" fi + if systemd_agent_may_attach_lxc; then + # lxc-attach into an unprivileged guest writes /proc//uid_map, + # which needs CAP_SETUID in the parent user namespace. NoNewPrivileges + # drops CAP_SETUID from the effective set and also stops lxc-attach + # falling back to the setuid newuidmap/newgidmap helpers, so the probe + # dies with "write_id_mapping: Operation not permitted" and Docker in + # every unprivileged LXC stays invisible. + # + # This is granted for any PVE agent, not only one installed with + # --enable-commands, because command execution can be turned on later + # from the server (applyRemoteConfig) without rewriting this unit. That + # path used to leave the agent able to run commands but unable to + # attach to unprivileged guests, and the failure is only visible at + # debug level. + ambient_line=$'\n'"AmbientCapabilities=CAP_SETUID CAP_SETGID" + fi local hardening_lines hardening_lines="NoNewPrivileges=${no_new_privileges} @@ -1025,7 +1042,7 @@ ProtectKernelModules=true ProtectControlGroups=true LockPersonality=true RestrictSUIDSGID=${restrict_suidsgid} -SystemCallArchitectures=native" +SystemCallArchitectures=native${ambient_line}" if [[ -d /usr/syno ]]; then # Synology DSM ships a heavily patched, old systemd whose kernels # cannot apply these sandbox directives; NoNewPrivileges alone kills @@ -1053,7 +1070,18 @@ EOF } systemd_agent_requires_lxc_attach() { - if [[ "$ENABLE_COMMANDS" != "true" || "$ENABLE_PROXMOX" != "true" ]]; then + if [[ "$ENABLE_COMMANDS" != "true" ]]; then + return 1 + fi + systemd_agent_may_attach_lxc +} + +# Whether this host could ever need lxc-attach, independent of whether command +# execution was enabled at install time. Command execution is also togglable +# from the server after the fact, so the unit has to be provisioned for it up +# front or the later toggle produces a half-working agent. +systemd_agent_may_attach_lxc() { + if [[ "$ENABLE_PROXMOX" != "true" ]]; then return 1 fi case "${PROXMOX_TYPE:-}" in diff --git a/scripts/installtests/install_sh_test.go b/scripts/installtests/install_sh_test.go index 67179b6a7..db45a34f6 100644 --- a/scripts/installtests/install_sh_test.go +++ b/scripts/installtests/install_sh_test.go @@ -266,7 +266,7 @@ func TestInstallSHAllowsProxmoxCommandAgentLXCAttach(t *testing.T) { script := string(content) required := []string{ `systemd_agent_requires_lxc_attach() {`, - `if [[ "$ENABLE_COMMANDS" != "true" || "$ENABLE_PROXMOX" != "true" ]]; then`, + `if [[ "$ENABLE_COMMANDS" != "true" ]]; then`, `""|pve|all)`, `no_new_privileges="false"`, `restrict_suidsgid="false"`, @@ -278,6 +278,37 @@ func TestInstallSHAllowsProxmoxCommandAgentLXCAttach(t *testing.T) { } } +// A PVE agent installed without --enable-commands must still be provisioned for +// lxc-attach, because command execution can be enabled later from the server +// without rewriting the unit. Without CAP_SETUID, lxc-attach cannot write +// /proc//uid_map for unprivileged guests, so Docker inside every +// unprivileged LXC silently disappears from the Proxmox page. +func TestInstallSHGrantsLXCAttachCapabilitiesForAnyPVEAgent(t *testing.T) { + content, err := os.ReadFile(repoFile("scripts", "install.sh")) + if err != nil { + t.Fatalf("read install.sh: %v", err) + } + + script := string(content) + required := []string{ + `systemd_agent_may_attach_lxc() {`, + `if [[ "$ENABLE_PROXMOX" != "true" ]]; then`, + `if systemd_agent_may_attach_lxc; then`, + `AmbientCapabilities=CAP_SETUID CAP_SETGID`, + `SystemCallArchitectures=native${ambient_line}`, + } + for _, needle := range required { + if !strings.Contains(script, needle) { + t.Fatalf("install.sh missing unprivileged-LXC attach capability grant: %s", needle) + } + } + + if strings.Contains(script, `systemd_agent_may_attach_lxc`) && + !strings.Contains(script, "command execution can be turned on later") { + t.Fatal("install.sh must document why the capability grant is not gated on ENABLE_COMMANDS") + } +} + func TestInstallSHPreflightChecksAgentDownloadArtifact(t *testing.T) { var requestedDownloadPath string var requestedDownloadArch string