Skip to content

Least Privilege & User Handover Release - v1.2.2

Choose a tag to compare

@colvdv colvdv released this 09 May 05:58
8bbcca0

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 norduser and group norduser. This user is added to the nordvpn group, allowing for a more secure execution environment.
  • Added gosu: Installed gosu to handle the graceful transition from root (necessary for network configuration) to a non-privileged user for the main process execution.
  • Permission Hardening: Added explicit chown and chmod commands within the entrypoint to manage permissions for /run/nordvpn and /var/lib/nordvpn, ensuring the nordvpn group has the necessary access.

🛠 Improvement & Logic Changes

  • Updated Healthcheck: The HEALTHCHECK now executes the nordvpn status command via gosu 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 norduser call.
    • The container now actively monitors the nordvpnd process via pgrep to handle daemon exits more responsively.
  • Metadata: Updated the org.opencontainers.image.version label to 1.2.2.

📦 Dependency Updates

  • Added gosu to the apt-get install layer.

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 format docker 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/.