From 70bd0ec94c87069b0f4d8777332ac62bbd541ab6 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 11 Jun 2015 15:45:38 -0400 Subject: 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 --- install/tools/ipa-replica-install | 1 + ipaplatform/base/paths.py | 1 + ipapython/install/cli.py | 11 +- ipaserver/install/cainstance.py | 24 ++ ipaserver/install/certs.py | 12 + ipaserver/install/custodiainstance.py | 42 +- ipaserver/install/dsinstance.py | 93 ++++- ipaserver/install/httpinstance.py | 20 +- ipaserver/install/installutils.py | 29 +- ipaserver/install/krbinstance.py | 13 +- ipaserver/install/replication.py | 90 ++++ ipaserver/install/server/install.py | 6 +- ipaserver/install/server/replicainstall.py | 633 ++++++++++++++++++++++++++++- ipaserver/install/server/upgrade.py | 4 +- 14 files changed, 921 insertions(+), 58 deletions(-) diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install index 10a10827e..60a853b41 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 efbcc0f96..98aa5f9b7 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -358,6 +358,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 ce64baa5f..6d66e1551 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): @@ -300,10 +303,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/cainstance.py b/ipaserver/install/cainstance.py index c4788816a..ebf9a7ea5 100644 --- a/ipaserver/install/cainstance.py +++ b/ipaserver/install/cainstance.py @@ -253,6 +253,30 @@ def is_step_one_done(): return False +def find_ca_server(host_name, conn, api=api): + """ + :param host_name: the preferred server + :param conn: a connection to the LDAP server + :return: the selected host name + + Find a server that is a CA. + """ + base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), + api.env.basedn) + query_filter = conn.make_filter({'objectClass': 'ipaConfigObject', + 'ipaConfigString': 'enabledService', + 'cn': 'CA'}, rules='&') + entries, trunc = conn.find_entries(filter=query_filter, base_dn=base_dn) + if len(entries): + if host_name is not None: + for entry in entries: + if entry.dn[1].value == host_name: + return host_name + # if the preferred is not found, return the first in the list + return entries[0].dn[1].value + return None + + def is_ca_installed_locally(): """Check if CA is installed locally by checking for existence of CS.cfg :return:True/False diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index 3e07ee398..68cf9ce4e 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 c21b4537d..f506ba163 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 ipapython.secrets.kem import IPAKEMKeys +from ipapython.secrets.client import CustodiaClient from ipaplatform.paths import paths from service import SimpleServiceInstance from ipapython import ipautil @@ -9,11 +10,14 @@ import os class CustodiaInstance(SimpleServiceInstance): - def __init__(self): + def __init__(self, host_name=None, realm=None): 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') + self.ldap_uri = None + self.fqdn = host_name + self.realm = realm def __config_file(self): template_file = os.path.basename(self.config_file) + '.template' @@ -28,22 +32,48 @@ class CustodiaInstance(SimpleServiceInstance): fd.flush() fd.close() - def create_instance(self, *args, **kwargs): + def create_instance(self, dm_password=None): + suffix = ipautil.realm_to_suffix(self.realm) 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) + super(CustodiaInstance, self).create_instance(gensvc_name='KEYS', + fqdn=self.fqdn, + dm_password=dm_password, + ldap_suffix=suffix, + realm=self.realm) 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): - self.realm = realm + def upgrade_instance(self): if not os.path.exists(self.config_file): self.__config_file() if not os.path.exists(self.server_keys): self.__gen_keys() + def create_replica(self, master_host_name): + suffix = ipautil.realm_to_suffix(self.realm) + self.ldap_uri = 'ldap://%s' % master_host_name + self.master_host_name = 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(gensvc_name='KEYS', + fqdn=self.fqdn, + ldap_suffix=suffix, + realm=self.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, master_host_name): + cli = CustodiaClient(self.fqdn, master_host_name, self.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 312188273..f3a837baa 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -255,8 +255,8 @@ 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) @@ -301,6 +301,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 sasl mappings to the directory", self.__configure_sasl_mappings) self.step("adding default layout", self.__add_default_layout) @@ -314,6 +315,8 @@ 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() @@ -331,7 +334,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. @@ -353,8 +357,15 @@ 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)) + self.step("restarting directory server", self.__restart_instance) + + 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) @@ -374,14 +385,25 @@ 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 @@ -1128,3 +1150,58 @@ 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) + try: + os.unlink(paths.DS_KEYTAB) + except OSError: + pass + + 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) + db.create_pin_file() + + # Connect to self over ldapi as Directory Manager and configure SSL + conn = ipaldap.IPAdmin(self.fqdn, ldapi=True, realm=self.realm) + conn.do_external_bind('root') + + mod = [(ldap.MOD_REPLACE, "nsSSLClientAuth", "allowed"), + (ldap.MOD_REPLACE, "nsSSL3Ciphers", "+all"), + (ldap.MOD_REPLACE, "allowWeakCipher", "off")] + conn.modify_s(DN(('cn', 'encryption'), ('cn', 'config')), mod) + + mod = [(ldap.MOD_ADD, "nsslapd-security", "on")] + conn.modify_s(DN(('cn', 'config')), mod) + + entry = conn.make_entry( + DN(('cn', 'RSA'), ('cn', 'encryption'), ('cn', 'config')), + objectclass=["top", "nsEncryptionModule"], + cn=["RSA"], + nsSSLPersonalitySSL=[self.nickname], + nsSSLToken=["internal (software)"], + nsSSLActivation=["on"], + ) + conn.add_entry(entry) + + conn.unbind() + + # check for open secure port 636 from now on + self.open_ports.append(636) diff --git a/ipaserver/install/httpinstance.py b/ipaserver/install/httpinstance.py index 4269d3697..ba16a61b2 100644 --- a/ipaserver/install/httpinstance.py +++ b/ipaserver/install/httpinstance.py @@ -112,7 +112,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 @@ -132,6 +132,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() @@ -147,12 +148,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(): @@ -183,10 +185,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) @@ -309,14 +311,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 acf309e78..7fca84384 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -47,12 +47,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 + if six.PY3: unicode = str @@ -1115,3 +1117,28 @@ 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 864615d96..1dd807c71 100644 --- a/ipaserver/install/krbinstance.py +++ b/ipaserver/install/krbinstance.py @@ -173,7 +173,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 @@ -181,12 +181,17 @@ 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 b50dc0584..858e3f36b 100644 --- a/ipaserver/install/replication.py +++ b/ipaserver/install/replication.py @@ -1605,6 +1605,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/install.py b/ipaserver/install/server/install.py index e936b6798..3164d0b94 100644 --- a/ipaserver/install/server/install.py +++ b/ipaserver/install/server/install.py @@ -814,10 +814,8 @@ 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), - realm_name) + custodia = custodiainstance.CustodiaInstance(host_name, realm_name) + custodia.create_instance(dm_password) # Create a HTTP instance http = httpinstance.HTTPInstance(fstore) diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py index c0b0761eb..109874877 100644 --- a/ipaserver/install/server/replicainstall.py +++ b/ipaserver/install/server/replicainstall.py @@ -8,13 +8,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 @@ -24,14 +26,19 @@ 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 +from binascii import hexlify from .common import BaseServer @@ -60,7 +67,29 @@ def make_pkcs12_info(directory, cert_name, password_name): return None -def install_replica_ds(config): +def install_http_certs(config, fstore): + + # Obtain keytab for the HTTP service + fstore.backup_file(paths.IPA_KEYTAB) + try: + os.unlink(paths.IPA_KEYTAB) + except OSError: + pass + + 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, promote=False): dsinstance.check_ports() # if we have a pkcs12 file, create the cert db from @@ -79,12 +108,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=promote, ) return ds -def install_krb(config, setup_pkinit=False): +def install_krb(config, setup_pkinit=False, promote=False): krb = krbinstance.KrbInstance() # pkinit files @@ -94,7 +124,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 @@ -115,7 +145,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 @@ -131,7 +161,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: @@ -153,9 +184,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: @@ -283,6 +315,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: @@ -311,6 +380,37 @@ 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 @@ -433,6 +533,14 @@ def install_check(installer): # available current = 0 + if current != 0: + raise RuntimeError( + "You cannot use a replica file to join a replica when the " + "domain is above level 0. Please join the system to the " + "domain by running ipa-client-install first, the try again " + "without a replica file." + ) + # Detect if current level is out of supported range # for this IPA version under_lower_bound = current < constants.MIN_DOMAIN_LEVEL @@ -596,12 +704,9 @@ 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)) + custodia = custodiainstance.CustodiaInstance(config.host_name, + config.realm_name) + custodia.create_instance(config.dirman_password) # The DS instance is created before the keytab, add the SSL cert we # generated @@ -662,6 +767,475 @@ 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" + "You must use a replica file or join the system " + "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: + raise RuntimeError("Password must be provided for %s." + % principal) + else: + if sys.stdin.isatty(): + root_logger.info("Password must be provided in " + + "non-interactive mode. " + + "This can be done via " + + "echo password | ipa-client-install " + + "... or with the -w option.") + raise RuntimeError("Password must be provided in " + + "non-interactive mode.") + else: + stdin = sys.stdin.readline() + + try: + ipautil.kinit_password(principal, stdin, ccache_name) + except RuntimeError as e: + raise RuntimeError("Kerberos authentication failed: %s" % e) + + 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() + 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 + + if current == 0: + raise RuntimeError( + "You must provide a file generated by ipa-replica-prepare to " + "create a replica when the domain is at level 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) + + # Find if any server has a CA + ca_host = cainstance.find_ca_server(api.env.server, conn) + if ca_host is not None: + config.ca_host_name = ca_host + ca_enabled = True + else: + # 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, 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 = hexlify(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, promote=True) + + # 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'), + ipaconf.setOption('enable_ra', 'True'), + ipaconf.setOption('ra_plugin', 'dogtag'), + 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(config.host_name, + config.realm_name) + custodia.create_replica(config.master_host_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.master_host_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" + + no_pkinit = Knob( + bool, False, + description="disables pkinit setup steps", + ) + + skip_schema_check = Knob( + bool, False, + description="skip check for updated CA DS schema on the remote master", + ) + + +class ReplicaDNS(common.Installable, core.Group, core.Composite): + description = "DNS" + + setup_dns = Knob( + bool, False, + description="configure bind with our zone", + ) + + forwarders = Knob( + (list, 'ip'), None, + description=("Add a DNS forwarder. This option can be used multiple " + "times"), + cli_name='forwarder', + ) + + no_forwarders = Knob( + bool, False, + description="Do not add any DNS forwarders, use root servers instead", + ) + + reverse_zones = Knob( + (list, str), [], + description=("The reverse DNS zone to use. This option can be used " + "multiple times"), + cli_name='reverse-zone', + cli_metavar='REVERSE_ZONE', + ) + + no_reverse = Knob( + bool, False, + description="Do not create new reverse DNS zone", + ) + + no_dnssec_validation = Knob( + bool, False, + description="Disable DNSSEC validation", + ) + + no_host_dns = Knob( + bool, False, + description="Do not use DNS for hostname lookup during installation", + ) + + no_dns_sshfp = Knob( + bool, False, + description="do not automatically create DNS SSHFP records", + ) + + class Replica(BaseServer): replica_file = Knob( str, None, @@ -710,6 +1284,15 @@ class Replica(BaseServer): description="skip connection check to remote master", ) + principal = Knob( + str, None, + sensitive=True, + description="User Principal allowed to promote replicas", + cli_short_name='P', + ) + + promote = False + # ca external_ca = None external_ca_type = None @@ -742,11 +1325,11 @@ class Replica(BaseServer): self._update_hosts_file = False 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) + self.promote = True + else: + if not ipautil.file_exists(self.replica_file): + raise RuntimeError("Replica file %s does not exist" + % self.replica_file) if self.setup_dns: #pylint: disable=no-member @@ -759,6 +1342,12 @@ class Replica(BaseServer): @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) diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py index 42c9cf0f5..3cafb4e3c 100644 --- a/ipaserver/install/server/upgrade.py +++ b/ipaserver/install/server/upgrade.py @@ -1515,8 +1515,8 @@ 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) + custodia = custodiainstance.CustodiaInstance(api.env.host, api.env.realm) + custodia.upgrade_instance() ca_restart = any([ ca_restart, -- cgit