Use GnuPG 2 for backup/restore

ipa-backup and ipa-restore now use GnuPG 2 for asymmetric encryption, too.
The gpg2 command behaves a bit different and requires a gpg2 compatible
config directory. Therefore the --keyring option has been deprecated.

The backup and restore tools now use root's GPG keyring by default.
Custom configuration and keyring can be used by setting GNUPGHOME
environment variables.

Fixes: https://pagure.io/freeipa/issue/7560
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
This commit is contained in:
Christian Heimes 2018-05-23 12:33:01 +02:00
parent dbc3788405
commit 8e165480ac
7 changed files with 206 additions and 91 deletions

View File

@ -757,7 +757,6 @@ Provides: python2-ipaplatform = %{version}-%{release}
%{!?python_provide:Provides: python-ipaplatform = %{version}-%{release}}
Requires: %{name}-common = %{version}-%{release}
Requires: python2-gssapi >= 1.2.0-5
Requires: gnupg
Requires: gnupg2
Requires: keyutils
Requires: python2 >= 2.7.9
@ -811,7 +810,6 @@ Provides: python3-ipaplatform = %{version}-%{release}
%{?python_provide:%python_provide python3-ipaplatform}
Requires: %{name}-common = %{version}-%{release}
Requires: python3-gssapi >= 1.2.0
Requires: gnupg
Requires: gnupg2
Requires: keyutils
Requires: python3-cryptography >= 1.6

View File

@ -43,10 +43,7 @@ A backup can not be restored in a different version of IPA.
Back up data only. The default is to back up all IPA files plus data.
.TP
\fB\-\-gpg\fR
Encrypt the back up file.
.TP
\fB\-\-gpg\-keyring\fR=\fIGPG_KEYRING\fR
The full path to a GPG keyring. The keyring consists of two files, a public and a private key (.sec and .pub respectively). Specify the path without an extension.
Encrypt the back up file. Set \fBGNUPGHOME\fR environment variable to use a custom keyring and gpg2 configuration.
.TP
\fB\-\-logs\fR
Include the IPA service log files in the backup.
@ -71,6 +68,10 @@ Log to the given file
1 if an error occurred
2 if IPA is not configured
.SH "ENVIRONMENT VARIABLES"
.PP
\fBGNUPGHOME\fR
Use custom GnuPG keyring and settings (default: \fB~/.gnupg\fR).
.SH "FILES"
.PP
\fI/var/lib/ipa/backup\fR
@ -83,4 +84,5 @@ The default directory for storing backup files.
The log file for backups
.PP
.SH "SEE ALSO"
ipa\-restore(1).
.BR ipa\-restore(1)
.BR gpg2(1)

View File

@ -32,7 +32,7 @@ The type of backup is automatically detected. A data restore can be done from ei
.TP
\fBWARNING\fR: A full restore will restore files like /etc/passwd, /etc/group, /etc/resolv.conf as well. Any file that IPA may have touched is backed up and restored.
.TP
An encrypted backup is also automatically detected and the root keyring is used by default. The \-\-keyring option can be used to define the full path to the private and public keys.
An encrypted backup is also automatically detected and the root keyring and gpg-agent is used by default. Set \fBGNUPGHOME\fR environment variable to use a custom keyring and gpg2 configuration.
.TP
Within the subdirectory is file, header, that describes the back up including the type, system, date of backup, the version of IPA, the version of the backup and the services on the master.
.TP
@ -61,9 +61,6 @@ The Directory Manager password.
\fB\-\-data\fR
Restore the data only. The default is to restore everything in the backup.
.TP
\fB\-\-gpg\-keyring\fR=\fIGPG_KEYRING\fR
The full path to a GPG keyring. The keyring consists of two files, a public and a private key (.sec and .pub respectively). Specify the path without an extension.
.TP
\fB\-\-no\-logs\fR
Exclude the IPA service log files in the backup (if they were backed up).
.TP
@ -91,6 +88,10 @@ Log to the given file
0 if the command was successful
1 if an error occurred
.SH "ENVIRONMENT VARIABLES"
.PP
\fBGNUPGHOME\fR
Use custom GnuPG keyring and settings (default: \fB~/.gnupg\fR).
.SH "FILES"
.PP
\fI/var/lib/ipa/backup\fR
@ -103,4 +104,5 @@ The default directory for storing backup files.
The log file for restoration
.PP
.SH "SEE ALSO"
ipa\-backup(1).
.BR ipa\-backup(1)
.BR gpg2(1)

