Blog

Let's Encrypt SAN Certificate, Nginx-Proxy and Docker

I’ve used Let’s Encrypt certificates to properly secure my internal services. Previously my setup was a Windows IIS running automatic renewal and sharing all certificate files on a NAS share. This worked really well from a Let’s Encrypt perspective, however since most of my services are not Windows services, the manual pain of converting certificates into the proper formats resulting in expired certificates in several places. Since I use NGINX to proxy some external services, I decided to also implement my Let’s Encrypt process on NGINX.

There are several solutions available, however I chose Jason Wilder’s setup https://github.com/jwilder. The base is an nginx-proxy image which can be combined with an autoupdating service Let’s Encrypt as well as dynamic reloading of the configuration.

nginx-proxy sets up a container running nginx and docker-gen. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.

Let’s Encrypt SAN Certificate

Previously I was generating unique certificates for each service, however this time I chose to generate SAN (Subject Alternate Name - Multi Domain) certificates for the sake of laziness. I now get one physical certificate which I use on my internal services. SAN certificates are not supported by older browsers but works really well otherwise.

nginx-proxy

All the heavy lifting has been done for use, we just need to follow the guide and configure it.

https://github.com/jwilder/nginx-proxy

http://jasonwilder.com/blog/2014/03/25/automated-nginx-reverse-proxy-for-docker/

Install Docker-Compose

I chose to use Docker-Compose due to it being easier to keep track of configuration, especially since we are using several interdependant containers.

I had to download and install docker-compose on my host, since I was running the micro version of VMWare’s Photon image.

1
2
curl -L https://github.com/docker/compose/releases/download/1.12.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

Configure docker-compose.yml

The docker-compose.yml file is versioned in the gist: https://gist.github.com/mry/b0a2d80e151343fd01061f78fc634f49

My current setup ships the logs to a Splunk instance using the HTTP Event Listener and built in Docker logging. This does however bump the docker version requirements to 1.13.0.

The certificate files will be placed in one folder. Make sure to use your foremost hostname first since it will default to use that name.

I have a DNS setting to reference my external DNS (hosted by CloudFlare) since the names maps to internal ip:s inside my network. Whenever I add a new internal host, I must update both the internal DNS and the external DNS. I have a simple port-forward rule in my firewall to allow external access on ports 80 and 443 to this instance. Sometime in the future, I will switch to the DNS based domain verification instead so I can close down this opening.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
version: '2'  	
services:
nginx-proxy:
image: jwilder/nginx-proxy
container_name: nginx-proxy
logging:
driver: splunk
options:
splunk-url: https://10.0.1.29:8088
splunk-token: 43D65CB7-4AE8-4ABD-B402-C345781009D4
splunk-insecureskipverify: 'true'
splunk-format: json
splunk-verify-connection: 'false'
splunk-source: letsencrypt-nginx-proxy
ports:
- '80:80'
- '443:443'
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- /etc/nginx/vhost.d # to update vhost configuration
- /usr/share/nginx/html # to write challenge files
- ./certs:/etc/nginx/certs:ro # update this to change cert location
web:
image: 'nginx'
container_name: nginx-web
logging:
driver: splunk
options:
splunk-url: https://10.0.1.29:8088
splunk-token: 43D65CB7-4AE8-4ABD-B402-C345781009D4
splunk-insecureskipverify: 'true'
splunk-format: json
splunk-verify-connection: 'false'
splunk-source: letsencrypt-nginx-web
expose:
- '80'
environment:
- VIRTUAL_HOST=rylander.io,humle.rylander.io,dumle.rylander.io,synods01.rylander.io,synods02.rylander.io,fw.rylander.io,humle-ilo.rylander.io,dumle-ilo.rylander.io,proget.rylander.io,unifi.rylander.io,couchpotato.rylander.io,jira.rylander.io,confluence.rylander.io,gitlab.rylander.io,artifactory.rylander.io,splunk.rylander.io
- VIRTUAL_PORT=80
- LETSENCRYPT_TEST=false
- LETSENCRYPT_HOST=rylander.io,humle.rylander.io,dumle.rylander.io,synods01.rylander.io,synods02.rylander.io,fw.rylander.io,humle-ilo.rylander.io,dumle-ilo.rylander.io,proget.rylander.io,unifi.rylander.io,couchpotato.rylander.io,jira.rylander.io,confluence.rylander.io,gitlab.rylander.io,artifactory.rylander.io,splunk.rylander.io
- [email protected]
ssl-companion:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: ssl-companion
logging:
driver: splunk
options:
splunk-url: https://10.0.1.29:8088
splunk-token: 43D65CB7-4AE8-4ABD-B402-C345781009D4
splunk-insecureskipverify: 'true'
splunk-format: json
splunk-verify-connection: 'false'
splunk-source: letsencrypt-ssl-companion
dns:
- '8.8.8.8'
- '8.8.4.4'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro #companion maps differently
- ./certs:/etc/nginx/certs:rw # same path as above, now RW
volumes_from:
- nginx-proxy
depends_on:
- nginx-proxy

Test run

Test run the docker-compose up command to catch any errors. If all is well just add the -d switch.

Create a service

To have the service behave nicely, you can set it up as a service

Service configuration

Enable the service

1
systemctl enable letsencrypt.service