main: add -Z to drop privileges after binding sockets - #40
main: add -Z to drop privileges after binding sockets#40JuliusBairaktaris wants to merge 1 commit into
Conversation
|
@jow- @hauke — this is the uhttpd side of running the daemon as a dedicated The reason it is a uhttpd change rather than just a procd 26 lines, inert unless |
uhttpd currently requires root for the whole lifetime of the process, even though all privileged work (binding the listeners, reading the TLS key) happens during startup. Add -Z to drop to an unprivileged user once that startup is done, using initgroups/setgid/setresuid so no saved-root or supplementary-group state survives. Service managers that cannot use a sandbox (plain procd, systemd) can then run the daemon unprivileged on privileged ports. The drop precedes the handler plugin initialization because the ubus plugin connects to ubusd there, and ubusd reads the peer credentials once, when the connection is accepted: a socket opened before the drop stays a uid 0 connection for the life of the process, and ubusd exempts uid 0 from every ACL it enforces. Connecting afterwards is what puts the daemon's ubus access under /usr/share/acl.d. The plugins need no privilege of their own - they dlopen a module and read a handler script - whereas the TLS key is read before the drop and keeps its root ownership. Assisted-by: Claude:claude-opus-5 Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
f979adf to
9751afb
Compare
|
Amended: the drop now happens before the handler plugins are initialised rather than after them. ubusd reads the peer uid from The plugins need no privilege of their own — they dlopen a module and read a handler script — and the TLS key is still read before the drop, so it keeps |
uhttpd drops to user uhttpd (uid 456, created via USERID) with -Z, which it applies itself once the listeners are bound and the TLS key is read. The jail it runs in holds CAP_NET_BIND_SERVICE for the 80/443 bind and CAP_SETUID + CAP_SETGID for the drop, under PR_SET_NO_NEW_PRIVS; none of the three survive it, since a uid change away from root clears the permitted and effective sets without SECBIT_KEEP_CAPS. Ambient and inheritable stay empty: nothing has to cross an execve() here, because the bind still happens as root. Dropping inside the daemon rather than with procd's user parameter is what keeps the key at root:root 0600. The key path is a uci value, so dropping before execve() would mean chowning an operator-supplied path to the unprivileged user, and would need CAP_NET_BIND_SERVICE ambient to reach the bind. The jail is opt-in like the odhcpd one: applied when /sbin/ujail exists and /etc/capabilities/uhttpd.json is installed, and procd_add_jail with no flags keeps the instance out of a mount namespace. ubusd denies every non-root uid by default, so /usr/share/acl.d/uhttpd.json grants uid 456 what LuCI reaches: the ubus proxy connects to ubusd after the drop, so the grant is what gates it, and so do the CGI handlers on their own connections. Objects rpcd owns (file, uci, rc, iwinfo, luci, luci-rpc, luci.*, network.rrdns, rpc-sys) are granted with a method wildcard: rpcd re-checks the session ACL for non-root callers, so the grant alone does not authorise anything. The objects other daemons own - system, network, network.device, network.interface, service, hostapd.* and log from procd, netifd, hostapd and logd, dsl from ltq-vdsl-vr9-app and fingerprint from ufp where those are installed - are gated by ubusd alone and are granted method-explicit: system/reboot and hostapd.*/wps_start + del_client remain reachable without a session token, a residual DoS/proximity surface rather than privilege escalation, and still a large net reduction from running as root. session/create, session/grant and session/revoke are withheld so a compromised uhttpd cannot mint itself a privileged session. -Z does not exist in the pinned uhttpd, so it rides along as a package patch, openwrt/uhttpd#40 unchanged, until the next PKG_SOURCE_VERSION bump picks it up from upstream and the patch goes away. Assisted-by: Claude:claude-opus-5 Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
All the privileged work uhttpd does happens during startup: binding the
listeners, reading the TLS key, and dlopen'ing the handler plugins. Once
run_server()is reached, nothing the daemon does needs uid 0 — it servesfiles, proxies ubus, and forks CGI children, all of which are happy as a
normal user.
-Z userdrops to that user at exactly that point, right afteruh_tls_init()and theuh_plugin_init()calls and before the daemonizingfork, using
initgroups()+setgid()+setresuid()so no saved-set-uidand no supplementary group survives the transition.
Doing the drop inside uhttpd rather than in the service manager is what keeps
the TLS private key at
root:root 0600. A sandbox that changes the uidbefore
execve()forces the key to be readable by the unprivileged userinstead, and on OpenWrt the key path is a uci value, so handing it over means
chowning an operator-supplied path. Binding a privileged port stays free for
the same reason — it happens while the process is still root, so no ambient
CAP_NET_BIND_SERVICEhas to be carried across the exec.Opt-in and inert unless
-Zis passed.This is a prerequisite for openwrt/openwrt#24558, which runs uhttpd as a
dedicated non-root user.