summaryrefslogtreecommitdiffstats
path: root/ipa-client
diff options
context:
space:
mode:
authorJohn Dennis <jdennis@redhat.com>2012-11-15 14:57:52 -0500
committerRob Crittenden <rcritten@redhat.com>2013-01-23 14:26:42 -0500
commita1991aeac19c3fec1fdd0d184c6760c90c9f9fc9 (patch)
tree1832274281bcb92cd933b2262b2be221efd031f5 /ipa-client
parent91f4af7e6af53e1c6bf17ed36cb2161863eddae4 (diff)
downloadfreeipa-a1991aeac19c3fec1fdd0d184c6760c90c9f9fc9.tar.gz
freeipa-a1991aeac19c3fec1fdd0d184c6760c90c9f9fc9.tar.xz
freeipa-a1991aeac19c3fec1fdd0d184c6760c90c9f9fc9.zip
Use secure method to acquire IPA CA certificate
Major changes ipa-client-install: * Use GSSAPI connection to LDAP server to download CA cert (now the default method) * Add --ca-cert-file option to load the CA cert from a disk file. Validate the file. If this option is used the supplied CA cert is considered definitive. * The insecure HTTP retrieval method is still supported but it must be explicitly forced and a warning will be emitted. * Remain backward compatible with unattended case (except for aberrant condition when preexisting /etc/ipa/ca.crt differs from securely obtained CA cert, see below) * If /etc/ipa/ca.crt CA cert preexists the validate it matches the securely acquired CA cert, if not: - If --unattended and not --force abort with error - If interactive query user to accept new CA cert, if not abort In either case warn user. * If interactive and LDAP retrieval fails prompt user if they want to proceed with insecure HTTP method * If not interactive and LDAP retrieval fails abort unless --force * Backup preexisting /etc/ipa/ca.crt in FileStore prior to execution, if ipa-client-install fails it will be restored. Other changes: * Add new exception class CertificateInvalidError * Add utility convert_ldap_error() to ipalib.ipautil * Replace all hardcoded instances of /etc/ipa/ca.crt in ipa-client-install with CACERT constant (matches existing practice elsewhere). * ipadiscovery no longer retrieves CA cert via HTTP. * Handle LDAP minssf failures during discovery, treat failure to check ldap server as a warninbg in absebce of a provided CA certificate via --ca-cert-file or though existing /etc/ipa/ca.crt file. Signed-off-by: Simo Sorce <simo@redhat.com> Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Diffstat (limited to 'ipa-client')
-rwxr-xr-xipa-client/ipa-install/ipa-client-install405
-rw-r--r--ipa-client/ipaclient/ipadiscovery.py50
-rw-r--r--ipa-client/man/ipa-client-install.18
3 files changed, 408 insertions, 55 deletions
diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index a38c82806..bd299f9ba 100755
--- a/ipa-client/ipa-install/ipa-client-install
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -25,14 +25,18 @@ try:
import os
import time
import socket
+ import ldap
+ import ldap.sasl
+ import urlparse
from ipapython.ipa_log_manager import *
import tempfile
import getpass
from ipaclient import ipadiscovery
+ from ipaclient.ipadiscovery import CACERT
import ipaclient.ipachangeconf
import ipaclient.ntpconf
- from ipapython.ipautil import run, user_input, CalledProcessError, file_exists, realm_to_suffix
+ from ipapython.ipautil import run, user_input, CalledProcessError, file_exists, realm_to_suffix, convert_ldap_error
import ipapython.services as ipaservices
from ipapython import ipautil
from ipapython import sysrestore
@@ -40,12 +44,13 @@ try:
from ipapython import certmonger
from ipapython.config import IPAOptionParser
from ipalib import api, errors
+ from ipalib import x509
from ipapython.dn import DN
from ipapython.ssh import SSHPublicKey
from ipalib.rpc import delete_persistent_client_session_data
import SSSDConfig
from ConfigParser import RawConfigParser
- from optparse import SUPPRESS_HELP, OptionGroup
+ from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError
except ImportError:
print >> sys.stderr, """\
There was a problem importing one of the required Python modules. The
@@ -55,6 +60,7 @@ error was:
""" % sys.exc_value
sys.exit(1)
+SUCCESS = 0
CLIENT_INSTALL_ERROR = 1
CLIENT_NOT_CONFIGURED = 2
CLIENT_ALREADY_CONFIGURED = 3
@@ -63,6 +69,21 @@ CLIENT_UNINSTALL_ERROR = 4 # error after restoring files/state
client_nss_nickname_format = 'IPA Machine Certificate - %s'
def parse_options():
+ def validate_ca_cert_file_option(option, opt, value, parser):
+ if not os.path.exists(value):
+ raise OptionValueError("%s option '%s' does not exist" % (opt, value))
+ if not os.path.isfile(value):
+ raise OptionValueError("%s option '%s' is not a file" % (opt, value))
+ if not os.path.isabs(value):
+ raise OptionValueError("%s option '%s' is not an absolute file path" % (opt, value))
+
+ try:
+ cert = x509.load_certificate_from_file(value)
+ except Exception, e:
+ raise OptionValueError("%s option '%s' is not a valid certificate file" % (opt, value))
+
+ parser.values.ca_cert_file = value
+
parser = IPAOptionParser(version=version.VERSION)
basic_group = OptionGroup(parser, "basic options")
@@ -108,6 +129,9 @@ def parse_options():
basic_group.add_option("-U", "--unattended", dest="unattended",
action="store_true",
help="unattended (un)installation never prompts the user")
+ basic_group.add_option("--ca-cert-file", dest="ca_cert_file",
+ type="string", action="callback", callback=validate_ca_cert_file_option,
+ help="load the CA certificate from this file")
# --on-master is used in ipa-server-install and ipa-replica-install
# only, it isn't meant to be used on clients.
basic_group.add_option("--on-master", dest="on_master", action="store_true",
@@ -171,6 +195,34 @@ def nickname_exists(nickname):
else:
return False
+def cert_summary(msg, cert, indent=' '):
+ if msg:
+ s = '%s\n' % msg
+ else:
+ s = ''
+ s += '%sSubject: %s\n' % (indent, cert.subject)
+ s += '%sIssuer: %s\n' % (indent, cert.issuer)
+ s += '%sValid From: %s\n' % (indent, cert.valid_not_before_str)
+ s += '%sValid Until: %s\n' % (indent, cert.valid_not_after_str)
+
+ return s
+
+def get_cert_path(cert_path):
+ """
+ If a CA certificate is passed in on the command line, use that.
+
+ Else if a CA file exists in CACERT then use that.
+
+ Otherwise return None.
+ """
+ if cert_path is not None:
+ return cert_path
+
+ if os.path.exists(CACERT):
+ return CACERT
+
+ return None
+
# Checks whether nss_ldap or nss-pam-ldapd is installed. If anyone of mandatory files was found returns True and list of all files found.
def nssldap_exists():
files_to_check = [{'function':'configure_ldap_conf', 'mandatory':['/etc/ldap.conf','/etc/nss_ldap.conf','/etc/libnss-ldap.conf'], 'optional':['/etc/pam_ldap.conf']},
@@ -709,7 +761,7 @@ def configure_openldap_conf(fstore, cli_basedn, cli_server):
{'name':'empty', 'type':'empty'},
{'name':'URI', 'type':'option', 'value':'ldaps://'+ cli_server[0]},
{'name':'BASE', 'type':'option', 'value':cli_basedn},
- {'name':'TLS_CACERT', 'type':'option', 'value':'/etc/ipa/ca.crt'},
+ {'name':'TLS_CACERT', 'type':'option', 'value':CACERT},
{'name':'empty', 'type':'empty'}]
target_fname = '/etc/openldap/ldap.conf'
@@ -779,7 +831,7 @@ def configure_krb5_conf(cli_realm, cli_domain, cli_server, cli_kdc, dnsok,
kropts.append({'name':'master_kdc', 'type':'option', 'value':ipautil.format_netloc(server, 88)})
kropts.append({'name':'admin_server', 'type':'option', 'value':ipautil.format_netloc(server, 749)})
kropts.append({'name':'default_domain', 'type':'option', 'value':cli_domain})
- kropts.append({'name':'pkinit_anchors', 'type':'option', 'value':'FILE:/etc/ipa/ca.crt'})
+ kropts.append({'name':'pkinit_anchors', 'type':'option', 'value':'FILE:%s' % CACERT})
ropts = [{'name':cli_realm, 'type':'subsection', 'value':kropts}]
opts.append({'name':'realms', 'type':'section', 'value':ropts})
@@ -960,7 +1012,7 @@ def configure_sssd_conf(fstore, cli_realm, cli_domain, cli_server, options, clie
# Note that SSSD will force StartTLS because the channel is later used for
# authentication as well if password migration is enabled. Thus set the option
# unconditionally.
- domain.set_option('ldap_tls_cacert', '/etc/ipa/ca.crt')
+ domain.set_option('ldap_tls_cacert', CACERT)
if options.dns_updates:
domain.set_option('ipa_dyndns_update', True)
@@ -1283,6 +1335,309 @@ def print_port_conf_info():
" TCP: 464\n"
" UDP: 464, 123 (if NTP enabled)")
+def get_ca_cert_from_file(url):
+ '''
+ Get the CA cert from a user supplied file and write it into the
+ CACERT file.
+
+ Raises errors.NoCertificateError if unable to read cert.
+ Raises errors.FileError if unable to write cert.
+ '''
+
+ # pylint: disable=E1101
+ try:
+ parsed = urlparse.urlparse(url, 'file')
+ except Exception, e:
+ raise errors.FileError("unable to parse file url '%s'" % (url))
+
+ if parsed.scheme != 'file':
+ raise errors.FileError("url is not a file scheme '%s'" % (url))
+
+ filename = parsed.path
+
+ if not os.path.exists(filename):
+ raise errors.FileError("file '%s' does not exist" % (filename))
+
+ if not os.path.isfile(filename):
+ raise errors.FileError("file '%s' is not a file" % (filename))
+
+ root_logger.debug("trying to retrieve CA cert from file %s", filename)
+ try:
+ cert = x509.load_certificate_from_file(filename)
+ except Exception, e:
+ raise errors.NoCertificateError(entry=filename)
+
+ try:
+ x509.write_certificate(cert.der_data, CACERT)
+ except Exception, e:
+ raise errors.FileError(reason =
+ u"cannot write certificate file '%s': %s" % (CACERT, e))
+
+def get_ca_cert_from_http(url, ca_file, warn=True):
+ '''
+ Use HTTP to retrieve the CA cert and write it into the CACERT file.
+ This is insecure and should be avoided.
+
+ Raises errors.NoCertificateError if unable to retrieve and write cert.
+ '''
+
+ if warn:
+ root_logger.warning("Downloading the CA certificate via HTTP, " +
+ "this is INSECURE")
+
+ root_logger.debug("trying to retrieve CA cert via HTTP from %s", url)
+ try:
+
+ run(["/usr/bin/wget", "-O", ca_file, url])
+ except CalledProcessError, e:
+ raise errors.NoCertificateError(entry=url)
+
+def get_ca_cert_from_ldap(url, basedn, ca_file):
+ '''
+ Retrieve th CA cert from the LDAP server by binding to the
+ server with GSSAPI using the current Kerberos credentials.
+ Write the retrieved cert into the CACERT file.
+
+ Raises errors.NoCertificateError if cert is not found.
+ Raises errors.NetworkError if LDAP connection can't be established.
+ Raises errors.LDAPError for any other generic LDAP error.
+ Raises errors.OnlyOneValueAllowed if more than one cert is found.
+ Raises errors.FileError if unable to write cert.
+ '''
+
+ ca_cert_attr = 'cAcertificate;binary'
+ dn = DN(('cn', 'CAcert'), ('cn', 'ipa'), ('cn', 'etc'), basedn)
+
+ SASL_GSSAPI = ldap.sasl.sasl({},'GSSAPI')
+
+ root_logger.debug("trying to retrieve CA cert via LDAP from %s", url)
+
+ conn = ldap.initialize(url)
+ conn.set_option(ldap.OPT_X_SASL_NOCANON, ldap.OPT_ON)
+ try:
+ conn.sasl_interactive_bind_s('', SASL_GSSAPI)
+ result = conn.search_st(str(dn), ldap.SCOPE_BASE, 'objectclass=pkiCA',
+ [ca_cert_attr], timeout=10)
+ except ldap.NO_SUCH_OBJECT, e:
+ root_logger.debug("get_ca_cert_from_ldap() error: %s",
+ convert_ldap_error(e))
+ raise errors.NoCertificateError(entry=url)
+
+ except ldap.SERVER_DOWN, e:
+ root_logger.debug("get_ca_cert_from_ldap() error: %s",
+ convert_ldap_error(e))
+ raise errors.NetworkError(uri=url, error=str(e))
+ except Exception, e:
+ root_logger.debug("get_ca_cert_from_ldap() error: %s",
+ convert_ldap_error(e))
+ raise errors.LDAPError(str(e))
+
+ if len(result) != 1:
+ raise errors.OnlyOneValueAllowed(attr=ca_cert_attr)
+
+ attrs = result[0][1]
+ try:
+ der_cert = attrs[ca_cert_attr][0]
+ except KeyError:
+ raise errors.NoCertificateError(entry=ca_cert_attr)
+
+ try:
+ x509.write_certificate(der_cert, ca_file)
+ except Exception, e:
+ raise errors.FileError(reason =
+ u"cannot write certificate file '%s': %s" % (ca_file, e))
+
+def validate_new_ca_cert(existing_ca_cert, ca_file, ask, override=False):
+
+ try:
+ new_ca_cert = x509.load_certificate_from_file(ca_file)
+ except Exception, e:
+ raise errors.FileError(
+ "Unable to read new ca cert '%s': %s" % (ca_file, e))
+
+ if existing_ca_cert is None:
+ root_logger.info(
+ cert_summary("Successfully retrieved CA cert", new_ca_cert))
+ return
+
+ if existing_ca_cert.der_data != new_ca_cert.der_data:
+ root_logger.warning(
+ "The CA cert available from the IPA server does not match the\n"
+ "local certificate available at %s" % CACERT)
+ root_logger.warning(
+ cert_summary("Existing CA cert:", existing_ca_cert))
+ root_logger.warning(
+ cert_summary("Retrieved CA cert:", new_ca_cert))
+ if override:
+ root_logger.warning("Overriding existing CA cert\n")
+ elif not ask or not user_input(
+ "Do you want to replace the local certificate with the CA\n"
+ "certificate retrieved from the IPA server?", True):
+ raise errors.CertificateInvalidError(name='Retrieved CA')
+ else:
+ root_logger.debug(
+ "Existing CA cert and Retrieved CA cert are identical")
+ os.remove(ca_file)
+
+
+def get_ca_cert(fstore, options, server, basedn):
+ '''
+ Examine the different options and determine a method for obtaining
+ the CA cert.
+
+ If successful the CA cert will have been written into CACERT.
+
+ Raises errors.NoCertificateError if not successful.
+
+ The logic for determining how to load the CA cert is as follow:
+
+ In the OTP case (not -p and -w):
+
+ 1. load from user supplied cert file
+ 2. else load from HTTP
+
+ In the 'user_auth' case ((-p and -w) or interactive):
+
+ 1. load from user supplied cert file
+ 2. load from LDAP using SASL/GSS/Krb5 auth
+ (provides mutual authentication, integrity and security)
+ 3. if LDAP failed and interactive ask for permission to
+ use insecure HTTP (default: No)
+
+ In the unattended case:
+
+ 1. load from user supplied cert file
+ 2. load from HTTP if --force specified else fail
+
+ In all cases if HTTP is used emit warning message
+ '''
+
+ ca_file = CACERT + ".new"
+
+ def ldap_url():
+ return urlparse.urlunparse(('ldap', ipautil.format_netloc(server),
+ '', '', '', ''))
+
+ def file_url():
+ return urlparse.urlunparse(('file', '', options.ca_cert_file,
+ '', '', ''))
+
+ def http_url():
+ return urlparse.urlunparse(('http', ipautil.format_netloc(server),
+ '/ipa/config/ca.crt', '', '', ''))
+
+
+ interactive = not options.unattended
+ otp_auth = options.principal is None and options.password is not None
+ existing_ca_cert = None
+
+ if options.ca_cert_file:
+ url = file_url()
+ try:
+ get_ca_cert_from_file(url)
+ except Exception, e:
+ root_logger.debug(e)
+ raise errors.NoCertificateError(entry=url)
+ root_logger.debug("CA cert provided by user, use it!")
+ else:
+ if os.path.exists(CACERT):
+ if os.path.isfile(CACERT):
+ try:
+ existing_ca_cert = x509.load_certificate_from_file(CACERT)
+ except Exception, e:
+ raise errors.FileError(reason=u"Unable to load existing" +
+ " CA cert '%s': %s" % (CACERT, e))
+ else:
+ raise errors.FileError(reason=u"Existing ca cert '%s' is " +
+ "not a plain file" % (CACERT))
+
+ if otp_auth:
+ if existing_ca_cert:
+ root_logger.info("OTP case, CA cert preexisted, use it")
+ else:
+ url = http_url()
+ override = not interactive
+ if interactive and not user_input(
+ "Do you want download the CA cert from " + url + " ?\n"
+ "(this is INSECURE)", False):
+ raise errors.NoCertificateError(message=u"HTTP certificate"
+ " download declined by user")
+ try:
+ get_ca_cert_from_http(url, ca_file, override)
+ except Exception, e:
+ root_logger.debug(e)
+ raise errors.NoCertificateError(entry=url)
+
+ try:
+ validate_new_ca_cert(existing_ca_cert, ca_file,
+ False, override)
+ except Exception, e:
+ os.unlink(ca_file)
+ raise
+ else:
+ # Auth with user credentials
+ url = ldap_url()
+ try:
+ get_ca_cert_from_ldap(url, basedn, ca_file)
+ try:
+ validate_new_ca_cert(existing_ca_cert,
+ ca_file, interactive)
+ except Exception, e:
+ os.unlink(ca_file)
+ raise
+ except errors.NoCertificateError, e:
+ root_logger.debug(str(e))
+ url = http_url()
+ if existing_ca_cert:
+ root_logger.warning(
+ "Unable to download CA cert from LDAP\n"
+ "but found preexisting cert, using it.\n")
+ elif interactive and not user_input(
+ "Unable to download CA cert from LDAP.\n"
+ "Do you want to download the CA cert from " + url + "?\n"
+ "(this is INSECURE)", False):
+ raise errors.NoCertificateError(message=u"HTTP "
+ "certificate download declined by user")
+ elif not interactive and not options.force:
+ root_logger.error(
+ "In unattended mode without a One Time Password "
+ "(OTP) or without --ca-cert-file\nYou must specify"
+ " --force to retrieve the CA cert using HTTP")
+ raise errors.NoCertificateError(message=u"HTTP "
+ "certificate download requires --force")
+ else:
+ try:
+ get_ca_cert_from_http(url, ca_file)
+ except Exception, e:
+ root_logger.debug(e)
+ raise errors.NoCertificateError(entry=url)
+ try:
+ validate_new_ca_cert(existing_ca_cert,
+ ca_file, interactive)
+ except Exception, e:
+ os.unlink(ca_file)
+ raise
+ except Exception, e:
+ root_logger.debug(str(e))
+ raise errors.NoCertificateError(entry=url)
+
+
+ # We should have a cert now, move it to the canonical place
+ if os.path.exists(ca_file):
+ os.rename(ca_file, CACERT)
+ elif existing_ca_cert is None:
+ raise errors.InternalError(u"expected CA cert file '%s' to "
+ u"exist, but it's absent" % (ca_file))
+
+
+ # Make sure the file permissions are correct
+ try:
+ os.chmod(CACERT, 0644)
+ except Exception, e:
+ raise errors.FileError(reason=u"Unable set permissions on ca "
+ u"cert '%s': %s" % (CACERT, e))
+
+
def install(options, env, fstore, statestore):
dnsok = False
@@ -1340,7 +1695,7 @@ def install(options, env, fstore, statestore):
# Do discovery on the first server passed in, we'll do sanity checking
# on any others
- ret = ds.search(domain=options.domain, server=options.server, hostname=hostname)
+ ret = ds.search(domain=options.domain, server=options.server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
if ret == ipadiscovery.BAD_HOST_CONFIG:
root_logger.error("Can't get the fully qualified name of this host")
@@ -1377,7 +1732,7 @@ def install(options, env, fstore, statestore):
cli_domain_source = 'Provided interactively'
root_logger.debug(
"will use interactively provided domain: %s", cli_domain)
- ret = ds.search(domain=cli_domain, server=options.server, hostname=hostname)
+ ret = ds.search(domain=cli_domain, server=options.server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
if not cli_domain:
if ds.domain:
@@ -1401,7 +1756,7 @@ def install(options, env, fstore, statestore):
cli_server = [user_input("Provide your IPA server name (ex: ipa.example.com)", allow_empty = False)]
cli_server_source = 'Provided interactively'
root_logger.debug("will use interactively provided server: %s", cli_server[0])
- ret = ds.search(domain=cli_domain, server=cli_server, hostname=hostname)
+ ret = ds.search(domain=cli_domain, server=cli_server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
else:
# Only set dnsok to True if we were not passed in one or more servers
@@ -1439,6 +1794,12 @@ def install(options, env, fstore, statestore):
"has been explicitly restricted.")
ret = 0
+ if ret == ipadiscovery.NO_TLS_LDAP:
+ root_logger.warning("The LDAP server requires TLS is but we do not " +
+ "have the CA.")
+ root_logger.info("Proceeding without strict verification.")
+ ret = 0
+
if ret != 0:
root_logger.error("Failed to verify that %s is an IPA Server.",
cli_server[0])
@@ -1490,7 +1851,7 @@ def install(options, env, fstore, statestore):
# Now do a sanity check on the other servers
if options.server and len(options.server) > 1:
for server in options.server[1:]:
- ret = ds.search(domain=cli_domain, server=server, hostname=hostname)
+ ret = ds.search(domain=cli_domain, server=server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
if ret == ipadiscovery.NOT_IPA_SERVER:
root_logger.error("%s is not an IPA v2 Server.", server)
print_port_conf_info()
@@ -1539,21 +1900,6 @@ def install(options, env, fstore, statestore):
root_logger.debug(
"will use principal provided as option: %s", options.principal)
- # Get the CA certificate
- try:
- # Remove anything already there so that wget doesn't use its
- # too-clever renaming feature
- os.remove("/etc/ipa/ca.crt")
- except Exception:
- pass
-
- try:
- run(["/usr/bin/wget", "-O", "/etc/ipa/ca.crt", "http://%s/ipa/config/ca.crt" % ipautil.format_netloc(cli_server[0])])
- except CalledProcessError, e:
- root_logger.error(
- 'Retrieving CA from %s failed: %s', cli_server[0], str(e))
- return CLIENT_INSTALL_ERROR
-
if not options.on_master:
nolog = tuple()
# First test out the kerberos configuration
@@ -1650,6 +1996,15 @@ def install(options, env, fstore, statestore):
join_args.append(password)
nolog = (password,)
+ # Get the CA certificate
+ try:
+ os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG']
+ get_ca_cert(fstore, options, cli_server[0], cli_basedn)
+ del os.environ['KRB5_CONFIG']
+ except Exception, e:
+ root_logger.error("Cannot obtain CA certificate\n%s", e)
+ return CLIENT_INSTALL_ERROR
+
# Now join the domain
(stdout, stderr, returncode) = run(join_args, raiseonerr=False, env=env, nolog=nolog)
@@ -1717,7 +2072,7 @@ def install(options, env, fstore, statestore):
# Add the CA to the default NSS database and trust it
try:
- run(["/usr/bin/certutil", "-A", "-d", "/etc/pki/nssdb", "-n", "IPA CA", "-t", "CT,C,C", "-a", "-i", "/etc/ipa/ca.crt"])
+ run(["/usr/bin/certutil", "-A", "-d", "/etc/pki/nssdb", "-n", "IPA CA", "-t", "CT,C,C", "-a", "-i", CACERT])
except CalledProcessError, e:
root_logger.info("Failed to add CA to the default NSS database.")
return CLIENT_INSTALL_ERROR
diff --git a/ipa-client/ipaclient/ipadiscovery.py b/ipa-client/ipaclient/ipadiscovery.py
index 2214a81ba..18b77a684 100644
--- a/ipa-client/ipaclient/ipadiscovery.py
+++ b/ipa-client/ipaclient/ipadiscovery.py
@@ -30,11 +30,14 @@ from ipapython.ipautil import run, CalledProcessError, valid_ip, get_ipa_basedn,
realm_to_suffix, format_netloc
from ipapython.dn import DN
+CACERT = '/etc/ipa/ca.crt'
+
NOT_FQDN = -1
NO_LDAP_SERVER = -2
REALM_NOT_FOUND = -3
NOT_IPA_SERVER = -4
NO_ACCESS_TO_LDAP = -5
+NO_TLS_LDAP = -6
BAD_HOST_CONFIG = -10
UNKNOWN_ERROR = -15
@@ -45,6 +48,7 @@ error_names = {
REALM_NOT_FOUND: 'REALM_NOT_FOUND',
NOT_IPA_SERVER: 'NOT_IPA_SERVER',
NO_ACCESS_TO_LDAP: 'NO_ACCESS_TO_LDAP',
+ NO_TLS_LDAP: 'NO_TLS_LDAP',
BAD_HOST_CONFIG: 'BAD_HOST_CONFIG',
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
}
@@ -135,7 +139,7 @@ class IPADiscovery(object):
domain = domain[p+1:]
return (None, None)
- def search(self, domain = "", server = "", hostname=None):
+ def search(self, domain = "", server = "", hostname=None, ca_cert_path=None):
root_logger.debug("[IPA Discovery]")
root_logger.debug(
'Starting IPA discovery with domain=%s, server=%s, hostname=%s',
@@ -224,14 +228,14 @@ class IPADiscovery(object):
ldapaccess = True
if self.server:
# check ldap now
- ldapret = self.ipacheckldap(self.server, self.realm)
+ ldapret = self.ipacheckldap(self.server, self.realm, ca_cert_path=ca_cert_path)
if ldapret[0] == 0:
self.server = ldapret[1]
self.realm = ldapret[2]
self.server_source = self.realm_source = (
'Discovered from LDAP DNS records in %s' % self.server)
- elif ldapret[0] == NO_ACCESS_TO_LDAP:
+ elif ldapret[0] == NO_ACCESS_TO_LDAP or ldapret[0] == NO_TLS_LDAP:
ldapaccess = False
# If one of LDAP servers checked rejects access (maybe anonymous
@@ -260,12 +264,10 @@ class IPADiscovery(object):
return ldapret[0]
- def ipacheckldap(self, thost, trealm):
+ def ipacheckldap(self, thost, trealm, ca_cert_path=None):
"""
Given a host and kerberos realm verify that it is an IPA LDAP
- server hosting the realm. The connection is an SSL connection
- so the remote IPA CA cert must be available at
- http://HOST/ipa/config/ca.crt
+ server hosting the realm.
Returns a list [errno, host, realm] or an empty list on error.
Errno is an error number:
@@ -279,31 +281,17 @@ class IPADiscovery(object):
i = 0
- # Get the CA certificate
- try:
- # Create TempDir
- temp_ca_dir = tempfile.mkdtemp()
- except OSError, e:
- raise RuntimeError("Creating temporary directory failed: %s" % str(e))
-
- try:
- run(["/usr/bin/wget", "-O", "%s/ca.crt" % temp_ca_dir, "-T", "15", "-t", "2",
- "http://%s/ipa/config/ca.crt" % format_netloc(thost)])
- except CalledProcessError, e:
- root_logger.error('Retrieving CA from %s failed', thost)
- root_logger.debug('Retrieving CA from %s failed: %s', thost, str(e))
- return [NOT_IPA_SERVER]
-
#now verify the server is really an IPA server
try:
ldap_url = "ldap://" + format_netloc(thost, 389)
root_logger.debug("Init LDAP connection with: %s", ldap_url)
lh = ldap.initialize(ldap_url)
- ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, True)
- ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, "%s/ca.crt" % temp_ca_dir)
+ if ca_cert_path:
+ ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, True)
+ ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_cert_path)
+ lh.set_option(ldap.OPT_X_TLS_DEMAND, True)
+ lh.start_tls_s()
lh.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
- lh.set_option(ldap.OPT_X_TLS_DEMAND, True)
- lh.start_tls_s()
lh.simple_bind_s("","")
# get IPA base DN
@@ -358,14 +346,16 @@ class IPADiscovery(object):
root_logger.debug("LDAP Error: Anonymous acces not allowed")
return [NO_ACCESS_TO_LDAP]
+ # We should only get UNWILLING_TO_PERFORM if the remote LDAP server
+ # has minssf > 0 and we have attempted a non-TLS connection.
+ if ca_cert_path is None and isinstance(err, ldap.UNWILLING_TO_PERFORM):
+ root_logger.debug("LDAP server returned UNWILLING_TO_PERFORM. This likely means that minssf is enabled")
+ return [NO_TLS_LDAP]
+
root_logger.error("LDAP Error: %s: %s" %
(err.args[0]['desc'], err.args[0].get('info', '')))
return [UNKNOWN_ERROR]
- finally:
- os.remove("%s/ca.crt" % temp_ca_dir)
- os.rmdir(temp_ca_dir)
-
def ipadns_search_srv(self, domain, srv_record_name, default_port,
break_on_first=True):
diff --git a/ipa-client/man/ipa-client-install.1 b/ipa-client/man/ipa-client-install.1
index abd74666e..35aea4e4a 100644
--- a/ipa-client/man/ipa-client-install.1
+++ b/ipa-client/man/ipa-client-install.1
@@ -97,6 +97,14 @@ Print debugging information to stdout
.TP
\fB\-U\fR, \fB\-\-unattended\fR
Unattended installation. The user will not be prompted.
+.TP
+\fB\-\-ca-cert-file\fR=\fICA_FILE\fR
+Do not attempt to acquire the IPA CA certificate via automated means,
+instead use the CA certificate found locally in in \fICA_FILE\fR. The
+\fICA_FILE\fR must be an absolute path to a PEM formatted certificate
+file. The CA certificate found in \fICA_FILE\fR is considered
+authoritative and will be installed without checking to see if it's
+valid for the IPA domain.
.SS "SSSD OPTIONS"
.TP