diff options
-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) |