highJun 30, 2026

Kahi Supervisor Privilege Drop and Socket Permission Issues

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Kahi's process supervisor silently failed to apply configured user credentials to child processes, left supplementary groups intact after dropping privileges, and exposed FastCGI Unix sockets to all l

Packagegithub.com/kahiteam/kahi
Ecosystemgo
Affected<= 0.1.0-alpha.8
Fixed in0.1.0-alpha.9

The problem

In Kahi <= v0.1.0-alpha.8, a process configured with `user = "uid:gid"` had its credential resolved but the resulting `syscall.Credential` was never attached to the `exec.Cmd` that spawned the child. The child inherited the supervisor's own UID/GID (typically root when run as root) with no error raised.

When privilege drop did occur elsewhere, the code called `setgid` and `setuid` but skipped `setgroups(2)`, leaving the launching user's supplementary groups (for example `docker`, which grants near-root access) active on child processes.

A third issue: FastCGI Unix-domain sockets were only `chmod`-ed when `socket_mode` was explicitly set in config. With `socket_mode` unset, the socket inherited the umask default, commonly `0666`, making it world-readable and world-writable to any local user.

Proof of concept

A working proof-of-concept for this issue in github.com/kahiteam/kahi, with the exact payload below.

bash
# kahi.toml for a privileged supervisor running as root

[programs.myapp]
command = "/usr/local/bin/myapp"
user = "1000:1000"   # credential resolved but NEVER applied; child runs as root

[programs.fcgi]
command = "/usr/local/bin/php-fpm"
socket = "/run/kahi/php.sock"
# socket_mode intentionally omitted:
# socket inherits umask -> 0666 (world-accessible)
# Any local user can connect and send FastCGI requests

# Verify child is actually root (not the configured uid 1000):
# $ ps -eo pid,uid,gid,cmd | grep myapp
# -> UID=0 GID=0 /usr/local/bin/myapp
#
# Verify socket permissions:
# $ stat /run/kahi/php.sock
# -> 0666 srw-rw-rw-
#
# Verify supplementary groups survive the drop (when drop path IS hit):
# $ cat /proc/$(pgrep myapp)/status | grep Groups
# -> Groups: 0 4 24 27 117 998   (docker=998 still present)

Bug 1 is a silent no-op: the Go code resolved the `syscall.Credential` struct but assigned it to a local variable instead of setting `cmd.SysProcAttr.Credential`, so the child's process tokens were never changed. The patch makes this fail-closed: if the credential cannot be applied, the supervisor refuses to start the process.

Bug 2 is a classic incomplete privilege drop. POSIX requires `setgroups([])` before `setgid`/`setuid`; omitting it leaves supplementary group membership (including privileged groups like `docker`) inherited by children. The fix adds a `setgroups(2)` call with an empty slice before the UID/GID change.

Bug 3 is a missing secure default. Conditional chmod only on explicit config means the socket retains whatever the process umask allows. The fix defaults the socket mode to `0700` and applies the chmod unconditionally. CWE-272 (Least Privilege Violation) and CWE-732 (Incorrect Permission Assignment) cover all three issues.

The fix

Upgrade to v0.1.0-alpha.9. The patch makes privilege handling fail-closed (credential is applied or the process refuses to start), calls `setgroups([]int{})` before `setgid`/`setuid`, and defaults FastCGI Unix sockets to mode `0700`. For versions <= v0.1.0-alpha.8 with no immediate upgrade path: do not rely on per-process `user` in config, run the supervisor directly as the intended unprivileged user, set an explicit restrictive `socket_mode` on all FastCGI programs, and avoid running the supervisor as root.

Reporter not attributed.

References: [1][2]

Related research