> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.hoop.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Known Limitations

> What an ssh lane does not do, stated before you deploy it rather than after

Every limit here is a gap between what the lane promises and what it does, or a
boundary drawn on purpose. Read it alongside
[OpenSSH Differences](/docs/setup/configuration/hoop-sidecar/protocols/ssh/limitations),
which is the other half: what `sshd` does that this lane does not.

***

## The list

| Limit | Why |
| - | - |
| **A value that crosses a read boundary is not masked** | the session stream is masked one read at a time, so a value cut in half by a boundary matches in neither half. See below — downloads are not affected |
| **An audit outage does not end a session already open** | `fail_open: false` refuses a new connection, and refuses every `exec`, `env` and file operation as it is asked for. An interactive shell asks for nothing between its open and its close, so there is no point left at which to refuse it. A shell already running when the sink failed keeps running |
| **No guardrails on an interactive shell** | a keystroke stream has no statement boundary, and reconstructing one is unsound — history recall and tab completion diverge it from what runs, a rule that fires deletes what the user was editing, and no signal sees through shell expansion. A shell is admitted, masked on output, and recorded as events. Drop `shell` from `capabilities_allowed` to close the gap |
| **No session content is recorded** | not keystrokes, not output, not file bytes, and there is no setting that adds them. The trail answers who connected, what they asked for and what was refused — not what scrolled past |
| **Masking cannot change length** | an `ssh` lane rewrites a byte stream in place, so `strategy: mask` with an ASCII `mask_char` is the only strategy the lane accepts. Anything else is refused at load |
| **An upload a mask rule would touch is refused, not rewritten** | silently altering a file someone believes they uploaded is worse than refusing it |
| **rsync and masking cannot coexist** | rsync checksums at both ends and correctly discards bytes that were rewritten in between. Any transfer protocol that verifies its own content integrity does the same |
| **A forward is relayed blind** | nothing inspects those bytes, which is why `destinations_allowed` is the whole control and is default-deny |
| **Accounts cannot be checked at load** | the login name arrives in the handshake, so a host missing an account it meant to serve is discovered by the first session that asks for it |
| **Four rule types are refused** | `table`, `http_resource`, `http_status` and `grpc_status` have nothing on an SSH statement to read |

***

## Terminating bastion limits

These apply only to a listener carrying an [`ssh.relay`](/docs/setup/configuration/hoop-sidecar/protocols/ssh/bastion-termination) block. Everything in the list above still applies as well.

| Limit | Why |
| - | - |
| **No file transfer at all** | `sftp`, `scp` and `rsync` do not work against a terminated target. Against a remote `sftp-server` the subsystem is an opaque stream, and path gating and download masking would need it decoded both ways. Naming `sftp` on a target fails at load |
| **No remote forwarding (`ssh -R`)** | it exposes the user's machine to the target rather than the reverse, and needs a listener on the target plus reverse-channel plumbing that does not exist |
| **Forwarded bytes are not inspected** | an arbitrary TCP stream has no statement and no terminal output. `forwards_allowed` is the only control, which is why it is required whenever a target admits `local_forward` |
| **No per-user or per-group forwarding grants** | anyone who can reach a target gets that target's whole `forwards_allowed`. `permit-port-forwarding` is already required to jump, so it cannot distinguish who may forward from who may merely connect |
| **The bastion holds every session's plaintext** | that is the mode, not a defect. It also holds credentials that reach every target behind it, which makes it the highest-value host in the deployment |
| **`HostKeyAlias` collapses per-target host identity** | the bastion presents its own key in the target's place, so one alias covers the fleet and a user who bypasses the bastion gets a first-contact prompt rather than a mismatch warning. A host CA would fix this and does not exist yet |
| **`agent_identity` does not work for automation** | a client with no agent is refused, never downgraded. A certificate carrying `source-address` is also checked against the **bastion's** address, so certs pinned to user networks stop working through a bastion |
| **No per-target rule sets** | `guardrails`, `mask` and the analyzer apply to every target on the lane |
| **Changing `relay` needs a restart** | the whole block is listener topology. `identities` is the exception: its keys are read per session, so enrolment takes effect on the next connection |

***

## A value that crosses a read boundary

The session stream is read in 32 KB blocks, and each block is masked on its
own. A value that begins in one block and ends in the next matches in neither,
so it reaches the client in the clear — and because nothing matched, no masking
event is recorded for it either.

Two ways it happens:

* **Output longer than 32 KB.** Every boundary is a place a value can be cut.
  `cat` a large file and the odds rise with its size.
* **A program that writes a value in more than one call.** `printf "user: alice@"`
  followed by `printf "example.com"` is two writes, and can arrive as two reads.

**Downloads are not affected.** A file that a mask rule covers is read whole
before it is rewritten, so there is no boundary inside it to cut a value on.
The same is true of an upload, which is buffered before it is judged.

### What to do about it

Treat masking on terminal output as a reduction, not a guarantee. Where a value
must never be shown, **deny the path instead of rewriting it** — a denial is
exact, a rewrite is per-block:

```yaml theme={"dark"}
guardrails:
  rules:
    - name: protected-paths
      type: pattern_match
      pattern_regex: '(secrets\.env|\.aws/credentials)'
      operations: [exec_line, sftp_read]
      message: this path is not readable through hoop
```

A fix has to hold bytes back until the next read confirms them, and the same
length rule that makes `strategy: mask` the only strategy here forbids
returning fewer bytes than were given. It is open work, and it needs a decision
about what a user sees while bytes are held: a shell prompt has no trailing
newline, so it would sit in the held tail and never be displayed.

***

## Next

<CardGroup cols={2}>
  <Card title="OpenSSH Differences" icon="git-compare" href="/docs/setup/configuration/hoop-sidecar/protocols/ssh/limitations">
    The assumptions carried over from `sshd` that do not hold here.
  </Card>

  <Card title="Configuration" icon="file-code" href="/docs/setup/configuration/hoop-sidecar/protocols/ssh/configuration">
    Every attribute, the full capability surface, and what each one can carry.
  </Card>

  <Card title="Bastion SSH Termination" icon="arrows-split-up-and-left" href="/docs/setup/configuration/hoop-sidecar/protocols/ssh/bastion-termination">
    The mode these extra limits belong to, and the trade it makes.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.