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:
#!/bin/bash # # /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.
Want to use code from this post? Check out the license.
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.
Really awesome thanks!
Glad this was helpful. Lets Encrypt has been great, but a bit of an uphill battle for someone like me who really doesn't have a lot "server management" experience. Slowly getting there!
Very good tips!
Do you need to restart the relevant containers when the certificates are renewed?
Yes - I'm 99% sure that you need to restart the container in order for it to pick up the new certificate. Although, to be most accurate, you need to restart the service that consumes the certificate. So, for example, if you have nginx as the server, you would - I'm 99% sure - only need to restart the nginx service in order to pick up the new certificate. However, since I was using a container to run nginx - and because I'm not so great with all this server stuff - I restarted the nginx container as a means to restart the nginx service.
How long did you have to wait for your txt records to propagate? I've been struggling with mine :(
Hmmm, mine were available almost immediately. That said, I was using DigitalOcean's network management. So, it's possible that makes it available much more quickly?
lol unfortunately im out of my depth or too impatient to keep learning with my non existent server skills, i've had to resort to paying someone on fiver to do it, which they have done successfully 57 bucks well spent if you ask me :)
Ha ha, I know exactly how you feel. The Server stuff is such an uphill battle for me (I'm primarily a front-end developer). In fact, I'm having trouble getting my DigitalOcean instance to re-provision the SSL cert. It seemed to be working for a while; then, failed, and I can't figure out how to get to to work again. Meh!!!!
Haha I recommend you do the same, also get them to enable a cron job so that it will auto renew and never expire. ??
See, I already have a cron job :D #FlipTable .... I just have no idea why it's not working. Yeah, time to get some more professional help.
It doesn't work for a 4th level dns entry or else I missed something.
ie it works for sub.example.com but not for www.sub.example.com
nor it accepts to add explicitely -d '..example.com'
yes, I missed the RFC 2818 !!!
More details on wikipedia
it is possible to get
and eventually to set a lot of them in the same certificate
Where did you find documentation about the arguments you can supply to the letsencrypt-container? Struggling a bit with automated renewal and txt records
It's been a while, but I think it was here: https://letsencrypt.readthedocs.io/en/latest/using.html
I've since had a lot of trouble refreshing the certificate. A lot of this stuff is over my head and I'm just hacking in the weeds trying to get something to work.
Thanks! I ended up with using https://hub.docker.com/r/adferrand/letsencrypt-dns/ for managing and renewing my certificates. Works like a charm!