mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2026-10-24 22:27:41 -05:00
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 <abokovoy@redhat.com> Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
This commit is contained in:
1 parent
c3d228d4a3
commit
1d2897e3d7
7 files changed
+98
-18
No files matched your search
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -27,7 +27,7 @@ No arguments.
|
||||
* ipauserobjectclasses : :ref:`Str<Str>`
|
||||
* ipapwdexpadvnotify : :ref:`Int<Int>`
|
||||
* ipaconfigstring : :ref:`StrEnum<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<Str>`
|
||||
* ipaselinuxusermapdefault : :ref:`Str<Str>`
|
||||
* ipakrbauthzdata : :ref:`StrEnum<StrEnum>`
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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])
|
||||
Reference in new issue
Block a user