Tyler's Site

Abstract

Setting up a basic web server is one of the first projects that many people getting started in system administration and Linux take on. There are many good guides on the topic with some of the best ones I’ve seen coming from Vultr; however, not many of them include setting up a web server with HTTP/2 and HTTP/3 support, as well as other security headers and optimizations. So, I am going to throw my guide in the hat as a resource for getting started.

Requirements

Before actually setting up a public web server, there are some things that are needed.

  1. Domain: As many people know, a domain is the name gets associated with IP addresses to make finding resources on the Internet easier. For example, my domain is foxide.xyz, which contains various kinds of records for different protocols.
  2. Hosting: The software and pages that will be served have to live somewhere, and generally this would be publicly accessible if the website is public. There are a few different options for this, but for most people a VPS is going to be the simplest. Though, it is also possible to host the website from home, however, most ISPs aren’t very happy with customers that chose to do so (unless you have a business plan).
  3. Operating System: This one could probably be lumped into hosting, however, picking an operating system is extremely important and there are some notes worth pointing out. The most important one being, use something that you are comfortable with and understand how to use. FreeBSD is a wonderful operating system and runs at least a quarter of the Internet’s traffic (via Netflix), however, I would have a difficult time recommending it to someone that has no experience in a Unix-like system. If you are extremely knowledgeable in an OS, even if it is not generally a server OS like Arch Linux, I would say that is a better choice for YOU than something else. Because when there is an issue, you will be more likely to know how to fix it rather than having to stumble around an unfamiliar environment.

For this blog post, I am going to be using example.com as the domain for our website, and I am going to be going through the process both on Debian and FreeBSD. Nginx will be the web server of choice as it is a fairly popular one, and has support for all of the protocols that we need. I am not going to get into the process of installing the operating system, but if needed this guide will show how to install Debian, and this one will show how to install FreeBSD. The only real recommendation that would potentially go against default install options would be to make sure that the file system is ZFS, or is using LVM in the case of Debian (unless you installed ZFS as the file-system for Debian). Doing one of these will make backups much easier in the future.

Setup

After installing, configuring, and updating your new web server, we need to install anything that we will need for being a web server, namely Nginx. We will, however, need some other tools such as certbot and a good editor.

Installing packages for Debian:

apt install vim nginx certbot

FreeBSD:

pkg install nginx vim-tiny py311-certbot

I will be using vim as the editor, but any other editor can be used; simply replace vim with any other editor and it should be fine. The next step is to enable nginx so that it can deliver web pages. For Debian and other SystemD based distros that looks like:

# Commands should be run as root
systemctl enable nginx
systemctl start nginx

and for FreeBSD:

# Commands should be run as root
sysrc nginx_enable
service nginx start

Then traveling to the IP (or domain name if that is already setup) will show a default nginx page that should look as follows:

INCLUDE

Technically, this is a functioning web server. HTML could be put into the relevant locations, and web pages would become available; however, this is well below the bare minimum for modern websites. We can do better.

DNS Settings

Next, let’s go ahead and setup the domain name to route to the correct IP address; many of the following steps will hinge on this being setup appropriately, so it would be good to have it done now (or sooner).

To set the DNS records, login to the panel for your registrar and edit the domain settings. This is an exercise left to the reader, as each registrar’s website is going to look completely different from each other. For the web page to work we at least need to add an ‘A’ record or a ‘AAAA’ record; adding both record types is ideal as that will cover both IPv4 and IPv6 addresses and thus being reachable by nearly any device that can reach the Internet. The first record type is the ‘A’ record which is for IPv4 addresses The value for the A record should be the public IP address of the web server, to find out what that value is on Debian:

ip addr

1: enp9s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 70:85:c2:f4:22:7d brd ff:ff:ff:ff:ff:ff
    inet 172.245.181.191 brd 172.245.181.255 scope global dynamic noprefixroute enp9s0
       valid_lft 567sec preferred_lft 567sec

and on FreeBSD

