Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dvsni support for nginx #387

Merged
merged 7 commits into from May 12, 2015
Merged

Conversation

diracdeltas
Copy link
Contributor

This adds DVSNI support for Nginx.

This doesn't seem to quite work yet - when I run this on an ubuntu server at letsencrypt.icann.wtf, I get the following mysterious error:

  File "./venv/bin/letsencrypt", line 9, in <module>
    load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')()
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/scripts/main.py", line 247, in main
    cert_file, chain_file = acme.obtain_certificate(doms)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/client.py", line 126, in obtain_certificate
    authzr = self.auth_handler.get_authorizations(domains)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/auth_handler.py", line 69, in get_authorizations
    domain, self.account.new_authzr_uri)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 297, in request_domain_challenges
    typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 278, in request_challenges
    response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz))
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 143, in _post
    self._check_response(response, content_type=content_type)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 92, in _check_response
    raise messages2.Error.from_json(jobj)
letsencrypt.acme.messages2.Error

@coveralls
Copy link

Coverage Status

Coverage increased (+0.11%) to 92.42% when pulling a9a1132 on diracdeltas:feature/nginx-dvsni into e4ac29e on letsencrypt:master.

@kuba
Copy link
Contributor

kuba commented May 5, 2015

@diracdeltas, try applying my patch from #388 for debugging.


config = []
for idx, addrs in enumerate(ll_addrs):
config.append(self._make_server_block(self.achalls[idx], addrs))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configs = [self._make_server_block(*pair) for pair in itertools.izip(achalls, ll_addrs)] would be much more pythonic / readable

@jdkasten
Copy link
Contributor

jdkasten commented May 5, 2015

#387 (comment) FYI: #388 has been merged into master.

yan added 3 commits May 5, 2015 23:40
We are assuming that if a server_name isn't specified, it matches the empty
string. Prior to 0.8.48, it would match the machine's hostname.
@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 92.42% when pulling cf48791 on diracdeltas:feature/nginx-dvsni into a0b410f on letsencrypt:master.

@diracdeltas
Copy link
Contributor Author

Thanks for the comments and more useful error logging; looks like this is failing due to a 502 at https://www.letsencrypt-demo.org/acme/new-authz

@diracdeltas
Copy link
Contributor Author

FWIW, I am seeing the exact same error on my server when I run the Apache configurator on master @ a0b410f. So maybe the issue is that my domain name is not recognized as valid by Boulder, c.f. letsencrypt/boulder#115.

Traceback (most recent call last):
  File "venv/bin/letsencrypt", line 9, in <module>
    load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')()
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt
-0.1-py2.7.egg/letsencrypt/scripts/main.py", line 247, in main
    cert_key, cert_file, chain_file = acme.obtain_certificate(doms)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt
-0.1-py2.7.egg/letsencrypt/client/client.py", line 124, in obtain_certificate
    authzr = self.auth_handler.get_authorizations(domains)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt
-0.1-py2.7.egg/letsencrypt/client/auth_handler.py", line 69, in get_authorizations
    domain, self.account.new_authzr_uri)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt
-0.1-py2.7.egg/letsencrypt/client/network2.py", line 297, in request_domain_challenges
    typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt
-0.1-py2.7.egg/letsencrypt/client/network2.py", line 278, in request_challenges
    response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz))
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt
-0.1-py2.7.egg/letsencrypt/client/network2.py", line 143, in _post
    self._check_response(response, content_type=content_type)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt
-0.1-py2.7.egg/letsencrypt/client/network2.py", line 98, in _check_response
    raise errors.NetworkError(response)
letsencrypt.client.errors.NetworkError: <Response [502]>

@diracdeltas
Copy link
Contributor Author

Never mind, 502 was due to a temporary boulder error.

@diracdeltas
Copy link
Contributor Author

Now this seems to be working up to the point where DVSNI challenges fail ("Waiting for verification...")

