Repository: certbot/certbot

Issue:
archive/domain/privkeyN.pem is set to 0644 instead of 0600 (or 0440)
The private keys in /etc/letsencrypt/archive/_domain_/privkey<N>.pem are currently set to 0644. 
I guess it's better to have only root to be able to read the private key, hence set them to 0600.



Discussion:
Hrm. What permissions do you see on the directory `/etc/letsencrypt/archive`?  Can you actually read a privkey as an unprivileged user?

Probably the keys should be 0640 and set the group that runs the webserver, if it isn't running as root?  And the directory should be 0750, assuming such a group exists?

This is going to be hard to get right from `letsencrypt-auto`.  But we should have control of these variables concisely exported for OS packagers.  @hlieberman @fmarier.

This is part of #420.

Hm, you're right.
/etc/letsencrypt/archive is set to 700, so a regular user shouldn't be able to reach the keys...

One point made on community.letsencrypt.org is that users may move the certs or keys out of /etc/letsencrypt.  Of course they should be symlinking in rather than moving / copying keys out, but it will happen.  So bumping this ticket up to **nice for 1.0**.

Actually, the point about not moving keys makes me think that in fact the permissions on everything in the directory should be 0440, not 0640.

A related matter would be to honor the group permissions set by the directory the certs reside in. I use a "ssl" group that's the only one allowed to touch the certs aside root, for example.

Since ~2006, Debian-based distros come along with a system group `ssl-cert` which is designed for use with private key material (according to changelog).

For symlinks from /etc/ssl/..., something like this would be nice:

```
-rw-r--r-- 1 root root     2.1K Jan 17 17:54 cert1.pem
-rw-r--r-- 1 root root     1.1K Jan 17 17:54 chain1.pem
-rw-r--r-- 1 root root     3.2K Jan 17 17:54 fullchain1.pem
-rw-r----- 1 root ssl-cert 3.2K Jan 17 17:54 privkey1.pem
```

These settings should be fine for e.g. Debian 8 / Apache and many other applications.

I guess that users have different requirements (e.g. according to distro), so I think the permissions should be configurable via `cli.ini` and maybe via parameters (overwriting `cli.ini` settings).

