-
-
Notifications
You must be signed in to change notification settings - Fork 190
(Advanced) Semi Manual DNS Challenge Validation
NOTE: This content is out of date. An updated version can be found here.
- Intro
- Server Selection
- Account Setup
- Create an Order
- Authorizations and Challenges
- Publishing a DNS Challenge
- Using CNAMEs for Challenge Redirection
- Notify the ACME Server
- Finishing Up
- Debugging Challenge Failures
- Renewals
The beauty of the ACME protocol is that it's an open standard. And while Posh-ACME primarily targets users who want to avoid understanding all of the protocol complexity, it also exposes functions that allow you to do things a bit closer to the protocol level than just running New-PACertificate
and Submit-Renewal
. This can enable more advanced automation scenarios and allow you to support additional challenge types that the module doesn't directly support yet. This tutorial will focus on the scenario where you want to use DNS validation, but either none of the existing DNS plugins work with your provider or your DNS modification process is incompatible with a typical plugin's workflow for some reason.
From a high level, the ACME conversation looks more or less like this:
- Create an account
- Create a certificate order
- Prove control of the "identifiers" (names) in the requested cert by answering challenges
- Finalize the order by submitting a CSR
- Download the signed certificate
If you're curious about what's going on under the hood during this tutorial, it is advised to append -Verbose
to your commands or run $VerbosePreference = 'Continue'
. If you really want to get deep, you can also turn on debug logging by running $DebugPreference = 'Continue'
. The defaults for both of those preferences are SilentlyContinue
if you want to change them back later.
It is always advised not to use the production Let's Encrypt server while testing code. The staging server is the easiest alternative, but still has some rate limits that you can run afoul of if you're not careful. There is also Pebble which is a tiny ACME server you can self-host and is built for testing code against. For simplicity, we'll select the Let's Encrypt staging server.
Set-PAServer LE_STAGE
Requesting a certificate always starts with creating an account on the ACME server which is basically just a public/private key pair that is used to sign the protocol messages you send to the server along with some metadata like one or more email addresses to send expiration notifications to. If you've been previously using the module against the staging server, you likely already have an account. If so, you can either skip this section or create a second account which is also supported.
New-PAAccount -AcceptTOS -Contact 'me@example.com'
The only required parameter for a new order is the set of names you want included in the certificate. Optional parameters include things like -KeyLength
to change the private key type/size, -Install
which tells Posh-ACME to automatically store the signed cert in the Windows certificate store (requires local admin), and -PfxPass
which lets you set the decryption password for the certificate PFX file.
In this example, we're going to create a typical wildcard cert which includes both the wildcard name and the standalone apex domain name.
$domains = '*.example.com','example.com'
New-PAOrder $domains
Assuming you didn't use names that were previously validated on this account, you should get output that looks something like this where the status is pending
. If the status is ready
, create an order with different names that haven't been previously validated.
MainDomain status KeyLength SANs OCSPMustStaple CertExpires
---------- ------ --------- ---- -------------- -----------
*.example.com pending 2048 example.com False
The distinction between an order, authorization, and challenge can be confusing if you're not familiar with the ACME protocol. So let's clarify first. An order is a request for a certificate that contains one or more "identifiers", otherwise known as names like site1.example.com
. Each identifier in an order has an authorization object associated with it that indicates whether this account is authorized to get a cert for that name. New authorizations start in a pending state awaiting the client to complete a challenge associated with that authorization. Each authorization can have multiple different challenges (DNS, HTTP, etc) that indicate the different methods the ACME server will accept to prove ownership of the name. You only need to complete one of the offered challenges in order to satisfy an authorization.
Get-PAAuthorizations
can be used with the output of Get-PAOrder
to retrieve the current set of authorizations (and their challenges) for an order. So lets put those details into a variable and display them.
$auths = Get-PAOrder | Get-PAAuthorizations
$auths
This should give an output that looks something like this. The first status column is the overall status of the authorization. The last two columns are the status of the dns-01
and http-01
challenges. Normally the challenge specific details are buried a bit deeper in the challenges property, but Posh-ACME tries to help by pulling out the important bits into properties on the root object.
fqdn status Expires DNS01Status HTTP01Status
---- ------ ------- ----------- ------------
example.com pending 10/10/2018 4:58:41 PM pending pending
*.example.com pending 10/10/2018 4:58:41 PM pending
Let's take a look at the full details of one of the authorization objects by running $auths[0] | fl
. You should get an output like this. The wildcard entry would be missing HTTP related challenge info.
identifier : @{type=dns; value=example.com}
status : pending
expires : 2018-08-13T16:52:23Z
challenges : {@{type=dns-01; status=pending; url=https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<DNS_CHAL_ID>; token=<DNS_TOKEN>},
@{type=http-01; status=pending; url=https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<HTTP_CHAL_ID>; token=<HTTP_TOKEN>},
@{type=tls-alpn-01; status=pending; url=https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<ALPN_CHAL_ID>; token=<ALPN_TOKEN>}}
DNSId : example.com
fqdn : example.com
location : https://acme-staging-v02.api.letsencrypt.org/acme/authz/<AUTH_ID>
DNS01Status : pending
DNS01Url : https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<DNS_CHAL_ID>
DNS01Token : <DNS_TOKEN>
HTTP01Status : pending
HTTP01Url : https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<HTTP_CHAL_ID>
HTTP01Token : <HTTP_TOKEN>
The things we care about in this example are the DNS01Url
and DNS01Token
properties. The token value is what we're going to use to prove we control this identifier. The URL is how we inform the ACME server that it should perform the validation check for the challenge.
There are two things you need to satisfy a DNS challenge, the name of the TXT record and the value the ACME server expects to be in it. The name of the record will always be _acme-challenge.
plus the identifier's FQDN. The exception is wildcard records which remove the *.
portion of the FQDN before prepending the _acme-challenge.
portion.
The astute reader may have realized that in our example, this means the name of the TXT record is the same for both identifiers, example.com
and *.example.com
. They both translate to _acme-challenge.example.com
. This tends to confuse people at first, but it's really no different than having multiple A records pointing to different IPs for a website. The ACME validation server is smart enough to check all of the returned results and find the one it cares about.
The value for each TXT record is a "key authorization" value which is comprised of the token value and the account's public key thumbprint that is then hashed with SHA256 and encoded as Base64Url. The Get-KeyAuthorization
function can be used generate the token+thumbprint portion. So putting everything together, you might do something like this to build your publishing details.
$toPublish = $auths | Select @{L='TXTName'; E={"_acme-challenge.$($_.fqdn.Replace('*.',''))"}}, `
@{L='TXTValue';E={(Get-KeyAuthorization $_.DNS01Token -ForDNS)}}
Now it's up to you to publish those records on your DNS server. From the Internet, you should be able to go to run a DNS query for those TXT records and receive response values with those key authorization values. Depending on your DNS provider and replication topology, it may take anywhere from seconds to minutes for the records you create to be queryable from the Internet. Make sure you either know how long it's supposed to take and wait that long before proceeding, or query your external nameservers directly until they return the expected results.
ACME validation servers will follow CNAME records to validate challenges. It can be useful if your primary DNS server has no API or the security posture of your organization doesn't allow an automated process such as an ACME client to have write access to the zone you need to create TXT records in. If you know this will be the case, you can create a permanent CNAME record for the _acme-challenge.<FQDN>
name that points to another FQDN somewhere else. Then write your TXT record to that other target and as long as that zone is still Internet-facing, the validation will succeed.
This step simply asks the ACME server to do its own check against the challenges you just published. Use the Send-ChallengeAck
function like this.
$auths.DNS01Url | Send-ChallengeAck
The challenges are usually validated pretty quick. But there may be a delay if the ACME server is overloaded. You can poll the status of your authorizations by re-running Get-PAOrder | Get-PAAuthorizations
. Eventually, the status for each one will either be "valid" or "invalid". Good output should look something like this.
fqdn status Expires DNS01Status HTTP01Status
---- ------ ------- ----------- ------------
example.com valid 11/10/2018 12:39:36 AM valid pending
*.example.com valid 11/10/2018 12:39:36 AM valid
Now that you have all of your identifiers authorized, your order status should now be "ready" which you can check with Get-PAOrder -Refresh
. It should look something like this.
MainDomain status KeyLength SANs OCSPMustStaple CertExpires
---------- ------ --------- ---- -------------- -----------
*.example.com ready 2048 example.com False
The easiest thing to do now is actually to use New-PACertificate
. It's smart enough to pick up your in-progress order and finish it up for you with the $domains
variable you created at the beginning.
New-PACertificate $domains
Use Get-PACertificate | fl
to get a full list of cert properties including the filesystem paths where the files are stored.
If for some reason one or more of your challenge validations failed, you can retrieve the error details from the ACME server like this.
(Get-PAOrder | Get-PAAuthorizations).challenges.error | fl
The concept of a renewal doesn't actually exist in the ACME protocol. What most clients call a renewal is just a new order with the same parameters as last time. So the only thing extra you need to deal with is knowing when to renew. When you successfully complete a certificate order, Posh-ACME will attach a RenewAfter
property to the order object which you can use to calculate whether it's time to renew or not. The property is an ISO 8601 date/time string which can be parsed and checked with DateTimeOffset
like this.
$renewAfter = [DateTimeOffset]::Parse((Get-PAOrder).RenewAfter)
if ([DateTimeOffset]::Now -gt $renewAfter) {
# time to renew
}