Read-only policies
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);
scpruns as anexeccommand 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
execrequests (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).
Hard read-only on SSH
Section titled “Hard read-only on SSH”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:
# 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/.sshsudo tee /home/tessera-ro/.ssh/authorized_keys < tessera-ro.pub >/dev/nullsudo chown tessera-ro:tessera-ro /home/tessera-ro/.ssh/authorized_keyssudo chmod 600 /home/tessera-ro/.ssh/authorized_keysVerify 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 Tessera | Constrained by the OS | Use it for | |
|---|---|---|---|
| RO grant on the privileged target | yes | no | preventing accidents; a fast, no-setup guardrail |
| RW grant on the read-only target | no | yes | an actual read-only guarantee |
| RW grant on the privileged target | no | no | making 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.
Databases
Section titled “Databases”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.
Kubernetes
Section titled “Kubernetes”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”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
secretsto stop reading them in read-only sessions).exec,attach,portforwardand theproxysubresource 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.
We would like to count visits with Google Analytics, which needs a cookie. Nothing is loaded and nothing is stored unless you accept. What this sets.