I also want to note that wrong permissions (in the sense of user's requirements) cannot necessarily be healed by a later user interaction: Once the private key is readable after being written to disk with weak permissions, it could theoretically be stolen if the admin user changes permissions too late. Therefore, an _emtpy_ file with correct file permissions must be written to disk _before_ key material is written into the file.

I suggest to introduce three config values for `cli.ini` (including example settings):

```
private-key-owner = root
private-key-group = ssl-cert
private-key-permisson = 640
```

Optionally, this could also be done for public key material (just for completeness).

I'm not sure for other distros than Debian, if there is a `ssl-cert` group (or equivalent). But assumed, such a group is widely used, this could be a nice default for LE (when `cli.ini` is unmodified or unused).

I guess the Debian package maintainers might want to go for `ssl-cert` as a default anyways.

References:
- https://launchpad.net/ubuntu/+source/ssl-cert/1.0.11.0ubuntu1
- http://metadata.ftp-master.debian.org/changelogs/main/s/ssl-cert/unstable_changelog
- http://ubuntuforums.org/showthread.php?t=1534480

Courier Mailserver is complaining when LE's public certificates are symlinked from `/etc/ssl/certs`:

```
courieresmtpd: STARTTLS failed: couriertls: /etc/ssl/certs/xxxxxxxx.0: Permission denied
```

Result: Courier only receives mail from _some_ mailservers (I assume servers not supporting encryption). This even happens while I don't use LE certs for my mail server host name (yet). Courier has its own certificates stored in `/etc/courier`, but it uses public certificates stored in `/etc/ssl/certs`.

The reason for that is that `/etc/letsencrypt/live` and `/etc/letsencrypt/archive` are only readable to the `root` user, which is a good idea as long as private key material is not protected by itself.

Current state:

```
drwx------ 7 root root 4.0K Jan 19 17:40 live
drwxr-xr-x 2 root root 4.0K Jan 19 14:30 live/www.example.com
lrwxrwxrwx 2 root root 4.0K Jan 19 14:30 live/www.example.com/privkey.pem (symlink)
lrwxrwxrwx 2 root root 4.0K Jan 19 14:30 live/www.example.com/fullchain.pem (symlink)
drwx------ 7 root root 4.0K Jan 19 17:40 archive (currently protects private certificates)
drwxr-xr-x 2 root root 4.0K Jan 19 14:30 archive/www.example.com
-rw-r--r-- 2 root root 4.0K Jan 19 14:30 archive/www.example.com/privkey3.pem
-rw-r--r-- 2 root root 4.0K Jan 19 14:30 archive/www.example.com/fullchain3.pem
```

To mitigate such issues and still be able to symlink the latest public certificate, the directories `/etc/letsencrypt/live` and `/etc/letsencrypt/live` should be world readable _AND_ private certificates should be protected separately (in a configurable manner).

Should be:

```
drwxr-xr-x 7 root root 4.0K Jan 19 17:40 live
drwxr-xr-x 2 root root 4.0K Jan 19 14:30 live/www.example.com
lrwxrwxrwx 2 root root 4.0K Jan 19 14:30 live/www.example.com/privkey.pem (symlink)
lrwxrwxrwx 2 root root 4.0K Jan 19 14:30 live/www.example.com/fullchain.pem (symlink)
drwxr-xr-x 7 root root 4.0K Jan 19 17:40 archive
drwxr-xr-x 2 root root 4.0K Jan 19 14:30 archive/www.example.com
-rw-r----- 2 root root 4.0K Jan 19 14:30 archive/www.example.com/privkey3.pem (group configurable per certificate directory)
-rw-r--r-- 2 root root 4.0K Jan 19 14:30 archive/www.example.com/fullchain3.pem
```

With these permissions, symlinks to LE's public certificates would be world readable (which is fine for public certificates) while private certificates would be readable to only root and a configurable group, e.g.

```
-rw-r----- 2 root root 4.0K Jan 19 14:30 archive/www.example1.com/privkey3.pem
-rw-r----- 2 root aabb 4.0K Jan 19 14:30 archive/www.example2.com/privkey3.pem
-rw-r----- 2 root ssl-cert 4.0K Jan 19 14:30 archive/www.example3.com/privkey3.pem
```

@hlieberman has agreed to try to look at the set of proposed PRs for this, pick the most promising, and shepherd it towards landability.

That may allow Debian to packaged with a letsencrypt gid option!

To add a bit more here: I agree that the good practice for using those files elsewhere on the server (like, webserver, MTA, MDA…) is symlinking against files in /etc/letsencrypt/live.

However, my MTA at least complains about default file permissions (on the key file):
```
insecure permissions: must be at most rwxr-----
```

And then refuse to start.

So, on each cert renewal, I change the file permission to 400 (but could live with 640 or 440 to).

So, I must agree that this should really be considered. ;)

kick

It's a pity to see this moving from milestone to milestone without visible process! What's holding it back?

This sounds good to me - I'd love to be able to share the certificates amongst a few users with an `ssl` group or something.

@joostrijneveld the core development team is small and there is a lot of important work to do; this hasn't been at the top of the list because the directory's permissions protect the keys by default. We've been hoping a volunteer would show up to help with this, though it's actually a fairly subtle task to get this right without creating regressions, and the solution should probably address #2964 at the same time.

I use manuale (`pip install manuale`) which is a tiny and non-automatic client which sets acceptable file permissions.

To help volunteers who want to work on this, we should probably do some design to figure out how we want this to work. Here's a first draft proposal:

* Some kind of `--uid` flag to set the user id for all of `/etc/letsencrypt/`
* Some kind of `--gid` flag to set the group id for all of `/etc/letsencrypt/`
* One or more mode flags to set the file permissions for keys, certs, directories. But how many should we have? It would be nice to just have one, but we would need at 2-3 to allow preservation of Certbot's current behaviour (which would be highly advisable for compatibility purposes).
Currently we have:

