Skip to main content

Session recording: what is captured

Every byte in both directions, in asciicast for terminals and parsed events for RDP, encrypted per session — with no secret redaction, a real player, and nothing yet to feed it.

Written for whoever runs IT14 min readUpdated

A privileged session recording is meant to answer a question months later: what did that administrator actually do on that server. This article says exactly what is captured, how it is protected, how long it is kept — and which parts of it you can rely on in the current release.

What is captured, per protocol

Four protocols can be proxied — ssh, rdp, database and https — and they produce three genuinely different kinds of recording.

What the stored recording contains for each session protocol, and what a reviewer can get out of it.
ProtocolRecording bodyWhat a reviewer can reconstruct
sshAsciicast v2 — a header line, then one JSON array per chunk of the form [seconds, "i"|"o", text]. "i" is what the operator sent, "o" is what the server sent back.Everything that crossed the connection, with direction and timing. This is a real terminal-recording format, so an ordinary asciicast player renders it once decrypted.
rdpOne JSON object per line: keystrokes, mouse events, clipboard use, drive and device redirection, logon results and errors, and graphics updates recorded as type, size and changed rectangle.The text that was typed, the side channels that were used, and a timeline of screen activity. Not the pixels — graphics are deliberately not decoded. See Proxied RDP sessions.
database, httpsThe same asciicast structure over an opaque byte stream. These sessions are a plain TCP relay with no protocol understanding.Very little of interpretive value, and no credential injection happened either — a database session through this path is a port forward. Treat the recording as proof a connection existed, not as a record of what was done.

The terminal dimensions in an asciicast header are the defaults, eighty by twenty-four, because the gateway does not read the terminal-size request. A replay may therefore wrap differently from what the operator saw.

One thing that has not been fixed: the storage key always ends .session whatever the content is. Do not infer the format from the filename; read the media type on the session.

The keystroke index

Alongside the recording body, every session carries a structured index stored in the database rather than in the object store — so it survives independently of the recording, and it is what the console renders.

`offset_ms`
Milliseconds since the session started. Offsets rather than wall-clock timestamps, deliberately: a replay control can scrub without aligning against a clock, and an offset cannot be made ambiguous by a timezone or a leap second.
`direction`
client->target for what the operator sent, target->client for what the server sent back, and — on RDP only — gateway for events the gateway itself generated, such as the credential injection.
`summary`
A short preview of the content, one entry per four-kilobyte chunk. Bytes outside the printable range become full stops.
`size`
How many bytes that entry represents.

There is no secret redaction. None

The operational advice that follows is short: never authenticate a second time inside a proxied session. The whole point of credential injection is that the secret never crosses the operator's keyboard, and typing one anyway puts it into an evidence file that outlives the session by a year.

How a recording is encrypted

  1. A new 256-bit data key is generated for that one session and exists nowhere else.
  2. The recording is sealed with AES-256-GCM under it. GCM is authenticated encryption: a modified recording fails to decrypt rather than decrypting to something plausible.
  3. The data key is itself encrypted under a key derived from the deployment's master key, and the wrapped key is stored beside the recording. The plaintext data key is discarded.
  4. A SHA-256 of the recording before encryption is stored as an integrity value. That is what you cite if you ever need to demonstrate that a recording you exported is the one that was made.

Objects are tagged with the organisation id, the session id and a media marker, so an operator looking at the storage account can tell whose data a blob is without decrypting it.

Two honest notes about the keys. The same master key protects session recordings, vaulted credentials and integration credentials — it is not the PAM key in practice but the key for every secret the platform holds on your behalf. And the only key wrapper that exists is a local one derived from that environment variable: the interface was designed so a cloud KMS could be plugged in without touching the service layer, and no such wrapper has been written. If you were told this deployment wraps keys in a managed KMS, it does not.

Where recordings are stored

One of three backends, chosen by configuration: a local directory, Azure Blob with a shared-access signature, or Azure Blob with a managed identity. A backend that is configured and broken is a fatal start-up error — the service refuses to run rather than accepting privileged sessions it cannot record.

A deployment with no backend configured falls back to an in-process store, and says so at start-up in as many words:

PAM session recordings are IN-PROCESS ONLY: no recording object store is configured, so every recording is lost on restart and transcript search will find nothing.
the API's start-up log

If you are running Qorionix yourself, that line is the one to check before you promise an auditor anything. In a hosted deployment a store is configured.

Recording is also switched off entirely for a session whose governing policy says so, and it is switched off in two independent places rather than one — nothing is buffered, and the recorder itself is replaced with one that discards. Asking for a recording of such a session returns 404 with the reason spelled out: session X was not recorded (record_session was false for the governing policy).

Retention, and the floor you cannot go below

There is a retention policy now, it is enforced by a sweep every six hours, and it has a deliberate floor.

The retention bounds, and what each one is for.
SettingValue
Minimum30 days. You cannot set less.
Default365 days
Maximum2,555 days — seven years

A value outside that range is refused with invalid request: retention_days must be between 30 and 2555. The floor exists for one reason, and the code says it out loud: retention is the one setting a tenant administrator can use to destroy the privileged-access audit trail, and somebody covering their tracks would set it to one day.

A purge is a crypto-shred and it is done in a deliberate order: the object first, the transcript second, the database row last. The row survives with the recording's digest, size and duration intact and its wrapped data key destroyed — so purged under policy stays distinguishable from never recorded, which is a distinction an auditor will eventually ask you to make. Every purge is audited as pam.recording.purge, and a purge that fails records the failure rather than passing over it.

Reading or changing the retention rule needs pam:session_content.manage. If the sweep cannot start, the service warns that recordings will be kept indefinitely rather than failing silently.

