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…

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