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