ifconfig
vtnet0: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        options=4c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,TXCSUM_IPV6>
        ether 00:16:3c:d3:f3:4e
        inet 172.245.181.191 netmask 0xffffff80 broadcast 172.245.181.255
        media: Ethernet autoselect (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>

The IPv4 address is the number next to ‘inet’ (172.245.181.191 in the example cases), that address needs to be added as an ‘A’ record in your registrar. Once that record propagates throughout the Internet (can take up to 48 hours) then the domain name should point to that IP address. This can be tested by using a simple ping command, or by using nslookup:

# Ping simply checks that the connection can be established
ping example.com
PING example.com (96.7.128.175): 56 data bytes
64 bytes from 96.7.128.175: icmp_seq=0 ttl=51 time=78.648 ms
64 bytes from 96.7.128.175: icmp_seq=1 ttl=51 time=71.742 ms
64 bytes from 96.7.128.175: icmp_seq=2 ttl=51 time=74.054 ms
64 bytes from 96.7.128.175: icmp_seq=3 ttl=51 time=71.280 ms
...

--- example.com ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 71.280/73.931/78.648/2.919 ms

# nslookup will show the DNS information used to find the domain
nslookup example.com

Server:     172.16.0.10
Address:    172.16.0.10#53

Non-authoritative answer:
Name:   example.com
Address: 23.215.0.138
Name:   example.com
Address: 2600:1406:bc00:53::b81e:94ce

My VPS does not have IPv6, however, it would appear as under the ‘inet’ entry with the entry name as ‘inet6’. Similar to the ‘A’ record for the IPv4 address, put that address as a record in your registrar, but the record type should be ‘AAAA’ rather than ‘A’. Similar commands can be used to check the work, nslookup will show both IPv4 and IPv6 information, while ping will need to add the -6 flag to force IPv6.

Server Blocks

Nginx operates with server blocks for each domain (or subdomain) that is being hosted on that instance of nginx. Certbot will use DNS to see if the domain is pointing to the correct domain, then will also check the nginx server blocks to make sure that nginx is prepared to serve web traffic for that domain. Server blocks will usually reside in the HTTP block of the nginx config file, or they will be in separate files to be included in the main config file. By default Debian (as well as many other distros) will encourage creating separate files for each domain or subdomain; the main advantages of this are better separation of files for making changes as well as being able to more quickly find where the relevant configuration will be. The method that Debian uses to do this is to have two directories in the /etc/nginx directory: sites-available and sites-enabled. The sites-available directory is for domains that could be enabled, but are not currently active. On the other hand, the sites-enabled directory are for the currently active sites. To make an available site active, simply create a symbolic link (symlink) to the sites-enabled directory:

# must be run as root
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com

On FreeBSD, this is not setup by default and server blocks could just go within the HTTP block in the main nginx.conf file. However, if you wanted to re-create this setup on FreeBSD (or another system that did not already have it) simply put the following line in your HTTP block in the nginx.conf file:

# File path for most BSD systems
# for Linux systems, remove the /usr/local
include /usr/local/etc/nginx/sites-enabled/*;

Then create the sites-available and sites-enabled directories manually.

Now let’s create a server block for our domain! Below is a very simple example of a server block for the domain ‘example.com’:

server {
    listen 80;
    listen [::]:80;
    server_name example.com;

    location / {
        root /srv/www/example.com
        index index.html index.htm
    }
}

The above example should go into a either go in a file in sites-available then symlinked, or in the HTTP block of nginx.conf. This server block will listen on port 80 (HTTP) for a request for example.com. If an HTTP request is made for that domain on that port, it will serve either index.html or index.htm located in /srv/www/example.com. The config can be tested by running nginx -t; assuming that no errors are returned than nginx can be restarted.

# These commands must have root privileges
# On Debian:
systemctl restart nginx
# On FreeBSD:
service nginx restart

Then typing http://example.com in the URL bar of a web browser should now render one of the index files located in /srv/www/example.com. The next step is setting up our TLS certificates.

Transport Layer Security (TLS)

After the DNS records and the server blocks in nginx have been configured, then we can setup TLS, still often referred to as SSL. The tool for doing this (at least for the average person) is Certot, which is a tool that provides free TLS certificates via LetsEncrypt. Certbot will interface with LetEncrypt to acquire a certificate for a domain, but it will also write the relevant configurations for the domain in the web server (nginx in our case) to enable it. Install the tool if it is not already installed (within the context of this guide, it was installed alongside nginx). Then we simply need to run the following command:

# Commands must be run as root
certbot --nginx

Certbot will look in the nginx configuration file for the server_name deceleration to find which domains to add to the certificate. Alternatively, the domains and subdomains can be manually declared using the -d flag as follows:

# Commands must be run as root
certbot -d example.com,git.example.com,docs.example.com --nginx
# Multiple -d flags can be used instead of commas
certbot -d example.com -d git.example.com -d docs.example.com --nginx

After running one of the two commands, certbot will acquire a TLS certificate, and edit the nginx config to enable it for the relevant domains. The simplest way to confirm that the certificate is installed and valid, simply visit the URL in a browser and look for the lock icon in the URL bar. However, for a command line way to check use the following command:

echo | openssl s_client -connect example.org:443 2>/dev/null | openssl x509 -noout -dates

Firewall Rules

Explaining firewall configurations are well outside of the scope of this already long post. That being said, it is very important to set up some sort of a firewall on the server since it is public facing. In general, the rules should cover at least the following:

There are many different firewall softwares to choose from, however, one of the more popular ones for Linux systems is the Uncomplicated Firewall (UFW) and on FreeBSD PF is the most common firewall.

Nginx Configuration Improvements

At this point, you should have a pretty solid web server setup. However, there are still things that can be done to improve the setup a bit more. The big ticket items are adding support for HTTP/2 and HTTP/3 (and QUIC) as well as enabling some other security features for the domain. The main benefit of HTTP/2 over HTTP/1 is the ability to multiplex requests, meaning that rather than having to request one asset at a time, multiple assets can be requested at a time reducing load times. It also offers things like header compression, prioritization, and binary framing. HTTP/3 and QUIC further improve the protocol by building in TLS (TLS 1.3 specifically) by default, thus reducing connection requests. It also improves congestion control and adds support for connection migration. One of the biggest changes to HTTP/3 over HTTP/2 is that it uses UDP over TCP, and the connection verification as well as packet loss handling are built into QUIC. This is why web servers that want to support QUIC must enable HTTP and HTTPS traffic on UDP as well as TCP.

We are going to start with an example server block that we will build off of in the following sections:

listen example.com:443 ssl;
server_name example.com;

# Other server config stuff below
...

All of the features that we will be working with are added toward the beginning of the server block near the listening and server name sections, so, I am only including that in the example.

HTTP/2 Support

Adding HTTP/2 support is fairly simple, just add http2 on; in the server block as follows:

listen example.com:443 ssl;
server_name example.com;

# Enable HTTP/2 Support
http2 on;

# Other server config stuff below
...

Then restart the nginx service and confirm. The easiest way to confirm is to find an online service that will check for you such as keycdn.com. However, if you want to verify yourself, you can do so by using Firefox’s developer tools. Open the developer tools by pressing CTRL+Shift+I, then reload the page. Click on the line that has the HTML file for the root of the domain, and look for the HTTP version in the box on the right. If you see ‘HTTP/2’, then HTTP/2 support is enabled and working (be mindful that browsers like to cache things, and Firefox may be using a cached version of the website).

HTTP/3 and QUIC Support

Adding support for HTTP/2 is very simple and straightforward, however, HTTP/3 and QUIC are not quite that simple.

# Adding listen address to specificly listen for quic traffic
listen example.com:443 quic reuseport;
listen example.com:443 ssl;
server_name example.com;

# Enable HTTP/3
http3 on;
quic_retry on;
add_header Alt-Svc 'h3=":$server_port"; ma=86400, h3-29=":$server_port"; ma=86400';

# Enable HTTP/2
http2 on;

# Other server config stuff below
...

Extra Security Features

The last items for this post is enabling some extra security headers in Nginx for the site. Namely, HTTP Strict Transport Security (HSTS), Content Security Policy (CSP), and secure headers. These items work together to further reduce threat actors from performing various types of attacks both to the server itself, and to clients that are interacting with the server to boost the overall security of the website. The following configs should be enough to get started for basic sites (such as static sites), but definitely do research as to how these options should be configured for your site. Using things like Firefox’s built-in developer tools are super helpful here.


# Resources
    # Security Header Stuff
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Content-Type-Options nosniff;
        add_header Access-Control-Allow-Origin "${allowed_origins}";
        add_header Access-Control-Allow-Credentials true;
        add_header X-Frame-Options "SAMEORIGIN";
     
        # Opt out of Google's FLoC Network
        add_header Permissions-Policy interest-cohort=();
     
        # Enable SharedArrayBuffer in Firefox (for .xlsx export)
        add_header Cross-Origin-Resource-Policy cross-origin;
        add_header Cross-Origin-Embedder-Policy require-corp;

       # HSTS (ngx_http_headers_module is required) (63072000 seconds)
       add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

       # OCSP stapling
       ssl_stapling on;
       ssl_stapling_verify on;

       # 0-RTT Stuff
       ssl_early_data on;

       # Content Security Policy
       add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline';" always;

    # verify chain of trust of OCSP response using Root CA and Intermediate certs
    ssl_trusted_certificate /usr/local/share/certs/ca-root-nss.crt;

    # replace with the IP address of your resolver
    resolver 9.9.9.9 149.112.112.112 208.67.222.222 208.67.220.220 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1;

Final Thoughts

This post was quite big, but much of it is stuff that is easily found online in one place or another, the idea of this post was putting most everything one would need to get an HTTP/2 and HTTP/3 enabled web server up and running in one place as I have not found that online (at least not yet).

Using online tools to assist in confirming that things are working properly is, in my opinion, the way to go especially for the security headers. The tool I have found and like the most is this one, but your mileage and opinions may vary. I also wanted to note that I did not cover the 0-RTT header because it is not enabled on my web server. Reason for that is that I used OpenSSL, which does not (at the time of writing) support that particular header. If you want that to be enabled, the recommendation in this article is to use boringSSL. This might be worth doing in some cases, but I did not feel it was worth it in mine.