LDAP autobind authenticateAsDN for BIND named

Use new nsslapd-ldapiautoauthdnattr feature to switch BIND named from
GSSAPI bind to EXTERNAL LDAPI bind.

Requires 389-DS >= 1.4.4.11 or >= 2.0.2

Fixes: https://pagure.io/freeipa/issue/8544
See: https://github.com/389ds/389-ds-base/issues/4381
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Francois Cami <fcami@redhat.com>
This commit is contained in:
Christian Heimes 2020-12-04 12:18:22 +01:00 committed by Alexander Bokovoy
parent 6434968a7c
commit 16e1cbdc5e
10 changed files with 143 additions and 12 deletions

View File

@ -57,6 +57,5 @@ dyndb "ipa" "$BIND_LDAP_SO" {
base "cn=dns,$SUFFIX"; base "cn=dns,$SUFFIX";
server_id "$FQDN"; server_id "$FQDN";
auth_method "sasl"; auth_method "sasl";
sasl_mech "GSSAPI"; sasl_mech "EXTERNAL";
sasl_user "DNS/$FQDN";
}; };

View File

@ -1,6 +1,14 @@
# countainer for autobind, already exists in new 389-DS instances
dn: cn=auto_bind,cn=config
default: objectClass: nsContainer
default: objectClass: top
default: cn: auto_bind
# map LDAPI autobind uid/gid to user entries (not used by root autobind) # map LDAPI autobind uid/gid to user entries (not used by root autobind)
dn: cn=config dn: cn=config
only: nsslapd-ldapimaptoentries: on only: nsslapd-ldapimaptoentries: on
only: nsslapd-ldapientrysearchbase: cn=auto_bind,cn=config
only: nsslapd-ldapidnmappingbase: cn=auto_bind,cn=config
# lib389 configures 389-DS for root-autobind. This entry is no longer needed. # lib389 configures 389-DS for root-autobind. This entry is no longer needed.
dn: cn=root-autobind,cn=config dn: cn=root-autobind,cn=config

View File

@ -0,0 +1,7 @@
# create / update LDAPI autobind rules for services
# also used by ipa-restore to ensure correct mappings
# NOTE: ipa-ldap-updater runs reload ldapi mappings tasks
dn: cn=named,cn=auto_bind,cn=config
onlyifexist: uidNumber: $NAMED_UID
onlyifexist: gidNumber: $NAMED_GID

View File

@ -45,6 +45,7 @@ app_DATA = \
41-caacl.update \ 41-caacl.update \
41-lightweight-cas.update \ 41-lightweight-cas.update \
45-roles.update \ 45-roles.update \
49-autobind-services.update \
50-7_bit_check.update \ 50-7_bit_check.update \
50-dogtag10-migration.update \ 50-dogtag10-migration.update \
50-groupuuid.update \ 50-groupuuid.update \

View File

@ -763,6 +763,8 @@ class LDAPClient:
'nsslapd-enable-upgrade-hash': True, 'nsslapd-enable-upgrade-hash': True,
'nsslapd-db-locks': True, 'nsslapd-db-locks': True,
'nsslapd-logging-hr-timestamps-enabled': True, 'nsslapd-logging-hr-timestamps-enabled': True,
'nsslapd-ldapientrysearchbase': True,
'nsslapd-ldapidnmappingbase': True,
}) })
time_limit = -1.0 # unlimited time_limit = -1.0 # unlimited

View File

