From a0bfbec19f99d01f59299b02a2f5f2f3a76fc1c3 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Tue, 11 Jan 2011 10:27:48 -0500 Subject: Use GSSAPI for replication Uses a temporary simple replication agreement over SSL to init the tree. Then once all principals have been created switches replication to GSSAPI. Fixes: https://fedorahosted.org/freeipa/ticket/690 --- install/tools/ipa-replica-install | 3 +- install/tools/ipa-replica-manage | 4 +- ipaserver/install/krbinstance.py | 14 +++- ipaserver/install/replication.py | 140 ++++++++++++++++++++++++++++++++++---- 4 files changed, 145 insertions(+), 16 deletions(-) diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install index 76f7f8c9a..3c912a759 100755 --- a/install/tools/ipa-replica-install +++ b/install/tools/ipa-replica-install @@ -205,7 +205,8 @@ def install_krb(config, setup_pkinit=False): pkcs12_info = (config.dir + "/pkinitcert.p12", config.dir + "/pkinit_pin.txt") - krb.create_replica(config.ds_user, config.realm_name, config.host_name, + krb.create_replica(config.ds_user, config.realm_name, + config.master_host_name, config.host_name, config.domain_name, config.dirman_password, ldappwd_filename, kpasswd_filename, setup_pkinit, pkcs12_info) diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage index 2400176fe..0fd06fd26 100755 --- a/install/tools/ipa-replica-manage +++ b/install/tools/ipa-replica-manage @@ -312,7 +312,7 @@ def add_link(realm, replica1, replica2, dirman_passwd, options): options.passsync, options.win_subtree, options.cacert) else: - repl1.setup_replication(replica2, "cn=Directory Manager", dirman_passwd) + repl1.setup_gssapi_replication(replica2, "cn=Directory Manager", dirman_passwd) print "Connected '%s' to '%s'" % (replica1, replica2) def re_initialize(realm, options): @@ -350,7 +350,7 @@ def force_sync(realm, thishost, fromhost, dirman_passwd): sys.exit(1) if len(entry) > 1: logging.error("Found multiple agreements for %s. Only initializing the first one returned: %s" % (thishost, entry[0].dn)) - repl.force_synch(entry[0].dn, entry[0].nsds5replicaupdateschedule, repl.conn) + repl.force_synch(entry[0].dn, entry[0].nsds5replicaupdateschedule) def main(): options, args = parse_options() diff --git a/ipaserver/install/krbinstance.py b/ipaserver/install/krbinstance.py index 4ad2fcec9..d89ad0b33 100644 --- a/ipaserver/install/krbinstance.py +++ b/ipaserver/install/krbinstance.py @@ -34,6 +34,7 @@ from ipalib import util from ipalib import errors from ipaserver import ipaldap +from ipaserver.install import replication import ldap from ldap import LDAPError @@ -181,7 +182,8 @@ class KrbInstance(service.Service): self.kpasswd = KpasswdInstance() self.kpasswd.create_instance('KPASSWD', self.fqdn, self.admin_password, self.suffix) - def create_replica(self, ds_user, realm_name, host_name, + def create_replica(self, ds_user, realm_name, + master_fqdn, host_name, domain_name, admin_password, ldap_passwd_filename, kpasswd_filename, setup_pkinit=False, pkcs12_info=None, @@ -191,6 +193,7 @@ class KrbInstance(service.Service): self.subject_base = subject_base self.__copy_ldap_passwd(ldap_passwd_filename) self.__copy_kpasswd_keytab(kpasswd_filename) + self.master_fqdn = master_fqdn self.__common_setup(ds_user, realm_name, host_name, domain_name, admin_password) @@ -202,6 +205,7 @@ class KrbInstance(service.Service): 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) self.__common_post_setup() @@ -543,6 +547,14 @@ class KrbInstance(service.Service): dn = "krbprincipalname=%s,cn=%s,cn=kerberos,%s" % (princ_realm, self.realm, self.suffix) self.admin_conn.inactivateEntry(dn, False) + def __convert_to_gssapi_replication(self): + repl = replication.ReplicationManager(self.realm, + self.fqdn, + self.dm_password) + repl.convert_to_gssapi_replication(self.master_fqdn, + r_binddn="cn=Directory Manager", + r_bindpw=self.dm_password) + def uninstall(self): if self.is_configured(): self.print_msg("Unconfiguring %s" % self.service_name) diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py index 3e467a9e7..547746592 100644 --- a/ipaserver/install/replication.py +++ b/ipaserver/install/replication.py @@ -59,6 +59,7 @@ class ReplicationManager: def __init__(self, realm, hostname, dirman_passwd): self.hostname = hostname self.dirman_passwd = dirman_passwd + self.realm = realm tmp = util.realm_to_suffix(realm) self.suffix = ipaldap.IPAdmin.normalizeDN(tmp) @@ -353,15 +354,15 @@ class ReplicationManager: entry.setValues("nsds7NewWinGroupSyncEnabled", 'false') entry.setValues("nsds7WindowsDomain", windomain) - def agreement_dn(self, hostname, port=PORT): - cn = "meTo%s%d" % (hostname, port) + def agreement_dn(self, hostname): + cn = "meTo%s" % (hostname) dn = "cn=%s, %s" % (cn, self.replica_dn()) return (cn, dn) def setup_agreement(self, a, b, repl_man_dn=None, repl_man_passwd=None, - iswinsync=False, win_subtree=None): + iswinsync=False, win_subtree=None, isgssapi=False): cn, dn = self.agreement_dn(b.host) try: a.getEntry(dn, ldap.SCOPE_BASE) @@ -369,7 +370,7 @@ class ReplicationManager: except errors.NotFound: pass - port = PORT + port = 389 if repl_man_dn is None: repl_man_dn = self.repl_man_dn if repl_man_passwd is None: @@ -387,15 +388,20 @@ class ReplicationManager: entry.setValues('nsds5replicahost', b.host) entry.setValues('nsds5replicaport', str(port)) entry.setValues('nsds5replicatimeout', str(TIMEOUT)) - entry.setValues('nsds5replicabinddn', repl_man_dn) - entry.setValues('nsds5replicacredentials', repl_man_passwd) - entry.setValues('nsds5replicabindmethod', 'simple') entry.setValues('nsds5replicaroot', self.suffix) entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456') - entry.setValues('nsds5replicatransportinfo', 'SSL') entry.setValues('nsDS5ReplicatedAttributeList', '(objectclass=*) $ EXCLUDE %s' % " ".join(excludes)) - entry.setValues('description', "me to %s%d" % (b.host, port)) + entry.setValues('description', "me to %s" % b.host) + entry.setValues('nsds5replicabinddn', repl_man_dn) + if isgssapi: + entry.setValues('nsds5replicatransportinfo', 'LDAP') + entry.setValues('nsds5replicabindmethod', 'SASL/GSSAPI') + else: + entry.setValues('nsds5replicacredentials', repl_man_passwd) + entry.setValues('nsds5replicatransportinfo', 'TLS') + entry.setValues('nsds5replicabindmethod', 'simple') + if iswinsync: self.setup_winsync_agmt(entry, win_subtree) @@ -403,6 +409,64 @@ class ReplicationManager: entry = a.waitForEntry(entry) + def setup_krb_princs_as_replica_binddns(self, a, b): + """ + Search the appropriate principal names so we can get + the correct DNs to store in the replication agreements. + Then modify the replica object to allow these DNs to act + as replication agents. + """ + + rep_dn = self.replica_dn() + filter_a = '(krbprincipalname=ldap/%s@%s)' % (a.host, self.realm) + filter_b = '(krbprincipalname=ldap/%s@%s)' % (b.host, self.realm) + + a_pn = b.search_s(self.suffix, ldap.SCOPE_SUBTREE, filterstr=filter_a) + b_pn = a.search_s(self.suffix, ldap.SCOPE_SUBTREE, filterstr=filter_b) + + # Add kerberos principal DNs as valid bindDNs for replication + try: + mod = [(ldap.MOD_ADD, "nsds5replicabinddn", b_pn[0].dn)] + a.modify_s(rep_dn, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + pass + try: + mod = [(ldap.MOD_ADD, "nsds5replicabinddn", a_pn[0].dn)] + b.modify_s(rep_dn, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + pass + + return (a_pn[0].dn, b_pn[0].dn) + + def gssapi_update_agreements(self, a, b): + + (a_pn_dn, b_pn_dn) = self.setup_krb_princs_as_replica_binddns(a, b) + + #change replication agreements to connect to other host using GSSAPI + cn, a_ag_dn = self.agreement_dn(b.host) + mod = [(ldap.MOD_REPLACE, "nsds5replicabinddn", a_pn_dn), + (ldap.MOD_DELETE, "nsds5replicacredentials", None), + (ldap.MOD_REPLACE, "nsds5replicatransportinfo", "LDAP"), + (ldap.MOD_REPLACE, "nsds5replicabindmethod", "SASL/GSSAPI")] + a.modify_s(a_ag_dn, mod) + + cn, b_ag_dn = self.agreement_dn(a.host) + mod = [(ldap.MOD_REPLACE, "nsds5replicabinddn", b_pn_dn), + (ldap.MOD_DELETE, "nsds5replicacredentials", None), + (ldap.MOD_REPLACE, "nsds5replicatransportinfo", "LDAP"), + (ldap.MOD_REPLACE, "nsds5replicabindmethod", "SASL/GSSAPI")] + b.modify_s(b_ag_dn, mod) + + # Finally remove the temporary replication manager user + try: + a.delete_s(self.repl_man_dn) + except ldap.NO_SUCH_OBJECT: + pass + try: + b.delete_s(self.repl_man_dn) + except ldap.NO_SUCH_OBJECT: + pass + def delete_agreement(self, hostname): cn, dn = self.agreement_dn(hostname) return self.conn.deleteEntry(dn) @@ -582,6 +646,58 @@ class ReplicationManager: return self.start_replication(self.conn, ad_conn, self.repl_man_dn, self.repl_man_passwd) + def convert_to_gssapi_replication(self, r_hostname, r_binddn, r_bindpw): + r_conn = ipaldap.IPAdmin(r_hostname, port=PORT, cacert=CACERT) + if r_bindpw: + r_conn.do_simple_bind(binddn=r_binddn, bindpw=r_bindpw) + else: + r_conn.sasl_interactive_bind_s('', SASL_AUTH) + + # First off make sure servers are in sync so that both KDCs + # have all princiapls and their passwords and can release + # the right tickets. We do this by force pushing all our changes + filter = "(&(nsDS5ReplicaHost=%s)(objectclass=nsds5ReplicationAgreement))" % r_hostname + entry = self.conn.search_s("cn=config", ldap.SCOPE_SUBTREE, filter) + if len(entry) == 0: + raise RuntimeError("Missing %s -> %s replication agreement" % + (self.hostname, r_hostname)) + if len(entry) > 1: + logging.info("Found multiple agreements for %s." % r_hostname) + logging.info("Syncing only the first one: %s" % entry[0].dn) + + self.force_synch(entry[0].dn, entry[0].nsds5replicaupdateschedule) + + # now wait until we are sure replication has succeeded. + cn, dn = self.agreement_dn(r_hostname) + self.wait_for_repl_update(self.conn, dn, 30) + + # now that directories are in sync, + # change the agreements to use GSSAPI + self.gssapi_update_agreements(self.conn, r_conn) + + def setup_gssapi_replication(self, r_hostname, r_binddn=None, r_bindpw=None): + """ + Directly sets up GSSAPI replication. + Only usable to connect 2 existing replicas (needs existing kerberos + principals) + """ + # 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=PORT, cacert=CACERT) + if r_bindpw: + r_conn.do_simple_bind(binddn=r_binddn, bindpw=r_bindpw) + else: + r_conn.sasl_interactive_bind_s('', SASL_AUTH) + + # Allow krb principals to act as replicas + (self_dn, r_dn) = self.setup_krb_princs_as_replica_binddns(self.conn, r_conn) + + # Create mutual replication agreementsausiung SASL/GSSAPI + self.setup_agreement(self.conn, r_conn, + repl_man_dn=self_dn, isgssapi=True) + self.setup_agreement(r_conn, self.conn, + repl_man_dn=r_dn, isgssapi=True) + def initialize_replication(self, dn, conn): mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] try: @@ -589,7 +705,7 @@ class ReplicationManager: except ldap.ALREADY_EXISTS: return - def force_synch(self, dn, schedule, conn): + def force_synch(self, dn, schedule): newschedule = '2358-2359 0' # On the remote chance of a match. We force a synch to happen right @@ -600,12 +716,12 @@ class ReplicationManager: logging.info("Changing agreement %s schedule to %s to force synch" % (dn, newschedule)) mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ newschedule ])] - conn.modify_s(dn, mod) + self.conn.modify_s(dn, mod) time.sleep(1) logging.info("Changing agreement %s to restore original schedule %s" % (dn, schedule)) mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ schedule ])] - conn.modify_s(dn, mod) + self.conn.modify_s(dn, mod) def get_agreement_type(self, hostname): cn, dn = self.agreement_dn(hostname) -- cgit