mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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>
This commit is contained in:
committed by
Rob Crittenden
parent
91f4af7e6a
commit
a1991aeac1
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1682,6 +1682,23 @@ class ProtectedEntryError(ExecutionError):
|
||||
format = _('%(label)s %(key)s cannot be deleted/modified: %(reason)s')
|
||||
|
||||
|
||||
class CertificateInvalidError(CertificateError):
|
||||
"""
|
||||
**4310** Raised when a certificate is not valid
|
||||
|
||||
For example:
|
||||
|
||||
>>> raise CertificateInvalidError(name=_(u'CA'))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
CertificateInvalidError: CA certificate is not valid
|
||||
|
||||
"""
|
||||
|
||||
errno = 4310
|
||||
format = _('%(name)s certificate is not valid')
|
||||
|
||||
|
||||
##############################################################################
|
||||
# 5000 - 5999: Generic errors
|
||||
|
||||
|
||||
@@ -1174,3 +1174,39 @@ def restore_hostname(statestore):
|
||||
run(['/bin/hostname', old_hostname])
|
||||
except CalledProcessError, e:
|
||||
print >>sys.stderr, "Failed to set this machine hostname back to %s: %s" % (old_hostname, str(e))
|
||||
|
||||
def convert_ldap_error(exc):
|
||||
"""
|
||||
Make LDAP exceptions prettier.
|
||||
|
||||
Some LDAP exceptions have a dict with descriptive information, if
|
||||
this exception has a dict extract useful information from it and
|
||||
format it into something usable and return that. If the LDAP
|
||||
exception does not have an information dict then return the name
|
||||
of the LDAP exception.
|
||||
|
||||
If the exception is not an LDAP exception then convert the
|
||||
exception to a string and return that instead.
|
||||
"""
|
||||
if isinstance(exc, ldap.LDAPError):
|
||||
name = exc.__class__.__name__
|
||||
|
||||
if len(exc.args):
|
||||
d = exc.args[0]
|
||||
if isinstance(d, dict):
|
||||
desc = d.get('desc', '').strip()
|
||||
info = d.get('info', '').strip()
|
||||
if desc and info:
|
||||
return '%s %s' % (desc, info)
|
||||
elif desc:
|
||||
return desc
|
||||
elif info:
|
||||
return info
|
||||
else:
|
||||
return name
|
||||
else:
|
||||
return name
|
||||
else:
|
||||
return name
|
||||
else:
|
||||
return str(exc)
|
||||
|
||||
@@ -227,7 +227,10 @@ class CertDB(object):
|
||||
if not subject_base:
|
||||
self.subject_base = DN(('O', 'IPA'))
|
||||
|
||||
self.cacert_name = get_ca_nickname(self.realm)
|
||||
if self.self_signed_ca:
|
||||
self.cacert_name = get_ca_nickname(self.realm, 'CN=%s Certificate Authority')
|
||||
else:
|
||||
self.cacert_name = get_ca_nickname(self.realm)
|
||||
self.valid_months = "120"
|
||||
self.keysize = "1024"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user