Directories which are world-readable (755):
* `/etc/letsencrypt`
* `/etc/letsencrypt/renewal`
* `/etc/letsencrypt/csr`

Directories which are readable only by root (700):
* the `live/`, `archive/`, `keys/` and `accounts/` subdirectories

Almost all files are world-readable (644) but protected by the permissions of their directories, except:
* the `keys/` directory has archived keys that are user-readable (400).

It will be messy to make config options for all of these different modes. The easiest way to preserve full compatibility would be to mask over the top of whatever the user supplies, but we could also have "old permissions" which are the default, and switch to something totally user-specified if the user provides a `--permissions` flag.  Or we could take the risk of assuming that nothing outside of `live/` and `archive/` is really crucial from a backwards compatibility perspective.
I'd really like the ability to specify different permissions for the key material and certificates. The web server I use (H2O) reads keys as root, but performs OCSP updating as a low trust user, and with the current setup it can't access the certificates.
The assumption that one "should be symlinking in rather than moving / copying keys" is a particularly dangerous one. Not only have some pointed out that certain applications have problems with the symlinks, but also consider:
* Applications that copy their config directory to a chroot environment
  * The target of the link may not exist
  * The target of the link will likely be outside of the chroot sub-tree (../letsencrpyt/...)
* Applications that run in a FreeBSD jail, or similar construct
  * Symlinking out of the jail's root directory is considered a security risk
* OSes where different services are assigned to different groups, that may need to access the same cert and/or key

Given that management of a single instance of certbot and associated scripts appears to be easier than one in each of a dozen or more jails (and probably easier on the ACME servers), I'm of the opinion that one should _expect_ keys and certs to be _copied_ in many situations. 

There are additional advantages to copying, as chattr/chflags can then be used _selectively_ to additionally lock down the keys somewhat in Linux, and much more firmly in FreeBSD.

As such, stronger, as-generated permissions on _at least_ the keys would be greatly appreciated. 

Personally, I'd go with 400 on the keys and 444 on the certs, if only to prevent accidental modification or erasure. I'd be happy with 600 and 644 as defaults.

It's been mentioned about the problems with some MTAs, but I would explicitly add here.
Sendmail+tls on FreeBSD complains about the private key being **group readable** and refuses to use them:
Feb 11 23:47:24 <mail.warn> HOST sm-authmta[3265]: STARTTLS=server: file /usr/local/etc/letsencrypt/live/HOST/privkey.pem unsafe: Group readable file

And I agree with @jeffsf 's thoughtful arguments suggestions.

Here I suffered from the same problem that due to incorrect private key permissions my sendmail STARTTLS support suddenly stopped working after renewal of my certificates. Here I have now adapted my daily cronjob to do the following:

```
MAILTO="root"
0 */12 * * * root /usr/bin/certbot renew --text >>/var/log/certbot.log 2>&1 || (find /etc/letsencrypt/archive -type f -name "privkey*.pem" -exec chmod 600 {} \; ; /etc/init.d/nginx reload; /etc/init.d/dovecot reload; /etc/init.d/sendmail restart)
```

Thus, I will enforce proper 600 permissions after successful execution of certbot. This temporary solved my issues until certbot itself supports to set file permissions correctly.
The workaround I am using is `post_hook` option for certbot. I think it is much more elegant.
In etc/letsencrypt/renewal/DOMAIN.conf, it looks as follows:
`post_hook = chmod 400 /usr/local/etc/letsencrypt/live/DOMAIN/privkey.pem; service apache24 reload; services dovecot reload; service sendmail reload`
From the command line it would be
`--post_hook "chmod 400 /usr/local/etc/letsencrypt/live/DOMAIN/privkey.pem; service apache24 reload; service dovecot reload; service sendmail reload"`

