git: 23c19ba170c5 - main - security/py-privleap: New port: Limited Privilege Escalation Framework
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Mon, 25 May 2026 17:48:23 UTC
The branch main has been updated by dtxdf:
URL: https://cgit.FreeBSD.org/ports/commit/?id=23c19ba170c5c6ce8102452cd6416a223e62c791
commit 23c19ba170c5c6ce8102452cd6416a223e62c791
Author: Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org>
AuthorDate: 2026-05-25 17:46:31 +0000
Commit: Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org>
CommitDate: 2026-05-25 17:47:38 +0000
security/py-privleap: New port: Limited Privilege Escalation Framework
privleap is a privilege escalation framework similar in purpose to
sudo and doas, but very different conceptually. It is designed to
allow user-level applications to run very specific operations as
root without allowing full root control of the machine. Unlike
directly executable privilege escalation frameworks like sudo,
privleap runs as a background service that listens for signals from
other applications. Each signal can request a particular, pre-configured
action to be taken. Signals are authenticated, and each action is
taken only if the signal passes authentication. Any console output
from the action is then returned to the caller. This system allows
privleap to function without being SUID-root, and avoids a lot of
the potential pitfalls of sudo, doas, run0, etc.
WWW: https://www.kicksecure.com/wiki/Privleap
---
security/Makefile | 1 +
security/py-privleap/Makefile | 50 +++++++++
security/py-privleap/distinfo | 3 +
security/py-privleap/files/pam.conf | 5 +
security/py-privleap/files/pam_create_socket.sh | 8 ++
.../patch-auto-generated-man-pages_privleapd.1 | 11 ++
..._lib_python3_dist-packages_privleap_privleap.py | 19 ++++
...lib_python3_dist-packages_privleap_privleapd.py | 125 +++++++++++++++++++++
.../files/patch-usr_libexec_privleap_shim.py | 11 ++
security/py-privleap/files/privleapd.in | 23 ++++
security/py-privleap/pkg-descr | 12 ++
security/py-privleap/pkg-message | 11 ++
security/py-privleap/pkg-plist | 27 +++++
13 files changed, 306 insertions(+)
diff --git a/security/Makefile b/security/Makefile
index 72e050a741ef..4b2e4e6750f5 100644
--- a/security/Makefile
+++ b/security/Makefile
@@ -1030,6 +1030,7 @@
SUBDIR += py-pnu-vuxml
SUBDIR += py-policyuniverse
SUBDIR += py-potr
+ SUBDIR += py-privleap
SUBDIR += py-pwntools
SUBDIR += py-pyaes
SUBDIR += py-pyaff4
diff --git a/security/py-privleap/Makefile b/security/py-privleap/Makefile
new file mode 100644
index 000000000000..802949ea9dfa
--- /dev/null
+++ b/security/py-privleap/Makefile
@@ -0,0 +1,50 @@
+PORTNAME= privleap
+DISTVERSION= 5.7-1
+CATEGORIES= security python
+PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX}
+
+MAINTAINER= dtxdf@FreeBSD.org
+COMMENT= Limited Privilege Escalation Framework
+WWW= https://www.kicksecure.com/wiki/Privleap
+
+LICENSE= AGPLv3
+LICENSE_FILE= ${WRKSRC}/COPYING
+
+RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}PAM>=0:security/py-PAM@${PY_FLAVOR}
+
+USES= python:3.13+ shebangfix
+USE_GITHUB= yes
+GH_ACCOUNT= Kicksecure
+USE_RC_SUBR= privleapd
+
+SHEBANG_FILES= usr/bin/* usr/libexec/privleap/shim.py
+
+NO_ARCH= yes
+
+SUB_LIST= PYTHON_CMD=${PYTHON_CMD}
+
+do-build:
+ @${PYTHON_CMD} -OO ${PYTHON_LIBDIR}/compileall.py \
+ -d ${PYTHON_SITELIBDIR} \
+ -f ${WRKSRC}/usr/lib/python3/dist-packages/${PORTNAME}
+
+do-install:
+.for script in leapctl leaprun privleapd
+ ${INSTALL_SCRIPT} ${WRKSRC}/usr/bin/${script} ${STAGEDIR}${PREFIX}/bin/${script}
+.endfor
+ @${MKDIR} ${STAGEDIR}${PYTHON_SITELIBDIR}/${PORTNAME}
+ @cd ${WRKSRC}/usr/lib/python3/dist-packages/${PORTNAME} && \
+ ${COPYTREE_SHARE} . ${STAGEDIR}${PYTHON_SITELIBDIR}/${PORTNAME}
+ @${MKDIR} ${STAGEDIR}${PREFIX}/libexec/${PORTNAME}
+ ${INSTALL_SCRIPT} ${WRKSRC}/usr/libexec/${PORTNAME}/shim.py ${STAGEDIR}${PREFIX}/libexec/${PORTNAME}
+ ${INSTALL_SCRIPT} ${FILESDIR}/pam_create_socket.sh ${STAGEDIR}${PREFIX}/libexec/${PORTNAME}
+ ${INSTALL_MAN} ${WRKSRC}/auto-generated-man-pages/leapctl.8 ${STAGEDIR}${PREFIX}/share/man/man8
+ ${INSTALL_MAN} ${WRKSRC}/auto-generated-man-pages/leaprun.8 ${STAGEDIR}${PREFIX}/share/man/man8
+ ${INSTALL_MAN} ${WRKSRC}/auto-generated-man-pages/privleap.conf.d.5 ${STAGEDIR}${PREFIX}/share/man/man5
+ ${INSTALL_MAN} ${WRKSRC}/auto-generated-man-pages/privleapd.1 ${STAGEDIR}${PREFIX}/share/man/man1
+ @${MKDIR} ${STAGEDIR}${ETCDIR}/conf.d
+ @cd ${WRKSRC}/etc/${PORTNAME}/conf.d && ${COPYTREE_SHARE} . ${STAGEDIR}${ETCDIR}/conf.d
+ @${MKDIR} ${STAGEDIR}${PREFIX}/etc/pam.d
+ ${INSTALL_DATA} ${FILESDIR}/pam.conf ${STAGEDIR}${PREFIX}/etc/pam.d/privleapd
+
+.include <bsd.port.mk>
diff --git a/security/py-privleap/distinfo b/security/py-privleap/distinfo
new file mode 100644
index 000000000000..95993b9ad24f
--- /dev/null
+++ b/security/py-privleap/distinfo
@@ -0,0 +1,3 @@
+TIMESTAMP = 1779647261
+SHA256 (Kicksecure-privleap-5.7-1_GH0.tar.gz) = 6ee88c2fbe1e868691ff5634994cf22d613e91abe8eba5b82083d875ac54afb5
+SIZE (Kicksecure-privleap-5.7-1_GH0.tar.gz) = 120717
diff --git a/security/py-privleap/files/pam.conf b/security/py-privleap/files/pam.conf
new file mode 100644
index 000000000000..2ba52ff0b2ac
--- /dev/null
+++ b/security/py-privleap/files/pam.conf
@@ -0,0 +1,5 @@
+auth sufficient pam_rootok.so
+auth include system
+account include system
+session include system
+password include system
diff --git a/security/py-privleap/files/pam_create_socket.sh b/security/py-privleap/files/pam_create_socket.sh
new file mode 100755
index 000000000000..9571b35c7e38
--- /dev/null
+++ b/security/py-privleap/files/pam_create_socket.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if [ -n "${PAM_USER}" ] && [ ! -S "/var/run/privleapd/comm/${PAM_USER}" ]; then
+ exec leapctl --create "${PAM_USER}"
+ exit 1
+fi
+
+exit 0
diff --git a/security/py-privleap/files/patch-auto-generated-man-pages_privleapd.1 b/security/py-privleap/files/patch-auto-generated-man-pages_privleapd.1
new file mode 100644
index 000000000000..177dfa327d28
--- /dev/null
+++ b/security/py-privleap/files/patch-auto-generated-man-pages_privleapd.1
@@ -0,0 +1,11 @@
+--- auto-generated-man-pages/privleapd.1.orig 2026-05-24 21:15:07 UTC
++++ auto-generated-man-pages/privleapd.1
+@@ -6,7 +6,7 @@
+ .SH "SYNOPSIS"
+ \fBprivleapd [\-C|\-\-check\-config] [\-h|\-\-help|\-?]\fR
+ .SH "DESCRIPTION"
+-The backend server for privleap\. Runs predefined code snippets as root when requested by an authorized client\. privleapd listens for connections on UNIX sockets under \fB/run/privleapd\fR, it does not send or receive information over a network\.
++The backend server for privleap\. Runs predefined code snippets as root when requested by an authorized client\. privleapd listens for connections on UNIX sockets under \fB/var/run/privleapd\fR, it does not send or receive information over a network\.
+ .P
+ You usually will not need to run this directly \- if \fBprivleap\fR was installed from your distribution's repository, it should be pre\-configured to start on boot\. The one time you usually will want to run it is when ensuring that all configuration is valid before restarting privleapd\.
+ .SH "OPTIONS"
diff --git a/security/py-privleap/files/patch-usr_lib_python3_dist-packages_privleap_privleap.py b/security/py-privleap/files/patch-usr_lib_python3_dist-packages_privleap_privleap.py
new file mode 100644
index 000000000000..8304748235a6
--- /dev/null
+++ b/security/py-privleap/files/patch-usr_lib_python3_dist-packages_privleap_privleap.py
@@ -0,0 +1,19 @@
+--- usr/lib/python3/dist-packages/privleap/privleap.py.orig 2026-05-24 20:47:02 UTC
++++ usr/lib/python3/dist-packages/privleap/privleap.py
+@@ -1020,7 +1020,6 @@ class PrivleapSession:
+ """
+
+ assert self.backend_socket is not None
+- self.backend_socket.shutdown(socket.SHUT_RDWR)
+ self.backend_socket.close()
+ self.is_session_open = False
+
+@@ -1223,7 +1222,7 @@ class PrivleapCommon:
+ Common constants and functions used throughout privleap.
+ """
+
+- state_dir: Path = Path("/run/privleapd")
++ state_dir: Path = Path("/var/run/privleapd")
+ control_path: Path = Path(state_dir, "control")
+ comm_dir: Path = Path(state_dir, "comm")
+ config_file_regex: re.Pattern[str] = re.compile(r"[-A-Za-z0-9_]+\.conf\Z")
diff --git a/security/py-privleap/files/patch-usr_lib_python3_dist-packages_privleap_privleapd.py b/security/py-privleap/files/patch-usr_lib_python3_dist-packages_privleap_privleapd.py
new file mode 100644
index 000000000000..ef8d4a5a3a71
--- /dev/null
+++ b/security/py-privleap/files/patch-usr_lib_python3_dist-packages_privleap_privleapd.py
@@ -0,0 +1,125 @@
+--- usr/lib/python3/dist-packages/privleap/privleapd.py.orig 2026-05-24 19:41:39 UTC
++++ usr/lib/python3/dist-packages/privleap/privleapd.py
+@@ -28,8 +28,6 @@ from dataclasses import dataclass
+ from typing import cast, SupportsIndex, NoReturn, Any, IO
+ from dataclasses import dataclass
+
+-import sdnotify # type: ignore
+-
+ from .privleap import (
+ ConfigData,
+ PrivleapAction,
+@@ -116,9 +114,6 @@ class PrivleapdGlobal:
+ allowed_group_list: list[str] = []
+ expected_disallowed_user_list: list[str] = []
+
+- # Readable and writable by main thread only
+- sdnotify_object: sdnotify.SystemdNotifier = sdnotify.SystemdNotifier()
+-
+ # Thread IPC mechanisms
+ # control-to-main pipe read end, for main thread
+ ctm_read_fd: int = 0
+@@ -609,7 +604,7 @@ def run_action(
+ if target_user is None and target_group is None:
+ # Both user and group are unset, default to "root" for both.
+ target_user = "root"
+- target_group = "root"
++ target_group = "wheel"
+ elif target_group is None:
+ # Target user is set but group is unset, set the group to the target
+ # user's default group.
+@@ -629,14 +624,13 @@ def run_action(
+
+ action_process: subprocess.Popen[bytes] = subprocess.Popen(
+ [
+- "/usr/libexec/privleap/shim.py",
++ "/usr/local/libexec/privleap/shim.py",
+ calling_user,
+ target_user,
+ target_group,
+ str(PrivleapdGlobal.old_umask),
+- "/usr/bin/bash",
++ "/bin/sh",
+ "-c",
+- "--",
+ desired_action.action_command,
+ ],
+ stdout=subprocess.PIPE,
+@@ -841,10 +835,10 @@ def send_action_results(
+ assert action_process.stderr is not None
+ assert comm_session.backend_socket is not None
+
+- epoll_obj: select.epoll = select.epoll()
+- epoll_obj.register(comm_session.backend_socket.fileno(), select.EPOLLIN)
+- epoll_obj.register(action_process.stdout.fileno(), select.EPOLLIN)
+- epoll_obj.register(action_process.stderr.fileno(), select.EPOLLIN)
++ epoll_obj: select.poll = select.poll()
++ epoll_obj.register(comm_session.backend_socket.fileno(), select.POLLIN)
++ epoll_obj.register(action_process.stdout.fileno(), select.POLLIN)
++ epoll_obj.register(action_process.stderr.fileno(), select.POLLIN)
+
+ # Comm threads that are currently streaming stdio from a process to a
+ # client may be stuck waiting for the process to write something to stdout
+@@ -855,7 +849,7 @@ def send_action_results(
+ # written to this variable (it is always a single NULL byte), we just need
+ # to break the epoll_obj.poll() call.
+ assert listen_socket_info.term_notify_read_fd != 0
+- epoll_obj.register(listen_socket_info.term_notify_read_fd, select.EPOLLIN)
++ epoll_obj.register(listen_socket_info.term_notify_read_fd, select.POLLIN)
+
+ try:
+ stdout_done: bool = False
+@@ -899,7 +893,6 @@ def send_action_results(
+ action_process.wait()
+
+ finally:
+- epoll_obj.close()
+ action_process.stdout.close()
+ action_process.stderr.close()
+ action_process.terminate()
+@@ -1673,8 +1666,8 @@ def main_loop() -> NoReturn:
+
+ assert PrivleapdGlobal.ctm_read_pipe is not None
+ epoll_fd_set: set[int] = set()
+- epoll_obj: select.epoll = select.epoll()
+- epoll_obj.register(PrivleapdGlobal.ctm_read_fd, select.EPOLLIN)
++ epoll_obj: select.poll = select.poll()
++ epoll_obj.register(PrivleapdGlobal.ctm_read_fd, select.POLLIN)
+ socket_list_changed: bool = True
+
+ while True:
+@@ -1688,14 +1681,13 @@ def main_loop() -> NoReturn:
+ ]
+ read_sock_fileno_set: set[int] = set(read_sock_fileno_list)
+ for register_fileno in read_sock_fileno_set - epoll_fd_set:
+- epoll_obj.register(register_fileno, select.EPOLLIN)
++ epoll_obj.register(register_fileno, select.POLLIN)
+ epoll_fd_set.update(read_sock_fileno_set)
+ for remove_fileno in epoll_fd_set - read_sock_fileno_set:
+ epoll_fd_set.remove(remove_fileno)
+ socket_list_changed = False
+
+ epoll_event_fd_list: list[int] = [x[0] for x in epoll_obj.poll(5)]
+- PrivleapdGlobal.sdnotify_object.notify("WATCHDOG=1")
+
+ if PrivleapdGlobal.ctm_read_fd in epoll_event_fd_list:
+ # Connection change, i.e. adding or removing a socket. The
+@@ -1723,8 +1715,7 @@ def main_loop() -> NoReturn:
+ ready_sock_info_obj = sock_info_obj
+ break
+ if ready_sock_info_obj is None:
+- logging.critical("privleapd lost track of a socket!")
+- sys.exit(1)
++ continue
+ if ready_sock_info_obj.listen_socket.socket_type == (
+ PrivleapSocketType.CONTROL
+ ):
+@@ -1799,8 +1790,6 @@ def main() -> NoReturn:
+ populate_state_dir()
+ open_control_socket()
+ open_persistent_comm_sockets(in_control_thread=False)
+- PrivleapdGlobal.sdnotify_object.notify("READY=1")
+- PrivleapdGlobal.sdnotify_object.notify("STATUS=Fully started")
+ if PrivleapdGlobal.test_mode:
+ Path("/tmp/privleapd-ready-for-test").touch()
+ control_handler_thread: Thread = Thread(
diff --git a/security/py-privleap/files/patch-usr_libexec_privleap_shim.py b/security/py-privleap/files/patch-usr_libexec_privleap_shim.py
new file mode 100644
index 000000000000..a2a92f3ce3c7
--- /dev/null
+++ b/security/py-privleap/files/patch-usr_libexec_privleap_shim.py
@@ -0,0 +1,11 @@
+--- usr/libexec/privleap/shim.py.orig 2026-05-24 22:32:43 UTC
++++ usr/libexec/privleap/shim.py
+@@ -100,7 +100,7 @@ action_env["LOGNAME"] = target_user_info.pw_name
+ action_env: dict[str, str] = os.environ.copy()
+ action_env["HOME"] = target_user_info.pw_dir
+ action_env["LOGNAME"] = target_user_info.pw_name
+-action_env["SHELL"] = "/usr/bin/bash"
++action_env["SHELL"] = "/bin/sh"
+ action_env["PWD"] = target_user_info.pw_dir
+ action_env["USER"] = target_user_info.pw_name
+ action_env["LC_ALL"] = "C"
diff --git a/security/py-privleap/files/privleapd.in b/security/py-privleap/files/privleapd.in
new file mode 100755
index 000000000000..37b93ae71e07
--- /dev/null
+++ b/security/py-privleap/files/privleapd.in
@@ -0,0 +1,23 @@
+#!/bin/sh
+#
+# PROVIDE: privleapd
+# REQUIRE: LOGIN
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name=privleapd
+desc="Limited Privilege Escalation Framework"
+rcvar="${name}_enable"
+
+load_rc_config "${name}"
+
+: ${privleapd_enable:=NO}
+
+pidfile="/var/run/${name}.pid"
+procname="%%PREFIX%%/bin/${name}"
+command_interpreter="%%PYTHON_CMD%%"
+command="/usr/sbin/daemon"
+command_args="-cS -l authpriv -t \"${desc}\" -T \"${name}\" -p \"${pidfile}\" \"${procname}\""
+
+run_rc_command "$1"
diff --git a/security/py-privleap/pkg-descr b/security/py-privleap/pkg-descr
new file mode 100644
index 000000000000..098f56fb78b2
--- /dev/null
+++ b/security/py-privleap/pkg-descr
@@ -0,0 +1,12 @@
+privleap is a privilege escalation framework similar in purpose to
+sudo and doas, but very different conceptually. It is designed to
+allow user-level applications to run very specific operations as
+root without allowing full root control of the machine. Unlike
+directly executable privilege escalation frameworks like sudo,
+privleap runs as a background service that listens for signals from
+other applications. Each signal can request a particular, pre-configured
+action to be taken. Signals are authenticated, and each action is
+taken only if the signal passes authentication. Any console output
+from the action is then returned to the caller. This system allows
+privleap to function without being SUID-root, and avoids a lot of
+the potential pitfalls of sudo, doas, run0, etc.
diff --git a/security/py-privleap/pkg-message b/security/py-privleap/pkg-message
new file mode 100644
index 000000000000..72a4265e0c43
--- /dev/null
+++ b/security/py-privleap/pkg-message
@@ -0,0 +1,11 @@
+[
+{ type: install
+ message: <<EOM
+pam_exec(8) could be used to call leapctl(8) to create the
+communication socket required by leaprun(8) by simply adding
+the following to a PAM policy file:
+
+ session optional pam_exec.so /usr/local/libexec/privleap/pam_create_socket.sh
+EOM
+}
+]
diff --git a/security/py-privleap/pkg-plist b/security/py-privleap/pkg-plist
new file mode 100644
index 000000000000..2ee26aa4acbc
--- /dev/null
+++ b/security/py-privleap/pkg-plist
@@ -0,0 +1,27 @@
+bin/leapctl
+bin/leaprun
+bin/privleapd
+etc/pam.d/privleapd
+%%ETCDIR%%/conf.d/privleap.conf
+%%PYTHON_SITELIBDIR%%/privleap/__init__.py
+%%PYTHON_SITELIBDIR%%/privleap/__pycache__/__init__%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/__pycache__/leapctl%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/__pycache__/leaprun%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/__pycache__/privleap%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/__pycache__/privleapd%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/leapctl.py
+%%PYTHON_SITELIBDIR%%/privleap/leaprun.py
+%%PYTHON_SITELIBDIR%%/privleap/privleap.py
+%%PYTHON_SITELIBDIR%%/privleap/privleapd.py
+%%PYTHON_SITELIBDIR%%/privleap/tests/__init__.py
+%%PYTHON_SITELIBDIR%%/privleap/tests/__pycache__/__init__%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/tests/__pycache__/run_test%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/tests/__pycache__/run_test_util%%PYTHON_TAG%%.opt-2.pyc
+%%PYTHON_SITELIBDIR%%/privleap/tests/run_test.py
+%%PYTHON_SITELIBDIR%%/privleap/tests/run_test_util.py
+libexec/privleap/shim.py
+libexec/privleap/pam_create_socket.sh
+share/man/man1/privleapd.1.gz
+share/man/man5/privleap.conf.d.5.gz
+share/man/man8/leapctl.8.gz
+share/man/man8/leaprun.8.gz