Adding Name-based hosting To Nginx on OpenBSD with Acme-Client

Now that my OpenBSD.Amsterdam VPS is up and running, and I have working backups, I thought I'd migrate some static sites over to this host and free up another dedicated server I'm using. Adding extra static HTML won't add to the VPS' general load and won't introduce new risks to #Chargen.One.

To do this, I need to implement name-based Virtual Hosting. I'm going to show how this is done for one site, hackingforfoodbanks.org, then build upon it for multiple hosts. Finally, I'll modularize elements of the configuration to make things more manageable, including HTTPS support.

To make Name-based virtual hosting work, it's necessary to update /etc/acme-client.conf, the DNS Records for the domain in question, and the nginx configuration.

Moving DNS

This is the simplest part of the job. It's simply a case of logging into a DNS provider, and pointing the relevant DNS records at the HTTP server. Log into the DNS provider or server, point the relevant 'A' and/or 'CNAME' records to the HTTP server's IP address, and be prepared to wait up to 24 hours.

Now DNS is out of the way, the next thing is to clean up the nginx config from earlier.

Segregating the Nginx config

The config as-is is fine for just hosting Chargen.One but could get a bit unwieldy if I move all of my static sites across. I created a subdirectory in /etc/nginx/ called sites, into which I can add server blocks for each site I want to host. This splits the configuration up into more manageable per-site blocks.

Before adding a new host, I split out the default chargen.one site config into a new file, /etc/nginx/sites/default.conf. This is a copy of the main /etc/nginx/nginx.conf site with everything from the openings server{ to closing } characters included. It looks like this:

server {
	listen       80 default_server;
	listen       [::]:80 default_server;
	server_name  _;
	root         /var/www/htdocs/c1;
	
	include acme.conf;
	
	#access_log  logs/host.access.log  main;
	#error_page  404              /404.html;
	
	# redirect server error pages to the static page /50x.html
	error_page   500 502 503 504  /50x.html;
	location = /50x.html {
	    root  /var/www/htdocs/c1;
	}
	
	# For reading content
	location ~ ^/(css|img|js|fonts)/ {
	        root /var/www/htdocs/c1;
	        # Optionally cache these files in the browser:
	        # expires 12M;
	}

	
	location ~ ^/.well-known/(webfinger|nodeinfo|host-meta) {
	    proxy_set_header Host $host;
	    proxy_set_header X-Real-IP $remote_addr;
	    proxy_set_header X-Forwarded-For $remote_addr;
	    proxy_pass http://127.0.0.1:8080;
	    proxy_redirect off;
	}
	
	location ~ ^/(css|img|js|fonts)/ {
	    root /var/www/htdocs/c1;
	    # Optionally cache these files in the browser:
	    # expires 12M;
	}
		
	location /{
	    proxy_set_header Host $host;
	    proxy_set_header X-Real-IP $remote_addr;
	    proxy_set_header X-Forwarded-For $remote_addr;
	    proxy_pass http://127.0.0.1:8080;
	    proxy_redirect off;
	}
}

# HTTPS server
#
server {
	listen       443 default_server;
	server_name  _;
	root         /var/www/htdocs/c1;
	include /etc/nginx/acme.conf;
	
	ssl                  on;
	ssl_certificate      /etc/ssl/chargen.one.fullchain.pem;
	ssl_certificate_key  /etc/ssl/private/chargen.one.key;
	ssl_session_timeout  5m;
	ssl_session_cache    shared:SSL:1m;
	ssl_ciphers  HIGH:!aNULL:!MD5:!RC4;
	ssl_prefer_server_ciphers   on;
	
	location ~ ^/.well-known/(webfinger|nodeinfo|host-meta) {
	    proxy_set_header Host $host;
	    proxy_set_header X-Real-IP $remote_addr;
	    proxy_set_header X-Forwarded-For $remote_addr;
	    proxy_pass http://127.0.0.1:8080;
	    proxy_redirect off;
	}
		
	location ~ ^/(css|img|js|fonts)/ {
	    root /var/www/htdocs/c1;
	    # Optionally cache these files in the browser:
	    # expires 12M;
	}
		
	location / {
	    proxy_set_header Host $host;
	    proxy_set_header X-Real-IP $remote_addr;
	    proxy_set_header X-Forwarded-For $remote_addr;
	    proxy_pass http://127.0.0.1:8080;
	    proxy_redirect off;
	}
}

With that entire block removed from the main config, below the line server_tokens off;, there's just the following remaining in /etc/nginx/nginx.conf:

