# Security model

> The trust boundaries, what each one enforces, and where the boundary is not.

Source: https://refpoint.dev/docs/guides/security-model

refpoint reads a target and never changes it. It connects with the credentials
you give it, runs read-only operations, and reports what came back. Two inputs
are untrusted throughout: the profile that says what to run, and the target
that answers. Each boundary below has a default, and most have an opt-out that
must be written down.

| Boundary | Default | Opt-out |
|---|---|---|
| Commands on a target | Refused unless both the operator and the profile allow the executable | `--allowed-commands '*'` |
| HTTP to private addresses | Refused after the name is resolved | `--allow-private-http` plus `allow_private_addresses` on the check |
| SSH host key | Verified against `known_hosts`; a changed key is refused | `--ssh-known-hosts-file=insecure` |
| WinRM over plain HTTP | Refused unless the peer negotiates message sealing | `--winrm-allow-unencrypted` |
| WinRM over TLS | Certificate verified | `--winrm-insecure` |
| Reads from a target | 64 MiB per file, 16 MiB per stream; over the cap is an error | None |
| Config file | `./config.toml`, then `~/.refpoint/config.toml`; the path loaded is printed first | None |

The three transport opt-outs warn on the run they loosen. None is a default.

## A profile is code

A profile runs commands with your credentials on your fleet. Review one you did
not write as you would a script you are about to run as root. A `depends_on`
entry extends that review to whatever it resolves to: dependency paths are
unconfined by design, so profiles can compose.

Everything below bounds what a mistake can do. None of it contains a hostile
profile author.

## What a profile can make refpoint do

### Run commands

Two lists gate the `command` resource, and both must permit every executable in
the line.

| Gate | Set by | Empty means |
|---|---|---|
| Operator ceiling | `--allowed-commands` | Nothing is allowed |
| Profile declaration | `allowed_commands` in the `profile` block | The ceiling governs alone |

The profile can narrow the ceiling, never widen it. `*` is an explicit entry
someone has to write. A refusal names the gate that closed, so the message
reaches whoever can change it. Here the operator allowed only `true`:

```
  !  [mix-02]  Fails  impact: 1.0
    !  always-fail[command: false]
          error: command "false" blocked: executable "false" is not in allowed_commands (true)
```

A blocked command is an errored control, so the run exits 2. The
[command allowlist](/docs/guides/command-allowlist) guide covers the parser and
its limits.

### Make HTTP requests

`http` checks are made from the host running refpoint, not from the target.
Without a gate, a profile could use the runner to probe the runner's own
network. Two gates, both required:

- **Operator ceiling** `--allow-private-http`.
- **Check opt-in** `allow_private_addresses` on the check.

Without both, the hostname is resolved before anything connects and refused if
any address is loopback, private, link-local, unspecified or interface-local
multicast. Resolving first catches a public name pointing at `127.0.0.1`.
Every redirect hop is checked the same way, the chain stops after 3 hops, and a
response body is read to 1 MiB.

### Supply profile files

| Input | Limit |
|---|---|
| Profile file | 4 MiB |
| Bracket nesting | 200 levels |
| Evaluated string | 256 KiB |

The nesting cap prevents a stack overflow, which is fatal and which no recover
catches.

- **Expression functions are pure.** No filesystem, no environment, no clock.
  A profile evaluates to the same thing on a laptop and in a pipeline.
- **Control characters are refused** in resource addresses and block labels,
  since both appear verbatim in logs and reports.
- **Dependency cycles are refused.**
- **The parsers are fuzzed.** The variable, waiver and inventory file parsers
  have fuzz targets that run in CI.

### Change the configuration

Configuration is read from `./config.toml` first, then
`~/.refpoint/config.toml`. A config file inside a repository you cloned
governs a run started from that directory. It can change targets, disable host
key verification or widen the allowed commands.

Every run prints the absolute path it loaded to stderr before anything else:

```
config loaded: /home/you/work/baseline/config.toml
```

An unexpected path on that line is a finding, not a detail.

## What a target can make refpoint do

| Input | Protection |
|---|---|
| Text | Sanitised where it is rendered. Only sanitised text reaches the terminal renderer; the type that renderer accepts cannot be constructed any other way. HTML output is escaped by the template engine. |
| Volume | 64 MiB per file read, 16 MiB per output stream. Exceeding a cap is an error, never a truncation. |
| A panic | Contained. A panic in a resource provider becomes that control's error; a panic during a target's scan becomes that target's error. |

Output from a target can carry terminal control sequences, and anything that
can repaint a screen can hide a failure. A quietly shortened stream could turn a
failure into a pass. One bad answer does not end a fleet-wide run.

## Transport

- **SSH host keys** are verified against `known_hosts` by default. The key
  algorithms offered are limited to those recorded for that host, so a server
  cannot steer the handshake to an algorithm whose key you never pinned.
  `--ssh-known-hosts-file=accept-new` records an unknown host on first contact
  and still refuses a changed key.
- **WinRM over plain HTTP** is pre-flighted. A peer that cannot negotiate
  message sealing is refused, because the password would otherwise cross the
  network in cleartext. `--winrm-https` uses port 5986.

## Credentials

| Secret | Handling |
|---|---|
| SSH key file | Bytes zeroed once parsed |
| SSH, WinRM and sudo passwords | Cleared from the in-memory configuration once the transports are open |
| `--sudo` without a password | Runs `sudo -n`. A command that needs a password fails at once instead of hanging on a prompt |
| `--sudo` with a password | Runs `sudo -S` with the prompt suppressed. The password is read from stdin and stays off the command line |
| `~/.refpoint` | Created 0700 |
| Config files, reports, audit logs, generated keys | Created 0600 and re-set when opened, because an existing file keeps its mode. `refpoint keygen` refuses to overwrite a key |

## Where the boundary is not

Three limits, each a property of the design rather than a gap awaiting a fix.

- **The allowlist works on executable names.** It cannot see inside an
  interpreter. Allow `awk`, `find`, `perl` or `sh`, and anything is allowed.
- **The opt-outs are real.** Each entry in the opt-out column of the first table
  switches a protection off. They exist for the cases that need them.
- **A profile you have not read is code you have not read.** The bounds above
  limit accidents, not intent.

## Before production

1. Read the profile and everything its `depends_on` entries resolve to.
2. Set `--allowed-commands` to the executables the profile needs.
   `refpoint check` lists them.
3. Record host keys in `known_hosts` before the first run.
4. Check the `config loaded:` line on the first run.

## Reporting a vulnerability

Report a suspected vulnerability privately to security@refpoint.dev rather than
in a public issue. The repository's security policy says what to include.

Related: [the command allowlist](/docs/guides/command-allowlist),
[evidence and attestation](/docs/guides/evidence-and-attestation),
[release integrity](/docs/guides/release-integrity).