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 behn.mynet
- The single wireguard client we set up at
10.99.1.1
is going to get the domain namepc.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 servernsupdate
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 emailmyuser@mynet
and should be set to the server admin. If you want to use a public email address, you can set it to something likehostmaster.example.com.
forhostmaster@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:
- An api users can access to add/delete hosts
- An api admins can access to add/delete hosts and users
- A frontend for the user dashboard
- 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.