Skip to content

How To Self Host HTTP Challenges

Ryan Bolger edited this page Aug 21, 2019 · 7 revisions

How To Self Host HTTP Challenges

Intro

Posh-ACME 3.6.0 introduced a new function called Invoke-HttpChallengeListener which is a super convenient way to self-host HTTP challenge data just long enough to satisfy an order's authorizations. Under the hood, it uses .NET's System.Net.HttpListener, so it won't touch any other web server configuration you have running. There are some prerequisites you have to take care of on Windows if you're not running PowerShell as an administrator and some other considerations for any OS. This article will explain all those and go through some examples using the function.

Windows Only Prerequisites

When running on Windows, the HttpListener class depends on a kernel mode web server called http.sys. Because it's a system-level service, non-administrator users can't use it without an explicit URL reservation that gives them permission. Open up an elevated PowerShell session and run the following to see the current list of URL reservations.

netsh http show urlacl

Modern Windows versions will have a bunch of these even in a default install for various system components and services. We need to add one that matches what Invoke-HttpChallengeListener will be trying to use. By default, it will use http://+:80/.well-known/acme-challenge/. The easiest thing to do is create the reservation and give permissions to "Everyone". But it's perfectly reasonable to only grant permissions to the user or group who will need it as well. Just adjust the command line appropriately with the target user/group instead of an sddl string.

netsh http add urlacl url=http://+:80/.well-known/acme-challenge/ sddl=D:(A;;GX;;;S-1-1-0)

Invoke-HttpChallengeListener has a -Port parameter if you need to run the listener on an alternate port because something else is already running on port 80. Just make sure your reservation also uses the alternate port.

The function also has the option to override the HttpListener prefixes entirely with the -ListenerPrefixes parameter. If you end up using it, make sure your reservation(s) match what you pass to that parameter.

Additional Considerations

Regardless of the underlying OS, you need to make sure the listener won't conflict with other software running on the system. If port 80 is in use, you'll have to run the listener on an alternate port. But keep in mind, the ACME challenge validations must be served from port 80 on the internet-facing side of things. Often, there will be port forwarding or a reverse proxy in place that maps the internet-facing port 80 to an internal alternate port.

Basic Usage

We're going to assume you've been through the main tutorial and already have an ACME server and account configured. If not, go back and do that first.

New Order

First, we create a new order as normal. You can include as many additional names as you want, but make sure they all resolve to this server. The resulting order should have a "pending" status.

New-PAOrder example.com

Check Authorizations and Test the Listener

Let's take a look at what our authorizations look like before we do anything else.

Get-PAOrder | Get-PAAuthorizations

Each FQDN in the result should have a "pending" status in the result unless it was already validated from this account within the past 30 days. The DNS01Status and HTTP01Status should also be pending. Before we actually try the listener for real, let's test it to make sure that the challenges it is serving actually work. First, run the following to generate the URLs that we need to test. These are ultimately what the ACME servers will be checking for valid results.

Get-PAOrder | Get-PAAuthorizations | ?{ $_.status -eq 'pending' } | 
    Select @{L='Url';E={"http://$($_.fqdn)/.well-known/acme-challenge/$($_.HTTP01Token)"}}

If you plan to use the default listener on port 80, you can call Invoke-HttpChallengeListener with no arguments. Otherwise, add the -Port parameter. We're also going to add verbose output and run the listener without notifying the ACME server by adding the -WhatIf parameter.

# test the listener on the default port 80
Invoke-HttpChallengeListener -WhatIf -Verbose

If you're on Windows and you get an "Access Denied" error, you either forgot to add your URL reservation, made a typo, or granted the permissions to the wrong user/group. The output should look something like this.

VERBOSE: Authorizations found with HTTP01Status pending: 1
VERBOSE: Adding listener prefix http://+/.well-known/acme-challenge/
VERBOSE: HttpListener started with 120 second timeout
VERBOSE: Send-ChallengeAck for example.com
What if: Performing the operation "Send-ChallengeAck" on target "example.com".
VERBOSE: Checking authorization status
VERBOSE: Checking authorization status
VERBOSE: Checking authorization status
VERBOSE: Checking authorization status

Try opening a web browser to the test URLs you generated earlier. They should respond with text contents that includes the token and additional data. If your environment has different DNS resolution for internal versus external clients, you may need to do this from an external host. When finished, you can either wait for the timeout to expire or press Ctrl+C.

Run Listener For Real

Assuming everything went well, you're ready to do things for real. Just run the command again without -WhatIf. You can keep -Verbose if you want to better see what's going on.

# run the listener on the default port 80
Invoke-HttpChallengeListener -Verbose

This time, you should see additional verbose messages like the following in addition to an updated copy of the authorizations which should now have "valid" status.

VERBOSE: Responding to 18.224.20.83 for example.com
VERBOSE: Responding to 54.245.186.160 for example.com
VERBOSE: Responding to 66.133.109.36 for example.com
VERBOSE: Responding to 3.122.105.36 for example.com
VERBOSE: Checking authorization status
VERBOSE: No pending authorizations remaining, stopping HttpListener

fqdn               status Expires              DNS01Status HTTP01Status
----               ------ -------              ----------- ------------
example.com        valid  9/3/2019 10:08:59 AM pending     valid

Get The Finalized Certificate

Once all your authorizations are valid, all that remains is to get the finalized certificate which we can do by just running New-PACertificate with the same set of domains as the original call to New-PAOrder.

New-PACertificate example.com

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

Renewals

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
}