# Installing wagon Before installing wagon, the server needs to be set up with the following basic services: - wireguard - bind9 - openssl ca Theoretically, wireguard, bind9, and wagon could all live in docker containers, or none of them. If wagon is in docker but either wireguard or bind9 *aren't*, then the wagon container has to use "host" network mode. Almost all commands in this guide need to be run as root/admin. ## 1. Wireguard On the server, [install wireguard](https://www.wireguard.com/install/). Then modify and run these commands to set some variables: ```sh # Choose a short lowercase name for the network net_name=mynet # Choose a number (between 2 and 254) for the network net_num=99 # Find an unused UDP port between 1024 and 65535 srv_listenport=58395 ``` Create the wireguard interface ```sh srv_privkey="$(wg genkey)" psk="$(wg genpsk)" ip link add dev "${net_name}" type wireguard ip addr add dev "${net_name}" "10.${net_num}.0.1/8" echo "${srv_privkey}" | wg set "${net_name}" listen-port "${srv_listenport}" private-key /dev/stdin ip link set up dev "${net_name}" ``` Get the server's public key and psk (copy the output of these command) ```sh wg pubkey <<<"${srv_privkey}" XXXXXXXXXXXXXX echo "${psk}" YYYYYYYYYYYYYY ``` Now on your first client, install wireguard and set these variables: ```sh net_name=mynet # Match what's on the server net_num=99 # Match what's on the server srv_listenport=58395 # Match what's on the server srv_pubkey='XXXXXXXXXXXXXX' # The public key we copied above psk='YYYYYYYYYYYYYY' # The psk we copied above public_ip='1.2.3.4' # The server's public IP address ``` Create the interface on the client and add the server as a peer: ```sh our_privkey="$(wg genkey)" ip link add dev "${net_name}" type wireguard ip addr add dev "${net_name}" "10.${net_num}.1.1/8" echo "${our_privkey}" | wg set "${net_name}" private-key /dev/stdin \ peer "${srv_pubkey}" allowed-ips "10.0.0.0/8" endpoint "${public_ip}:${srv_listenport}" persistent-keepalive 25 ip link set up dev "${net_name}" ``` Now grab the client's pubkey: ```sh wg pubkey <<<"${our_privkey}" ZZZZZZZZZZZZZZ ``` Go back to the server and add the client as a peer: ```sh our_pubkey='ZZZZZZZZZZZZZZ' # From the client wg set "${net_name}" peer "${our_pubkey}" allowed-ips "10.${net_num}.1.1/32" ``` Make sure the client can ping the server with `ping 10.${net_num}.0.1` and the server can ping the client with `ping 10.${net_num}.1.1`. If that's not working, post your error message on the matrix channel. If it is working, get a cup of coffee because the next section is a doozy. ## 2. bind9 These instructions are adapted from [Digital Ocean: How To Configure BIND as a Private Network DNS Server on Ubuntu 22.04 (2022-08)](https://www.digitalocean.com/community/tutorials/how-to-configure-bind-as-a-private-network-dns-server-on-ubuntu-22-04) On the server, open a root shell and [install bind9 nameserver](https://kb.isc.org/docs/aa-00648). Set some variables to make the rest easier. ```sh tld='mynet' # The same as $net_name (see below) net_num='99' # Match what's used with wireguard public_ip='1.2.3.4' # The server's public IP address, as above ``` The `$tld` should match the `$net_name`, which is the wireguard interface name. Probably using a different `$tld` could be possible in a future version. Ok, let's configure bind. In older documentation, you'll see reference to `named.conf`. In newer versions of bind, this file just includes `named.conf.options` and `named.conf.local`. We will follow this convention too. If you're already running a nameserver, you can add these configurations alongside your existing settings. **`/etc/bind/named.conf.options`** ```named.conf // Access control lists acl "mynet_acl" { // change 99 to $net_num 10.99.0.0/16; }; acl "intervpn_acl" { 10.0.0.0/8; }; options { // This is bind's default location for zonefiles // Just make sure you include it in backups. directory "/var/cache/bind"; // https://serverfault.com/a/381923 notify explicit; // Add listen-on-v6 if using IPv6 // Listen on your public IP too if you are // also running a public nameserver listen-on { 127.0.0.1; // localhost 10.99.0.1; // VPN }; // Whom we provide nameservice to // https://tldp.org/HOWTO/DNS-HOWTO-6.html#ss6.2 recursion yes; allow-recursion { localhost; localnets; intervpn_acl; // or mynet_acl to be more restricitve }; // Enable DNSSEC validation for forwarded queries dnssec-validation auto; // Keep these settings restricted and change them // per-zone in named.conf.local. allow-transfer { none; }; allow-query { localhost; localnets; }; // If we don't know a domain, forward to these nameservers // A good list of public nameservers by country: // https://dnschecker.org/public-dns forwarders { 1.1.1.1; 8.8.8.8; }; }; ``` That wasn't so bad, was it? Now we'll set the per-zone settings. wagon requires reverse DNS so stop complaining and just do it. I spent a while searching for a good tool to turn IP addresses into their respective rDNS domains before I realized that `nslookup` does that. `nslookup` and `nsupdate` should have been installed with bind; if not, install them. Ok, let's use nslookup to grab the rDNS domain for our IP: ```sh nslookup 10.99.0.1 ** server can't find 1.0.99.10.in-addr.arpa: NXDOMAIN ``` Now we know that to provide rDNS for all the IPs in `10.99.0.0/16`, we must serve on the entire zone `99.10.in-addr.arpa`. Do this for IPv6 if needed to get a zone ending in `.ip6.arpa`. **`/etc/bind/named.conf.local`** ```named.conf // We'll create these keys in the next step include "/etc/bind/keys/admin.key"; include "/etc/bind/keys/wagon.key"; // mynet is your tld zone "mynet" { type master; // "file" is relative to the "directory" we // set in named.conf.options file "mynet.db"; allow-query { localhost; localnets; intervpn_acl; // or mynet_acl to be more restricitve }; // This should be include slave servers AND // Any machine that will be running wagon allow-transfer { localhost; 10.99.0.1; }; // This should be set to slave servers also-notify { localhost; }; // Here we give two keys nsupdate permissions update-policy { grant admin zonesub ANY; grant wagon zonesub ANY; }; }; // The rDNS zone we got from nslookup zone "99.10.in-addr.arpa" { type master; file "10.99.db"; // relative to "directory" // The next settings can be copied verbatim from above allow-query {localhost; localnets; intervpn_acl; }; allow-transfer { localhost; 10.99.0.1; }; update-policy { grant admin zonesub ANY; grant wagon zonesub ANY; }; }; // Any existing zones will live happily alongside // zone "example.com" { ``` Excellent. This file referenced four files that don't exist yet and must be created. Let's start with the keys. `nsupdate` uses symetric keys, so one copy will live on the bind server and the other will be copied to the nsupdate client. As you can see from the first lines of this file, I like to keep my keys in `/etc/bind/keys`. Let's create this directory and the two keys named above. Actually you should rename the "admin" key as your username and give a different key to each admin. "wagon" is of course the key our future dashboard will use to update the nameserver. ```bash mkdir /etc/bind/keys tsig-keygen -a hmac-sha512 admin >/etc/bind/keys/admin.key tsig-keygen -a hmac-sha512 wagon >/etc/bind/keys/wagon.key chown -R root:bind /etc/bind/keys chmod 750 /etc/bind/keys chmod 640 /etc/bind/keys/*.key cat /etc/bind/keys/admin.key ``` Copy the key we just catted and paste it into an `admin.key` file on your pc. Now you will be able to modify DNS records by running `nsupdate -k admin.key` on your PC. When we set up wagon, we'll copy the value from `wagon.key` into the wagon config. But let's not get ahead of ourselves, we still have one more thing to do with bind: create the zonefiles. Firstly, let's assume some variables: - Our server's $HOSTNAME is `hn` and it's domain name will be `hn.mynet` - The single wireguard client we set up at `10.99.1.1` is going to get the domain name `pc.myuser.mynet`. Start with fDNS: **`/var/cache/bind/mynet.db`** ```bind $ORIGIN . $TTL 604800 ; 1 week mynet IN SOA hn.mynet. myuser.mynet. ( 2 ; serial 604800 ; refresh (1 week) 86400 ; retry (1 day) 2419200 ; expire (4 weeks) 604800 ; minimum (1 week) ) NS hn.mynet. $ORIGIN mynet. ; Record for our servers hn A 10.99.0.1 *.hn CNAME hn.mynet. ; Record for our user $ORIGIN myuser.mynet. pc A 10.99.1.1 *.pc CNAME pc.myuser.mynet. ``` In the SOA line, there are two values that require explanation: - **`hn.mynet.`** is the default server `nsupdate` will send updates to. Of course it's our VPN domain, not a public domain name; we don't accept nsupdates from the internet. - **`myuser.mynet.`** is actually the email `myuser@mynet` and should be set to the server admin. If you want to use a public email address, you can set it to something like `hostmaster.example.com.` for `hostmaster@example.com`. Now do one for rDNS (same thing goes for the SOA line here): **`/var/cache/bind/10.99.db`** ```bind $ORIGIN . $TTL 604800 ; 1 week 99.10.in-addr.arpa IN SOA hn.mynet. myuser.mynet. ( 2 ; serial 604800 ; refresh (1 week) 86400 ; retry (1 day) 2419200 ; expire (4 weeks) 604800 ; minimum (1 week) ) NS hn.mynet. ; Server records $ORIGIN 0.99.10.in-addr.arpa. 1 PTR hn.mynet. ; User records $ORIGIN 1.99.10.in-addr.arpa. 1 PTR pc.myuser.mynet. ``` Easy. Now start the nameserver and check that it doesn't throw any errors: ```bash systemctl start named systemctl enable named systemctl status named ``` If it's not working, fix it and then go back to your pc and check the lookups. ```bash nslookup pc.myuser.mynet 10.99.0.1 nslookup hn.mynet 10.99.0.1 nslookup 10.99.0.1 10.99.0.1 nslookup 10.99.1.1 10.99.0.1 ``` Each of these commands uses `10.99.0.1` as the nameserver by setting it as the second argument; you can also make that your default nameserver or the nameserver for the `mynet` TLD. Look into setting "search domains" for your VPN interface in your operating system. `systemd-resolved` users, for example, can run these commands: ```bash resolvectl dns mynet 10.99.0.1 resolvectl domain mynet '~mynet' '~99.10.in-addr.arpa' ``` This will tell the OS to send `.mynet` queries to our vpn nameserver. Not all programs respect this setting though; `dig`, `ping`, and your browser will work but you'll still have to set the nameserver by hand for `nslookup` (as above) and `nsupdate` using the "server" command (even though we set it in our SOA): ```bash nsupdate -k admin.key > server 10.99.0.1 > add test.mynet 86400 TXT "hello" > delete test.mynet TXT > send > quit ``` ## 3. Certificate authority The last major step is to set up the certificate authority. Unlike wireguard and bind, this won't require running some background service; we just generate a few files and keep them safe. A good place to keep your SSL certs and keys is in `/etc/ssl/private/mynet`. Let's make things easier by setting some variables: ```bash tld='mynet' crt_dir="/etc/ssl/private/${tld}" ca_key="${crt_dir}/_ca.key" ca_crt="${crt_dir}/_ca.crt" ``` Now we'll create the ca key and cert. You will be asked for some details about your organization; put whatever you want. You'll also be asked to create a passphrase: create and store one using the most secure methods! You'll need this passphrase for the `wagon` config later. Here we're setting `-days 3650` which will require re-signing and re-distributing the certificate every ten years. You can avoid that by setting it to 100 years with `-days 36500`. This field is required but I think there is no limit, so you can set it to `99999999` if you want. ```bash openssl genrsa -des3 -out "${ca_key}" 4096 openssl req -x509 -new -nodes -key "${ca_key}" -sha256 -days 3650 -out "${ca_crt}" ln -s "${ca_crt}" "/etc/ssl/certs/${tld}.pem" ``` The last step makes the cert available to verification from the host OS. This cert file, `/etc/ssl/private/mynet/_ca.crt` should be shared with everyone who will be accessing your network. One easy way to do this is to serve it on your public website at `https://www.example.com/ca.crt` so users can easily download it. It must be added to every user's OS and/or browser. How this is done will depend on the OS and browser... so you should provide instructions to your users! A sample of such instructions can be found at [www.gf4.pw/nebuchadnezzar/ca/](https://www.gf4.pw/nebuchadnezzar/ca/). We can use these CA files to sign certificates for hosts using our `mynet` domain. Let's sign one for the server first: ```bash org='My Cool Network' tld=mynet host=hn domain="${host}.${tld}" crt_dir="/etc/ssl/private/${tld}" host_dir="${crt_dir}/${host}" ca_crt="${crt_dir}/_ca.crt" ca_key="${crt_dir}/_ca.key" ips='IP:10.99.0.1' # Create a subdirectory for the host's files mkdir -p "${host_dir}" # Generate the host's key openssl genrsa -out "${host_dir}/server.key" 2048 # Set certificate configuration # If /etc/ssl/openssl.cnf doesn't exist, look for # openssl.cnf somewhere in your openssl installation cat /etc/ssl/openssl.cnf \ <(printf "\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain},${ips}\n") \ >"${host_dir}.cnf" # Now we'll create the certificate signing request openssl req -new -sha256 -reqexts SAN \ -key "${host_dir}/server.key" \ -config "${crt_dir}/${host}.cnf" \ -subj "/O=${org}/OU=${host}/CN=${domain}" \ -out "${crt_dir}/${host}.csr" # Finally, sign the certificate # This will request the CA passphrase set previously # Set -days to whatever you want using the tips above openssl x509 -req -sha256 -extensions SAN \ -CAcreateserial -days "3650" -CA "${ca_crt}" -CAkey "${ca_key}" \ -in "${crt_dir}/${host}.csr" \ -extfile "${crt_dir}/${host}.cnf" \ -out "${host_dir}/server.crt" ``` That should do it! Let's check that the cert is valid for all domains and IPs: ```bash openssl x509 -text -noout -in "${host_dir}/server.crt" | grep -A1 'Subject Alternative Name' ``` That should return something like: ```bash X509v3 Subject Alternative Name: DNS:hn.mynet, DNS:*.hn.mynet, IP Address:10.99.0.1 ``` It contains our domain, wildcard domain, and IP address. Since everything went well, we can delete the CSR and cnf file: ```bash rm -f "${crt_dir}/${host}.csr" "${crt_dir}/${host}.cnf" ``` One last thing: we need to generate a certificate and key for our pc. Everything is basically the same as with the server, except that our domain will be `pc.myuser.mynet` instead of `hn.mynet`. So let's breeze through this and check the comments from above if you get confused. ```bash org='My Cool Organization' tld=mynet host='pc.myuser' domain="${host}.${tld}" crt_dir="/etc/ssl/private/${tld}" host_dir="${crt_dir}/${host}" ca_crt="${crt_dir}/_ca.crt" ca_key="${crt_dir}/_ca.key" ips='IP:10.99.1.1' days=3650 mkdir -p "${host_dir}" openssl genrsa -out "${host_dir}/server.key" 2048 cat /etc/ssl/openssl.cnf \ <(printf "\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain},${ips}\n") \ >"${host_dir}.cnf" openssl req -new -sha256 -reqexts SAN \ -key "${host_dir}/server.key" \ -config "${crt_dir}/${host}.cnf" \ -subj "/O=${org}/OU=${host}/CN=${domain}" \ -out "${crt_dir}/${host}.csr" openssl x509 -req -sha256 -extensions SAN \ -CAcreateserial -days "3650" -CA "${ca_crt}" -CAkey "${ca_key}" \ -in "${crt_dir}/${host}.csr" \ -extfile "${crt_dir}/${host}.cnf" \ -out "${host_dir}/server.crt" openssl x509 -text -noout -in "${host_dir}/server.crt" | grep -A1 'Subject Alternative Name' rm -f "${crt_dir}/${host}.csr" "${crt_dir}/${host}.cnf" ``` You might be thinking, this would all be easier as a script. A script that could add clients to wireguard and bind, then generate and server the ssl files. This is what `wagon` is designed to do. ## 4. Wagon I keep services in `/srv` so I would do: ```sh cd /srv git clone https://gitea.gf4.pw/gf4/wagon.git cd wagon ``` ### 4.1. Configuration Copy the sample environment file and docker-compose file: ```sh cp etc/config.sample etc/config cp etc/servers.sample etc/servers cp docker-compose.yml.sample docker-compose.yml ``` Configure the `docker-compose.yml` file however you like, or don't use it at all. The other two files are tab-separated text files. Lines starting with a hash (`#`) are ignored as comments The `etc/servers` file is a list of servers on the `/16` network. For now, just set our single server with the correct variables. ```tsv # host ipv4 ipv6 pubkey wg-endpoint admin-endpoint secret hn 10.99.0.1 XXXX XXXXX= 1.2.3.4:51820 https://wagon-admin.hn.mynet XXXXXX ``` We're just gonna leave `XXXX` as a placeholder for ipv6 since we aren't using it. But do set the pubkey to hn's wireguard public key from above. Set admin-endpoint to whatever you want right now; this is actually used for server-to-server communication, not administration. Same thing for secret: leave it as `XXXXXX` or generate something random; in any case it isn't used unless your network has multiple servers. Now edit the `etc/config` file ```sh TLD='mynet' LOCAL_SERVER='hn' IPV4_NET='10.11.0.0/16' IPV6_NET='fd69:1337:0:420:f4:11::/96' WG_DNS='DNS=10.11.0.1' SSL_CONFIG_DIR="/etc/ssl/private/${TLD}" SSL_CA_CERT="${SSL_CONFIG_DIR}/_ca.crt" SSL_CA_KEY="${SSL_CONFIG_DIR}/_ca.key" SSL_ORG='My Cool Organization' SSL_DAYS='3650' SSL_CA_PASS='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' DNS_KEY='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==' DNS_MASTER='10.3.0.1' DNS_TTL='86400' ``` This file should be mostly self-explanitory. "SSL_CA_PASS" is the CA key passphrase created in the last section. The "DNS_KEY" can be found in the "secret" section of the `/etc/bind/keys/wagon.keys` file, which looks like this: ```tsig key "wgapi-ksn" { algorithm hmac-sha512; secret "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=="; }; ``` Wagon comes as 4 services: 1. An api users can access to add/delete hosts 2. An api admins can access to add/delete hosts and users 3. A frontend for the user dashboard 4. A frontend for the admin dashboard The two frontends were built with knockoutJS and html and are very bare (no css) as they are packaged, but you can easily incoporate them in your existing web portal's design. There is no login (authentication is IP-based) so the frontend works fine on static sites. For now, there's no authentication for the admin dashboard and maybe there never will be (out-of-scope). It runs on a different port, so simply set firewall and web proxy rules for whatever authentication configuration you like. With that in mind, let's boot up the two API servers. This guide assumes the use of docker and docker-compose, but you can run everything outside docker too. You just need to host the `dashboard.cgi` script on one endpoint and `admin.cgi` on another. The `back/dashboard.Dockerfile` and `back/admin.Dockerfile` files can be a guide to doing so with apache2. If you *are* using docker, you should be able to `touch /var/log/wagon.log` and run `docker-compose up` from the wagon directory. This should make the user API available on `localhost:4442` and the admin API on `localhost:4441`. That's not bad. We could take requests on that port, but let's take secure https requests on a subdomain instead. With `nginx`, this would work: **`/etc/nginx/sites-enabled/wagon.conf`** ```nginx # User API server { server_name wagon-dashboard-api.hn.mynet; listen 10.11.0.1:443 ssl http2; ssl_certificate /etc/ssl/private/mynet/hn/server.crt; ssl_certificate_key /etc/ssl/private/mynet/hn/server.key; ssl_stapling off; allow 10.11.0.0/16; # All users deny all; # Everyone else location / { proxy_pass http://localhost:4442; } } # Admin API server { server_name wagon-admin-api.hn.mynet; listen 10.11.0.1:443 ssl http2; ssl_certificate /etc/ssl/private/mynet/hn/server.crt; ssl_certificate_key /etc/ssl/private/mynet/hn/server.key; ssl_stapling off; allow 10.11.1.0/24; # One admin allow 10.11.7.0/24; # Another admin deny all; # Everyone else location / { proxy_pass http://localhost:4441; } ``` Our frontends are going to need these APIs. At the top of `front/dashboard.js` is a hardcoded variable: ```js const API_URL = 'https://wg-dashboard-backend.myhost.mytld' ``` Set that to the nginx proxy virtual host we just set: ```js const API_URL = 'https://wagon-dashboard-api.hn.mynet' ``` Or use direct http: ```js const API_URL = 'http://localhost:4442' ``` Do likewise in `front/admin.js` and set the `TLD` too: ```js const API_URL = 'https://wagon-admin-api.hn.mynet' // or const API_URL = 'http://localhost:4441' const TLD = 'mynet' ``` The frontend should work now, though it could use a bit of design work or implementation in your website. That's the whole installation, phew! Take a break. When you come back, start learning how to [use wagon](USAGE.md).