Session Recording
Mezite records every SSH session, capturing the full terminal input and output stream. Recordings provide a complete audit trail of what happened during a session and can be replayed for incident review, compliance, or training.
Recording is on by default for agent-based nodes and needs
no additional configuration: mezd records every session it terminates
and uploads it to the auth service for central storage and playback. The default
mode streams each recording to the auth service in real time as the session
runs, rather than waiting until the session ends — see Recording Modes below
for what that trades off.
How Recording Works
Session recording happens at the agent (the mezd
process running on the target node), not at the proxy. This is an important
architectural decision:
- The proxy routes SSH connections between clients and agents via encrypted tunnels. It only sees the raw SSH protocol stream (key exchange, encrypted packets) — not the actual terminal content.
- The agent terminates the SSH connection and allocates a PTY (pseudo-terminal) for the user's shell. It has direct access to the clean, decrypted terminal I/O — the commands typed and the output displayed.
By recording at the agent after SSH decryption and PTY allocation, Mezite captures exactly what the user sees in their terminal — clean text, not encrypted protocol bytes.
Recording Flow
-
User connects via
msh ssh. The proxy routes the connection through a reverse tunnel to the agent. - The agent allocates a PTY and starts the user's shell. A recording stream is opened to the auth service via gRPC.
- As terminal I/O flows through the PTY, each chunk is written to a local recording file and simultaneously streamed to the auth service with millisecond-precision timestamps.
- When the session ends, the recording is finalized. The auth service stores it in the configured backend (local filesystem or S3) and links it to the session's audit events.
What Gets Recorded
Session recording captures terminal I/O — the byte stream between the user's terminal and the remote shell:
- All commands typed by the user (stdin)
- All output displayed in the terminal (stdout/stderr)
- Terminal control sequences (colors, cursor movement, screen clears)
- Timing information for each chunk (enabling real-time playback at any speed)
Recordings do not capture:
- File transfer contents (SCP/SFTP payloads)
- Port-forwarded traffic
- Activity outside the Mezite-proxied session
Recording Modes
Recording mode is a cluster-wide setting,
session_recording, that every agent follows automatically.
There is also a per-agent override for the rare case where one node
deliberately needs to behave differently from the rest of the fleet:
- The cluster setting
session_recordingpicks the recording strategy for the whole cluster:node,node-sync,proxy, oroff. Agents pick this up over the connection they already maintain to the proxy and apply it to every session started after the change — a session already in progress keeps the mode it started under. Default when never explicitly set:node-sync. -
MEZITE_RECORDING_MODEon a given agent pins that one agent's mode permanently, ignoring the cluster setting entirely. Leave it unset (the default) so the agent follows the cluster; set it only when that specific node needs to diverge.
If an agent can't yet reach the proxy to learn the cluster value — e.g.
immediately at startup, before its first successful connection — it
defaults to node-sync rather than the weaker
node, so a moment of uncertainty about the cluster's
setting never silently downgrades recording.
Agentless OpenSSH sessions are the exception: the proxy is the
man-in-the-middle for those, and it always records them regardless of
session_recording. Recording is mandatory on that path — if
the recorder cannot be created, the session is refused rather than run
unrecorded.
| Mode | Where recorded | Upload | Integrity |
|---|---|---|---|
node | Agent | After session ends (bulk upload) | Session survives an unreachable auth service |
node-sync (default) | Agent | Real-time streaming via gRPC | See node-sync below — not a strict no-loss guarantee |
proxy | Proxy | Written by the proxy | Raw channel bytes, not clean terminal text |
off | — | — | No recording |
| Agentless MITM | Proxy | Written by the proxy |
Always on regardless of session_recording — session refused
if the recorder cannot start
|
node-sync
The agent records terminal I/O locally and streams each chunk to the auth service in real time via gRPC as the session runs, rather than waiting until it ends. This bounds how much of a session a crash, a kill, or the host going away can cost you — recorded output is already on the auth service, not sitting only in a local file that dies with the host.
It is not an absolute no-loss guarantee. If the agent can't open the
stream to the auth service in the first place (the auth service is
unreachable when the session starts), the session proceeds with
local-only recording instead of being refused — that session gets node-equivalent durability, not node-sync's. Once a stream has been established, though, a later break (network failure, auth service restart)
terminates the session rather than silently continuing to record only locally
— so a session that's actively streaming can't quietly lose its real-time
guarantee without you finding out.
This is the recommended mode for compliance-sensitive environments — it's also the default, both for the cluster setting when never explicitly configured and for an agent's own fail-safe if it hasn't yet learned the cluster's setting.
node (Async)
The agent records terminal I/O to a local file. After the session ends, the recording is uploaded to the auth service via gRPC. The session continues even if the auth service is temporarily unreachable — the upload is retried later.
Use this mode when session availability is more important than recording durability (e.g., the agent must keep serving sessions even if the auth service is down for an extended maintenance window), and you're willing to accept that a host failure between session start and upload loses that session's recording.
Changing the Recording Mode
The cluster setting is the normal way to change how the whole fleet records. It's stored in the auth service; agents pick it up over the connection they already maintain to the proxy (at most a few seconds of propagation delay) and apply it to sessions started from then on — sessions already in progress keep the mode they started under.
# View current value
mezctl config get session_recording
# Stream to the auth service in real time (the default)
mezctl config set session_recording node-sync
# Record locally, upload after the session ends
mezctl config set session_recording node
# Have the proxy record instead (lower fidelity — raw channel bytes)
mezctl config set session_recording proxy
# Turn recording off cluster-wide
mezctl config set session_recording off
To pin one agent to a mode that ignores the cluster setting entirely,
set
MEZITE_RECORDING_MODE in its environment and restart it. This
is a permanent override for that agent — the cluster setting is delivered
to it just the same, but it's ignored until the environment variable is removed:
echo 'MEZITE_RECORDING_MODE=node-sync' | sudo tee -a /etc/mezite/agent.env
sudo systemctl restart mezd Storage
Session recordings are stored by the auth service. Two storage backends are available:
Local Filesystem (Default)
Recordings are stored on the auth service's local filesystem. This is suitable for single-node deployments and development.
# Recordings are written under <data_dir>/recordings/, e.g.
# /var/lib/mezite/recordings/<session-id>.rec
# List what the cluster knows about, rather than reading the disk:
mezctl recordings ls S3-Compatible Storage
For production deployments, configure an S3-compatible backend for durable, scalable recording storage. This works with AWS S3, MinIO, or any S3-compatible object store.
# Set the storage backend to S3
MEZITE_RECORDING_BACKEND=s3
# S3 bucket and region
MEZITE_S3_BUCKET=mezite-session-recordings
MEZITE_S3_REGION=us-east-1
# Credentials (or use IAM roles if running on AWS)
MEZITE_S3_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
MEZITE_S3_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Optional: prefix for S3 keys
MEZITE_S3_PREFIX=recordings/
# Optional: force path-style addressing (required for MinIO)
MEZITE_S3_FORCE_PATH_STYLE=true
# Optional: custom endpoint (for MinIO or other S3-compatible stores)
MEZITE_S3_ENDPOINT=http://minio:9000 Encryption at Rest
Recording encryption at rest is conditional. It is on
only when recording_enc_key /
MEZITE_RECORDING_ENC_KEY is set; if it is empty, recordings are
written to the backend (local or s3) in the
clear. Treat this as a deliberate operator choice, not a default.
When set, the value must be a hex-encoded 32-byte key. Each recording chunk is sealed with AES-256-GCM before being handed to the storage backend, so encryption covers both local-filesystem and S3 paths. The recording structure (timestamps, directions) remains parseable without the key — only the terminal-I/O payload is encrypted — which keeps playback indexing fast while still requiring the key to actually replay a session.
Rotating MEZITE_RECORDING_ENC_KEY orphans the previous recordings:
their chunks can no longer be replayed without the old key. Keep the old value
alongside the new one until the retention window for the old recordings has
elapsed, then retire it.
# Generate a 32-byte hex-encoded AES-256 key (one-time)
openssl rand -hex 32
# Wire it into the cluster
export MEZITE_RECORDING_ENC_KEY=3d992ae268214710f48d680749460...
mezhub --config=mezite.yaml Playback
Use msh play to replay a recorded session in your terminal:
# List recent sessions
msh sessions ls
# SESSION ID USER NODE LOGIN STARTED ENDED
# a1b2c3d4-e5f6-7890-abcd-ef1234567890 alice web-server-01 ubuntu 2026-04-11T10:28:35Z 2026-04-11T10:41:09Z
# Play back a session
msh play a1b2c3d4-e5f6-7890-abcd-ef1234567890
# Playing session a1b2c3d4... (user: alice, host: web-server-01)
# ubuntu@web-server-01:~$ ls -la
# total 48
# drwxr-xr-x 6 ubuntu ubuntu 4096 Apr 11 10:28 .
# ...
# Play at 2x speed
msh play --speed=2 a1b2c3d4-e5f6-7890-abcd-ef1234567890 Audit Integration
Session recordings are linked to the corresponding session.start
and session.end audit events. Each audit event includes the session
ID, user, node, login, and duration — the recording adds the full terminal
I/O for that session.
# List recorded sessions (admins see all; non-admins see their own)
msh sessions ls
# Filter recordings by user via the admin CLI (admin only)
mezctl recordings ls --user alice
# Pull a recording to disk
mezctl recordings download <session-id> --output ./session.cast
# List session.end events from the audit log
mezctl audit ls --type=session.end --since=24h Permissions
Access to session recordings is controlled by RBAC:
- Users can view recordings of their own sessions.
- Admins can view all recordings and manage recording settings.
Agent Configuration
Recording mode itself normally needs no per-agent configuration — see
Changing the Recording Mode above for the cluster setting that controls
it. MEZITE_RECORDING_MODE exists only to pin an individual agent
away from the cluster setting.
# Pins this agent's recording mode, ignoring the cluster session_recording
# setting entirely. Leave unset (the default) so the agent follows the
# cluster; set only when this specific node needs to diverge from the fleet.
MEZITE_RECORDING_MODE=node-sync
# Enhanced command-capture stream (Linux only)
# When on, the agent emits a structured per-command stream alongside the
# full terminal-I/O recording so compliance queries can answer "did X run"
# without scanning the full recording. The env var is named MEZITE_BPF_ENABLED
# for forward compatibility, but the current implementation polls the
# session shell's process tree under /proc — it is not true eBPF today.
# Very short-lived commands (sub-100ms) may be missed by the poll. The
# full terminal recording always captures everything regardless of this flag.
MEZITE_BPF_ENABLED=true Agentless Nodes
Agentless OpenSSH nodes (registered with mezctl nodes add --openssh) are recorded via SSH MITM. The proxy terminates the
client's SSH session and opens a separate SSH connection to the target
host using a short-lived certificate signed by the Mezite User CA. The
proxy sits between two decrypted SSH sessions and records the plaintext
channel data — exactly what the user types and sees.
This requires no additional configuration beyond the standard agentless
setup (TrustedUserCAKeys on the target host). Recordings appear
in msh sessions ls and can be played back with
msh play just like agent-recorded sessions.
Current Status
| Feature | Status |
|---|---|
| Agent-side PTY recording | Available |
| Real-time streaming (node-sync) | Available |
| Async upload (node) | Available |
| Local filesystem storage | Available |
| S3-compatible storage | Available |
| AES-256-GCM encryption at rest | Available |
msh play CLI playback | Available |
| Command-capture recording (Linux, /proc-based) | Available |
| Agentless node recording (SSH MITM) | Available |
| Web UI session player | Planned |
Next Steps
- SSH Access — Set up SSH access and node agents.
- Audit Logging — Query audit events linked to session recordings.
- RBAC Configuration — Control who can view session recordings.
- Reverse Proxy Deployment — Deploy MezHub behind a TLS-terminating load balancer.