How to check SSH logs on Linux: auth.log, /var/log/secure and journalctl

On Debian 13 and Ubuntu 26.04, journalctl -t sshd shows no logins at all.

sshd still logs them. It logs them under a different name. This page covers where SSH logs live, how to read them, and the rename that breaks filters written for older OpenSSH.

The quick answer

Ask for the unit, not the program name. This works on every OpenSSH version:

journalctl -u ssh            # Debian, Ubuntu
journalctl -u sshd           # RHEL, Rocky, Alma, Fedora, Arch

Add --since today or -g Accepted to narrow it. The unit name is the one difference between distributions.

Prefer a text file? sshd also writes through syslog, if a syslog daemon is installed:

Distribution File Written by
Debian, Ubuntu /var/log/auth.log rsyslog, rule auth,authpriv.*
RHEL, Rocky, Alma /var/log/secure rsyslog, rule authpriv.* (Red Hat sets SyslogFacility AUTHPRIV)
sudo grep sshd /var/log/auth.log | tail -50

No /var/log/auth.log?

Debian stopped installing rsyslog in Debian 12. A fresh install of 12 or 13 has no rsyslog, so it has no auth.log. The journal still has every line. Hosts upgraded from Debian 11 keep rsyslog.

Check before you go looking:

ls -l /var/log/auth.log

No such file or directory means the host is journal-only. Use journalctl -u ssh. If something needs the file, sudo apt install rsyslog brings it back. The file starts empty. Older lines stay in the journal.

Why journalctl -t sshd finds no logins

OpenSSH 9.8 split sshd into two programs. sshd listens for connections. sshd-session handles each connection once it arrives. The release notes warn that some log messages now carry the name sshd-session instead of sshd.

In practice, that is every line about a login. sshd starts sshd-session with the binary’s own path as its name, and sshd-session writes Accepted, Failed and Invalid user. The listener keeps only its own lines, such as Server listening on 0.0.0.0 port 22.

So -t sshd still returns something. That is the trap. It looks like a working filter with a quiet day behind it.

Which name does your server use? Look for the binary:

ls /usr/lib/openssh/sshd-session /usr/libexec/openssh/sshd-session 2>/dev/null

Debian and Ubuntu install it in the first path. RHEL and Fedora use the second. Any output means login lines come from sshd-session.

Release OpenSSH Login lines tagged
Debian 12 9.2 sshd
Debian 13 10.0 sshd-session
Ubuntu 24.04 LTS 9.6 sshd
Ubuntu 26.04 LTS 10.2 sshd-session
RHEL 10, CentOS Stream 10 9.9 sshd-session
CentOS Stream 9 9.9 since September 2025 sshd-session

A fleet mid-upgrade has both. Match both:

journalctl -t sshd -t sshd-session
journalctl SYSLOG_IDENTIFIER=sshd SYSLOG_IDENTIFIER=sshd-session

Repeating a match on the same field gives either value. That is the field-match rule, and -t follows it too.

The file side has the same split. A line in auth.log now reads sshd-session[812]: Accepted publickey .... grep sshd still matches it, because the new name contains the old one. grep 'sshd\[' does not. Neither does an rsyslog rule written as $programname == "sshd".

What the lines say

Every check you will want keys off one of these:

Accepted publickey for deploy from 203.0.113.7 port 51234 ssh2: ED25519 SHA256:...
Accepted password for root from 198.51.100.4 port 40122 ssh2
Failed password for root from 192.0.2.55 port 33460 ssh2
Failed password for invalid user admin from 192.0.2.55 port 33456 ssh2
Invalid user admin from 192.0.2.55 port 33456
Connection closed by authenticating user root 192.0.2.55 port 33470 [preauth]

Accepted is a login. Failed and Invalid user are not. [preauth] marks a line from before authentication finished.

Three greps cover most questions:

