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

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