Here’s me generating two certs – one for “edge.raxnet.global” (with a SAN of “mx.raxnet.global”), another for “adfs.raxnet.global”. Both are “public” certificates, using Let’s Encrypt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
PS C:\Windows\system32> New-PACertificate 'edge.raxnet.global','mx.raxnet.global' -AcceptTOS -Contact letsencrypt@rakhesh.com -DnsPlugin Azure -PluginArgs $azParams -DnsAlias 'edge.acme.raxnet.global' WARNING: Fewer DnsPlugin values than Domain values supplied. Using Azure for the rest. WARNING: Fewer DnsAlias values that Domain values supplied. Using edge.acme.raxnet.global for the rest. Subject NotAfter KeyLength Thumbprint AllSANs ------- -------- --------- ---------- ------- CN=edge.raxnet.global 19-Apr-19 3:39:53 PM 2048 144209DC1CFD00C5136DA145FF71E57DC302CD84 {edge.raxnet.global, mx.raxnet.global} PS C:\Windows\system32> New-PACertificate 'adfs.raxnet.global' -AcceptTOS -Contact letsencrypt@rakhesh.com -DnsPlugin Azure -PluginArgs $azParams -DnsAlias 'adfs.acme.raxnet.global' Subject NotAfter KeyLength Thumbprint AllSANs ------- -------- --------- ---------- ------- CN=adfs.raxnet.global 19-Apr-19 3:40:24 PM 2048 3DF5E9E4AE62A68CCE8F8518329E2975413BFCB2 {adfs.raxnet.global} |
Easy peasy!
And here are the files themselves:
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 |
PS C:\> Get-PACertificate -List | fl Subject : CN=adfs.raxnet.global NotBefore : 19-Jan-19 3:40:24 PM NotAfter : 19-Apr-19 3:40:24 PM KeyLength : 2048 Thumbprint : 3DF5E9E4AE62A68CCE8F8518329E2975413BFCB2 AllSANs : {adfs.raxnet.global} CertFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\adfs.raxnet.global\cert.cer KeyFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\adfs.raxnet.global\cert.key ChainFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\adfs.raxnet.global\chain.cer FullChainFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\adfs.raxnet.global\fullchain.cer PfxFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\adfs.raxnet.global\cert.pfx PfxFullChain : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\adfs.raxnet.global\fullchain.pfx PfxPass : System.Security.SecureString Subject : CN=edge.raxnet.global NotBefore : 19-Jan-19 3:39:53 PM NotAfter : 19-Apr-19 3:39:53 PM KeyLength : 2048 Thumbprint : 144209DC1CFD00C5136DA145FF71E57DC302CD84 AllSANs : {edge.raxnet.global, mx.raxnet.global} CertFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\edge.raxnet.global\cert.cer KeyFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\edge.raxnet.global\cert.key ChainFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\edge.raxnet.global\chain.cer FullChainFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\edge.raxnet.global\fullchain.cer PfxFile : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\edge.raxnet.global\cert.pfx PfxFullChain : C:\Users\Rakhesh\AppData\Local\Posh-ACME\acme-v02.api.letsencrypt.org\49761602\edge.raxnet.global\fullchain.pfx PfxPass : System.Security.SecureString |
The password for all these is ‘poshacme’.
I don’t know if I will ever use this at work but I was reading up on Let’s Encrypt and ACME Certificate Authorities and decided to play with it for my home lab. A bit of work went behind the scenes into the above stuff so here’s some notes.
First off, ACME Certificates are all about automation. You get certificates that are valid for only 90 days, and the idea is that every 60 days you renew them automatically. So the typical approach of a CA verifying your identity via email doesn’t work. All verification is via automated methods like a DNS record or HTTP query, and you need some tool to do all this for you. There’s no website where you go and submit a CSR or get a certificate bundle to download. Yeah, shocker! In the ACME world everything is via clients, a list of which you can find here. Most of these are for Linux or *nix, but there are a few Windows ones too (and if you use a web server like Caddy you even get HTTPS out of the box with Let’s Encrypt).
To dip my feet I started with Certify a Web, a GUI client for all this. It was fine, but didn’t hook me on much and so I moved to Posh-ACME a purely PowerShell CLI tool. I’ve liked this so far.
Apart from installing the tool via something like Install-Module -Name Posh-ACME
there’s some background work that needs to be done. A good starting point is the official Quick Start followed by the Tutorial. What I go into below is just a rehash for myself of what’s already in the official docs. :)
Requesting a certificate via Posh-ACME is straightforward. Essentially, if I want a certificate for a domain ’sso.raxnet.global’ I would do something like this:
1 2 3 4 5 6 7 |
PS C:\> New-PACertificate 'sso.raxnet.global' -AcceptTOS -Contact admin@rakhesh.com WARNING: DnsPlugin not specified. Defaulting to Manual. Please create the following TXT records: ------------------------------------------ _acme-challenge.sso.raxnet.global -> _jTw7ckGRGUj_-eeTdgGBoV5ycjd797R6n2oFOTmaLw ------------------------------------------ |
At this point I’d have to go and make the specified TXT record. The command will wait 2 mins and then check for the existence of the TXT record. Once it finds it the certificates are generated and all is good. (If it doesn’t find the record it keeps waiting; or I can Ctrl+C to cancel it). My ACME account will be admin@rakhesh.com
and the certificate tied to that.
If I want to be super lazy, this is all I need to do! :) Run this command every 60 days or so (worst case every 89 days), update the _acme-challenge.domain
TXT record as requested (the random value changes each time), and bam! I am done.
If I want to automate it however I need to do some more stuff. Specifically, 1) you need to be on a DNS provider that gives you an API to update its records, and 2) hopefully said DNS provider is on the Posh-ACME supported list. If so, all is good. I use Azure DNS for my domain, and instructions for using Azure DNS are already in their documentation. If I were on a DNS provider that didn’t have APIs, or for whatever reason if I wanted to use a different DNS provider to my main domain, I can even make use of CNAMEs. I like this CNAME idea, so even though I could have used my primary zone hosted in Azure DNS I decided to make another zone in Azure DNS and down the CNAME route.
So, here’s how the CNAME thing works. Notice above Posh-ACME asked me to create a record called _acme-challenge.sso.raxnet.global
? Basically for every domain you are requesting a certificate for (including a name in the Subject Alternative Name (SAN)), you need to create a _acme-challenge.<domain>
TXT record with the random challenge given by ACME. However, what you can also do is say have a separate domain like for example ‘acme.myotherdomain’ and I can pre-create CNAME records like _acme-challenge.<whatever>.mymaindomain
-> <whatever>.myotherdomain
such that when the validation process looks for _acme-challenge.<whatever>.mydomain
it will follow it through to <whatever>.myotherdomain
and update & verify the record there. So my main domain never gets touched by any automatic process; only this other domain that I setup (which can even be a sub-domain of my main domain) is where all the automatic action happens.
In my case I created a CNAME from sso.raxnet.global
to sso.acme.raxnet.global
(where acme.raxnet.global is my Azure DNS hosted zone). I have to create the CNAME record before hand but I don’t need to make any TXT records in the acme.raxnet.global zone – that happens automatically.
To automate things I then made a service account (aka “App registrations” in Azure-speak) whose credentials I could pass on to Posh-ACME, and whose rights were restricted. The Posh-ACME documentation has steps on creating a custom role in Azure to just update TXT records; I was a bit lazy here and simply made a new App Registration via the Azure portal and delegated it “DNS Contributor” rights to the zone.
Not shown in the screenshot, after creating the App Registration I went to its settings and also assigned a password.
That done, the next step is to collect various details such as the subscription ID & tenant ID & and the App Registration name and password into a variable. Something like this:
1 2 3 4 5 |
$azParams = @{ AZSubscriptionId=‘REPLACE ME'; AZTenantId=‘REPLACE ME'; AZAppCred=(Get-Credential) } |
This is a one time thing as the credentials and details you enter here are then stored in the local profile. This means renewals and any new certificate requests don’t require the credentials etc. to be passed along as long as they use the same DNS provider plugin.
That’s it really. This is the big thing you really have to do to make the DNS part automated. Assuming I have already filling in the $azParams
hash-table as above (by copy-pasting it into a PowerShell window after filling in the details and then entering the App Registration name and password when prompted) I can request a new certificate thus:
1 |
New-PACertificate 'sso.raxnet.global' -AcceptTOS -Contact admin@rakhesh.com -DnsPlugin Azure -PluginArgs $azParams -DnsAlias 'sso.acme.raxnet.global' |
Key points:
- The
-DnsPlugin
switch specifies that I want to use the Azure plugin - The
-PluginArgs
switch passes along the arguments this plugin expects; in this case the credentials etc. I filled into the$azParams
hash-table - The
-DnsAlias
switch specifies the CNAME records to update; you specify one for each domain. For example, in this case ‘sso.raxnet.global’ will be aliased to ‘sso.acme.raxnet.global’ so the latter is what the DNS plugin will go and update. If I specified two domains e.g. ‘sso.raxnet.global’,’sso2.raxnet.global’ (an array of domains) then I would have had to specify two aliases ‘sso.acme.raxnet.global’,’sso2.acme.raxnet.global’ OR I could just specify one alias ‘sso.acme.raxnet.global’ provided I have created CNAMES from both domains to this same entry, and the plugin will use this alias for both domains. My first example at the beginning of this post does exactly that.
That’s it! To renew my certs I have to use the Submit-Renewal
cmdlet. I don’t even need to run it manually. All I need do is create a scheduled task to run the cmdlet Submit-Renewal -AllAccounts
to renew all my certificates tied to the current profile (so if I have certificates under two different accounts – e.g. admin@rakhesh.com and admin2@rakhesh.com but both are in the same Windows account where I am running this cmdlet from, both accounts would have their certs renewed).
What I want to try next is how to get these certs updated with Exchange and ADFS. Need to figure out if I can automatically copy these downloaded certs to my Exchange and ADFS servers.