Traceback (most recent call last):
  File "venv/bin/letsencrypt", line 9, in <module>
    load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')()
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/scripts/main.py", line 247, in main
    cert_key, cert_file, chain_file = acme.obtain_certificate(doms)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/client.py", line 124, in obtain_certificate
    authzr = self.auth_handler.get_authorizations(domains)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/auth_handler.py", line 79, in get_authorizations
    self._respond(cont_resp, dv_resp, best_effort)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/auth_handler.py", line 137, in _respond
    self._poll_challenges(chall_update, best_effort)
  File "/home/yan/lets-encrypt-preview/venv/local/lib/python2.7/site-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/auth_handler.py", line 187, in _poll_challenges
    "Failed Authorization procedure for %s" % domain)
letsencrypt.client.errors.AuthorizationError: Failed Authorization procedure for foo.icann.wtf

At this point, the nginx conf files are set up correctly for SSL (yay) and visiting https://foo.icann.wtf works with an invalid cert. However, cleanup never happens when the AuthorizationError is raised, so the DVSNI challenge virtual host is still around.

@kuba
Copy link
Contributor

kuba commented May 7, 2015

https://foo.icann.wtf/ -> "Apache2 Ubuntu Default Page"... Maybe your plugin doesn't really activate nginx?

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 92.42% when pulling 4dc566a on diracdeltas:feature/nginx-dvsni into a0b410f on letsencrypt:master.

@diracdeltas
Copy link
Contributor Author

@kuba, You're seeing that because my default folder is the Apache one. Check the server HTTP header - it should say "nginx/1.8" or something.

@diracdeltas
Copy link
Contributor Author

I fixed the error above and DVSNI seems to be basically working now. The challenge hosts are set up correctly and the challenges pass. However, boulder is currently out-of-sync with the client (both Apache and Nginx) due to confusion about whether a cert chain is served; @schoen is working on this right now.

In the ncurses console, I see:

                       Could not parse file: /etc/nginx/mime.types                          │
                      │ Performing the following challenges:                                 │
                      │   DVSNI challenge for foo.icann.wtf.                                 │
                      │   DVSNI challenge for letsencrypt.icann.wtf.                         │
                      │ Waiting for verification...                                          │
                      │ Cleaning up challenges                                               │
                      │ Could not parse file: /etc/nginx/mime.types                          │
                      │ Generating key (2048 bits):                                          │
                      │ /etc/letsencrypt/keys/0007_key-letsencrypt.pem                       │
                      │ Creating CSR: /etc/letsencrypt/certs/0007_csr-letsencrypt.pem        │
                      │ Server issued certificate; certificate written to                    │
                      │ /etc/letsencrypt/certs/0006_cert-letsencrypt.pem

"Could not parse mime.types" is a spurious error - it's not an nginx config file, so it shouldn't be parsed anyway. I should probably not log it as an error.

@kuba
Copy link
Contributor

kuba commented May 8, 2015

