wagon/INSTALL.md

21 KiB

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. Then modify and run these commands to set some variables:

# 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

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)

wg pubkey <<<"${srv_privkey}"
XXXXXXXXXXXXXX
echo "${psk}"
YYYYYYYYYYYYYY

Now on your first client, install wireguard and set these variables:

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:

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:

wg pubkey <<<"${our_privkey}"
ZZZZZZZZZZZZZZ

Go back to the server and add the client as a peer:

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)

On the server, open a root shell and install bind9 nameserver. Set some variables to make the rest easier.

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

// 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:

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

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

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

$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

$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:

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.

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:

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):

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:

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.

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

We can use these CA files to sign certificates for hosts using our mynet domain. Let's sign one for the server first:

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:

openssl x509 -text -noout -in "${host_dir}/server.crt" | grep -A1 'Subject Alternative Name'

That should return something like:

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:

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.

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:

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:

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.

#	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

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:

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

# 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:

const API_URL = 'https://wg-dashboard-backend.myhost.mytld'

Set that to the nginx proxy virtual host we just set:

const API_URL = 'https://wagon-dashboard-api.hn.mynet'

Or use direct http:

const API_URL = 'http://localhost:4442'

Do likewise in front/admin.js and set the TLD too:

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.