Repository: certbot/certbot
Pull request: Change certbot-auto's installation path to /opt
In general, the purpose of this PR is to move `certbot-auto`'s install path to a single location to help people who have multiple installations and prep for #3522. This also fixes #3480.

I've broken design decisions into sections so reviewers or others in the future can read why we made specific decisions that they may be interested in. This PR should probably get 2 reviews due to its risk of breaking all the things.

#### Why /opt/eff.org/certbot/venv?

Per my understanding of the [Linux FHS](http://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html), `/opt` is the best place for these files. This approach is also often used on other UNIX like systems.

The recommended layout of an installation in `/opt` is described [here](http://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s13.html). I avoided going with `/opt/<package>` (`/opt/certbot`) to avoid conflicts with sysadmins who may have reasonably installed Certbot there.

Instead, I went with `/opt/<provider>`. While we don't have a LANANA provider name, [this page](http://www.lanana.org/lsbreg/providers/index.html) says we can use our fully qualified domain.

Finally, the layout within `/opt/eff.org` was chosen to allow EFF to package other software here in the future and `certbot/venv` was created per Erik's thought that we may want to put other things in `/opt/eff.org/certbot` in the future. The layout of an `/opt/<provider>` directory is not specified by FHS.

#### FHS deviations

The only one that I'm aware of is by installing ourselves in `/opt` we are supposed to use `/var/opt`, `/etc/opt`, etc., however, this is a major breaking change and I don't think we should do this (now).

#### Use of `sudo -E`

sudo usually preserves $HOME.

We allow people to place Certbot configuration files in their home directory (see https://certbot.eff.org/docs/using.html#configuration-file).

Certbot configuration files can contain hooks.

∴ certbot-auto already allows arbitrary code execution as root if an attacker can modify the files of an unprivileged user who runs certbot-auto using sudo so an attacker gains no new privileges by us preserving the environment.

I plan to document this in our user guide, however, since this behavior is not new, I don't think it should go in this large PR. I will document us preserving environment variables with `SUDO` (which `su` already does) in the same location.

#### Symlink from old venv path to new

I didn't do this if the old venv doesn't exist. This could cause problems for people for automated integrations installing `certbot-auto` and reaching into the venv directly. I'm not sure these people exist and I think this approach is cleaner, however, I don't have a strong preference and am happy to modify this PR so we always create the symlink.

#### certbot-auto test changes

A number of changes were necessary.

1. The tests used `venv_path` for both the path to the `letsencrypt-auto` script and the path to the virtual environment. Despite `letsencrypt-auto` deleting its venv, we were able to get away with this because we didn't actually set `VENV_PATH` but instead used `XDG_DATA_HOME`. Now that this variable isn't used in the same way, I split this to have separate paths to the venv and le-auto.
2. Because `certbot-auto` now does more as `root`, we sometimes can't delete directories used in testing when running as an unprivileged user. I fixed this by setting `ignore_errors=True` in the call to `rmtree` which shouldn't matter because we run these tests in Docker and they already do much more aggressive things like invoking your package manager.
3. In a similar vein, I had to split up one of the tests as the unprivileged tests don't have the permission to modify the venv owned as root.

#### Additional testing

I've done a lot of manual and hackily automated testing. In particular, I've tested the `su`, `sudo`, and `root` cases, both with and without existing Certbot installations on Debian, Red Hat, and FreeBSD systems. I've also successfully tested a modified `test_leauto_upgrades.sh` script with the following code:
```
if [ "$(tools/readlink.py ~/.local/share/letsencrypt)" != "/opt/eff.org/certbot/venv" ]; then
    echo symlink from old venv path not properly created!
    exit 1
fi
```
We should make this change after/during the release.

#### Warnings from pip

This should be fixed with erikrose/pipstrap#11. We already disable the cache when we invoke `pip` in certbot-auto.

Problem statement:
certbot-auto doesn't appear to respect HTTPS_PROXY env var
Hello, thanks for certbot! I'm attempting to use it behind an https proxy, and it hangs after asking me for my email address, eventually erroring out with this message:

```
ConnectionError: HTTPSConnectionPool(host='acme-v01.api.letsencrypt.org', port=443): Max retries exceeded with url: /directory (Caused by NewConnectionError('<requests.packages.urllib3.connection.VerifiedHTTPSConnection object at 0x7f7f4f42f510>: Failed to establish a new connection: [Errno 110] Connection timed out',))
```

I'm fairly confident that this is due to it not using the https proxy set in both `https_proxy` and `HTTPS_PROXY` environment variables. Any ideas?

Hints from issue discussion:
Certbot uses `python-requests` to communicate with the server which [respects the HTTPS_PROXY env var](http://docs.python-requests.org/en/master/user/advanced/#proxies).

Are you using `letsencrypt-auto`/`certbot-auto`? If so, I suspect the problem is `sudo`. The script uses `sudo` internally to invoke Certbot which will drop your environment variables by default.

Assuming the subject is correct, certbot-auto could probably warn if it detects HTTPS_PROXY and has selected sudo...
Another thing we could do here is to run certbot with something like `sudo HTTPS_PROXY=$HTTPS_PROXY certbot ...` if we detect the variable is set. This way, it continues to work and the user doesn't have to know/worry about it.
I'm getting the exact same error, but having no `HTTPS_PROXY` set. Any idea how to debug?
Certbot is unable to communicate with Let's Encrypt. On your server where you are having this problem, what is the output of `curl https://acme-v01.api.letsencrypt.org/directory`?
```
{
  "key-change": "https://acme-v01.api.letsencrypt.org/acme/key-change",
  "new-authz": "https://acme-v01.api.letsencrypt.org/acme/new-authz",
  "new-cert": "https://acme-v01.api.letsencrypt.org/acme/new-cert",
  "new-reg": "https://acme-v01.api.letsencrypt.org/acme/new-reg",
  "revoke-cert": "https://acme-v01.api.letsencrypt.org/acme/revoke-cert"
}
```
I see that I have lied to you. It's not the exact same error, only rather similar:

```
ConnectionError: HTTPSConnectionPool(host='acme-staging.api.letsencrypt.org', port=443): Max retries exceeded with url: /directory (Caused by NewConnectionError('<requests.packages.urllib3.connection.VerifiedHTTPSConnection object at 0x7fb7eba77910>: Failed to establish a new connection: [Errno -2] Name or service not known',))
```

Commit message: Remove SUDO_ENV.

Patch to review:
diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto
index 023d5044e25..c894f372faa 100755
--- a/letsencrypt-auto-source/letsencrypt-auto
+++ b/letsencrypt-auto-source/letsencrypt-auto
@@ -23,9 +23,11 @@ fi
 if [ -z "$XDG_DATA_HOME" ]; then
   XDG_DATA_HOME=~/.local/share
 fi
-VENV_NAME="letsencrypt"
 if [ -z "$VENV_PATH" ]; then
-  VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
+  # We export these values so they are preserved properly if this script is
+  # rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value.
+  export OLD_VENV_PATH="$XDG_DATA_HOME/letsencrypt"
+  export VENV_PATH="/opt/eff.org/certbot/venv"
 fi
 VENV_BIN="$VENV_PATH/bin"
 LE_AUTO_VERSION="0.17.0.dev0"
@@ -49,6 +51,7 @@ Help for certbot itself cannot be provided until it is installed.
                                             implies --non-interactive
 
 All arguments are accepted and forwarded to the Certbot client when run."
+export CERTBOT_AUTO="$0"
 
 for arg in "$@" ; do
   case "$arg" in
@@ -119,12 +122,14 @@ else
   exit 1
 fi
 
-# certbot-auto needs root access to bootstrap OS dependencies, and
-# certbot itself needs root access for almost all modes of operation
-# The "normal" case is that sudo is used for the steps that need root, but
-# this script *can* be run as root (not recommended), or fall back to using
-# `su`. Auto-detection can be overridden by explicitly setting the
-# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
+# Certbot itself needs root access for almost all modes of operation.
+# certbot-auto needs root access to bootstrap OS dependencies and install
+# Certbot a protected path so it can be safely run as root. To accomplish this,
+# this script will attempt to run itself as root if it doesn't have the
+# necessary privileges by using `sudo` or falling back to `su` if it is not
+# available. The mechanism used to obtain root access can set explicitly by the
+# user by overriding the environment variable LE_AUTO_SUDO to 'sudo',
+# 'su_sudo', or '' as used below.
 
 # Because the parameters in `su -c` has to be a string,
 # we need to properly escape it.
@@ -147,35 +152,38 @@ su_sudo() {
   su root -c "$args"
 }
 
-SUDO_ENV=""
-export CERTBOT_AUTO="$0"
-if [ -n "${LE_AUTO_SUDO+x}" ]; then
-  case "$LE_AUTO_SUDO" in
-    su_sudo|su)
-      SUDO=su_sudo
-      ;;
-    sudo)
-      SUDO=sudo
-      SUDO_ENV="CERTBOT_AUTO=$0"
-      ;;
-    '') ;; # Nothing to do for plain root method.
-    *)
-      error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
-      exit 1
-  esac
-  say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
-else
-  if test "`id -u`" -ne "0" ; then
-    if $EXISTS sudo 1>/dev/null 2>&1; then
-      SUDO=sudo
-      SUDO_ENV="CERTBOT_AUTO=$0"
+if [ "$1" = "--cb-auto-has-root" ]; then
+  shift 1
+elif [ "$1" != "--le-auto-phase2" ]; then
+  # if $1 is --le-auto-phase2, we've executed this branch before
+  if [ -n "${LE_AUTO_SUDO+x}" ]; then
+    case "$LE_AUTO_SUDO" in
+      su_sudo|su)
+        SUDO=su_sudo
+        ;;
+      sudo)
+        SUDO="sudo -E"
+        ;;
+      '') ;; # Nothing to do for plain root method.
+      *)
+        error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
+        exit 1
+    esac
+    say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
+  else
+    if test "`id -u`" -ne "0" ; then
+      if $EXISTS sudo 1>/dev/null 2>&1; then
+        SUDO="sudo -E"
+      else
+        say \"sudo\" is not available, will use \"su\" for installation steps...
+        SUDO=su_sudo
+      fi
     else
