Least Privilege & User Handover Release - v1.2.2
v1.2.2 | Least Privilege & User Handover Release
Release v1.2.2 Changelog
🚀 Security & Privilege Management
- Non-Privileged User Implementation: Introduced a new system user
norduserand groupnorduser. This user is added to thenordvpngroup, allowing for a more secure execution environment. - Added
gosu: Installedgosuto handle the graceful transition from root (necessary for network configuration) to a non-privileged user for the main process execution. - Permission Hardening: Added explicit
chownandchmodcommands within the entrypoint to manage permissions for/run/nordvpnand/var/lib/nordvpn, ensuring thenordvpngroup has the necessary access.
🛠 Improvement & Logic Changes
- Updated Healthcheck: The
HEALTHCHECKnow executes thenordvpn statuscommand viagosu norduser. This verifies that the VPN status is accessible to non-root users. - Entrypoint Refactoring:
- Reformatted error messages for brevity.
- Transitioned the final execution loop to an
exec gosu nordusercall. - The container now actively monitors the
nordvpndprocess viapgrepto handle daemon exits more responsively.
- Metadata: Updated the
org.opencontainers.image.versionlabel to1.2.2.
📦 Dependency Updates
- Added
gosuto theapt-get installlayer.
Instructions - (v1.2.2)
This guide will walk you through the creation of all of the files, their contents, and directories needed in order to route a Docker application container through a Docker container for NordVPN. We are using audiobookshelf as the routed container example in this guide, but by changing a few things, you can adapt this guide for any application container.
🛠️ 1. Create the Dockerfile for the NordVPN Container
Create a directory (e.g. sudo mkdir ~/nordvpn-meshnet/), open it (e.g. cd ~/nordvpn-meshnet/) and save the following as Dockerfile inside it (e.g. sudo nano Dockerfile, keyboard shortcut Shift+Insert to paste with formatting, then Ctrl+X to save, followed by y to confirm saving, then Enter to confirm filename):
# REQUIRED RUNTIME ARGUMENTS:
# --cap-add=NET_ADMIN
# --cap-add=NET_RAW
# --device /dev/net/tun
#
# JUSTIFICATION:
# NET_ADMIN: Required for NordVPN to modify routing tables and iptables.
# NET_RAW: Required for NordVPN to create and manage raw sockets.
# /dev/net/tun: Required for the creation of the VPN tunnel interface.
FROM ubuntu:24.04@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b
LABEL org.opencontainers.image.authors="COLVDV" \
org.opencontainers.image.title="NordVPN Docker Gateway" \
org.opencontainers.image.description="NordVPN Docker Gateway with Meshnet" \
org.opencontainers.image.version="1.2.2" \
org.opencontainers.image.url="https://github.com/colvdv/nordvpn-docker-gateway" \
org.opencontainers.image.licenses="MIT" \
capabilities.net_admin="required" \
capabilities.net_raw="required"
# Optimized build layer
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
gnupg \
ca-certificates \
iproute2 \
iptables \
gosu \
&& mkdir -p -m 0700 /root/.gnupg \
&& wget -qO /tmp/nordvpn.asc https://repo.nordvpn.com/gpg/nordvpn_public.asc \
# Verify fingerprint to prevent MITM attacks
&& gpg --dry-run --quiet --import --import-options show-only /tmp/nordvpn.asc | grep -q "BC5480EFEC5C081CE5BCFBE26B219E535C964CA1" \
&& cat /tmp/nordvpn.asc | gpg --dearmor > /usr/share/keyrings/nordvpn-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/nordvpn-keyring.gpg] https://repo.nordvpn.com/deb/nordvpn/debian stable main" > /etc/apt/sources.list.d/nordvpn.list \
&& apt-get update \
# Pinned to specific NordVPN version (4.6.0, the latest as of this writing) for reproducibility. Check https://nordvpn.com/blog/nordvpn-linux-release-notes/ or remove the version tag to pull the latest Linux release version.
&& apt-get install -y --no-install-recommends nordvpn=4.6.0 \
# Create a non-privileged user and add them to the 'nordvpn' group
&& groupadd -r norduser && useradd -m -g norduser -G nordvpn norduser \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/nordvpn.asc /root/.gnupg
# HEALTHCHECK: Uses gosu to check status as the non-privileged user
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD gosu norduser nordvpn status | grep -qE "Status: Disconnected|Status: Connected" || exit 1
# ENTRYPOINT LOGIC
# Checks for NET_ADMIN & NET_RAW capabilities
# Checks for TUN device
# Clears stale PID/socket files to prevent startup failure after unclean shutdowns
# Polls for daemon readiness before proceeding
# Graceful shutdown handler
# Starts as root, does the networking, then HANDS OVER the process to norduser.
ENTRYPOINT ["/bin/bash", "-c", \
"set -e; \
if ! iptables -L -n > /dev/null 2>&1; then echo 'ERROR: Missing capabilities.'; exit 1; fi; \
if [ ! -c /dev/net/tun ]; then echo 'ERROR: /dev/net/tun not found.'; exit 1; fi; \
rm -rf /run/nordvpn && mkdir -p /run/nordvpn && \
chown -R root:nordvpn /run/nordvpn /var/lib/nordvpn && \
chmod 770 /run/nordvpn /var/lib/nordvpn; \
/etc/init.d/nordvpn start && \
timeout 30 bash -c 'until nordvpn status &>/dev/null; do sleep 1; done' && \
trap '/etc/init.d/nordvpn stop; exit 0' SIGTERM SIGINT; \
echo 'Initialization complete. Handing over to norduser...'; \
exec gosu norduser bash -c 'while pgrep nordvpnd > /dev/null; do sleep 5; done; echo \"Daemon exited. Shutting down.\"; exit 1'"]
Remember to update/remove the nordvpn version tag (=4.6.0) to pull the desired/latest linux release.
⚙️ 2. Setup & Build
Create a persistent directory to keep your NordVPN login and Meshnet settings safe across container restarts:
sudo mkdir ~/nordvpn-meshnet/data
Build the nordvpn-docker image (note: remember the dot at the end of the command line):
sudo docker build -t nordvpn-docker .
🚀 3. Deploy the NordVPN Gateway Container
Run the container with the necessary networking permissions.
(Note: For audiobookshelf we map port 13378 on the host to port 80 in the container. Because our app will share this network, it will be accessible via port 80, or specify your preferred port.):
sudo docker run -d \
--name nordvpn-meshnet \
--hostname abs-meshnet \
--restart unless-stopped \
--init \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--device /dev/net/tun:/dev/net/tun \
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
-v ~/nordvpn-meshnet/data:/var/lib/nordvpn \
-p 13378:80 \
nordvpn-docker
[!TIP]
Pro Tip: After starting the NordVPN Docker Container, interact with NordVPN using the following command formatdocker exec -it nordvpn-meshnet nordvpn <COMMAND>(e.g.docker exec -it nordvpn-meshnet nordvpn login --token <YOUR_TOKEN>to login to your NordVPN account using a token).
🔗 4. Link your Application Container (audiobookshelf Example)
In your application’s (audiobookshelf) docker-compose.yml (e.g., ~/audiobookshelf/docker-compose.yml), the "magic" happens with network_mode.
services:
audiobookshelf:
container_name: audiobookshelf
image: ghcr.io/advplyr/audiobookshelf:latest
network_mode: "container:nordvpn-meshnet" # Attach to the NordVPN container
volumes:
# Media directories
- /mnt/media/Audio:/Audio
- /mnt/media/Documents:/Documents
- /mnt/media/Video:/Video
# Application data
- /mnt/media/_SYSTEM/~Audiobookshelf/backups:/Audiobookshelf Backups
- /mnt/media/_SYSTEM/~Audiobookshelf/config:/config
- /mnt/media/_SYSTEM/~Audiobookshelf/metadata:/metadata
environment:
- TZ=America/Denver
- ABS_BIND_ADDRESS=0.0.0.0
restart: unless-stopped
Change the volume directories specified in the docker-compose.yml above to fit your setup.
Make sure all host volume paths exist before creating the audiobookshelf container in the next step.
This docker-compose.yml is a slightly modified version of the one we are instructed to create when following the official audiobookshelf guide for Docker Compose; instead of specifying the ports here, we've bound the application's network identity to the NordVPN container (nordvpn-meshnet), and in step 3 we mapped port 13378 to port 80 (or the one you specified) in the NordVPN container already. Your port mappings may be different depending on the application you are working with; see your application's documentation for more information.
✨ 5. Deploy the Application Container
Run the container: sudo docker-compose up -d
Conclusion & Notes 🎉
The NordVPN Container (nordvpn-meshnet) should now access the audiobookshelf container successfully, hurray!
- 🚫 LAN Access to the audiobookshelf container doesn't work with this setup, but since Meshnet uses the shortest path it can find, it goes through LAN when available. (You can test this by running a
traceroute.) - 🌐 To access audiobookshelf over Meshnet, open the Meshnet device IP (http://x.x.x.x/) or Meshnet device name in your browser from a linked Meshnet device (http://device-name.nord/ or http://device-nickname/), no port specification needed since the Meshnet container is pointing to port 80 now (unless you specified a different port earlier in step 3).
- 🏠 To access audiobookshelf from the local machine it is still http://localhost:13378/.