Skip to content

Read-only policies

Availability SSH read-only policy: Pro and up. Configurable Kubernetes/database policies: Ultimate.

Read-only sessions block mutating actions. The defaults work out of the box; the lists are configurable under Settings → Policy. For the concept and its limits, see Read-only sessions.

For SSH read-only sessions, Tessera matches the first word of each command against a blocklist. Blocked commands are refused and recorded. A few things are always blocked regardless of the list:

  • output redirection (>, >>),
  • the entire SFTP subsystem — refused in read-only, so file transfer is blocked in both directions (downloads too, not just uploads); scp runs as an exec command and is refused the same way (see below),
  • write subcommands of service managers,
  • running a script by path (./deploy.sh, /opt/x) or via an interpreter (bash x.sh, python app.py) — since its contents can’t be vetted,
  • non-interactive exec requests (ssh host <cmd>) are refused entirely; read-only is for an interactive shell.

The blocklist ships with a broad default set — not just rm/chmod/reboot, but privilege escalation (sudo, su, doas), package managers, user/group management, network and firewall configuration, kernel modules, and more. It’s editable under Settings → Policy — add commands, remove ones you don’t want, or reset to defaults. Changes apply to new read-only sessions.

Beyond the blocklist, Tessera also refuses TCP/socket port forwarding (ssh -L/-D) in read-only sessions, un-wraps commands hidden behind wrappers (env, timeout, nohup, xargs, nice, …) to re-check the real command, and blocks find with mutating actions (-delete, -exec).

The only thing that can truly stop a shell from writing is the operating system on the target. So make the injected credential a read-only OS account, and let the account — not the filter — be the boundary.

1. Create a read-only account on the target. On Ubuntu/Debian:

Terminal window
# No password login, no sudo. The account exists only for Tessera to log into.
sudo adduser --disabled-password --gecos "Tessera read-only" tessera-ro
# Read access to the logs an operator actually needs.
sudo usermod -aG adm,systemd-journal tessera-ro
# Install the public key of the SSH key you will store in Tessera.
sudo install -d -m 700 -o tessera-ro -g tessera-ro /home/tessera-ro/.ssh
sudo tee /home/tessera-ro/.ssh/authorized_keys < tessera-ro.pub >/dev/null
sudo chown tessera-ro:tessera-ro /home/tessera-ro/.ssh/authorized_keys
sudo chmod 600 /home/tessera-ro/.ssh/authorized_keys

Verify the account cannot escalate — sudo -l -U tessera-ro should report nothing — and that it is in no group granting write access to what you care about (docker is the usual mistake: it is root by another name).

2. Register the host a second time in Tessera. A target holds exactly one credential, so the read-only account needs its own target. Add a second server with the same host and port and a name that says what it is — for example web-01 and web-01 (read-only) — and set its credential to the tessera-ro account. Target names are unique, hosts are not, so this is expected usage.

3. Grant users read-write on the read-only target. This looks backwards and is not: the OS account is what makes the session read-only, and Tessera’s grant mode only controls whether the command filter runs on top. Granting RW gives users the full, unfiltered shell that the account already constrains — scripts, pipelines and scp keep working, and nothing depends on a blocklist being exhaustive.

Grant RW on the privileged target only to the people who are supposed to change things.

Filtered by TesseraConstrained by the OSUse it for
RO grant on the privileged targetyesnopreventing accidents; a fast, no-setup guardrail
RW grant on the read-only targetnoyesan actual read-only guarantee
RW grant on the privileged targetnonomaking changes

Both targets are audited and recorded identically, and the audit records which account was used, so you can still tell the two paths apart afterwards.

The cost is one extra target per host — which counts against your plan’s target cap — and the account has to be created on each machine, so this pattern earns its keep on the servers where the guarantee matters rather than on all of them. Everywhere else, rely on grants, JIT access, session recording and audit as the primary controls, with the read-only filter as the guardrail.

For database read-only sessions the proxy is protocol-aware: it parses the PostgreSQL/MySQL wire protocol and allows only recognized read statements through — anything not identified as a read (INSERT, UPDATE, DELETE, CREATE, DROP, TRUNCATE, and so on, including unrecognized statements) is rejected. It’s an allow-list floor, not a blocklist.

This is backed at the engine level, not just keyword filtering: PostgreSQL sessions set default_transaction_read_only=on, and MySQL sessions issue SET SESSION TRANSACTION READ ONLY — failing closed if that can’t be applied. Prepared-statement writes are re-checked at execute time, so a live read-write → read-only downgrade also catches statements that were prepared while the session was still read-write.

For Kubernetes read-only sessions, all mutating API verbs (create, update, patch, delete) are filtered at the reverse-proxy, and the exec / attach / portforward subresources are blocked. The pod/service/node proxy subresource is always blocked in read-only — a non-removable floor, not part of the admin-editable list.

Configurable database & Kubernetes policies

Section titled “Configurable database & Kubernetes policies”
Availability Ultimate · Enterprise.

On Ultimate the database and Kubernetes policies are editable just like the SSH list:

  • Kubernetes — a list of blocked subresources you can extend, and blocked resources you can deny reads of (for example add secrets to stop reading them in read-only sessions). exec, attach, portforward and the proxy subresource are an always-on floor — they’re blocked regardless of this list (the list shows them as defaults but can only add more, never re-enable those). Mutating verbs themselves aren’t part of the list; they’re always blocked.
  • Database — the write/DDL keyword blocklist. Remove a keyword to allow that statement, or add one (for example SET) to block it.

Read-only is enforced live — downgrading a session applies the filter immediately.