A plain SSH bastion host records who connected, not what they ran once they were in. SSH session recording with Formal starts in the SSH path: the Formal Connector, a protocol-aware proxy, ties every session to a person, checks it against policy, and records it.
What does SSH session recording capture in Formal?
The Connector terminates the user’s SSH connection and opens its own connection to the host, over SSH or AWS SSM. Because the terminal stream passes through it, the Connector records every session and makes it available in Sessions. That is the base of SSH access management with Formal: one identity per person, one policy layer, one record.
For each SSH session, you get:
- A full terminal recording you can pause, rewind and fast-forward.
- The extracted commands, so you can scan a session without replaying it.
- A risk score from 1 to 5 and a natural-language summary, based on commands, file access, privilege escalation and destructive actions.
One limit: the Sessions list doesn’t filter by risk level. Query Logs instead, for example resource.technology:ssh AND event_type:session-analysis AND session.analysis.data.riskscore:>=4.
How do you stop a risky SSH session while it runs?
Session Monitors act during the session. You describe a threat in plain English, the AI analyzes SSH commands as they run, and a match lands in input.triggered_monitors, which re-evaluates the session policy. Monitors need a Connector with an AI provider configured. This policy is from the enforcement docs:
package formal.v2
import future.keywords.if
import future.keywords.in
session := {
"action": "quarantine",
"message": "Your access is paused. Contact the security team.",
"reason": "Curl piped to bash in an SSH session"
} if {
some monitor in input.triggered_monitors
monitor.name == "ssh_curl_to_bash"
}
blockandsuspendend the session; the user can reconnect.quarantineends all of the user’s sessions and blocks them from SSH, Kubernetes and RDP until an admin clicks Unblock User.mfaasks for device-owner verification through the Formal Endpoint.
Start a new monitor with allow and a reason to see what it catches before you enforce.
Can Formal replace your SSH bastion host?
The Connector runs in your VPC and does the bastion’s job: one entry point to your hosts, plus identity, policy and recording. At Cursor, Formal secures SSH alongside PostgreSQL, Redis, ClickHouse and Kubernetes, and the policies and audit logs for all of them live in one place.
Policies target hosts precisely: EC2 by instance ARN and tags, ECS by cluster ARN, service ARN, task ID, task tags and container name. Session rules also read the command the client asked to run, which is how a policy tells a git push (git-receive-pack) from a clone.
On AWS, the Connector reaches EC2 and ECS Fargate through SSM, using an ambient IAM role or an IAM Role Native User. AWS documents that Session Manager works without open inbound ports, bastion hosts or SSH keys; Formal adds identity, policy and recording on top. For the engineering details, read how we built SSH port forwarding over SSM.
Do engineers still need SSH keys or certificates?
Not for the hosts. A Native User on the Connector holds the host credential: a password, or an SSH key with an optional CA-signed SSH certificate, stored in Formal or read from Connector environment variables. Engineers never receive it. They sign in to Formal through your SSO provider and authenticate to the Connector as idp:formal:human:<email>.
For SSH key management, this shrinks the problem NIST IR 7966 describes, user keys spread across hosts, to the Native User credentials you configure.
Which SSH tools work through Formal?
Engineers keep their clients, with the settings in the SSH guide. On macOS and Linux laptops, the Endpoint’s Transparent Mode intercepts SSH to the real host with the user’s own keys and applies the same session policies. On macOS, it can also block coding agents from pushing to sensitive repositories, a useful control next to command allowlisting in Claude Code.
How does just-in-time SSH access work?
Block production SSH with a default session policy, then grant time-bound exceptions as policy suspensions, approved through Access Requests or a Slack approval workflow. See Workflows and what just-in-time access is.