This works (at least on FreeBSD) since `chmod` without any flags acts on the actual file to which the symlink is pointing. I am not sure if this is the same default for `chmod` on all systems. (You might need to adjust the path for your OS and you'd have your own "restart" commands)

Disclaimer: I am still waiting for my actual renewal to happen automatically from the cron job.
Using a `post_hook` is a good idea, but FWIR it does not work if you use certbot through a dedicated (e.g. certbot) linux user (because no way I’m giving it root rights). But I handle all those things (chmod, restarting whatever program use those certs…) in my systemd service file, which provide such kinds of hooks too.
I haven't tried this with a dedicated user, but I don't see the reason why it shouldn't work. Since the directory permissions allow that user to **W**rite the files (certs), the same user can change the permissions too.
So, that part can be placed in `post_hook`.
Restarting your daemons is a different issue; there you'd need your elevated permissions. Obviously, since you have to run something else from your cron it probably doesn't make a big difference.

Also, - just in case you missed it: you can avoid the extra script (find, etc.) if you run `chmod` on the soft-links (which remain the same upon renewal).
@St-Ranger: I use chmod directly too. ;) You’re right about changing permissions, as a matter of fact my issue is with `chown`, because at least opensmtpd (if I remember correctly) complains if the keys are not owned by root. And apparently you cannot chown a file to another user (or at least not to root), for security reasons I guess. So like you said, I still need to run things outside of the hook.
@ArchangeGabriel: The fact that you have to do `chown` from outside of certbot is not certbot's fault (or even relevant to certbot per se). Rather,  it is an obvious consequence of your conscious choice of not running certbot as root. This is different for many other users who run certbot as root (which, as far as I understand, is an intended primary _m.o._).

A quick digression: Most *nix systems do not allow ownership giveaway "for obvious [security reasons](http://unix.stackexchange.com/questions/27350/why-cant-a-normal-user-chown-a-file)"
So, a user can `chown` only from and to the user (and the group) whose effective permissions you have. Thus, a non-root user can `chown` only to a different group that he also belongs to.
AFAIR, all BSD's are like that, but some System V OSes, e.g. IRIX, did allow ownership giveaway and that was a problem.


>@ArchangeGabriel: The fact that you have to do `chown` from outside of
>certbot is not certbot's fault (or even relevant to certbot per se).
>Rather,  it is an obvious consequence of your conscious choice of not
>running certbot as root. This is different for many other users who run
>certbot as root (which, as far as I understand, is an intended primary
>_m.o._).

You’re absolutely right, and I never implied this was a certbot issue. I know that whatever happens, I’ll still need chown and restarts from within my systemd service.

