From 82eca6c0a994c4db8f85ea0d5c012cd4d80edefe Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 30 Jan 2024 11:17:27 +0200 Subject: [PATCH] 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 --- API.txt | 2 +- .../ipa-slapi-plugins/ipa-pwd-extop/common.c | 47 +++++++++++++------ .../ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h | 2 + .../ipa-slapi-plugins/ipa-pwd-extop/prepost.c | 14 ++++++ doc/api/config_mod.md | 2 +- ipaserver/plugins/config.py | 3 +- ipatests/test_integration/test_otp.py | 46 ++++++++++++++++++ 7 files changed, 98 insertions(+), 18 deletions(-) diff --git a/API.txt b/API.txt index 7d91077fc340ababee5c9a4b8a695290728b9135..5ed1f5327d9154bf2b301a781b723213c7677ed9 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 d30764bb2a05c7ca4a33ea114a2dc19af39e216f..1355f20d3ab990c81b5b41875d659a9bc9f97085 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 79606a8c795d166590c4655f9021aa414c3684d9..97697000674d8fbbe3a924af63261482db173852 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 6898e6596e1cbbb2cc69ba592401619ce86899d8..69023515018d522651bccb984ddd8e9174c22f59 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 c479a034416068c72c0d70deabb149acf8002e44..b3203c350605af5a386544c858a9a5f7f724342f 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 eface545def441d1a6fe9bdb054ab62eaa6589d3..45bd0c108dc958e3e141055901ea3872bc30d511 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 8e2ea563f1190e39fab0cab2f54da1f382c29356..d2dfca4cbf8c60955e888b6f92bd88a2608bb265 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]) -- 2.44.0