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

  1. User connects via msh ssh. The proxy routes the connection through a reverse tunnel to the agent.
  2. The agent allocates a PTY and starts the user's shell. A recording stream is opened to the auth service via gRPC.
  3. 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.
  4. 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_recording picks the recording strategy for the whole cluster: node, node-sync, proxy, or off. 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_MODE on 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.

Cluster-wide recording mode bash
# 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:

Per-agent override bash
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.

Default local storage (no configuration needed) bash
# 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.

S3 storage configuration (environment variables) bash
# 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.
Enable recording encryption bash
# 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:

Session playback bash
# 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.

Find and play sessions from audit events bash
# 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.

Agent recording environment variables bash
# 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 recordingAvailable
Real-time streaming (node-sync)Available
Async upload (node)Available
Local filesystem storageAvailable
S3-compatible storageAvailable
AES-256-GCM encryption at restAvailable
msh play CLI playbackAvailable
Command-capture recording (Linux, /proc-based)Available
Agentless node recording (SSH MITM)Available
Web UI session playerPlanned

Next Steps