aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJake Hunsaker <jhunsake@redhat.com>2018-05-25 13:38:27 -0400
committerBryn M. Reeves <bmr@redhat.com>2018-07-12 14:36:39 +0100
commit7b475f1da0f843b20437896737be04cc1c7bbc0a (patch)
tree82d3040f2a793a5033c3e988c94eb0003a37e05c
parent0a76861b9690889b59a95161af473e62c962c787 (diff)
downloadsos-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.128
-rw-r--r--sos/__init__.py10
-rw-r--r--sos/archive.py63
-rw-r--r--sos/sosreport.py20
-rw-r--r--tests/archive_tests.py3
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)