Skip to content

(Advanced) Semi Manual DNS Challenge Validation

Ryan Bolger edited this page Oct 10, 2018 · 16 revisions

Work In Progress

(Advanced) Semi-Manual DNS Challenge Validation

Intro

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.

Server Selection

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

Account Setup

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'

Create an Order

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.

New-PAOrder '*.example.com','example.com'

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

Authorizations and Challenges

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.

Publishing a DNS 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 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 value for each TXT record is a "key authorization" value which is comprised of the token value and the account's public key thumbprint. The Get-KeyAuthorization function can be used to generate this value. 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}}

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 multiple minutes for the records you create to be queryable from the Internet.

Using CNAMEs for Challenge Redirection

ACME validation servers will follow CNAME records to validate challenges. It can be useful it your primary DNS server is either not programmatically accessible 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.

Notify the ACME Server

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.HTTP01Url | 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

Finishing Up

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. All you should need to specify are the same names you used originally with New-PAOrder

New-PACertificate $auths.fqdn

Use Get-PACertificate | fl to get a full list of cert properties including the filesystem paths where the files are stored.

Debugging Challenge Failures

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