Skip site navigation (1)Skip section navigation (2)
Date:      Mon, 25 May 2026 17:48:23 +0000
From:      =?utf-8?Q?Jes=C3=BAs?= Daniel Colmenares Oviedo <dtxdf@FreeBSD.org>
To:        ports-committers@FreeBSD.org, dev-commits-ports-all@FreeBSD.org, dev-commits-ports-main@FreeBSD.org
Subject:   git: 23c19ba170c5 - main - security/py-privleap: New port: Limited Privilege Escalation Framework
Message-ID:  <6a148b67.3becf.54c1c593@gitrepo.freebsd.org>

index | next in thread | raw e-mail

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


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a148b67.3becf.54c1c593>