From 82eca6c0a994c4db8f85ea0d5c012cd4d80edefe Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Mar 13 2024 08:30:26 +0000 Subject: ipa-pwd-extop: allow enforcing 2FA-only over LDAP bind When authentication indicators were introduced in 2016, ipa-pwd-extop plugin gained ability to reject LDAP BIND when an LDAP client insists the authentication must use an OTP token. This is used by ipa-otpd to ensure Kerberos authentication using OTP method is done with at least two factors (the token and the password). This enfrocement is only possible when an LDAP client sends the LDAP control. There are cases when LDAP clients cannot be configured to send a custom LDAP control during BIND operation. For these clients an LDAP BIND against an account that only has password and no valid token would succeed even if admins intend it to fail. Ability to do LDAP BIND without a token was added to allow users to add their own OTP tokens securely. If administrators require full enforcement over LDAP BIND, it is cannot be achieved with LDAP without sending the LDAP control to do so. Add IPA configuration string, EnforceLDAPOTP, to allow administrators to prevent LDAP BIND with a password only if user is required to have OTP tokens. With this configuration enabled, it will be not possible for users to add OTP token if one is missing, thus ensuring no user can authenticate without OTP and admins will have to add initial OTP tokens to users explicitly. Fixes: https://pagure.io/freeipa/issue/5169 Signed-off-by: Alexander Bokovoy Reviewed-By: Florence Blanc-Renaud --- diff --git a/API.txt b/API.txt index 7d91077..5ed1f53 100644 --- a/API.txt +++ b/API.txt @@ -1082,7 +1082,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('ca_renewal_master_server?', autofill=False) option: Str('delattr*', cli_name='delattr') option: Flag('enable_sid?', autofill=True, default=False) -option: StrEnum('ipaconfigstring*', autofill=False, cli_name='ipaconfigstring', values=[u'AllowNThash', u'KDC:Disable Last Success', u'KDC:Disable Lockout', u'KDC:Disable Default Preauth for SPNs']) +option: StrEnum('ipaconfigstring*', autofill=False, cli_name='ipaconfigstring', values=[u'AllowNThash', u'KDC:Disable Last Success', u'KDC:Disable Lockout', u'KDC:Disable Default Preauth for SPNs', u'EnforceLDAPOTP']) option: Str('ipadefaultemaildomain?', autofill=False, cli_name='emaildomain') option: Str('ipadefaultloginshell?', autofill=False, cli_name='defaultshell') option: Str('ipadefaultprimarygroup?', autofill=False, cli_name='defaultgroup') diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c index d30764b..1355f20 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c @@ -83,6 +83,7 @@ static struct ipapwd_krbcfg *ipapwd_getConfig(void) char *tmpstr; int ret; size_t i; + bool fips_enabled = false; config = calloc(1, sizeof(struct ipapwd_krbcfg)); if (!config) { @@ -241,28 +242,35 @@ static struct ipapwd_krbcfg *ipapwd_getConfig(void) config->allow_nt_hash = false; if (ipapwd_fips_enabled()) { LOG("FIPS mode is enabled, NT hashes are not allowed.\n"); + fips_enabled = true; + } + + sdn = slapi_sdn_new_dn_byval(ipa_etc_config_dn); + ret = ipapwd_getEntry(sdn, &config_entry, NULL); + slapi_sdn_free(&sdn); + if (ret != LDAP_SUCCESS) { + LOG_FATAL("No config Entry?\n"); + goto free_and_error; } else { - sdn = slapi_sdn_new_dn_byval(ipa_etc_config_dn); - ret = ipapwd_getEntry(sdn, &config_entry, NULL); - slapi_sdn_free(&sdn); - if (ret != LDAP_SUCCESS) { - LOG_FATAL("No config Entry?\n"); - goto free_and_error; - } else { - tmparray = slapi_entry_attr_get_charray(config_entry, - "ipaConfigString"); - for (i = 0; tmparray && tmparray[i]; i++) { + tmparray = slapi_entry_attr_get_charray(config_entry, + "ipaConfigString"); + for (i = 0; tmparray && tmparray[i]; i++) { + if (strcasecmp(tmparray[i], "EnforceLDAPOTP") == 0) { + config->enforce_ldap_otp = true; + continue; + } + if (!fips_enabled) { if (strcasecmp(tmparray[i], "AllowNThash") == 0) { config->allow_nt_hash = true; continue; } } - if (tmparray) slapi_ch_array_free(tmparray); } - - slapi_entry_free(config_entry); + if (tmparray) slapi_ch_array_free(tmparray); } + slapi_entry_free(config_entry); + return config; free_and_error: @@ -571,6 +579,13 @@ int ipapwd_gen_checks(Slapi_PBlock *pb, char **errMesg, rc = LDAP_OPERATIONS_ERROR; } + /* do not return the master key if asked */ + if (check_flags & IPAPWD_CHECK_ONLY_CONFIG) { + free((*config)->kmkey->contents); + free((*config)->kmkey); + (*config)->kmkey = NULL; + } + done: return rc; } @@ -1103,8 +1118,10 @@ void free_ipapwd_krbcfg(struct ipapwd_krbcfg **cfg) krb5_free_default_realm(c->krbctx, c->realm); krb5_free_context(c->krbctx); - free(c->kmkey->contents); - free(c->kmkey); + if (c->kmkey) { + free(c->kmkey->contents); + free(c->kmkey); + } free(c->supp_encsalts); free(c->pref_encsalts); slapi_ch_array_free(c->passsync_mgrs); diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h index 79606a8..9769700 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h @@ -70,6 +70,7 @@ #define IPAPWD_CHECK_CONN_SECURE 0x00000001 #define IPAPWD_CHECK_DN 0x00000002 +#define IPAPWD_CHECK_ONLY_CONFIG 0x00000004 #define IPA_CHANGETYPE_NORMAL 0 #define IPA_CHANGETYPE_ADMIN 1 @@ -109,6 +110,7 @@ struct ipapwd_krbcfg { char **passsync_mgrs; int num_passsync_mgrs; bool allow_nt_hash; + bool enforce_ldap_otp; }; int ipapwd_entry_checks(Slapi_PBlock *pb, struct slapi_entry *e, diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c index 6898e65..6902351 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c @@ -1431,6 +1431,7 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb) "krbPasswordExpiration", "krblastpwchange", NULL }; + struct ipapwd_krbcfg *krbcfg = NULL; struct berval *credentials = NULL; Slapi_Entry *entry = NULL; Slapi_DN *target_sdn = NULL; @@ -1505,6 +1506,18 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb) /* Try to do OTP first. */ syncreq = otpctrl_present(pb, OTP_SYNC_REQUEST_OID); otpreq = otpctrl_present(pb, OTP_REQUIRED_OID); + if (!syncreq && !otpreq) { + ret = ipapwd_gen_checks(pb, &errMesg, &krbcfg, IPAPWD_CHECK_ONLY_CONFIG); + if (ret != 0) { + LOG_FATAL("ipapwd_gen_checks failed!?\n"); + slapi_entry_free(entry); + slapi_sdn_free(&sdn); + return 0; + } + if (krbcfg->enforce_ldap_otp) { + otpreq = true; + } + } if (!syncreq && !ipapwd_pre_bind_otp(dn, entry, credentials, otpreq)) goto invalid_creds; @@ -1543,6 +1556,7 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb) return 0; invalid_creds: + free_ipapwd_krbcfg(&krbcfg); slapi_entry_free(entry); slapi_sdn_free(&sdn); slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL); diff --git a/doc/api/config_mod.md b/doc/api/config_mod.md index c479a03..b3203c3 100644 --- a/doc/api/config_mod.md +++ b/doc/api/config_mod.md @@ -27,7 +27,7 @@ No arguments. * ipauserobjectclasses : :ref:`Str` * ipapwdexpadvnotify : :ref:`Int` * ipaconfigstring : :ref:`StrEnum` - * Values: ('AllowNThash', 'KDC:Disable Last Success', 'KDC:Disable Lockout', 'KDC:Disable Default Preauth for SPNs') + * Values: ('AllowNThash', 'KDC:Disable Last Success', 'KDC:Disable Lockout', 'KDC:Disable Default Preauth for SPNs', 'EnforceLDAPOTP') * ipaselinuxusermaporder : :ref:`Str` * ipaselinuxusermapdefault : :ref:`Str` * ipakrbauthzdata : :ref:`StrEnum` diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py index eface54..45bd0c1 100644 --- a/ipaserver/plugins/config.py +++ b/ipaserver/plugins/config.py @@ -247,7 +247,8 @@ class config(LDAPObject): doc=_('Extra hashes to generate in password plug-in'), values=(u'AllowNThash', u'KDC:Disable Last Success', u'KDC:Disable Lockout', - u'KDC:Disable Default Preauth for SPNs'), + u'KDC:Disable Default Preauth for SPNs', + u'EnforceLDAPOTP'), ), Str('ipaselinuxusermaporder', label=_('SELinux user map order'), diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py index 8e2ea56..d2dfca4 100644 --- a/ipatests/test_integration/test_otp.py +++ b/ipatests/test_integration/test_otp.py @@ -21,6 +21,9 @@ from ipaplatform.paths import paths from ipatests.pytest_ipa.integration import tasks from ipapython.dn import DN +from ldap.controls.simple import BooleanControl + +from ipalib import errors PASSWORD = "DummyPassword123" USER = "opttestuser" @@ -450,3 +453,46 @@ class TestOTPToken(IntegrationTest): assert "ipa-otpd" not in failed_services.stdout_text finally: del_otptoken(self.master, otpuid) + + def test_totp_ldap(self): + master = self.master + basedn = master.domain.basedn + USER1 = 'user-forced-otp' + binddn = DN(f"uid={USER1},cn=users,cn=accounts,{basedn}") + + tasks.create_active_user(master, USER1, PASSWORD) + tasks.kinit_admin(master) + # Enforce use of OTP token for this user + master.run_command(['ipa', 'user-mod', USER1, + '--user-auth-type=otp']) + try: + conn = master.ldap_connect() + # First, attempt authenticating with a password but without LDAP + # control to enforce OTP presence and without server-side + # enforcement of the OTP presence check. + conn.simple_bind(binddn, f"{PASSWORD}") + # Add an OTP token now + otpuid, totp = add_otptoken(master, USER1, otptype="totp") + # Next, enforce Password+OTP for a user with OTP token + master.run_command(['ipa', 'config-mod', '--addattr', + 'ipaconfigstring=EnforceLDAPOTP']) + # Next, authenticate with Password+OTP and with the LDAP control + # this operation should succeed + otpvalue = totp.generate(int(time.time())).decode("ascii") + conn.simple_bind(binddn, f"{PASSWORD}{otpvalue}", + client_controls=[ + BooleanControl( + controlType="2.16.840.1.113730.3.8.10.7", + booleanValue=True)]) + # Remove token + del_otptoken(self.master, otpuid) + # Now, try to authenticate without otp and without control + # this operation should fail + try: + conn.simple_bind(binddn, f"{PASSWORD}") + except errors.ACIError: + pass + master.run_command(['ipa', 'config-mod', '--delattr', + 'ipaconfigstring=EnforceLDAPOTP']) + finally: + master.run_command(['ipa', 'user-del', USER1])