Blame SOURCES/sos-bz1614952-archive-encryption.patch

fce3c4
From 7b475f1da0f843b20437896737be04cc1c7bbc0a Mon Sep 17 00:00:00 2001
fce3c4
From: Jake Hunsaker <jhunsake@redhat.com>
fce3c4
Date: Fri, 25 May 2018 13:38:27 -0400
fce3c4
Subject: [PATCH] [sosreport] Add mechanism to encrypt final archive
fce3c4
fce3c4
Adds an option to encrypt the resulting archive that sos generates.
fce3c4
There are two methods for doing so:
fce3c4
fce3c4
	--encrypt-key	Uses a key-pair for asymmetric encryption
fce3c4
	--encrypt-pass  Uses a password for symmetric encryption
fce3c4
fce3c4
For key-pair encryption, the key-to-be-used must be imported into the
fce3c4
root user's keyring, as gpg does not allow for the use of keyfiles.
fce3c4
fce3c4
If the encryption process fails, sos will not abort as the unencrypted
fce3c4
archive will have already been created. The assumption being that the
fce3c4
archive is still of use and/or the user has another means of encrypting
fce3c4
it.
fce3c4
fce3c4
Resolves: #1320
fce3c4
fce3c4
Signed-off-by: Jake Hunsaker <jhunsake@redhat.com>
fce3c4
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
fce3c4
---
fce3c4
 man/en/sosreport.1     | 28 ++++++++++++++++++++++
fce3c4
 sos/__init__.py        | 10 ++++----
fce3c4
 sos/archive.py         | 63 ++++++++++++++++++++++++++++++++++++++++++++++----
fce3c4
 sos/sosreport.py       | 20 ++++++++++++++--
fce3c4
 tests/archive_tests.py |  3 ++-
fce3c4
 5 files changed, 113 insertions(+), 11 deletions(-)
fce3c4
fce3c4
diff --git a/man/en/sosreport.1 b/man/en/sosreport.1
fce3c4
index b0adcd8bb..b6051edc1 100644
fce3c4
--- a/man/en/sosreport.1
fce3c4
+++ b/man/en/sosreport.1
fce3c4
@@ -22,6 +22,8 @@ sosreport \- Collect and package diagnostic and support data
fce3c4
           [--log-size]\fR
fce3c4
           [--all-logs]\fR
fce3c4
           [-z|--compression-type method]\fR
fce3c4
+          [--encrypt-key KEY]\fR
fce3c4
+          [--encrypt-pass PASS]\fR
fce3c4
           [--experimental]\fR
fce3c4
           [-h|--help]\fR
fce3c4
 
fce3c4
@@ -120,6 +122,32 @@ increase the size of reports.
fce3c4
 .B \-z, \--compression-type METHOD
fce3c4
 Override the default compression type specified by the active policy.
fce3c4
 .TP
fce3c4
+.B \--encrypt-key KEY
fce3c4
+Encrypts the resulting archive that sosreport produces using GPG. KEY must be
fce3c4
+an existing key in the user's keyring as GPG does not allow for keyfiles.
fce3c4
+KEY can be any value accepted by gpg's 'recipient' option.
fce3c4
+
fce3c4
+Note that the user running sosreport must match the user owning the keyring
fce3c4
+from which keys will be obtained. In particular this means that if sudo is
fce3c4
+used to run sosreport, the keyring must also be set up using sudo
fce3c4
+(or direct shell access to the account).
fce3c4
+
fce3c4
+Users should be aware that encrypting the final archive will result in sos
fce3c4
+using double the amount of temporary disk space - the encrypted archive must be
fce3c4
+written as a separate, rather than replacement, file within the temp directory
fce3c4
+that sos writes the archive to. However, since the encrypted archive will be
fce3c4
+the same size as the original archive, there is no additional space consumption
fce3c4
+once the temporary directory is removed at the end of execution.
fce3c4
+
fce3c4
+This means that only the encrypted archive is present on disk after sos
fce3c4
+finishes running.
fce3c4
+
fce3c4
+If encryption fails for any reason, the original unencrypted archive is
fce3c4
+preserved instead.
fce3c4
+.TP
fce3c4
+.B \--encrypt-pass PASS
fce3c4
+The same as \--encrypt-key, but use the provided PASS for symmetric encryption
fce3c4
+rather than key-pair encryption.
fce3c4
 .TP
fce3c4
 .B \--batch
fce3c4
 Generate archive without prompting for interactive input.
