Me: “This weekend I need to update these buildout scripts for Debian 12 and implement a new provider middleware.”
Dog’s expression: “Or we could just go for a walk?”
Me: “Or we could just go for a walk, sure.”
Dan Q
Foundry is a wonderful virtual tabletop tool well-suited to playing tabletop roleplaying games with your friends, no matter how far away they are. It compares very favourably to the market leader Roll20, once you get past some of the initial set-up challenges and a moderate learning curve.
You can run it on your own computer and let your friends “connect in” to it, so long as you’re able to reconfigure your router a little, but you’ll be limited by the speed of your home Internet connection and people won’t be able to drop in and e.g. tweak their character sheet except when you’ve specifically got the application running.
A generally better option is to host your Foundry server in the cloud. For most of its history, I’ve run mine on Fox, my NAS, but I’ve recently set one up on a more-conventional cloud virtual machine too. A couple of friends have asked me about how to set up their own, so here’s a quick guide:
Getting a virtual server is really easy nowadays.
You’ll need:
Choose a root password when you set up your server. If you’re a confident SSH user, add your public key so you can log in easily (and then disable password authentication entirely!).
For laziness, this guide has you run Foundry as root on your new server. Ensure you understand the implications of this.2
DNS propogation can be pretty fast, but… sometimes it isn’t. So get this step underway before you need it.
Your newly-created server will have an IP address, and you’ll be told what it is. Put that IP address into an A-record for your domain.
In my examples, my domain name is vtt.danq.me and my server is at 1.2.3.4. Yours will be different!
Connect to your new server using SSH. Your host might even provide a web interface if you don’t have an SSH client installed: e.g. Linode’s “Launch LISH Console” button will do pretty-much exactly that for you. Log in as root
using the password you chose
when you set up the server (or your SSH private key, if that’s your preference). Then, run each of the commands below in order (the full script is available as a single file if you
prefer).
You’ll need unzip
(to decompress Foundry), nodejs
(to run Foundry), ufw
(a firewall, to prevent unexpected surprises), nginx
(a
webserver, to act as a reverse proxy to Foundry), certbot
(to provide a free SSL certificate for Nginx),
nvm
(to install pm2
) and pm2
(to keep Foundry running in the background). You can install them all like this:
apt update apt upgrade apt install -y unzip nodejs ufw nginx certbot nvm npm install -g pm2
By default, Foundry runs on port 30000. If we don’t configure it carefully, it can be accessed directly, which isn’t what we intend: we want connections to go through the webserver (over https, with http redirecting to https). So we configure our firewall to allow only these ports to be accessed. You’ll also want ssh enabled so we can remotely connect into the server, unless you’re exclusively using an emergency console like LISH for this purpose:
ufw allow ssh ufw allow http ufw allow https ufw enable
Putting the domain name we’re using into a variable for the remainder of the instructions saves us from typing it out again and again. Make sure you type your domain name (that you pointed to your server in step 2), not mine (vtt.danq.me):
DOMAIN=vtt.danq.me
So long as the DNS change you made has propogated, this should Just Work. If it doesn’t, you might need to wait for a bit then try again.
certbot certonly --agree-tos --register-unsafely-without-email --rsa-key-size 4096 --webroot -w /var/www/html -d $DOMAIN
Don’t continue past this point until you’ve succeeded in getting the SSL certificate sorted.
The certificate will renew itself automatically, but you also need Nginx to restart itself whenever that happens. You can set that up like this:
printf "#!/bin/bash\nservice nginx restart\n" > /etc/letsencrypt/renewal-hooks/post/restart-nginx.sh chmod +x /etc/letsencrypt/renewal-hooks/post/restart-nginx.sh
You can, of course, manually write the Nginx configuration file: just remove the > /etc/nginx/sites-available/foundry
from the end of the printf
line to see
the configuration it would write and then use/adapt to your satisfaction.
set +H printf "server {\n listen 80;\n listen [::]:80;\n server_name $DOMAIN;\n\n # Redirect everything except /.well-known/* (used for ACME) to HTTPS\n root /var/www/html/;\n if (\$request_uri !~ \"^/.well-known/\") {\n return 301 https://\$host\$request_uri;\n }\n}\n\nserver {\n listen 443 ssl http2;\n listen [::]:443 ssl http2;\n server_name $DOMAIN;\n\n ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;\n ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;\n\n client_max_body_size 300M;\n\n location / {\n # Set proxy headers\n proxy_set_header Host \$host;\n proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto \$scheme;\n\n # These are important to support WebSockets\n proxy_set_header Upgrade \$http_upgrade;\n proxy_set_header Connection \"Upgrade\";\n\n proxy_pass http://127.0.0.1:30000/;\n }\n}\n" > /etc/nginx/sites-available/foundry ln -sf /etc/nginx/sites-available/foundry /etc/nginx/sites-enabled/foundry service nginx restart
mkdir {vtt,data} cd vtt
Substitute in your Timed URL in place of <url from website>
(keep the quotation marks – "
–
though!):
wget -O foundryvtt.zip "<url from website>" unzip foundryvtt.zip rm foundryvtt.zip
Now you’re finally ready to launch Foundry! We’ll use PM2 to get it to run automatically in the background and keep running:
pm2 start --name "Foundry" node -- resources/app/main.js --dataPath=/root/data
You can watch the logs for Foundry with PM2, too. It’s a good idea to take a quick peep at them to check it launched okay (press CTRL-C to exit):
pm2 logs 0
Provide your license key to get started, and then immediately change the default password: a new instance of Foundry has a blank default password, which means that anybody on Earth can administer your server: get that changed to something secure!
Now you’re running on Foundry!
1 Which currency you pay in, and therefore how much you pay, for a Foundry license depends on where in the world you are
where your VPN endpoint says you are. You might like to plan accordingly.
2 Running Foundry as root is dangerous, and you should consider the risks for yourself. Adding a new user is relatively simple, but for a throwaway server used for a single game session and then destroyed, I wouldn’t bother. Specifically, the risk is that a vulnerability in Foundry, if exploited, could allow an attacker to reconfigure any part of your new server, e.g. to host content of their choice or to relay spam emails. Running as a non-root user means that an attacker who finds such a vulnerability can only trash your Foundry instance.
Earlier this year, for reasons of privacy/love of selfhosting, I moved the DanQ.me mailing list from Mailchimp to Listmonk (there’s a blog post about how I set it up), relaying outbound messages via an SMTP server provided by my domain registrar, Gandi.
Unfortunately, Gandi recently announced that they’ll no longer be providing email services for free, so rather than fork out €4/month for an email address I won’t even read, I decided to inhouse it.
And because I learned a few things while doing so, I wrote this blog post so that next time I have to configure Postfix + DKIM, I’ll know where to find a guide. If it helps you in the meantime, that’s just a bonus.
Running your own mailserver is a pain. I used to do it for all of my email, but – like many other nerds – when spam reached its peak and deliverability became an issue, I gave up and oursourced it1.
Luckily, I don’t need it to do much. I just need a mail transfer agent with an (unauthenticated, but local-only) SMTP endpoint: something that Listmonk can dump emails into, which will then reach out to the mailservers representing each of the recipients and relay them on. A default install of Postfix does all that out-of-the-box, so I ransudo apt install postfix
, accepted all the default
options, and put the details into Listmonk.
Next, I tweaked my DNS configuration to add an SPF record, and tested it. This ought to have been enough to achieve approximate parity with what Gandi had been providing me with. Not bad.
I sent a test email to a Gmail account, where I noticed two problems:
The first problem was that Postfix on Debian isn’t configured by-default to use opportunistic TLS when talking to other
mailservers. That’s a bit weird, but I’m sure there’s a good reason for it. The solution was to add smtp_tls_security_level = may
to my
/etc/postfix/main.cf
.
The second problem was that without a valid DKIM signature on them, about half of my test emails were going straight to the spam folder. Again, it seems that since the last time I seriously ran a mailserver 20 years ago, this has become something that isn’t strictly required… but your emails aren’t going to get through if you don’t.
I’ve put it off this long, but I think it’s finally time for me to learn some practical DKIM.
What’s DKIM, then?
In this way, a recipient can be sure that an email received from a domain was sent with the authorisation of the owner of that domain. Properly-implemented, this is a strong mitigation against email spoofing.
To set up my new server to sign outgoing mail, I installed OpenDKIM and its keypair generator using sudo apt install opendkim
opendkim-tools
. It’s configuration file at /etc/opendkim.conf
needed the following lines added to it:
# set up a socket for Postfix to connect to: Socket inet:12301@localhost # set up a file to specify which IPs/hosts can send through us without authentication and get their messages signed: ExternalIgnoreList refile:/etc/opendkim/TrustedHosts InternalHosts refile:/etc/opendkim/TrustedHosts # set up a file to specify which selector/domain are used to each incoming email address: SigningTable refile:/etc/opendkim/SigningTable # set up a file to specify which signing key to use for each selector/domain: KeyTable refile:/etc/opendkim/KeyTable
Into /etc/opendkim/TrustedHosts
I put a list of local IPs/domains that would have their emails signed by this server. Mine looks like this (in this example I’m using
example.com
as my domain name, and default
as the selector for it: the selector can be anything you like, it only matters if you’ve got multiple
mailservers signing mail for the same domain). Note that 192.168.0.0/16
is the internal subnet on which my sending VM will
run.
127.0.0.0.1 ::1 192.168.0.0/16 *.example.com example.com
/etc/opendkim/SigningTable
maps email addresses (I’m using a wildcard) to the subdomain whose TXT record will hold the public key for the signature. This also goes on to
inform the KeyTable
which private key to use:
*@example.com default._domainkey.example.com
And then /etc/opendkim/KeyTable
says where to find the private key for that:
default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private
Next, we need to create and secure that keypair. In /etc/opendkim/keys/example.com/
, we run:
sudo opendkim-genkey -s default -d example.com sudo chown opendkim:opendkim default.private
At last, we can configure Postfix to filter all mail through OpenDKIM by adding to our /etc/postfix/main.cf
file:
milter_protocol = 2 milter_default_action = accept smtpd_milters = inet:localhost:12301 non_smtpd_milters = inet:localhost:12301
The public key needs publishing via DNS. Conveniently, when you create a keypair using its tools, OpenDKIM provides a sample (in
BIND-style) for you to copy-paste from or adapt: look in /etc/opendkim/keys/example.com/default.txt
!
Once we’ve restarted both services (sudo service postfix restart; sudo service opendkim restart
), we can test it!
So I learned something new today.
If you, too, love to spend your Saturday mornings learning something new, have a look at those subscription options to decide how you’d like to hear about whatever I get up to next.
1 I still outsource my personal email, and I sing the praises of the excellent folks behind ProtonMail.
2 My desktop email client also doubles as my newsreader, because, yes, of course you can still find me on USENET. Which, by the way, is undergoing a mini-revival…
3 Why doesn’t DKIM sign
all the headers in an email? Because intermediary servers and email clients will probably add their own headers, thereby invalidating the signature! DKIM gets used to sign the From:
header, for obvious reasons, and ought to be used for other headers whose tampering could be
significant such as the Date:
and Subject:
, but it’s really up to the signing server to choose a subset.