# vpn-router A Debian package that configures a Linux host or virtual machine as a VPN router: - site-to-site IKEv2 IPSec with pre-shared key authentication (strongSwan swanctl) - road-warrior (P2S) access using IKEv2 EAP-TLS, with a bundled certificate authority - OpenVPN (P2S) access with a certificate and a password per user, the users kept in a local LDAP directory and managed with `vpn-router-user` or the web service - optional WireGuard endpoint - routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules - firewall rules for the ports Docker publishes, when Docker is installed Nothing in the package is specific to a cloud provider. It is built on the external and internal NIC names, and a `mode` setting says how much of the rest you are supplying and how much is read from the system. Detection happens only in the mode that asks for it, so a router never quietly guesses which interface to NAT out of unless you told it to. Platform-specific additions live in separate modules and are selected explicitly. Installing the package configures nothing by default. The shipped platform is `none`, which means the files are laid down and the machine is left alone until you say otherwise. ## Installing The package can be installed by hand or as a step in automated provisioning. Both are supported equally, and both end up with the same two things: a configuration file and a service that applies it. ``` /etc/vpn-router/vpn-router.json the configuration systemctl restart vpn-router-setup apply it ``` If the host runs Docker, the recommended order is Docker first and the package second. The package secures the ports Docker publishes when it finds the Docker service; the steps for a host that already runs Docker, and for installing Docker later, are under [Docker](#docker). ### By hand ```sh apt-get install vpn-router # answer "none" to the platform question $EDITOR /etc/vpn-router/vpn-router.json systemctl restart vpn-router-setup ``` Answering `none` to the platform question defers everything: no further questions are asked and nothing on the machine is configured. The only change an install makes is the package's own `/etc/sysctl.d/` drop-in, which enables IP forwarding and relaxes `rp_filter` as a router needs. Set `platform` in the file when you are ready. ### Automated Preseed the answers and the router comes up configured, with no follow-up command. Note that `platform` must be something other than `none`, and `mode` says how much you are supplying: ```shell debconf-set-selections <<'EOF' vpn-router vpn-router/platform select generic vpn-router vpn-router/mode select manual vpn-router vpn-router/external_interface string eth0 vpn-router vpn-router/internal_interface string eth1 vpn-router vpn-router/int_addr string 10.1.1.4 vpn-router vpn-router/int_gateway_ip string 10.1.1.1 vpn-router vpn-router/local_fqdn string router.example.com vpn-router vpn-router/local_cidrs string 10.0.0.0/24 vpn-router vpn-router/remote_addrs string peer.example.net vpn-router vpn-router/remote_id string peer.example.net vpn-router vpn-router/remote_cidrs string 192.168.0.0/24 vpn-router vpn-router/psk password s3cr3t EOF DEBIAN_FRONTEND=noninteractive apt-get install -y vpn-router ``` With mode `interfaces` the `int_addr` and `int_gateway_ip` lines are dropped and read from the internal NIC instead; with mode `auto` the interface names go too. Alternatively write `/etc/vpn-router/vpn-router.json` directly before or after installing. A configuration-management tool should do that and then restart `vpn-router-setup`. See [examples/](examples/) for a cloud-init template and a shell installer, and [examples/azure/](examples/azure/) / [examples/gcp/](examples/gcp/) for full Terraform examples that deploy a router on Azure or GCP. ## Configuration `/etc/vpn-router/vpn-router.json` is JSON, mode 0600, and is created on first install. It is managed with `ucf`, so it is yours to edit: a later `dpkg-reconfigure` applies what changed and asks before touching anything you altered by hand. A complete example is installed at `/usr/share/doc/vpn-router/vpn-router.json.example`. The file is an object of sections, each an object of settings. A section or setting that is left out takes its default, and an empty string or empty list means unset. Every value must have the type shown below; an unknown section or setting is an error. | Section | Setting | Type | Default | Meaning | |---|---|---|---|---| | `general` | `platform` | string | `none` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely | | `general` | `mode` | string | `manual` | How much you supply: `manual`, `interfaces` or `auto` | | `interfaces` | `external` | string | | Name of the NIC facing the untrusted network | | `interfaces` | `internal` | string | | Name of the NIC facing the protected network | | `wan` | `local_fqdn` | string | | This router's name or IP address, used as its IKE identity in `fqdn` mode | | `wan` | `local_id_mode` | string | `fqdn` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` | | `local` | `cidrs` | list | | Local subnets advertised into the tunnel | | `local` | `int_addr` | string | | This host's address on the internal network | | `local` | `int_gateway_ip` | string | | Next hop on the internal side | | `remote` | `addrs` | list | | Remote gateway addresses or FQDNs | | `remote` | `id` | string | | Remote IKE identity, an FQDN or an IPv4 or IPv6 address | | `remote` | `cidrs` | list | | Remote subnets reachable through the tunnel | | `remote` | `psk` | string | | Pre-shared key | | `remote` | `psk_file` | string | | Path to a file holding the key; wins over `psk` | | `p2s` | `enabled` | boolean | `false` | Road-warrior access | | `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients | | `p2s` | `ca_name` | string | `VPN Router CA` | Common name for a locally created CA | | `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint | | `wireguard` | `address` | string | | Address and prefix for `wg0`, for example `192.168.200.1/24` | | `wireguard` | `listen_port` | integer | `51820` | UDP port | | `openvpn` | `enabled` | boolean | `false` | OpenVPN access for users | | `openvpn` | `address_pool` | string | | IPv4 CIDR assigned to OpenVPN clients | | `openvpn` | `port` | integer | `1194` | Port of the OpenVPN server on the external interface | | `openvpn` | `protocol` | string | `udp` | `udp` or `tcp` | | `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules | | `docker` | `exposed_ports` | list | | TCP ports and port ranges (`8000:8100`) reachable from the external interface; at most 15, a range counting as two | | `web` | `enabled` | boolean | `true` | The status web service on the internal address | | `web` | `port` | integer | `8080` | TCP port of the status web service | | `web` | `jwt_secret` | string | | Secret, at least 32 characters, that signs the tokens for `/config`; without it `/config` is not served | A minimal site-to-site configuration in `auto` mode: ```json { "general": { "platform": "generic", "mode": "auto" }, "wan": { "local_fqdn": "router.example.com" }, "local": { "cidrs": ["10.0.0.0/24"] }, "remote": { "addrs": ["peer.example.net"], "id": "peer.example.net", "cidrs": ["192.168.0.0/24"], "psk": "s3cr3t" } } ``` The router's IKE identity (`wan.local_fqdn` in `fqdn` mode) and the remote one (`remote.id`) are an FQDN or an IPv4 or IPv6 address. They are written to the strongSwan configuration as given; strongSwan reads an address as an IP identity and a name as an FQDN, so a leading `@` is not needed and is refused. Road-warrior and OpenVPN access need a name in `wan.local_fqdn`. WireGuard peers are added by hand in `/etc/wireguard/wg0.conf`. The package owns the `[Interface]` section of that file and rewrites it when the settings above change, but it carries every `[Peer]` section across untouched. Every feature is independent. Leaving a group empty simply means that feature is not configured, and its generated files are removed. ### What `mode` decides `mode` states how much of the network configuration you are supplying, and therefore how much the package reads from the system: | `mode` | You provide | The package works out | |---|---|---| | `manual` | both interface names, `int_addr`, `int_gateway_ip` | nothing | | `interfaces` | both interface names | `int_addr` and `int_gateway_ip`, from those interfaces | | `auto` | nothing | the interface names too: the external one from the routing table, the internal one as the only other NIC with an address | `manual` is the only mode in which no detection code runs at all. `auto` is the convenient one and can pick the wrong interface, which is a trade you make deliberately by choosing it rather than something the package does behind your back. Detection ignores virtual interfaces, those the kernel lists under `/sys/devices/virtual`: loopback, bridges such as `docker0`, veth pairs and tunnels. Only the NICs of the machine count, so Docker does not change the result. If the host has more than two NICs with an address, `auto` stops and says so; use mode `interfaces` and name them. The addresses strongSwan binds on always come from the external NIC, in every mode, so they are never configured twice and cannot drift out of step with the host. If a named interface does not exist the service stops and says so. In `interfaces` and `auto` mode the internal gateway is taken from the routing table, falling back to the first host address of the connected subnet with a warning when the routing table has nothing to say. ## Docker Docker publishes container ports in its own firewall rules, which bypass UFW and can expose ports on the external interface. Docker evaluates the `DOCKER-USER` chain before its own rules, so the package filters there. With `docker.mode` set to `auto`, the package secures Docker when `docker.service` is installed, and does nothing otherwise. ### On a host that already runs Docker Installing Docker first is the recommended order. After installing the package: 1. Set `general.platform` in `/etc/vpn-router/vpn-router.json`. While it is `none`, nothing is configured, the Docker rules included. 2. List the TCP ports the running containers must expose to the internet, for example `"docker": { "exposed_ports": ["443"] }`. The default is none: once the rules are applied, every published port that is not listed is unreachable from the external interface, and remains reachable through the IPsec tunnel. 3. Apply the configuration with `systemctl restart vpn-router-setup`. Docker is running, so this also restarts `vpn-router-docker`, which installs the rules. 4. Check the result: ```sh iptables -S DOCKER-USER # "-j VPN-ROUTER-DOCKER" is the first rule iptables -S VPN-ROUTER-DOCKER # the rules of the external interface ``` ### When Docker is installed after the package The Docker rules are not applied when Docker starts for the first time, so run `systemctl restart vpn-router-setup` after installing Docker, then check as above. A reboot or a restart of Docker applies them as well. The rules sit in their own chain, `VPN-ROUTER-DOCKER`, entered by one jump at the top of `DOCKER-USER`; other rules in `DOCKER-USER` are left alone. For traffic that arrives on the external interface, in this order: 1. IPsec protected traffic is accepted. 2. TCP to the ports in `docker.exposed_ports` is accepted; none by default. 3. Packets of established and related connections are accepted. 4. Everything else is dropped. Traffic from every other interface, including the internal one and the Docker bridges, is not affected. The rules cover IPv4 only. `vpn-router-setup` starts after `docker.service`, so Docker is up when it looks for it; the ordering has no effect on a host without Docker. The `vpn-router-docker` service applies the rules after `docker.service` starts and removes them when Docker stops, so restarting Docker restores them. `systemctl restart vpn-router-setup` applies a changed configuration while Docker runs. With `docker.mode` set to `on`, the service fails when Docker is not installed or its daemon is not running; with `off` it removes the rules. ## Applying changes ```sh systemctl restart vpn-router-setup ``` The service also runs at boot. With platform `none` it does nothing and says so. Otherwise it regenerates the configuration files, resolves the interfaces and their addresses as `mode` directs, applies routes, generates WireGuard keys and injects firewall rules. It provisions the user directory in `slapd` while OpenVPN or road-warrior access is configured. It then starts `strongswan`, `wg-quick@wg0` and `openvpn-server@vpn-router` if the corresponding feature is configured, reloading or restarting them only when their inputs changed, and restarts `systemd-resolved` only when the road-warrior DNS drop-in changed. Turning a feature off in the configuration stops its service and removes its files. Every step is idempotent, so restarting with no changes does nothing. To regenerate the configuration files and PKI without starting, stopping or reloading anything: ```sh /usr/lib/vpn-router/configure ``` This does still write to the system: it renders into `/etc/swanctl/`, `/etc/systemd/resolved.conf.d/` and `/etc/wireguard/`, and if road-warrior access is enabled with an empty PKI directory it creates the certificate authority and adds it to the host trust store. What it leaves alone is routes, firewall rules and services - that is what `systemctl restart vpn-router-setup` adds. ## Certificates `/etc/vpn-router/pki` is the PKI directory, laid out the way `simple-ca` expects: ``` ca_cert.pem ca_key.pem ca_bundle.pem