How-to: Nginx configuration to enable ACME Challenge support on all HTTP virtual hosts

I run multiple websites on Debian Jessie using Nginx server. In order to simplify automatic certificate renewal, I have enabled ACME challenge support on all virtual hosts. Every website that I host is capable of serving following URI:

http://xxx.domain.tld/.well-known/acme-challenge/xxxxxxxxxxx

In my Nginx configuration I try to include snippets as much as possible instead of creating huge .conf files for every website. For example, every server that listens to HTTP includes following snippet at the beginning of the server definition. Please note that I have specified an explicit IPv4 address, since my server has multiple IP addresses and I need to run it on a specific address only. Your configuration might be different. So this is my listen.conf file:

# ------------------------------------------------------------------------------------------------
# Listen on primary IP address
listen 1.2.3.4:80;
listen [::]:80;

# Include location directive for Let's Encrypt ACME Challenge
include /etc/nginx/snippets/letsencrypt-acme-challenge.conf;
# ------------------------------------------------------------------------------------------------

Now what about this letsencrypt-acme-challenge.conf? As I said, I wanted all my websites to support ACME challenge, so I can get a certificate for any of them. Below is the content of the letsencrypt-acme-challenge.conf file:

#############################################################################
# Configuration file for Let's Encrypt ACME Challenge location
# This file is already included in listen_xxx.conf files.
# Do NOT include it separately!
#############################################################################
#
# This config enables to access /.well-known/acme-challenge/xxxxxxxxxxx
# on all our sites (HTTP), including all subdomains.
# This is required by ACME Challenge (webroot authentication).
# You can check that this location is working by placing ping.txt here:
# /var/www/letsencrypt/.well-known/acme-challenge/ping.txt
# And pointing your browser to:
# http://xxx.domain.tld/.well-known/acme-challenge/ping.txt
#
# Sources:
# https://community.letsencrypt.org/t/howto-easy-cert-generation-and-renewal-with-nginx/3491
#
#############################################################################

# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel
# other regex checks, because in our other config files have regex rule that denies access to files with dotted names.
location ^~ /.well-known/acme-challenge/ {

    # Set correct content type. According to this:
    # https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29
    # Current specification requires "text/plain" or no content header at all.
    # It seems that "text/plain" is a safe option.
    default_type "text/plain";

    # This directory must be the same as in /etc/letsencrypt/cli.ini
    # as "webroot-path" parameter. Also don't forget to set "authenticator" parameter
    # there to "webroot".
    # Do NOT use alias, use root! Target directory is located here:
    # /var/www/common/letsencrypt/.well-known/acme-challenge/
    root         /var/www/letsencrypt;
}

# Hide /acme-challenge subdirectory and return 404 on all requests.
# It is somewhat more secure than letting Nginx return 403.
# Ending slash is important!
location = /.well-known/acme-challenge/ {
    return 404;
}

Enjoy!

16 Likes

I know this is an old thread, but since Google finds it for many searches I thought I'd post my recent experience.

I found the configuration above didn't work for me, using the acmetool client and nginx. The primary problem was Acme was writing the challenge file to

/var/www/acme-challenge/

whereas Nginx was looking in this directory

/var/www/acme-challenge/.well-known/acme-challenge/

The combination of the acmetool logs and the Nginx logs made this obvious. Here are the two commands that helped parse the acmetool logs, from MrTen on this github page

acmetool --xlog.severity=debug > /tmp/dump 2>&1 
fgrep -v fdb: /tmp/dump | fgrep -v storageops: | less

The reason was found on Server Fault, on this question:

In case of the root directive, full path is appended to the root including the location part, whereas in case of the alias directive, only the portion of the path NOT including the location part is appended to the alias.

The nginx location block that worked for me is as below

location ^~ /.well-known/acme-challenge/ {
  alias /var/www/acme-challenge/;
}
1 Like

acetylator original post is right, I just lost a full hour trying to follow the other comment.

letsencrypt places files inside:

/var/www/whatever/.well-known/acme-challenge

if you use an alias, nginx will look for files under:

/var/www/whatever/

and won't find what it's looking for.

so, use root, not alias

I wonder why mine works with alias instead of root. Here’s what works for me, with irrelevant guff removed. I might give this some thought some time, but not today.