fce3c4
diff --git a/sos/__init__.py b/sos/__init__.py
fce3c4
index ef4524c60..cd9779bdc 100644
fce3c4
--- a/sos/__init__.py
fce3c4
+++ b/sos/__init__.py
fce3c4
@@ -45,10 +45,10 @@ def _default(msg):
fce3c4
 _arg_names = [
fce3c4
     'add_preset', 'alloptions', 'all_logs', 'batch', 'build', 'case_id',
fce3c4
     'chroot', 'compression_type', 'config_file', 'desc', 'debug', 'del_preset',
fce3c4
-    'enableplugins', 'experimental', 'label', 'list_plugins', 'list_presets',
fce3c4
-    'list_profiles', 'log_size', 'noplugins', 'noreport', 'note',
fce3c4
-    'onlyplugins', 'plugopts', 'preset', 'profiles', 'quiet', 'sysroot',
fce3c4
-    'threads', 'tmp_dir', 'verbosity', 'verify'
fce3c4
+    'enableplugins', 'encrypt_key', 'encrypt_pass', 'experimental', 'label',
fce3c4
+    'list_plugins', 'list_presets', 'list_profiles', 'log_size', 'noplugins',
fce3c4
+    'noreport', 'note', 'onlyplugins', 'plugopts', 'preset', 'profiles',
fce3c4
+    'quiet', 'sysroot', 'threads', 'tmp_dir', 'verbosity', 'verify'
fce3c4
 ]
fce3c4
 
fce3c4
 #: Arguments with non-zero default values
fce3c4
@@ -84,6 +84,8 @@ class SoSOptions(object):
fce3c4
     del_preset = ""
fce3c4
     desc = ""
fce3c4
     enableplugins = []
fce3c4
+    encrypt_key = None
fce3c4
+    encrypt_pass = None
fce3c4
     experimental = False
fce3c4
     label = ""
fce3c4
     list_plugins = False
fce3c4
diff --git a/sos/archive.py b/sos/archive.py
fce3c4
index e153c09ad..263e3dd3f 100644
fce3c4
--- a/sos/archive.py
fce3c4
+++ b/sos/archive.py
fce3c4
@@ -142,11 +142,12 @@ class FileCacheArchive(Archive):
fce3c4
     _archive_root = ""
fce3c4
     _archive_name = ""
fce3c4
 
fce3c4
-    def __init__(self, name, tmpdir, policy, threads):
fce3c4
+    def __init__(self, name, tmpdir, policy, threads, enc_opts):
fce3c4
         self._name = name
fce3c4
         self._tmp_dir = tmpdir
fce3c4
         self._policy = policy
fce3c4
         self._threads = threads
fce3c4
+        self.enc_opts = enc_opts
fce3c4
         self._archive_root = os.path.join(tmpdir, name)
fce3c4
         with self._path_lock:
fce3c4
             os.makedirs(self._archive_root, 0o700)
fce3c4
@@ -384,12 +385,65 @@ def finalize(self, method):
fce3c4
                       os.stat(self._archive_name).st_size))
fce3c4
         self.method = method
fce3c4
         try:
fce3c4
-            return self._compress()
fce3c4
+            res = self._compress()
fce3c4
         except Exception as e:
fce3c4
             exp_msg = "An error occurred compressing the archive: "
fce3c4
             self.log_error("%s %s" % (exp_msg, e))
fce3c4
             return self.name()
fce3c4
 
fce3c4
+        if self.enc_opts['encrypt']:
fce3c4
+            try:
fce3c4
+                return self._encrypt(res)
fce3c4
+            except Exception as e:
fce3c4
+                exp_msg = "An error occurred encrypting the archive:"
fce3c4
+                self.log_error("%s %s" % (exp_msg, e))
fce3c4
+                return res
fce3c4
+        else:
fce3c4
+            return res
fce3c4
+
fce3c4
+    def _encrypt(self, archive):
fce3c4
+        """Encrypts the compressed archive using GPG.
fce3c4
+
fce3c4
+        If encryption fails for any reason, it should be logged by sos but not
fce3c4
+        cause execution to stop. The assumption is that the unencrypted archive
fce3c4
+        would still be of use to the user, and/or that the end user has another
fce3c4
+        means of securing the archive.
fce3c4
+
fce3c4
+        Returns the name of the encrypted archive, or raises an exception to
fce3c4
+        signal that encryption failed and the unencrypted archive name should
fce3c4
+        be used.
fce3c4
+        """
fce3c4
+        arc_name = archive.replace("sosreport-", "secured-sosreport-")
fce3c4
+        arc_name += ".gpg"
fce3c4
+        enc_cmd = "gpg --batch -o %s " % arc_name
fce3c4
+        env = None
fce3c4
+        if self.enc_opts["key"]:
fce3c4
+            # need to assume a trusted key here to be able to encrypt the
fce3c4
+            # archive non-interactively
fce3c4
+            enc_cmd += "--trust-model always -e -r %s " % self.enc_opts["key"]
fce3c4
+            enc_cmd += archive
fce3c4
+        if self.enc_opts["password"]:
fce3c4
+            # prevent change of gpg options using a long password, but also
fce3c4
+            # prevent the addition of quote characters to the passphrase
fce3c4
+            passwd = "%s" % self.enc_opts["password"].replace('\'"', '')
fce3c4
+            env = {"sos_gpg": passwd}
fce3c4
+            enc_cmd += "-c --passphrase-fd 0 "
fce3c4
+            enc_cmd = "/bin/bash -c \"echo $sos_gpg | %s\"" % enc_cmd
fce3c4
+            enc_cmd += archive
fce3c4
+        r = sos_get_command_output(enc_cmd, timeout=0, env=env)
fce3c4
+        if r["status"] == 0:
fce3c4
+            return arc_name
fce3c4
+        elif r["status"] == 2:
fce3c4
+            if self.enc_opts["key"]:
fce3c4
+                msg = "Specified key not in keyring"
fce3c4
+            else:
fce3c4
+                msg = "Could not read passphrase"
fce3c4
+        else:
fce3c4
+            # TODO: report the actual error from gpg. Currently, we cannot as
fce3c4
+            # sos_get_command_output() does not capture stderr
fce3c4
+            msg = "gpg exited with code %s" % r["status"]
fce3c4
+        raise Exception(msg)
fce3c4
+
fce3c4
 
