Keep Docker-in-LXC working when commands are enabled after install

lxc-attach into an unprivileged guest writes /proc/<pid>/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 socket probe dies with
"write_id_mapping: 61 Operation not permitted".

install.sh already relaxed NoNewPrivileges for this, but only when the
agent was installed with --enable-commands. Command execution is also
togglable from the server afterwards: applyRemoteConfig starts the
command client without rewriting the unit. An agent installed without
the flag and switched on later therefore ends up able to run commands
and unable to attach to unprivileged guests, so Docker inside every
unprivileged LXC disappears from the Proxmox page. The probe failure is
logged at debug level and retried on every poll, so the surface looks
empty rather than broken while the agent re-probes the whole guest list.

Grant CAP_SETUID/CAP_SETGID to any PVE agent rather than gating on the
install-time flag, so the later toggle lands on a unit that can attach.
Ambient capabilities restore exactly the privilege lxc-attach needs and
leave the rest of the sandbox intact; the existing install-time
relaxation is unchanged.

Verified on a live PVE node. With the hardened unit the probe succeeded
only on the three privileged guests and failed on every unprivileged
one. After the ambient grant CapEff regained CAP_SETUID and both
unprivileged Docker guests were discovered, taking that node from one
Docker LXC to three.
This commit is contained in:
rcourtman
2026-08-20 11:05:39 +01:00
parent d3a1fc07b8
commit c2f6848006
4 changed files with 88 additions and 3 deletions
@@ -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/<pid>/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
@@ -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/<pid>/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
+30 -2
View File
@@ -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/<pid>/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
+32 -1
View File
@@ -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/<pid>/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