Fresh apt-get install nginx on jessie (I did not touch /etc/nginx at all).

  1. Plugin forces me to choose _ as the domain name, and hence boulder returns an expected syntax error:

    (venv)root@le:~/lets-encrypt-preview# letsencrypt -te -m admin@foo.com
    INFO:root:Generating key (2048 bits): /etc/letsencrypt/accounts/www.letsencrypt-demo.org/acme/new-reg/keys/0001_admin@foo.com
    WARNING:root:Could not parse file: /etc/nginx/mime.types
    
    How would you like to authenticate with the Let's Encrypt CA?
    -------------------------------------------------------------------------------
    1: Nginx Web Server
    2: Standalone Authenticator
    -------------------------------------------------------------------------------
    Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1
    
    Which names would you like to activate HTTPS for?
    -------------------------------------------------------------------------------
    1: _
    -------------------------------------------------------------------------------
    Select the appropriate numbers separated by commas and/or spaces (Enter 'c' to
    cancel):foo.com
    ** Error - Invalid selection **
    
    Which names would you like to activate HTTPS for?
    -------------------------------------------------------------------------------
    1: _
    -------------------------------------------------------------------------------
    Select the appropriate numbers separated by commas and/or spaces (Enter 'c' to
    cancel):1
    ERROR:root:Error: {u'detail': u'Error creating new authz: Syntax error'}
    ERROR:root:Response from server: {"detail":"Error creating new authz: Syntax error"}
    Traceback (most recent call last):
    File "/root/lets-encrypt-preview/venv/bin/letsencrypt", line 9, in <module>
      load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')()
    File "/root/lets-encrypt-preview/letsencrypt/scripts/main.py", line 247, in main
      cert_key, cert_file, chain_file = acme.obtain_certificate(doms)
    File "/root/lets-encrypt-preview/letsencrypt/client/client.py", line 124, in obtain_certificate
      authzr = self.auth_handler.get_authorizations(domains)
    File "/root/lets-encrypt-preview/letsencrypt/client/auth_handler.py", line 69, in get_authorizations
      domain, self.account.new_authzr_uri)
    File "/root/lets-encrypt-preview/letsencrypt/client/network2.py", line 297, in request_domain_challenges
      typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri)
    File "/root/lets-encrypt-preview/letsencrypt/client/network2.py", line 278, in request_challenges
      response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz))
    File "/root/lets-encrypt-preview/letsencrypt/client/network2.py", line 143, in _post
      self._check_response(response, content_type=content_type)
    File "/root/lets-encrypt-preview/letsencrypt/client/network2.py", line 92, in _check_response
      raise messages2.Error.from_json(jobj)
    letsencrypt.acme.messages2.Error: Error creating new authz: Syntax error
    
  2. but it doesn't work if I specify domain on the CLI either:

    (venv)root@le:~/lets-encrypt-preview# letsencrypt -ted foo.com
    WARNING:root:Could not parse file: /etc/nginx/mime.types
    
    How would you like to authenticate with the Let's Encrypt CA?
    -------------------------------------------------------------------------------
    1: Nginx Web Server
    2: Standalone Authenticator
    -------------------------------------------------------------------------------
    Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1
    INFO:root:Performing the following challenges:
    INFO:root:  DVSNI challenge for foo.com.
    ERROR:root:No nginx vhost exists with server_name or alias of: foo.com
    ERROR:root:No default 443 nginx vhost exists
    ERROR:root:Please specify server_names in the Nginx config
    Traceback (most recent call last):
    File "/root/lets-encrypt-preview/venv/bin/letsencrypt", line 9, in <module>
      load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')()
    File "/root/lets-encrypt-preview/letsencrypt/scripts/main.py", line 247, in main
      cert_key, cert_file, chain_file = acme.obtain_certificate(doms)
    File "/root/lets-encrypt-preview/letsencrypt/client/client.py", line 124, in obtain_certificate
      authzr = self.auth_handler.get_authorizations(domains)
    File "/root/lets-encrypt-preview/letsencrypt/client/auth_handler.py", line 75, in get_authorizations
      cont_resp, dv_resp = self._solve_challenges()
    File "/root/lets-encrypt-preview/letsencrypt/client/auth_handler.py", line 109, in _solve_challenges
      dv_resp = self.dv_auth.perform(self.dv_c)
    File "/root/lets-encrypt-preview/letsencrypt/client/plugins/nginx/configurator.py", line 519, in perform
      for i, resp in enumerate(sni_response):
    TypeError: 'NoneType' object is not iterable
    

@diracdeltas
Copy link
Contributor Author

@kuba Good catch. From http://nginx.org/en/docs/http/server_names.html#miscellaneous_names, it says _ is an arbitrary invalid server_name which will never intersect with any real name. So it is expected behavior that foo.com does not match any server block in your config.

I wasn't sure should happen in this case - should nginx just pick any block? Maybe something like:

  1. If there are no matching blocks, pick the first-occurring default_server that is listening on 443. If none of them are listening on 443, pick the first occurring default_server
  2. If there are no default servers, pick the first-occurring block that is listening on 443
  3. Otherwise pick the first-occurring block

@coveralls
Copy link

Coverage Status

Coverage increased (+0.19%) to 92.51% when pulling f7116a7 on diracdeltas:feature/nginx-dvsni into a0b410f on letsencrypt:master.

@kuba
Copy link
Contributor

