#69773·teleport

SSH sessions register with logind as `class=background` because `PAM_TTY` is never set

Author: quanahCreated Sep 14, 2026Updated Sep 16, 2026
Labelsc-dx

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  WHAT

XDG_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:

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.12

Node 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
  1. On a host with systemd 259 and PAM enabled in the Teleport SSH service, connect with tsh ssh.
  2. Confirm the shell has a PTY: tty returns /dev/pts/N.
  3. loginctl show-session "$XDG_SESSION_ID" -p Class -p Type -p TTYbackground, unspecified, empty.
  4. w0 users.
  5. For contrast, connect to the same host with ssh directly:
$ 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   ?    w

Side 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 in

So 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:

yaml
  pam:
    enabled: true
    environment:
      XDG_SESSION_TYPE: tty
      XDG_SESSION_CLASS: user

w 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