Previously, we’ve seen how to get a wildly popular and disruptive Italian dictionary website back online. This article represents the next installment of a series of articles that the renowned author Kilgore Trout likened to the successor of Harry Potter.
We’ll going to look at the following issues:
Installing a certificate for multiple domains and subdomains
It’s time for me to come clean. During this electrifying series, I’ve led you, dear audience, to believe that only benjamintoll.com and the subdomain italy.benjamintoll.com was hosted on my server.
However, this isn’t true, it’s also hosting another site, theowlsnest.farm. I can only hope with the passage of time that you’ll learn to forgive me.
Anyway, like the previous two, theowlsnest.farm is configured as a service and is part of the same Docker cluster. In addition, it is part of the same bridge network, and most importantly, is verified using the same DV certificate issued by the Let’s Encrypt certificate authority.
Neat.
It’s very easy to add this domain with the others on the same certificate, even though it’s a different base domain from benjamintoll.com.
For example, let’s take another look at how the original certificate was issued. For reference, I discussed this in the previous article of the series, and in it you can see the shell script and the command that was used to generate the cert.
Here’s the command:
$ ./create_cert.sh \
-d benjamintoll.com \
-d www.benjamintoll.com \
-d italy.benjamintoll.com \
-e btoll@example.com \
-p
The only problem is that a new certificate needs to be created anytime a new virtual server is added (i.e, an additional domain or subdomain). Of course, this isn’t a big deal because the certificates are free, but you may run into rate limits!
$ ./create_cert.sh \
-d benjamintoll.com \
-d www.benjamintoll.com \
-d italy.benjamintoll.com \
-d theowlsnest.farm \
-d www.theowlsnest.farm \
-d theowlsnestfarm.com \
-d www.theowlsnestfarm.com \
-e benjam72@yahoo.com \
-p
All traffic to the
theowlsnest.farmandtheowlsnestfarm.comdomains and theirwww.subdomains is proxied to the same location.
That’s it! Not too shabby!
In fact, since the location and names of the certs shouldn’t change and they’re being mounted into the container, all I need to do after generating the certificate is just restart cluster:
$ docker-compose down
$ docker-compose up -d
So, we’re able to generate a certificate to allow for many various values to be associated with it through the Subject Alternative Name (SAN) X.509 extension. Just to be clear, this isn’t a feature of the Let’s Encrypt CA but a feature of the format of public key certificates.
You can also extract information about the certificate using the x509 subcommand of the openssl tool. The openssl-x509 man page has a lot of great examples to get you started.
For example, to view the values of the SAN extension, issue the following command:
$ sudo openssl x509 -in cert.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name:
DNS:benjamintoll.com, DNS:italy.benjamintoll.com, DNS:theowlsnest.farm, DNS:theowlsnestfarm.com, DNS:www.benjamintoll.com, DNS:www.theowlsnest.farm, DNS:www.theowlsnestfarm.com
Of course, you can also view the details of the certificate in the browser.
Cleaning up the nginx configuration
Now comes one of the fun parts: refactoring the nginx configuration. Currently, all of the server blocks have been dumped unceremoniously in conf.d/default.conf, and while this works, it makes reading and understanding harder than it should be.
For example, here is the nginx.conf file that was installed during the installation:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
We’ll see in a bit how much of the duplicated code can be moved into this block, thereby greatly simplifying the overall configuration.
Before we do, though, note the last directive before the closing brace:
include /etc/nginx/conf.d/*.conf;
This is what allows configurations to be parsed nginx. Any configurations it finds will be included here in the outermost http block, and this allows the server blocks defined in conf.d/ to inherit from the default values in the http block.
In addition, each server block can contain one or more location blocks.
Here is the mental model:
http {
...
server {
...
}
server {
...
location {
}
}
server {
...
location {
}
location {
}
location {
}
}
}
For the refactor, there are two things that should be addressed:
- There should be some way to separate the
serverblocks by domain. - There are a lot of duplicate configurations in the
serverblocks with regards to TLS.
Before we change anything, though, it’s necessary to first understand some fundamentals. This will help inform us as to the best way to proceed.
-
Inherited global default values should be placed in the outermost
httpblock. For my install, this block can be found in thenginx.confconfiguration file in/etc/nginx/. -
These values are inherited by the inner
serverblocks. Their precedence, however, is higher, so the global default can be easily overridden for any particular customization. -
Enabling the
includedirective mentioned before allows us to put as many config files inconf.das we want, and they are parsed in alphabetical order. The only stipulation is that the extension be.conf, although you could have itincludeany directory and file extension that you’d like.
Bear in mind that I’m only touching on some of the functionality of the nginx web server here.
With these key points in mind, let’s do some refactoring.
Currently, the only configuration in conf.d/ is default.conf. In that file, there are server blocks including:
-
The only block that listens on port 80. This rewrites any requests to use TLS. In addition, it contains a
locationblock to handlecertbotchallenge requests for certificate renewals. -
Blocks for the
benjamintoll.comdomain. This includes theitalysubdomain and redirects for thewwwsubdomain. -
Blocks for the
theowlsnest.farmdomain. This includes redirects for thewwwsubdomain. -
Blocks for the
theowlsnestfarm.comdomain. This includes redirects for thewwwsubdomain.
Here is the file structure on disk:
$ tree conf.d/
conf.d/
├── benjamintoll.conf
├── default.conf
├── theowlsnest.conf
└── theowlsnestfarm.conf
The name of each config file is the domain and allows for easy understanding and configuration lookup. Each file is parsed in alphabetical order and added to the bottom of the outer http server block at the bottom where the include directive is located.
We’ll just take a look at one of the files. No matter the file, though, it’s been stripped down to include the minimal config necessary, since all of the server blocks with the exception of one, contained the same duplicate configuration, which was moved to the http block to become global default configurations.
conf.d/theowlsnest.conf
server {
server_name www.theowlsnest.farm;
listen 443 ssl http2;
listen [::]:443 ssl http2;
return 301 https://theowlsnest.farm$request_uri;
}
server {
server_name theowlsnest.farm;
listen 443 ssl http2;
listen [::]:443 ssl http2;
location / {
proxy_pass http://owlsnestfarm;
}
}
Hopefully, this is self-explanatory.
Next, we’ll look at the nginx.conf file to see where all of the duplicated configs now reside.
nginx.conf
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
gzip_disable "msie6";
# This only hides nginx version.
server_tokens off;
security_headers on;
# OCSP stapling.
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4;
# TLS.
ssl on;
ssl_buffer_size 8k;
ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
ssl_dhparam /etc/ssl/certs/dhparam-4096.pem;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
ssl_session_tickets off;
ssl_certificate /etc/letsencrypt/live/benjamintoll.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/benjamintoll.com/privkey.pem;
include /etc/nginx/conf.d/*.conf;
}
Prior to the refactor, all the security header and TLS directives had existed in each server block. That’s no bueno! Now, it resides in only one file and is inherited by every internal server block.
It makes sense to put the TLS directives globally, since we obviously want to use TLS everywhere. The only server block that doesn’t use it is the default block that rewrites the requests to use https. In that block, I’ve simply turned off TLS by including the directive:
ssl off;
It’s that easy.
Conclusion
By now, most developers should have heard of the Let’s Encrypt CA and its goal of securing the Internet. As I’ve shown here and in other articles, it’s very easy to generate a certificate to identify your domains and encrypt your traffic. There’s no reason not to be using it.
In addition, although I’ve only gone into a small portion of what nginx allows you to do, it’s enough to get a fundamental understanding of how the configuration files and their containing blocks work to leverage the idea of global default values that can be overridden “locally” in nested server blocks.
This powerful pattern allows for simple configurations that quickly, at a glance, allow one to understand its intent. And refactoring the configurations out of one monolithic config file into “namespaced” server blocks further hastens understanding and eases maintainability.