Hello freeipa-devel,

this patcheset implement the main piece of the replica promotion
feature.

It first introduces the custodia modules, custodia is a service that
allows to securely transfer secrets between FreeIPA instances, using
asymetric crypto and LDAP published keys to insure confidentiality.

These patches intentionally duplicate some code in the installer in
order to avoid regression in the "classic" installer code path, in the
hope that the promotion functionality will not unintentionally break the
classic prepare/install code paths.

To use test this patchset you need the jwcrypto and custodia python
packages. Jwcrypto ins in fedora rawhide already (built today for f22
too) and Custodia is under review. I prepared two copr repositories for
now so people can build.
Use dnf copr enable simo/jwcrypto and dnf copr enable simo/custodia on
your devel VMs to get the proper packages (dnf install custodia will
suffice to drag in all dependencies).

To test do NOT follow the usual path of creating a replica file on the
master server with the ipa-replica-prepare tool.
Instead prepare a machine and run:
ipa-client-install
ipa-replica-install --promote

That should be it.

You can optionally test the --setup-dns install option, but --setup-ca
and --seyup-kra do not work yet.

If you kinit admin right after the client install, you'll be asked no
passwords.

Note that you need to raise the domain level to 1 before you can use the
replica promotion code as it is intended to be used with the topology
plugin activated.

This patchset depends on the previous one sent last week.

Cheers,
Simo.

-- 
Simo Sorce * Red Hat, Inc * New York
>From 5087b3dc3b3cf09613e5c516561b800cd1f73f76 Mon Sep 17 00:00:00 2001
From: Simo Sorce <s...@redhat.com>
Date: Fri, 8 May 2015 13:39:29 -0400
Subject: [PATCH 1/6] IPA Custodia Daemon

IPA specific plugins and configuration for using Custodia as a key
transfer mechanism.

Signed-off-by: Simo Sorce <s...@redhat.com>
---
 daemons/ipa-custodia/Makefile            |  27 ++++
 daemons/ipa-custodia/ipakeys/__init__.py |   0
 daemons/ipa-custodia/ipakeys/common.py   |  45 +++++++
 daemons/ipa-custodia/ipakeys/kem.py      | 203 +++++++++++++++++++++++++++++++
 daemons/ipa-custodia/ipakeys/store.py    | 197 ++++++++++++++++++++++++++++++
 daemons/ipa-custodia/ipakeys/tests.py    |  55 +++++++++
 daemons/ipa-custodia/setup.py            |  16 +++
 install/updates/73-custodia.update       |   4 +
 ipaplatform/base/paths.py                |   1 +
 9 files changed, 548 insertions(+)
 create mode 100644 daemons/ipa-custodia/Makefile
 create mode 100644 daemons/ipa-custodia/ipakeys/__init__.py
 create mode 100644 daemons/ipa-custodia/ipakeys/common.py
 create mode 100644 daemons/ipa-custodia/ipakeys/kem.py
 create mode 100644 daemons/ipa-custodia/ipakeys/store.py
 create mode 100644 daemons/ipa-custodia/ipakeys/tests.py
 create mode 100755 daemons/ipa-custodia/setup.py
 create mode 100644 install/updates/73-custodia.update