journalctl -u ssh --since today -g 'Accepted'        # who got in
journalctl -u ssh --since today -g 'Failed password' # who tried a password
journalctl -u ssh --since today -g 'Invalid user'    # who guessed a username

What the default log level leaves out

sshd logs at INFO unless told otherwise. Two kinds of line sit one level lower, at VERBOSE.

Failed key attempts. sshd writes Failed publickey at VERBOSE. It raises the line to INFO only once a connection reaches half of MaxAuthTries. That is 3 of the default 6. A client offering one wrong key leaves no Failed line at all.

Connection lines. Connection from 203.0.113.7 port 51234 on 10.0.0.2 port 22 is VERBOSE too.

Want both? Put the setting in a drop-in file:

echo 'LogLevel VERBOSE' | sudo tee /etc/ssh/sshd_config.d/10-loglevel.conf
sudo sshd -t && sudo systemctl reload ssh
sudo sshd -T | grep -i loglevel

Use sshd instead of ssh in the reload on RHEL and Fedora. sshd -t checks the config before the reload can break it. sshd -T prints the value sshd will use.

Why a drop-in? sshd keeps the first value it reads for each keyword. On Debian, Ubuntu and RHEL, the stock sshd_config includes sshd_config.d/*.conf near the top. A LogLevel line added to the bottom of the main file loses to anything in the drop-ins.

The logs are on the machine you are investigating

That is fine for a quiet Tuesday. It is not fine after a break-in. Whoever got root can delete the lines that show how. And at five servers, nobody greps five auth logs.

Copy the lines off each host as sshd writes them. Install the CL Agent and point it at your log server:

URL = "https://logs.example.com/api/v1/ingest_logs/YOUR-SOURCE-TOKEN"

Put that in /etc/clagent.toml and start the agent. It ships the whole journal as JSON. The full setup is in sending journalctl logs to a central server.

Each sshd line arrives with SYSLOG_IDENTIFIER, MESSAGE and _HOSTNAME. Match both names, and one query covers old and new hosts:

SELECT json_extract(msg, '$._HOSTNAME') AS host,
       json_extract(msg, '$.MESSAGE')   AS message,
       timestamp
FROM logs
WHERE json_valid(msg)
  AND json_extract(msg, '$.SYSLOG_IDENTIFIER') IN ('sshd', 'sshd-session')
  AND json_extract(msg, '$.MESSAGE') LIKE 'Accepted %'
ORDER BY timestamp DESC
LIMIT 50;

Keep json_valid(msg) first. One plain-text line in the source makes json_extract fail with malformed JSON. SQLite stops evaluating an AND at the first false term, so the guard protects the terms after it.

Not sure which hosts have moved to the new name? Ask:

SELECT json_extract(msg, '$._HOSTNAME')          AS host,
       json_extract(msg, '$.SYSLOG_IDENTIFIER') AS ident,
       COUNT(*)                                 AS lines
FROM logs
WHERE json_valid(msg)
  AND json_extract(msg, '$.SYSLOG_IDENTIFIER') LIKE 'sshd%'
GROUP BY host, ident;

Any sshd-session row means the host runs OpenSSH 9.8 or newer. A host with only sshd rows is older, or nobody has logged in yet.

To be told about the next login, save the same filter as an alert rule:

SELECT 1 FROM logs
WHERE timestamp > CAST(strftime('%s', 'now', '-10 minutes') AS INTEGER)
  AND json_valid(msg)
  AND json_extract(msg, '$.SYSLOG_IDENTIFIER') IN ('sshd', 'sshd-session')
  AND json_extract(msg, '$.MESSAGE') LIKE 'Accepted %'
LIMIT 1;

A rule fires when its query returns any row. Rules run every five minutes, so the ten-minute window keeps a line near the boundary from slipping between two runs. Alerting on every login gets noisy fast. Seeing who logged in over SSH has the rules worth keeping: root with a password, any password on a key-only box, and an account not on your list.

Where to go next

💌 Get notified on new features and updates

Only sent when a new version is released. Nothing else.