The player, and the part that does not work

There is a real, purpose-built session player, and it is wired into the Privileged Sessions screen (/pam/sessions), which describes itself accurately:

Live and archived privileged sessions brokered through the gateway. Recordings play here; every replay is written to the evidence chain.
the Privileged Sessions screen

It is not a video player, because there is no video to play. What it does depends on the protocol:

  • SSH, database and HTTPS sessions replay as text: the captured terminal output is written out character by character, at the pace it originally happened. Pauses are real pauses. Watching one feels like watching over somebody's shoulder, because that is structurally what it is.
  • RDP sessions show a timeline of individual recorded events instead — keystrokes, mouse activity, clipboard use, file access, window changes, screen updates and sign-on — rendered as a readable list rather than as pixels. There is still no video for RDP and there never will be for a session already recorded, because the graphics were never decoded. What you get is a structured account of what happened, in order, with times.

The controls are what you would expect of a player: play and pause, a seek bar, a clock, and an adjustable playback speed for when a session contains twenty minutes of somebody reading a log file. Beside it is a panel listing just what was typed — for an RDP session, a comparable per-event summary — and clicking an entry jumps the playback straight to that moment. In practice that panel, not the seek bar, is how you find the thing you came for.

What you can rely on today, and what to tell an auditor:

  • The session metadata is durable and complete — who opened it, against which credential and target, when, its outcome, the injection evidence, the channel events and the keystroke index. All ordinary database rows.
  • The recording body is stored, encrypted and retained, and its integrity hash is recorded. It is evidence; it is just not evidence you can currently watch in a browser.
  • The transcript is searchable, which for most investigative questions is more useful than replay anyway. See Session transcripts.
  • Do not promise a screen replay. Not for RDP, where graphics are deliberately not decoded, and not for SSH, where there were never any graphics.

Reading a recording needs pam:session_content.read. That permission exists because the recording used to be gated on the ordinary session-read permission, which every read-only account holds — meaning every viewer in every tenant could obtain the recording of any privileged session. The new permission is granted to no built-in role except admin.

The Evidence tab

Alongside the sessions list on the same screen there is an Evidence tab. It is a different thing from a single session's transcript, and the two get confused because they sit next to each other. A transcript is the content of one session. The Evidence tab is the tamper-evident, hash-chained audit trail of privileged actions across the organisation — the same chain described in Access requests and approvals and, for the vault side, in The credential vault.

Each entry is linked to the one before it by a hash, so an entry cannot be altered or removed without breaking every link after it. Two controls make that useful rather than theoretical:

**Verify chain**
Checks every entry against the one before it and tells you plainly whether the chain is intact or broken — and, if it is broken, at which entry. That is the number to quote when you report it. Run this before you export anything you intend to rely on.
**Export CSV**
Saves the whole chain as a file, for an auditor, an insurer, or your own records. It is an export of the trail rather than of any recording, so it contains no session content.

Viewing a recording is itself an audited action

Reading a session's recording writes its own entry to the audit trail, pam.session.recording.read, naming who looked, when, and the reason they gave in the dialog (recorded only when a reason was actually typed). This applies regardless of whose session it was — including to the person whose session it is. Being the subject of a recording does not, by itself, entitle you to see it; that is governed by the permission, not by whether your name is on the row. A second, narrower event, pam.session_content.read, is written alongside it recording the reader and the address they connected from.

A failed attempt is audited too, not only a successful one — the same pam.session.recording.read event, carrying the error instead of the recording's size and format. Somebody probing for a recording they are not allowed to see leaves a record of the attempt, which is the entry you would actually want to find, and the one a system that logged only successes would throw away. Both the success and the failure are also appended to the tamper-evident evidence chain described in the Evidence tab above, alongside every other privileged action — reading evidence is treated as evidence in its own right. One deliberate omission: the recording's own presigned link is never written into the audit row, because that link is itself a bearer credential for the recording and the audit store has different access controls than the recording store.

The principle is worth stating in one line, because it is the reason the reason-for-viewing dialog exists at all: reading privileged evidence is a privileged action in its own right. A recording contains keystrokes, pasted key material and any password a target prompted for. Somebody reading one is doing something at least as sensitive as the session that produced it, and the trail treats it that way.

What is recorded outside the recording

The audit trail around a session is separate from the recording and more dependable than it, because it is ordinary rows. Four events are written for every proxied session, and one more when something is wrong:

  • pam.gateway.listening — a port was bound for this session, with the host and port.
  • pam.gateway.connected — an operator connected, with the address they connected from.
  • pam.gateway.session_closed — the session ended, with the outcome and the recording reference.
  • pam.gateway.identity_mismatch — the identity the target logged on is not the identity we injected. Raised as its own event rather than as a field on the close event, precisely so it cannot be missed.

There is a fifth worth watching for, written by the background reaper rather than by a person: pam.session.expire, with terminated_by: policy and the overrun in seconds. It is deliberately distinct from a human termination, because does this platform enforce session duration limits is a question an auditor answers by querying for that action, and folding it into the human event would make the enforcement invisible.

One reported number is currently wrong and you should ignore it: the bytes-in and bytes-out figures on an SSH, database or HTTPS session. Bytes-in reports the size of the encrypted recording blob and bytes-out reports zero. RDP sessions report both correctly.

What to do next

  1. Search a transcript — it is the working half of this feature and the one that answers investigative questions.
  2. Read how a proxied SSH session works, including what credential injection does and does not cover.
  3. Read what an RDP recording contains before promising anyone a screen replay.
  4. Set your retention deliberately rather than leaving it at a year, and remember it applies to recordings made after the change.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in privileged access