kuba commented May 8, 2015

First of all, I would expect that it's possible to choose either a domain already found in configs or supply something completely new and proceed with the plugin. Is that what the current plugin architecture allows? @jdkasten?

It seems to me that any block with server_name _ should be completely ignored as it does not suggest any valid vhost names. Moreover, if domain provided (foo.com) doesn't match any existing block, fresh new block should be created (with server_name foo.com). I would guess that nginx config file specification gives precedence for all non-_ blocks, so you don't have to touch _ block at all to arrive at a working and secured configuration. However, configurator could optionally ask if any non-secured vhosts should be disabled.

@diracdeltas
Copy link
Contributor Author

Moreover, if domain provided (foo.com) doesn't match any existing block, fresh new block should be created (with server_name foo.com).

That seems preferable

@diracdeltas
Copy link
Contributor Author

I would guess that nginx config file specification gives precedence for all non-_ blocks, so you don't have to touch _ block at all to arrive at a working and secured configuration. However, configurator could optionally ask if any non-secured vhosts should be disabled.

According to http://nginx.org/en/docs/http/request_processing.html, if there are no matches then the request is handled by the default block which is either the first server block in the config or the first server block marked default_server. So adding a server block for foo.com will override the server block _ for requests to foo.com.

Disabling non-secured vhosts seems like a potential enhancement function.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 92.52% when pulling dfb9461 on diracdeltas:feature/nginx-dvsni into a0b410f on letsencrypt:master.

@kuba
Copy link
Contributor

kuba commented May 9, 2015

I got it working with #397 patch applied! :)

(venv)root@le:~# letsencrypt -te      
Enter email address (optional, press Enter to skip) (Enter 'c' to cancel):     
INFO:root:Generating key (2048 bits): /etc/letsencrypt/accounts/www.letsencrypt-demo.org/acme/new-reg/keys/0000_default

How would you like to authenticate with the Let's Encrypt CA?
-------------------------------------------------------------------------------
1: Nginx Web Server
2: Standalone Authenticator
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1

-------------------------------------------------------------------------------
No names were found in your configuration files.
You should specify ServerNames in your config files in order to allow for
accurate installation of your certificate.
If you do use the default vhost, you may specify the name manually. Would you
like to continue?
-------------------------------------------------------------------------------
(Y)es/(N)o: Y
Please enter in your domain name(s) (comma and/or space separated)  (Enter 'c'
to cancel):foo.com
INFO:root:Performing the following challenges:
INFO:root:  DVSNI challenge for foo.com.
INFO:root:Waiting for verification...
INFO:root:Cleaning up challenges
INFO:root:Generating key (2048 bits): /etc/letsencrypt/keys/0000_key-letsencrypt.pem
INFO:root:Creating CSR: /etc/letsencrypt/certs/0000_csr-letsencrypt.pem
INFO:root:Server issued certificate; certificate written to /etc/letsencrypt/certs/0000_cert-letsencrypt.pem
INFO:root:Cert chain written to /etc/letsencrypt/certs/0000_chain-letsencrypt.pem
INFO:root:Deployed Certificate to VirtualHost /etc/nginx/nginx.conf for set(['foo.com'])

-------------------------------------------------------------------------------
Congratulations! You have successfully enabled https://foo.com!
-------------------------------------------------------------------------------

Is it possible to put new server blocks in separate files in sites-available and symlinks in sites-enabled?

Is the following error recoverable? Can we handle it somehow?

(venv)root@le:~# rm -rf /etc/letsencrypt 
(venv)root@le:~# letsencrypt -te
Enter email address (optional, press Enter to skip) (Enter 'c' to cancel):
INFO:root:Generating key (2048 bits): /etc/letsencrypt/accounts/www.letsencrypt-demo.org/acme/new-reg/keys/0000_default

How would you like to authenticate with the Let's Encrypt CA?
-------------------------------------------------------------------------------
1: Nginx Web Server
2: Standalone Authenticator
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1