@ -760,8 +760,11 @@ class BindInstance(service.Service):
self.step("setting up records for other masters", self.__add_others) self.step("setting up records for other masters", self.__add_others)
# all zones must be created before this step # all zones must be created before this step
self.step("adding NS record to the zones", self.__add_self_ns) self.step("adding NS record to the zones", self.__add_self_ns)
# The service entry is used for LDAPI autobind. The keytab is no
# longer used to authenticate the server. The server still needs
# the keytab to handle incoming nsupdate requests with TSIG.
self.step("setting up kerberos principal", self.__setup_principal) self.step("setting up kerberos principal", self.__setup_principal)
self.step("setting up LDAPI autobind", self.setup_autobind)
self.step("setting up named.conf", self.setup_named_conf) self.step("setting up named.conf", self.setup_named_conf)
self.step("setting up server configuration", self.step("setting up server configuration",
self.__setup_server_configuration) self.__setup_server_configuration)
@ -1029,6 +1032,11 @@ class BindInstance(service.Service):
dns_principal, str(e)) dns_principal, str(e))
raise raise
def setup_autobind(self):
self.add_autobind_entry(
constants.NAMED_USER, constants.NAMED_GROUP, self.principal
)
def setup_named_conf(self, backup=False): def setup_named_conf(self, backup=False):
"""Create, update, or migrate named configuration files """Create, update, or migrate named configuration files

View File

@ -40,7 +40,7 @@ from ipapython import admintool, certdb
from ipapython.dn import DN from ipapython.dn import DN
from ipaserver.install.replication import (wait_for_task, ReplicationManager, from ipaserver.install.replication import (wait_for_task, ReplicationManager,
get_cs_replication_manager) get_cs_replication_manager)
from ipaserver.install import installutils from ipaserver.install import installutils, ldapupdate
from ipaserver.install import dsinstance, httpinstance, cainstance, krbinstance from ipaserver.install import dsinstance, httpinstance, cainstance, krbinstance
from ipaserver.masters import get_masters from ipaserver.masters import get_masters
from ipapython import ipaldap from ipapython import ipaldap
@ -467,6 +467,12 @@ class Restore(admintool.AdminTool):
oddjobd.enable() oddjobd.enable()
oddjobd.start() oddjobd.start()
http.remove_httpd_ccaches() http.remove_httpd_ccaches()
# update autobind configuration in case uid/gid have changed
ld = ldapupdate.LDAPUpdate(api=api)
autobind_update = os.path.join(
paths.UPDATES_DIR, "49-autobind-services.update"
)
ld.update([autobind_update])
# have the daemons pick up their restored configs # have the daemons pick up their restored configs
tasks.systemd_daemon_reload() tasks.systemd_daemon_reload()
# Restart IPA a final time. # Restart IPA a final time.

View File