>A quick digression: Most *nix systems do not allow ownership giveaway
>"for obvious [security
>reasons](http://unix.stackexchange.com/questions/27350/why-cant-a-normal-user-chown-a-file)"
>So, a user can `chown` only from and to the user (and the group) whose
>effective permissions you have. Thus, a non-root user can `chown` only
>to a different group that he also belongs to.
>AFAIR, all BSD's are like that, but some System V OSes, e.g. IRIX, did
>allow ownership giveaway and that was a problem.

Thank you for this reference.

Hi @pde  

Just trying to get my head around the logic of not setting the file to 400, is it that one wants people to be able to dink with directory permissions in order to determine access to keys?  After all, in the default configuration, the grandparent is set to 700, owned by root, and so only root can access the file one way or another.  At that point it's simply a minor nuisance to have to exit_hook the thing, but a nuisance never-the-less.

I ask because what you're proposing seems to me to suggest too many configuration options, with which to hang oneself and complicate your code.  Better it would seem to WONTFIX this and point people with sendmail and such at a simple workaround.

Thoughts?
Nearly 2 years have gone by and this simple thing is still unfixed?
After doing updates or renewals, I just run this script from /etc/letsencrypt
/etc/letsencrypt/setprivkeyperms.sh
~~~~~~~~~~~~
#!/bin/bash
find /etc/letsencrypt/archive -type f -perm 0644 -name 'privkey*.pem' -exec chmod 600 {} +
~~~~~~~~~~~~
Once done, I just rsync the dirs to other servers in the cluster.
Sendmail and apache are happy.


# 
I re-discovered this issue when investigating an intrusion on a Debian server, as I looked for world-readable files that may have been accessible by the intruder. 

Private keys and certs generated by certbot are world-readable. 
Upon generation and renewal, live keys are immediately copied into the archive directory. Both key files and the archive directory itself are world readable, not 700 contrary to what @ZaiLynch claimed. 

Edit + update: 

I made a new clean install of cerbot 0.10.2 on Debian 9 and directories archive, accounts, keys, live all have right 700, so the keys are not world-readable with a clean install. 
It may have been a human error with the previous installation, I have fixed directory rights manually and will write a script to monitor files rights and ensure they stay non-world-readable. 
I discovered this problem today after wasting quite a bit of time and effort trying to figure out why my email clients stopped being able to send email.  Apparently it was due to automated certbot renewal which made the key world readable, which caused sendmail to refuse to use it, which broke mail for everyone because the server is specifically restricted (as it should be) from allowing clients to authenticate on a non-secured channel.

I wouldn't consider this a low priority problem just because it only causes problems for people using services other than http.  Good SSL security is important for other services too.  2+ years and multiple reports (#3108 #3754 are dupes of this issue) for this to still be unresolved seems pretty crazy to me.
This is definitely still on our roadmap, and there'll be much more work on improving mailserver support in Certbot in the coming months. In the mean time, you can use the `--deploy-hook` option to run a script at renewal time to make sendmail happy. 
(Having your hook script `cp` the relevant files and then `chmod` them, and then configuring `sendmail` to use the copied versions, is probably the safest approach here).
@pde Can you clarify why running `chmod` directly against the privkey might be considered unsafe? Your earlier comment [here](https://github.com/certbot/certbot/issues/1473#issuecomment-157798920) suggests that users should *not* be copying the keys out of their original location.

It does seem like there are scenarios where copying a world-readable private key file into a world-readable directory even temporarily could lead to a loss of access control, e.g. if a snapshot were triggered.
This issue also makes trouble for samba - our domain controller uses certbot for certificates, and the `644` permissions cause samba to refuse starting as the privkey file is too visible. Found the problem and managed to fix it by correcting the permissions, but that debugging should really not have been necessary...

I really don't see why the permissions aren't just set properly in the first place (even though the directory is "safe" with `700`)?
It's a bit strange that the code which originally generates the key, in https://github.com/certbot/certbot/blob/master/certbot/crypto_util.py#L65, already does the right thing (it uses open mode 0600, which is perfectly appropriate for private keys), and there is even a `safe_open` function which supports such modes, but it is not used when the keys are actually saved to the archive directory...

In my opinion, 0600 is the right default mode for any private key material, and you should really use that.  If e.g. Debian or another distro wants to introduce separate 'ssl-cert' groups with additional permissions, then they can patch their copy of certbot while packaging.  I don't think it is up to upstream to cope with all the distro-specific variations.

The following diff should make >95% of people happy (and I'm applying it locally anyway):

```
diff --git a/certbot/storage.py b/certbot/storage.py
index 32d6771c..88b804f4 100644
--- a/certbot/storage.py
+++ b/certbot/storage.py
@@ -1123,7 +1123,7 @@ class RenewableCert(object):
             logger.debug("Writing symlink to old private key, %s.", old_privkey)
             os.symlink(old_privkey, target["privkey"])
         else:
-            with open(target["privkey"], "wb") as f:
+            with util.safe_open(target["privkey"], "wb", chmod=0o400) as f:
                 logger.debug("Writing new private key to %s.", target["privkey"])
                 f.write(new_privkey)
```

Resolving patch:
diff --git a/certbot/storage.py b/certbot/storage.py
--- a/certbot/storage.py
+++ b/certbot/storage.py
@@ -29,6 +29,7 @@
 ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
 README = "README"
 CURRENT_VERSION = util.get_strict_version(certbot.__version__)
+BASE_PRIVKEY_MODE = 0o600
 
 
 def renewal_conf_files(config):
@@ -1050,13 +1051,14 @@ def new_lineage(cls, lineagename, cert, privkey, chain, cli_config):
         # Put the data into the appropriate files on disk
         target = dict([(kind, os.path.join(live_dir, kind + ".pem"))
                        for kind in ALL_FOUR])
+        archive_target = dict([(kind, os.path.join(archive, kind + "1.pem"))
+                               for kind in ALL_FOUR])
         for kind in ALL_FOUR:
-            os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), kind + "1.pem"),
-                       target[kind])
+            os.symlink(_relpath_from_file(archive_target[kind], target[kind]), target[kind])
         with open(target["cert"], "wb") as f:
             logger.debug("Writing certificate to %s.", target["cert"])
             f.write(cert)