diff --git a/daemons/ipa-custodia/Makefile b/daemons/ipa-custodia/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..fbe148103f0731ee40717b71b32be6dd074289df
--- /dev/null
+++ b/daemons/ipa-custodia/Makefile
@@ -0,0 +1,27 @@
+all: lint pep8 test
+	echo "All tests passed"
+
+lint:
+	# Analyze code
+	# don't show recommendations, info, comments, report
+	# W0613 - unused argument
+	# Ignore cherrypy class members as they are dynamically added
+	pylint -d c,r,i,W0613 -r n -f colorized \
+		   --notes= \
+		   --disable=star-args \
+		   ./ipakeys
+
+pep8:
+	# Check style consistency
+	pep8 ipakeys
+
+clean:
+	rm -fr build dist *.egg-info
+	find ./ -name '*.pyc' -exec rm -f {} \;
+
+cscope:
+	git ls-files | xargs pycscope
+
+test:
+	rm -f .coverage
+	nosetests -s
diff --git a/daemons/ipa-custodia/ipakeys/__init__.py b/daemons/ipa-custodia/ipakeys/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/daemons/ipa-custodia/ipakeys/common.py b/daemons/ipa-custodia/ipakeys/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..caed9a5e7481e561b8032e65981a242d84b57c1a
--- /dev/null
+++ b/daemons/ipa-custodia/ipakeys/common.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2015  IPA Project Contributors, see COPYING for license
+from __future__ import print_function
+import ldap
+import ldap.sasl
+import ldap.filter
+
+
+class IKLdap(object):
+
+    def __init__(self, uri, auth_type=None):
+        self.uri = uri
+        if auth_type is not None:
+            self.auth_type = auth_type
+        else:
+            if uri.startswith('ldapi'):
+                self.auth_type = 'EXTERNAL'
+            else:
+                self.auth_type = 'GSSAPI'
+        self._basedn = None
+
+    @property
+    def basedn(self):
+        if self._basedn is None:
+            conn = self.connect()
+            r = conn.search_s('', ldap.SCOPE_BASE)
+            self._basedn = r[0][1]['defaultnamingcontext'][0]
+        return self._basedn
+
+    def connect(self):
+        conn = ldap.initialize(self.uri)
+        if self.auth_type == 'EXTERNAL':
+            auth_tokens = ldap.sasl.external(None)
+        elif self.auth_type == 'GSSAPI':
+            auth_tokens = ldap.sasl.sasl({}, 'GSSAPI')
+        else:
+            raise ValueError(
+                'Invalid authentication type: %s' % self.auth_type)
+        conn.sasl_interactive_bind_s('', auth_tokens)
+        return conn
+
+    def build_filter(self, formatstr, args):
+        escaped_args = dict()
+        for key, value in args.iteritems():
+            escaped_args[key] = ldap.filter.escape_filter_chars(value)
+        return formatstr.format(**escaped_args)
diff --git a/daemons/ipa-custodia/ipakeys/kem.py b/daemons/ipa-custodia/ipakeys/kem.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c1f9427d1fd898ed787f87d82d74bc19ebeb7b7
--- /dev/null
+++ b/daemons/ipa-custodia/ipakeys/kem.py
@@ -0,0 +1,203 @@
+# Copyright (C) 2015  IPA Project Contributors, see COPYING for license
+
+from __future__ import print_function
+from ipaplatform.paths import paths
+import ConfigParser
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import rsa, ec
+from custodia.message.kem import KEMKeysStore
+from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP
+from jwcrypto.common import json_decode, json_encode
+from jwcrypto.common import base64url_encode
+from jwcrypto.jwk import JWK
+from ipakeys.common import IKLdap
+from binascii import unhexlify
+import ldap
+
+
+IPA_REL_BASE_DN = 'cn=custodia,cn=ipa,cn=etc'
+IPA_KEYS_QUERY = '(&(ipaKeyUsage={usage:s})(memberPrincipal={princ:s}))'
+RFC5280_USAGE_MAP = {KEY_USAGE_SIG: 'digitalSignature',
+                     KEY_USAGE_ENC: 'dataEncipherment'}
+
+
+class KEMLdap(IKLdap):
+
+    @property
+    def keysbase(self):
+        return '%s,%s' % (IPA_REL_BASE_DN, self.basedn)
+
+    def _encode_int(self, i):
+        I = hex(i).rstrip("L").lstrip("0x")
+        return base64url_encode(unhexlify((len(I) % 2) * '0' + I))
+
+    def _parse_public_key(self, ipa_public_key):
+        public_key = serialization.load_der_public_key(ipa_public_key,
+                                                       default_backend())
+        num = public_key.public_numbers()
+        if isinstance(num, rsa.RSAPublicNumbers):
+            return {'kty': 'RSA',
+                    'e': self._encode_int(num.e),
+                    'n': self._encode_int(num.n)}
+        elif isinstance(num, ec.EllipticCurvePublicNumbers):
+            if num.curve.name == 'secp256r1':
+                curve = 'P-256'
+            elif num.curve.name == 'secp384r1':
+                curve = 'P-384'
+            elif num.curve.name == 'secp521r1':
+                curve = 'P-521'
+            else:
+                raise TypeError('Unsupported Elliptic Curve')
+            return {'kty': 'EC',
+                    'crv': curve,
+                    'x': self._encode_int(num.x),
+                    'y': self._encode_int(num.y)}
+        else:
+            raise TypeError('Unknown Public Key type')
+
+    def get_key(self, usage, principal):
+        conn = self.connect()
+        scope = ldap.SCOPE_SUBTREE
+
+        ldap_filter = self.build_filter(IPA_KEYS_QUERY,
+                                        {'usage': RFC5280_USAGE_MAP[usage],
+                                         'princ': principal})
+        r = conn.search_s(self.keysbase, scope, ldap_filter)
+        if len(r) != 1:
+            raise ValueError("Incorrect number of results (%d) searching for"
+                             "public key for %s" % (len(r), principal))
+        ipa_public_key = r[0][1]['ipaPublicKey'][0]
+        jwk = self._parse_public_key(ipa_public_key)
+        jwk['use'] = KEY_USAGE_MAP[usage]
+        return json_encode(jwk)
+
+    def _format_public_key(self, key):
+        if isinstance(key, str):
+            jwkey = json_decode(key)
+            if 'kty' not in jwkey:
+                raise ValueError('Invalid key, missing "kty" attribute')
+            if jwkey['kty'] == 'RSA':
+                pubnum = rsa.RSAPublicNumbers(jwkey['e'], jwkey['n'])
+                pubkey = pubnum.public_key(default_backend())
+            elif jwkey['kty'] == 'EC':
+                if jwkey['crv'] == 'P-256':
+                    curve = ec.SECP256R1
+                elif jwkey['crv'] == 'P-384':
+                    curve = ec.SECP384R1
+                elif jwkey['crv'] == 'P-521':
+                    curve = ec.SECP521R1
+                else:
+                    raise TypeError('Unsupported Elliptic Curve')
+                pubnum = ec.EllipticCurvePublicNumbers(
+                    jwkey['x'], jwkey['y'], curve)
+                pubkey = pubnum.public_key(default_backend())
+            else:
+                raise ValueError('Unknown key type: %s' % jwkey['kty'])
+        elif isinstance(key, rsa.RSAPublicKey):
+            pubkey = key
+        elif isinstance(key, ec.EllipticCurvePublicKey):
+            pubkey = key
+        else:
+            raise TypeError('Unknown key type: %s' % type(key))
+
+        return pubkey.public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.SubjectPublicKeyInfo)
+
+    def set_key(self, usage, host, principal, key):
+        public_key = self._format_public_key(key)
+        conn = self.connect()
+        name = '%s/%s' % (KEY_USAGE_MAP[usage], host)
+        dn = 'cn=%s,%s' % (name, self.keysbase)
+        try:
+            mods = [('objectClass', ['nsContainer',
+                                     'ipaKeyPolicy',
+                                     'ipaPublicKeyObject',
+                                     'groupOfPrincipals']),
+                    ('cn', name),
+                    ('ipaKeyUsage', RFC5280_USAGE_MAP[usage]),
+                    ('memberPrincipal', principal),
+                    ('ipaPublicKey', public_key)]
+            conn.add_s(dn, mods)
+        except Exception:  # pylint: disable=broad-except
+            # This may fail if the entry already exists
+            mods = [(ldap.MOD_REPLACE, 'memberPrincipal', principal),
+                    (ldap.MOD_REPLACE, 'ipaPublicKey', public_key)]
+            conn.modify_s(dn, mods)
+
+
+def newServerKeys(path, keyid):
+    skey = JWK(generate='RSA', use='sig', kid=keyid)
+    ekey = JWK(generate='RSA', use='enc', kid=keyid)
+    with open(path, 'w+') as f:
+        f.write('[%s,%s]' % (skey.export(), ekey.export()))
+    return [skey.get_op_key('verify'), ekey.get_op_key('encrypt')]
+
+
+class IPAKEMKeys(KEMKeysStore):
+    """A KEM Keys Store.
+
+    This is a store that holds public keys of registered
+    clients allowed to use KEM messages. It takes the form
+    of an authorizer merely for the purpose of attaching
+    itself to a 'request' so that later on the KEM Parser
+    can fetch the appropariate key to verify/decrypt an
+    incoming request and make the payload available.
+
+    The KEM Parser will actually perform additional
+    authorization checks in this case.
+
+    SimplePathAuthz is extended here as we want to attach the
+    store only to requests on paths we are configured to
+    manage.
+    """
+
+    def __init__(self, config=None, ipaconf=paths.IPA_DEFAULT_CONF):
+        super(IPAKEMKeys, self).__init__(config)
+        conf = ConfigParser.ConfigParser()
+        conf.read(ipaconf)
+        self.host = conf.get('global', 'host')
+        self.realm = conf.get('global', 'realm')
+        self.ldap_uri = config.get('ldap_uri', None)
+        if self.ldap_uri is None:
+            self.ldap_uri = conf.get('global', 'ldap_uri', None)
+        self._server_keys = None
+
+    def find_key(self, kid, usage):
+        if kid is None:
+            raise TypeError('Key ID is None, should be a SPN')
+        conn = KEMLdap(self.ldap_uri)
+        return conn.get_key(usage, kid)
+
+    def generate_server_keys(self):
+        principal = 'host/%s@%s' % (self.host, self.realm)
+        # Neutralize the key with read if any
+        self._server_keys = None
+        # Generate private key and store it
+        pubkeys = newServerKeys(self.config['server_keys'], principal)
+        # Store public key in LDAP
+        ldapconn = KEMLdap(self.ldap_uri)
+        ldapconn.set_key(KEY_USAGE_SIG, self.host, principal, pubkeys[0])
+        ldapconn.set_key(KEY_USAGE_ENC, self.host, principal, pubkeys[1])
+
+    @property
+    def server_keys(self):
+        if self._server_keys is None:
+            with open(self.config['server_keys']) as f:
+                jsonkeys = f.read()
+            dictkeys = json_decode(jsonkeys)
+            self._server_keys = (JWK(**dictkeys[KEY_USAGE_SIG]),
+                                 JWK(**dictkeys[KEY_USAGE_ENC]))
+        return self._server_keys
+
+
+# Manual testing
+if __name__ == '__main__':
+    IKK = IPAKEMKeys({'paths': '/',
+                      'server_keys': '/etc/ipa/custodia/server.keys'})
+    IKK.generate_server_keys()
+    print(('SIG', IKK.server_keys[0].export_public()))
+    print(('ENC', IKK.server_keys[1].export_public()))
+    print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm), usage=KEY_USAGE_SIG))
+    print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm), usage=KEY_USAGE_ENC))
diff --git a/daemons/ipa-custodia/ipakeys/store.py b/daemons/ipa-custodia/ipakeys/store.py
new file mode 100644
index 0000000000000000000000000000000000000000..580657979465a163bece79f37a9adc7be6e857d3
--- /dev/null
+++ b/daemons/ipa-custodia/ipakeys/store.py
@@ -0,0 +1,197 @@
+# Copyright (C) 2015  IPA Project Contributors, see COPYING for license
+
+from __future__ import print_function
+from base64 import b64encode, b64decode
+from custodia.store.interface import CSStore
+from jwcrypto.common import json_decode, json_encode
+from ipaplatform.paths import paths
+from ipapython import ipautil
+from ipakeys.common import IKLdap
+import ldap
+import os
+import shutil
+import sys
+import StringIO
+import tempfile
+
+
+class UnknownKeyName(Exception):
+    pass
+
+
+class DBMAPHandler(object):
+
+    def __init__(self, config, dbmap, nickname):
+        raise NotImplementedError
+
+    def export_key(self):
+        raise NotImplementedError
+
+    def import_key(self, value):
+        raise NotImplementedError
+
+def log_error(error):
+    print(error, file=sys.stderr)
+
+
+def PKI_TOMCAT_password_callback():
+    password = None
+    with open(paths.PKI_TOMCAT_PASSWORD_CONF) as f:
+        for line in f.readlines():
+            key, value = line.strip().split('=')
+            if key == 'internal':
+                password = value
+                break
+    return password
+
+
+def HTTPD_password_callback():
+    with open(paths.ALIAS_PWDFILE_TXT) as f:
+        password = f.read()
+    return password
+
+
+class NSSCertDB(DBMAPHandler):
+
+    def __init__(self, config, dbmap, nickname):
+        if 'type' not in dbmap or dbmap['type'] != 'NSSDB':
+            raise ValueError('Invalid type "%s",'
+                             ' expected "NSSDB"' % (dbmap['type'],))
+        if 'path' not in dbmap:
+            raise ValueError('Configuration does not provide NSSDB path')
+        if 'pwcallback' not in dbmap:
+            raise ValueError('Configuration does not provide Password Calback')
+        self.nssdb_path = dbmap['path']
+        self.nickname = nickname
+        self.nssdb_password = dbmap['pwcallback']()
+
+    def export_key(self):
+        tdir = tempfile.mkdtemp(dir=paths.TMP)
+        try:
+            nsspwfile = os.path.join(tdir, 'nsspwfile')
+            with open(nsspwfile, 'w+') as f:
+                f.write(self.nssdb_password)
+            pk12pwfile = os.path.join(tdir, 'pk12pwfile')
+            password = b64encode(os.urandom(16))
+            with open(pk12pwfile, 'w+') as f:
+                f.write(password)
+            pk12file = os.path.join(tdir, 'pk12file')
+            ipautil.run([paths.PK12UTIL,
+                         "-d", self.nssdb_path,
+                         "-o", pk12file,
+                         "-n", self.nickname,
+                         "-k", nsspwfile,
+                         "-w", pk12pwfile])
+            with open(pk12file, 'r') as f:
+                data = f.read()
+        finally:
+            shutil.rmtree(tdir)
+        return json_encode({'export password': password,
+                            'pkcs12 data': b64encode(data)})
+
+    def import_key(self, value):
+        v = json_decode(value)
+        tdir = tempfile.mkdtemp(dir=paths.TMP)
+        try:
+            nsspwfile = os.path.join(tdir, 'nsspwfile')
+            with open(nsspwfile, 'w+') as f:
+                f.write(self.nssdb_password)
+            pk12pwfile = os.path.join(tdir, 'pk12pwfile')
+            with open(pk12pwfile, 'w+') as f:
+                f.write(v['export password'])
+            pk12file = os.path.join(tdir, 'pk12file')
+            with open(pk12file, 'w+') as f:
+                f.write(b64decode(v['pkcs12 data']))
+            ipautil.run([paths.PK12UTIL,
+                         "-d", self.nssdb_path,
+                         "-i", pk12file,
+                         "-n", self.nickname,
+                         "-k", nsspwfile,
+                         "-w", pk12pwfile])
+        finally:
+            shutil.rmtree(tdir)
+
+
+# Exfiltrate the DM password Hash so it can be set in replica's and this
+# way let a replica be install without knowing the DM password and yet
+# still keep the DM password synchronized across replicas
+class DMLDAP(DBMAPHandler):
+
+    def __init__(self, config, dbmap, nickname):
+        if 'type' not in dbmap or dbmap['type'] != 'DMLDAP':
+            raise ValueError('Invalid type "%s",'
+                             ' expected "DMLDAP"' % (dbmap['type'],))
+        if nickname != 'DMHash':
+            raise UnknownKeyName("Unknown Key Named '%s'" % nickname)
+        self.ldap = IKLdap(config['ldap_uri'], config.get('auth_type', None))
+
+    def export_key(self):
+        conn = self.ldap.connect()
+        r = conn.search_s('cn=config', ldap.SCOPE_BASE,
+                          attrlist=['nsslapd-rootpw'])
+        if len(r) != 1:
+            raise RuntimeError('DM Hash not found!')
+        return json_encode({'dmhash': r[0][1]['nsslapd-rootpw'][0]})
+
+    def import_key(self, value):
+        v = json_decode(value)
+        conn = self.ldap.connect()
+        mods = [(ldap.MOD_REPLACE, 'nsslapd-rootpw', str(v['dmhash']))]
+        conn.modify_s('cn=config', mods)
+
+
+NAME_DB_MAP = {
+    'ca': {
+        'type': 'NSSDB',
+        'path': paths.PKI_TOMCAT_ALIAS_DIR,
+        'handler': NSSCertDB,
+        'pwcallback': PKI_TOMCAT_password_callback,
+    },
+    'ra': {
+        'type': 'NSSDB',
+        'path': paths.HTTPD_ALIAS_DIR,
+        'handler': NSSCertDB,
+        'pwcallback': HTTPD_password_callback,
+    },
+    'dm': {
+        'type': 'DMLDAP',
+        'handler': DMLDAP,
+    }
+}
+
+
+class IPAKeys(CSStore):
+
+    def __init__(self, config=None):
+        self.config = config
+
+    def _get_handler(self, key):
+        path = key.split('/', 3)
+        if len(path) != 3 or path[0] != 'keys':
+            raise ValueError('Invalid name')
+        if path[1] not in NAME_DB_MAP:
+            raise UnknownKeyName("Unknown DB named '%s'" % path[1])
+        dbmap = NAME_DB_MAP[path[1]]
+        return dbmap['handler'](self.config, dbmap, path[2])
+
+    def get(self, key):
+        try:
+            key_handler = self._get_handler(key)
+            value = key_handler.export_key()
+        except Exception as e:  # pylint: disable=broad-except
+            log_error('Error retrievieng key "%s": %s' % (key, str(e)))
+            value = None
+        return value
+
+    def set(self, key, value, replace=False):
+        try:
+            key_handler = self._get_handler(key)
+            key_handler.import_key(value)
+        except Exception as e:  # pylint: disable=broad-except
+            log_error('Error storing key "%s": %s' % (key, str(e)))
+
+    def list(self, keyfilter=None):
+        raise NotImplementedError
+
+    def cut(self, key):
+        raise NotImplementedError
diff --git a/daemons/ipa-custodia/ipakeys/tests.py b/daemons/ipa-custodia/ipakeys/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a2ef3d23d0516341875505eab7cdf542b9062c6
--- /dev/null
+++ b/daemons/ipa-custodia/ipakeys/tests.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2015  FreeIPA Project Contributors - see LICENSE file
+
+from __future__ import print_function
+from ipakeys.store import IPAKeys, NAME_DB_MAP, NSSCertDB
+import os
+import shutil
+import subprocess
+import unittest
+
+
+def _test_password_callback():
+    with open('test-ipakeys-store/pwfile') as f:
+        password = f.read()
+    return password
+
+
+class ipakeysTests(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        try:
+            shutil.rmtree('test-ipakeys-store')
+        except Exception:  # pylint: disable=broad-except
+            pass
+        testdir = 'test-ipakeys-store'
+        pwfile = os.path.join(testdir, 'pwfile')
+        os.mkdir(testdir)
+        with open(pwfile, 'w') as f:
+            f.write('testpw')
+        cls.certdb = os.path.join(testdir, 'certdb')
+        os.mkdir(cls.certdb)
+        cls.cert2db = os.path.join(testdir, 'cert2db')
+        os.mkdir(cls.cert2db)
+        seedfile = os.path.join(testdir, 'seedfile')
+        with open(seedfile, 'w') as f:
+            seed = os.urandom(1024)
+            f.write(seed)
+        subprocess.call(['certutil', '-d', cls.certdb, '-N', '-f', pwfile])
+        subprocess.call(['certutil', '-d', cls.cert2db, '-N', '-f', pwfile])
+        subprocess.call(['certutil', '-d', cls.certdb, '-S', '-f', pwfile,
+                         '-s', 'CN=testCA', '-n', 'testCACert', '-x',
+                         '-t', 'CT,C,C', '-m', '1', '-z', seedfile])
+
+    def test_ipakeys(self):
+        IK = IPAKeys({})
+
+        NAME_DB_MAP['test'] = {
+            'type': 'NSSDB',
+            'path': self.certdb,
+            'handler': NSSCertDB,
+            'pwcallback': _test_password_callback,
+        }
+        value = IK.get('keys/test/testCACert')
+
+        NAME_DB_MAP['test']['path'] = self.cert2db
+        IK.set('keys/test/testCACert', value)
diff --git a/daemons/ipa-custodia/setup.py b/daemons/ipa-custodia/setup.py
new file mode 100755
index 0000000000000000000000000000000000000000..cafe9429c8705804e103cc4d504d3b397a3fde39
--- /dev/null
+++ b/daemons/ipa-custodia/setup.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2015  Custodia project Contributors, for licensee see COPYING
+
+from distutils.core import setup
+
+setup(
+    name = 'ipa-custodia',
+    version = '0.0.1',
+    license = 'GPLv3+',
+    maintainer = 'IPA Project Contributors',
+    maintainer_email = 's...@redhat.com',
+    url='http://freeipa.org/',
+    packages = ['ipakeys'],
+)
+
diff --git a/install/updates/73-custodia.update b/install/updates/73-custodia.update
new file mode 100644
index 0000000000000000000000000000000000000000..f6520fb2e36dd1b234344a8cc4199ab72c664163
--- /dev/null
+++ b/install/updates/73-custodia.update
@@ -0,0 +1,4 @@
+dn: cn=custodia,cn=ipa,cn=etc,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: custodia
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 4c93c1f7162b0aeb4f798ef84e1ac8db4573518b..18c6e05bd260079351e28eb1b16872c75c6fbcaf 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -350,6 +350,7 @@ class BasePathNamespace(object):
     DB2BAK = '/usr/sbin/db2bak'
     KDCPROXY_CONFIG = '/etc/ipa/kdcproxy/kdcproxy.conf'
     CERTMONGER = '/usr/sbin/certmonger'
+    IPA_CUSTODIA_CONF_DIR = '/etc/ipa/custodia'
 
 
 path_namespace = BasePathNamespace
-- 
2.4.2

>From b54e183943f3e63b928649f8d264f777c8ae6e5f Mon Sep 17 00:00:00 2001
From: Simo Sorce <s...@redhat.com>
Date: Wed, 8 Jul 2015 07:42:09 -0400
Subject: [PATCH 2/6] Add Custodia Client code

Add utility lass to implement custodia clients.
Depends on the server being installed to fetch the replica server keys.

Signed-off-by: Simo Sorce <s...@redhat.com>
---
 daemons/ipa-custodia/ipakeys/client.py | 100 +++++++++++++++++++++++++++++++++
 1 file changed, 100 insertions(+)
 create mode 100644 daemons/ipa-custodia/ipakeys/client.py

diff --git a/daemons/ipa-custodia/ipakeys/client.py b/daemons/ipa-custodia/ipakeys/client.py
new file mode 100644
index 0000000000000000000000000000000000000000..b82453387ecbaad93c02c5eefb555052fac4427b
--- /dev/null
+++ b/daemons/ipa-custodia/ipakeys/client.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2015  IPA Project Contributors, see COPYING for license
+
+from __future__ import print_function
+from custodia.message.kem import KEMClient, KEY_USAGE_SIG, KEY_USAGE_ENC
+from jwcrypto.common import json_decode
+from jwcrypto.jwk import JWK
+from ipakeys.kem import IPAKEMKeys
+from ipakeys.store import IPAKeys
+from ipaplatform.paths import paths
+from base64 import b64encode
+import ldapurl
+import gssapi
+import os
+import requests
+
+
+class CustodiaClient(object):
+
+    def _client_keys(self):
+        return self.ikk.server_keys
+
+    def _server_keys(self, server, realm):
+        principal = 'host/%s@%s' % (server, realm)
+        sk = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_SIG)))
+        ek = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_ENC)))
+        return (sk, ek)
+
+    def _ldap_uri(self, realm):
+        dashrealm = '-'.join(realm.split('.'))
+        socketpath = paths.SLAPD_INSTANCE_SOCKET_TEMPLATE % (dashrealm,)
+        return 'ldapi://' + ldapurl.ldapUrlEscape(socketpath)
+
+    def _keystore(self, realm, ldap_uri, auth_type):
+        config = dict()
+        if ldap_uri is None:
+            config['ldap_uri'] = self._ldap_uri(realm)
+        else:
+            config['ldap_uri'] = ldap_uri
+        if auth_type is not None:
+            config['auth_type'] = auth_type
+
+        return IPAKeys(config)
+
+
+    def __init__(self, client, server, realm, ldap_uri=None, auth_type=None):
+        self.client = client
+        self.creds = None
+
+        self.service_name = gssapi.Name('HTTP@%s' % (server,),
+                                        gssapi.NameType.hostbased_service)
+        self.server = server
+
+        keyfile = os.path.join(paths.IPA_CUSTODIA_CONF_DIR, 'server.keys')
+        self.ikk = IPAKEMKeys({'server_keys': keyfile})
+
+        self.kemcli = KEMClient(self._server_keys(server, realm),
+                                self._client_keys())
+
+        self.keystore = self._keystore(realm, ldap_uri, auth_type)
+
+        # FIXME: Remove warnings about missig subjAltName
+        requests.packages.urllib3.disable_warnings()
+
+    def init_creds(self):
+        name = gssapi.Name('host@%s' % (self.client,),
+                           gssapi.NameType.hostbased_service)
+        store = {'client_keytab': paths.KRB5_KEYTAB,
+                 'ccache': 'MEMORY:Custodia_%s' % b64encode(os.urandom(8))}
+        return gssapi.Credentials(name=name, store=store, usage='initiate')
+
+    def _auth_header(self):
+        if not self.creds or self.creds.lifetime < 300:
+            self.creds = self.init_creds()
+        ctx = gssapi.SecurityContext(name=self.service_name, creds=self.creds)
+        authtok = ctx.step()
+        return {'Authorization': 'Negotiate %s' % b64encode(authtok)}
+
+    def fetch_key(self, keyname):
+
+        # Prepare URL
+        url = 'https://%s/ipa/keys/%s' % (self.server, keyname)
+
+        # Prepare signed/encrypted request
+        encalg = ('RSA1_5', 'A256CBC-HS512')
+        request = self.kemcli.make_request(keyname, encalg=encalg)
+
+        # Prepare Authentication header
+        headers = self._auth_header()
+
+        # Perform request
+        r = requests.get(url, headers=headers,
+                         params={'type': 'kem', 'value': request})
+        r.raise_for_status()
+        reply = r.json()
+
+        if 'type' not in reply or reply['type'] != 'kem':
+            raise RuntimeError('Invlid JSON response type')
+
+        value = self.kemcli.parse_reply(keyname, reply['value'])
+        self.keystore.set('keys/%s' % keyname, value)
-- 
2.4.2

>From ff1b9582655ca5ddf306ac3f7107dd3859d736fa Mon Sep 17 00:00:00 2001
From: Simo Sorce <s...@redhat.com>
Date: Fri, 29 May 2015 15:53:37 -0400
Subject: [PATCH 3/6] Install ipa-custodia with the rest of ipa

Also templatize ipa-custodia configuration file

Signed-off-by: Simo Sorce <s...@redhat.com>
---
 Makefile                                   |  2 ++
 daemons/ipa-custodia/ipa-custodia.service  | 13 ++++++++
 daemons/ipa-custodia/setup.py              |  2 +-
 freeipa.spec.in                            | 19 ++++++++++++
 install/conf/ipa.conf                      | 10 +++++-
 install/share/Makefile.am                  |  1 +
 install/share/bootstrap-template.ldif      |  6 ++++
 install/share/custodia.conf.template       | 28 +++++++++++++++++
 ipaplatform/base/paths.py                  |  3 ++
 ipaserver/install/custodiainstance.py      | 50 ++++++++++++++++++++++++++++++
 ipaserver/install/installutils.py          |  6 ++++
 ipaserver/install/server/install.py        |  7 ++++-
 ipaserver/install/server/replicainstall.py |  9 +++++-
 ipaserver/install/server/upgrade.py        |  6 +++-
 ipaserver/install/service.py               |  1 +
 15 files changed, 158 insertions(+), 5 deletions(-)
 create mode 100644 daemons/ipa-custodia/ipa-custodia.service
 create mode 100644 install/share/custodia.conf.template
 create mode 100644 ipaserver/install/custodiainstance.py