fce3c4
 # Compatibility version of the tarfile.TarFile class. This exists to allow
fce3c4
 # compatibility with PY2 runtimes that lack the 'filter' parameter to the
fce3c4
@@ -468,8 +522,9 @@ class TarFileArchive(FileCacheArchive):
fce3c4
     method = None
fce3c4
     _with_selinux_context = False
fce3c4
 
fce3c4
-    def __init__(self, name, tmpdir, policy, threads):
fce3c4
-        super(TarFileArchive, self).__init__(name, tmpdir, policy, threads)
fce3c4
+    def __init__(self, name, tmpdir, policy, threads, enc_opts):
fce3c4
+        super(TarFileArchive, self).__init__(name, tmpdir, policy, threads,
fce3c4
+                                             enc_opts)
fce3c4
         self._suffix = "tar"
fce3c4
         self._archive_name = os.path.join(tmpdir, self.name())
fce3c4
 
fce3c4
diff --git a/sos/sosreport.py b/sos/sosreport.py
fce3c4
index 60802617c..00c3e8110 100644
fce3c4
--- a/sos/sosreport.py
fce3c4
+++ b/sos/sosreport.py
fce3c4
@@ -316,6 +316,13 @@ def _parse_args(args):
fce3c4
     preset_grp.add_argument("--del-preset", type=str, action="store",
fce3c4
                             help="Delete the named command line preset")
fce3c4
 
fce3c4
+    encrypt_grp = parser.add_mutually_exclusive_group()
fce3c4
+    encrypt_grp.add_argument("--encrypt-key",
fce3c4
+                             help="Encrypt the final archive using a GPG "
fce3c4
+                                  "key-pair")
fce3c4
+    encrypt_grp.add_argument("--encrypt-pass",
fce3c4
+                             help="Encrypt the final archive using a password")
fce3c4
+
fce3c4
     return parser.parse_args(args)
fce3c4
 
fce3c4
 
fce3c4
@@ -431,16 +438,25 @@ def get_temp_file(self):
fce3c4
         return self.tempfile_util.new()
fce3c4
 
fce3c4
     def _set_archive(self):
fce3c4
+        enc_opts = {
fce3c4
+            'encrypt': True if (self.opts.encrypt_pass or
fce3c4
+                                self.opts.encrypt_key) else False,
fce3c4
+            'key': self.opts.encrypt_key,
fce3c4
+            'password': self.opts.encrypt_pass
fce3c4
+        }
fce3c4
+
fce3c4
         archive_name = os.path.join(self.tmpdir,
fce3c4
                                     self.policy.get_archive_name())
fce3c4
         if self.opts.compression_type == 'auto':
fce3c4
             auto_archive = self.policy.get_preferred_archive()
fce3c4
             self.archive = auto_archive(archive_name, self.tmpdir,
fce3c4
-                                        self.policy, self.opts.threads)
fce3c4
+                                        self.policy, self.opts.threads,
fce3c4
+                                        enc_opts)
fce3c4
 
fce3c4
         else:
fce3c4
             self.archive = TarFileArchive(archive_name, self.tmpdir,
fce3c4
-                                          self.policy, self.opts.threads)
fce3c4
+                                          self.policy, self.opts.threads,
fce3c4
+                                          enc_opts)
fce3c4
 
fce3c4
         self.archive.set_debug(True if self.opts.debug else False)
fce3c4
 
fce3c4
diff --git a/tests/archive_tests.py b/tests/archive_tests.py
fce3c4
index b4dd8d0ff..e5b329b5f 100644
fce3c4
--- a/tests/archive_tests.py
fce3c4
+++ b/tests/archive_tests.py
fce3c4
@@ -19,7 +19,8 @@ class TarFileArchiveTest(unittest.TestCase):
fce3c4
 
fce3c4
     def setUp(self):
fce3c4
         self.tmpdir = tempfile.mkdtemp()
fce3c4
-        self.tf = TarFileArchive('test', self.tmpdir, Policy(), 1)
fce3c4
+        enc = {'encrypt': False}
fce3c4
+        self.tf = TarFileArchive('test', self.tmpdir, Policy(), 1, enc)
fce3c4
 
fce3c4
     def tearDown(self):
fce3c4
         shutil.rmtree(self.tmpdir)