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