trust-fetch-domains: use custom krb5.conf overlay for all trust operations

Operations in FIPS mode make impossible use of NTLMSSP when
authenticating to trusted Active Directory domain controllers because
RC4 cipher is not allowed. Instead, Kerberos authentication have to be
used. We switched to enforce Kerberos authentication when communicating
with trusted domains' domain controllers everywhere.

Kerberos library uses system wide configuration which in IPA defaults to
resolving location of KDCs via DNS SRV records. Once trust is
established, SSSD will populate a list of closest DCs and provide them
through the KDC locator plugin. But at the time the trust is established
performing DNS SRV-based discovery of Kerberos KDCs might fail due to
multiple reasons. It might also succeed but point to a DC that doesn't
know about the account we have to use to establish trust.

One edge case is when DNS SRV record points to an unreachable DC,
whether due to a firewall or a network topology limitations. In such
case an administrator would pass --server <server> option to
'ipa trust-add' or 'ipa trust-fetch-domains' commands.

'ipa trust-fetch-domains' runs a helper via oddjobd. This helper was
already modified to support --server option and generated custom
krb5.conf overlay to pin to a specific AD DC. However, this
configuration was removed as soon as we finished talking to AD DCs.

With switch to always use Kebreros to authenticate in retrieval of the
topology information, we have to use the overlay everywhere as well.

Convert the code that generated the overlay file into a context that
generates the overlay and sets environment. Reuse it in other
trust-related places where this matters.

Oddjob helper runs as root and can write to /run/ipa for the krb5.conf
overlay.

Server side of 'ipa trust-add' code calls into ipaserver/dcerpc.py and
runs under ipaapi so can only write to /tmp.  Since it is a part of the
Apache instance, it uses private /tmp mounted on tmpfs.

Fixes: https://pagure.io/freeipa/issue/8664
Related: https://pagure.io/freeipa/issue/8655
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
This commit is contained in:
Alexander Bokovoy 2021-01-18 09:44:32 +02:00 committed by Rob Crittenden
parent 94242563d5
commit ae7cd4702d
3 changed files with 152 additions and 137 deletions

View File