diff --git a/Makefile b/Makefile
index 3c81466d3728022c1d9cf5bb216990f14a59b7e5..5e8d6a61afffe4b38f4462acef9246cf12a0a1b5 100644
--- a/Makefile
+++ b/Makefile
@@ -179,9 +179,11 @@ server-install: server
 	if [ "$(DESTDIR)" = "" ]; then \
 		$(PYTHON) setup.py install; \
 		(cd ipaplatform && $(PYTHON) setup.py install); \
+		(cd daemons/ipa-custodia && $(PYTHON) setup.py install); \
 	else \
 		$(PYTHON) setup.py install --root $(DESTDIR); \
 		(cd ipaplatform && $(PYTHON) setup.py install --root $(DESTDIR)); \
+		(cd daemons/ipa-custodia && $(PYTHON) setup.py install --root $(DESTDIR)); \
 	fi
 
 tests: version-update tests-man-autogen
diff --git a/daemons/ipa-custodia/ipa-custodia.service b/daemons/ipa-custodia/ipa-custodia.service
new file mode 100644
index 0000000000000000000000000000000000000000..ff930fbbb95d34699b1fb050413c226433c883f9
--- /dev/null
+++ b/daemons/ipa-custodia/ipa-custodia.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=IPA Custodia Service
+
+[Service]
+Type=simple
+
+ExecStart=/usr/sbin/custodia /etc/ipa/custodia/custodia.conf
+PrivateTmp=yes
+Restart=on-failure
+RestartSec=60s
+
+[Install]
+WantedBy=multi-user.target
diff --git a/daemons/ipa-custodia/setup.py b/daemons/ipa-custodia/setup.py
index cafe9429c8705804e103cc4d504d3b397a3fde39..4ddf05c891ebd5ca0ed37a7361631696bac88e86 100755
--- a/daemons/ipa-custodia/setup.py
+++ b/daemons/ipa-custodia/setup.py
@@ -12,5 +12,5 @@ setup(
     maintainer_email = 's...@redhat.com',
     url='http://freeipa.org/',
     packages = ['ipakeys'],
+    data_files = [('/usr/lib/systemd/system', ['ipa-custodia.service'])]
 )
-
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 0351952c692eb0cee2148053462c50b6d9073b5d..12117de6dabc56fda3b37784a87c3c13a9f9ced8 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -160,6 +160,9 @@ Requires: softhsm >= 2.0.0rc1-1
 Requires: p11-kit
 Requires: systemd-python
 Requires: %{etc_systemd_dir}
+Requires: python-gssapi >= 1.1.0
+Requires: python-nss >= 0.16.0-1
+Requires: custodia
 
 Conflicts: %{alt_name}-server
 Obsoletes: %{alt_name}-server < %{version}
@@ -534,6 +537,18 @@ mkdir -p %{buildroot}%{_sysconfdir}/cron.d
 (cd %{buildroot}/%{python_sitelib}/ipatests && find . -type f  | \
     sed -e 's,\.py.*$,.*,g' | sort -u | \
     sed -e 's,\./,%%{python_sitelib}/ipatests/,g' ) >tests-python.list
+
+mkdir -p %{buildroot}%{_sysconfdir}/ipa/custodia
+
+(cd %{buildroot}/%{python_sitelib}/ipakeys && find . -type f  | \
+    grep -v tests | \
+    sed -e 's,\.py.*$,.*,g' | sort -u | \
+    sed -e 's,\./,%%{python_sitelib}/ipakeys/,g' ) >>server-python.list
+
+(cd %{buildroot}/%{python_sitelib}/ipakeys && find . -type f  | \
+    grep tests | xargs rm -f)
+
+
 %endif # ONLY_CLIENT
 
 %clean
@@ -743,6 +758,7 @@ fi
 %attr(644,root,root) %{_unitdir}/ipa-dnskeysyncd.service
 %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.socket
 %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.service
+%attr(644,root,root) %{_unitdir}/ipa-custodia.service
 %attr(644,root,root) %{etc_systemd_dir}/httpd.service
 # END
 %dir %{python_sitelib}/ipaserver
@@ -752,6 +768,7 @@ fi
 %dir %{python_sitelib}/ipaserver/advise
 %dir %{python_sitelib}/ipaserver/advise/plugins
 %dir %{python_sitelib}/ipaserver/plugins