@ -34,7 +34,6 @@ import warnings
import six import six
from ipaserver.install import installutils
from ipapython import ipautil, ipaldap from ipapython import ipautil, ipaldap
from ipalib import errors from ipalib import errors
from ipalib import api, create_api from ipalib import api, create_api
@ -43,6 +42,7 @@ from ipaplatform.constants import constants as platformconstants
from ipaplatform.paths import paths from ipaplatform.paths import paths
from ipaplatform.tasks import tasks from ipaplatform.tasks import tasks
from ipapython.dn import DN from ipapython.dn import DN
from ipaserver.install import installutils, replication
if six.PY3: if six.PY3:
unicode = str unicode = str
@ -137,6 +137,35 @@ def safe_output(attr, values):
_sentinel = object() _sentinel = object()
def run_ldapi_reload_task(conn):
"""Create and wait for reload ldapi mappings task
:param conn: ldap2 connection
:return: exitcode
"""
task_cn = "reload_{}".format(int(time.time()))
task_dn = DN(
('cn', task_cn), ('cn', 'reload ldapi mappings'),
('cn', 'tasks'), ('cn', 'config')
)
entry = conn.make_entry(
task_dn,
objectClass=['top', 'extensibleObject'],
cn=[task_cn],
ttl=[10],
)
logger.debug('Creating reload task %s', task_dn)
conn.add_entry(entry)
# task usually finishes in a few ms, avoid 1 sec delay in wait_for_task
time.sleep(0.1)
exitcode = replication.wait_for_task(api.Backend.ldap2, task_dn)
logger.debug(
'Task %s has finished with exit code %i',
task_dn, exitcode
)
return exitcode
class LDAPUpdate: class LDAPUpdate:
action_keywords = { action_keywords = {
"default", "add", "remove", "only", "onlyifexist", "deleteentry", "default", "add", "remove", "only", "onlyifexist", "deleteentry",
@ -146,6 +175,7 @@ class LDAPUpdate:
('cn', 'index'), ('cn', 'userRoot'), ('cn', 'ldbm database'), ('cn', 'index'), ('cn', 'userRoot'), ('cn', 'ldbm database'),
('cn', 'plugins'), ('cn', 'config') ('cn', 'plugins'), ('cn', 'config')
) )
ldapi_autobind_suffix = DN(('cn', 'auto_bind'), ('cn', 'config'))
def __init__(self, dm_password=_sentinel, sub_dict=None, def __init__(self, dm_password=_sentinel, sub_dict=None,
online=_sentinel, ldapi=_sentinel, api=api): online=_sentinel, ldapi=_sentinel, api=api):
@ -296,6 +326,9 @@ class LDAPUpdate:
SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT, SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT,
SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER, SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER,
FIPS="#" if tasks.is_fips_enabled() else "", FIPS="#" if tasks.is_fips_enabled() else "",
# uid / gid for autobind
NAMED_UID=platformconstants.NAMED_USER.uid,
NAMED_GID=platformconstants.NAMED_GROUP.gid,
) )
for k, v in default_sub.items(): for k, v in default_sub.items():
self.sub_dict.setdefault(k, v) self.sub_dict.setdefault(k, v)
@ -889,6 +922,7 @@ class LDAPUpdate:
def _run_updates(self, all_updates): def _run_updates(self, all_updates):
index_attributes = set() index_attributes = set()
update_ldapi_mappings = False
for update in all_updates: for update in all_updates:
if 'deleteentry' in update: if 'deleteentry' in update:
self._delete_record(update) self._delete_record(update)
@ -896,8 +930,16 @@ class LDAPUpdate:
self._run_update_plugin(update['plugin']) self._run_update_plugin(update['plugin'])
else: else:
entry, modified = self._update_record(update) entry, modified = self._update_record(update)
if modified and entry.dn.endswith(self.index_suffix): if modified:
index_attributes.add(entry.single_value['cn']) if entry.dn.endswith(self.index_suffix):
index_attributes.add(entry.single_value['cn'])
if (
entry.dn.endswith(self.ldapi_autobind_suffix)
and "nsLDAPIFixedAuthMap" in entry.get(
"objectClass", ()
)
):
update_ldapi_mappings = True
if index_attributes: if index_attributes:
# The LDAPUpdate framework now keeps record of all changed/added # The LDAPUpdate framework now keeps record of all changed/added
@ -907,6 +949,10 @@ class LDAPUpdate:
task_dn = self.create_index_task(*sorted(index_attributes)) task_dn = self.create_index_task(*sorted(index_attributes))
self.monitor_index_task(task_dn) self.monitor_index_task(task_dn)
if update_ldapi_mappings:
# update mappings when any autobind entry is added or modified
run_ldapi_reload_task(self.conn)
def update(self, files, ordered=True): def update(self, files, ordered=True):
"""Execute the update. files is a list of the update files to use. """Execute the update. files is a list of the update files to use.
:param ordered: Update files are executed in alphabetical order :param ordered: Update files are executed in alphabetical order

View File

@ -1454,6 +1454,9 @@ def upgrade_bind(fstore):
else: else:
bind_started = False bind_started = False
# create or update autobind entry
bind.setup_autobind()
try: try:
changed = bind.setup_named_conf(backup=True) changed = bind.setup_named_conf(backup=True)
if changed: if changed:

View File

