Repository: certbot/certbot

Issue:
Audit nginx plugin for guaranteed config reversion in case of error
This is the nginx equivalent of #1394 



Discussion:
This is being worked on by @tcz001 

Just need some confirm, is this issue only related with all `try catch` containing file dump?
If yes, the pull request https://github.com/letsencrypt/letsencrypt/pull/1457 fixed most places we throw any error related with file dump process.

The missing part is [here](https://github.com/twstrike/letsencrypt/commit/cc3ab2142ef531bc1f6b0198745a272b52be324f), most revert functions are missing error handling in nginx_configurator (which is taken in apache_augeas_configurator)

In the Apache case, we had to think about config file writing as well as symlink generation in `/etc/apache/sites-enabled`, module enablement and a few things.  I'm not sure if nginx has all of those cases, but it may have the symlinks on some systems?

We had to think about `try except` wrappers for everything, including cases like control-c triggering an exit.

Actually another thing that was important in the Apache case was making sure that the checkpoint we rolled back to was the correct one.  That was tricky because with the `run` subcommand (the default) we would make temporary config changes for authentication, and then permanent config changes for installation, and in some cases we had bugs where we would roll back to the wrong one.

Yes you're right, Nginx also uses symlinks in site-enabled as best
practice.

2016年1月26日 下午1:07，"Peter Eckersley" notifications@github.com写道：

> In the Apache case, we had to think about config file writing as well as
> symlink generation in /etc/apache/sites-enabled, module enablement and a
> few things. I'm not sure if nginx has all of those cases, but it may have
> the symlinks on some systems?
> 
> We had to think about try except wrappers for everything, including cases
> like control-c triggering an exit.
> 
> —
> Reply to this email directly or view it on GitHub.

AFAICS the temporary checkpoint is working as expectation. The installation
step sometimes make two permanent checkpoints while choosing second strict
mode. I can try to write some test to see if that's a problem when catching
error.
2016年1月26日 下午1:17，"蒋颿" fan.torchz@gmail.com写道：

> Yes you're right, Nginx also uses symlinks in site-enabled as best
> practice.
> 
> 2016年1月26日 下午1:07，"Peter Eckersley" notifications@github.com写道：
> 
> > In the Apache case, we had to think about config file writing as well as
> > symlink generation in /etc/apache/sites-enabled, module enablement and a
> > few things. I'm not sure if nginx has all of those cases, but it may have
> > the symlinks on some systems?
> > 
> > We had to think about try except wrappers for everything, including
> > cases like control-c triggering an exit.
> > 
> > —
> > Reply to this email directly or view it on GitHub.

This [PR](https://github.com/letsencrypt/letsencrypt/pull/2282) should fix most of problem and show the OSError and IOError message in a Plugin Error, 

I'm sorry I wasn't around to answer questions about this.

The goal here is to ensure that the Nginx plugin can always restore your config. If the plugin errors out while performing an operation, all in progress changes should be reverted. Additionally, the user should be able to run `letsencrypt rollback` to undo changes caused by the Nginx plugin on previous runs of the client.

To do this, we need to ensure the Nginx plugin uses the `reverter` properly which I have verified it isn't. I don't think there are too many problems, but I found at least one after some quick testing.

There are three main cases we care about: `perform`, `deploy_cert` and `enhance`. Common to all of these cases, `letsencrypt` first instantiates the Nginx plugin and then calls `prepare`. After this, the functions that may have been called on the Nginx plugin start to diverge.

| Modifying function(s) | Recovery function | Desired outcome |
| --- | --- | --- |
| `perform` | `cleanup` | After `cleanup` is called, either when `perform` returns or raises an exception, all changes to the Nginx configuration should be reverted. Running `letsencrypt certonly` with Nginx should NEVER leave permanent changes to the user's config. |
| `deploy_cert` followed by `save` | `recovery_routine` | Similar to the example above, `recovery_routine` is called if `deploy_cert`/`save` raises an exception. When this occurs, all changes to the Nginx configuration should be reverted and the user's Nginx config should be identical to before their last run of the client. |
| `enhance` followed by `save` | `recovery_routine` | Same as above, except `enhance` can only by a user after calling `deploy_cert` + `save`. When `recovery_routine` is called here, their config should be reverted to the state after the calls to `deploy_cert` and `save`. |

You don't have to worry about calling the recovery function in the table above. The core client handles that for you, however, you need to make sure that all of the changes you made are reverted properly.

The error I found occurred in the `perform` case. On a Debian Jessie docker image, I installed `nginx-light` and ran `letsencrypt certonly --nginx`, providing valid domain names when prompted. When `letsencrypt` exited, my config had been modified.

I hope this all makes sense. Please let me know if you have any questions.

[This comment](https://github.com/letsencrypt/letsencrypt/pull/2510#issue-134968581) provides more information on the problem I found with the Nginx plugin while testing. There may or may not be more problems with Nginx reversion, but the issue described above and linked comment is the problem I found.

For a little more information, in Apache we duplicate some work when someone calls `letsencrypt run` which is the use of the `IAuthenticator` functions followed by the `IInstaller` functions from `letsencrypt/interfaces.py`. `letsencrypt certonly` only uses the `IAuthenticator` functions and after `cleanup` is called, the user's config should be back to it's initial state.

Thanks for inputs. That example makes sense, I will recheck the plugin.


Resolving patch:
diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py
--- a/certbot-nginx/certbot_nginx/tls_sni_01.py
+++ b/certbot-nginx/certbot_nginx/tls_sni_01.py
@@ -46,8 +46,6 @@ def perform(self):
         if not self.achalls:
             return []
 
-        self.configurator.save()
-
         addresses = []
         default_addr = "{0} default_server ssl".format(
             self.configurator.config.tls_sni_01_port)


Test patch:
diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py
--- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py
+++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py
@@ -80,7 +80,7 @@ def test_perform1(self, mock_save):
 
         mock_setup_cert.assert_called_once_with(self.achalls[0])
         self.assertEqual([response], responses)
-        self.assertEqual(mock_save.call_count, 2)
+        self.assertEqual(mock_save.call_count, 1)
 
         # Make sure challenge config is included in main config
         http = self.sni.configurator.parser.parsed[