Repository navigation
main: add -Z to drop privileges after binding sockets - #40
JuliusBairaktaris wants to merge 1 commit into
Conversation
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>
|
@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 |
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>
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. It ships with uhttpd-mod-ubus rather than the base package, which links no ubus library and opens no connection to grant: 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.*, 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. network.rrdns is an exec plugin with no session ACL of its own, so it is named by its single method instead. 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 ltq-vdsl-vr11-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>
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: 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. It ships with uhttpd-mod-ubus rather than the base package, which links no ubus library and opens no connection to grant: 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.*, rpc-sys) are granted with a method wildcard, which openwrt/rpcd#38 puts behind the session ACL for non-root callers; without it rc, rpc-sys and iwinfo carry no session check at all. network.rrdns is an rpcd plugin with a single method, which the grant names rather than wildcarding. The objects other daemons own 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. The session object is granted only what LuCI's login flow uses, 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>
9751afb to
5a822f2
Compare
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: 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. It ships with uhttpd-mod-ubus rather than the base package, which links no ubus library and opens no connection to grant: 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.*, rpc-sys) are granted with a method wildcard, which openwrt/rpcd#38 puts behind the session ACL for non-root callers; without it rc, rpc-sys and iwinfo carry no session check at all. network.rrdns is an rpcd plugin with a single method, which the grant names rather than wildcarding. The objects other daemons own 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. The session object is granted only what LuCI's login flow uses, 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>
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: 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. It ships with uhttpd-mod-ubus rather than the base package, which links no ubus library and opens no connection to grant: 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.*, rpc-sys) are granted with a method wildcard, which openwrt/rpcd#38 puts behind the session ACL for non-root callers; without it rc, rpc-sys and iwinfo carry no session check at all. network.rrdns is an rpcd plugin with a single method, which the grant names rather than wildcarding. The objects other daemons own 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. The session object is granted only what LuCI's login flow uses, 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>
Adds
-Z user. uhttpd needs root only to bind its listeners and read the TLSkey. With
-Zit drops to that user after startup (initgroups(),setgid(),setresuid()), before it daemonizes. Without-Znothing changes.The drop happens after
uh_tls_init()and before the plugins areinitialised. The ubus plugin connects in its init, and ubusd decides whether a
connection is root when it is accepted. Connecting after the drop puts uhttpd's
ubus access under
/usr/share/acl.d.CGI children inherit the dropped uid, so a handler that needs root needs its
own fix (openwrt/cgi-io#6). openwrt/openwrt#24740 is the package side that
passes
-Z.Tested on an ipq807x router: an rpcd
file execwith no session returneduid=0(root)over a socket opened before the drop andPermission deniedoverone opened after. I have not tested much else at the dropped uid yet.
Is before the plugin init the right place, or does a lua or ucode handler need
root during its init?
I used AI to help me write the change.