You put a bastion in front of your servers so every SSH connection goes through one box. Then the auditor, or your own incident review, asks the obvious follow-up: what did people actually do once they were in? /var/log/auth.log says who logged in and when. It says nothing about the rm -rf that came twenty minutes later.
This guide shows how to record SSH sessions on a bastion host with tools that ship with most Linux distributions, and where the common setups quietly record nothing at all.
Quick Summary: What You’ll Learn
- The two working patterns: record a shell on the bastion, or record on every target.
- Why a bastion used only as a ProxyJump hop can’t see your sessions, no matter what you install on it.
- How to set up tlog (journald-backed recording) and the older
script+ForceCommandapproach. - How auditd and
pam_tty_auditgive you a second, kernel-level record keyed to the original login. - When a recording proxy (Warpgate, The Bastion, Teleport) is the better answer.
How do you record SSH sessions on a Linux bastion host?
You wrap the user’s login shell in a recorder, so everything that passes through the terminal is written somewhere the user can’t edit. On Linux that recorder is usually tlog (tlog-rec-session), which writes to the systemd journal, or util-linux script, launched from an sshd ForceCommand. You then add auditd rules so every executed program is logged against the user’s original login ID, and you ship both off the box.
That works only if the session actually runs a shell on the bastion. If your users connect with ProxyJump, it doesn’t, and that changes the whole design.
Why doesn’t ProxyJump traffic get recorded on the bastion?
With ProxyJump (or ssh -J), the bastion opens a TCP connection to the target and forwards bytes. The SSH session itself is negotiated end to end between your laptop and the target server, so the bastion relays ciphertext. There is no shell on the bastion, no pseudo-terminal, nothing for tlog or script to wrap. The bastion’s logs show “user X opened a forwarded connection to 10.0.1.20:22” and that is all it can know.
The ProxyJump bastion guide explains why that model is good for key hygiene: your private key never sits on the bastion. The price is that the bastion can’t record what happens inside the session. You have three honest options:
- Record on the bastion, connect onward from a shell. Users land in a recorded shell on the bastion and run
sshto targets from there. Everything they type on the target passes through the recorded terminal. The catch is credentials: the onwardsshneeds either keys stored on the bastion or agent forwarding, and agent forwarding has real risks on a shared box. - Keep ProxyJump, record on every target. Install tlog (or the
scriptwrapper) and auditd on the servers themselves. The bastion stays a dumb, hardened hop; recording happens where the commands run. - Use a recording proxy. A purpose-built bastion terminates the SSH connection, authenticates the user, opens its own connection to the target and records the stream in between. More on these below.
What doesn’t work is installing a recorder on a ProxyJump-only bastion and assuming you’re covered.
Which recording approach fits your setup?
- tlog: records terminal output (and input, if you turn it on) plus window size changes, as JSON messages into journald or syslog. Playback with
tlog-play. Packaged on RHEL and Fedora; on Debian it’s in bookworm, testing and unstable but not in trixie at the time of writing, so check your release before you plan around it. script+ForceCommand: available everywhere util-linux is. Records to files with timing data forscriptreplay. Simple, but the recording runs as the user and writes files the user owns, so tamper resistance is weak.- auditd +
pam_tty_audit: not a screen recording. It logs every program executed and, optionally, the keystrokes typed on the TTY, tied to the login UID that survivessudo. Use it next to one of the above, not instead. - Recording proxy: records at the connection level, works with ProxyJump-style clients in some cases, and gives you a replay UI. It is another service to run, patch and back up.
For the trade-offs between shell history, per-command logs and full recordings from an audit point of view, see SSH session logging options compared. This guide is about getting the recording itself working.
How do you set up tlog on a bastion host?
tlog’s recorder, tlog-rec-session, is meant to be a user’s login shell. It starts the real shell under a pseudo-terminal and logs whatever passes between them. By default it prints a notice before the shell starts (“ATTENTION! Your session is being recorded!”), which is also the polite thing to do.
On RHEL or Fedora:
sudo dnf install tlogThe config lives in /etc/tlog/tlog-rec-session.conf. It’s JSON, and comments are allowed. A bastion-friendly version:
{
// the real shell to start under recording
"shell": "/bin/bash",
// write to the systemd journal (the default writer)
"writer": "journal",
"log": {
// record keystrokes too; this captures typed passwords, see below
"input": true,
"output": true,
"window": true
}
}Input logging is off by default for a reason: it records everything typed, including passwords entered at prompts that don’t echo. Turn it on only if you need it, and treat the logs as sensitive.
Then enable recording per user. The simplest way is to swap the login shell:
sudo usermod -s /usr/bin/tlog-rec-session aliceIf you manage users through SSSD, you can do it by user or group without touching each account. In sssd.conf:
[session_recording]
scope = some
groups = bastion-usersSSSD then replaces the shell with tlog-rec-session in its responses and passes the original shell along, so the user’s real shell still starts. The sssd-session-recording(5) man page notes that the groups option has a performance cost, because every uncached user lookup has to resolve group membership.
How do you find and replay a tlog recording?
With the journal writer, tlog copies a few fields into journal fields: TLOG_USER, TLOG_SESSION and TLOG_REC (a host-unique recording ID). Find a user’s recordings, then play one back:
journalctl TLOG_USER=alice -o verbose | grep TLOG_REC=
tlog-play -r journal -M TLOG_REC=<recording-id>tlog-play reproduces the session on your terminal, so make your terminal the same size as the recorded one. --goto jumps to a timestamp, --speed changes playback speed, and --lax plays a recording with missing messages instead of aborting.
Because the data goes to journald, the user can’t open a file and edit their own recording afterwards. That’s the main practical advantage over script.
How do you record with script and ForceCommand?
If tlog isn’t packaged for your distribution, util-linux script does the job with more caveats. Since util-linux 2.35 it can log input and output to one file with a timing file next to it.
Create a log directory that users can write into but not list:
sudo install -d -m 1733 -o root -g root /var/log/ssh-recThe wrapper, /usr/local/sbin/recorded-shell:
#!/bin/bash
# Interactive sessions only: a client-supplied command would skip the recorder.
if [ -n "$SSH_ORIGINAL_COMMAND" ]; then
echo "This bastion accepts interactive sessions only." >&2
exit 1
fi
ts=$(date +%Y%m%d-%H%M%S)
base="/var/log/ssh-rec/${USER}-${ts}-$$"
echo "This session is recorded."
exec script -q -f --log-io "${base}.io" --log-timing "${base}.tm" --command "/bin/bash -l"Make it executable (sudo chmod 755 /usr/local/sbin/recorded-shell), then apply it to a group in /etc/ssh/sshd_config:
Match Group bastion-users
ForceCommand /usr/local/sbin/recorded-shell
DisableForwarding yesForceCommand ignores whatever command the client asked for and runs this one through the user’s login shell. It also applies to subsystems, which means sftp and scp stop working for that group. DisableForwarding turns off TCP, agent, X11 and Unix-socket forwarding, so users can’t tunnel around the recorded shell. It also breaks ProxyJump through this bastion, which is the trade-off from the previous section made explicit. The sshd_config man page adds a warning worth repeating: disabling TCP forwarding doesn’t improve security while users still have a shell, because they can install their own forwarders. Recording is what covers that gap.
Before reloading sshd, test the config and keep a second session open in case you lock yourself out:
sudo sshd -t && sudo systemctl reload ssh # the unit is "sshd" on RHEL and FedoraReplay a session with the timing file:
scriptreplay --log-timing alice-20261019-091502-4242.tm --log-io alice-20261019-091502-4242.ioBe honest with yourself about what this proves. script runs as the user, and the files it writes belong to the user, so the user can modify their own recording. The 1733 directory stops people from listing or deleting other users’ files, not their own. If tamper resistance matters, ship the files off the bastion continuously and back them with auditd, which the user can’t touch.
How does auditd add a second record?
auditd works at the kernel level and tags every event with the audit UID (auid): the ID the user originally logged in with. It stays the same after sudo -i or su, which is exactly the attribution that screen recordings lose when everyone ends up as root.
Log every program a real user executes. In /etc/audit/rules.d/exec.rules:
-a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=unset -k user-exec
-a always,exit -F arch=b32 -S execve -F auid>=1000 -F auid!=unset -k user-execauid>=1000 matches normal users (check UID_MIN in /etc/login.defs on your system), and auid!=unset skips processes that never had a login, such as daemons. Load the rules and query them:
sudo augenrules --load
sudo ausearch -k user-exec -i -ts todayFor keystrokes, pam_tty_audit tells the kernel to audit TTY input for matching users. Add it to the sshd PAM stack (/etc/pam.d/sshd):
session required pam_tty_audit.so disable=* enable=bastion-*The patterns are globs on user names (or UID ranges). Read the input back with aureport --tty -i. The man page is blunt about the side effect: all input to an SSH session is logged, including passwords typed into software on a remote host, because only the local TTY state decides what gets audited. On a bastion where people ssh onward and type sudo passwords on targets, that means those passwords land in your audit log. Decide whether that’s acceptable before you enable it.
Where should the recordings be stored?
Anything that stays only on the bastion can be deleted by whoever compromises the bastion. Three rules hold regardless of the tool:
- Ship off the box, continuously. Forward the journal (tlog) or the audit log to a central collector you already run. The logging options post has a section on centralizing with rsyslog and Vector.
- Set retention on purpose. Recordings with input logging are sensitive data. Keep them as long as your policy requires and no longer; if you’re mapping this to SOC 2, the SOC 2 controls for SSH access post covers what auditors ask to see.
- Tell people. tlog’s notice and the
echoin the wrapper aren’t decoration. Recorded users should know, and a visible notice also deters casual misuse.
When should you use a recording proxy instead?
If you need recording and you also want users to keep connecting with their normal clients, a proxy that terminates SSH is often less work than hardening shells on every host:
- Warpgate (Apache-2.0) sits on the bastion, authenticates users itself, connects to the target and records sessions, with an admin web UI for live sessions and replay. Users keep their normal SSH client and log in with specially formatted credentials that tell Warpgate which target they want.
- The Bastion from OVHcloud (Apache-2.0) records every interactive session with ttyrec and ships helper scripts to encrypt and push recordings to a remote store.
- Teleport also records sessions, with its own access model and client tooling.
The trade-off is the same for all three: you’re adding an authentication system with its own users, upgrades and failure modes in front of every server. For a handful of servers, tlog on each host plus auditd is usually enough. Once you have many people, many targets and a compliance deadline, a proxy starts paying for itself.
Where does termique fit?
termique is an SSH client, not a bastion, so it can’t replace server-side recording: it only sees sessions opened through termique, and anyone using plain ssh bypasses it. What it adds is the client-side layer for teams that share hosts.
On Pro, you can turn on Command logging per host. Every session on that host then records a timestamp, session ID, the input (and terminal output, if enabled), whether the input was pasted, and which termique user ran it. If you share a host with a teammate, they see a consent prompt when they accept, and you can read their session logs afterwards. Two things to know before you enable it: command logs are stored on the termique backend and are not end-to-end encrypted, unlike your passwords and private keys, which are encrypted on your device before they sync. Use it on hosts where that’s acceptable. The per-command audit logs post goes into what this kind of log catches and misses.
termique is a free, native ssh manager we build, with end-to-end encrypted credentials. The free plan covers 3 hosts; Pro is $5 a month for unlimited hosts, host sharing and command logs.
FAQ
How do I record and audit SSH sessions on a Linux bastion host?
Make users land in a recorded shell (tlog as the login shell, or script via ForceCommand), add auditd execve rules keyed on auid, and ship both to a central log store. If users only pass through with ProxyJump, record on the target servers instead, because the bastion only relays encrypted traffic.
Can users bypass SSH session recording?
With a script wrapper, yes in several ways: they own their recording file, and without ForceCommand and disabled forwarding they can request a command or open a tunnel that never touches the recorder. tlog writing to journald and auditd are much harder to tamper with from a normal account. Root on the bastion can bypass anything local, which is why you ship logs off the box.
Does session recording capture passwords?
It can. script --log-io, tlog with "input": true and pam_tty_audit all record typed input, including passwords at prompts that don’t echo. Output-only recording (tlog’s default) doesn’t capture them unless they appear on screen.
How do I replay a recorded SSH session?
For tlog in the journal: tlog-play -r journal -M TLOG_REC=<id>. For script files: scriptreplay --log-timing <file>.tm --log-io <file>.io. Match your terminal size to the recorded one for clean playback.
Is shell history enough for an SSH audit log?
No. History is written by the user’s own shell, can be disabled or edited, and doesn’t record output. It’s useful for convenience, not as evidence.
Try termique free
If your team shares hosts and wants a client-side record of who ran what, download termique and turn on command logging for the hosts that need it. Keep tlog or auditd on the servers for everything that doesn’t go through termique.
