fix: run purevpn-cli as non-root vpnuser with real sudo

purevpn-cli is designed to run as non-root and calls sudo internally for
privileged VPN setup. Running as root skips this flow and crashes.

- Add vpnuser (home=/root so login tokens are shared with root setup)
- Configure sudoers secure_path to include /opt/purevpn-cli/bin
- Wrap all purevpn-cli calls in entrypoint with pvpn() helper (su vpnuser)
- Keep iptables/danted running as root

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 18:44:03 +01:00
parent 34b5c4a8cd
commit a2a1ba3c37
2 changed files with 20 additions and 11 deletions

View File

@@ -14,11 +14,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
net-tools openresolv \ net-tools openresolv \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# ── Allow passwordless sudo for all (container is already isolated) ─────────── # ── Non-root vpnuser ─────────────────────────────────────────────────────────
RUN echo "ALL ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers # purevpn-cli is designed to run as non-root; it calls sudo internally for
# privileged VPN setup. Home is /root so login tokens written by root are shared.
RUN useradd -M -d /root -s /bin/bash vpnuser
# ── Sudoers: passwordless + correct PATH for vpnuser ─────────────────────────
RUN echo "vpnuser ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers \
&& echo 'Defaults:vpnuser secure_path="/opt/purevpn-cli/bin:/opt/purevpn-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"' >> /etc/sudoers
# ── Stub openvpn-systemd-resolved ──────────────────────────────────────────── # ── Stub openvpn-systemd-resolved ────────────────────────────────────────────
# Not in Debian repos; purevpn-cli checks for it before calling sudo.
RUN mkdir -p /usr/lib/openvpn \ RUN mkdir -p /usr/lib/openvpn \
&& printf '#!/bin/sh\nexit 0\n' \ && printf '#!/bin/sh\nexit 0\n' \
| tee /usr/local/bin/openvpn-systemd-resolved \ | tee /usr/local/bin/openvpn-systemd-resolved \

View File

@@ -21,6 +21,10 @@ VPN_WAIT=60
log() { echo "[$(date '+%H:%M:%S')] [$(hostname)] $*"; } log() { echo "[$(date '+%H:%M:%S')] [$(hostname)] $*"; }
die() { log "FATAL: $*" >&2; exit 1; } die() { log "FATAL: $*" >&2; exit 1; }
# purevpn-cli must run as non-root; it calls sudo internally for VPN setup.
# vpnuser has home=/root so login tokens written here are found automatically.
pvpn() { su -s /bin/bash -c "HOME=/root $(printf '%q ' "$@")" vpnuser; }
SOCKS_PID="" SOCKS_PID=""
# ── iptables: let HAProxy reach danted regardless of VPN kill-switch ───────── # ── iptables: let HAProxy reach danted regardless of VPN kill-switch ─────────
@@ -106,9 +110,9 @@ manual_mode() {
log "═══ MANUAL CONNECT MODE ═══" log "═══ MANUAL CONNECT MODE ═══"
log "Connect to this container with:" log "Connect to this container with:"
log " docker exec -it $(hostname) bash" log " docker exec -it $(hostname) bash"
log "Then run:" log "Then run (as vpnuser or root with HOME=/root):"
log " purevpn-cli --login" log " su -s /bin/bash -c 'HOME=/root purevpn-cli --login' vpnuser"
log " purevpn-cli --connect \"Germany\" # or any location" log " su -s /bin/bash -c 'HOME=/root purevpn-cli --connect \"Germany\"' vpnuser"
while true; do while true; do
log "Waiting for $VPN_IF to appear …" log "Waiting for $VPN_IF to appear …"
@@ -165,13 +169,13 @@ auto_mode() {
log "Logging into PureVPN …" log "Logging into PureVPN …"
expect -c " expect -c "
set timeout 120 set timeout 120
spawn purevpn-cli --login spawn su -s /bin/bash -c {HOME=/root purevpn-cli --login} vpnuser
expect -re {[Ee]mail|[Uu]sername|[Ll]ogin} { send \"$PUREVPN_USER\r\" } expect -re {[Ee]mail|[Uu]sername|[Ll]ogin} { send \"$PUREVPN_USER\r\" }
expect -re {[Pp]assword} { send \"$PUREVPN_PASS\r\" } expect -re {[Pp]assword} { send \"$PUREVPN_PASS\r\" }
expect eof expect eof
" && log "Login OK" || { " && log "Login OK" || {
log "expect login failed, trying stdin …" log "expect login failed, trying stdin …"
printf '%s\n%s\n' "$PUREVPN_USER" "$PUREVPN_PASS" | purevpn-cli --login || true printf '%s\n%s\n' "$PUREVPN_USER" "$PUREVPN_PASS" | pvpn purevpn-cli --login || true
} }
# ── Connect loop (rotates through locations on failure/drop) ───────────── # ── Connect loop (rotates through locations on failure/drop) ─────────────
@@ -181,7 +185,7 @@ auto_mode() {
USED_LOCATIONS+=("$loc") USED_LOCATIONS+=("$loc")
log "Connecting → '$loc'" log "Connecting → '$loc'"
purevpn-cli --connect "$loc" & pvpn purevpn-cli --connect "$loc" &
VPN_PID=$! VPN_PID=$!
if wait_for_tunnel "$VPN_WAIT"; then if wait_for_tunnel "$VPN_WAIT"; then
@@ -192,13 +196,13 @@ auto_mode() {
monitor_loop || true monitor_loop || true
# Tunnel dropped — kill VPN process and try next location # Tunnel dropped — kill VPN process and try next location
kill "$VPN_PID" 2>/dev/null || true kill "$VPN_PID" 2>/dev/null || true
purevpn-cli --disconnect 2>/dev/null || true pvpn purevpn-cli --disconnect 2>/dev/null || true
sleep 3 sleep 3
log "Rotating to next location …" log "Rotating to next location …"
else else
log "'$loc' timed out — trying next location" log "'$loc' timed out — trying next location"
kill "$VPN_PID" 2>/dev/null || true kill "$VPN_PID" 2>/dev/null || true
purevpn-cli --disconnect 2>/dev/null || true pvpn purevpn-cli --disconnect 2>/dev/null || true
sleep 3 sleep 3
fi fi
done done