diff options
author | Jake Hunsaker <jhunsake@redhat.com> | 2018-05-25 13:38:27 -0400 |
---|---|---|
committer | Bryn M. Reeves <bmr@redhat.com> | 2018-07-12 14:36:39 +0100 |
commit | 7b475f1da0f843b20437896737be04cc1c7bbc0a (patch) | |
tree | 82d3040f2a793a5033c3e988c94eb0003a37e05c | |
parent | 0a76861b9690889b59a95161af473e62c962c787 (diff) | |
download | sos-7b475f1da0f843b20437896737be04cc1c7bbc0a.tar.gz |
[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 <jhunsake@redhat.com>
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
-rw-r--r-- | man/en/sosreport.1 | 28 | ||||
-rw-r--r-- | sos/__init__.py | 10 | ||||
-rw-r--r-- | sos/archive.py | 63 | ||||
-rw-r--r-- | sos/sosreport.py | 20 | ||||
-rw-r--r-- | 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 b0adcd8b..b6051edc 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 ef4524c6..cd9779bd 100644 --- a/sos/__init__.py +++ b/sos/__init__.py @@ -45,10 +45,10 @@ _sos = _default _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 e153c09a..263e3dd3 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 @@ class FileCacheArchive(Archive): 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 60802617..00c3e811 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 @@ class SoSReport(object): 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 b4dd8d0f..e5b329b5 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) |