Skip to content
slopscale
Esc
↑↓navigate↵open⌘Jpreview
On this page

SSH session recording

Record Tailscale SSH sessions as asciinema files with the embedded recorder or tsrecorder, enforce recording per rule, and list or download recordings.

Session recording keeps the terminal of every SSH session that Tailscale SSH admits, the way Tailscale’s session recording does. The client on the machine being logged into streams the session to a recorder node as it happens, and the recorder writes it to an asciinema file. Slopscale can run the recorder inside the server, or the policy can name any other recorder, such as Tailscale’s tsrecorder.

The embedded recorder

Turn it on in the config file:

ssh_recording:
  enabled: true
  dir: /var/lib/slopscale/recordings
  state_dir: /var/lib/slopscale/recorder
  retention: 2160h

The server then joins its own tailnet as a node named slopscale-recorder with the tag tag:slopscale-recorder, approved and reachable on port 80 from every machine, and stores each session under dir as one .cast file. state_dir holds the node’s keys, so it keeps its identity across restarts; on the first start the server mints a short-lived pre-auth key for it. retention deletes recordings older than the duration, hourly; leave it unset to keep them.

The embedded recorder is a default recorder for every SSH rule. It needs no entry in the policy, and the policy needs no rule for it: the server adds a grant that lets every machine reach every recorder on its port.

Other recorders

The tailnet default is a setting: slopscale settings set --ssh-recorders tag:recorder, POST /api/v1/settings with sshRecorders, or the SSH session recording section of the console’s Settings page. A recorder is a tag, a host from the policy’s hosts section, or a tailnet address. The default applies to every SSH rule that names no recorder of its own.

A rule names its own with recorder, which wins over the default, and can require it with enforceRecorder:

{
  "ssh": [
    {
      "action": "accept",
      "src": ["group:ops"],
      "dst": ["tag:prod"],
      "users": ["autogroup:nonroot"],
      "recorder": ["tag:recorder"],
      "enforceRecorder": true
    }
  ]
}

A check rule records like an accept rule once the check passes.

Enforcement

Without enforcement, a session whose recorder cannot be reached goes on unrecorded, and the client reports the failure to the server. With it, the client rejects the session when no recorder answers, and ends a session whose recording breaks off. The tailnet-wide switch is slopscale settings set --ssh-recording-enforce, sshRecordingEnforce in the API, or Require recording in the console; it covers the default recorders. A rule’s enforceRecorder covers the rule’s own recorders, or the default ones when the rule names none.

Every failure the client reports lands in the audit log as ssh.recording.rejected, ssh.recording.terminated or ssh.recording.failed on the machine that was logged into, with the connecting machine, the SSH user and the recorders tried, and fires the sshRecordingFailed webhook event.

Recordings

Recordings from the embedded recorder are listed newest first on the console’s SSH sessions page, by slopscale ssh-recordings list, or through /api/v1/ssh-recording. Each carries who connected from where, the SSH user and local account, the command when one ran instead of a shell, the file’s size and whether the upload ended cleanly. A recording still being uploaded is listed as such.

$ slopscale ssh-recordings list
$ slopscale ssh-recordings download -i 12
$ asciinema play ssh-recording-12.cast

download writes the asciinema file, -o - sends it to standard output, and delete removes the recording and its file. Listing and downloading need the logs:configuration:read scope, which every admin role and the auditor hold; deleting needs logs:configuration.

Recordings sent to another recorder live wherever that recorder keeps them; Slopscale only knows about its own.

How it works

The server puts the recorders’ addresses on the SSH action it sends the machine being logged into, with /machine/ssh/event as the address to report failures to. When a session starts, that machine’s client posts the session to the first recorder that answers. The embedded recorder speaks the same upload protocol as tsrecorder, one HTTP POST per session whose body is the asciinema stream, so a client that works with Tailscale’s recorder works with it unchanged. Recordings are indexed in the ssh_recordings table, and the recorder’s node is an ordinary tagged node in every other respect.

Last updated on September 27, 2026

Was this page helpful?