-        with open(target["privkey"], "wb") as f:
+        with util.safe_open(archive_target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f:
             logger.debug("Writing private key to %s.", target["privkey"])
             f.write(privkey)
             # XXX: Let's make sure to get the file permissions right here
@@ -1120,14 +1122,15 @@ def save_successor(self, prior_version, new_cert,
               os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version)))
              for kind in ALL_FOUR])
 
+        old_privkey = os.path.join(
+            self.archive_dir, "privkey{0}.pem".format(prior_version))
+
         # Distinguish the cases where the privkey has changed and where it
         # has not changed (in the latter case, making an appropriate symlink
         # to an earlier privkey version)
         if new_privkey is None:
             # The behavior below keeps the prior key by creating a new
             # symlink to the old key or the target of the old key symlink.
-            old_privkey = os.path.join(
-                self.archive_dir, "privkey{0}.pem".format(prior_version))
             if os.path.islink(old_privkey):
                 old_privkey = os.readlink(old_privkey)
             else:
@@ -1135,9 +1138,16 @@ def save_successor(self, prior_version, new_cert,
             logger.debug("Writing symlink to old private key, %s.", old_privkey)
             os.symlink(old_privkey, target["privkey"])
         else:
-            with open(target["privkey"], "wb") as f:
+            with util.safe_open(target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f:
                 logger.debug("Writing new private key to %s.", target["privkey"])
                 f.write(new_privkey)
+            # If the previous privkey in this lineage has an existing gid or group mode > 0,
+            # let's keep those.
+            group_mode = stat.S_IMODE(os.stat(old_privkey).st_mode) & \
+                (stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP)
+            mode = BASE_PRIVKEY_MODE | group_mode
+            os.chown(target["privkey"], -1, os.stat(old_privkey).st_gid)
+            os.chmod(target["privkey"], mode)
 
         # Save everything else
         with open(target["cert"], "wb") as f:


Test patch:
diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py
--- a/certbot/tests/storage_test.py
+++ b/certbot/tests/storage_test.py
@@ -13,6 +13,7 @@
 
 import certbot
 from certbot import cli
+from certbot import compat
 from certbot import errors
 from certbot.storage import ALL_FOUR
 
@@ -91,6 +92,8 @@ def _write_out_kind(self, kind, ver, value=None):
                    link)
         with open(link, "wb") as f:
             f.write(kind.encode('ascii') if value is None else value)
+        if kind == "privkey":
+            os.chmod(link, 0o600)
 
     def _write_out_ex_kinds(self):
         for kind in ALL_FOUR:
@@ -543,6 +546,47 @@ def test_save_successor(self, mock_rv):
         self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10)))
         self.assertFalse(os.path.exists(temp_config_file))
 