Which names would you like to activate HTTPS for?
-------------------------------------------------------------------------------
1: foo.com
-------------------------------------------------------------------------------
Select the appropriate numbers separated by commas and/or spaces (Enter 'c' to
cancel):1
INFO:root:Performing the following challenges:
INFO:root:  DVSNI challenge for foo.com.
ERROR:root:Nginx Restart Failed!
ERROR:root:
ERROR:root:nginx: [emerg] BIO_new_file("/etc/letsencrypt/certs/0001_cert-letsencrypt.pem") failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/etc/letsencrypt/certs/0001_cert-letsencrypt.pem','r') error:2006D080:BIO routines:BIO_new_file:no such file)

INFO:root:Waiting for verification...
Traceback (most recent call last):
  File "/root/lets-encrypt-preview/venv/bin/letsencrypt", line 9, in <module>
    load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')()
  File "/root/lets-encrypt-preview/letsencrypt/scripts/main.py", line 247, in main
    cert_key, cert_file, chain_file = acme.obtain_certificate(doms)
  File "/root/lets-encrypt-preview/letsencrypt/client/client.py", line 124, in obtain_certificate
    authzr = self.auth_handler.get_authorizations(domains)
  File "/root/lets-encrypt-preview/letsencrypt/client/auth_handler.py", line 79, in get_authorizations
    self._respond(cont_resp, dv_resp, best_effort)
  File "/root/lets-encrypt-preview/letsencrypt/client/auth_handler.py", line 137, in _respond
    self._poll_challenges(chall_update, best_effort)
  File "/root/lets-encrypt-preview/letsencrypt/client/auth_handler.py", line 187, in _poll_challenges
    "Failed Authorization procedure for %s" % domain)
letsencrypt.client.errors.AuthorizationError: Failed Authorization procedure for foo.com

I also experience some other spurious heisen bugs, so I'd recommend more testing.

@diracdeltas
Copy link
Contributor Author

Is it possible to put new server blocks in separate files in sites-available and symlinks in sites-enabled?

sites-available and sites-enabled are not guaranteed to exist in nginx. They are often included by convention to mimic apache, but for instance, there is no equivalent of 'a2ensite'. We could create them for people and move their ssl-ified server blocks into separate files, but I feel like this is too intrusive (developers might have legit reasons for not wanting the site directories).

@diracdeltas
Copy link
Contributor Author

Is the following error recoverable? Can we handle it somehow?

Is that a sporadic or consistent error? Does the file actually exist?

@kuba
Copy link
Contributor

kuba commented May 11, 2015

As you can see I did rm -rf /etc/letsencrypt after doing a successful configuration and before attempting it once again. So all my keys were lost in the meantime. It shouldn't cause the entire plugin to panic though. It should be entirely possible to continue and issue new certs.

@diracdeltas
Copy link
Contributor Author

Oh, I see. Nginx hard-fails on start if the SSL cert files are missing, which seems correct on its part. I'll remove directives referencing missing files in /etc/letsencrypt in a prepare_to_restart method.

@diracdeltas
Copy link
Contributor Author

I'll remove directives referencing missing files in /etc/letsencrypt in a prepare_to_restart method.

Probably better to do so in a separate pull request. I can add a TODO note in this one if you'd prefer.

@jdkasten
Copy link
Contributor

Awesome work, @diracdeltas! Sorry I have been so slow to get around to this...

@kuba Block selection (or VirtualHost selection) /domains is a major section I have been trying to tackle in my local Apache branch. It should probably be possible in the end to use names found within the server blocks or present a menu of choices if none match. (Optionally add names that were not found etc.) This still isn't implemented completely in Apache and I believe the current master branch relies extremely heavily on names present in the configs. (It is the only option)

@jdkasten
Copy link
Contributor

I think a separate pull request would be fine. I think this is fine to merge in now and hopefully get some more exposure.
Sorry again for the delay.

jdkasten added a commit that referenced this pull request May 12, 2015
@jdkasten jdkasten merged commit 8229c72 into certbot:master May 12, 2015
@jdkasten
Copy link
Contributor

Also thanks @kuba for reviewing and for helping to test it out!

@diracdeltas diracdeltas mentioned this pull request May 12, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants