Wardengate

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 manifest

Each 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.

FormatProtocolsTypical use
MP4 (H.264 + AAC-silence)RDP, SSH (rendered)Sharing with auditors / legal review
SVG (animated)SSHEmbedding in postmortems and tickets
asciicast v2SSHOpen-format long-term archive
JSONL transcriptDB, SSH, RDP sidecarsFeeding 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 subpoena

Setting 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