View File

@ -167,8 +167,8 @@ class BasePathNamespace(object):
CHROMIUM_BROWSER = "/usr/bin/chromium-browser"
FIREFOX = "/usr/bin/firefox"
GETCERT = "/usr/bin/getcert"
GPG = "/usr/bin/gpg"
GPG2 = "/usr/bin/gpg2"
GPG_CONNECT_AGENT = "/usr/bin/gpg-connect-agent"
GPG_AGENT = "/usr/bin/gpg-agent"
IPA_GETCERT = "/usr/bin/ipa-getcert"
KADMIN_LOCAL = '/usr/sbin/kadmin.local'

View File

@ -17,11 +17,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import absolute_import
from __future__ import absolute_import, print_function
import logging
import optparse # pylint: disable=deprecated-module
import os
import shutil
import sys
import tempfile
import time
import pwd
@ -53,45 +55,38 @@ ISO8601_DATETIME_FMT = '%Y-%m-%dT%H:%M:%S'
logger = logging.getLogger(__name__)
"""
A test gpg can be generated like this:
A test GnuPG key can be generated like this:
# cat >keygen <<EOF
%echo Generating a standard key
Key-Type: RSA
Key-Length: 2048
Name-Real: IPA Backup
Name-Comment: IPA Backup
Name-Email: root@example.com
Expire-Date: 0
%pubring /root/backup.pub
%secring /root/backup.sec
%commit
%echo done
%echo Generating a standard key
Key-Type: RSA
Key-Length: 2048
Name-Real: IPA Backup
Name-Comment: IPA Backup
Name-Email: root@example.com
Expire-Date: 0
Passphrase: SecretPassPhrase42
%commit
%echo done
EOF
# gpg --batch --gen-key keygen
# gpg --no-default-keyring --secret-keyring /root/backup.sec \
--keyring /root/backup.pub --list-secret-keys
# export GNUPGHOME=/root/backup
# mkdir -p $GNUPGHOME
# gpg2 --batch --gen-key keygen
# gpg2 --list-secret-keys
"""
def encrypt_file(filename, keyring, remove_original=True):
def encrypt_file(filename, remove_original=True):
source = filename
dest = filename + '.gpg'
args = [paths.GPG,
'--batch',
'--default-recipient-self',
'-o', dest]
if keyring is not None:
args.append('--no-default-keyring')
args.append('--keyring')
args.append(keyring + '.pub')
args.append('--secret-keyring')
args.append(keyring + '.sec')
args.append('-e')
args.append(source)
args = [
paths.GPG2,
'--batch',
'--default-recipient-self',
'--output', dest,
'--encrypt', source,
]
result = run(args, raiseonerr=False)
if result.returncode != 0:
@ -233,16 +228,22 @@ class Backup(admintool.AdminTool):
def add_options(cls, parser):
super(Backup, cls).add_options(parser, debug_option=True)
parser.add_option("--gpg-keyring", dest="gpg_keyring",
help="The gpg key name to be used (or full path)")
parser.add_option("--gpg", dest="gpg", action="store_true",
default=False, help="Encrypt the backup")
parser.add_option("--data", dest="data_only", action="store_true",
parser.add_option(
"--gpg-keyring", dest="gpg_keyring",
help=optparse.SUPPRESS_HELP)
parser.add_option(
"--gpg", dest="gpg", action="store_true",
default=False, help="Encrypt the backup")
parser.add_option(
"--data", dest="data_only", action="store_true",
default=False, help="Backup only the data")
parser.add_option("--logs", dest="logs", action="store_true",
parser.add_option(
"--logs", dest="logs", action="store_true",
default=False, help="Include log files in backup")
parser.add_option("--online", dest="online", action="store_true",
default=False, help="Perform the LDAP backups online, for data only.")
parser.add_option(
"--online", dest="online", action="store_true",
default=False,
help="Perform the LDAP backups online, for data only.")
def setup_logging(self, log_file_mode='a'):
@ -255,9 +256,11 @@ class Backup(admintool.AdminTool):
installutils.check_server_configuration()
if options.gpg_keyring is not None:
if not os.path.exists(options.gpg_keyring + '.pub'):
raise admintool.ScriptError('No such key %s' %
options.gpg_keyring)
print(
"--gpg-keyring is no longer supported, use GNUPGHOME "
"environment variable to use a custom GnuPG2 directory.",
file=sys.stderr
)
options.gpg = True
if options.online and not options.data_only:
@ -266,7 +269,7 @@ class Backup(admintool.AdminTool):
if options.gpg:
tmpfd = write_tmp_file('encryptme')
newfile = encrypt_file(tmpfd.name, options.gpg_keyring, False)
newfile = encrypt_file(tmpfd.name, False)
os.unlink(newfile)
if options.data_only and options.logs:
@ -627,7 +630,7 @@ class Backup(admintool.AdminTool):
if encrypt:
logger.info('Encrypting %s', filename)
filename = encrypt_file(filename, keyring)
filename = encrypt_file(filename)
shutil.move(self.header, backup_dir)

View File

@ -17,11 +17,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import absolute_import
from __future__ import absolute_import, print_function
import logging
import optparse # pylint: disable=deprecated-module
import os
import shutil
import sys
import tempfile
import time
import pwd
@ -77,7 +79,7 @@ def recursive_chown(path, uid, gid):
os.chmod(os.path.join(root, file), 0o640)
def decrypt_file(tmpdir, filename, keyring):
def decrypt_file(tmpdir, filename):
source = filename
(dest, ext) = os.path.splitext(filename)
@ -87,19 +89,12 @@ def decrypt_file(tmpdir, filename, keyring):
dest = os.path.basename(dest)
dest = os.path.join(tmpdir, dest)
args = [paths.GPG,
'--batch',
'-o', dest]
if keyring is not None:
args.append('--no-default-keyring')
args.append('--keyring')
args.append(keyring + '.pub')
args.append('--secret-keyring')
args.append(keyring + '.sec')
args.append('-d')
args.append(source)
args = [
paths.GPG2,
'--batch',
'--output', dest,
'--decrypt', source,
]
result = run(args, raiseonerr=False)
if result.returncode != 0:
@ -161,21 +156,30 @@ class Restore(admintool.AdminTool):
def add_options(cls, parser):
super(Restore, cls).add_options(parser, debug_option=True)
parser.add_option("-p", "--password", dest="password",
parser.add_option(
"-p", "--password", dest="password",
help="Directory Manager password")
parser.add_option("--gpg-keyring", dest="gpg_keyring",
help="The gpg key name to be used")
parser.add_option("--data", dest="data_only", action="store_true",
parser.add_option(
"--gpg-keyring", dest="gpg_keyring",
help=optparse.SUPPRESS_HELP)
parser.add_option(
"--data", dest="data_only", action="store_true",
default=False, help="Restore only the data")
parser.add_option("--online", dest="online", action="store_true",
default=False, help="Perform the LDAP restores online, for data only.")
parser.add_option("--instance", dest="instance",
parser.add_option(
"--online", dest="online", action="store_true",
default=False,
help="Perform the LDAP restores online, for data only.")
parser.add_option(
"--instance", dest="instance",
help="The 389-ds instance to restore (defaults to all found)")
parser.add_option("--backend", dest="backend",
parser.add_option(
"--backend", dest="backend",
help="The backend to restore within the instance or instances")
parser.add_option('--no-logs', dest="no_logs", action="store_true",
parser.add_option(
'--no-logs', dest="no_logs", action="store_true",
default=False, help="Do not restore log files from the backup")
parser.add_option('-U', '--unattended', dest="unattended",
parser.add_option(
'-U', '--unattended', dest="unattended",
action="store_true", default=False,
help="Unattended restoration never prompts the user")
@ -201,9 +205,11 @@ class Restore(admintool.AdminTool):
parser.error("must provide path to backup directory")
if options.gpg_keyring:
if (not os.path.exists(options.gpg_keyring + '.pub') or
not os.path.exists(options.gpg_keyring + '.sec')):
parser.error("no such key %s" % options.gpg_keyring)
print(
"--gpg-keyring is no longer supported, use GNUPGHOME "
"environment variable to use a custom GnuPG2 directory.",
file=sys.stderr
)
def ask_for_options(self):
@ -327,7 +333,7 @@ class Restore(admintool.AdminTool):
try:
dirsrv = services.knownservices.dirsrv
self.extract_backup(options.gpg_keyring)
self.extract_backup()
if restore_type == 'FULL':
self.restore_default_conf()
@ -754,8 +760,7 @@ class Restore(admintool.AdminTool):
self.backup_services = config.get('ipa', 'services').split(',')
# pylint: enable=no-member
def extract_backup(self, keyring=None):
def extract_backup(self):
'''
Extract the contents of the tarball backup into a temporary location,
decrypting if necessary.
@ -776,7 +781,7 @@ class Restore(admintool.AdminTool):
if encrypt:
logger.info('Decrypting %s', filename)
filename = decrypt_file(self.dir, filename, keyring)
filename = decrypt_file(self.dir, filename)
os.chdir(self.dir)

View File

@ -1,15 +1,23 @@
#
# Copyright (C) 2017 FreeIPA Contributors. See COPYING for license
#
from __future__ import absolute_import
import binascii
import os
import re
import subprocess
import shutil
import tempfile
import textwrap
import pytest
from ipaplatform.paths import paths
from ipapython import ipautil
from ipaserver.install import installutils
from ipaserver.install import ipa_backup
from ipaserver.install import ipa_restore
EXAMPLE_CONFIG = [
'foo=1\n',
@ -22,8 +30,6 @@ WHITESPACE_CONFIG = [
]
@pytest.fixture
def tempdir(request):
tempdir = tempfile.mkdtemp()
@ -35,6 +41,87 @@ def tempdir(request):
return tempdir
GPG_GENKEY = textwrap.dedent("""
%echo Generating a standard key
Key-Type: RSA
Key-Length: 2048
Name-Real: IPA Backup
Name-Comment: IPA Backup
Name-Email: root@example.com
Expire-Date: 0
Passphrase: {passphrase}
%commit
%echo done
""")
@pytest.fixture
def gpgkey(request, tempdir):
passphrase = "Secret123"
gnupghome = os.path.join(tempdir, "gnupg")
os.makedirs(gnupghome, 0o700)
# provide clean env for gpg test
env = os.environ.copy()
orig_gnupghome = env.get('GNUPGHOME')
env['GNUPGHOME'] = gnupghome
env['LC_ALL'] = 'C.UTF-8'
env['LANGUAGE'] = 'C'
devnull = open(os.devnull, 'w')
# allow passing passphrases to agent
with open(os.path.join(gnupghome, "gpg-agent.conf"), 'w') as f:
f.write("verbose\n")
f.write("allow-preset-passphrase\n")
# run agent in background
agent = subprocess.Popen(
[paths.GPG_AGENT, '--batch', '--daemon'],
env=env, stdout=devnull, stderr=devnull
)
def fin():
if orig_gnupghome is not None:
os.environ['GNUPGHOME'] = orig_gnupghome
else:
os.environ.pop('GNUPGHOME', None)
agent.kill()
agent.wait()
request.addfinalizer(fin)
# create public / private key pair
keygen = os.path.join(gnupghome, 'keygen')
with open(keygen, 'w') as f:
f.write(GPG_GENKEY.format(passphrase=passphrase))
subprocess.check_call(
[paths.GPG2, '--batch', '--gen-key', keygen],
env=env, stdout=devnull, stderr=devnull
)
# get keygrip of private key
out = subprocess.check_output(
[paths.GPG2, "--list-secret-keys", "--with-keygrip"],
env=env, stderr=subprocess.STDOUT
)
mo = re.search("Keygrip = ([A-Z0-9]{32,})", out.decode('utf-8'))
if mo is None:
raise ValueError(out.decode('utf-8'))
keygrip = mo.group(1)
# unlock private key
cmd = "PRESET_PASSPHRASE {} -1 {}".format(
keygrip,
binascii.hexlify(passphrase.encode('utf-8')).decode('utf-8')
)
subprocess.check_call(
[paths.GPG_CONNECT_AGENT, cmd, "/bye"],
env=env, stdout=devnull, stderr=devnull
)
# set env for the rest of the progress
os.environ['GNUPGHOME'] = gnupghome
class test_set_directive_lines(object):
def test_remove_directive(self):
lines = installutils.set_directive_lines(
@ -198,3 +285,21 @@ def test_gpg_encrypt(tempdir):
with pytest.raises(ipautil.CalledProcessError):
installutils.decrypt_file(encrypted, decrypted, password='invalid')
def test_gpg_asymmetric(tempdir, gpgkey):
src = os.path.join(tempdir, "asymmetric.txt")
encrypted = src + ".gpg"
payload = 'Dummy text\n'
with open(src, 'w') as f:
f.write(payload)
ipa_backup.encrypt_file(src, remove_original=True)
assert os.path.isfile(encrypted)
assert not os.path.exists(src)
ipa_restore.decrypt_file(tempdir, encrypted)
assert os.path.isfile(src)
with open(src) as f:
assert f.read() == payload