|
|
cce5df |
From 1441b999d3fe9b4e59fe942294d13480ecee7d94 Mon Sep 17 00:00:00 2001
|
|
|
cce5df |
From: Alexander Bokovoy <abokovoy@redhat.com>
|
|
|
cce5df |
Date: Wed, 28 Oct 2020 17:46:56 +0200
|
|
|
cce5df |
Subject: [PATCH] rpcserver: fallback to non-armored kinit in case of trusted
|
|
|
cce5df |
domains
|
|
|
cce5df |
|
|
|
cce5df |
MIT Kerberos implements FAST negotiation as specified in RFC 6806
|
|
|
cce5df |
section 11. The implementation relies on the caller to provide a hint
|
|
|
cce5df |
whether FAST armoring must be used.
|
|
|
cce5df |
|
|
|
cce5df |
FAST armor can only be used when both client and KDC have a shared
|
|
|
cce5df |
secret. When KDC is from a trusted domain, there is no way to have a
|
|
|
cce5df |
shared secret between a generic Kerberos client and that KDC.
|
|
|
cce5df |
|
|
|
cce5df |
[MS-KILE] section 3.2.5.4 'Using FAST When the Realm Supports FAST'
|
|
|
cce5df |
allows KILE clients (Kerberos clients) to have local settings that
|
|
|
cce5df |
direct it to enforce use of FAST. This is equal to the current
|
|
|
cce5df |
implementation of 'kinit' utility in MIT Kerberos requiring to use FAST
|
|
|
cce5df |
if armor cache (option '-T') is provided.
|
|
|
cce5df |
|
|
|
cce5df |
[MS-KILE] section 3.3.5.7.4 defines a way for a computer from a
|
|
|
cce5df |
different realm to use compound identity TGS-REQ to create FAST TGS-REQ
|
|
|
cce5df |
explicitly armored with the computer's TGT. However, this method is not
|
|
|
cce5df |
available to IPA framework as we don't have access to the IPA server's
|
|
|
cce5df |
host key. In addition, 'kinit' utility does not support this method.
|
|
|
cce5df |
|
|
|
cce5df |
Active Directory has a policy to force use of FAST when client
|
|
|
cce5df |
advertizes its use. Since we cannot know in advance whether a principal
|
|
|
cce5df |
to obtain initial credentials for belongs to our realm or to a trusted
|
|
|
cce5df |
one due to enterprise principal canonicalization, we have to try to
|
|
|
cce5df |
kinit. Right now we fail unconditionally if FAST couldn't be used and
|
|
|
cce5df |
libkrb5 communication with a KDC from the user realm (e.g. from a
|
|
|
cce5df |
trusted forest) causes enforcement of a FAST.
|
|
|
cce5df |
|
|
|
cce5df |
In the latter case, as we cannot use FAST anyway, try to kinit again
|
|
|
cce5df |
without advertizing FAST. This works even in the situations when FAST
|
|
|
cce5df |
enforcement is enabled on Active Directory side: if client doesn't
|
|
|
cce5df |
advertize FAST capability, it is not required. Additionally, FAST cannot
|
|
|
cce5df |
be used for any practical need for a trusted domain's users yet.
|
|
|
cce5df |
|
|
|
cce5df |
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
|
|
|
cce5df |
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
|
|
|
cce5df |
---
|
|
|
cce5df |
ipalib/errors.py | 6 ++
|
|
|
cce5df |
ipaserver/rpcserver.py | 94 ++++++++++++++++---------
|
|
|
cce5df |
ipatests/test_integration/test_trust.py | 21 ++++++
|
|
|
cce5df |
3 files changed, 86 insertions(+), 35 deletions(-)
|
|
|
cce5df |
|
|
|
cce5df |
diff --git a/ipalib/errors.py b/ipalib/errors.py
|
|
|
cce5df |
index 1b17ca7ed..fa51e15c0 100644
|
|
|
cce5df |
--- a/ipalib/errors.py
|
|
|
cce5df |
+++ b/ipalib/errors.py
|
|
|
cce5df |
@@ -245,6 +245,12 @@ class PluginModuleError(PrivateError):
|
|
|
cce5df |
format = '%(name)s is not a valid plugin module'
|
|
|
cce5df |
|
|
|
cce5df |
|
|
|
cce5df |
+class KrbPrincipalWrongFAST(PrivateError):
|
|
|
cce5df |
+ """
|
|
|
cce5df |
+ Raised when it is not possible to use our FAST armor for kinit
|
|
|
cce5df |
+ """
|
|
|
cce5df |
+ format = '%(principal)s cannot use Anonymous PKINIT as a FAST armor'
|
|
|
cce5df |
+
|
|
|
cce5df |
##############################################################################
|
|
|
cce5df |
# Public errors:
|
|
|
cce5df |
|
|
|
cce5df |
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
|
|
|
cce5df |
index 181295471..ed775170e 100644
|
|
|
cce5df |
--- a/ipaserver/rpcserver.py
|
|
|
cce5df |
+++ b/ipaserver/rpcserver.py
|
|
|
cce5df |
@@ -46,9 +46,11 @@ from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES
|
|
|
cce5df |
from ipalib.frontend import Local
|
|
|
cce5df |
from ipalib.install.kinit import kinit_armor, kinit_password
|
|
|
cce5df |
from ipalib.backend import Executioner
|
|
|
cce5df |
-from ipalib.errors import (PublicError, InternalError, JSONError,
|
|
|
cce5df |
+from ipalib.errors import (
|
|
|
cce5df |
+ PublicError, InternalError, JSONError,
|
|
|
cce5df |
CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError,
|
|
|
cce5df |
- ExecutionError, PasswordExpired, KrbPrincipalExpired, UserLocked)
|
|
|
cce5df |
+ ExecutionError, PasswordExpired, KrbPrincipalExpired, KrbPrincipalWrongFAST,
|
|
|
cce5df |
+ UserLocked)
|
|
|
cce5df |
from ipalib.request import context, destroy_context
|
|
|
cce5df |
from ipalib.rpc import (xml_dumps, xml_loads,
|
|
|
cce5df |
json_encode_binary, json_decode_binary)
|
|
|
cce5df |
@@ -957,6 +959,34 @@ class login_password(Backend, KerberosSession):
|
|
|
cce5df |
self.api.Backend.wsgi_dispatch.mount(self, self.key)
|
|
|
cce5df |
|
|
|
cce5df |
def __call__(self, environ, start_response):
|
|
|
cce5df |
+ def attempt_kinit(user_principal, password,
|
|
|
cce5df |
+ ipa_ccache_name, use_armor=True):
|
|
|
cce5df |
+ try:
|
|
|
cce5df |
+ # try to remove in case an old file was there
|
|
|
cce5df |
+ os.unlink(ipa_ccache_name)
|
|
|
cce5df |
+ except OSError:
|
|
|
cce5df |
+ pass
|
|
|
cce5df |
+ try:
|
|
|
cce5df |
+ self.kinit(user_principal, password,
|
|
|
cce5df |
+ ipa_ccache_name, use_armor=use_armor)
|
|
|
cce5df |
+ except PasswordExpired as e:
|
|
|
cce5df |
+ return self.unauthorized(environ, start_response,
|
|
|
cce5df |
+ str(e), 'password-expired')
|
|
|
cce5df |
+ except InvalidSessionPassword as e:
|
|
|
cce5df |
+ return self.unauthorized(environ, start_response,
|
|
|
cce5df |
+ str(e), 'invalid-password')
|
|
|
cce5df |
+ except KrbPrincipalExpired as e:
|
|
|
cce5df |
+ return self.unauthorized(environ,
|
|
|
cce5df |
+ start_response,
|
|
|
cce5df |
+ str(e),
|
|
|
cce5df |
+ 'krbprincipal-expired')
|
|
|
cce5df |
+ except UserLocked as e:
|
|
|
cce5df |
+ return self.unauthorized(environ,
|
|
|
cce5df |
+ start_response,
|
|
|
cce5df |
+ str(e),
|
|
|
cce5df |
+ 'user-locked')
|
|
|
cce5df |
+ return None
|
|
|
cce5df |
+
|
|
|
cce5df |
logger.debug('WSGI login_password.__call__:')
|
|
|
cce5df |
|
|
|
cce5df |
# Get the user and password parameters from the request
|
|
|
cce5df |
@@ -1007,26 +1037,14 @@ class login_password(Backend, KerberosSession):
|
|
|
cce5df |
ipa_ccache_name = os.path.join(paths.IPA_CCACHES,
|
|
|
cce5df |
'kinit_{}'.format(os.getpid()))
|
|
|
cce5df |
try:
|
|
|
cce5df |
- # try to remove in case an old file was there
|
|
|
cce5df |
- os.unlink(ipa_ccache_name)
|
|
|
cce5df |
- except OSError:
|
|
|
cce5df |
- pass
|
|
|
cce5df |
- try:
|
|
|
cce5df |
- self.kinit(user_principal, password, ipa_ccache_name)
|
|
|
cce5df |
- except PasswordExpired as e:
|
|
|
cce5df |
- return self.unauthorized(environ, start_response, str(e), 'password-expired')
|
|
|
cce5df |
- except InvalidSessionPassword as e:
|
|
|
cce5df |
- return self.unauthorized(environ, start_response, str(e), 'invalid-password')
|
|
|
cce5df |
- except KrbPrincipalExpired as e:
|
|
|
cce5df |
- return self.unauthorized(environ,
|
|
|
cce5df |
- start_response,
|
|
|
cce5df |
- str(e),
|
|
|
cce5df |
- 'krbprincipal-expired')
|
|
|
cce5df |
- except UserLocked as e:
|
|
|
cce5df |
- return self.unauthorized(environ,
|
|
|
cce5df |
- start_response,
|
|
|
cce5df |
- str(e),
|
|
|
cce5df |
- 'user-locked')
|
|
|
cce5df |
+ result = attempt_kinit(user_principal, password,
|
|
|
cce5df |
+ ipa_ccache_name, use_armor=True)
|
|
|
cce5df |
+ except KrbPrincipalWrongFAST:
|
|
|
cce5df |
+ result = attempt_kinit(user_principal, password,
|
|
|
cce5df |
+ ipa_ccache_name, use_armor=False)
|
|
|
cce5df |
+
|
|
|
cce5df |
+ if result is not None:
|
|
|
cce5df |
+ return result
|
|
|
cce5df |
|
|
|
cce5df |
result = self.finalize_kerberos_acquisition('login_password',
|
|
|
cce5df |
ipa_ccache_name, environ,
|
|
|
cce5df |
@@ -1038,21 +1056,24 @@ class login_password(Backend, KerberosSession):
|
|
|
cce5df |
pass
|
|
|
cce5df |
return result
|
|
|
cce5df |
|
|
|
cce5df |
- def kinit(self, principal, password, ccache_name):
|
|
|
cce5df |
- # get anonymous ccache as an armor for FAST to enable OTP auth
|
|
|
cce5df |
- armor_path = os.path.join(paths.IPA_CCACHES,
|
|
|
cce5df |
- "armor_{}".format(os.getpid()))
|
|
|
cce5df |
+ def kinit(self, principal, password, ccache_name, use_armor=True):
|
|
|
cce5df |
+ if use_armor:
|
|
|
cce5df |
+ # get anonymous ccache as an armor for FAST to enable OTP auth
|
|
|
cce5df |
+ armor_path = os.path.join(paths.IPA_CCACHES,
|
|
|
cce5df |
+ "armor_{}".format(os.getpid()))
|
|
|
cce5df |
|
|
|
cce5df |
- logger.debug('Obtaining armor in ccache %s', armor_path)
|
|
|
cce5df |
+ logger.debug('Obtaining armor in ccache %s', armor_path)
|
|
|
cce5df |
|
|
|
cce5df |
- try:
|
|
|
cce5df |
- kinit_armor(
|
|
|
cce5df |
- armor_path,
|
|
|
cce5df |
- pkinit_anchors=[paths.KDC_CERT, paths.KDC_CA_BUNDLE_PEM],
|
|
|
cce5df |
- )
|
|
|
cce5df |
- except RuntimeError as e:
|
|
|
cce5df |
- logger.error("Failed to obtain armor cache")
|
|
|
cce5df |
- # We try to continue w/o armor, 2FA will be impacted
|
|
|
cce5df |
+ try:
|
|
|
cce5df |
+ kinit_armor(
|
|
|
cce5df |
+ armor_path,
|
|
|
cce5df |
+ pkinit_anchors=[paths.KDC_CERT, paths.KDC_CA_BUNDLE_PEM],
|
|
|
cce5df |
+ )
|
|
|
cce5df |
+ except RuntimeError as e:
|
|
|
cce5df |
+ logger.error("Failed to obtain armor cache")
|
|
|
cce5df |
+ # We try to continue w/o armor, 2FA will be impacted
|
|
|
cce5df |
+ armor_path = None
|
|
|
cce5df |
+ else:
|
|
|
cce5df |
armor_path = None
|
|
|
cce5df |
|
|
|
cce5df |
try:
|
|
|
cce5df |
@@ -1080,6 +1101,9 @@ class login_password(Backend, KerberosSession):
|
|
|
cce5df |
'while getting initial credentials') in str(e):
|
|
|
cce5df |
raise UserLocked(principal=principal,
|
|
|
cce5df |
message=unicode(e))
|
|
|
cce5df |
+ elif ('kinit: Error constructing AP-REQ armor: '
|
|
|
cce5df |
+ 'Matching credential not found') in str(e):
|
|
|
cce5df |
+ raise KrbPrincipalWrongFAST(principal=principal)
|
|
|
cce5df |
raise InvalidSessionPassword(principal=principal,
|
|
|
cce5df |
message=unicode(e))
|
|
|
cce5df |
|
|
|
cce5df |
diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py
|
|
|
cce5df |
index a6a055c2a..bec918a31 100644
|
|
|
cce5df |
--- a/ipatests/test_integration/test_trust.py
|
|
|
cce5df |
+++ b/ipatests/test_integration/test_trust.py
|
|
|
cce5df |
@@ -175,6 +175,27 @@ class TestTrust(BaseTestTrust):
|
|
|
cce5df |
tasks.kdestroy_all(self.master)
|
|
|
cce5df |
tasks.kinit_admin(self.master)
|
|
|
cce5df |
|
|
|
cce5df |
+ def test_password_login_as_aduser(self):
|
|
|
cce5df |
+ """Test if AD user can login with password to Web UI"""
|
|
|
cce5df |
+ ad_admin = 'Administrator@%s' % self.ad_domain
|
|
|
cce5df |
+
|
|
|
cce5df |
+ tasks.kdestroy_all(self.master)
|
|
|
cce5df |
+ user_and_password = ('user=%s&password=%s' %
|
|
|
cce5df |
+ (ad_admin, self.master.config.ad_admin_password))
|
|
|
cce5df |
+ host = self.master.hostname
|
|
|
cce5df |
+ cmd_args = [
|
|
|
cce5df |
+ paths.BIN_CURL,
|
|
|
cce5df |
+ '-v',
|
|
|
cce5df |
+ '-H', 'referer:https://{}/ipa'.format(host),
|
|
|
cce5df |
+ '-H', 'Content-Type:application/x-www-form-urlencoded',
|
|
|
cce5df |
+ '-H', 'Accept:text/plain',
|
|
|
cce5df |
+ '--cacert', paths.IPA_CA_CRT,
|
|
|
cce5df |
+ '--data', user_and_password,
|
|
|
cce5df |
+ 'https://{}/ipa/session/login_password'.format(host)]
|
|
|
cce5df |
+ result = self.master.run_command(cmd_args)
|
|
|
cce5df |
+ assert "Set-Cookie: ipa_session=MagBearerToken" in result.stdout_text
|
|
|
cce5df |
+ tasks.kinit_admin(self.master)
|
|
|
cce5df |
+
|
|
|
cce5df |
def test_ipauser_authentication_with_nonposix_trust(self):
|
|
|
cce5df |
ipauser = u'tuser'
|
|
|
cce5df |
original_passwd = 'Secret123'
|
|
|
cce5df |
--
|
|
|
cce5df |
2.29.2
|
|
|
cce5df |
|