Operate
Recording & playback
Session recording is the feature auditors open Wardengate for, and it is the feature users forget exists ten seconds into a shell. This page covers what is captured for each protocol, how it is stored and encrypted, how to play recordings back, and the knobs that keep recordings useful without making them a liability.
What is captured
Capture is protocol-aware. A recording is not a raw packet dump — it is the minimum structured stream needed to render the session back at high fidelity while keeping storage bounded.
SSH sessions
The PTY stream is captured in an asciicast-style format: a header with terminal dimensions and environment, followed by timestamped frames of output bytes. Keystroke-level input is recorded separately so playback can replay typos, backspaces, and interrupted commands. A command metadata sidecar records each parsed shell command, the working directory when known, and the exit status. SSH channel events — port-forward open, exec channel, SFTP open — are captured as structured entries alongside the PTY stream.
RDP sessions
The framebuffer is encoded on the gateway as H.264 at a configurable rate (default 8 fps at 85 % quality) and written in 30-second chunks. Clipboard events, printer redirections, drive-mount requests, and smart-card redirections are recorded as structured sidecar events. Keyboard input is captured as scancodes so playback can distinguish typed passwords from pasted text without persisting the password itself — the character stream is masked by the redaction layer when it targets a known secret input field.
Database sessions
Database recordings are query-level, not wire-level. Each statement is parsed by the gateway, classified (DQL/DML/DDL/DCL), and written with the bind parameter count, the affected row count, the execution time, and the returned column schema. Result sets are not persisted by default — most organisations do not want exfiltration-grade logs — but you can opt in per policy with row count caps.
Storage layout
Every session writes to a deterministic prefix under the configured bucket. Chunks are small enough to stream individually and large enough not to thrash object-storage request budgets.
s3://wg-recordings/
2026/04/20/
sess_a91c7f20bb48/
manifest.json # session metadata + chunk index + hashes
index.jsonl # parsed commands / queries / rdp events
stream-000.cast.enc # SSH asciicast chunk (encrypted)
stream-001.cast.enc
frame-000.h264.enc # RDP H.264 chunk (encrypted)
frame-001.h264.enc
sidecar.jsonl.enc # clipboard, channel, redirection events
seal.sig # detached signature of manifestEach chunk is encrypted on the gateway with a per-session data key that is itself wrapped with the cluster KMS key. The control plane never holds plaintext recording bytes — playback streams through the control plane but decryption happens on the viewer’s browser with a short-lived data key fetched over an authenticated channel.
Playback UI
The operator console embeds a viewer that adapts to the protocol. SSH recordings render into a terminal emulator, RDP recordings play as video, and database recordings appear as a chronological list of statements with expandable bind metadata. All three share the same chrome: a scrubber, a speed control (0.25x through 8x), a keyboard-driven frame step, and a split-pane timeline that shows command or query markers next to the stream.
Jumping to a specific command is one click: select it in the index panel and the stream seeks to the exact frame. The viewer also accepts a deep link of the form /recordings/sess_a91c7f20bb48?t=00:14:22 so tickets, audit notes, and Slack messages can link straight to the moment in question.
Search within a session
Search targets the parsed index, not the raw stream — it is effectively instant even for multi-hour sessions. Queries are plain substring by default; wrap in slashes for regex. Results highlight the matching command or frame and let you seek the viewer to any hit.
wgctl recording search sess_a91c7f20bb48 "sudo"
00:02:14 sudo -i
00:04:57 sudo systemctl restart nginx
00:11:03 sudo journalctl -u nginx -n 200
wgctl recording search sess_a91c7f20bb48 '/^DROP /i'
00:23:51 DROP TABLE staging.imports;Speed and navigation controls
space— play / pause.j / k— seek back / forward 5 s.[ / ]— previous / next parsed command.1…8— set playback speed 0.25x / 0.5x / 1x / 2x / 4x / 8x.s— skip idle gaps (any stretch with no output).
“Skip idle” is what makes a four-hour recording reviewable. Most sessions are mostly waiting; the viewer compresses those gaps to a configurable minimum without losing the timeline.
Exporting recordings
Exports are supported for handing recordings to auditors, legal, or third-party investigators who cannot be given console access. Every export produces its own audit event and, on request, a signed manifest covering the exported artefacts.
| Format | Protocols | Typical use |
|---|---|---|
| MP4 (H.264 + AAC-silence) | RDP, SSH (rendered) | Sharing with auditors / legal review |
| SVG (animated) | SSH | Embedding in postmortems and tickets |
| asciicast v2 | SSH | Open-format long-term archive |
| JSONL transcript | DB, SSH, RDP sidecars | Feeding into a SIEM or data warehouse |
wgctl recording export sess_a91c7f20bb48 \
--format mp4 \
--out /tmp/sess_a91c7f20bb48.mp4 \
--sign
wrote /tmp/sess_a91c7f20bb48.mp4 (14.2 MB)
wrote /tmp/sess_a91c7f20bb48.mp4.manifest (signed, 1.1 KB)Redaction rules
Redaction runs on the gateway before bytes are written to storage — a redacted recording never contains the secret, even transiently. Rules are declared per policy and apply to both input and output streams. The engine understands three matcher types: named secrets (resolved against the vault catalog), regex patterns, and structured field matchers for RDP UI elements (password input widgets) and DB bind parameters.
apiVersion: wardengate/v1
kind: Policy
metadata:
name: prod-ssh-write
spec:
recording:
mode: full
redact:
- secret: AWS_SECRET_ACCESS_KEY
- secret: DATABASE_URL
- regex: "(?i)api[_-]?key\\s*[=:]\\s*['\"]?([A-Za-z0-9_-]{20,})"
replace: "api_key=<redacted>"
- regex: "-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"
scope: output
replace: "<redacted private key>"
- field: rdp.password_input
- field: pg.bind[0]
where: "query LIKE 'UPDATE users SET password_hash%'"Redaction is one-way: the gateway keeps a rolling hash of the matched bytes so analysts can prove a specific secret was present at a given frame without ever recovering it. False negatives are the failure mode that matters here, so redaction rules are dry-run-able against historical recordings to see what they would have caught.
Retention policies
Retention is declared per policy, not globally — high-privilege sessions often need to live longer than routine ones. The default is 365 days; the minimum the UI will accept without an explicit acknowledgement is 30 days.
spec:
recording:
retention:
days: 1825 # 5 years for prod DB writes
mode: worm # object-lock: retention period is immutable
legalHold: false # promote to true via API on subpoenaSetting mode: worm enables S3 Object Lock (or the equivalent on other backends) for the recording prefix. Legal hold overrides the configured retention and blocks deletion entirely; the hold release is its own audit event. Expiry runs daily and emits a recording.expired event per deleted session so retention itself is provable after the fact.
Access to recordings
Viewing a recording is itself an auditable action. The default role set grants recording read only to auditor and admin; operators can see their own sessions and nothing else. Every playback, every search hit, and every export lands in the recording-access log so you can answer “who watched Alice’s session last Tuesday?” without ambiguity.
Related
- Session management — live controls for the sessions being recorded.
- Audit & reporting — how recording access is logged alongside other privileged events.
- Backup & restore — treating the recording bucket as part of your DR plan.