server {
  root     /var/www/example.com;
  location ^~ /.well-known/acme-challenge/ {
    default_type "text/plain";
  alias /var/www/acme-challenge/;
}

Update Feb 2017

I’ve been playing around some more, and root seems to work better now. Why, I’m not sure, I haven’t thought things through fully. It could be something to do with the webroot I use for this. I use and tell acmetool to use /var/www/acmetool/.well-known/acmetool/

2 Likes

@tomwald: Certbot and acmetool apparently behave differently, so root is appropriate for one and alias is appropriate for the other. :ok_hand:

Certbot with “-w /foo” puts files in /foo/.well-known/acme-challenge. Apparently when acmetool is told to use “/foo”, it puts the files straight in /foo.

1 Like

Thanks @mnordhoff , great information :slight_smile:

I use the following in a global file that is loaded by all sites and it works 100%

location ^~ /.well-known/acme-challenge/ {
	allow all;
    default_type "text/plain";
}

As pointed out to me by @serverco the line alias /var/www/acme-challenge/; is needed as per @tomwald 's answer above :slight_smile:

Thanks for the tips here, folks. I followed but was still having failures. Then I followed this tutorial for nginx on Ubuntu, and it covered every detail. Got me working in no time. Most tutorial I’ve used from Digital Ocean has been excellent. https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04

I wonder if you could just use both root and alias with the location { } and cover all bases!

location ^~ /.well-known/acme-challenge/ {
 default_type "text/plain";
 alias /var/www/acme-challenge/;
 root /var/www/acme-challenge/;
}

Well that was a FAIL:
as per nginx -t
nginx: [emerg] “root” directive is duplicate, “alias” directive was specified earlier in /etc/nginx/conf.d/my.conf
And reversing the alias/root order is also a FAIL:
nginx: [emerg] “alias” directive is duplicate, “root” directive was specified earlier in /etc/nginx/conf.d/my.conf

So it seems that root and alias are synonymous now.

1 Like

They're not. For a request to http://example.com/.well-known/acme-challenge/xxxxxxxx:

"alias /var/www/acme-challenge/;" results in the file path /var/www/acme-challenge/xxxxxxxx.

"root /var/www/acme-challenge/; results in the file path /var/www/acme-challenge/.well-known/acme-challenge/xxxxxxxx.

1 Like

OK that makes sense.
But it doesn’t explain when they can’t both be used with a single location {}
And maybe we should include the preferred/recommended use - for future readers.

1 Like

I have my root as a read-only file system, clearly nothing can be written to it as part of the challenge process.

Is there a way to make the challenge work without being able to write into the root tree?

You can use the standalone plugin on an alternate port, and proxy the /.well-known/acme-challenge/ prefix in your web server to the port that the standalone plugin runs on. This way, there is no need to write into the document root.

You could also try any of the non-webroot challenges, such as dns-01.

Edit: You can also tell your web server to serve /.well-known/acme-challenge from outside of your regular document root, such as /var/letsencrypt/. That way no contamination with your application dir tree.

1 Like

I'm sorry if I appear a bit dim, but could you please show me the code that I would use in the server block(s) to do this?

e.g. in nginx

location /.well-known/acme-challenge {
  root /var/letsencrypt;
}

Apache httpd also has some documentation on this exact topic.

1 Like

Thanks - is this in addition to the include /etc/nginx/snippets/letsencrypt-acme-challenge.conf; ?

OK…confused a little/lot…

Removed the .conf include and replaced it with

location /.well-known/acme-challenge/ {
root /data/test/www/;
}

Restarted nginx, reran the cert and received…

Domain: www.dummy.com
Type: unauthorized
Detail: Invalid response from
http://www.dummy.com/.well-known/acme-challenge/1QWaPomyF2g3cvOdZ8T6j1K7yYhfMj-MqiWAEQTRl_4:
"

404 Not Found

404 Not Found


"

The folder /data/test/www does exist and is writable…but it seems that it is not being accessed?

Did you also tell the Let’s Encrypt client to put the challenge file in the other directory?

e.g. with certbot it would be --webroot-path /data/test/www

1 Like

Um…no. Where do I put that entry?

You haven’t specified how you are getting the certificate (certbot or otherwise, so I don’t know exactly what you need to change).

That said, the webroot docs for Certbot are here, which specifically mention the webroot path:

Depending on how you are running Cerbot (if Certbot at all), you may need to modify the webroot path in the config file in /etc/letsencrypt/renewal.

1 Like