include /etc/nginx/sites/*.conf;

If I want to disable a site, I change the file extension from .conf to .dis and restart nginx. That way I can easily see which sites are enabled and which sites aren't without having to mess with the ln command or symbolic links.

Adding a new virtual host

The first host is the hardest, but onces up and running provides a template for any future hosts. I keep things fairly minimal, but adding support for PHP-based sites is as simple as copying from the default OpenBSD nginx config. The TLS config still points to the chargen.one certificate as only the certificate's associated hostnames change, not the filename.

    server {
        listen       80;
        server_name  hackingforfoodbanks.org www.hackingforfoodbanks.org;
        root         /var/www/htdocs/hackingforfoodbanks;

        include /etc/nginx/acme.conf;
        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root  /var/www/htdocs/hackingforfoodbanks;
        }

        location / {
            try_files $uri $uri/ =404;
            # Optionally cache these files in the browser:
            # expires 12M;
        }

    }

    # HTTPS server
    #
    server {
        listen 443;
        server_name  hackingforfoodbanks.org www.hackingforfoodbanks.org;
        root         /var/www/htdocs/hackingforfoodbanks;
        include /etc/nginx/acme.conf;

        ssl                  on;
        ssl_certificate      /etc/ssl/chargen.one.fullchain.pem;
        ssl_certificate_key  /etc/ssl/private/chargen.one.key;

        ssl_session_timeout  5m;
        ssl_session_cache    shared:SSL:1m;

        ssl_ciphers  HIGH:!aNULL:!MD5:!RC4;
        ssl_prefer_server_ciphers   on;

	location / {
	    root /var/www/htdocs/hackingforfoodbanks;
	    # Optionally cache these files in the browser:
	    # expires 12M;
	}

}

The only major differences are the removal of default_server in the listen directives, the changes to server_name and root to point to the correct spot and the removal of all of the dynamic parts associated with Chargen.One. Check whether or not there are problems with the nginx config before restarting by using the following command:

nginx -t -c /etc/nginx/nginx.conf

Providing the syntax is ok, restart nginx with rcctl restart nginx as root, or via doas.

Adding domains to acme-client

The final part of the puzzle is to add LetsEncrypt support for the new domain. The easiest way to add domains to acme-client is through the alternative names feature. Here's what I've added to /etc/acme-client.conf in order to support the hackingforfoodbanks.org URL.

alternative names { hackingforfoodbanks.org www.hackingforfoodbanks.org }

After adding that, and deleting the existing /etc/ssl/chargen.one.crt file, acme-client can be called to add the new domain.

rm /etc/ssl/chargen.one.crt
acme-client -vFAD chargen.one

Note that the alternative names for our new domains are under the chargen.one domain section. The domain section name is passed to acme-client, not the domain itself.

With a fully functioning certificate and nginx setup, run rcctl restart nginx to finish things off, and test the new site in a browser.

Adding HTTPS redirects

You might want to redirect some of your sites to HTTPS rather than serve a HTTP version of your site. While often touted as a panacea, this introduces a mix of advantages and drawbacks.

I'm not saying don't use HTTPS for a static site. There is no harm in supporting both, especially for a static web site. Just consider the site's audience and make a reasoned, deliberate decision as to whether or not to support accessing your content over HTTP before proceeding.

This site is accessible over HTTP and HTTPS precisely so users of older systems can still access the content via the reader, but authenticated access only works over HTTPS, and no mixed content is loaded.

As people accessing hackingforfoodbanks.org may not have access to current technology (e.g. foodbank users), I made a conscious decision to leave HTTP access open. For another site, rawhex.com, there's less of a requirement to leave HTTP access open, so I'll redirect that to HTTPS.

It's always annoying when a doc doesn't show the whole config for something complicated, so here's the /etc/nginx/sites/rawhex.conf file in full:

    server {
        listen       80;
        server_name  rawhex.com www.rawhex.com;
        return 301 https://$server_name$request_uri;

    }

    # HTTPS server
    #
    server {
        listen 443;
        server_name  rawhex.com www.rawhex.com;
        root         /var/www/htdocs/www.rawhex.com;
        include /etc/nginx/acme.conf;

        ssl                  on;
        ssl_certificate      /etc/ssl/chargen.one.fullchain.pem;
        ssl_certificate_key  /etc/ssl/private/chargen.one.key;

        ssl_session_timeout  5m;
        ssl_session_cache    shared:SSL:1m;

        ssl_ciphers  HIGH:!aNULL:!MD5:!RC4;
        ssl_prefer_server_ciphers   on;

        # add HSTS header to ensure we don't hit the redirect again
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;


        location / {
            root /var/www/htdocs/www.rawhex.com;
            # Optionally cache these files in the browser:
            # expires 12M;
        }

}

The HTTP 301 redirect shrinks the rest of the block to almost nothing. The HSTS header is a way to ensure that once redirected, a browser will only make requests over HTTPS, even if the user clicks on a HTTP link. The end result is an A+ score from Qualys' SSL Labs. There are things that can be done to improve the score, but these come at the cost of compatibility with older browsers and Operating Systems such as Windows Vista and 7.

Modularizing further

You might've noticed in the above that I'm repeating a lot of SSL settings. For HTTPS sites, it's best to keep things consistent. As such I've moved my ssl settings (aside from HSTS) into a separate file, /etc/nginx/https.conf. This means I only have to change one file for all HTTPS site configs. The current version of my file looks like this:

        ssl                  on;
        ssl_certificate      /etc/ssl/chargen.one.fullchain.pem;
        ssl_certificate_key  /etc/ssl/private/chargen.one.key;

        ssl_session_timeout  30m;
        ssl_session_cache    shared:SSL:2m;

        ssl_ciphers  HIGH:!aNULL:!MD5:!RC4;
        ssl_prefer_server_ciphers   on;

I set a higher SSL Session timeout and cache for performance purposes. People should be able to use a single SSL session to cover a full visit to and around the site. People rarely spend longer than 30 minutes there unless they leave a tab open, at which point I'm happy to reinitialize.

Please don't confuse SSL Sessions with HTTP or application sessions. They're different things. If in doubt, the defaults are probably fine.

Now all I have to do is add include /etc/nginx/https.conf; below include /etc/nginx/acme.conf; to my sites, and any changes to ciphers or timeouts will be picked up systemwide with a single change.

Conclusion

Now that I can add static sites to the Chargen.One system, I'll migrate the rest of my content over. With a clean, modular nginx config, content is served speedily and thanks to OpenBSD, to a level of security I'm comfortable with. I still need to find somewhere to move my git repos to, and I'm not sure chargen.one is right for that, but roman has a few ideas that I might borrow from.