Starting from Scratch
David 2020-05-14
Building my own birdfeeder
I have a near-pathological tendency to build my own birdfeeder (PDF, page 3). I like building things for myself, rather than using off-the-shelf stuff, even if it's harder. Having recently subscribed to the newsletter The Dork Web and through it stumbled across a series of articles advocating for the "Personal website-verse" and since it's currently Coronapoclaypse, I figured I'd try again to build my own website. This time, though, I've decided to start from as close to absolute-scratch as I possibly can. I'm writing this in a plain HTML file. No WYSIWYG, no Markdown, nothing. At least, not yet anyway. Sometimes the easiest way to figure out what you need is to purposefully deprive yourselves of things and see what hurts the most.
I'm using this as an opportunity to learn things: how to build responsive websites, how to set up a web server, how to write better, and other such things. So far this is just a single index.html file. I want this site to be fast, and easy to read on desktop or mobile, so I'm including CSS from the beginning to make it render in a readable way on most devices (or so I hope). I was particularly inspired by the solar-powered website from Low-Tech Magazine and how they managed to make their site use fewer resources and load more quickly by using some tricks--e.g. dithering their images to make the file sizes smaller without sacrificing the image-heavy aesthetic of the site.
The tools that I'm using to make this site so far are: vim for text editing, with the spell-check turned on, MDN to look up all the web-tech I'll be needing to make this thing look half-way decent, and FreeBSD on a Digital Ocean virtual private server running nginx to serve the site. I've purposefully avoided any dependencies at this stage. I'm not even using any web-fonts, just whatever the default font your operating system uses is. Honestly, on my Arch Linux desktop, it looks fine to me. I notice that I may be overusing hyperlinks, but I figure if you have the ability to cite things and cross-reference things directly in the text, you might as well do so. Oh, also, the colour-scheme is largely taken from Solarized, which is my favourite theme to use when programming. I prefer the "dark" version for writing but the "light" version for reading, so this site's colours are from the "light" version.
The CSS naming convention that I'm trying to use is BEM (Block, Element Modifier which is what I use at work and I've found is pretty good at alleviating the pain of CSS only really having global "variables" (class names and such).
Preparing and Securing the Server
As a paranoid person, the first thing that I do with any new server is attempt to secure it as best I can. On FreeBSD, that includes things like disabling password-based authentication (and requiring key-based authentication), disallowing root SSH login, setting up a firewall, etc. Lest we get ahead of ourselves, we need to create a normal user first (via adduser), adding ourselves the the "wheel" group so that we can su to root, if we need to. We also set a root password.
I've got SSH keys on every computer I own, so it's firstly a matter of getting those, via scp usually, onto the server and into my user's $HOME/.ssh/authorized_keys file. Generally, that looks like this:
$ scp $HOME/.ssh/id_rsa.pub user@server:/home/user/.ssh/my_key
$ ssh user@server
$ cd .ssh
$ cat my_key >> authorized_keys
$ rm my_key
After this most basic setup, it's a good idea to make sure the server's operating system and packages are up-to-date. To do that on FreeBSD we run the following as root (note that you may want to reboot later, after we've done some more configuration, as opposed to immediately):
# freebsd-update fetch install
# pkg update
# pkg upgrade
# reboot
Once that's done, I update the sshd configuration (which lives in /etc/ssh/sshd_config) to include the following:
...
# Disallow password-based login (i.e. require keyfiles to be used)
PasswordAuthentication no
ChallengeResponseAuthentication no
...
# Disallow root from logging in via SSH directly
PermitRootLogin no
...
After that, sshd needs to be restarted on the server via:
# service sshd restart
Assuming I didn't mess anything up, that should make ssh a bit more secure. After this, I set up the firewall. Since the server is just a basic static-site HTTP server, I use IPFW since it's simple to configure. To do this, I add the following to /etc/rc.conf:
firewall_enable="YES"
firewall_quiet="YES"
firewall_type="workstation"
# Allow SSH (22), HTTP (80) and HTTPS (443) traffic in
firewall_myservices="22 80 443"
firewall_allowservices="any"
firewall_logdeny="YES"
These options are explained in a few different, sometimes difficult-to-find, places. The Three main ones (at the top) just enable the firewall and are explained in the handbook. The two "services" options are hard to find official documentation for, but they are explained in /etc/rc.firewall (Side note: one way to figure out what a configuration option means when Google—or DuckDuckGo—is being less-than-helpful is to do a recursive grep in /usr/src). firewall_myservices option is a space-delineated list of ports/protocols that the firewall will let talk to the outside world. The firewall_allowservices option is a list of IP addresses that are allowed to talk to the services in myservices. By giving it the value "any," we are allowing anyone to connect to the ports we've opened. The last option, firewall_logdeny turns on logging of "default denied packets should be logged (in /var/log/security). Finally, we start IPFW via:
# service ipfw start
Now is a good time to add install "sudo" if it's not already installed on the system and add myself to the sudoers file. I install the binary package via:
# pkg install sudo
I can then add myself to the "sudoers" file to allow my regular user to have some access to root privileges. This is done via the visudo command which opens up a text editor (in my case, vi), to edit the sudo config file safely. Since I'm the only user on this machine, I usually just allow the "wheel" group to have sudo access since my user is the only member of the group. For reference, the users in the "wheel" group are permitted to su to root.
%wheel ALL=(ALL) ALL
What this line actually means is: the group "wheel" (the percent-sign means "group" vs "user") may, on ALL (the first "ALL") hosts, run as "ALL" things like users, groups (the second "ALL"), "ALL" commands (the third "ALL"). Basically, it gives people in the "wheel" group root access.
The last thing we set up would, up until recently, have haven fail2ban. Now, FreeBSD has a built-in equivalent called "blacklistd." What these programs do is to blacklist IPs that make too many failed connection attempts to, for example, SSH. This is done in order to beef up the security of your server by disallowing bad actors from accessing it. We start by adding blacklistd to the /etc/rc.conf and then starting it via:
# sysrc blacklistd_enable="YES"
# service blacklistd start
Since we're using IPFW, we need to tell blacklistd about this. I'm not sure where this is properly documented, but you can discover it via looking at /usr/libexec/blacklistd-helper. Anyway, we need to do the following to tell blacklistd that we use IPFW:
# touch /etc/ipfw-blacklist.rc
Note, incidentally, that blacklistd is documented in the handbook.
The next thing we do is to make sure OpenSSH (aka "sshd") is configured to use blacklistd. Within /etc/ssh/sshd_config we enable the following option:
UseBlacklist yes
We then restart sshd (as above) to make OpenSSH pick up the new configuration.
The last thing we do is to make sure the sever knows what time it is. To set up the local time zone and then set up ntpd to sync the time properly, run:
# tzsetup
# sysrc ntpd_enable="YES"
# sysrc ntpd_sync_on_start="YES"
# service ntpd start
Setting Up the Web Server (Nginx)
Obviously, the first thing we need to do to get our Nginx web server up and running is to actually install Nginx. We do that via the binary packaging system:
# pkg install nginx
Before we configure nginx, we create a directory for the site's data to live in. We do that as follows:
# mkdir -p /usr/local/www/site
# chown -R www /usr/local/site
# chgrp -R www /usr/local/site
On FreeBSD, user-installed software is typically (e.g. by pkg) installed to to /usr/local directory, which has a directory structure that roughly looks like the root directory structure. As such, the nginx configuration file lives at /usr/local/etc/nginx/nginx.conf. The changes we make are as follows:
...
user www; # FreeBSD has a "www" user on the system by default
worker_processes 1; # Set to the number of cores you have, I only have 1
...
http {
...
gzip on; # Turn on compression
...
server {
listen 80;
server_name example.com; # Use your domain name here
...
server_tokens off; # Avoid leaking version number
...
location / {
root /usr/local/www/site;
...
Website Deployment
We've got nginx configured now, so all there is left to do is to teach nginx about the website's content so that it can serve said content. In the spirit of "starting from scratch" I'm going to do that in the least efficient but most instructive possible way. Since the site is just a single HTML file at the moment, this is relatively simple. On my home machine, I run the following:
$ scp index.html user@server:/home/user
Then, I ssh into the server and run the following:
$ sudo mv index.html /usr/local/www/site
$ sudo chown www /usr/local/www/site/index.html
$ sudo chgrp www /usr/local/www/site/index.html
Finally, we can start nginx (and restart it every boot) as follows:
# sysrc nginx_enable="YES"
# service nginx start
TLS & HTTPS: Encryption
It's arguable that a completely static site doesn't need encryption, but non-https websites are now beginning to be shown as "insecure" in search engines, so we're probably going to need encryption. Fortunately, unlike the situation only a few years ago, we can now get an SSL certificate cost-free. This is because of Let's Encrypt.
The Electronic Frontier Foundation (EFF) has released an application called Certbot to automte use of Let's Encrypt. Handily, Cerbot has customized guides for various operating systems and web server combinations. We'll be using the one for FreeBSD and Nginx.
We intall and run certbot to get a certificate as follows (note that we need to stop nginx before we do this because cerbot temporarily runs its own web server on port 80):
# service nginx stop
# pkg install py37-certbot
# certbot certonly --standalone
We'll need to update the nginx configuration to use TLS and use our new certificate. We need to tell nginx to use TLS (SSL) and then tell it where the certificates live. We'll also use an HTTP 301 ("Moved Permanently") response to all normal HTTP requests so that those requests are served over HTTPS. We will also enable HTTP Strict Transport Security (HSTS) to teach browsers to only ever use HTTPS. The max-age is in seconds and is equal to 1 year.
...
# We add this to make nginx always server HTTPS vs HTTP
server {
listen 80;
server_name example.com;
listen 443 ssl;
ssl_certificate /usr/local/nginx/ssl/nginx.crt;
ssl_certificate_key /usr/local/nginx/ssl/nginx.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
if ($scheme != "https") {
return 301 https://$host$request_url;
}
...
We can set up our certificate to auto-renew via cron. Per the cerbot guide, we add the following to /etc/crontab
0 0,12 * * * root python3.7 -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q --pre-hook 'service nginx stop' --post-hook 'service nginx start'
The above means: At minute 0, hour 0/12 (i.e. midnight), every day of the month, every month of the year, every day of the week, wait a random amount of time between 0 and 3600 seconds (1 hour), stop nginx, renew the certificate, and restart nginx.
Finally, we start nginx again via sudo service nginx start. Now our website is complete and visible to the world!
Addendum: Git
I use text files for almost everything because it makes things far easier to version-control and I use Git for that. Let's put our website in a git repository and use the web server as the remote repository. We first need to install git on our server via sudo pkg install git. Then, on our local machine:
$ cd /your/website
$ git init
$ git add .
$ git commit -m 'Initial commit'
$ cd ..
$ git clone --bare website website.git
$ scp -r website.git user@server:/home/user
$ rm -rf website.git
$ cd website
$ git remote add origin ssh://user@website:/home/user/website.git
$ git branch --set-upstream-to=origin/master