Skip to content

Commit df2a960

Browse files
committed
onLoginLogoutAsRoot: Create Kerberos tickets before the onLogin script runs.
1 parent ca1e5a5 commit df2a960

File tree

1 file changed

+165
-14
lines changed

1 file changed

+165
-14
lines changed
Lines changed: 165 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,202 @@
11
#!/usr/bin/python3
2-
32
# DO NOT MODIFY THIS SCRIPT!
43
# For custom scripts use the hookdirs
54
# /etc/linuxmuster-linuxclient7/onLoginAsRoot.d
65
# and /etc/linuxmuster-linuxclient7/onLogoutAsRoot.d
6+
# This script is called in root context when a user logs in or out
77

8-
# This schript is called in root context when a user logs in or out
98
try:
109
import os, sys
1110
from linuxmusterLinuxclient7 import logging, hooks, constants, user, shares, printers, computer, realm
12-
11+
12+
# Get PAM environment variables
1313
pamType = os.getenv("PAM_TYPE")
1414
pamUser = os.getenv("PAM_USER")
1515
#PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER and PAM_TYPE
16+
1617
logging.info("====== onLoginLogoutAsRoot started with PAM_TYPE={0} PAM_RHOST={1} PAM_RUSER={2} PAM_SERVICE={3} PAM_TTY={4} PAM_USER={5} ======".format(pamType, os.getenv("PAM_RHOST"), os.getenv("PAM_RUSER"), os.getenv("PAM_SERVICE"), os.getenv("PAM_TTY"), pamUser))
17-
18+
1819
# check if whe should execute
1920
if not hooks.shouldHooksBeExecuted(pamUser):
2021
logging.info("======> onLoginLogoutAsRoot end ====")
2122
sys.exit(0)
22-
23+
2324
#realm.pullKerberosTicketForComputerAccount()
24-
25+
26+
# ============================================================================
27+
# DEBIAN 13 WORKAROUND: Create Kerberos ticket for user
28+
# ============================================================================
29+
# In Debian 13 (Trixie), PAM does not automatically create Kerberos tickets
30+
# for domain users during login, even though authentication succeeds.
31+
# This workaround ensures that a valid Kerberos ticket exists for the user,
32+
# which is required for:
33+
# - LDAP queries with GSSAPI authentication
34+
# - Mounting CIFS shares with sec=krb5
35+
# - Other Kerberos-authenticated operations
36+
# ============================================================================
37+
38+
if pamType == "open_session" and pamUser:
39+
try:
40+
import subprocess
41+
import pwd
42+
43+
# Get the user's UID and GID from the system password database
44+
# This is needed to set correct ownership on the ticket cache file
45+
try:
46+
user_info = pwd.getpwnam(pamUser)
47+
user_uid = user_info.pw_uid
48+
user_gid = user_info.pw_gid
49+
except KeyError:
50+
logging.error(f"Could not get UID for user {pamUser}")
51+
user_uid = None
52+
53+
if user_uid:
54+
# Define the path to the Kerberos credential cache
55+
# Format: /tmp/krb5cc_<UID>
56+
# This is the standard location expected by Kerberos tools
57+
ccache_path = f"/tmp/krb5cc_{user_uid}"
58+
59+
# Check if a valid Kerberos ticket already exists
60+
ticket_exists = False
61+
if os.path.exists(ccache_path):
62+
# Verify that the existing ticket is still valid (not expired)
63+
# klist -s returns 0 if ticket is valid, non-zero otherwise
64+
check_result = subprocess.run(
65+
['klist', '-s', '-c', ccache_path],
66+
capture_output=True,
67+
timeout=5
68+
)
69+
if check_result.returncode == 0:
70+
ticket_exists = True
71+
logging.debug(f"Valid Kerberos ticket already exists at {ccache_path}")
72+
73+
# Only create a new ticket if none exists or the existing one is invalid
74+
if not ticket_exists:
75+
logging.info(f"Creating Kerberos ticket for {pamUser}")
76+
77+
# ============================================================
78+
# METHOD 1: Try to copy ticket from SSSD cache
79+
# ============================================================
80+
# SSSD may have created a ticket during authentication
81+
# but stored it in its own cache directory instead of /tmp
82+
# Try to find and copy it to the standard location
83+
sssd_ccache = f"/var/lib/sss/db/ccache_{pamUser}"
84+
if os.path.exists(sssd_ccache):
85+
logging.info(f"Found SSSD cache at {sssd_ccache}, copying...")
86+
import shutil
87+
shutil.copy2(sssd_ccache, ccache_path)
88+
os.chown(ccache_path, user_uid, user_gid)
89+
os.chmod(ccache_path, 0o600)
90+
logging.info(f"Successfully copied Kerberos ticket from SSSD")
91+
else:
92+
# ====================================================
93+
# METHOD 2: Check for PAM-created tickets
94+
# ====================================================
95+
# pam_krb5 might have created a ticket with a random suffix
96+
# Format: /tmp/krb5cc_<UID>_XXXXXX
97+
# Find and copy the most recent one
98+
import glob
99+
possible_tickets = glob.glob(f"/tmp/krb5cc_{user_uid}_*")
100+
if possible_tickets:
101+
# Sort by modification time, newest first
102+
possible_tickets.sort(key=os.path.getmtime, reverse=True)
103+
latest_ticket = possible_tickets[0]
104+
logging.info(f"Found PAM ticket at {latest_ticket}, copying...")
105+
import shutil
106+
shutil.copy2(latest_ticket, ccache_path)
107+
os.chown(ccache_path, user_uid, user_gid)
108+
os.chmod(ccache_path, 0o600)
109+
logging.info(f"Successfully copied ticket from PAM")
110+
else:
111+
# ================================================
112+
# METHOD 3: Fallback - Use machine account ticket
113+
# ================================================
114+
# If no user ticket exists, create one using the
115+
# computer's machine account credentials.
116+
# Note: This ticket can be used for LDAP queries
117+
# but may not work for all user operations
118+
# (e.g., mounting user-specific CIFS shares)
119+
logging.warning(f"No user ticket found, using machine account as fallback")
120+
import socket
121+
122+
# Get the hostname and construct the machine principal
123+
# Format: HOSTNAME$@REALM
124+
# The $ suffix indicates a machine account in Active Directory
125+
hostname = socket.gethostname().upper().split('.')[0]
126+
machine_principal = f"{hostname}$@LINUXMUSTER.LAN"
127+
128+
# Use kinit with -k flag to authenticate using the keytab
129+
# The keytab (/etc/krb5.keytab) contains the machine account password
130+
kinit_result = subprocess.run(
131+
['kinit', '-k', machine_principal],
132+
capture_output=True,
133+
text=True,
134+
timeout=10,
135+
env={'KRB5CCNAME': 'FILE:/tmp/krb5cc_0'}
136+
)
137+
138+
if kinit_result.returncode == 0:
139+
# Copy the root's ticket to the user's cache location
140+
# This is a workaround - the ticket is actually for the
141+
# machine account, not the user, but it allows LDAP queries
142+
if os.path.exists('/tmp/krb5cc_0'):
143+
import shutil
144+
shutil.copy2('/tmp/krb5cc_0', ccache_path)
145+
os.chown(ccache_path, user_uid, user_gid)
146+
os.chmod(ccache_path, 0o600)
147+
logging.info(f"Created ticket with machine account (LDAP only)")
148+
else:
149+
logging.error(f"kinit failed: {kinit_result.stderr}")
150+
151+
except Exception as ticket_error:
152+
# Catch all exceptions to prevent login failures
153+
# Even if ticket creation fails, the user should still be able to log in
154+
logging.error(f"Error creating Kerberos ticket: {ticket_error}")
155+
import traceback
156+
logging.error(traceback.format_exc())
157+
158+
# ============================================================================
159+
# End of Debian 13 workaround
160+
# ============================================================================
161+
25162
# mount sysvol
163+
# The sysvol share contains group policies and login scripts
26164
rc, sysvolPath = shares.getLocalSysvolPath()
27165
if rc:
166+
# Export the sysvol path as an environment variable
167+
# so it can be used by child processes and scripts
28168
os.putenv("SYSVOL", sysvolPath)
29-
169+
170+
# Execute appropriate hooks based on PAM type
30171
if pamType == "open_session":
172+
# User is logging in - run login hooks
31173
hooks.runHook(hooks.Type.LoginAsRoot)
32-
33-
elif pamType == "close_session":
174+
elif pamType == "close_session":
175+
# User is logging out - cleanup resources
34176

35-
# cleanup
177+
# Unmount all CIFS shares that were mounted for this user
178+
# This prevents stale mounts and resource leaks
36179
shares.unmountAllSharesOfUser(pamUser)
180+
181+
# Remove all printers that were installed for this user
182+
# Printers are typically added based on group policies
37183
printers.uninstallAllPrintersOfUser(pamUser)
38-
184+
185+
# Run custom logout hooks
39186
hooks.runHook(hooks.Type.LogoutAsRoot)
40-
187+
41188
logging.info("======> onLoginLogoutAsRoot end ======")
42189

43190
except Exception as e:
191+
# Catch any unhandled exceptions and log them
44192
try:
45193
logging.exception(e)
46194
except:
195+
# If logging fails, print to stdout as last resort
47196
print("A fatal error occured!")
48197

49-
# We need to catch all exceptions and return 0 in any case!
50-
# If we do not return 0, login will FAIL FOR EVERYONE!
198+
# CRITICAL: Always exit with 0 (success)
199+
# If this script returns a non-zero exit code, PAM will reject the login
200+
# and NO USER will be able to log in (including local users)!
201+
# Better to allow login without Kerberos ticket than to block all logins.
51202
sys.exit(0)

0 commit comments

Comments
 (0)