diff --git a/install/share/gssproxy.conf.template b/install/share/gssproxy.conf.template index 9d111009f..aaa7dc109 100644 --- a/install/share/gssproxy.conf.template +++ b/install/share/gssproxy.conf.template @@ -16,3 +16,10 @@ allow_client_ccache_sync = true cred_usage = initiate euid = $IPAAPI_USER + +[service/ipa-sweeper] + mechs = krb5 + cred_store = keytab:$HTTP_KEYTAB + socket = $SWEEPER_SOCKET + euid = $IPAAPI_USER + cred_usage = initiate diff --git a/install/tools/ipa-ccache-sweeper.in b/install/tools/ipa-ccache-sweeper.in index 33bd5f7fe..4a1e1fef1 100644 --- a/install/tools/ipa-ccache-sweeper.in +++ b/install/tools/ipa-ccache-sweeper.in @@ -17,31 +17,39 @@ import stat import sys import time -from gssapi.raw import acquire_cred_from +from ipalib.krb_utils import get_credentials_if_valid from ipaplatform.paths import paths -# process file as a ccache and indicate whether it is expired def should_delete(fname, t, minlife): + """Process file as a ccache and indicate whether it is expired""" + # skip directories and other non-files + st = os.stat(fname) + if not stat.S_ISREG(st.st_mode): + return False + + # ignore files that are newer than minlife minutes + if t - st.st_mtime < minlife * 60: + return False + + # gssproxy inquires input credentials. If they are expired + # then gssproxy acquires creds from cred_store according to + # the configuration of gssproxy's service, which in this case + # hasn't cred_store(besides `keytab:`, used for decryption of + # ccache). If there is no ccache within cred_store then gssproxy + # adds its own one("MEMORY:internal_%d"), which hasn't + # any credentials, thus, scan_ccache fails with KRB5_FCC_NOFILE. + # Since the caller requires INITIATE-ONLY and the client keytab + # is not provided in cred_store the result of gss_acquire_cred_from + # is KRB5_FCC_NOFILE, which is mapped by gssproxy to + # 0x04200000 + KRB5_FCC_NOFILE. try: - # skip directories and other non-files - st = os.stat(fname) - if not stat.S_ISREG(st.st_mode): - return False + creds = get_credentials_if_valid(ccache_name=fname) + return creds is None + except ValueError: + return True - # ignore files that are newer than minlife minutes - if t - st.st_mtime < minlife * 60: - return False - - creds = acquire_cred_from({b"ccache": fname.encode("UTF-8")}) - except FileNotFoundError: - # someone else did the work for us - return False - except Exception as e: - print("Not deleting %s due to error %s" % (fname, e)) - return False - - return creds.lifetime == 0 + return False if __name__ == "__main__": @@ -52,7 +60,8 @@ if __name__ == "__main__": args = parser.parse_args() os.environ["GSS_USE_PROXY"] = "yes" - os.environ["GSSPROXY_BEHAVIOR"] = "REMOTE_FIRST" + os.environ["GSSPROXY_BEHAVIOR"] = "REMOTE_ONLY" + os.environ["GSSPROXY_SOCKET"] = paths.IPA_CCACHE_SWEEPER_GSSPROXY_SOCK print("Running sweeper...") @@ -60,8 +69,12 @@ if __name__ == "__main__": os.chdir(paths.IPA_CCACHES) for fname in os.listdir(paths.IPA_CCACHES): - if should_delete(fname, t, args.minlife): - os.unlink(fname) + try: + if should_delete(fname, t, args.minlife): + os.unlink(fname) + except FileNotFoundError: + # someone else did the work for us + pass print("Sweeper finished successfully!") sys.exit(0) diff --git a/ipalib/krb_utils.py b/ipalib/krb_utils.py index 471009c6d..621e3627b 100644 --- a/ipalib/krb_utils.py +++ b/ipalib/krb_utils.py @@ -39,6 +39,12 @@ KRB5_FCC_PERM = 2529639106 # Credentials cache permissions inc KRB5_CC_FORMAT = 2529639111 # Bad format in credentials cache KRB5_REALM_CANT_RESOLVE = 2529639132 # Cannot resolve network address for KDC in requested realm +# mechglue/gss_plugin.c: #define MAP_ERROR_BASE 0x04200000 +GSSPROXY_MAP_ERROR_BASE = 69206016 + +# GSSProxy error codes +GSSPROXY_KRB5_FCC_NOFILE = GSSPROXY_MAP_ERROR_BASE + KRB5_FCC_NOFILE + krb_ticket_expiration_threshold = 60*5 # number of seconds to accmodate clock skew krb5_time_fmt = '%m/%d/%y %H:%M:%S' ccache_name_re = re.compile(r'^((\w+):)?(.+)') @@ -146,7 +152,9 @@ def get_credentials(name=None, ccache_name=None): try: return gssapi.Credentials(usage='initiate', name=name, store=store) except gssapi.exceptions.GSSError as e: - if e.min_code == KRB5_FCC_NOFILE: # pylint: disable=no-member + if e.min_code in ( # pylint: disable=no-member + KRB5_FCC_NOFILE, GSSPROXY_KRB5_FCC_NOFILE, + ): raise ValueError('"%s", ccache="%s"' % (e, ccache_name)) raise diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 78363beab..0378f453b 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -454,6 +454,9 @@ class BasePathNamespace: TDBTOOL = '/usr/bin/tdbtool' SECRETS_TDB = '/var/lib/samba/private/secrets.tdb' LETS_ENCRYPT_LOG = '/var/log/letsencrypt/letsencrypt.log' + IPA_CCACHE_SWEEPER_GSSPROXY_SOCK = ( + "/var/lib/gssproxy/ipa_ccache_sweeper.sock" + ) def check_paths(self): """Check paths for missing files diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py index 0d87aa55a..469bb75f8 100644 --- a/ipaplatform/redhat/tasks.py +++ b/ipaplatform/redhat/tasks.py @@ -522,6 +522,7 @@ class RedHatTaskNamespace(BaseTaskNamespace): HTTP_CCACHE=paths.HTTP_CCACHE, HTTPD_USER=constants.HTTPD_USER, IPAAPI_USER=ipaapi_user, + SWEEPER_SOCKET=paths.IPA_CCACHE_SWEEPER_GSSPROXY_SOCK, ) ) diff --git a/ipatests/test_integration/test_krbtpolicy.py b/ipatests/test_integration/test_krbtpolicy.py index b2a8f3429..63e75ae67 100644 --- a/ipatests/test_integration/test_krbtpolicy.py +++ b/ipatests/test_integration/test_krbtpolicy.py @@ -12,6 +12,7 @@ import pytest import time from datetime import datetime +from ipalib.constants import IPAAPI_USER from ipaplatform.paths import paths from ipatests.test_integration.base import IntegrationTest @@ -205,15 +206,15 @@ class TestPWPolicy(IntegrationTest): "--user-auth-type", "otp"]) kinit_check_life(self.master, USER1) - def test_ccache_sweep(self, reset_to_default_policy): - """Test that the ccache sweeper works + def test_ccache_sweep_expired(self, reset_to_default_policy): + """Test that the ccache sweeper works on expired ccaches - Force wipe all existing ccaches - - Set the ticket policy to a short value, 30 seconds. + - Set the ticket policy to a short value, 20 seconds. - Do a series of kinit, ipa command, kdestroy to generate ccaches - - sleep() + - sleep() for expiration - Run the sweeper - - Verify that all ccaches are gone + - Verify that all expired ccaches are gone """ MAXLIFE = 20 reset_to_default_policy(self.master) # this will reset at END of test @@ -226,16 +227,58 @@ class TestPWPolicy(IntegrationTest): ['find', paths.IPA_CCACHES, '-type', 'f', '-delete'] ) for _i in range(5): - tasks.kdestroy_all(self.master) tasks.kinit_admin(self.master) self.master.run_command(['ipa', 'user-show', 'admin']) - tasks.kdestroy_all(self.master) - time.sleep(MAXLIFE) - self.master.run_command( - ['/usr/libexec/ipa/ipa-ccache-sweeper', '-m', '0'] + tasks.kdestroy_all(self.master) + + result = self.master.run_command( + "ls -1 {0} | wc -l".format(paths.IPA_CCACHES) ) - time.sleep(5) + assert int(result.stdout_text.strip()) == 5 + + # let ccache expire + time.sleep(MAXLIFE) + ccache_sweep_cmd = ["/usr/libexec/ipa/ipa-ccache-sweeper", "-m", "0"] + + # should be run as ipaapi for GSSProxy + self.master.run_command( + ["runuser", "-u", IPAAPI_USER, "--"] + ccache_sweep_cmd + ) + result = self.master.run_command( "ls -1 {0} | wc -l".format(paths.IPA_CCACHES) ) assert int(result.stdout_text.strip()) == 0 + + def test_ccache_sweep_valid(self): + """Test that the ccache sweeper doesn't remove valid ccaches + - Force wipe all existing ccaches + - Run the sweeper + - Verify that all valid ccaches weren't removed + Note: assumed that ccache expiration doesn't happen during test + """ + tasks.kdestroy_all(self.master) + self.master.run_command( + ["find", paths.IPA_CCACHES, "-type", "f", "-delete"] + ) + + for _i in range(5): + tasks.kinit_admin(self.master) + self.master.run_command(["ipa", "user-show", "admin"]) + tasks.kdestroy_all(self.master) + + result = self.master.run_command( + "ls -1 {0} | wc -l".format(paths.IPA_CCACHES) + ) + assert int(result.stdout_text.strip()) == 5 + + ccache_sweep_cmd = ["/usr/libexec/ipa/ipa-ccache-sweeper", "-m", "0"] + + # should be run as ipaapi for GSSProxy + self.master.run_command( + ["runuser", "-u", IPAAPI_USER, "--"] + ccache_sweep_cmd + ) + result = self.master.run_command( + "ls -1 {0} | wc -l".format(paths.IPA_CCACHES) + ) + assert int(result.stdout_text.strip()) == 5