From 1717ed676c90bb1b95d32bc786f17eb4fe339a1d Mon Sep 17 00:00:00 2001 From: shaggy Date: Fri, 28 Aug 2026 13:43:38 +0530 Subject: [PATCH 1/2] ovpnagent(mac): add peer credential validation to prevent unprivileged access The macOS ovpnagent listens on a Unix domain socket with mode 0777 and accepts all connections without verifying the peer's identity. Any local process can connect and issue privileged commands including: - POST /tun-setup: configure system routing and DNS as root, receive the tun file descriptor via SCM_RIGHTS - POST /add-bypass-route: modify the kernel routing table - GET /tun-destroy: tear down active VPN tunnels This change: 1. Validates the connecting process's UID via LOCAL_PEERCRED in allow_client(), rejecting connections that are not from root or the same user the agent runs as 2. Restricts the Unix socket mode from 0777 to 0600 The peercreds() and root_or_self_uid() infrastructure already exists in openvpn/common/peercred.hpp but was not being used by the macOS agent. --- openvpn/ovpnagent/mac/ovpnagent.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openvpn/ovpnagent/mac/ovpnagent.cpp b/openvpn/ovpnagent/mac/ovpnagent.cpp index 530c85aac..1a41e80a5 100644 --- a/openvpn/ovpnagent/mac/ovpnagent.cpp +++ b/openvpn/ovpnagent/mac/ovpnagent.cpp @@ -315,6 +315,20 @@ class MyListener : public WS::Server::Listener private: bool allow_client(AsioPolySock::Base &sock) override { + // Only accept connections from root or the same UID that + // the agent process is running as. This prevents any local + // unprivileged process from issuing privileged commands. + SockOpt::Creds cr; + if (!sock.peercreds(cr)) + { + OPENVPN_LOG("ovpnagent: failed to get peer credentials, rejecting connection"); + return false; + } + if (!cr.root_or_self_uid()) + { + OPENVPN_LOG("ovpnagent: rejected connection from UID " << cr.uid << " (not root or same user)"); + return false; + } return true; } @@ -546,7 +560,7 @@ class ServerThread : public ServerThreadBase config->http_server_id = OVPNAGENT_NAME_STRING "/" HTTP_SERVER_VERSION; config->frame = frame; config->stats = tc.stats; - config->unix_mode = 0777; + config->unix_mode = 0600; MyClientFactory::Ptr factory = new MyClientFactory(); listener.reset(new MyListener(io_context_arg, config, tc.listen_list, factory)); From ee20602d3985536b49c617471108ae74bba0aae4 Mon Sep 17 00:00:00 2001 From: shaggy Date: Sat, 29 Aug 2026 09:30:54 +0530 Subject: [PATCH 2/2] ovpnagent(mac): replace root-only gate with configurable UID allowlist Restores socket mode to 0666 so unprivileged clients can connect again. allow_client() now accepts root unconditionally, and non-root peers only when their UID is listed in /etc/openvpn/ovpnagent.allowed_uids (numeric UID or username, one per line). The special entry "all" restores the legacy unrestricted behaviour for deployments that want it. Missing or unreadable allowlist file means root-only access. --- openvpn/ovpnagent/mac/ovpnagent.cpp | 61 +++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/openvpn/ovpnagent/mac/ovpnagent.cpp b/openvpn/ovpnagent/mac/ovpnagent.cpp index 1a41e80a5..fa78997d1 100644 --- a/openvpn/ovpnagent/mac/ovpnagent.cpp +++ b/openvpn/ovpnagent/mac/ovpnagent.cpp @@ -17,6 +17,11 @@ #include #include +#include +#include +#include +#include +#include #include #include @@ -313,23 +318,63 @@ class MyListener : public WS::Server::Listener } private: + // Peer-credential gate: root is always allowed. Non-root peers are + // allowed only when their UID is listed in the agent's allowlist. + // The allowlist is read from /etc/openvpn/ovpnagent.allowed_uids: + // - one entry per line: a numeric UID or a username + // - the special value "all" disables the UID gate entirely + // (NOT recommended; preserves legacy open behaviour) + // An absent or unreadable file means root-only access. + static std::unique_ptr> load_allowed_uids() + { + auto uids = std::make_unique>(); + std::ifstream in("/etc/openvpn/ovpnagent.allowed_uids"); + std::string line; + while (std::getline(in, line)) + { + while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) + line.pop_back(); + if (line.empty() || line[0] == '#') + continue; + if (line == "all") + return nullptr; // gate disabled + if (std::all_of(line.begin(), line.end(), ::isdigit)) + { + uids->insert(uid_t(std::stoul(line))); + } + else + { + if (struct passwd *pw = ::getpwnam(line.c_str())) + uids->insert(pw->pw_uid); + } + } + return uids; + } + bool allow_client(AsioPolySock::Base &sock) override { - // Only accept connections from root or the same UID that - // the agent process is running as. This prevents any local - // unprivileged process from issuing privileged commands. SockOpt::Creds cr; if (!sock.peercreds(cr)) { OPENVPN_LOG("ovpnagent: failed to get peer credentials, rejecting connection"); return false; } - if (!cr.root_or_self_uid()) + if (cr.uid == 0) + return true; + + static const std::unique_ptr> allowed = load_allowed_uids(); + if (!allowed) { - OPENVPN_LOG("ovpnagent: rejected connection from UID " << cr.uid << " (not root or same user)"); - return false; + OPENVPN_LOG("ovpnagent: UID allowlist contains 'all'; allowing UID " << cr.uid); + return true; } - return true; + if (allowed->find(cr.uid) != allowed->end()) + { + OPENVPN_LOG("ovpnagent: allowing UID " << cr.uid << " (in allowlist)"); + return true; + } + OPENVPN_LOG("ovpnagent: rejected connection from UID " << cr.uid << " (not root and not in allowlist)"); + return false; } std::string bypass_host; @@ -560,7 +605,7 @@ class ServerThread : public ServerThreadBase config->http_server_id = OVPNAGENT_NAME_STRING "/" HTTP_SERVER_VERSION; config->frame = frame; config->stats = tc.stats; - config->unix_mode = 0600; + config->unix_mode = 0666; MyClientFactory::Ptr factory = new MyClientFactory(); listener.reset(new MyListener(io_context_arg, config, tc.listen_list, factory));