4

I have a web site being served with nginx with the following requirements:

  1. Redirect all http -> https
  2. Zero-downtime Let's Encrypt certificate renewal

In order to satisfy (1) I have a small http->https redirect in my nginx config. In order to satisfy (2) I'll need to modify said config so that I can use the webroot Let's Encrypt authentication method.

I'm trying to figure out the optimal solution that will satisfy both requirements. I've come up with the following, which works.

Before:

server {
    listen 80;
    server_name example.com;
    return 301 https://example.com$request_uri;
}

After:

server {
    listen 80;
    server_name example.com;
    location ~ /\.well-known\/acme-challenge {
        root /usr/share/nginx/html;
        allow all;
    }
    if ($request_uri !~ /\.well-known) {
        return 301 https://example.com$request_uri;
    }
}

However, I was hoping to figure out another way of doing it. The reasons for that are:

  1. if is evil. Might not be such a big deal in this case because this is just the http->https redirect, which should be very low-traffic.

  2. More importantly, avoiding the if would make it easier to bolt on webroot authentication to all of my sites running behind nginx since I could just plop that location directive in a .conf that I could then include willy-nilly inside all of my little http->https redirect blocks.

In this question, Alex gives an example using a bare return statement, but that doesn't work for me (nginx -t complains with nginx: [emerg] invalid number of arguments in "return" directive in /etc/nginx/...).

Is there a better way of doing this? Or is my solution above as good as it gets?

mgalgs
  • 345
  • 2
  • 9
  • I'n not sure zero downtime with 100% uptime possible, but you can probably get close. Can you clarify your requirements? My Let's Encrypt certificates seem to renew, and I redirect everything from http to https, and I serve the acme-challenge directory over https. Maybe I'm wrong but it seems ok... have you actually tried this and had a failure? There's a LE forum thread that may be interesting here: https://community.letsencrypt.org/t/could-not-connect-to-http-domain-name-org-well-known-acme-challenge-xxxx/15748/3 – Tim Nov 11 '16 at 04:35
  • I don't mean zero downtime _ever_, I just mean zero downtime while renewing my certs (which I plan on doing in a nightly cron job). I have users on my site 24x7 so even a few seconds in the middle of the night is not ideal. I hadn't even thought about renewing over https! Problem is that I have a www -> non-www redirect on https too, so either way I'll have to use this `if`... So I'm thinking that (unless there's a better way to do it without the `if`) I'll just go with what I have in my Q. – mgalgs Nov 11 '16 at 15:30
  • Ah there's one more problem with doing it over https, the webroot plugin [doesn't actually support the `tls-sni-01` challenge type](https://certbot.eff.org/docs/using.html#plugins). – mgalgs Nov 11 '16 at 16:36

1 Answers1

6

You can replace the if with a normal location:

server {
    listen 80;
    server_name example.com;
    location /.well-known/acme-challenge {
        root /usr/share/nginx/html;
        allow all;
    }
    location / {
        return 301 https://example.com$request_uri;
    }
}

Reason: the location with the longest matching prefix is selected and remembered.

To find location matching a given request, nginx first checks locations defined using the prefix strings (prefix locations). Among them, the location with the longest matching prefix is selected and remembered. Then regular expressions are checked, in the order of their appearance in the configuration file. The search of regular expressions terminates on the first match, and the corresponding configuration is used. If no match with a regular expression is found then the configuration of the prefix location remembered earlier is used.

Source: Nginx Docs

mgalgs
  • 345
  • 2
  • 9
  • @Tim quoted in his comment a LE-forum thread which does solve the problem similar, but uses `rewrite` instead of `return`. this is not adviced by nginx devs as rewrites are generally costly performance wise. – Phillip -Zyan K Lee- Stockmann Nov 11 '16 at 22:42
  • 1
    Yes, it's not quite as good that way. However the regular expression match above can probably be removed, as I think Nginx evaluates rules from most to least specific. Might reduce CPU usage very slightly. – Tim Nov 11 '16 at 22:45
  • you are right. The quoted part states it .. just didn't notice it applies here as well: the longest prefix location will be used. I will edit the answer. Thanks @Tim – Phillip -Zyan K Lee- Stockmann Nov 11 '16 at 22:48
  • another thought I had just now: which would be used? a `return` within the `server {}` or the `location /.well-known/acme-challenge` ? perhaps `location /` is not necessary at all? – Phillip -Zyan K Lee- Stockmann Nov 11 '16 at 22:53
  • "location /" with the 301 return is required for the https redirect. – Tim Nov 11 '16 at 23:06
  • yeah. I tried it. A `return` on the same level as the `location` would always be preferred. – Phillip -Zyan K Lee- Stockmann Nov 11 '16 at 23:08
  • imho, this config could be modified to a more general approach with removing `server_name` and replacing the domain within the `return` with `$host`. optionally the `root` could make use of the `$host` variable as well. this would result in a "dynamic" delivery of the appropriate challenge when using multiple ssl server blocks. – Phillip -Zyan K Lee- Stockmann Nov 11 '16 at 23:12