Grant CAP_NET_RAW to the installed systemd unit for ICMP probes

The installer's unit hardens with NoNewPrivileges=true, which strips the
ping binary's setuid/file capabilities, so ICMP availability probes could
never work on a systemd install. Grant the capability ambiently instead,
document the systemctl edit override for existing units, and pin the
hardening block with an install test and contract clause.

Addresses #1554 (discussion)
This commit is contained in:
rcourtman
2026-07-11 18:15:22 +01:00
parent a44e5513ca
commit 5a6871813f
4 changed files with 73 additions and 0 deletions
+29
View File
@@ -455,6 +455,35 @@ Example API payload for simple ping monitoring:
Pulse stores and returns that target as `protocol: "icmp"` so dashboards,
alerts, and resource projections keep one canonical protocol value.
### ICMP probe privileges
ICMP probes run the system `ping` binary, which needs the `CAP_NET_RAW`
capability. The systemd unit written by the installer hardens the service
with `NoNewPrivileges=true`, which strips ping's setuid bit and file
capabilities, so the unit also grants the capability directly with
`AmbientCapabilities=CAP_NET_RAW`. Units written by older versions of the
installer lack that line, and ICMP probes fail with
`icmp probe failed: ping: socktype: SOCK_RAW ... missing cap_net_raw+p capability`.
To fix an existing install, either re-run the install script (it rewrites
the unit) or add the capability as an override:
```bash
systemctl edit pulse # pulse-backend on ProxmoxVE community-script installs
```
```ini
[Service]
AmbientCapabilities=CAP_NET_RAW
```
Then `systemctl daemon-reload && systemctl restart pulse`.
Docker installs are unaffected: Docker's default capability set includes
`NET_RAW`. If you run the container with `--cap-drop=ALL`, add
`--cap-add=NET_RAW` to keep ICMP probes working. TCP and HTTP/HTTPS probes
need no special privileges.
---
## 🔒 HTTPS / TLS
@@ -407,6 +407,17 @@ TLS floor in the dynamic config.
signature verification depends on `ssh-keygen` from `openssh-client` and
must not fail on a minimal supported host solely because that package was
absent before installation started.
The server systemd unit that root `install.sh` writes
(`install_systemd_service`) hardens with `NoNewPrivileges=true`, which
strips setuid and file capabilities from every child the server executes.
ICMP availability probes exec the system `ping` binary, so the same
hardening block must also grant `AmbientCapabilities=CAP_NET_RAW` with a
matching `CapabilityBoundingSet=CAP_NET_RAW`; dropping either regresses
ICMP availability checks to permanent failure on every systemd install
(discussion #1554). `scripts/installtests/root_install_sh_test.go`
(`TestRootInstallServiceGrantsIcmpProbeCapability`) pins the pairing, and
`docs/CONFIGURATION.md` documents the `systemctl edit` override for units
written before the grant existed.
The top-level `install.sh` asset published on GitHub Releases must be the
root Pulse SERVER installer (the LXC / systemd / Proxmox VE installer that
accepts `--version vX.Y.Z`, `--rc`, `--stable`, and friends). The rendered
+4
View File
@@ -4217,6 +4217,10 @@ PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$INSTALL_DIR $CONFIG_DIR
# ICMP availability probes exec the system ping binary; NoNewPrivileges strips
# its setuid/file capabilities, so grant CAP_NET_RAW ambiently instead (#1554).
AmbientCapabilities=CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_RAW
[Install]
WantedBy=multi-user.target
@@ -1279,3 +1279,32 @@ func TestRootInstallScriptSourceGuardSurvivesPipedExecution(t *testing.T) {
t.Fatalf("source guard did not fall through to the installer body when piped:\n%s", got)
}
}
func TestRootInstallServiceGrantsIcmpProbeCapability(t *testing.T) {
content, err := os.ReadFile(filepath.Join("..", "..", "install.sh"))
if err != nil {
t.Fatalf("read root install.sh: %v", err)
}
script := string(content)
required := []string{
`NoNewPrivileges=true`,
`AmbientCapabilities=CAP_NET_RAW`,
`CapabilityBoundingSet=CAP_NET_RAW`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing systemd ICMP capability grant: %s", needle)
}
}
// The capability grant must live in the same hardening block that sets
// NoNewPrivileges, so every unit the installer writes gets both.
hardening := script[strings.Index(script, "# Security hardening"):]
if end := strings.Index(hardening, "[Install]"); end >= 0 {
hardening = hardening[:end]
}
if !strings.Contains(hardening, "AmbientCapabilities=CAP_NET_RAW") {
t.Fatal("AmbientCapabilities=CAP_NET_RAW is not in the unit's security hardening block")
}
}