@ -4,16 +4,12 @@ from ipaserver import dcerpc
from ipaserver.install.installutils import ScriptError
from ipapython import config, ipautil
from ipalib import api
from ipalib.facts import is_ipa_configured
from ipapython.dn import DN
from ipapython.dnsutil import DNSName
from ipaplatform.constants import constants
from ipaplatform.paths import paths
import io
import sys
import os
import tempfile
import textwrap
import six
import gssapi
@ -121,43 +117,6 @@ def get_forest_root_domain(api_instance, trusted_domain, server=None):
return remote_domain.info["dns_forest"]
def generate_krb5_config(realm, server):
"""Generate override krb5 config file for trusted domain DC access
:param realm: realm of the trusted AD domain
:param server: server to override KDC to
:returns: tuple (temporary config file name, KRB5_CONFIG string)
"""
cfg = paths.KRB5_CONF
tcfg = None
if server:
content = textwrap.dedent(u"""
[realms]
%s = {
kdc = %s
}
""") % (
realm.upper(),
server,
)
(fd, tcfg) = tempfile.mkstemp(dir="/run/ipa",
prefix="krb5conf", text=True)
with io.open(fd, mode='w', encoding='utf-8') as o:
o.write(content)
cfg = ":".join([tcfg, cfg])
return (tcfg, cfg)
if not is_ipa_configured():
# LSB status code 6: program is not configured
raise ScriptError(
"IPA is not configured "
+ "(see man pages of ipa-server-install for help)",
6,
)
if not os.getegid() == 0:
# LSB status code 4: user had insufficient privilege
raise ScriptError("You must be root to run ipactl.", 4)
@ -234,97 +193,95 @@ trusted_domain = trusted_domain_entry.single_value.get("cn").lower()
# At this point if we didn't find trusted forest name, an exception will be raised
# and script will quit. This is actually intended.
rc = 0
# Generate MIT Kerberos configuration file that potentially overlays
# the KDC to connect to for a trusted domain to allow --server option
# to take precedence.
cfg_file, cfg = generate_krb5_config(trusted_domain, options.server)
with ipautil.private_krb5_config(trusted_domain, options.server) as cfg_file:
if not (options.admin and options.password):
oneway_keytab_name = os.path.join("/var/lib/sss/keytabs/",
trusted_domain + ".keytab")
if not (options.admin and options.password):
oneway_keytab_name = "/var/lib/sss/keytabs/" + trusted_domain + ".keytab"
oneway_principal = str(
"%s$@%s" % (own_trust_flatname, trusted_domain.upper())
)
oneway_principal = str(
"%s$@%s" % (own_trust_flatname, trusted_domain.upper())
)
# If keytab does not exist, retrieve it
if not os.path.isfile(oneway_keytab_name):
retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
# If keytab does not exist, retrieve it
if not os.path.isfile(oneway_keytab_name):
retrieve_keytab(api, ccache_name,
oneway_keytab_name, oneway_principal)
try:
have_ccache = False
try:
# The keytab may have stale key material (from older trust-add run)
have_ccache = False
try:
# The keytab may have stale key material (from older trust-add run)
cred = kinit_keytab(
oneway_principal,
oneway_keytab_name,
oneway_ccache_name,
)
if cred.lifetime > 0:
have_ccache = True
except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
pass
if not have_ccache:
if os.path.exists(oneway_ccache_name):
os.unlink(oneway_ccache_name)
kinit_keytab(
oneway_principal,
oneway_keytab_name,
oneway_ccache_name,
)
except (gssapi.exceptions.GSSError, gssapi.raw.misc.GSSError):
# If there was failure on using keytab, assume it is stale and retrieve again
retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
if os.path.exists(oneway_ccache_name):
os.unlink(oneway_ccache_name)
cred = kinit_keytab(
oneway_principal,
oneway_keytab_name,
oneway_ccache_name,
config=cfg,
)
if cred.lifetime > 0:
have_ccache = True
except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
pass
if not have_ccache:
if os.path.exists(oneway_ccache_name):
os.unlink(oneway_ccache_name)
kinit_keytab(
oneway_principal,
oneway_keytab_name,
oneway_ccache_name,
config=cfg,
)
except (gssapi.exceptions.GSSError, gssapi.raw.misc.GSSError):
# If there was failure on using keytab, assume it is stale and retrieve again
retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
if os.path.exists(oneway_ccache_name):
os.unlink(oneway_ccache_name)
cred = kinit_keytab(
oneway_principal,
oneway_keytab_name,
else:
cred = kinit_password(
options.admin,
options.password,
oneway_ccache_name,
config=cfg,
canonicalize=True,
enterprise=True,
)
else:
cred = kinit_password(
options.admin,
options.password,
oneway_ccache_name,
canonicalize=True,
enterprise=True,
config=cfg,
if cred and cred.lifetime > 0:
have_ccache = True
if not have_ccache:
rc = 1
raise GeneratorExit
# We are done: we have ccache with TDO credentials and can fetch domains
ipa_domain = api.env.domain
os.environ["KRB5CCNAME"] = oneway_ccache_name
# retrieve the forest root domain name and contact it to retrieve trust
# topology info
forest_root = get_forest_root_domain(
api, trusted_domain, server=options.server
)
domains = dcerpc.fetch_domains(
api, ipa_domain, forest_root, creds=True, server=options.server
)
if cred and cred.lifetime > 0:
have_ccache = True
# We still need to use the override for KDC configuration in case the --server
# was forced, thus only switch to the old ccache.
if old_ccache:
os.environ["KRB5CCNAME"] = old_ccache
if not have_ccache:
sys.exit(1)
# We are done: we have ccache with TDO credentials and can fetch domains
ipa_domain = api.env.domain
os.environ["KRB5CCNAME"] = oneway_ccache_name
os.environ["KRB5_CONFIG"] = cfg
trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)[
"result"
]
# retrieve the forest root domain name and contact it to retrieve trust
# topology info
forest_root = get_forest_root_domain(
api, trusted_domain, server=options.server
)
domains = dcerpc.fetch_domains(
api, ipa_domain, forest_root, creds=True, server=options.server
)
trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
if old_ccache:
os.environ["KRB5CCNAME"] = old_ccache
if old_config:
os.environ["KRB5_CONFIG"] = old_config
if cfg_file:
os.remove(cfg_file)
trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)[
"result"
]
trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
sys.exit(0)
sys.exit(rc)

View File