@ -42,7 +42,7 @@ from ipaserver.masters import (
CONFIGURED_SERVICE, ENABLED_SERVICE, HIDDEN_SERVICE, SERVICE_LIST CONFIGURED_SERVICE, ENABLED_SERVICE, HIDDEN_SERVICE, SERVICE_LIST
) )
from ipaserver.servroles import HIDDEN from ipaserver.servroles import HIDDEN
from ipaserver.install.ldapupdate import LDAPUpdate from ipaserver.install.ldapupdate import LDAPUpdate, run_ldapi_reload_task
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -332,6 +332,15 @@ class Service:
kerberos.Principal( kerberos.Principal(
(self.service_prefix, self.fqdn), realm=self.realm)) (self.service_prefix, self.fqdn), realm=self.realm))
def get_principal_dn(self, principal=None):
if principal is None:
principal = self.principal
return DN(
('krbprincipalname', principal),
self.api.env.container_service,
self.suffix
)
def _ldap_update(self, filenames, *, basedir=paths.UPDATES_DIR): def _ldap_update(self, filenames, *, basedir=paths.UPDATES_DIR):
"""Apply update ldif files """Apply update ldif files
@ -419,7 +428,7 @@ class Service:
# This can happen when installing a replica # This can happen when installing a replica
return None return None
entry.pop('krbpwdpolicyreference', None) # don't copy virtual attr entry.pop('krbpwdpolicyreference', None) # don't copy virtual attr
newdn = DN(('krbprincipalname', principal), ('cn', 'services'), ('cn', 'accounts'), self.suffix) newdn = self.get_principal_dn(principal)
hostdn = DN(('fqdn', self.fqdn), ('cn', 'computers'), ('cn', 'accounts'), self.suffix) hostdn = DN(('fqdn', self.fqdn), ('cn', 'computers'), ('cn', 'accounts'), self.suffix)
api.Backend.ldap2.delete_entry(entry) api.Backend.ldap2.delete_entry(entry)
entry.dn = newdn entry.dn = newdn
@ -437,7 +446,7 @@ class Service:
The principal needs to be fully-formed: service/host@REALM The principal needs to be fully-formed: service/host@REALM
""" """
dn = DN(('krbprincipalname', principal), ('cn', 'services'), ('cn', 'accounts'), self.suffix) dn = self.get_principal_dn(principal)
hostdn = DN(('fqdn', self.fqdn), ('cn', 'computers'), ('cn', 'accounts'), self.suffix) hostdn = DN(('fqdn', self.fqdn), ('cn', 'computers'), ('cn', 'accounts'), self.suffix)
entry = api.Backend.ldap2.make_entry( entry = api.Backend.ldap2.make_entry(
dn, dn,
@ -451,6 +460,49 @@ class Service:
api.Backend.ldap2.add_entry(entry) api.Backend.ldap2.add_entry(entry)
return dn return dn
def add_autobind_entry(self, user, group, principal):
"""Add or update LDAPI autobind entry to map uid/gid to principal
:param user: ipaplatform User object
:param group: ipaplatform Group object
:param principal: service principal to bind as
:return: dn of new autobind entry
"""
authdn = self.get_principal_dn(principal)
dn = DN(
("cn", self.service_name), ("cn", "auto_bind"), ("cn", "config")
)
settings = {
"uidNumber": [user.uid],
"gidNumber": [group.gid],
"nsslapd-authenticateAsDN": [authdn]
}
ldap2 = self.api.Backend.ldap2
try:
entry = ldap2.get_entry(dn)
except errors.NotFound:
entry = ldap2.make_entry(
dn,
objectclass=["top", "nsLDAPIFixedAuthMap"],
cn=[self.service_name],
**settings,
)
ldap2.add_entry(entry)
logger.debug("Created autobind entry %s", dn)
else:
entry.update(settings)
try:
ldap2.update_entry(entry)
except errors.EmptyModlist:
logger.debug("Autobind entry %s already configured", dn)
else:
logger.debug("Updated autobind entry %s", dn)
# refresh LDAPI mappings
run_ldapi_reload_task(self.api.Backend.ldap2)
return dn
def add_cert_to_service(self): def add_cert_to_service(self):
""" """
Add a certificate to a service Add a certificate to a service
@ -459,8 +511,7 @@ class Service:
""" """
if self.cert is None: if self.cert is None:
raise ValueError("{} has no cert".format(self.service_name)) raise ValueError("{} has no cert".format(self.service_name))
dn = DN(('krbprincipalname', self.principal), ('cn', 'services'), dn = self.get_principal_dn()
('cn', 'accounts'), self.suffix)
entry = api.Backend.ldap2.get_entry(dn) entry = api.Backend.ldap2.get_entry(dn)
entry.setdefault('userCertificate', []).append(self.cert) entry.setdefault('userCertificate', []).append(self.cert)
try: try: