nginx / njs-acme

Nginx NJS module runtime to work with ACME providers like Let's Encrypt for automated no-reload TLS certificate issue/renewal.
Apache License 2.0
54 stars 9 forks source link

Support for multiple server blocks #57

Open NetForce1 opened 4 months ago

NetForce1 commented 4 months ago

Is your feature request related to a problem? Please describe

As variables set using js_var are global, it cannot be used to set njs_acme_server_names when multiple server blocks are used. Only one certificate would be generated, and on all but one server block the wrong certificate will be selected by acme.js_cert and acme.js_key.

What we need is a good way to handle multiple server definitions.

Describe the solution you'd like

The documentation and maybe examples need to be updated to address this problem. I don't know enough of nginx internals to know what the best solution would be.

Something I can think of is this: Use $ssl_server_name for serving certificates, and $host for the auto mode. Like this:

server {
    listen 8000;
    server_name _default;

    set $njs_acme_server_names $host;
    js_var $njs_acme_account_email 'test@example.com';

   # locations etc
}

server {
    listen 8443 ssl;
    server_name proxy.nginx.com;

    js_var $njs_acme_server_names $ssl_server_name;
    js_var $njs_acme_account_email 'test@example.com';

   # locations etc
} 

That would mean though that js_periodic cannot be used.

This is still a suboptimal solution though, as it means approaches cannot be mixed in a single nginx install. It would fall apart when support for wildcard certificates is added.

So, maybe what's really needed is an alternative way of passing variables to njs functions. Set cannot be used, because the TLS stuff comes before set is handled.

zsteinkamp commented 4 months ago

Heya @NetForce1 ... The way I do this is to use a single cert with multiple hostnames in it (SAN cert). I can then use that cert in as many server blocks as necessary. Example here: https://github.com/zsteinkamp/web-proxy/blob/nginx-plus/nginx.conf

This requests a cert with 5 hostnames, and uses that in 4 server blocks in the config.

Understood there are some limitations to this approach (i.e. runs in a single ACME "account", there may be limits on the number of hostnames a given provider supports), but it may be a decent solution for some.

I'll start a discussion with the njs team about supporting js_var scopes in different server blocks. That seems to be an important shortcoming to address.

zsteinkamp commented 4 months ago

Someone filed an issue with njs yesterday on this topic. https://github.com/nginx/njs/issues/700 I will add to it.

NetForce1 commented 4 months ago

Great, thanks for the support. Combining the domain names in a multi-SAN certificate is something I want to avoid. We are hosting on customer domains, and I'd rather not publicly mix them.

zsteinkamp commented 4 months ago

Hey @NetForce1 -- I'm doing some local testing and this is working for me using $server_name. It would work if your server blocks had a single hostname.

nginx.conf

  server {
    listen 80;
    listen 443 ssl;
    ...
    server_name proxy.nginx.com;
    ...
    js_var $njs_acme_server_names $server_name;
    ...
    location @acmePeriodicAuto {
      # Check certificate validity each minute
      js_periodic acme.clientAutoMode interval=1m;
    }
    location ~ "^/\.well-known/acme-challenge/[-_A-Za-z0-9]{22,128}$" {
      js_content acme.challengeResponse;
    }
    location / {
      ...
    }
  }
}

This could also be split between HTTP and HTTPS server blocks (with challenge response and periodic in the HTTP block), you just need to make sure to specify the server_name in each.

NetForce1 commented 3 months ago

Hi @zsteinkamp Thanks, I will test this one for a while. I think you can even use it with multiple hostnames, but only a single server_name directive.

NetForce1 commented 2 months ago

I think you can even use it with multiple hostnames, but only a single server_name directive. Alas, you can't. I thought I had seen that work, but apparently not.

I have a work-around that uses js_set to specify the hostnames using javascript, and can use a lookup table to determine the alternative names based on $server_name. But, we deploy our config using Ansible, and quite often just update a single nginx-config-file (we have a file per customer).

What might work is a js_preload_object per server-block, with the first server_name as the key. Or, store the lookup table in a js_shared_dict_zone, and add a reload-endpoint that is called by our Ansible playbook.