-
Notifications
You must be signed in to change notification settings - Fork 0
/
pamltsp
348 lines (330 loc) · 13 KB
/
pamltsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#!/bin/sh
# This file is part of LTSP, https://ltsp.org
# Copyright 2019-2021 the LTSP team, see AUTHORS
# SPDX-License-Identifier: GPL-3.0-or-later
# Provide PAM authentication to a server via SSH and optionally SSHFS $HOME.
# It's not an LTSP applet in order to be able to run without LTSP.
# It may be called by the user (see install below),
# or by pam_exec, either as root for the initial login,
# or as the user for screensaver unlocking etc.
die() {
printf "%s\n" "$*" >&2
dbg die "$*"
exit 1
}
dbg() { echo "`LC_ALL=C date` $1: $2" >> /tmp/1; }
require_root() {
if [ "$_UID" != 0 ]; then
die "${1:-$0 must be run as root}"
fi
}
# Install pamltsp in the system PAM configuration
install() {
local auth_control tab
require_root
# Currently the PAM configuration is Debian/Ubuntu specific
if [ "$PAM_AUTH_TYPE" = "Primary" ]; then
auth_control="[success=end default=ignore]"
elif [ "$PAM_AUTH_TYPE" = "Additional" ]; then
tab=$(printf "\t")
auth_control="optional$tab$tab"
else
die "Aborting due to PAM_AUTH_TYPE=$PAM_AUTH_TYPE"
fi
# The seteuid option is needed for `getent shadow`, for mkdir/chown $HOME,
# for caching shadow options to /run/ltsp/pam/, for using systemd-run etc.
printf "Name: SSH based authentication for LTSP
Default: yes
Priority: 0
Auth-Type: %s
Auth:
\t%s\tpam_exec.so expose_authtok seteuid stdout quiet %s pam_auth
Session-Interactive-Only: yes
Session-Type: Additional
Session:
\toptional\tpam_exec.so seteuid stdout quiet %s pam_session\n" \
"$PAM_AUTH_TYPE" "$auth_control" "$_SELF" "$_SELF" \
>/usr/share/pam-configs/ltsp
pam-auth-update --package ltsp ||
die "Could not configure PAM for SSH authentication!"
sed 's/.*\(KillUserProcesses=\).*/\1yes/' -i /etc/systemd/logind.conf
}
# May be called:
# As root by pam auth>Primary, for SSH or SSHFS or passwordless logins
# As root by pam auth>Additional, for LDAP SSHFS mounting
# As root by pam open_session, for autologin SSHFS mounting
# As user by pam auth, for screensaver unlocking
# As the user doesn't have enough rights to `getent shadow`;
# necessary information should be cached in /run/ltsp/pam/ on login.
pam_auth() {
local pw_entry remote pw_name pw_passwd pw_uid pw_gid pw_gecos pw_dir \
pw_shell sp_entry sp_namp sp_pwdp _dummy sshfs_params msg
pam_log
# Verify that we're being called from PAM and fetch the user entry
if [ -z "$PAM_USER" ] || ! pw_entry=$(getent passwd "$PAM_USER"); then
die "User $PAM_USER doesn't exist"
fi
# Detect if the user is local or remote
if grep -q "^$PAM_USER:" /etc/passwd; then
remote=0
dbg pam_auth "remote=0"
else
remote=1
dbg pam_auth "remote=1"
# For remote users, we *may* do SSHFS; nothing more
test "$PAM_TYPE" = "open_session" && dbg pam_auth "<- PAM_TYPE = open_session" && return 0
test "$PAM_AUTH_TYPE" = "Primary" && dbg pam_auth "<- PAM_AUTH_TYPE = Primary" && return 1
test "$SSHFS" = 0 && dbg pam_auth "<- SSHFS = 0" && return 0
fi
# Retrieve the user's sp_entry from shadow or cache
if [ "$remote" = 1 ]; then
# Don't ask for remote shadow entries, e.g.
# https://bugzilla.redhat.com/show_bug.cgi?id=751291#c4
sp_entry="$PAM_USER:pamltsp:remote-user"
elif [ "$_UID" = 0 ]; then
sp_entry=$(getent shadow "$PAM_USER")
elif [ -f "/run/ltsp/pam/$PAM_USER/shadow" ]; then
sp_entry=$(cat "/run/ltsp/pam/$PAM_USER/shadow")
else
die "Unhandled user $PAM_USER"
fi
IFS=:
# Variable names from `man getpwent/getspent`
read -r pw_name pw_passwd pw_uid pw_gid pw_gecos pw_dir pw_shell <<EOF
$pw_entry
EOF
read -r sp_namp sp_pwdp _dummy <<EOF
$sp_entry
EOF
test "$_OLDIFS" = "not set" && unset IFS || IFS="$_OLDIFS"
test "$pw_name" = "$sp_namp" || die "Invalid passwd/shadow for $PAM_USER"
# If this is not a pamltsp user
if [ "${sp_pwdp}" = "${sp_pwdp#pamltsp}" ]; then
if [ "$PAM_TYPE" = "open_session" ] ||
[ "$PAM_AUTH_TYPE" = "Additional" ]; then
dbg pam_auth "<- Not a pamltsp user (open_session/addidional)"
return 0
else
dbg pam_auth "<- Not a pamltsp user (Refuse authentication)"
# Refuse authentication
return 1
fi
fi
# Cache the shadow entry for screensaver and passwordless authentication
if [ "$_UID" = 0 ]; then
# The first PAM call since boot is always with _UID = 0
mkdir -p /run/ltsp/pam
# This is 700 for pid and in case we wanted .ICEauthority
mkdir -p -m 700 "/run/ltsp/pam/$pw_name"
chown "$pw_uid:$pw_gid" "/run/ltsp/pam/$pw_name"
echo "$sp_entry" >"/run/ltsp/pam/$pw_name/shadow"
fi
# Support "pamltsp=" for passwordless logins without SSH authentication
# for guest-like accounts with NFS/local home.
pass=${sp_pwdp#pamltsp}
pass=${pass%%,*}
test "$pass" = "=" && dbg pam_auth "<- pass = '='" && return 0
# jessie-mate breaks with IdentityFile=/dev/null and shows:
# Enter passphrase for key '/dev/null':
# It works with IdentityFile=/nonexistent
set -- -F /dev/null -o UserKnownHostsFile="/etc/ltsp/ssh_known_hosts" \
-o IdentitiesOnly=yes -o IdentityFile=/nonexistent \
-o NumberOfPasswordPrompts=1 $SSH_OPTIONS
unset success
# Indicate that an authentication attempt is in progress
# See https://github.com/libfuse/sshfs/issues/183
trap "rm -f '/run/ltsp/pam/$PAM_USER/pid'" HUP INT QUIT SEGV PIPE TERM EXIT
echo "$$" >"/run/ltsp/pam/$PAM_USER/pid"
# Check if SSHFS is required.
# `mountpoint` blocks until the sshfs password in entered.
# That may be a good thing, to avoid race conditions.
if [ "$SSHFS" = 0 ] || ! command -v sshfs >/dev/null ||
mountpoint -q /home || mountpoint -q "$pw_dir"; then
dbg pam_auth "SSHFS condition 0"
if [ "$PAM_TYPE" = "open_session" ]; then
# This point is reached when we already mounted home in auth,
# or for passwordless logins with NFS or local home
dbg pam_auth "<- already mounted"
return 0
fi
# The ssh call logic is documented in ssh-askpass
export DISPLAY=" "
export SSH_ASKPASS="${_SELF%/*}/ssh-askpass"
if ssh -qns "$@" "$pw_name@$SSH_SERVER" sftp; then
success=1
if [ ! -e "$pw_dir" ] && [ "$MKHOMEDIR" != 0 ]; then
# Emulate pam_mkhomedir
mkdir -p -m 0755 "$pw_dir"
cp -a /etc/skel/. "$pw_dir"
chown -R "$pw_uid:$pw_gid" "$pw_dir"
fi
fi
else
dbg pam_auth "SSHFS condition 1"
test "$_UID" = 0 || die "SSHFS needed without root?!"
# On SSHFS gnome-keyring requires disable_hardlinks, but this
# breaks ICEauthority, so just remove gnome-keyring for now
# https://bugzilla.gnome.org/show_bug.cgi?id=730587
rm -f /usr/bin/gnome-keyring-daemon
# $pw_dir must not be in use to be mounted; cd elsewhere
cd / || true
# If a previous sshfs was killed instead of properly unmounted,
# `mountpoint` returns 1 "Transport endpoint is not connected",
# yet the mount point is there in /proc/mounts, and a new sshfs fails.
# Run an extra fusermount for this case.
# To reproduce, login as admin, and run: systemctl stop gdm3
# It needs a few seconds to kill all the processes.
# Logins will fail for those seconds, and work properly afterwards.
dbg pam_auth "UMOUNT"
grep -qs "/home fuse.sshfs" /proc/mounts && fusermount -u "/home"
for f in ADMINS USERS shared STUDENTS; do
grep -qs "/FILES/$f fuse.sshfs" /proc/mounts && fusermount -u "/FILES/$f"
done
# Create an empty home dir if it's not there; nope, no skel for SSHFS
if [ ! -d "$pw_dir" ] && [ "$MKHOMEDIR" != 0 ]; then
mkdir -p -m 0755 "$pw_dir"
chown "$pw_uid:$pw_gid" "$pw_dir"
fi
# SSHFS doesn't appear to support SSH_ASKPASS, but it can read stdin
# TODO: but why doesn't it read it directly from pam_exec?
# allow_other,default_permissions: allow the user but not other users
# We don't use `sudo -u` for sshfs to avoid modprobe'ing fuse,
# sed'ing fuse.conf, chown'ing home etc; if we did, that would be:
# allow_root: for the DM to setup .Xauthority
# nonempty: in case of .bash_history or local home
sshfs_params=password_stdin,allow_other,default_permissions
# fuse3 defaults to nonempty and doesn't accept it
command -v fusermount3 >/dev/null ||
sshfs_params="$sshfs_params,nonempty"
dbg pam_auth "MOUNT"
if pw=$("${_SELF%/*}/ssh-askpass")
echo "$pw" | sshfs -o "$sshfs_params" "$@" "$pw_name@$SSH_SERVER:/home" "/home" 2>&1
for f in ADMINS USERS shared STUDENTS; do
echo "$pw" | sshfs -o "$sshfs_params" "$@" "$pw_name@$SSH_SERVER:/FILES/$f" "/FILES/$f" 2>&1
done
msg="RC=$?"; then
success=1
else
# If it's empty, remove it to avoid a tmpfs home
rmdir --ignore-fail-on-non-empty "$pw_dir"
fi
fi
if [ "$success" = 1 ]; then
dbg pam_auth "<- AUTH_SUCCESS"
return 0
fi
if [ "$PAM_TYPE" = "auth" ] && [ "$PAM_AUTH_TYPE" = "Primary" ]; then
# su: gettext -d Linux-PAM "Authentication failure"
# login: gettext -d shadow 'Login incorrect' (this works in fedora30 too)
msg=$(gettext -d shadow "Login incorrect")
msg=${msg:-Authentication failure}
dbg pam_auth "<- AUTH_FAIL auth Primary"
else
msg="Pamltsp failed to mount home via SSHFS: $msg"
dbg pam_auth "<- AUTH_FAIL NOT auth Primary"
fi
echo ".$msg." >&2
return 1
}
pam_log() {
return 0
}
pam_session() {
dbg pam_session "$PAM_TYPE"
pam_log
case "$PAM_TYPE" in
close_session) unmount_sshfs || return $? ;;
open_session) pam_auth || return $? ;;
esac
}
unmount_sshfs() {
local pw_entry pw_name pw_passwd pw_uid pw_gid pw_gecos pw_dir pw_shell
# Verify that we're being called from PAM and that the user exists
if [ -z "$PAM_USER" ] || ! pw_entry=$(getent passwd "$PAM_USER"); then
die "User $PAM_USER doesn't exist"
fi
IFS=:
# Variable names from `man getpwnam`
read -r pw_name pw_passwd pw_uid pw_gid pw_gecos pw_dir pw_shell <<EOF
$pw_entry
EOF
test "$_OLDIFS" = "not set" && unset IFS || IFS="$_OLDIFS"
# If $HOME isn't SSHFS, exit
grep -qs "/home fuse.sshfs" /proc/mounts || return 0
# TODO: only run this as root
# TODO: --quiet isn't supported in jessie-mate
# Tell systemd not to wait nor to kill this:
dbg unmount_sshfs "UMOUNT"
systemd-run --scope "$_SELF" unmount_sshfs_stage2 "$PAM_USER" "/home" >/dev/null 2>/dev/null </dev/null &
for f in ADMINS USERS shared STUDENTS; do
systemd-run --scope "$_SELF" unmount_sshfs_stage2 "$PAM_USER" "/FILES/$f" >/dev/null 2>/dev/null </dev/null &
done
# Without this sleep, systemd-run works 100% on vt2, 10% on GUI
sleep 0.1
return 0
}
# This runs with no file descriptors open, to be properly backgrounded
unmount_sshfs_stage2() {
local pw_name pw_dir i cmdline1 cmdline2
pw_name=$1
pw_dir=$2
# This isn't called from pam; just emulating it for pam_log
PAM_TYPE=unmount
pam_log
read -r cmdline1 <"/proc/$$/cmdline"
# Kill all other unmount_sshfs_stage2 of the same user; we're taking over!
for i in $(pidof -x "$_SELF"); do
test "$i" != "$$" || continue
read -r cmdline2 <"/proc/$i/cmdline"
test "$cmdline1" = "$cmdline2" || continue
kill "$i"
done
i=0
# Allow up to 120 seconds for systemd process killing and cleanup.
# Check every second if it finished.
while [ "$i" -lt 120 ]; do
sleep 1
# If it was... manually unmounted?
grep -qs "$pw_dir fuse.sshfs" /proc/mounts || return 0
# If a login for this same user is being attempted, postpone
if [ -e "/run/ltsp/pam/$PAM_USER/pid" ]; then
continue
else
i=$((i + 1))
fi
# If no user processes are running, unmount it
if [ "$(pgrep -cu "$pw_name")" = 0 ]; then
fusermount -u "$pw_dir"
return 0
fi
done
}
main() {
local func _OLDIFS _SELF
_OLDIFS="${IFS-not set}"
_SELF=$(readlink -f "$0")
_UID=$(id -u)
umask 0022
test -f /etc/ltsp/pamltsp.conf && . /etc/ltsp/pamltsp.conf
SSH_SERVER=${SSH_SERVER:-server}
if [ -z "$PAM_AUTH_TYPE" ]; then
# Use autodetection
if grep -qswE 'pam_ldap' /etc/pam.d/common-auth; then
PAM_AUTH_TYPE="Additional"
elif [ -f /etc/sssd/sssd.conf ] &&
grep -qswE 'pam_sss' /etc/pam.d/common-auth; then
PAM_AUTH_TYPE="Additional"
else
PAM_AUTH_TYPE="Primary"
fi
fi
case "$1" in
install | pam_auth | pam_session | unmount_sshfs_stage2)
func=$1
shift
$func "$@"
;;
*) die "Usage: $0 <install|pam_auth|pam_session> [params], not |$*|" ;;
esac
}
main "$@"