wagon/INSTALL.md

590 lines
21 KiB
Markdown

# 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).