Skip to content

Titre : [Enhancement] Add optional, modular firewall service for namespace security #16

@TLavocat

Description

@TLavocat

First, thank you for this excellent and very useful project.

I've been using it to run services inside the namespace. I discovered that any service running in the namespace that binds to 0.0.0.0 (like a WebUI on port 8080) is publicly exposed on the VPN's public IP address.

This is because a new network namespace has a default iptables policy of INPUT ACCEPT. This is a significant security risk, as users may (like I did) inadvertently expose sensitive services to the entire internet without realizing it.

Proposed Solution

The project should provide a "secure-by-default" posture. I propose adding an optional, modular firewall service that follows the same systemd logic as the existing interface and tunnel services.

This new service (namespaced-wireguard-vpn-firewall.service) will apply iptables rules inside the namespace. Its key features are:

  1. It depends on both the interface and tunnel services, ensuring it only runs after both rezine and veth-vpn1 are UP.
  2. It sets the default INPUT policy to DROP.
  3. It explicitly allows traffic on lo, RELATED,ESTABLISHED, and any user-defined ports.
  4. (Optional but recommended) It's configurable via the .conf file.

Implementation Details

Here are the files I created to solve this, which could be adapted for the project:

1. New script: /usr/sbin/namespaced-wireguard-vpn-firewall

This script reads variables from the .conf file and applies the rules.

#!/bin/bash

# Load config

if [ -f "/etc/namespaced-wireguard-vpn/namespaced-wireguard-vpn.conf" ]; then
source "/etc/namespaced-wireguard-vpn/namespaced-wireguard-vpn.conf"
fi

# --- Ports to open ---

# (These could also be loaded from the .conf file)

VETH_TCP_IN="8080" # e.g., WebUI ports (from host)
VETH_UDP_IN=""
VPN_TCP_IN="44651"  # e.g., P2P ports (from public)
VPN_UDP_IN="44651"

VPN_IFACE="$WIREGUARD_NAME"
VETH_IFACE="$TUNNEL_VPN_NAME"

if [ -z "$NETNS_NAME" ] || [ -z "$VPN_IFACE" ] || [ -z "$VETH_IFACE" ]; then
echo "Firewall Error: Core variables (NETNS_NAME, WIREGUARD_NAME, TUNNEL_VPN_NAME) not set." >&2
exit 1
fi

netns_exec="ip netns exec $NETNS_NAME"

case "$1" in
up)
echo "Applying firewall to namespace '$NETNS_NAME'..."

    # Wait for interfaces to have the <...UP...> flag
    while ! $netns_exec ip link show dev $VPN_IFACE | grep -q "<.*UP.*>"; do sleep 0.2; done
    while ! $netns_exec ip link show dev $VETH_IFACE | grep -q "<.*UP.*>"; do sleep 0.2; done
    # 1. Set default policy to DROP
    $netns_exec iptables -P INPUT DROP
    $netns_exec iptables -P FORWARD DROP
    $netns_exec iptables -P OUTPUT ACCEPT # Allow outgoing

    # 2. Flush old rules
    $netns_exec iptables -F INPUT

    # 3. Allow essentials
    $netns_exec iptables -A INPUT -i lo -j ACCEPT
    $netns_exec iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

    # 4. Open ports on VETH (from host)
    for port in $(echo $VETH_TCP_IN | tr ',' ' '); do
        [ -n "$port" ] && $netns_exec iptables -A INPUT -i $VETH_IFACE -p tcp --dport $port -j ACCEPT
    done
    for port in $(echo $VETH_UDP_IN | tr ',' ' '); do
        [ -n "$port" ] && $netns_exec iptables -A INPUT -i $VETH_IFACE -p udp --dport $port -j ACCEPT
    done

    # 5. Open ports on VPN (from public)
    for port in $(echo $VPN_TCP_IN | tr ',' ' '); do
        [ -n "$port" ] && $netns_exec iptables -A INPUT -i $VPN_IFACE -p tcp --dport $port -j ACCEPT
    done
    for port in $(echo $VPN_UDP_IN | tr ',' ' '); do
        [ -n "$port" ] && $netns_exec iptables -A INPUT -i $VPN_IFACE -p udp --dport $port -j ACCEPT
    done

    echo "Firewall rules applied. INPUT policy is now DROP."
    $netns_exec iptables -L INPUT -n -v
    ;;

down)
    echo "Resetting firewall for namespace '$NETNS_NAME'..."
    $netns_exec iptables -P INPUT ACCEPT
    $netns_exec iptables -P FORWARD ACCEPT
    $netns_exec iptables -F INPUT
    ;;
*)
    echo "Usage: $0 {up|down}"
    exit 1
    ;;
esac

2. New service: /etc/systemd/system/namespaced-wireguard-vpn-firewall.service

This service unit ensures the firewall is the last network item to run.

[Unit]
Description=VPN Namespace Firewall
PartOf=namespaced-wireguard-vpn.target

# CRITICAL: Wait for BOTH interfaces to be created

Requires=namespaced-wireguard-vpn-interface.service
Requires=namespaced-wireguard-vpn-tunnel.service
After=namespaced-wireguard-vpn-interface.service
After=namespaced-wireguard-vpn-tunnel.service

[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=/etc/namespaced-wireguard-vpn/namespaced-wireguard-vpn.conf
ExecStart=/usr/sbin/namespaced-wireguard-vpn-firewall up
ExecStopPost=/usr/sbin/namespaced-wireguard-vpn-firewall down

3. Modify: /etc/systemd/system/namespaced-wireguard-vpn.target

Add the new firewall service as a dependency for the main target.

 [Unit]
 Description=VPN Network Configuration Target
 Requires=namespaced-wireguard-vpn-interface.service
 Requires=namespaced-wireguard-vpn-netns.service
 Requires=namespaced-wireguard-vpn-tunnel.service
+Requires=namespaced-wireguard-vpn-firewall.service

[Install]
WantedBy=multi-user.target

Final Recommendation for Users

With this change, downstream services should no longer depend on netns.service. They should depend on the main target to ensure the firewall is active before they start.

Example for /etc/systemd/system/qbittorrent.service.d/10-vpn-netns.conf:

 [Unit]
-BindsTo=namespaced-wireguard-vpn-netns.service
-After=namespaced-wireguard-vpn-netns.service
+BindsTo=namespaced-wireguard-vpn.target
+After=namespaced-wireguard-vpn.target
 JoinsNamespaceOf=namespaced-wireguard-vpn-netns.service

[Service]
PrivateNetwork=yes

This seems like a robust and secure addition to the project. What do you think?

Thanks !

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions