# Safety model

What stops a plan doing damage: preview before change, approval on protected environments, roles, tenant isolation, secret references, and an append-only record.

Source: https://opafra.com/docs/security/safety-model

---

Opafra runs commands on production servers. This page is the answer to "what stops that
going wrong", written for someone evaluating whether to allow it.

Six controls, each independent, each covered in depth elsewhere.

## 1. Nothing is installed

Opafra connects over ordinary SSH as a user you nominate, using a credential you install.
There is no agent, no daemon, and no package on the host.

The security consequence is the one worth stating: **adopting Opafra grants it exactly
the access of one SSH user, and removing it is deleting one line from
`authorized_keys`**. There is no residue, no privileged process, and no inbound path from
your hosts to Opafra. Every connection is outbound from Opafra to the host, when a run
needs it.

See [what agentless means](/docs/inventory/agentless-ssh).

## 2. Preview before change

A [dry run](/docs/running/dry-run) executes a plan with every write suppressed. It
connects to each host, resolves every variable, and reports what would happen there.

Fifteen tools support a real read-only check and report `would create`, `would change` or
`no change` per host. Everything else, including raw commands, is shown as the exact
resolved text that would run rather than guessed at.

The honest limit is stated on that page: a preview is not a guarantee, and a raw command
cannot be predicted. What it reliably does is surface the difference between hosts before
the change rather than during it.

## 3. Approval on protected environments

An environment can require approval, and then every run whose targets land in it pauses
before its first step and waits for a person.

Two properties make this stronger than a per-plan gate:

- **It belongs to the environment.** A plan written later by someone who never read the
  policy is gated anyway.
- **Environments are derived from resolved targets.** Naming a production host directly
  is exactly as gated as targeting the environment, so there is no phrasing of a plan
  that opts out.

The gate opens once, before anything runs, rather than on every step. A gate on every
step trains approvers to click through, which produces an audit trail and no scrutiny.

Approval records who decided, when, and **which plan revision** they approved compared to
the revision at decision time, so an edit landing between request and approval is visible
rather than silently honoured.

See [approval gates](/docs/running/approval-gates).

## 4. Roles and tenant isolation

Four hierarchical roles. The meaningful boundary is Operator to Admin: an Operator changes
production, an Admin changes who may change production. Approving a gated run requires
Operator or above, so a decision is made by someone who could have made the change
themselves.

Tenancy is enforced at the query rather than the screen. Every read is scoped to the
caller's tenant, every change by id checks ownership, and defaults fail closed.

**An object you may not see reports as missing, not forbidden.** "Forbidden" confirms an
object exists, which turns an id into something worth guessing at.

See [organisations and roles](/docs/access/organisations-and-roles).

## 5. Secrets are references

A plan carries `secret://provider/namespace/key`, never a value. The reference resolves at
the moment of use and the credential goes to the host without ever entering the plan, the
run log, or a preview.

This is what lets a plan be reviewed by people who should not see the credential, kept in
git, and shown to an approver in full. Redaction over-matches on purpose, covering
references, arguments whose names look like credentials, and rendered config contents in a
diff.

A generated SSH private key is shown once, at creation, and no endpoint returns it again.

See [secret references](/docs/access/secret-references).

## 6. An append-only record

Runs, approvals with their revisions, inventory and credential changes, secret reads,
grants, membership changes and web SSH sessions are all recorded. Nothing updates or
deletes an entry.

Actor emails are snapshotted at the time of the action, so deleting a user does not blank
their history.

See [audit log](/docs/governance/audit-log).

## What Opafra does not claim

Worth being direct, because a security review is where overstatement gets found:

- **It cannot predict a raw command.** `ssh.cmd` runs arbitrary text with the connecting
  user's privilege. Preview shows the text; judgement is yours.
- **It does not stop a run's author approving it.** If your policy needs four eyes, that
  is policy you enforce, not a rule the product applies.
- **A revision change is reported, not blocked.** The approver is told the plan moved
  under them and decides.
- **The audit write is fire-and-forget.** A failed log write does not fail the operation,
  because refusing to run over a bookkeeping error would turn it into an outage.
- **It cannot act on an unreachable host.** There is nothing on the host to queue work,
  which is the trade-off agentless makes.

## How the controls combine

Each control is independent, and none of them is the whole answer. A dry run does not stop
a determined mistake; an approval gate does not know what a command does; the audit log
prevents nothing at all and answers everything afterwards.

Together they mean a change to production is previewed, authorised by a named person
against a known plan revision, executed with a scoped credential that never appears in
the plan, and recorded permanently.

## Next steps

- [Audit log](/docs/governance/audit-log) for exactly what is recorded
- [Dry runs](/docs/running/dry-run) for what a preview proves