+%dir %{python_sitelib}/ipakeys
 %dir %{_libdir}/ipa/certmonger
 %attr(755,root,root) %{_libdir}/ipa/certmonger/*
 %dir %{_usr}/share/ipa
@@ -861,6 +878,7 @@ fi
 %ghost %{_localstatedir}/lib/ipa/pki-ca/publish
 %ghost %{_localstatedir}/named/dyndb-ldap/ipa
 %attr(755,root,root) %{_libdir}/krb5/plugins/kdb/ipadb.so
+%dir %attr(0700,root,root) %{_sysconfdir}/ipa/custodia
 %{_mandir}/man1/ipa-replica-conncheck.1.gz
 %{_mandir}/man1/ipa-replica-install.1.gz
 %{_mandir}/man1/ipa-replica-manage.1.gz
@@ -899,6 +917,7 @@ fi
 %{_sysconfdir}/dbus-1/system.d/oddjob-ipa-trust.conf
 %{_sysconfdir}/oddjobd.conf.d/oddjobd-ipa-trust.conf
 %%attr(755,root,root) %{_libexecdir}/ipa/com.redhat.idm.trust-fetch-domains
+%{python_sitelib}/ipa_custodia-*.egg-info
 
 %endif # ONLY_CLIENT
 
diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf
index e2b602c8573078f517badac00a8c8c5bd593db28..af58e517b75b27fa927c21d9673433934b5e8fa4 100644
--- a/install/conf/ipa.conf
+++ b/install/conf/ipa.conf
@@ -1,5 +1,5 @@
 #
-# VERSION 18 - DO NOT REMOVE THIS LINE
+# VERSION 19 - DO NOT REMOVE THIS LINE
 #
 # This file may be overwritten on upgrades.
 #
@@ -103,6 +103,14 @@ WSGIScriptReloading Off
   Allow from all
 </Location>
 
+# Custodia stuff is redirected to the custodia daemon
+# after authentication
+<Location "/ipa/keys/">
+    ProxyPass "unix:/run/httpd/ipa-custodia.sock|http://localhost/keys/";
+    RequestHeader set GSS_NAME %{GSS_NAME}s
+    RequestHeader set REMOTE_USER %{REMOTE_USER}s
+</Location>
+
 # This is where we redirect on failed auth
 Alias /ipa/errors "/usr/share/ipa/html"
 
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index 80e959a751a0800c4d56c379a73b68a2f12570d7..952b22efe8d07d0fd86cde0ae8216902662ca858 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -28,6 +28,7 @@ app_DATA =				\
 	anonymous-vlv.ldif		\
 	bootstrap-template.ldif		\
 	caJarSigningCert.cfg.template	\
+	custodia.conf.template		\
 	default-aci.ldif		\
 	default-caacl.ldif		\
 	default-hbac.ldif		\
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index 2387f220fd4fe6e3ccd59f4b592f2473d7acfa44..357062780c5011f477a592af4422bb77466472a8 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -167,6 +167,12 @@ objectClass: nsContainer
 objectClass: top
 cn: certificates
 
+dn: cn=custodia,cn=ipa,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: custodia
+
 dn: cn=s4u2proxy,cn=etc,$SUFFIX
 changetype: add
 objectClass: nsContainer
diff --git a/install/share/custodia.conf.template b/install/share/custodia.conf.template
new file mode 100644
index 0000000000000000000000000000000000000000..27c8affc2548efa4d49ad803bf7145d943f7ab0e
--- /dev/null
+++ b/install/share/custodia.conf.template
@@ -0,0 +1,28 @@
+[global]
+server_version = "IPAKeys/0.0.1"
+server_socket = $IPA_CUSTODIA_SOCKET
+auditlog = $IPA_CUSTODIA_AUDIT_LOG
+
+[auth:simple]
+handler = custodia.httpd.authenticators.SimpleCredsAuth
+uid = 48
+gid = 48
+
+[auth:header]
+handler = custodia.httpd.authenticators.SimpleHeaderAuth
+header = GSS_NAME
+
+[authz:kemkeys]
+handler = ipakeys.kem.IPAKEMKeys
+paths = /keys
+store = ipa
+server_keys = $IPA_CUSTODIA_CONF_DIR/server.keys
+
+[store:ipa]
+handler = ipakeys.store.IPAKeys
+ldap_uri = $LDAP_URI
+
+[/keys]
+handler = custodia.secrets.Secrets
+allowed_keytypes = kem
+store = ipa
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 18c6e05bd260079351e28eb1b16872c75c6fbcaf..4dabc4de609213462a646d0249516fe7c07b0c76 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -351,6 +351,9 @@ class BasePathNamespace(object):
     KDCPROXY_CONFIG = '/etc/ipa/kdcproxy/kdcproxy.conf'
     CERTMONGER = '/usr/sbin/certmonger'
     IPA_CUSTODIA_CONF_DIR = '/etc/ipa/custodia'
+    IPA_CUSTODIA_CONF = '/etc/ipa/custodia/custodia.conf'
+    IPA_CUSTODIA_SOCKET = '/run/httpd/ipa-custodia.sock'
+    IPA_CUSTODIA_AUDIT_LOG = '/var/log/ipa-custodia.audit.log'
 
 
 path_namespace = BasePathNamespace
diff --git a/ipaserver/install/custodiainstance.py b/ipaserver/install/custodiainstance.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c26e51de5ce2d6de5b51ae41c9590230adae3f9
--- /dev/null
+++ b/ipaserver/install/custodiainstance.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2015 FreeIPa Project Contributors, see 'COPYING' for license.
+
+from ipakeys.kem import IPAKEMKeys
+from ipaplatform.paths import paths
+from service import SimpleServiceInstance
+from ipapython import ipautil
+from ipaserver.install import installutils
+import os
+
+class CustodiaInstance(SimpleServiceInstance):
+    def __init__(self):
+        super(CustodiaInstance, self).__init__("ipa-custodia")
+        self.config_file = paths.IPA_CUSTODIA_CONF
+        self.server_keys = os.path.join(paths.IPA_CUSTODIA_CONF_DIR,
+                                        'server.keys')
+
+    def __config_file(self):
+        template_file = os.path.basename(self.config_file) + '.template'
+        template = os.path.join(ipautil.SHARE_DIR, template_file)
+        sub_dict = dict(IPA_CUSTODIA_CONF_DIR=paths.IPA_CUSTODIA_CONF_DIR,
+                        IPA_CUSTODIA_SOCKET=paths.IPA_CUSTODIA_SOCKET,
+                        IPA_CUSTODIA_AUDIT_LOG=paths.IPA_CUSTODIA_AUDIT_LOG,
+                        LDAP_URI=installutils.realm_to_ldapi_uri(self.realm))
+        conf = ipautil.template_file(template, sub_dict)
+        fd = open(self.config_file, "w+")
+        fd.write(conf)
+        fd.flush()
+        fd.close()
+
+    def create_instance(self, *args, **kwargs):
+        self.step("Generating ipa-custodia config file", self.__config_file)
+        self.step("Generating ipa-custodia keys", self.__gen_keys)
+        super(CustodiaInstance, self).create_instance(*args, **kwargs)
+
+    def __gen_keys(self):
+        KeyStore = IPAKEMKeys({'server_keys': self.server_keys})
+        KeyStore.generate_server_keys()
+
+    def upgrade_instance(self, realm):
+        self.realm = realm
+        if not os.path.exists(self.config_file):
+            self.__config_file()
+        if not os.path.exists(self.server_keys):
+            self.__gen_keys()
+
+    def __start(self):
+        super(CustodiaInstance, self).__start()
+
+    def __enable(self):
+        super(CustodiaInstance, self).__enable()
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 02e8526317dbab909ed48a1823000922ce6e6b7a..06b703b94cf4d2ee238944c8d7b1f578a8cde86b 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -35,6 +35,7 @@ from contextlib import contextmanager
 from dns import resolver, rdatatype
 from dns.exception import DNSException
 import ldap
+import ldapurl
 from nss.error import NSPRError
 
 import ipaplatform
@@ -1082,6 +1083,11 @@ def check_version():
 def realm_to_serverid(realm_name):
     return "-".join(realm_name.split("."))
 
+def realm_to_ldapi_uri(realm_name):
+    serverid = realm_to_serverid(realm_name)
+    socketname = paths.SLAPD_INSTANCE_SOCKET_TEMPLATE % (serverid,)
+    return 'ldapi://' + ldapurl.ldapUrlEscape(socketname)
+
 def enable_and_start_oddjobd(sstore):
     oddjobd = services.service('oddjobd')
     sstore.backup_state('oddjobd', 'running', oddjobd.is_running())
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index 015050aa7257f5ef3487ad7f83bc8145de2ead57..d258a5ad29f6d8ee6b0b8acef3e9dfec38fce8db 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -29,7 +29,7 @@ import ipaclient.ntpconf
 from ipaserver.install import (
     bindinstance, ca, cainstance, certs, dns, dsinstance, httpinstance,
     installutils, kra, krbinstance, memcacheinstance, ntpinstance,
-    otpdinstance, replication, service, sysupgrade)
+    otpdinstance, custodiainstance, replication, service, sysupgrade)
 from ipaserver.install.installutils import (
     IPA_MODULES, BadHostError, get_fqdn, get_server_ip_address,
     is_ipa_configured, load_pkcs12, read_password, verify_fqdn)
@@ -803,6 +803,10 @@ def install(installer):
     otpd.create_instance('OTPD', host_name, dm_password,
                          ipautil.realm_to_suffix(realm_name))
 
+    custodia = custodiainstance.CustodiaInstance()
+    custodia.create_instance('KEYS', host_name, dm_password,
+                             ipautil.realm_to_suffix(realm_name))
+
     # Create a HTTP instance
     http = httpinstance.HTTPInstance(fstore)
     if options.http_cert_files:
@@ -1066,6 +1070,7 @@ def uninstall(installer):
     dsinstance.DsInstance(fstore=fstore).uninstall()
     if _server_trust_ad_installed:
         adtrustinstance.ADTRUSTInstance(fstore).uninstall()
+    custodiainstance.CustodiaInstance().uninstall()
     memcacheinstance.MemcacheInstance().uninstall()
     otpdinstance.OtpdInstance().uninstall()
     tasks.restore_network_configuration(fstore, sstore)
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index a0ae53438adb7f01555b75afa91a9d465b1a569a..0a890e64b77d2ffcfbaddc8d9cc9990272a21a50 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -26,7 +26,7 @@ import ipaclient.ntpconf
 from ipaserver.install import (
     bindinstance, ca, cainstance, certs, dns, dsinstance, httpinstance,
     installutils, kra, krbinstance, memcacheinstance, ntpinstance,
-    otpdinstance, service)
+    otpdinstance, custodiainstance, service)
 from ipaserver.install.installutils import create_replica_config
 from ipaserver.install.replication import (
     ReplicationManager, replica_conn_check)
@@ -582,6 +582,13 @@ def install(installer):
     CA.import_ra_cert(config.dir + "/ra.p12")
     CA.fix_ra_perms()
 
+    #FIXME: must be done earlier in replica to fetch keys for CA/ldap server
+    # before they are configured
+    custodia = custodiainstance.CustodiaInstance()
+    custodia.create_instance('KEYS', config.host_name,
+                             config.dirman_password,
+                             ipautil.realm_to_suffix(config.realm_name))
+
     # The DS instance is created before the keytab, add the SSL cert we
     # generated
     ds.add_cert_to_service()
diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index a342642b03abc05d0ce108ff2b774ea25904bd8e..937e9366632d60b06b42e8db2e4ab0b7dd98926f 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -33,6 +33,7 @@ from ipaserver.install import cainstance
 from ipaserver.install import certs
 from ipaserver.install import otpdinstance
 from ipaserver.install import schemaupdate
+from ipaserver.install import custodiainstance
 from ipaserver.install import sysupgrade
 from ipaserver.install import dnskeysyncinstance
 from ipaserver.install.upgradeinstance import IPAUpgrade
@@ -1447,7 +1448,7 @@ def upgrade_configuration():
         service.ldapi = True
         try:
             if not service.is_configured():
-                # 389-ds needs to be running to create the memcache instance
+                # 389-ds needs to be running to create the instances
                 # because we record the new service in cn=masters.
                 ds.start()
                 service.create_instance(ldap_name, fqdn, None,
@@ -1496,6 +1497,9 @@ def upgrade_configuration():
         except ipautil.CalledProcessError as e:
             root_logger.error("Failed to restart %s: %s", bind.service_name, e)
 
+    custodia = custodiainstance.CustodiaInstance()
+    custodia.upgrade_instance(api.env.realm)
+
     ca_restart = any([
         ca_restart,
         ca_upgrade_schema(ca),
diff --git a/ipaserver/install/service.py b/ipaserver/install/service.py
index 2f5f565b16b42bf82889f9d32b80cf6fa584d438..c9c65e0579da0f6c458519fc527f594a95ae4ddc 100644
--- a/ipaserver/install/service.py
+++ b/ipaserver/install/service.py
@@ -40,6 +40,7 @@ SERVICE_LIST = {
     'DNS': ('named', 30),
     'MEMCACHE': ('ipa_memcached', 39),
     'HTTP': ('httpd', 40),
+    'KEYS': ('ipa-custodia', 41),
     'CA': ('%sd' % dogtag.configured_constants().PKI_INSTANCE_NAME, 50),
     'KRA': ('%sd' % dogtag.configured_constants().PKI_INSTANCE_NAME, 51),
     'ADTRUST': ('smb', 60),
-- 
2.4.2

>From 14c28fcd5c0c444f790438b42d5ba4fc678e6bc0 Mon Sep 17 00:00:00 2001
From: Simo Sorce <s...@redhat.com>
Date: Thu, 11 Jun 2015 15:45:38 -0400
Subject: [PATCH 4/6] Implement replica promotion functionality

This patch implements a new flag --promote for the ipa-replica-install command
that allows an administrative user to 'promote' an already joined client to
become a full ipa server.

The only credentials used are that of an administrator. This code relies on
ipa-custodia being available on the peer master as well as a number of other
patches to allow a computer account to request certificates for its services.

Therefore this feature is marked to work only with domain level 1 and above
servers.

Ticket: https://fedorahosted.org/freeipa/ticket/2888

Signed-off-by: Simo Sorce <s...@redhat.com>
---
 install/tools/ipa-replica-install          |   1 +
 ipaplatform/base/paths.py                  |   1 +
 ipapython/install/cli.py                   |  11 +-
 ipaserver/install/certs.py                 |  12 +
 ipaserver/install/custodiainstance.py      |  25 +-
 ipaserver/install/dsinstance.py            |  59 +++-
 ipaserver/install/httpinstance.py          |  20 +-
 ipaserver/install/installutils.py          |  28 +-
 ipaserver/install/krbinstance.py           |  10 +-
 ipaserver/install/replication.py           |  90 +++++
 ipaserver/install/server/replicainstall.py | 549 ++++++++++++++++++++++++++++-
 ipaserver/plugins/ldap2.py                 |   9 +-
 12 files changed, 771 insertions(+), 44 deletions(-)

diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install
index 10a10827ec03a375e5d3bece0a0c6e23df8cffc0..60a853b419e503b5fb7fb1048034feb5c17afa02 100755
--- a/install/tools/ipa-replica-install
+++ b/install/tools/ipa-replica-install
@@ -30,6 +30,7 @@ ReplicaInstall = cli.install_tool(
     usage='%prog [options] REPLICA_FILE',
     log_file_name=paths.IPAREPLICA_INSTALL_LOG,
     debug_option=True,
+    use_private_ccache=False,
 )
 
 
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 4dabc4de609213462a646d0249516fe7c07b0c76..8f4d5d0d246a050e0d2521b8fad79183a53e10cd 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -354,6 +354,7 @@ class BasePathNamespace(object):
     IPA_CUSTODIA_CONF = '/etc/ipa/custodia/custodia.conf'
     IPA_CUSTODIA_SOCKET = '/run/httpd/ipa-custodia.sock'
     IPA_CUSTODIA_AUDIT_LOG = '/var/log/ipa-custodia.audit.log'
+    IPA_GETKEYTAB = '/usr/sbin/ipa-getkeytab'
 
 
 path_namespace = BasePathNamespace
diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py
index 1ba9a815c4c499dff0e7974f399f2de31eb932cd..a60800c3c8754207c3e3728f6707c15d645dea9b 100644
--- a/ipapython/install/cli.py
+++ b/ipapython/install/cli.py
@@ -20,6 +20,7 @@ __all__ = ['install_tool', 'uninstall_tool']
 
 def install_tool(configurable_class, command_name, log_file_name,
                  positional_arguments=None, usage=None, debug_option=False,
+                 use_private_ccache=True,
                  uninstall_log_file_name=None,
                  uninstall_positional_arguments=None, uninstall_usage=None):
     if (uninstall_log_file_name is not None or
@@ -47,6 +48,7 @@ def install_tool(configurable_class, command_name, log_file_name,
             usage=usage,
             debug_option=debug_option,
             uninstall_kwargs=uninstall_kwargs,
+            use_private_ccache=use_private_ccache,
         )
     )
 
@@ -71,6 +73,7 @@ class ConfigureTool(admintool.AdminTool):
     configurable_class = None
     debug_option = False
     positional_arguments = None
+    use_private_ccache = True
 
     @staticmethod
     def _transform(configurable_class):
@@ -301,10 +304,12 @@ class ConfigureTool(admintool.AdminTool):
 
         signal.signal(signal.SIGTERM, self.__signal_handler)
 
-        # Use private ccache
-        with private_ccache():
+        if self.use_private_ccache:
+            with private_ccache():
+                super(ConfigureTool, self).run()
+                cfgr.run()
+        else:
             super(ConfigureTool, self).run()
-
             cfgr.run()
 
     @staticmethod
diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py
index e8084950e86fa4bd2e6debd566faa399a3d5f946..020fffbceed6096ddb46e5c09553b167bf57b59d 100644
--- a/ipaserver/install/certs.py
+++ b/ipaserver/install/certs.py
@@ -652,6 +652,18 @@ class CertDB(object):
     def export_pem_cert(self, nickname, location):
         return self.nssdb.export_pem_cert(nickname, location)
 
+    def request_service_cert(self, nickname, principal, host, pwdconf=False):
+        self.create_from_cacert(paths.IPA_CA_CRT)
+        if pwdconf:
+            self.create_password_conf()
+        reqid = certmonger.request_cert(nssdb=self.secdir,
+                                        nickname=nickname,
+                                        principal=principal,
+                                        subject=host,
+                                        passwd_fname=self.passwd_fname)
+        # Now wait for the cert to appear. Check three times then abort
+        certmonger.wait_for_request(reqid, timeout=15)
+
 
 class _CrossProcessLock(object):
     _DATETIME_FORMAT = '%Y%m%d%H%M%S%f'
diff --git a/ipaserver/install/custodiainstance.py b/ipaserver/install/custodiainstance.py
index 4c26e51de5ce2d6de5b51ae41c9590230adae3f9..13b892944830d1a8a49c8e99ef76cdac535881bf 100644
--- a/ipaserver/install/custodiainstance.py
+++ b/ipaserver/install/custodiainstance.py
@@ -1,6 +1,7 @@
 # Copyright (C) 2015 FreeIPa Project Contributors, see 'COPYING' for license.
 
 from ipakeys.kem import IPAKEMKeys
+from ipakeys.client import CustodiaClient
 from ipaplatform.paths import paths
 from service import SimpleServiceInstance
 from ipapython import ipautil
@@ -13,6 +14,7 @@ class CustodiaInstance(SimpleServiceInstance):
         self.config_file = paths.IPA_CUSTODIA_CONF
         self.server_keys = os.path.join(paths.IPA_CUSTODIA_CONF_DIR,
                                         'server.keys')
+        self.ldap_uri = None
 
     def __config_file(self):
         template_file = os.path.basename(self.config_file) + '.template'
@@ -33,7 +35,8 @@ class CustodiaInstance(SimpleServiceInstance):
         super(CustodiaInstance, self).create_instance(*args, **kwargs)
 
     def __gen_keys(self):
-        KeyStore = IPAKEMKeys({'server_keys': self.server_keys})
+        KeyStore = IPAKEMKeys({'server_keys': self.server_keys,
+                               'ldap_uri': self.ldap_uri})
         KeyStore.generate_server_keys()
 
     def upgrade_instance(self, realm):
@@ -43,6 +46,26 @@ class CustodiaInstance(SimpleServiceInstance):
         if not os.path.exists(self.server_keys):
             self.__gen_keys()
 
+    def create_replica(self, service_name, host_name, master_host_name, realm):
+        self.master_host_name = master_host_name
+        suffix = ipautil.realm_to_suffix(realm)
+        self.ldap_uri = 'ldap://%s' % master_host_name
+
+        self.step("Generating ipa-custodia config file", self.__config_file)
+        self.step("Generating ipa-custodia keys", self.__gen_keys)
+        self.step("Importing RA Key", self.__import_ra_key)
+        super(CustodiaInstance, self).create_instance(service_name,
+                                                      host_name, None,
+                                                      suffix, realm)
+
+    def __import_ra_key(self):
+        cli = CustodiaClient(self.fqdn, self.master_host_name, self.realm)
+        cli.fetch_key('ra/ipaCert')
+
+    def import_dm_password(self, host_name, master_host_name, realm):
+        cli = CustodiaClient(host_name, master_host_name, realm)
+        cli.fetch_key('dm/DMHash')
+
     def __start(self):
         super(CustodiaInstance, self).__start()
 
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 81a467ee9d8222751cf98b90106de781646a365f..c5527044681742645eba364ee21eb7f6b64b60f3 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -252,8 +252,7 @@ class DsInstance(service.Service):
         self.step("configure autobind for root", self.__root_autobind)
         self.step("configure new location for managed entries", self.__repoint_managed_entries)
         self.step("configure dirsrv ccache", self.configure_dirsrv_ccache)
-        self.step("enable SASL mapping fallback", self.__enable_sasl_mapping_fallback)
-        self.step("restarting directory server", self.__restart_instance)
+        self.step("enabling SASL mapping fallback", self.__enable_sasl_mapping_fallback)
 
     def __common_post_setup(self):
         self.step("initializing group membership", self.init_memberof)
@@ -298,6 +297,7 @@ class DsInstance(service.Service):
             subject_base, idstart, idmax, pkcs12_info, ca_file=ca_file)
 
         self.__common_setup()
+        self.step("restarting directory server", self.__restart_instance)
 
         self.step("adding default layout", self.__add_default_layout)
         self.step("adding delegation layout", self.__add_delegation_layout)
@@ -310,6 +310,7 @@ class DsInstance(service.Service):
         if hbac_allow:
             self.step("creating default HBAC rule allow_all", self.add_hbac)
         self.step("creating default CA ACL rule", self.add_caacl)
+        self.step("adding sasl mappings to the directory", self.__configure_sasl_mappings)
         self.step("adding entries for topology management", self.__add_topology_entries)
 
         self.__common_post_setup()
@@ -327,7 +328,8 @@ class DsInstance(service.Service):
 
     def create_replica(self, realm_name, master_fqdn, fqdn,
                        domain_name, dm_password, subject_base,
-                       pkcs12_info=None, ca_file=None, ca_is_configured=None):
+                       pkcs12_info=None, ca_file=None,
+                       ca_is_configured=None, promote=False):
         # idstart and idmax are configured so that the range is seen as
         # depleted by the DNA plugin and the replica will go and get a
         # new range from the master.
@@ -349,8 +351,13 @@ class DsInstance(service.Service):
         self.master_fqdn = master_fqdn
         if ca_is_configured is not None:
             self.ca_is_configured = ca_is_configured
+        self.promote = promote
 
-        self.__common_setup(True)
+        self.__common_setup(enable_ssl=(not self.promote))
+        if self.promote:
+            self.step("creating DS keytab", self.__get_ds_keytab)
+            self.step("retriving DS Certificate", self.__get_ds_cert)
+        self.step("restarting directory server", self.__restart_instance)
 
         self.step("setting up initial replication", self.__setup_replica)
         self.step("adding sasl mappings to the directory", self.__configure_sasl_mappings)
@@ -370,14 +377,26 @@ class DsInstance(service.Service):
             self.realm,
             self.dm_password)
 
+        # Always connect to self over ldapi
+        conn = ipaldap.IPAdmin(self.fqdn, ldapi=True, realm=self.realm)
+        conn.do_external_bind('root')
         repl = replication.ReplicationManager(self.realm,
                                               self.fqdn,
-                                              self.dm_password)
-        repl.setup_replication(self.master_fqdn,
-                               r_binddn=DN(('cn', 'Directory Manager')),
-                               r_bindpw=self.dm_password)
+                                              self.dm_password, conn=conn)
+        if self.promote:
+            repl.setup_promote_replication(self.master_fqdn)
+        else:
+            repl.setup_replication(self.master_fqdn,
+                                   r_binddn=DN(('cn', 'Directory Manager')),
+                                   r_bindpw=self.dm_password)
         self.run_init_memberof = repl.needs_memberof_fixup()
 
+        # Now that the server is up make sure all changes happen against
+        # the local server (as repica pomotion does not have the DM password.
+        if self.admin_conn:
+            self.ldap_disconnect()
+        self.ldapi = True
+
 
     def __configure_sasl_mappings(self):
         # we need to remove any existing SASL mappings in the directory as otherwise they
@@ -428,6 +447,7 @@ class DsInstance(service.Service):
         )
         self.admin_conn.add_entry(entry)
 
+
     def __update_schema(self):
         # FIXME: https://fedorahosted.org/389/ticket/47490
         self._ldap_mod("schema-update.ldif")
@@ -1095,3 +1115,26 @@ class DsInstance(service.Service):
         # Create global domain level entry and set the domain level
         if self.domainlevel is not None:
             self._ldap_mod("domainlevel.ldif", self.sub_dict)
+
+    def __get_ds_keytab(self):
+
+        self.fstore.backup_file(paths.DS_KEYTAB)
+
+        installutils.install_service_keytab(self.principal,
+                                            self.master_fqdn,
+                                            paths.DS_KEYTAB)
+
+        # Configure DS to use the keytab
+        vardict = {"KRB5_KTNAME": paths.DS_KEYTAB}
+        ipautil.config_replace_variables(paths.SYSCONFIG_DIRSRV,
+                                         replacevars=vardict)
+
+        # Keytab must be owned by DS itself
+        pent = pwd.getpwnam(DS_USER)
+        os.chown(paths.DS_KEYTAB, pent.pw_uid, pent.pw_gid)
+
+    def __get_ds_cert(self):
+        subject = DN(('O', self.realm))
+        nssdb_dir = config_dirname(self.serverid)
+        db = certs.CertDB(self.realm, nssdir=nssdb_dir, subject_base=subject)
+        db.request_service_cert(self.nickname, self.principal, self.fqdn)
diff --git a/ipaserver/install/httpinstance.py b/ipaserver/install/httpinstance.py
index 7dcdda03dc7ea7b1011c92bc77d301e1b0e6d8e2..413500ea91b0da7ee8b316d5e45a7a6037431669 100644
--- a/ipaserver/install/httpinstance.py
+++ b/ipaserver/install/httpinstance.py
@@ -95,7 +95,7 @@ class HTTPInstance(service.Service):
     def create_instance(self, realm, fqdn, domain_name, dm_password=None,
                         autoconfig=True, pkcs12_info=None,
                         subject_base=None, auto_redirect=True, ca_file=None,
-                        ca_is_configured=None):
+                        ca_is_configured=None, promote=False):
         self.fqdn = fqdn
         self.realm = realm
         self.domain = domain_name
@@ -115,6 +115,7 @@ class HTTPInstance(service.Service):
         self.ca_file = ca_file
         if ca_is_configured is not None:
             self.ca_is_configured = ca_is_configured
+        self.promote = promote
 
         # get a connection to the DS
         self.ldap_connect()
@@ -130,12 +131,13 @@ class HTTPInstance(service.Service):
         if self.ca_is_configured:
             self.step("configure certmonger for renewals",
                       self.configure_certmonger_renewal_guard)
+        self.step("setting up httpd keytab", self.__create_http_keytab)
         self.step("setting up ssl", self.__setup_ssl)
         self.step("importing CA certificates from LDAP", self.__import_ca_certs)
         if autoconfig:
             self.step("setting up browser autoconfig", self.__setup_autoconfig)
-        self.step("publish CA cert", self.__publish_ca_cert)
-        self.step("creating a keytab for httpd", self.__create_http_keytab)
+        if not self.promote:
+            self.step("publish CA cert", self.__publish_ca_cert)
         self.step("clean up any existing httpd ccache", self.remove_httpd_ccache)
         self.step("configuring SELinux for httpd", self.configure_selinux_for_httpd)
         if not self.is_kdcproxy_configured():
@@ -165,10 +167,10 @@ class HTTPInstance(service.Service):
             self.print_msg(e.format_service_warning('web interface'))
 
     def __create_http_keytab(self):
-        installutils.kadmin_addprinc(self.principal)
-        installutils.create_keytab(paths.IPA_KEYTAB, self.principal)
-        self.move_service(self.principal)
-        self.add_cert_to_service()
+        if not self.promote:
+            installutils.kadmin_addprinc(self.principal)
+            installutils.create_keytab(paths.IPA_KEYTAB, self.principal)
+            self.move_service(self.principal)
 
         pent = pwd.getpwnam("apache")
         os.chown(paths.IPA_KEYTAB, pent.pw_uid, pent.pw_gid)
@@ -291,14 +293,16 @@ class HTTPInstance(service.Service):
                 db.track_server_cert(nickname, self.principal, db.passwd_fname, 'restart_httpd')
 
             self.__set_mod_nss_nickname(nickname)
-        else:
+            self.add_cert_to_service()
 
+        elif not self.promote:
             db.create_password_conf()
             self.dercert = db.create_server_cert(self.cert_nickname, self.fqdn,
                                                  ca_db)
             db.track_server_cert(self.cert_nickname, self.principal,
                                  db.passwd_fname, 'restart_httpd')
             db.create_signing_cert("Signing-Cert", "Object Signing Cert", ca_db)
+            self.add_cert_to_service()
 
         # Fix the database permissions
         os.chmod(certs.NSS_DIR + "/cert8.db", 0o660)
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 06b703b94cf4d2ee238944c8d7b1f578a8cde86b..61eb3221d8debf715cecce5d4a572df1f7542f66 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -45,12 +45,14 @@ from ipapython.admintool import ScriptError
 from ipapython.ipa_log_manager import root_logger, log_mgr
 from ipalib.util import validate_hostname
 from ipapython import config
-from ipalib import errors, x509
+from ipalib import api, errors, x509
 from ipapython.dn import DN
 from ipaserver.install import certs, service, sysupgrade
 from ipaplatform import services
 from ipaplatform.paths import paths
 from ipaplatform.tasks import tasks
+from ipapython import certmonger
+
 
 # Used to determine install status
 IPA_MODULES = [
@@ -1098,3 +1100,27 @@ def enable_and_start_oddjobd(sstore):
         oddjobd.start()
     except Exception as e:
         root_logger.critical("Unable to start oddjobd: {0}".format(str(e)))
+
+def install_service_keytab(principal, server, path):
+
+    try:
+        api.Backend.rpcclient.connect()
+
+        # Create services if none exists (we use the .forward method
+        # here so that we can control the client version number and avoid
+        # errors. This is a workaround until the API becomes version
+        # independent: FIXME
+
+        api.Backend.rpcclient.forward(
+            'service_add',
+            krbprincipalname=principal,
+            version=u'2.112'    # All the way back to 3.0 servers
+        )
+    except errors.DuplicateEntry:
+        pass
+    finally:
+        if api.Backend.rpcclient.isconnected():
+            api.Backend.rpcclient.disconnect()
+
+    args = [paths.IPA_GETKEYTAB, '-k', path, '-p', principal, '-s', server]
+    ipautil.run(args)
diff --git a/ipaserver/install/krbinstance.py b/ipaserver/install/krbinstance.py
index c255c81ca944c21a7c9770bb1b5beac20f68e2ac..2e3f0bb868599680016d46e669e99548de1f7250 100644
--- a/ipaserver/install/krbinstance.py
+++ b/ipaserver/install/krbinstance.py
@@ -172,7 +172,7 @@ class KrbInstance(service.Service):
                        master_fqdn, host_name,
                        domain_name, admin_password,
                        setup_pkinit=False, pkcs12_info=None,
-                       subject_base=None):
+                       subject_base=None, promote=False):
         self.pkcs12_info = pkcs12_info
         self.subject_base = subject_base
         self.master_fqdn = master_fqdn
@@ -180,12 +180,14 @@ class KrbInstance(service.Service):
         self.__common_setup(realm_name, host_name, domain_name, admin_password)
 
         self.step("configuring KDC", self.__configure_instance)
-        self.step("creating a keytab for the directory", self.__create_ds_keytab)
-        self.step("creating a keytab for the machine", self.__create_host_keytab)
+        if not promote:
+            self.step("creating a keytab for the directory", self.__create_ds_keytab)
+            self.step("creating a keytab for the machine", self.__create_host_keytab)
         self.step("adding the password extension to the directory", self.__add_pwd_extop_module)
         if setup_pkinit:
             self.step("installing X509 Certificate for PKINIT", self.__setup_pkinit)
-        self.step("enable GSSAPI for replication", self.__convert_to_gssapi_replication)
+        if not promote:
+            self.step("enable GSSAPI for replication", self.__convert_to_gssapi_replication)
 
         self.__common_post_setup()
 
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
index 2b36a5eb9287bf1789009a3198e540e333869e98..a7787638a50df7455f5b64cd5b5cb9fe28378696 100644
--- a/ipaserver/install/replication.py
+++ b/ipaserver/install/replication.py
@@ -1602,6 +1602,96 @@ class ReplicationManager(object):
         except errors.EmptyModlist:
             pass
 
+    def join_replication_managers(self, conn):
+        """
+        Create a pseudo user to use for replication.
+        """
+        dn = DN(('cn', 'replication managers'), ('cn', 'sysaccounts'),
+                ('cn', 'etc'), self.suffix)
+        mydn = DN(('krbprincipalname', 'ldap/%s@%s' % (self.hostname,
+                                                       self.realm)),
+                  ('cn', 'services'), ('cn', 'accounts'), self.suffix)
+
+        entry = conn.get_entry(dn)
+        if mydn not in entry['member']:
+            entry['member'].append(mydn)
+
+        try:
+            conn.update_entry(entry)
+        except errors.EmptyModlist:
+            pass
+
+    def add_temp_sasl_mapping(self, conn, r_hostname):
+        """
+        Create a special user to let SASL Mapping find a valid user
+        on first replication.
+        """
+        name = 'ldap/%s@%s' % (r_hostname, self.realm)
+        replica_binddn = DN(('cn', name), ('cn', 'config'))
+        entry = conn.make_entry(
+            replica_binddn,
+            objectclass=["top", "person"],
+            cn=[name],
+            sn=["replication manager pseudo user"]
+        )
+        conn.add_entry(entry)
+
+        entry = conn.get_entry(self.replica_dn())
+        entry['nsDS5ReplicaBindDN'].append(replica_binddn)
+        conn.update_entry(entry)
+
+        entry = conn.make_entry(
+            DN(('cn', 'Peer Master'), ('cn', 'mapping'), ('cn', 'sasl'),
+                ('cn', 'config')),
+            objectclass=["top", "nsSaslMapping"],
+            cn=["Peer Master"],
+            nsSaslMapRegexString=['^[^:@]+$'],
+            nsSaslMapBaseDNTemplate=[DN(('cn', 'config'))],
+            nsSaslMapFilterTemplate=['(cn=&@%s)' % self.realm],
+            nsSaslMapPriority=['1'],
+        )
+        conn.add_entry(entry)
+
+    def remove_temp_replication_user(self, conn, r_hostname):
+        """
+        Remove the special SASL Mapping user created in a previous step.
+        """
+        name = 'ldap/%s@%s' % (r_hostname, self.realm)
+        replica_binddn = DN(('cn', name), ('cn', 'config'))
+        conn.delete_entry(replica_binddn)
+
+        entry = conn.get_entry(self.replica_dn())
+        while replica_binddn in entry['nsDS5ReplicaBindDN']:
+            entry['nsDS5ReplicaBindDN'].remove(replica_binddn)
+        conn.update_entry(entry)
+
+    def setup_promote_replication(self, r_hostname):
+        # note - there appears to be a bug in python-ldap - it does not
+        # allow connections using two different CA certs
+        r_conn = ipaldap.IPAdmin(r_hostname, port=389, protocol='ldap')
+        r_conn.do_sasl_gssapi_bind()
+
+        # Setup the first half
+        l_id = self._get_replica_id(self.conn, r_conn)
+        self.basic_replication_setup(self.conn, l_id, self.repl_man_dn, None)
+        self.add_temp_sasl_mapping(self.conn, r_hostname)
+
+        # Now setup the other half
+        r_id = self._get_replica_id(r_conn, r_conn)
+        self.basic_replication_setup(r_conn, r_id, self.repl_man_dn, None)
+        self.join_replication_managers(r_conn)
+
+        self.setup_agreement(r_conn, self.conn.host, isgssapi=True)
+        self.setup_agreement(self.conn, r_hostname, isgssapi=True)
+
+        #Finally start replication
+        ret = self.start_replication(r_conn, master=False)
+        if ret != 0:
+            raise RuntimeError("Failed to start replication")
+
+        self.remove_temp_replication_user(self.conn, r_hostname)
+
+
 class CSReplicationManager(ReplicationManager):
     """ReplicationManager specific to CA agreements
 
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index 0a890e64b77d2ffcfbaddc8d9cc9990272a21a50..dcdc781455f0723e4ec19d4a28474ba8a09e6b56 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -6,13 +6,15 @@ import dns.exception as dnsexception
 import dns.name as dnsname
 import dns.resolver as dnsresolver
 import dns.reversename as dnsreversename
+import getpass
+import gssapi
 import os
 import shutil
 import socket
 import sys
 import tempfile
 
-from ipapython import dogtag, ipautil, sysrestore
+from ipapython import certmonger, dogtag, ipaldap, ipautil, sysrestore
 from ipapython.dn import DN
 from ipapython.install import common, core
 from ipapython.install.common import step
@@ -22,16 +24,21 @@ from ipaplatform import services
 from ipaplatform.tasks import tasks
 from ipaplatform.paths import paths
 from ipalib import api, certstore, constants, create_api, errors, x509
+import ipaclient.ipachangeconf
 import ipaclient.ntpconf
 from ipaserver.install import (
     bindinstance, ca, cainstance, certs, dns, dsinstance, httpinstance,
     installutils, kra, krbinstance, memcacheinstance, ntpinstance,
     otpdinstance, custodiainstance, service)
 from ipaserver.install.installutils import create_replica_config
+from ipaserver.install.installutils import ReplicaConfig
 from ipaserver.install.replication import (
     ReplicationManager, replica_conn_check)
+import SSSDConfig
+from subprocess import CalledProcessError
 
 DIRMAN_DN = DN(('cn', 'directory manager'))
+INSTALL_ERROR = 3
 
 
 def get_dirman_password():
@@ -56,6 +63,24 @@ def make_pkcs12_info(directory, cert_name, password_name):
         return None
 
 
+def install_http_certs(config, fstore):
+
+    # Obtain keytab for the HTTP service
+    fstore.backup_file(paths.IPA_KEYTAB)
+
+    principal = 'HTTP/%s@%s' % (config.host_name, config.realm_name)
+    installutils.install_service_keytab(principal,
+                                        config.master_host_name,
+                                        paths.IPA_KEYTAB)
+
+    # Obtain certificate for the HTTP service
+    nssdir = certs.NSS_DIR
+    subject = DN(('O', config.realm_name))
+    db = certs.CertDB(config.realm_name, nssdir=nssdir, subject_base=subject)
+    db.request_service_cert('Server-Cert', principal, config.host_name, True)
+    #FIXME: need Signing-Cert too ?
+
+
 def install_replica_ds(config):
     dsinstance.check_ports()
 
@@ -75,12 +100,13 @@ def install_replica_ds(config):
         pkcs12_info=pkcs12_info,
         ca_is_configured=ipautil.file_exists(config.dir + "/cacert.p12"),
         ca_file=config.dir + "/ca.crt",
+        promote=config.promote,
     )
 
     return ds
 
 
-def install_krb(config, setup_pkinit=False):
+def install_krb(config, setup_pkinit=False, promote=False):
     krb = krbinstance.KrbInstance()
 
     # pkinit files
@@ -90,7 +116,7 @@ def install_krb(config, setup_pkinit=False):
     krb.create_replica(config.realm_name,
                        config.master_host_name, config.host_name,
                        config.domain_name, config.dirman_password,
-                       setup_pkinit, pkcs12_info)
+                       setup_pkinit, pkcs12_info, promote=promote)
 
     return krb
 
@@ -111,7 +137,7 @@ def install_ca_cert(ldap, base_dn, realm, cafile):
         sys.exit(1)
 
 
-def install_http(config, auto_redirect):
+def install_http(config, auto_redirect, promote=False):
     # if we have a pkcs12 file, create the cert db from
     # that. Otherwise the ds setup will create the CA
     # cert
@@ -127,7 +153,8 @@ def install_http(config, auto_redirect):
         config.realm_name, config.host_name, config.domain_name,
         config.dirman_password, False, pkcs12_info,
         auto_redirect=auto_redirect, ca_file=config.dir + "/ca.crt",
-        ca_is_configured=ipautil.file_exists(config.dir + "/cacert.p12"))
+        ca_is_configured=ipautil.file_exists(config.dir + "/cacert.p12"),
+        promote=promote)
 
     # Now copy the autoconfiguration files
     try:
@@ -149,9 +176,10 @@ def install_http(config, auto_redirect):
 def install_dns_records(config, options, remote_api):
 
     if not bindinstance.dns_container_exists(
-            config.master_host_name,
+            config.host_name,
             ipautil.realm_to_suffix(config.realm_name),
-            dm_password=config.dirman_password):
+            realm=config.realm_name, ldapi=True,
+            autobind=ipaldap.AUTOBIND_ENABLED):
         return
 
     try:
@@ -279,6 +307,43 @@ def check_dns_resolution(host_name, dns_servers):
     return no_errors
 
 
+def check_ca_enabled(api):
+    try:
+        api.Backend.rpcclient.connect()
+        result = api.Backend.rpcclient.forward(
+            'ca_is_enabled',
+            version=u'2.112'    # All the way back to 3.0 servers
+        )
+        return result['result']
+    finally:
+        if api.Backend.rpcclient.isconnected():
+            api.Backend.rpcclient.disconnect()
+
+
+def configure_certmonger():
+    messagebus = services.knownservices.messagebus
+    try:
+        messagebus.start()
+    except Exception, e:
+        print("Messagebus service unavailable: %s" % str(e))
+        sys.exit(3)
+
+    # Ensure that certmonger has been started at least once to generate the
+    # cas files in /var/lib/certmonger/cas.
+    cmonger = services.knownservices.certmonger
+    try:
+        cmonger.restart()
+    except Exception, e:
+        print("Certmonger service unavailable: %s" % str(e))
+        sys.exit(3)
+
+    try:
+        cmonger.enable()
+    except Exception, e:
+        print("Failed to enable Certmonger: %s" % str(e))
+        sys.exit(3)
+
+
 def remove_replica_info_dir(installer):
     # always try to remove decrypted replica file
     try:
@@ -307,6 +372,36 @@ def common_cleanup(func):
     return decorated
 
 
+def promote_sssd(host_name):
+    sssdconfig = SSSDConfig.SSSDConfig()
+    sssdconfig.import_config()
+    domains = sssdconfig.list_active_domains()
+
+    ipa_domain = None
+
+    for name in domains:
+        domain = sssdconfig.get_domain(name)
+        try:
+            hostname = domain.get_option('ipa_hostname')
+            if hostname == host_name:
+                ipa_domain = domain
+        except SSSDConfig.NoOptionError:
+            continue
+
+    if ipa_domain is None:
+        raise RuntimeError("Couldn't find IPA domain in sssd.conf")
+    else:
+        domain.set_option('ipa_server', host_name)
+        domain.set_option('ipa_server_mode', True)
+        sssdconfig.save_domain(domain)
+        sssdconfig.write()
+
+        sssd = services.service('sssd')
+        try:
+            sssd.restart()
+        except CalledProcessError:
+            root_logger.warning("SSSD service restart was unsuccessful.")
+
 @common_cleanup
 def install_check(installer):
     options = installer
@@ -582,8 +677,6 @@ def install(installer):
     CA.import_ra_cert(config.dir + "/ra.p12")
     CA.fix_ra_perms()
 
-    #FIXME: must be done earlier in replica to fetch keys for CA/ldap server
-    # before they are configured
     custodia = custodiainstance.CustodiaInstance()
     custodia.create_instance('KEYS', config.host_name,
                              config.dirman_password,
@@ -647,6 +740,407 @@ def install(installer):
     remove_replica_info_dir(installer)
 
 
+@common_cleanup
+def promote_check(installer):
+    options = installer
+
+    # FIXME: to implement yet
+    if options.setup_ca:
+        raise NotImplementedError
+    if options.setup_kra:
+        raise NotImplementedError
+    if options.setup_dns:
+        raise NotImplementedError
+
+    tasks.check_selinux_status()
+
+    client_fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
+    if not client_fstore.has_files():
+        sys.exit("IPA client is not configured on this system.\n"
+                 "Please install it before configuring the replica, "
+                 "using 'ipa-client-install'.")
+
+    sstore = sysrestore.StateFile(paths.SYSRESTORE)
+
+    fstore = sysrestore.FileStore(paths.SYSRESTORE)
+
+    # Check to see if httpd is already configured to listen on 443
+    if httpinstance.httpd_443_configured():
+        sys.exit("Aborting installation")
+
+    check_dirsrv()
+
+    if not options.no_ntp:
+        try:
+            ipaclient.ntpconf.check_timedate_services()
+        except ipaclient.ntpconf.NTPConflictingService, e:
+            print("WARNING: conflicting time&date synchronization service '%s'"
+                  " will" % e.conflicting_service)
+            print "be disabled in favor of ntpd"
+            print ""
+        except ipaclient.ntpconf.NTPConfigurationError:
+            pass
+
+    api.bootstrap(context='installer')
+    api.finalize()
+
+    config = ReplicaConfig()
+    config.realm_name = api.env.realm
+    config.host_name = api.env.host
+    config.domain_name = api.env.domain
+    config.master_host_name = api.env.server
+    config.setup_ca = options.setup_ca
+    config.setup_kra = options.setup_kra
+
+    installutils.verify_fqdn(config.host_name, options.no_host_dns)
+    installutils.verify_fqdn(config.master_host_name, options.no_host_dns)
+
+    # Check if ccache is available
+    try:
+        root_logger.debug('KRB5CCNAME set to %s' %
+                          os.environ.get('KRB5CCNAME', None))
+        # get default creds, will raise if none found
+        default_cred = gssapi.creds.Credentials()
+        principal = str(default_cred.name)
+    except gssapi.raw.misc.GSSError as e:
+        root_logger.debug('Failed to find default ccache: %s' % e)
+        principal = None
+
+    # Check if the principal matches the requested one (if any)
+    if principal is not None and options.principal is not None:
+        op = options.principal
+        if op.find('@') == -1:
+            op = '%s@%s' % (op, config.realm_name)
+        if principal != op:
+            root_logger.debug('Specified principal %s does not match '
+                              'available credentials (%s)' %
+                              (options.principal, principal))
+            principal = None
+
+    if principal is None:
+        (ccache_fd, ccache_name) = tempfile.mkstemp()
+        os.close(ccache_fd)
+
+        if options.principal is not None:
+            principal = options.principal
+        else:
+            principal = 'admin'
+        stdin = None
+        if principal.find('@') == -1:
+            principal = '%s@%s' % (principal, config.realm_name)
+        if options.password is not None:
+            stdin = options.password
+        else:
+            if not options.unattended:
+                try:
+                    stdin = getpass.getpass("Password for %s: " % principal)
+                except EOFError:
+                    stdin = None
+                if not stdin:
+                    root_logger.error(
+                        "Password must be provided for %s.", principal)
+                    return INSTALL_ERROR
+            else:
+                if sys.stdin.isatty():
+                    root_logger.error("Password must be provided in " +
+                        "non-interactive mode.")
+                    root_logger.info("This can be done via " +
+                        "echo password | ipa-client-install ... " +
+                        "or with the -w option.")
+                    return INSTALL_ERROR
+                else:
+                    stdin = sys.stdin.readline()
+
+        try:
+            ipautil.kinit_password(principal, stdin, ccache_name)
+        except RuntimeError as e:
+            root_logger.error("Kerberos authentication failed: %s" % e)
+            return INSTALL_ERROR
+
+        os.environ['KRB5CCNAME'] = ccache_name
+
+    cafile = paths.IPA_CA_CRT
+    if not ipautil.file_exists(cafile):
+        raise RuntimeError("CA cert file is not available! Please reinstall"
+                           "the client and try again.")
+
+    ldapuri = 'ldaps://%s' % ipautil.format_netloc(config.master_host_name)
+    remote_api = create_api(mode=None)
+    remote_api.bootstrap(in_server=True, context='installer',
+                         ldap_uri=ldapuri)
+    remote_api.finalize()
+    conn = remote_api.Backend.ldap2
+    replman = None
+    try:
+        # Try out authentication
+        conn.connect(use_gssapi=True)
+        replman = ReplicationManager(config.realm_name,
+                                     config.master_host_name, None)
+
+        # Check that we don't already have a replication agreement
+        try:
+            (acn, adn) = replman.agreement_dn(config.host_name)
+            entry = conn.get_entry(adn, ['*'])
+        except errors.NotFound:
+            pass
+        else:
+            root_logger.info('Error: A replication agreement for this '
+                             'host already exists.')
+            print('A replication agreement for this host already exists. '
+                  'It needs to be removed.')
+            print "Run this command:"
+            print("    %% ipa-replica-manage del %s --force" %
+                  config.host_name)
+            sys.exit(3)
+
+        # Detect the current domain level
+        try:
+            current = remote_api.Command['domainlevel_get']()['result']
+        except errors.NotFound:
+            # If we're joining an older master, domain entry is not
+            # available
+            current = 0
+
+        # Detect if current level is out of supported range
+        # for this IPA version
+        under_lower_bound = current < constants.MIN_DOMAIN_LEVEL
+        above_upper_bound = current > constants.MAX_DOMAIN_LEVEL
+
+        if under_lower_bound or above_upper_bound:
+            message = ("This version of FreeIPA does not support "
+                       "the Domain Level which is currently set for "
+                       "this domain. The Domain Level needs to be "
+                       "raised before installing a replica with "
+                       "this version is allowed to be installed "
+                       "within this domain.")
+            root_logger.error(message)
+            sys.exit(3)
+
+        # Detect if the other master can handle replication managers
+        # cn=replication managers,cn=sysaccounts,cn=etc,$SUFFIX
+        dn = DN(('cn', 'replication managers'), ('cn', 'sysaccounts'),
+                ('cn', 'etc'), ipautil.realm_to_suffix(config.realm_name))
+        try:
+            entry = conn.get_entry(dn)
+        except errors.NotFound:
+            msg = ("The Replication Managers group is not available in "
+                   "the domain. Replica promotion requires the use of "
+                   "Replication Managers to be able to replicate data. "
+                   "Upgrade the peer master or use the ipa-replica-prepare "
+                   "command on the master and use a prep file to install "
+                   "this replica.")
+            root_logger.error(msg)
+            sys.exit(3)
+
+        dns_masters = remote_api.Object['dnsrecord'].get_dns_masters()
+        if dns_masters:
+            if not options.no_host_dns:
+                root_logger.debug('Check forward/reverse DNS resolution')
+                resolution_ok = (
+                    check_dns_resolution(config.master_host_name,
+                                         dns_masters) and
+                    check_dns_resolution(config.host_name, dns_masters))
+                if not resolution_ok and installer.interactive:
+                    if not ipautil.user_input("Continue?", False):
+                        sys.exit(0)
+        else:
+            root_logger.debug('No IPA DNS servers, '
+                              'skipping forward/reverse resolution check')
+
+        entry_attrs = conn.get_ipa_config()
+        subject_base = entry_attrs.get('ipacertificatesubjectbase', [None])[0]
+        if subject_base is not None:
+            config.subject_base = DN(subject_base)
+
+        # Check if Ca is enabled on peer master
+        ca_enabled = check_ca_enabled(api)
+        if not ca_enabled:
+            # FIXME: add way to pass in certificates
+            root_logger.error("The remote master does not have a CA "
+                              "installed, can't proceed without certs")
+            sys.exit(3)
+
+        if options.setup_ca:
+            if not ca_enabled:
+                root_logger.error("The remote master does not have a CA "
+                                  "installed, can't set up CA")
+                sys.exit(3)
+
+            options.realm_name = config.realm_name
+            options.host_name = config.host_name
+            options.subject = config.subject_base
+            ca.install_check(False, None, options)
+
+        if config.setup_kra:
+            try:
+                kra.install_check(remote_api, config, options)
+            except RuntimeError as e:
+                print str(e)
+                sys.exit(1)
+    except errors.ACIError:
+        sys.exit("\nInsufficiently privileges to promote the server.")
+    except errors.LDAPError:
+        sys.exit("\nUnable to connect to LDAP server %s" %
+                 config.master_host_name)
+    finally:
+        if replman and replman.conn:
+            replman.conn.unbind()
+        if conn.isconnected():
+            conn.disconnect()
+
+    if options.setup_dns:
+        dns.install_check(False, True, options, config.host_name)
+    else:
+        config.ips = installutils.get_server_ip_address(config.host_name,
+            fstore, not installer.interactive, False, options.ip_addresses)
+
+    # check connection
+    if not options.skip_conncheck:
+        replica_conn_check(
+            config.master_host_name, config.host_name, config.realm_name,
+            options.setup_ca, dogtag.Dogtag10Constants.DS_PORT)
+
+    if not ipautil.file_exists(cafile):
+        raise RuntimeError("CA cert file is not available.")
+
+    installer._ca_enabled = ca_enabled
+    installer._remote_api = remote_api
+    installer._fstore = fstore
+    installer._sstore = sstore
+    installer._config = config
+
+
+@common_cleanup
+def promote(installer):
+    options = installer
+    fstore = installer._fstore
+    sstore = installer._sstore
+    config = installer._config
+
+    # Save client file and merge in server directives
+    target_fname = paths.IPA_DEFAULT_CONF
+    fstore.backup_file(target_fname)
+    ipaconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Replica Promote")
+    ipaconf.setOptionAssignment(" = ")
+    ipaconf.setSectionNameDelimiters(("[","]"))
+
+    config.promote = installer.promote
+    config.dirman_password = ipautil.ipa_generate_password()
+
+    dogtag_constants = dogtag.install_constants
+
+    #FIXME: allow to use passed in certs instead
+    if installer._ca_enabled:
+        configure_certmonger()
+
+    # Create DS user/group if it doesn't exist yet
+    dsinstance.create_ds_user()
+
+    # Configure ntpd
+    if not options.no_ntp:
+        ipaclient.ntpconf.force_ntpd(sstore)
+        ntp = ntpinstance.NTPInstance()
+        ntp.create_instance()
+
+    # Configure dirsrv
+    ds = install_replica_ds(config)
+
+    # Always try to install DNS records
+    install_dns_records(config, options, api)
+
+    # Must install http certs before changing ipa configuration file
+    # or certmonger will fail to contact the peer master
+    install_http_certs(config, fstore)
+
+    # Create the management framework config file
+    gopts = [
+        ipaconf.setOption('host', config.host_name),
+        ipaconf.rmOption('server'),
+        ipaconf.setOption('xmlrpc_uri',
+                          'https://%s/ipa/xml' %
+                          ipautil.format_netloc(config.host_name)),
+        ipaconf.setOption('ldap_uri',
+                          installutils.realm_to_ldapi_uri(config.realm_name)),
+        ipaconf.setOption('mode', 'production')]
+    if installer.setup_ca:
+        gopts.append(ipaconf.setOption('enable_ra', 'True'))
+        gopts.append(ipaconf.setOption('ra_plugin', 'dogtag'))
+        gopts.append(
+            ipaconf.setOption('dogtag_version',
+                              dogtag.install_constants.DOGTAG_VERSION))
+    opts = [ipaconf.setSection('global', gopts)]
+
+    ipaconf.changeConf(target_fname, opts)
+    os.chmod(target_fname, 0o644)   # must be readable for httpd
+
+    custodia = custodiainstance.CustodiaInstance()
+    custodia.create_replica('KEYS', config.host_name, config.master_host_name,
+                            config.realm_name)
+
+    if config.setup_ca:
+        options.realm_name = config.realm_name
+        options.domain_name = config.domain_name
+        options.host_name = config.host_name
+        options.dm_password = config.dirman_password
+
+        ca.install(False, config, options)
+
+    krb = install_krb(config,
+                      setup_pkinit=not options.no_pkinit,
+                      promote=True)
+
+    http = install_http(config,
+                        auto_redirect=not options.no_ui_redirect,
+                        promote=True)
+
+    otpd = otpdinstance.OtpdInstance()
+    otpd.create_instance('OTPD', config.host_name, config.dirman_password,
+                         ipautil.realm_to_suffix(config.realm_name))
+
+    CA = cainstance.CAInstance(
+        config.realm_name, certs.NSS_DIR,
+        dogtag_constants=dogtag_constants)
+    CA.dm_password = config.dirman_password
+    CA.configure_certmonger_renewal()
+    CA.fix_ra_perms()
+
+
+    # Apply any LDAP updates. Needs to be done after the replica is synced-up
+    service.print_msg("Applying LDAP updates")
+    ds.apply_updates()
+
+    if options.setup_kra:
+        kra.install(api, config, options)
+    else:
+        service.print_msg("Restarting the directory server")
+        ds.restart()
+
+    service.print_msg("Restarting the KDC")
+    krb.restart()
+
+    if config.setup_ca:
+        dogtag_service = services.knownservices[dogtag_constants.SERVICE_NAME]
+        dogtag_service.restart(dogtag_constants.PKI_INSTANCE_NAME)
+
+    if options.setup_dns:
+        api.Backend.ldap2.connect(autobind=True)
+        dns.install(False, True, options)
+
+    # Restart httpd to pick up the new IPA configuration
+    service.print_msg("Restarting the web server")
+    http.restart()
+
+    ds.replica_populate()
+
+    custodia.import_dm_password(config.host_name, config.master_host_name,
+                                config.realm_name)
+
+    promote_sssd(config.host_name)
+
+    # Everything installed properly, activate ipa service.
+    services.knownservices.ipa.enable()
+
+
 class ReplicaCA(common.Installable, core.Group, core.Composite):
     description = "certificate system"
 
@@ -808,6 +1302,18 @@ class Replica(common.Installable, common.Interactive, core.Composite):
         description="skip connection check to remote master",
     )
 
+    promote = Knob(
+        bool, False,
+        description="promote an ipa client to become a master",
+    )
+
+    principal = Knob(
+        str, None,
+        sensitive=True,
+        description="User Principal allowed to promote replicas",
+        cli_short_name='P',
+    )
+
     def __init__(self, **kwargs):
         super(Replica, self).__init__(**kwargs)
 
@@ -816,12 +1322,13 @@ class Replica(common.Installable, common.Interactive, core.Composite):
 
         #pylint: disable=no-member
 
-        if self.replica_file is None:
-            raise RuntimeError(
-                "you must provide a file generated by ipa-replica-prepare")
-        if not ipautil.file_exists(self.replica_file):
-            raise RuntimeError(
-                "Replica file %s does not exist" % self.replica_file)
+        if not self.promote:
+            if self.replica_file is None:
+                raise RuntimeError(
+                    "you must provide a file generated by ipa-replica-prepare")
+            if not ipautil.file_exists(self.replica_file):
+                raise RuntimeError(
+                    "Replica file %s does not exist" % self.replica_file)
 
         if not self.dns.setup_dns:
             if self.dns.forwarders:
@@ -883,9 +1390,15 @@ class Replica(common.Installable, common.Interactive, core.Composite):
 
     @step()
     def main(self):
-        install_check(self)
-        yield
-        install(self)
+        if self.promote:
+            promote_check(self)
+            yield
+            promote(self)
+        else:
+            with ipautil.private_ccache():
+                install_check(self)
+                yield
+                install(self)
 
     ca = core.Component(ReplicaCA)
     dns = core.Component(ReplicaDNS)
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index 68feee4f09eb12e50867dfbe3c482a359838aa82..df6e3302389f10a8ef26047e7f83af11852fd8b1 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -30,6 +30,7 @@ Backend plugin for LDAP.
 import os
 import pwd
 
+import gssapi
 import krbV
 import ldap as _ldap
 
@@ -88,7 +89,8 @@ class ldap2(CrudBackend, LDAPClient):
 
     def create_connection(self, ccache=None, bind_dn=None, bind_pw='',
             tls_cacertfile=None, tls_certfile=None, tls_keyfile=None,
-            debug_level=0, autobind=False, serverctrls=None, clientctrls=None):
+            debug_level=0, autobind=False, use_gssapi=False,
+            serverctrls=None, clientctrls=None):
         """
         Connect to LDAP server.
 
@@ -150,6 +152,11 @@ class ldap2(CrudBackend, LDAPClient):
             self.gssapi_bind(server_controls=serverctrls,
                              client_controls=clientctrls)
             setattr(context, 'principal', principal)
+        elif use_gssapi:
+            cred = gssapi.creds.Credentials()
+            self.gssapi_bind(server_controls=serverctrls,
+                             client_controls=clientctrls)
+            setattr(context, 'principal', str(cred.name))
         else:
             # no kerberos ccache, use simple bind or external sasl
             if autobind:
-- 
2.4.2

>From eba80e12ede01e6ede9a0dec5bec49b31ade3166 Mon Sep 17 00:00:00 2001
From: Simo Sorce <s...@redhat.com>
Date: Wed, 15 Jul 2015 13:14:35 -0400
Subject: [PATCH 5/6] Change DNS installer code to use passed in api

Fixes a number of places where api was not passed around internally.
Also allows to install dns in replica promotion which requires an
alternative api to be created with the right configuration.

Signed-off-by: Simo Sorce <s...@redhat.com>
---
 ipaserver/install/bindinstance.py          | 92 +++++++++++++++---------------
 ipaserver/install/dns.py                   | 21 ++++---
 ipaserver/install/server/replicainstall.py | 23 ++++----
 3 files changed, 71 insertions(+), 65 deletions(-)

diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 6d347037312d054a97ba03965d9a6f0fe5e5bdf4..072787bb33b18e587bf80038f1cb7346d8a3d7cb 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -344,27 +344,27 @@ def add_ptr_rr(zone, ip_address, fqdn, dns_backup=None, api=api):
     name = get_reverse_record_name(zone, ip_address)
     add_rr(zone, name, "PTR", normalize_zone(fqdn), dns_backup, api)
 
-def add_ns_rr(zone, hostname, dns_backup=None, force=True):
+def add_ns_rr(zone, hostname, dns_backup=None, force=True, api=api):
     hostname = normalize_zone(hostname)
     add_rr(zone, "@", "NS", hostname, dns_backup=dns_backup,
-            force=force)
+            force=force, api=api)
 
-def del_rr(zone, name, type, rdata):
+def del_rr(zone, name, type, rdata, api=api):
     delkw = { '%srecord' % str(type.lower()) : unicode(rdata) }
     try:
         api.Command.dnsrecord_del(unicode(zone), unicode(name), **delkw)
     except (errors.NotFound, errors.AttrValueNotFound, errors.EmptyModlist):
         pass
 
-def del_fwd_rr(zone, host, ip_address):
+def del_fwd_rr(zone, host, ip_address, api=api):
     addr = netaddr.IPAddress(ip_address)
     if addr.version == 4:
-        del_rr(zone, host, "A", ip_address)
+        del_rr(zone, host, "A", ip_address, api=api)
     elif addr.version == 6:
-        del_rr(zone, host, "AAAA", ip_address)
+        del_rr(zone, host, "AAAA", ip_address, api=api)
 
-def del_ns_rr(zone, name, rdata):
-    del_rr(zone, name, 'NS', rdata)
+def del_ns_rr(zone, name, rdata, api=api):
+    del_rr(zone, name, 'NS', rdata, api=api)
 
 def get_rr(zone, name, type, api=api):
     rectype = '%srecord' % unicode(type.lower())
@@ -623,7 +623,7 @@ class BindInstance(service.Service):
         if self.first_instance:
             self.step("adding DNS container", self.__setup_dns_container)
 
-        if not dns_zone_exists(self.domain):
+        if not dns_zone_exists(self.domain, self.api):
             self.step("setting up our zone", self.__setup_zone)
         if self.reverse_zones:
             self.step("setting up reverse zone", self.__setup_reverse_zone)
@@ -738,12 +738,12 @@ class BindInstance(service.Service):
         self.__fix_dns_privilege_members()
 
     def __fix_dns_privilege_members(self):
-        ldap = api.Backend.ldap2
+        ldap = self.api.Backend.ldap2
 
         cn = 'Update PBAC memberOf %s' % time.time()
         task_dn = DN(('cn', cn), ('cn', 'memberof task'), ('cn', 'tasks'),
                      ('cn', 'config'))
-        basedn = DN(api.env.container_privilege, api.env.basedn)
+        basedn = DN(self.api.env.container_privilege, self.api.env.basedn)
         entry = ldap.make_entry(
             task_dn,
             objectclass=['top', 'extensibleObject'],
@@ -768,24 +768,24 @@ class BindInstance(service.Service):
     def __setup_zone(self):
         # Always use force=True as named is not set up yet
         add_zone(self.domain, self.zonemgr, dns_backup=self.dns_backup,
-                ns_hostname=api.env.host, force=True)
+                ns_hostname=self.api.env.host, force=True, api=self.api)
 
-        add_rr(self.domain, "_kerberos", "TXT", self.realm)
+        add_rr(self.domain, "_kerberos", "TXT", self.realm, api=self.api)
 
     def __add_self_ns(self):
         # add NS record to all zones
-        ns_hostname = normalize_zone(api.env.host)
-        result = api.Command.dnszone_find()
+        ns_hostname = normalize_zone(self.api.env.host)
+        result = self.api.Command.dnszone_find()
         for zone in result['result']:
             zone = unicode(zone['idnsname'][0])  # we need unicode due to backup
             root_logger.debug("adding self NS to zone %s apex", zone)
-            add_ns_rr(zone, ns_hostname, self.dns_backup, force=True)
+            add_ns_rr(zone, ns_hostname, self.dns_backup, force=True, api=self.api)
 
     def __setup_reverse_zone(self):
         # Always use force=True as named is not set up yet
         for reverse_zone in self.reverse_zones:
-            add_zone(reverse_zone, self.zonemgr, ns_hostname=api.env.host,
-                dns_backup=self.dns_backup, force=True)
+            add_zone(reverse_zone, self.zonemgr, ns_hostname=self.api.env.host,
+                dns_backup=self.dns_backup, force=True, api=self.api)
 
     def __add_master_records(self, fqdn, addrs):
         host, zone = fqdn.split(".", 1)
@@ -810,7 +810,7 @@ class BindInstance(service.Service):
             )
 
         for (rname, rdata) in srv_records:
-            add_rr(self.domain, rname, "SRV", rdata, self.dns_backup, self.api)
+            add_rr(self.domain, rname, "SRV", rdata, self.dns_backup, api=self.api)
 
         if not dns_zone_exists(zone, self.api):
             # add DNS domain for host first
@@ -824,11 +824,11 @@ class BindInstance(service.Service):
 
         # Add forward and reverse records to self
         for addr in addrs:
-            add_fwd_rr(zone, host, addr, self.api)
+            add_fwd_rr(zone, host, addr, api=self.api)
 
             reverse_zone = find_reverse_zone(addr, self.api)
             if reverse_zone:
-                add_ptr_rr(reverse_zone, addr, fqdn, None, self.api)
+                add_ptr_rr(reverse_zone, addr, fqdn, None, api=self.api)
 
     def __add_self(self):
         self.__add_master_records(self.fqdn, self.ip_addresses)
@@ -870,7 +870,7 @@ class BindInstance(service.Service):
 
         try:
             for addr in addrs:
-                add_fwd_rr(self.domain, IPA_CA_RECORD, addr, self.api)
+                add_fwd_rr(self.domain, IPA_CA_RECORD, addr, api=self.api)
         except errors.ValidationError:
             # there is a CNAME record in ipa-ca, we can't add A/AAAA records
             pass
@@ -884,7 +884,7 @@ class BindInstance(service.Service):
             try:
                 entries = ldap.get_entries(
                     DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
-                       api.env.basedn),
+                       self.api.env.basedn),
                     ldap.SCOPE_SUBTREE, '(&(objectClass=ipaConfigObject)(cn=CA))',
                     ['dn'])
             except errors.NotFound:
@@ -898,7 +898,7 @@ class BindInstance(service.Service):
 
                 host, zone = fqdn.split('.', 1)
                 if dns_zone_exists(zone, self.api):
-                    addrs = get_fwd_rr(zone, host, self.api)
+                    addrs = get_fwd_rr(zone, host, api=self.api)
                 else:
                     addrs = installutils.resolve_host(fqdn)
 
@@ -1017,8 +1017,8 @@ class BindInstance(service.Service):
 
     def add_ipa_ca_dns_records(self, fqdn, domain_name, ca_configured=True):
         host, zone = fqdn.split(".", 1)
-        if dns_zone_exists(zone):
-            addrs = get_fwd_rr(zone, host)
+        if dns_zone_exists(zone, self.api):
+            addrs = get_fwd_rr(zone, host, api=self.api)
         else:
             addrs = installutils.resolve_host(fqdn)
 
@@ -1028,7 +1028,7 @@ class BindInstance(service.Service):
 
     def convert_ipa_ca_cnames(self, domain_name):
         # get ipa-ca CNAMEs
-        cnames = get_rr(domain_name, IPA_CA_RECORD, "CNAME")
+        cnames = get_rr(domain_name, IPA_CA_RECORD, "CNAME", api=self.api)
         if not cnames:
             return
 
@@ -1044,11 +1044,11 @@ class BindInstance(service.Service):
             cname_fqdn[cname] = fqdn
 
         # get FQDNs of all IPA masters
-        ldap = api.Backend.ldap2
+        ldap = self.api.Backend.ldap2
         try:
             entries = ldap.get_entries(
                 DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
-                   api.env.basedn),
+                   self.api.env.basedn),
                 ldap.SCOPE_ONELEVEL, None, ['cn'])
             masters = set(e['cn'][0] for e in entries)
         except errors.NotFound:
@@ -1065,7 +1065,7 @@ class BindInstance(service.Service):
 
         # delete all CNAMEs
         for cname in cnames:
-            del_rr(domain_name, IPA_CA_RECORD, "CNAME", cname)
+            del_rr(domain_name, IPA_CA_RECORD, "CNAME", cname, api=self.api)
 
         # add A/AAAA records
         for cname in cnames:
@@ -1091,32 +1091,32 @@ class BindInstance(service.Service):
         )
 
         for (record, type, rdata) in resource_records:
-            del_rr(self.domain, record, type, rdata)
+            del_rr(self.domain, record, type, rdata, api=self.api)
 
-        areclist = get_fwd_rr(zone, host)
+        areclist = get_fwd_rr(zone, host, api=self.api)
         for rdata in areclist:
-            del_fwd_rr(zone, host, rdata)
+            del_fwd_rr(zone, host, rdata, api=self.api)
 
             rzone = find_reverse_zone(rdata)
             if rzone is not None:
                 record = get_reverse_record_name(rzone, rdata)
-                del_rr(rzone, record, "PTR", normalize_zone(fqdn))
+                del_rr(rzone, record, "PTR", normalize_zone(fqdn), api=self.api)
 
     def remove_ipa_ca_dns_records(self, fqdn, domain_name):
         host, zone = fqdn.split(".", 1)
-        if dns_zone_exists(zone):
-            addrs = get_fwd_rr(zone, host)
+        if dns_zone_exists(zone, self.api):
+            addrs = get_fwd_rr(zone, host, api=self.api)
         else:
             addrs = installutils.resolve_host(fqdn)
 
         for addr in addrs:
-            del_fwd_rr(domain_name, IPA_CA_RECORD, addr)
+            del_fwd_rr(domain_name, IPA_CA_RECORD, addr, api=self.api)
 
     def remove_server_ns_records(self, fqdn):
         """
         Remove all NS records pointing to this server
         """
-        ldap = api.Backend.ldap2
+        ldap = self.api.Backend.ldap2
         ns_rdata = normalize_zone(fqdn)
 
         # find all NS records pointing to this server
@@ -1124,7 +1124,7 @@ class BindInstance(service.Service):
         search_kw['nsrecord'] = ns_rdata
         attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)
         attributes = ['idnsname', 'objectclass']
-        dn = DN(api.env.container_dns, api.env.basedn)
+        dn = DN(self.api.env.container_dns, self.api.env.basedn)
 
         entries, truncated = ldap.find_entries(attr_filter, attributes, base_dn=dn)
 
@@ -1137,21 +1137,21 @@ class BindInstance(service.Service):
                 # zone record
                 zone = entry.single_value['idnsname']
                 root_logger.debug("zone record %s", zone)
-                del_ns_rr(zone, u'@', ns_rdata)
+                del_ns_rr(zone, u'@', ns_rdata, api=self.api)
             else:
                 zone = entry.dn[1].value  # get zone from DN
                 record = entry.single_value['idnsname']
                 root_logger.debug("record %s in zone %s", record, zone)
-                del_ns_rr(zone, record, ns_rdata)
+                del_ns_rr(zone, record, ns_rdata, api=self.api)
 
     def check_global_configuration(self):
         """
         Check global DNS configuration in LDAP server and inform user when it
         set and thus overrides his configured options in named.conf.
         """
-        result = api.Command.dnsconfig_show()
+        result = self.api.Command.dnsconfig_show()
         global_conf_set = any(param in result['result'] for \
-                              param in api.Object['dnsconfig'].params)
+                              param in self.api.Object['dnsconfig'].params)
 
         if not global_conf_set:
             print "Global DNS configuration in LDAP server is empty"
@@ -1162,8 +1162,8 @@ class BindInstance(service.Service):
         print "Global DNS configuration in LDAP server is not empty"
         print "The following configuration options override local settings in named.conf:"
         print ""
-        textui = ipalib.cli.textui(api)
-        api.Command.dnsconfig_show.output_for_cli(textui, result, None, reverse=False)
+        textui = ipalib.cli.textui(self.api)
+        self.api.Command.dnsconfig_show.output_for_cli(textui, result, None, reverse=False)
 
     def uninstall(self):
         if self.is_configured():
@@ -1174,7 +1174,7 @@ class BindInstance(service.Service):
         named_regular_running = self.restore_state("named-regular-running")
         named_regular_enabled = self.restore_state("named-regular-enabled")
 
-        self.dns_backup.clear_records(api.Backend.ldap2.isconnected())
+        self.dns_backup.clear_records(self.api.Backend.ldap2.isconnected())
 
 
         for f in [NAMED_CONF, RESOLV_CONF]:
diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py
index 9430d189978b0984b0b71d7d754516a4135053fb..344bb2e391c92003eb6670f26b470a76f8ed0e79 100644
--- a/ipaserver/install/dns.py
+++ b/ipaserver/install/dns.py
@@ -265,23 +265,26 @@ def install_check(standalone, replica, options, hostname):
         print "Using reverse zone(s) %s" % ', '.join(reverse_zones)
 
 
-def install(standalone, replica, options):
+def install(standalone, replica, options, xapi=None):
     global ip_addresses
     global dns_forwarders
     global reverse_zones
 
-    local_dnskeysyncd_dn = DN(('cn', 'DNSKeySync'), ('cn', api.env.host),
+    if xapi is None:
+        xapi = api
+
+    local_dnskeysyncd_dn = DN(('cn', 'DNSKeySync'), ('cn', xapi.env.host),
                               ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
-                              api.env.basedn)
-    conn = api.Backend.ldap2
+                              xapi.env.basedn)
+    conn = xapi.Backend.ldap2
 
     fstore = sysrestore.FileStore(paths.SYSRESTORE)
 
     conf_ntp = ntpinstance.NTPInstance(fstore).is_enabled()
 
-    bind = bindinstance.BindInstance(fstore, ldapi=True,
+    bind = bindinstance.BindInstance(fstore, ldapi=True, api=xapi,
                                      autobind=AUTOBIND_ENABLED)
-    bind.setup(api.env.host, ip_addresses, api.env.realm, api.env.domain,
+    bind.setup(xapi.env.host, ip_addresses, xapi.env.realm, xapi.env.domain,
                dns_forwarders, conf_ntp, reverse_zones, zonemgr=options.zonemgr,
                no_dnssec_validation=options.no_dnssec_validation,
                ca_configured=options.setup_ca)
@@ -296,14 +299,14 @@ def install(standalone, replica, options):
 
     # on dnssec master this must be installed last
     dnskeysyncd = dnskeysyncinstance.DNSKeySyncInstance(fstore, ldapi=True)
-    dnskeysyncd.create_instance(api.env.host, api.env.realm)
+    dnskeysyncd.create_instance(xapi.env.host, xapi.env.realm)
     if options.dnssec_master:
         ods = opendnssecinstance.OpenDNSSECInstance(fstore, ldapi=True)
         ods_exporter = odsexporterinstance.ODSExporterInstance(
             fstore, ldapi=True)
 
-        ods_exporter.create_instance(api.env.host, api.env.realm)
-        ods.create_instance(api.env.host, api.env.realm,
+        ods_exporter.create_instance(xapi.env.host, xapi.env.realm)
+        ods.create_instance(xapi.env.host, xapi.env.realm,
                             kasp_db_file=options.kasp_db_file)
     elif options.disable_dnssec_master:
         _disable_dnssec()
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index dcdc781455f0723e4ec19d4a28474ba8a09e6b56..44ce5d38dae5bc6179193119709bb2f91cfb3dae 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -749,8 +749,6 @@ def promote_check(installer):
         raise NotImplementedError
     if options.setup_kra:
         raise NotImplementedError
-    if options.setup_dns:
-        raise NotImplementedError
 
     tasks.check_selinux_status()
 
@@ -1004,7 +1002,6 @@ def promote_check(installer):
         raise RuntimeError("CA cert file is not available.")
 
     installer._ca_enabled = ca_enabled
-    installer._remote_api = remote_api
     installer._fstore = fstore
     installer._sstore = sstore
     installer._config = config
@@ -1052,6 +1049,8 @@ def promote(installer):
     # or certmonger will fail to contact the peer master
     install_http_certs(config, fstore)
 
+    ldapi_uri = installutils.realm_to_ldapi_uri(config.realm_name)
+
     # Create the management framework config file
     gopts = [
         ipaconf.setOption('host', config.host_name),
@@ -1059,8 +1058,7 @@ def promote(installer):
         ipaconf.setOption('xmlrpc_uri',
                           'https://%s/ipa/xml' %
                           ipautil.format_netloc(config.host_name)),
-        ipaconf.setOption('ldap_uri',
-                          installutils.realm_to_ldapi_uri(config.realm_name)),
+        ipaconf.setOption('ldap_uri', ldapi_uri),
         ipaconf.setOption('mode', 'production')]
     if installer.setup_ca:
         gopts.append(ipaconf.setOption('enable_ra', 'True'))
@@ -1104,7 +1102,6 @@ def promote(installer):
     CA.configure_certmonger_renewal()
     CA.fix_ra_perms()
 
-
     # Apply any LDAP updates. Needs to be done after the replica is synced-up
     service.print_msg("Applying LDAP updates")
     ds.apply_updates()
@@ -1122,10 +1119,6 @@ def promote(installer):
         dogtag_service = services.knownservices[dogtag_constants.SERVICE_NAME]
         dogtag_service.restart(dogtag_constants.PKI_INSTANCE_NAME)
 
-    if options.setup_dns:
-        api.Backend.ldap2.connect(autobind=True)
-        dns.install(False, True, options)
-
     # Restart httpd to pick up the new IPA configuration
     service.print_msg("Restarting the web server")
     http.restart()
@@ -1137,6 +1130,16 @@ def promote(installer):
 
     promote_sssd(config.host_name)
 
+    # Switch API so that it uses the new servr configuration
+    server_api = create_api(mode=None)
+    server_api.bootstrap(in_server=True, context='installer')
+    server_api.finalize()
+
+    if options.setup_dns:
+        server_api.Backend.rpcclient.connect()
+        server_api.Backend.ldap2.connect(autobind=True)
+        dns.install(False, True, options, server_api)
+
     # Everything installed properly, activate ipa service.
     services.knownservices.ipa.enable()
 
-- 
2.4.2

>From f4d26abf7a8bda1786a708bd01089f0831a2a2ee Mon Sep 17 00:00:00 2001
From: Simo Sorce <s...@redhat.com>
Date: Tue, 28 Jul 2015 11:31:16 -0400
Subject: [PATCH 6/6] Allow ipa-replica-conncheck to use default creds

If the user has already run kinit try to use those credentials.
The user can always override by explicitly passing the -p flag.

Signed-off-by: Simo Sorce <s...@redhat.com>
---
 install/tools/ipa-replica-conncheck        | 92 +++++++++++++++++++-----------
 ipaserver/install/replication.py           |  7 ++-
 ipaserver/install/server/replicainstall.py | 14 ++++-
 3 files changed, 74 insertions(+), 39 deletions(-)

diff --git a/install/tools/ipa-replica-conncheck b/install/tools/ipa-replica-conncheck
index 22348fc2158e59afc2e1aa51e3d3f51e90b99e39..d290d1ffc29b4750c8b6e42aa0cd06c20d3a14eb 100755
--- a/install/tools/ipa-replica-conncheck
+++ b/install/tools/ipa-replica-conncheck
@@ -38,11 +38,12 @@ import errno
 from socket import SOCK_STREAM, SOCK_DGRAM
 import distutils.spawn
 from ipaplatform.paths import paths
+import gssapi
 
 CONNECT_TIMEOUT = 5
 RESPONDERS = [ ]
 QUIET = False
-CCACHE_FILE = paths.CONNCHECK_CCACHE
+CCACHE_FILE = None
 KRB5_CONFIG = None
 
 class SshExec(object):
@@ -67,7 +68,12 @@ class SshExec(object):
         if verbose:
             cmd.insert(1, '-v')
 
-        env = {'KRB5_CONFIG': KRB5_CONFIG, 'KRB5CCNAME': CCACHE_FILE}
+        env = dict()
+        if KRB5_CONFIG is not None:
+            env['KRB5_CONFIG'] = KRB5_CONFIG
+        if CCACHE_FILE is not None:
+            env['KRB5CCNAME'] = CCACHE_FILE
+
         return ipautil.run(cmd, env=env, raiseonerr=False)
 
 
@@ -108,7 +114,7 @@ def parse_options():
     replica_group.add_option("-k", "--kdc", dest="kdc",
                       help="Master KDC. Defaults to master address")
     replica_group.add_option("-p", "--principal", dest="principal",
-                      default="admin", help="Principal to use to log in to remote master")
+                      default=None, help="Principal to use to log in to remote master")
     replica_group.add_option("-w", "--password", dest="password", sensitive=True,
                       help="Password for the principal"),
     parser.add_option_group(replica_group)
@@ -350,45 +356,63 @@ def main():
         remote_check_opts = ['--replica %s' % options.hostname]
 
         if options.auto_master_check:
-            (krb_fd, krb_name) = tempfile.mkstemp()
-            os.close(krb_fd)
-            configure_krb5_conf(options.realm, options.kdc, krb_name)
-            global KRB5_CONFIG
-            KRB5_CONFIG = krb_name
-
             print_info("Get credentials to log in to remote master")
-            if options.principal.find('@') == -1:
-                principal = '%s@%s' % (options.principal, options.realm)
-                user = options.principal
+            cred = None
+            if options.principal is None:
+                # Check if ccache is available
+                try:
+                    root_logger.debug('KRB5CCNAME set to %s' %
+                                      os.environ.get('KRB5CCNAME', None))
+                    # get default creds, will raise if none found
+                    cred = gssapi.creds.Credentials()
+                    principal = str(cred.name)
+                except gssapi.raw.misc.GSSError as e:
+                    root_logger.debug('Failed to find default ccache: %s' % e)
+                    # Use admin as the default principal
+                    principal = "admin"
             else:
                 principal = options.principal
-                user = options.principal.partition('@')[0]
 
-            if options.password:
-                password=options.password
-            else:
-                password = installutils.read_password(principal, confirm=False,
-                           validate=False, retry=False)
-                if password is None:
-                    sys.exit("Principal password required")
+            if cred is None:
+                (krb_fd, krb_name) = tempfile.mkstemp()
+                os.close(krb_fd)
+                configure_krb5_conf(options.realm, options.kdc, krb_name)
+                global KRB5_CONFIG
+                KRB5_CONFIG = krb_name
+                (ccache_fd, ccache_name) = tempfile.mkstemp()
+                os.close(ccache_fd)
+                global CCACHE_FILE
+                CCACHE_FILE = ccache_name
 
+                if principal.find('@') == -1:
+                    principal = '%s@%s' % (principal, options.realm)
 
-            stderr=''
-            (stdout, stderr, returncode) = ipautil.run([paths.KINIT, principal],
-                 env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE},
-                 stdin=password, raiseonerr=False)
-            if returncode != 0:
-                raise RuntimeError("Cannot acquire Kerberos ticket: %s" % stderr)
+                if options.password:
+                    password=options.password
+                else:
+                    password = installutils.read_password(principal, confirm=False,
+                               validate=False, retry=False)
+                    if password is None:
+                        sys.exit("Principal password required")
 
-            # Verify kinit was actually successful
-            stderr=''
-            (stdout, stderr, returncode) = ipautil.run([paths.BIN_KVNO,
-                 'host/%s' % options.master],
-                 env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE},
-                 raiseonerr=False)
-            if returncode != 0:
-                raise RuntimeError("Could not get ticket for master server: %s" % stderr)
 
+                stderr=''
+                (stdout, stderr, returncode) = ipautil.run([paths.KINIT, principal],
+                     env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE},
+                    stdin=password, raiseonerr=False)
+                if returncode != 0:
+                    raise RuntimeError("Cannot acquire Kerberos ticket: %s" % stderr)
+
+                # Verify kinit was actually successful
+                stderr=''
+                (stdout, stderr, returncode) = ipautil.run([paths.BIN_KVNO,
+                     'host/%s' % options.master],
+                    env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE},
+                    raiseonerr=False)
+                if returncode != 0:
+                    raise RuntimeError("Could not get ticket for master server: %s" % stderr)
+
+            user = principal.partition('@')[0]
             ssh = SshExec(user, options.master)
 
             print_info("Check SSH connection to remote master")
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
index a7787638a50df7455f5b64cd5b5cb9fe28378696..f827ddef8f869e0e32c7c11bdc84a9659882c7ab 100644
--- a/ipaserver/install/replication.py
+++ b/ipaserver/install/replication.py
@@ -62,7 +62,8 @@ STRIP_ATTRS = ('modifiersName',
 
 
 def replica_conn_check(master_host, host_name, realm, check_ca,
-                       dogtag_master_ds_port, admin_password=None):
+                       dogtag_master_ds_port, admin_password=None,
+                       principal="admin"):
     """
     Check the ports used by the replica both locally and remotely to be sure
     that replication will work.
@@ -72,10 +73,12 @@ def replica_conn_check(master_host, host_name, realm, check_ca,
     print "Run connection check to master"
     args = [paths.IPA_REPLICA_CONNCHECK, "--master", master_host,
             "--auto-master-check", "--realm", realm,
-            "--principal", "admin",
             "--hostname", host_name]
     nolog=tuple()
 
+    if principal is not None:
+        args.extend(["--principal", principal])
+
     if admin_password:
         args.extend(["--password", admin_password])
         nolog=(admin_password,)
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index 44ce5d38dae5bc6179193119709bb2f91cfb3dae..6a33a1fbd1ece339de196217f6b6a0ad385f667e 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -794,6 +794,7 @@ def promote_check(installer):
     installutils.verify_fqdn(config.master_host_name, options.no_host_dns)
 
     # Check if ccache is available
+    default_cred = None
     try:
         root_logger.debug('KRB5CCNAME set to %s' %
                           os.environ.get('KRB5CCNAME', None))
@@ -826,8 +827,8 @@ def promote_check(installer):
         stdin = None
         if principal.find('@') == -1:
             principal = '%s@%s' % (principal, config.realm_name)
-        if options.password is not None:
-            stdin = options.password
+        if options.admin_password is not None:
+            stdin = options.admin_password
         else:
             if not options.unattended:
                 try:
@@ -849,6 +850,9 @@ def promote_check(installer):
                 else:
                     stdin = sys.stdin.readline()
 
+            # set options.admin_password for future use
+            options.admin_password = stdin
+
         try:
             ipautil.kinit_password(principal, stdin, ccache_name)
         except RuntimeError as e:
@@ -994,9 +998,13 @@ def promote_check(installer):
 
     # check connection
     if not options.skip_conncheck:
+        p = None
+        if default_cred is None:
+            p = principal
         replica_conn_check(
             config.master_host_name, config.host_name, config.realm_name,
-            options.setup_ca, dogtag.Dogtag10Constants.DS_PORT)
+            options.setup_ca, dogtag.Dogtag10Constants.DS_PORT,
+            options.admin_password, principal=p)
 
     if not ipautil.file_exists(cafile):
         raise RuntimeError("CA cert file is not available.")
-- 
2.4.2

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to