SSH sessions register with logind as `class=background` because `PAM_TTY` is never set
Expected behavior:
With PAM enabled and a PTY allocated, a Teleport SSH session should register with systemd-logind as
an interactive user session — Class=user, Type=tty — so that tools which enumerate logind sessions
report it. w(1) should list the logged-in user, as it does for an sshd login on the same host.
Current behavior:
The session registers as Class=background, Type=unspecified, with an empty TTY, even though the
shell has a PTY. w reports no users while a user is interactively logged in:
$ tty
/dev/pts/3
$ loginctl show-session "$XDG_SESSION_ID" -p Class -p Type -p TTY -p Service -p Remote
Class=background
Type=unspecified
TTY=
Service=sshd
Remote=no
$ w
20:10:01 up 1 day, 23:26, 0 users, load average: 0.02, 0.15, 0.09
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHATXDG_SESSION_ID and XDG_RUNTIME_DIR are both set and loginctl list-sessions shows the session, so
PAM ran and pam_systemd registered it — the session is simply classified wrongly.
The same applies to the client address. Remote=no and w's FROM column is empty, because
PAM_RHOST is not set either.
Cause. pam_systemd derives Type and Class from the PAM items. From systemd's
src/login/pam_systemd.c:
} else if (streq_ptr(c->tty, "ssh")) {
/* ssh has been setting PAM_TTY to "ssh" (for the same reason as cron does this, see
* above. For further details look for "PAM_TTY_KLUDGE" in the openssh sources). */
c->type = "tty";
if (isempty(c->class))
c->class = "user";
c->tty = NULL;
}
...
if (isempty(c->type))
c->type = !isempty(c->display) ? "x11" : !isempty(c->tty) ? "tty" : "unspecified";
if (isempty(c->class))
c->class = streq(c->type, "unspecified") ? "background" : "user";OpenSSH sets PAM_TTY to the literal string "ssh" — its PAM_TTY_KLUDGE — because it opens the PAM
session before knowing whether a PTY will be negotiated. pam_systemd special-cases that value into
Type=tty, Class=user, and deliberately clears the tty field (which is why a correct sshd session
also reports TTY= empty). With no PAM_TTY at all, neither branch applies: Type falls through to
unspecified, and Class therefore to background.
Suggested fix. Set PAM_TTY before pam_open_session(). Using the literal "ssh", as OpenSSH
does, is the smallest change, needs no knowledge of whether a PTY will be allocated, and lands on a
value pam_systemd already special-cases for exactly this situation.
Using the real terminal name rather than OpenSSH's "ssh" placeholder means logind also records the
tty, and — because TerminalName is empty for a non-interactive request — a session with no PTY
correctly stays classified as background. Classification therefore follows what the session actually
is, per session.
unsafe and C.free are already used in pam.go, security/pam_appl.h is already in its cgo
preamble, and net is already imported in reexec.go, so nothing else changes.
⚠️- The patch has not been compiled or tested — that needs Linux with PAM development headers and
-tags pam,cgo. Please treat it as a starting point. It applies cleanly to master as of filing
(git apply --check passes).
Bug details:
- Teleport version
Teleport v18.11.0 git:v18.11.0-0-g080b9f5a go1.25.12Node agent with pam: { enabled: true }, default service_name of sshd. Host: Ubuntu 26.04.1 LTS,
systemd 259, procps built with systemd support.
- Recreation steps
- On a host with systemd 259 and PAM enabled in the Teleport SSH service, connect with
tsh ssh. - Confirm the shell has a PTY:
ttyreturns/dev/pts/N. loginctl show-session "$XDG_SESSION_ID" -p Class -p Type -p TTY→background,unspecified, empty.w→0 users.- For contrast, connect to the same host with
sshdirectly:
$ loginctl show-session "$XDG_SESSION_ID" -p Class -p Type -p TTY -p Service
Class=user
Type=tty
TTY=
Service=sshd
$ w
20:20:21 up 1 day, 23:37, 1 user, load average: 0.07, 0.03, 0.04
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
<user> pts/0 <address> 20:20 3.00s 0.00s ? wSide by side, same host, same release, same PAM stack, both with a PTY:
tsh ssh |
ssh |
|
|---|---|---|
Service |
sshd | sshd |
Type |
unspecified | tty |
Class |
background | user |
TTY |
empty | empty |
appears in w |
no | yes |
The empty TTY in both shows the tty field is not the discriminator — Type and Class are, and
those follow from whether PAM_TTY was set. The only variable between the two results is which
process opened the PAM session.
- Debug logs
Not captured; the behaviour is deterministic and visible in loginctl output above. pam_systemd
debug logging (pam_systemd.so debug) would show the derived type and class if useful.
Why this matters more on current distributions
Until recently the misclassification was masked: w and who read utmp, whose records exist
independently of logind. Ubuntu 26.04 ships systemd 259 with utmp support removed — /run/utmp
does not exist — so logind is the only remaining source of "who is logged in". A session classified
background is therefore invisible to session accounting entirely, with no fallback. who cannot be
fixed at all on such a release, as it reads utmp exclusively.
Session class also feeds logind's idle handling and lingering behaviour, so the classification has consequences beyond reporting.
Secondary observation: two PAM sessions per login
Each login produces two logind sessions, one Class=manager and one Class=background, and
correspondingly two PAM session opens. Where pam_wtmpdb is in the stack, last shows two entries per
login — and notably one of them carries both the PTY and the client address:
<user> pts/3 <address> Sun Sep 13 20:10 - 20:14 (00:04)
<user> Sun Sep 13 20:10 - still logged inSo the values are available to Teleport at some point in the flow; the session open that logind acts on simply does not have them. That suggests the session is registered before the PTY and peer address are attached, and the later, complete open does not update the already-registered logind session.
Workaround, for anyone who finds this before it is fixed
Teleport's PAM environment support can supply the properties directly, since pam_systemd reads
XDG_SESSION_TYPE and XDG_SESSION_CLASS from the PAM environment:
pam:
enabled: true
environment:
XDG_SESSION_TYPE: tty
XDG_SESSION_CLASS: userw then lists Teleport sessions. FROM stays empty, as there is no environment override for the
remote host. This is not a fix: it asserts tty for every session on the node, including
non-interactive commands and file transfers, where per-session classification from the PAM items would
be correct. It does confirm that pam_systemd, the PAM stack and the distribution are all behaving
correctly, and that the only missing inputs are the two PAM items.
I will open a PR with a potential fix
Source: gravitational/teleport