+    @test_util.broken_on_windows
+    @mock.patch("certbot.storage.relevant_values")
+    def test_save_successor_maintains_group_mode(self, mock_rv):
+        # Mock relevant_values() to claim that all values are relevant here
+        # (to avoid instantiating parser)
+        mock_rv.side_effect = lambda x: x
+        for kind in ALL_FOUR:
+            self._write_out_kind(kind, 1)
+        self.test_rc.update_all_links_to(1)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 1)).st_mode, 0o600))
+        os.chmod(self.test_rc.version("privkey", 1), 0o444)
+        # If no new key, permissions should be the same (we didn't write any keys)
+        self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 2)).st_mode, 0o444))
+        # If new key, permissions should be rest to 600 + preserved group
+        self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 3)).st_mode, 0o640))
+        # If permissions reverted, next renewal will also revert permissions of new key
+        os.chmod(self.test_rc.version("privkey", 3), 0o404)
+        self.test_rc.save_successor(3, b"newcert", b"new_privkey", b"new chain", self.config)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 4)).st_mode, 0o600))
+
+    @test_util.broken_on_windows
+    @mock.patch("certbot.storage.relevant_values")
+    @mock.patch("certbot.storage.os.chown")
+    def test_save_successor_maintains_gid(self, mock_chown, mock_rv):
+        # Mock relevant_values() to claim that all values are relevant here
+        # (to avoid instantiating parser)
+        mock_rv.side_effect = lambda x: x
+        for kind in ALL_FOUR:
+            self._write_out_kind(kind, 1)
+        self.test_rc.update_all_links_to(1)
+        self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config)
+        self.assertFalse(mock_chown.called)
+        self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config)
+        self.assertTrue(mock_chown.called)
+
     def _test_relevant_values_common(self, values):
         defaults = dict((option, cli.flag_default(option))
                         for option in ("authenticator", "installer",
@@ -629,6 +673,7 @@ def test_new_lineage(self, mock_rv):
             self.config.live_dir, "README")))
         self.assertTrue(os.path.exists(os.path.join(
             self.config.live_dir, "the-lineage.com", "README")))
+        self.assertTrue(compat.compare_file_modes(os.stat(result.key_path).st_mode, 0o600))
         with open(result.fullchain, "rb") as f:
             self.assertEqual(f.read(), b"cert" + b"chain")
         # Let's do it again and make sure it makes a different lineage
diff --git a/tests/certbot-boulder-integration.sh b/tests/certbot-boulder-integration.sh
--- a/tests/certbot-boulder-integration.sh
+++ b/tests/certbot-boulder-integration.sh
@@ -280,7 +280,29 @@ CheckCertCount() {
     fi
 }
 
+CheckGroup() {
+    group_mode() { echo $((0`stat -c %a $1` & 070)); }
+    group_owner() { echo `stat -c %G $1`; }
+    if [ `group_mode $1` -ne `group_mode $2` ] ; then
+        echo "Expected group permission `group_mode $1`, got `group_mode $2` on file $2"
+        exit 1
+    fi
+    if [ `group_owner $1` != `group_owner $2` ] ; then
+        echo "Expected group owner `group_owner $1`, got `group_owner $2` on file $2"
+        exit 1
+    fi
+}
+
+CheckOthersPermission() {
+    other_permission=$((0`stat -c %a $1` & 07))
+    if [ $other_permission -ne 0 ] ; then
+        echo "Expected file $1 not to be accessible by others"
+        exit 1
+    fi
+}
+
 CheckCertCount "le.wtf" 1
+
 # This won't renew (because it's not time yet)
 common_no_force_renew renew
 CheckCertCount "le.wtf" 1
@@ -294,6 +316,11 @@ rm -rf "$renewal_hooks_root"
 common renew --cert-name le.wtf --authenticator manual
 CheckCertCount "le.wtf" 2
 
+CheckOthersPermission "${root}/conf/archive/le.wtf/privkey1.pem"
+CheckOthersPermission "${root}/conf/archive/le.wtf/privkey2.pem"
+CheckGroup "${root}/conf/archive/le.wtf/privkey1.pem" "${root}/conf/archive/le.wtf/privkey2.pem"
+chmod 0444 "${root}/conf/archive/le.wtf/privkey2.pem"
+
 # test renewal with no executables in hook directories
 for hook_dir in $renewal_hooks_dirs; do
     touch "$hook_dir/file"
@@ -310,6 +337,9 @@ CreateDirHooks
 sed -i "4arenew_before_expiry = 4 years" "$root/conf/renewal/le.wtf.conf"
 common_no_force_renew renew --rsa-key-size 2048 --no-directory-hooks
 CheckCertCount "le.wtf" 3
+CheckGroup "${root}/conf/archive/le.wtf/privkey2.pem" "${root}/conf/archive/le.wtf/privkey3.pem"
+CheckOthersPermission "${root}/conf/archive/le.wtf/privkey3.pem"
+
 if [ -s "$HOOK_DIRS_TEST" ]; then
     echo "Directory hooks were executed with --no-directory-hooks!" >&2
     exit 1