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 <simo@redhat.com>
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
This commit is contained in:
Simo Sorce 2015-06-11 15:45:38 -04:00 committed by Jan Cholasta
parent 2cd0d20a2a
commit d03619fff3
14 changed files with 860 additions and 58 deletions

View File

@ -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,
)

View File

@ -360,5 +360,6 @@ 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

View File

@ -25,6 +25,7 @@ if six.PY3:
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
@ -52,6 +53,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,
)
)
@ -76,6 +78,7 @@ class ConfigureTool(admintool.AdminTool):
configurable_class = None
debug_option = False
positional_arguments = None
use_private_ccache = True
@staticmethod
def _transform(configurable_class):
@ -305,10 +308,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

View File

@ -260,6 +260,32 @@ 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.
"""
dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
query_filter = conn.make_filter({'objectClass': 'ipaConfigObject',
'ipaConfigString': 'enabledService',
'cn': 'CA'}, rules='&')
try:
entries, trunc = conn.find_entries(filter=query_filter, base_dn=dn)
except errors.NotFound:
return None
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

View File

@ -653,6 +653,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'

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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,412 @@ 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(ccache=os.environ.get('KRB5CCNAME'))
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 Replica(BaseServer):
replica_file = Knob(
str, None,
@ -710,6 +1221,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 +1262,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 +1279,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)

View File

@ -1540,8 +1540,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,