Yesterday, I shared my journey of going from total noob to mostly noob with a Docker host running nginx, Node.js, and DataDog on a DigitalOcean droplet. As part of that journey, I was using the LetsEncrypt Docker container to obtain an SSL certificate for my Express.js site. Right after I posted my write-up, however, Atticus White pointed out that LetsEncrypt just released the ability to obtain wildcard SSL certificates. I don't really need a wildcard SSL certificate for my site; but, since I don't know much about DNS, I thought it would be a fun learning experiment to use the LetsEncrypt Docker container to obtain one anyway.
In my previous post, I was using the "webroot" plug-in with the LetsEncrypt Docker container. The webroot plug-in allows the certbot to install files in the webroot of your site (running on port 80) in order to complete the authentication challenge. With a wildcard SSL certificate, however, LetsEncrypt requires you to use the DNS-01 challenge. This challenge asks you to add a TXT entry to your domain name servers. The certbot will then verify that those TXT entries exist before issuing the wildcard SSL certificate.
Out of the box, the LetsEncrypt Docker container has a number of DNS-oriented plug-ins for various hosting providers. These plug-ins automate the TXT authentication challenge using scripts that make HTTP calls to your hosting provider's API. There's even one for DigitalOcean, which is the hosting provider I'm using. That said, I opted to use the "manual" plug-in such that I could create the TXT entries myself and get a better sense of how DNS works.
To start with, I created a new shell script, "request_wildcard_certificate.sh", to wrap the invocation of the LetsEncrypt Docker container. This shell script asks that both my base domain and the wildcard domain be added to the certificate:
- # /etc/letsencrypt
- # WHAT: This is the default configuration directory. This is where certbot will store all
- # generated keys and issues certificates.
- # /var/lib/letsencrypt
- # WHAT: This is default working directory.
- # certonly
- # WHAT: This certbot subcommand tells certbot to obtain the certificate but not not
- # install it. We don't need to install it because we will be linking directly to the
- # generated certificate files from within our subsequent nginx configuration.
- # -d
- # WHAT: Defines one of the domains to be used in the certificate. We can have up to 100
- # domains in a single certificate. In this case, we're obtaining a wildcard-subdomain
- # certificate (which was just made possible!) in addition to the base domain.
- # --manual
- # WHAT: Tells certbot that we are going to use the "manual" plug-in, which means we will
- # require interactive instructions for passing the authentication challenge. In this case
- # (using DNS), we're going to need to know which DNS TXT entires to create in our domain
- # name servers.
- # --preferred-challenges dns
- # WHAT: Defines which of the authentication challenges we want to implement with our
- # manual configuration steps.
- # --server https://acme-v02.api.letsencrypt.org/directory
- # WHAT: The client end-point / resource that provides the actual certificates. The "v02"
- # end-point is the only one capable of providing wildcard SSL certificates at this time,
- # (ex, *.example.com).
- docker run -it --rm --name letsencrypt \
- -v "/etc/letsencrypt:/etc/letsencrypt" \
- -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
- quay.io/letsencrypt/letsencrypt:latest \
- certonly \
- -d dailyprime.me \
- -d *.dailyprime.me \
- --manual \
- --preferred-challenges dns \
- --server https://acme-v02.api.letsencrypt.org/directory
The "--manual" flag directs the certbot to provide instructions on how to fulfill the "--preferred-challenges", which is "dns" in this case. And, when we run the above commands, certbot prompts us to create two TXT entries (presumably one per "-d" request):
| || || |
| || |
| || || |
As you can see, it prompts you for some personal information. And then, it provides TXT entries to add to your DNS. At this point, I went into my DigitalOcean networking configuration to install the TXT values. And, to be honest, it took me a few attempts to figure out what exactly I was supposed to enter. After about four failures, I realized that I was supposed to define the TXT value using "_acme-challenge" as the hostname and the "challenge value" as the TXT "value":
| || || |
| || |
| || || |
At this point, I went back to the shell script, which was currently blocking after the TXT instructions, and hit Enter. At that point, the certbot pulled down my DNS records and confirmed that the TXT entries were in place with the correct values:
| || || |
| || |
| || || |
And just like that, I had a new wildcard SSL certificate! At that point, I updated my nginx proxy Docker image to pull from the updated file location and redeployed it to production. And now, when I go to my site and view the SSL certificate, you can see that it is allowing for wildcard domain names:
| || || |
| || |
| || || |
Woot woot! And, from what I understand, this should just "work" with my existing "renew" script as I don't believe that I need to keep any of the authentication challenges in place. Once the certbot accepts that I own the given domain, I believe that all subsequent renewal requests are made using a provisioned key:
If the signature over the nonce is valid, and the challenges check out, then the agent identified by the public key is authorized to do certificate management for example.com. We call the key pair the agent used an "authorized key pair" for example.com.
.... Once the agent has an authorized key pair, requesting, renewing, and revoking certificates is simple -- just send certificate management messages and sign them with the authorized key pair.
To obtain a certificate for the domain, the agent constructs a PKCS#10 Certificate Signing Request that asks the Let's Encrypt CA to issue a certificate for example.com with a specified public key. As usual, the CSR includes a signature by the private key corresponding to the public key in the CSR. The agent also signs the whole CSR with the authorized key for example.com so that the Let's Encrypt CA knows it's authorized.
When the Let's Encrypt CA receives the request, it verifies both signatures. If everything looks good, it issues a certificate for example.com with the public key from the CSR and returns it to the agent.
Very exciting stuff!
As a next step, I'm pretty sure that I need to go revoke the SSL certificate I obtained yesterday otherwise my renew script will try to renew both the original SSL certificate and the one I provisioned for the wildcard certificate.
Looking For A New Job?
Ooops, there are no jobs. Post one now for only $29 and own this real estate!
Nice and concise article. One thing to suggest is to use wildcard domain first to generate a certificate. In this case browser will display *.dailyprime.me.
\ -d *.dailyprime.me -d dailyprime.me \
Ah, interesting tip. Are their implications for what the browser displays? Or is this an aesthetic choice?
The renewal for the wildcard certificate seems to be breaking with this error:
> Cert is due for renewal, auto-renewing...
> Could not choose appropriate plugin: The manual plugin is not
> working; there may be problems with your existing configuration.
> The error was: PluginError('An authentication script must be provided
> with --manual-auth-hook when using the manual plugin non-interactively.',)
It seems that because I issues the certificate "manually", it's expecting the renewal process to do the same. Which seems kind of whack. Trying to figure it out.
Ok, I opened up a support ticket on the Certbot site and got my answer:
It looks like domain ownership needs to be verified on each renewal, not just the original issuance (since it's possible that the domain may have been expired or transferred to another organization). This is why it needs me to perform the same manual DNS / TXT verification even during the renewal.
In light of this, I may just go back to using an explicit subdomain certificate so that I can use the webroot-based authentication.
I have gone back to using a more explicit subdomain-driven certificate. However, when I went to revoke the existing wildcard certificate, I was getting the following error:
Revocation request must be signed by private key of cert to be revoked, by the account key of the account that issued it, or by the account key of an account that holds valid authorizations for all names in the certificate.
After a bit of Googling, it looks like I had to link to my
privkey.pem in the revoke. Here's what ended up finally
working for me:
#!/bin/bash /usr/bin/docker run --rm --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/log/letsencrypt:/var/log/letsencrypt" \ quay.io/letsencrypt/letsencrypt:latest \ revoke \ --cert-path /etc/letsencrypt/live/dailyprime.me-0001/cert.pem \ --key-path /etc/letsencrypt/live/dailyprime.me-0001/privkey.pem \ --reason superseded
--key-path invocation argument.
Then, once this has run, I deleted using the following command:
#!/bin/bash /usr/bin/docker run --rm --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/log/letsencrypt:/var/log/letsencrypt" \ quay.io/letsencrypt/letsencrypt:latest \ delete \ --cert-name dailyprime.me-0001
I believe this how now purged my LetsEncrypt wildcard certificate from both my server and the certbot system. Of course, I also had to revert my nginx configurations back to the old certificate, and re-request that certificate. It was a bunch of trial and error, but I finally got it.