@ -36,6 +36,8 @@ import re
import datetime
import netaddr
import time
import textwrap
import io
from contextlib import contextmanager
import locale
import collections
@ -1447,6 +1449,54 @@ def private_ccache(path=None):
pass
@contextmanager
def private_krb5_config(realm, server, dir="/run/ipa"):
"""Generate override krb5 config file for a trusted domain DC access
Provide a context where environment variable KRB5_CONFIG is set
with the overlay on top of paths.KRB5_CONF. Overlay's file path
is passed to the context in case it is needed for something else
:param realm: realm of the trusted AD domain
:param server: server to override KDC to
:param dir: path where to create a temporary krb5.conf overlay
"""
cfg = paths.KRB5_CONF
tcfg = None
if server:
content = textwrap.dedent(u"""
[realms]
%s = {
kdc = %s
}
""") % (
realm.upper(),
server,
)
(fd, tcfg) = tempfile.mkstemp(dir=dir, prefix="krb5conf", text=True)
with io.open(fd, mode='w', encoding='utf-8') as o:
o.write(content)
cfg = ":".join([tcfg, cfg])
original_value = os.environ.get('KRB5_CONFIG', None)
os.environ['KRB5_CONFIG'] = cfg
try:
yield tcfg
except GeneratorExit:
pass
finally:
if original_value is not None:
os.environ['KRB5_CONFIG'] = original_value
else:
os.environ.pop('KRB5_CONFIG', None)
if tcfg is not None and os.path.exists(tcfg):
os.remove(tcfg)
if six.PY2:
def fsdecode(value):
"""

View File

@ -1698,20 +1698,23 @@ def retrieve_remote_domain(hostname, local_flatname,
error=_('Non-Kerberos user name was specified, '
'please provide user@REALM variant instead'))
realm_admin = r"%s@%s" % (
realm_admin, rd.info['dns_forest'].upper())
realm_admin, rd.info['dns_domain'].upper())
realm = rd.info['dns_domain'].upper()
auth_string = r"%s%%%s" \
% (realm_admin, realm_passwd)
td = get_instance(local_flatname)
td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
enforce_smb_encryption(td.creds)
td.creds.parse_string(auth_string)
td.creds.set_workstation(hostname)
if realm_server is None:
# we must have rd.info['dns_hostname'] then
# as it is part of the anonymous discovery
td.retrieve(rd.info['dns_hostname'])
else:
td.retrieve(realm_server)
with ipautil.private_krb5_config(realm, realm_server, dir='/tmp'):
with ipautil.private_ccache():
td = get_instance(local_flatname)
td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
enforce_smb_encryption(td.creds)
td.creds.parse_string(auth_string)
td.creds.set_workstation(hostname)
if realm_server is None:
# we must have rd.info['dns_hostname'] then
# as it is part of the anonymous discovery
td.retrieve(rd.info['dns_hostname'])
else:
td.retrieve(realm_server)
td.read_only = False
return td
@ -1832,15 +1835,18 @@ class TrustDomainJoins:
# Establishing trust may throw an exception for topology
# conflict. If it was solved, re-establish the trust again
# Otherwise let the CLI to display a message about the conflict
try:
self.remote_domain.establish_trust(self.local_domain,
trustdom_pass,
trust_type, trust_external)
except TrustTopologyConflictSolved:
# we solved topology conflict, retry again
self.remote_domain.establish_trust(self.local_domain,
trustdom_pass,
trust_type, trust_external)
with ipautil.private_krb5_config(realm, realm_server, dir='/tmp'):
try:
self.remote_domain.establish_trust(self.local_domain,
trustdom_pass,
trust_type,
trust_external)
except TrustTopologyConflictSolved:
# we solved topology conflict, retry again
self.remote_domain.establish_trust(self.local_domain,
trustdom_pass,
trust_type,
trust_external)
try:
self.local_domain.establish_trust(self.remote_domain,
@ -1856,7 +1862,9 @@ class TrustDomainJoins:
# it only does verification for outbound trusts.
result = True
if trust_type == TRUST_BIDIRECTIONAL:
result = self.remote_domain.verify_trust(self.local_domain)
with ipautil.private_krb5_config(realm,
realm_server, dir='/tmp'):
result = self.remote_domain.verify_trust(self.local_domain)
return dict(
local=self.local_domain,
remote=self.remote_domain,