-      say \"sudo\" is not available, will use \"su\" for installation steps...
-      SUDO=su_sudo
+      SUDO=
     fi
-  else
-    SUDO=
   fi
+  $SUDO "$0" --cb-auto-has-root "$@"
+  exit 0
 fi
 
 BootstrapMessage() {
@@ -261,7 +269,7 @@ BootstrapDebCommon() {
     QUIET_FLAG='-qq'
   fi
 
-  $SUDO apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
+  apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
 
   # virtualenv binary can be found in different packages depending on
   # distro version (#346)
@@ -311,13 +319,13 @@ BootstrapDebCommon() {
           esac
         fi
         if [ "$add_backports" = 1 ]; then
-          $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
-          $SUDO apt-get $QUIET_FLAG update
+          sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
+          apt-get $QUIET_FLAG update
         fi
       fi
     fi
     if [ "$add_backports" != 0 ]; then
-      $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
+      apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
       augeas_pkg=
     fi
   }
@@ -336,7 +344,7 @@ BootstrapDebCommon() {
     # XXX add a case for ubuntu PPAs
   fi
 
-  $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
+  apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
     python \
     python-dev \
     $virtualenv \
@@ -380,9 +388,9 @@ BootstrapRpmCommon() {
     QUIET_FLAG='--quiet'
   fi
 
-  if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then
+  if ! $tool list *virtualenv >/dev/null 2>&1; then
     echo "To use Certbot, packages from the EPEL repository need to be installed."
-    if ! $SUDO $tool list epel-release >/dev/null 2>&1; then
+    if ! $tool list epel-release >/dev/null 2>&1; then
       error "Enable the EPEL repository and try running Certbot again."
       exit 1
     fi
@@ -394,7 +402,7 @@ BootstrapRpmCommon() {
       /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..."
       sleep 1s
     fi
-    if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then
+    if ! $tool install $yes_flag $QUIET_FLAG epel-release; then
       error "Could not enable EPEL. Aborting bootstrap!"
       exit 1
     fi
@@ -412,7 +420,7 @@ BootstrapRpmCommon() {
 
   # Some distros and older versions of current distros use a "python27"
   # instead of "python" naming convention. Try both conventions.
-  if $SUDO $tool list python >/dev/null 2>&1; then
+  if $tool list python >/dev/null 2>&1; then
     pkgs="$pkgs
       python
       python-devel
@@ -430,13 +438,13 @@ BootstrapRpmCommon() {
     "
   fi
 
-  if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then
+  if $tool list installed "httpd" >/dev/null 2>&1; then
     pkgs="$pkgs
       mod_ssl
     "
   fi
 
-  if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then
+  if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then
     error "Could not install OS dependencies. Aborting bootstrap!"
     exit 1
   fi
@@ -454,7 +462,7 @@ BootstrapSuseCommon() {
     QUIET_FLAG='-qq'
   fi
 
-  $SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \
+  zypper $QUIET_FLAG $zypper_flags in $install_flags \
     python \
     python-devel \
     python-virtualenv \
@@ -485,7 +493,7 @@ BootstrapArchCommon() {
   "
 
   # pacman -T exits with 127 if there are missing dependencies
-  missing=$($SUDO pacman -T $deps) || true
+  missing=$(pacman -T $deps) || true
 
   if [ "$ASSUME_YES" = 1 ]; then
     noconfirm="--noconfirm"
@@ -493,9 +501,9 @@ BootstrapArchCommon() {
 
   if [ "$missing" ]; then
     if [ "$QUIET" = 1]; then
-      $SUDO pacman -S --needed $missing $noconfirm > /dev/null
+      pacman -S --needed $missing $noconfirm > /dev/null
     else
-      $SUDO pacman -S --needed $missing $noconfirm
+      pacman -S --needed $missing $noconfirm
     fi
   fi
 }
@@ -517,13 +525,13 @@ BootstrapGentooCommon() {
 
   case "$PACKAGE_MANAGER" in
     (paludis)
-      $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
+      cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
       ;;
     (pkgcore)
-      $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
+      pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
       ;;
     (portage|*)
-      $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
+      emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
       ;;
   esac
 }
@@ -533,7 +541,7 @@ BootstrapFreeBsd() {
     QUIET_FLAG="--quiet"
   fi
 
-  $SUDO pkg install -Ay $QUIET_FLAG \
+  pkg install -Ay $QUIET_FLAG \
     python \
     py27-virtualenv \
     augeas \
@@ -548,7 +556,7 @@ BootstrapMac() {
   elif hash port 2>/dev/null; then
     say "Using MacPorts to install dependencies..."
     pkgman=port
-    pkgcmd="$SUDO port install"
+    pkgcmd="port install"
   else
     say "No Homebrew/MacPorts; installing Homebrew..."
     ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
@@ -568,8 +576,8 @@ BootstrapMac() {
   # Workaround for _dlopen not finding augeas on macOS
   if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
     say "Applying augeas workaround"
-    $SUDO mkdir -p /usr/local/lib/
-    $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
+    mkdir -p /usr/local/lib/
+    ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
   fi
 
   if ! hash pip 2>/dev/null; then
@@ -595,7 +603,7 @@ BootstrapMageiaCommon() {
     QUIET_FLAG='--quiet'
   fi
 
-  if ! $SUDO urpmi --force $QUIET_FLAG \
+  if ! urpmi --force $QUIET_FLAG \
       python \
       libpython-devel \
       python-virtualenv
@@ -604,7 +612,7 @@ BootstrapMageiaCommon() {
       exit 1
   fi
 
-  if ! $SUDO urpmi --force $QUIET_FLAG \
+  if ! urpmi --force $QUIET_FLAG \
       git \
       gcc \
       python-augeas \
@@ -1100,20 +1108,15 @@ UNLIKELY_EOF
       rm -rf "$VENV_PATH"
       exit 1
     fi
+
+    if [ -d "$OLD_VENV_PATH" -a ! -L "$OLD_VENV_PATH" ]; then
+      rm -rf "$OLD_VENV_PATH"
+      ln -s "$VENV_PATH" "$OLD_VENV_PATH"
+    fi
+
     say "Installation succeeded."
   fi
-  if [ -n "$SUDO" ]; then
-    # SUDO is su wrapper or sudo
-    say "Requesting root privileges to run certbot..."
-    say "  $VENV_BIN/letsencrypt" "$@"
-  fi
-  if [ -z "$SUDO_ENV" ] ; then
-    # SUDO is su wrapper / noop
-    $SUDO "$VENV_BIN/letsencrypt" "$@"
-  else
-    # sudo
-    $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@"
-  fi
+  "$VENV_BIN/letsencrypt" "$@"
 
 else
   # Phase 1: Upgrade certbot-auto if necessary, then self-invoke.
@@ -1124,12 +1127,14 @@ else
   # package). Phase 2 checks the version of the locally installed certbot.
 
   if [ ! -f "$VENV_BIN/letsencrypt" ]; then
-    if [ "$HELP" = 1 ]; then
-      echo "$USAGE"
-      exit 0
+    if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then
+      if [ "$HELP" = 1 ]; then
+        echo "$USAGE"
+        exit 0
+      fi
+      # If it looks like we've never bootstrapped before, bootstrap:
+      Bootstrap
     fi
-    # If it looks like we've never bootstrapped before, bootstrap:
-    Bootstrap
   fi
   if [ "$OS_PACKAGES_ONLY" = 1 ]; then
     say "OS packages installed."
@@ -1289,13 +1294,13 @@ UNLIKELY_EOF
       say "Replacing certbot-auto..."
       # Clone permissions with cp. chmod and chown don't have a --reference
       # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
-      $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
-      $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
+      cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
+      cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
       # Using mv rather than cp leaves the old file descriptor pointing to the
       # original copy so the shell can continue to read it unmolested. mv across
       # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the
-      # cp is unlikely to fail (esp. under sudo) if the rm doesn't.
-      $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
+      # cp is unlikely to fail if the rm doesn't.
+      mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
     fi  # A newer version is available.
   fi  # Self-upgrading is allowed.
 
diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template
index 284241a8213..190a3505e9c 100755
--- a/letsencrypt-auto-source/letsencrypt-auto.template
+++ b/letsencrypt-auto-source/letsencrypt-auto.template
@@ -23,9 +23,11 @@ fi
 if [ -z "$XDG_DATA_HOME" ]; then
   XDG_DATA_HOME=~/.local/share
 fi
-VENV_NAME="letsencrypt"
 if [ -z "$VENV_PATH" ]; then
-  VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
+  # We export these values so they are preserved properly if this script is
+  # rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value.
+  export OLD_VENV_PATH="$XDG_DATA_HOME/letsencrypt"
+  export VENV_PATH="/opt/eff.org/certbot/venv"
 fi
 VENV_BIN="$VENV_PATH/bin"
 LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}"
@@ -49,6 +51,7 @@ Help for certbot itself cannot be provided until it is installed.
                                             implies --non-interactive
 
 All arguments are accepted and forwarded to the Certbot client when run."
+export CERTBOT_AUTO="$0"
 
 for arg in "$@" ; do
   case "$arg" in
@@ -119,12 +122,14 @@ else
   exit 1
 fi
 
-# certbot-auto needs root access to bootstrap OS dependencies, and
-# certbot itself needs root access for almost all modes of operation
-# The "normal" case is that sudo is used for the steps that need root, but
-# this script *can* be run as root (not recommended), or fall back to using
-# `su`. Auto-detection can be overridden by explicitly setting the
-# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
+# Certbot itself needs root access for almost all modes of operation.
+# certbot-auto needs root access to bootstrap OS dependencies and install
+# Certbot a protected path so it can be safely run as root. To accomplish this,
+# this script will attempt to run itself as root if it doesn't have the
+# necessary privileges by using `sudo` or falling back to `su` if it is not
+# available. The mechanism used to obtain root access can set explicitly by the
+# user by overriding the environment variable LE_AUTO_SUDO to 'sudo',
+# 'su_sudo', or '' as used below.
 
 # Because the parameters in `su -c` has to be a string,
 # we need to properly escape it.
@@ -147,35 +152,38 @@ su_sudo() {
   su root -c "$args"
 }
 
-SUDO_ENV=""
-export CERTBOT_AUTO="$0"
-if [ -n "${LE_AUTO_SUDO+x}" ]; then
-  case "$LE_AUTO_SUDO" in
-    su_sudo|su)
-      SUDO=su_sudo
-      ;;
-    sudo)
-      SUDO=sudo
-      SUDO_ENV="CERTBOT_AUTO=$0"
-      ;;
-    '') ;; # Nothing to do for plain root method.
-    *)
-      error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
-      exit 1
-  esac
-  say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
-else
-  if test "`id -u`" -ne "0" ; then
-    if $EXISTS sudo 1>/dev/null 2>&1; then
-      SUDO=sudo
-      SUDO_ENV="CERTBOT_AUTO=$0"
+if [ "$1" = "--cb-auto-has-root" ]; then
+  shift 1
+elif [ "$1" != "--le-auto-phase2" ]; then
+  # if $1 is --le-auto-phase2, we've executed this branch before
+  if [ -n "${LE_AUTO_SUDO+x}" ]; then
+    case "$LE_AUTO_SUDO" in
+      su_sudo|su)
+        SUDO=su_sudo
+        ;;
+      sudo)
+        SUDO="sudo -E"
+        ;;
+      '') ;; # Nothing to do for plain root method.
+      *)
+        error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
+        exit 1
+    esac
+    say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
+  else
+    if test "`id -u`" -ne "0" ; then
+      if $EXISTS sudo 1>/dev/null 2>&1; then
+        SUDO="sudo -E"
+      else
+        say \"sudo\" is not available, will use \"su\" for installation steps...
+        SUDO=su_sudo
+      fi
     else
-      say \"sudo\" is not available, will use \"su\" for installation steps...
-      SUDO=su_sudo
+      SUDO=
     fi
-  else
-    SUDO=
   fi
+  $SUDO "$0" --cb-auto-has-root "$@"
+  exit 0
 fi
 
 BootstrapMessage() {
@@ -385,20 +393,15 @@ UNLIKELY_EOF
       rm -rf "$VENV_PATH"
       exit 1
     fi
+
+    if [ -d "$OLD_VENV_PATH" -a ! -L "$OLD_VENV_PATH" ]; then
+      rm -rf "$OLD_VENV_PATH"
+      ln -s "$VENV_PATH" "$OLD_VENV_PATH"
+    fi
+
     say "Installation succeeded."
   fi
-  if [ -n "$SUDO" ]; then
-    # SUDO is su wrapper or sudo
-    say "Requesting root privileges to run certbot..."
-    say "  $VENV_BIN/letsencrypt" "$@"
-  fi
-  if [ -z "$SUDO_ENV" ] ; then
-    # SUDO is su wrapper / noop
-    $SUDO "$VENV_BIN/letsencrypt" "$@"
-  else
-    # sudo
-    $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@"
-  fi
+  "$VENV_BIN/letsencrypt" "$@"
 
 else
   # Phase 1: Upgrade certbot-auto if necessary, then self-invoke.
@@ -409,12 +412,14 @@ else
   # package). Phase 2 checks the version of the locally installed certbot.
 
   if [ ! -f "$VENV_BIN/letsencrypt" ]; then
-    if [ "$HELP" = 1 ]; then
-      echo "$USAGE"
-      exit 0
+    if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then
+      if [ "$HELP" = 1 ]; then
+        echo "$USAGE"
+        exit 0
+      fi
+      # If it looks like we've never bootstrapped before, bootstrap:
+      Bootstrap
     fi
-    # If it looks like we've never bootstrapped before, bootstrap:
-    Bootstrap
   fi
   if [ "$OS_PACKAGES_ONLY" = 1 ]; then
     say "OS packages installed."
@@ -445,13 +450,13 @@ UNLIKELY_EOF
       say "Replacing certbot-auto..."
       # Clone permissions with cp. chmod and chown don't have a --reference
       # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
-      $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
-      $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
+      cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
+      cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
       # Using mv rather than cp leaves the old file descriptor pointing to the
       # original copy so the shell can continue to read it unmolested. mv across
       # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the
-      # cp is unlikely to fail (esp. under sudo) if the rm doesn't.
-      $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
+      # cp is unlikely to fail if the rm doesn't.
+      mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
     fi  # A newer version is available.
   fi  # Self-upgrading is allowed.
 
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh
index c3959484bd8..d144c5806bf 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh
@@ -18,7 +18,7 @@ BootstrapArchCommon() {
   "
 
   # pacman -T exits with 127 if there are missing dependencies
-  missing=$($SUDO pacman -T $deps) || true
+  missing=$(pacman -T $deps) || true
 
   if [ "$ASSUME_YES" = 1 ]; then
     noconfirm="--noconfirm"
@@ -26,9 +26,9 @@ BootstrapArchCommon() {
 
   if [ "$missing" ]; then
     if [ "$QUIET" = 1]; then
-      $SUDO pacman -S --needed $missing $noconfirm > /dev/null
+      pacman -S --needed $missing $noconfirm > /dev/null
     else
-      $SUDO pacman -S --needed $missing $noconfirm
+      pacman -S --needed $missing $noconfirm
     fi
   fi
 }
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh
index afd279ac262..12aa80d631d 100644
--- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh
@@ -21,7 +21,7 @@ BootstrapDebCommon() {
     QUIET_FLAG='-qq'
   fi
 
-  $SUDO apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
+  apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
 
   # virtualenv binary can be found in different packages depending on
   # distro version (#346)
@@ -71,13 +71,13 @@ BootstrapDebCommon() {
           esac
         fi
         if [ "$add_backports" = 1 ]; then
-          $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
-          $SUDO apt-get $QUIET_FLAG update
+          sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
+          apt-get $QUIET_FLAG update
         fi
       fi
     fi
     if [ "$add_backports" != 0 ]; then
-      $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
+      apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
       augeas_pkg=
     fi
   }
@@ -96,7 +96,7 @@ BootstrapDebCommon() {
     # XXX add a case for ubuntu PPAs
   fi
 
-  $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
+  apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
     python \
     python-dev \
     $virtualenv \
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh b/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh
index f1bc00f3bb7..cfbd2b8b1f0 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh
@@ -3,7 +3,7 @@ BootstrapFreeBsd() {
     QUIET_FLAG="--quiet"
   fi
 
-  $SUDO pkg install -Ay $QUIET_FLAG \
+  pkg install -Ay $QUIET_FLAG \
     python \
     py27-virtualenv \
     augeas \
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh
index 86a1ec7d615..46543bab9a8 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh
@@ -15,13 +15,13 @@ BootstrapGentooCommon() {
 
   case "$PACKAGE_MANAGER" in
     (paludis)
-      $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
+      cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
       ;;
     (pkgcore)
-      $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
+      pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
       ;;
     (portage|*)
-      $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
+      emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
       ;;
   esac
 }
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh
index b88e96999e9..b9f347f6774 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh
@@ -6,7 +6,7 @@ BootstrapMac() {
   elif hash port 2>/dev/null; then
     say "Using MacPorts to install dependencies..."
     pkgman=port
-    pkgcmd="$SUDO port install"
+    pkgcmd="port install"
   else
     say "No Homebrew/MacPorts; installing Homebrew..."
     ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
@@ -26,8 +26,8 @@ BootstrapMac() {
   # Workaround for _dlopen not finding augeas on macOS
   if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
     say "Applying augeas workaround"
-    $SUDO mkdir -p /usr/local/lib/
-    $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
+    mkdir -p /usr/local/lib/
+    ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
   fi
 
   if ! hash pip 2>/dev/null; then
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh
index 1c76bbcacd3..c9a540ce185 100644
--- a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh
@@ -3,7 +3,7 @@ BootstrapMageiaCommon() {
     QUIET_FLAG='--quiet'
   fi
 
-  if ! $SUDO urpmi --force $QUIET_FLAG \
+  if ! urpmi --force $QUIET_FLAG \
       python \
       libpython-devel \
       python-virtualenv
@@ -12,7 +12,7 @@ BootstrapMageiaCommon() {
       exit 1
   fi
 
-  if ! $SUDO urpmi --force $QUIET_FLAG \
+  if ! urpmi --force $QUIET_FLAG \
       git \
       gcc \
       python-augeas \
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh
index dcd53529243..6b979b034b6 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh
@@ -24,9 +24,9 @@ BootstrapRpmCommon() {
     QUIET_FLAG='--quiet'
   fi
 
-  if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then
+  if ! $tool list *virtualenv >/dev/null 2>&1; then
     echo "To use Certbot, packages from the EPEL repository need to be installed."
-    if ! $SUDO $tool list epel-release >/dev/null 2>&1; then
+    if ! $tool list epel-release >/dev/null 2>&1; then
       error "Enable the EPEL repository and try running Certbot again."
       exit 1
     fi
@@ -38,7 +38,7 @@ BootstrapRpmCommon() {
       /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..."
       sleep 1s
     fi
-    if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then
+    if ! $tool install $yes_flag $QUIET_FLAG epel-release; then
       error "Could not enable EPEL. Aborting bootstrap!"
       exit 1
     fi
@@ -56,7 +56,7 @@ BootstrapRpmCommon() {
 
   # Some distros and older versions of current distros use a "python27"
   # instead of "python" naming convention. Try both conventions.
-  if $SUDO $tool list python >/dev/null 2>&1; then
+  if $tool list python >/dev/null 2>&1; then
     pkgs="$pkgs
       python
       python-devel
@@ -74,13 +74,13 @@ BootstrapRpmCommon() {
     "
   fi
 
-  if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then
+  if $tool list installed "httpd" >/dev/null 2>&1; then
     pkgs="$pkgs
       mod_ssl
     "
   fi
 
-  if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then
+  if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then
     error "Could not install OS dependencies. Aborting bootstrap!"
     exit 1
   fi
diff --git a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh
index e60ca8628ea..56e7acda340 100755
--- a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh
+++ b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh
@@ -10,7 +10,7 @@ BootstrapSuseCommon() {
     QUIET_FLAG='-qq'
   fi
 
-  $SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \
+  zypper $QUIET_FLAG $zypper_flags in $install_flags \
     python \
     python-devel \
     python-virtualenv \
diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py
index 6f21c28d58d..c2a07c6ca07 100644
--- a/letsencrypt-auto-source/tests/auto_test.py
+++ b/letsencrypt-auto-source/tests/auto_test.py
@@ -4,7 +4,7 @@
 from contextlib import contextmanager
 from functools import partial
 from json import dumps
-from os import chmod, environ
+from os import chmod, environ, makedirs
 from os.path import abspath, dirname, exists, join
 import re
 from shutil import copy, rmtree
@@ -118,12 +118,13 @@ def serving(resources):
 
 
 @contextmanager
-def ephemeral_dir():
+def temp_paths():
+    """Creates and deletes paths for letsencrypt-auto and its venv."""
     dir = mkdtemp(prefix='le-test-')
     try:
-        yield dir
+        yield join(dir, 'letsencrypt-auto'), join(dir, 'venv')
     finally:
-        rmtree(dir)
+        rmtree(dir, ignore_errors=True)
 
 
 def out_and_err(command, input=None, shell=False, env=None):
@@ -160,21 +161,20 @@ def signed(content, private_key_name='signing.key'):
     return out
 
 
-def install_le_auto(contents, venv_dir):
+def install_le_auto(contents, install_path):
     """Install some given source code as the letsencrypt-auto script at the
     root level of a virtualenv.
 
     :arg contents: The contents of the built letsencrypt-auto script
-    :arg venv_dir: The path under which to install the script
+    :arg install_path: path where to install the script
 
     """
-    venv_le_auto_path = join(venv_dir, 'letsencrypt-auto')
-    with open(venv_le_auto_path, 'w') as le_auto:
+    with open(install_path, 'w') as le_auto:
         le_auto.write(contents)
-    chmod(venv_le_auto_path, S_IRUSR | S_IXUSR)
+    chmod(install_path, S_IRUSR | S_IXUSR)
 
 
-def run_le_auto(venv_dir, base_url, **kwargs):
+def run_le_auto(le_auto_path, venv_dir, base_url, **kwargs):
     """Run the prebuilt version of letsencrypt-auto, returning stdout and
     stderr strings.
 
@@ -182,7 +182,7 @@ def run_le_auto(venv_dir, base_url, **kwargs):
 
     """
     env = environ.copy()
-    d = dict(XDG_DATA_HOME=venv_dir,
+    d = dict(VENV_PATH=venv_dir,
              # URL to PyPI-style JSON that tell us the latest released version
              # of LE:
              LE_AUTO_JSON_URL=base_url + 'certbot/json',
@@ -201,7 +201,7 @@ def run_le_auto(venv_dir, base_url, **kwargs):
              **kwargs)
     env.update(d)
     return out_and_err(
-        join(venv_dir, 'letsencrypt-auto') + ' --version',
+        le_auto_path + ' --version',
         shell=True,
         env=env)
 
@@ -213,10 +213,12 @@ def set_le_script_version(venv_dir, version):
     print its version.
 
     """
-    with open(join(venv_dir, 'letsencrypt', 'bin', 'letsencrypt'), 'w') as script:
+    letsencrypt_path = join(venv_dir, 'bin', 'letsencrypt')
+    with open(letsencrypt_path, 'w') as script:
         script.write("#!/usr/bin/env python\n"
                      "from sys import stderr\n"
                      "stderr.write('letsencrypt %s\\n')" % version)
+    chmod(letsencrypt_path, S_IRUSR | S_IXUSR)
 
 
 class AutoTests(TestCase):
@@ -237,6 +239,11 @@ class AutoTests(TestCase):
     test suites.
 
     """
+    NEW_LE_AUTO = build_le_auto(
+            version='99.9.9',
+            requirements='letsencrypt==99.9.9 --hash=sha256:1cc14d61ab424cdee446f51e50f1123f8482ec740587fe78626c933bba2873a0')
+    NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO)
+
     def test_successes(self):
         """Exercise most branches of letsencrypt-auto.
 
@@ -252,20 +259,16 @@ def test_successes(self):
            the next, saving code.
 
         """
-        NEW_LE_AUTO = build_le_auto(
-                version='99.9.9',
-                requirements='letsencrypt==99.9.9 --hash=sha256:1cc14d61ab424cdee446f51e50f1123f8482ec740587fe78626c933bba2873a0')
-        NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO)
-
-        with ephemeral_dir() as venv_dir:
+        with temp_paths() as (le_auto_path, venv_dir):
             # This serves a PyPI page with a higher version, a GitHub-alike
             # with a corresponding le-auto script, and a matching signature.
             resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}),
-                         'v99.9.9/letsencrypt-auto': NEW_LE_AUTO,
-                         'v99.9.9/letsencrypt-auto.sig': NEW_LE_AUTO_SIG}
+                         'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO,
+                         'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG}
             with serving(resources) as base_url:
                 run_letsencrypt_auto = partial(
                         run_le_auto,
+                        le_auto_path,
                         venv_dir,
                         base_url,
                         PIP_FIND_LINKS=join(tests_dir(),
@@ -274,7 +277,7 @@ def test_successes(self):
 
                 # Test when a phase-1 upgrade is needed, there's no LE binary
                 # installed, and pip hashes verify:
-                install_le_auto(build_le_auto(version='50.0.0'), venv_dir)
+                install_le_auto(build_le_auto(version='50.0.0'), le_auto_path)
                 out, err = run_letsencrypt_auto()
                 ok_(re.match(r'letsencrypt \d+\.\d+\.\d+',
                              err.strip().splitlines()[-1]))
@@ -291,16 +294,28 @@ def test_successes(self):
                 self.assertFalse('Upgrading certbot-auto ' in out)
                 self.assertFalse('Creating virtual environment...' in out)
 
-                # Test when a phase-1 upgrade is not needed but a phase-2
-                # upgrade is:
+    def test_phase2_upgrade(self):
+        """Test a phase-2 upgrade without a phase-1 upgrade."""
+        with temp_paths() as (le_auto_path, venv_dir):
+            resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}),
+                         'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO,
+                         'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG}
+            with serving(resources) as base_url:
+                venv_bin = join(venv_dir, 'bin')
+                makedirs(venv_bin)
                 set_le_script_version(venv_dir, '0.0.1')
-                out, err = run_letsencrypt_auto()
+
+                install_le_auto(self.NEW_LE_AUTO, le_auto_path)
+                pip_find_links=join(tests_dir(), 'fake-letsencrypt', 'dist')
+                out, err = run_le_auto(le_auto_path, venv_dir, base_url,
+                                       PIP_FIND_LINKS=pip_find_links)
+
                 self.assertFalse('Upgrading certbot-auto ' in out)
                 self.assertTrue('Creating virtual environment...' in out)
 
     def test_openssl_failure(self):
         """Make sure we stop if the openssl signature check fails."""
-        with ephemeral_dir() as venv_dir:
+        with temp_paths() as (le_auto_path, venv_dir):
             # Serve an unrelated hash signed with the good key (easier than
             # making a bad key, and a mismatch is a mismatch):
             resources = {'': '<a href="certbot/">certbot/</a>',
@@ -308,9 +323,9 @@ def test_openssl_failure(self):
                          'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'),
                          'v99.9.9/letsencrypt-auto.sig': signed('something else')}
             with serving(resources) as base_url:
-                copy(LE_AUTO_PATH, venv_dir)
+                copy(LE_AUTO_PATH, le_auto_path)
                 try:
-                    out, err = run_le_auto(venv_dir, base_url)
+                    out, err = run_le_auto(le_auto_path, venv_dir, base_url)
                 except CalledProcessError as exc:
                     eq_(exc.returncode, 1)
                     self.assertTrue("Couldn't verify signature of downloaded "
@@ -320,7 +335,7 @@ def test_openssl_failure(self):
 
     def test_pip_failure(self):
         """Make sure pip stops us if there is a hash mismatch."""
-        with ephemeral_dir() as venv_dir:
+        with temp_paths() as (le_auto_path, venv_dir):
             resources = {'': '<a href="certbot/">certbot/</a>',
                          'certbot/json': dumps({'releases': {'99.9.9': None}})}
             with serving(resources) as base_url:
@@ -329,14 +344,14 @@ def test_pip_failure(self):
                     build_le_auto(
                         version='99.9.9',
                         requirements='configobj==5.0.6 --hash=sha256:badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb'),
-                    venv_dir)
+                    le_auto_path)
                 try:
-                    out, err = run_le_auto(venv_dir, base_url)
+                    out, err = run_le_auto(le_auto_path, venv_dir, base_url)
                 except CalledProcessError as exc:
                     eq_(exc.returncode, 1)
                     self.assertTrue("THESE PACKAGES DO NOT MATCH THE HASHES "
                                     "FROM THE REQUIREMENTS FILE" in exc.output)
-                    ok_(not exists(join(venv_dir, 'letsencrypt')),
+                    ok_(not exists(venv_dir),
                         msg="The virtualenv was left around, even though "
                             "installation didn't succeed. We shouldn't do "
                             "this, as it foils our detection of whether we "


Code review comments:
diff_hunk: @@ -147,35 +152,38 @@ su_sudo() {
   su root -c "$args"
 }
 
-SUDO_ENV=""
-export CERTBOT_AUTO="$0"
-if [ -n "${LE_AUTO_SUDO+x}" ]; then
-  case "$LE_AUTO_SUDO" in
-    su_sudo|su)
-      SUDO=su_sudo
-      ;;
-    sudo)
-      SUDO=sudo
-      SUDO_ENV="CERTBOT_AUTO=$0"
-      ;;
-    '') ;; # Nothing to do for plain root method.
-    *)
-      error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
-      exit 1
-  esac
-  say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
-else
-  if test "`id -u`" -ne "0" ; then
-    if $EXISTS sudo 1>/dev/null 2>&1; then
-      SUDO=sudo
-      SUDO_ENV="CERTBOT_AUTO=$0"
+if [ "$1" = "--cb-auto-has-root" ]; then
+  shift 1
+elif [ "$1" != "--le-auto-phase2" ]; then
+  # if $1 is --le-auto-phase2, we've executed this branch before
+  if [ -n "${LE_AUTO_SUDO+x}" ]; then
path: letsencrypt-auto-source/letsencrypt-auto
text: @user1:
Nit: put either this `SUDO` setting logic, and/or maybe all of this code block, in a named function for readability purposes?