bastion 0.60.0
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 3m36s
ci / fuzz (push) Successful in 3m35s

This commit is contained in:
Privatae
2026-11-02 00:10:58 +00:00
parent 4a5c310482
commit 4f8285ef18
100 changed files with 11537 additions and 840 deletions
+148 -147
View File
@@ -5,59 +5,25 @@
[![Platforms](https://img.shields.io/badge/platforms-linux%20%7C%20macOS%20%7C%20windows-lightgrey.svg)](#supported-platforms)
[![ci](https://git.privatae.ai/privatae/privatae-bastion/actions/workflows/ci.yml/badge.svg?branch=main)](https://git.privatae.ai/privatae/privatae-bastion/actions)
**One static Go binary that gives your AI Host access to a machine without SSH keys: it dials out, checks every signed grant on the machine itself, and keeps its own audit log.**
The bastion itself needs no Docker, no Python, no database and no separate
WireGuard install. A few optional features use software the machine must
already have: coder runs, the service slot and the memory mirror need a
container runtime (Docker, Podman or nerdctl); folder search runs the bastion
agent, which needs Python 3.8 or later; host-written ops in Python need
`/usr/bin/python3`; and reading an app's accessibility tree on Linux needs
python3 with its AT-SPI bindings. Without them, those features say what is
missing and the rest works.
You pair the binary with a short token from your host, and from then on your host can reach
this machine only through the tunnel this bastion dials out and the lanes it
gates. Your files stay where they are. Nothing on this machine drives the host:
there is no MCP endpoint here.
The published release is whatever `privatae.ai/bastion/version.json` names.
This repository receives each release after it is published, so
`deploy/VERSION` here can trail it. From 0.42.0 on, every release manifest is
signed and every published binary can be rebuilt from this source and compared
([Verify a release](#verify-a-release)). See [CHANGELOG.md](CHANGELOG.md).
**See every AI agent running on your computer, what each one is doing and what it can reach, and stop any of them. One program, no account, no AI of its own.**
```sh
# A server (macOS / Linux, as root): networked, anchored to the first host you pair
curl -fsSL https://privatae.ai/bastion/install.sh | sudo sh
sudo -H privatae-bastion pair <token> # token from your host's "Set up a bastion"
sudo -H privatae-bastion stepup-enroll # your second factor, so you can approve here
# Your own computer (macOS / Linux, as you)
curl -fsSL https://privatae.ai/bastion/install.sh | sh
privatae-bastion pair <token>
privatae-bastion run # tunnel + lanes up
curl -fsSL https://privatae.ai/bastion/install.sh | sh # macOS, Linux: sudo asks for your password once
privatae-bastion ui # the console, in your browser
```
```powershell
# Windows, your own computer
irm https://privatae.ai/bastion/install.ps1 | iex
privatae-bastion pair <token>
privatae-bastion run
irm https://privatae.ai/bastion/install.ps1 | iex # Windows: Windows asks for an administrator's yes once
```
A Windows server installs from an Administrator PowerShell with `-Networked`.
See [Install](#install).
It starts watching at once. No Host, no sign-up and no network is needed; a
Host is something you can connect later ([Works with a Host](#works-with-a-host)).
A Cloud Host's **Set up a bastion** card gives one line that installs the
bastion, pairs it and starts it, so it runs now and after a restart:
`curl -fsSL https://privatae.ai/bastion/install.sh | PRIVATAE_PAIR_CODE='skp_…' sh`, or
on Windows `& ([scriptblock]::Create((irm https://privatae.ai/bastion/install.ps1))) -Pair 'skp_…'`.
**AI readers and reviewers:** [docs/README.md](docs/README.md) lists every reference page, one line each, and [docs/claims.md](docs/claims.md) is the full table of security claims and the test behind each. [docs/STATUS.md](docs/STATUS.md) says what works today and what does not.
**AI readers and reviewers:** [docs/README.md](docs/README.md) lists every reference page, one line each, and [docs/claims.md](docs/claims.md) is the full table of security claims and the test behind each.
**Contents:** [Why a bastion](#why-a-bastion) · [Install](#install) ·
[Quick start](#quick-start) · [Local and networked modes](#local-and-networked-modes) ·
**Contents:** [What it sees](#what-it-sees) · [What you can do](#what-you-can-do) ·
[What it refuses to do](#what-it-refuses-to-do) · [Works with a Host](#works-with-a-host) ·
[Install](#install) · [Your own computer or a shared machine](#your-own-computer-or-a-shared-machine) ·
[Replacing an agent's SSH keys](#replacing-an-agents-ssh-keys) ·
[What the bastion does](#what-the-bastion-does) · [Security model](#security-model) ·
[How each claim is tested](#how-each-claim-is-tested) ·
@@ -65,138 +31,173 @@ on Windows `& ([scriptblock]::Create((irm https://privatae.ai/bastion/install.ps
[Supported platforms](#supported-platforms) · [Build from source](#build-from-source) ·
[Verify a release](#verify-a-release) · [Contributing](#contributing) · [License](#license)
## Why a bastion
## What it sees
A Privatae host is an AI that works for you. To be useful it needs to see your
files, use your machine's models, and sometimes act on the machine. The bastion
is the piece that makes that possible **without handing the host your machine**.
- **Every AI agent on the machine**: Claude Code, Codex, Gemini CLI, aider,
goose, Cline, opencode, Cursor's agent, Open Interpreter, Continue, crush
and Antigravity by their programs, and any other program holding a
connection to a model's API ([`agents.go`](client-go/agents.go)).
- **What each one is doing.** For Claude Code: the session's title, its
folder and git branch, whether permission prompts are off, and the command
it is running now ([`agentsession.go`](client-go/agentsession.go)). For the
others: the folder and the tool servers it runs.
- **Who started it**: you at a terminal here, a remote login, a scheduled job,
a service, or another agent.
- **The scheduled jobs that start agents** (cron and systemd timers, launchd,
Task Scheduler).
- **The problems in their tool-server (MCP) configs**: a key in plain text,
a package that is not pinned to a version, a remote server with no
credential, a server running that no config declares
([`mcpconfig.go`](client-go/mcpconfig.go)).
- **What changed since your baseline**: a new agent, a new schedule, a new
listening port.
The bastion dials your host; your host never dials your machine. The only
ports it holds open on an outside interface are its tunnels' WireGuard UDP
ports, one per host it reaches through a tunnel, each at a port the system
picks, and WireGuard drops every packet there that does not come from that
host's key. Every other port it opens binds loopback (the optional memory-mirror
forwarder also binds the replica container's private gateway address). The only thing your host can reach is the
inside of the tunnel this binary brought up, and what it may do there arrives
as a signed grant that this binary verifies locally. Once you enrol a second
factor on this machine, anything that would move your files, download a model,
or open a root shell waits for you to approve it **here**, with a code the host
never sees.
The same views are commands, each with `--json`: `agents`, `activity`,
`report`. A report is one Markdown file of all of it, with no prompts in it.
The result: the host gets a precise, auditable view of what you chose to share,
and you keep the keys. [SECURITY.md](SECURITY.md) says what you still have to
trust, and why.
## What you can do
- **Stop an agent**, from the console or `privatae-bastion stop <pid|session>`,
with your code: a six-digit code from an authenticator app on your phone.
- **Answer what agents ask.** An agent can ask the bastion for something (to
stop another agent, say); it waits on **Waiting for you** until you answer
with your code.
- **Mark an agent as yours**, so the ones you did not start stand out.
- **Record a baseline**, and see what is new since.
- **Write a report** of what the bastion knows about this machine.
## What it refuses to do
- **An agent cannot turn it off or widen what it allows.** Installed for the
whole machine (the default), the bastion runs as the system's service, so a
program running as you, an AI agent included, cannot stop it. Your code
lives where only the machine's administrator can read it, so an agent that
drives your browser can click a button but cannot type the code.
- **It sends nothing about this machine anywhere.** With no Host connected
its only outside request is for the list of releases
(`https://privatae.ai/bastion/version.json`, every 6 hours). Session titles
and prompts never leave the machine, even with a Host
([`agentsession.go`](client-go/agentsession.go)).
- **It runs no AI.** Nothing in it calls a model.
- **Its console is this machine's only.** It listens on `127.0.0.1`, and
opens for the machine's administrator and the account recorded as its
owner, nobody else ([console](docs/console.md)).
- **Nothing on this machine drives a Host.** There is no MCP endpoint here.
## Works with a Host
A Privatae Host is an AI that works for you. Connected to one, the bastion is
what lets it see your files, use your machine's models and act on the machine
**without handing it your machine**. The bastion dials your Host; your Host
never dials your machine. The only ports it holds open on an outside interface
are its tunnels' WireGuard UDP ports, and WireGuard drops every packet there
that does not come from that Host's key. What the Host may do arrives as a
signed grant that this binary checks here, and anything that would move your
files, download a model or open a root shell waits for your code
([Security model](#security-model), [SECURITY.md](SECURITY.md)).
```sh
sudo -H privatae-bastion pair <token> # token: your Host's Network app → Set up a bastion
```
A Cloud Host's **Set up a bastion** card gives one line that installs, pairs
and starts the bastion:
`curl -fsSL https://privatae.ai/bastion/install.sh | PRIVATAE_PAIR_CODE='skp_…' sh`, or
on Windows `& ([scriptblock]::Create((irm https://privatae.ai/bastion/install.ps1))) -Pair 'skp_…'`.
The bastion itself needs no Docker, no Python, no database and no separate
WireGuard install. A few features a Host can use need software the machine
must already have: coder runs, the service slot and the memory mirror need a
container runtime (Docker, Podman or nerdctl); folder search runs the bastion
agent, which needs Python 3.8 or later; host-written ops in Python need
`/usr/bin/python3`; and reading an app's accessibility tree on Linux needs
python3 with its AT-SPI bindings. Without them, those features say what is
missing and the rest works.
## Install
There are two ways to run the bastion, and the installer picks by who runs it.
**On a server** (run the installer as root). The bastion is installed as the
machine's system service in **networked** mode (Linux with systemd, or macOS;
on Linux without systemd a root install stays local, with no service), and
the first host you pair becomes its **anchor**. From then on, every terminal and every write on the
machine waits for your approval, and any other host that pairs waits for your
host to approve it.
```sh
curl -fsSL https://privatae.ai/bastion/install.sh | sudo sh
sudo -H privatae-bastion pair <token> # token: your host's Network app → Set up a bastion
sudo -H privatae-bastion stepup-enroll # your second factor, so you can approve here
```
On Windows, from an Administrator PowerShell:
```powershell
& ([scriptblock]::Create((irm https://privatae.ai/bastion/install.ps1))) -Networked
privatae-bastion pair <token>
privatae-bastion stepup-enroll
```
**On your own computer** (run the installer as yourself). The bastion runs
as your user in **local** mode and serves the host you pair it with, and
another only when you answer yes on this computer. The pairing's
capabilities are the authorization, and approvals work once you enrol a
second factor.
**On your own computer**, run the installer as yourself:
```sh
curl -fsSL https://privatae.ai/bastion/install.sh | sh
privatae-bastion pair <token>
privatae-bastion run
```
```powershell
irm https://privatae.ai/bastion/install.ps1 | iex
It installs the bastion **for the whole machine**: the systemd unit
`privatae-bastion` on Linux, the launchd daemon `ai.privatae.bastion` on macOS,
both running as root. sudo asks for your password once. Your account is
recorded as the machine's owner, so `privatae-bastion ui` opens the console
from it without sudo. Then set up your code, at a terminal:
```sh
sudo -H privatae-bastion stepup-enroll --machine
```
A root install that should stay local passes `--local`
(`… | sudo sh -s -- --local`). Reinstalling over a machine already paired in
local mode keeps it local unless you pass `--networked`. [Local and networked modes](#local-and-networked-modes)
sets out the difference, and [Replacing an agent's SSH keys](#replacing-an-agents-ssh-keys)
how to run a fleet of servers this way.
On Windows, `irm https://privatae.ai/bastion/install.ps1 | iex` does the same
with the Windows service `PrivataeBastion`, and Windows asks for an
administrator's yes once.
`--just-me` installs it for your account alone, with no sudo
(`curl -fsSL https://privatae.ai/bastion/install.sh | sh -s -- --just-me`;
on Windows `-JustMe`). Any program running as you can then stop it or change
what it allows, and the installer says so. Where sudo is missing or refuses,
that is what the installer falls back to, and it says that too. An install
for your account alone that is already paired stays where it is when you
reinstall.
**On a server**, run the installer as root:
```sh
curl -fsSL https://privatae.ai/bastion/install.sh | sudo sh
sudo -H privatae-bastion pair <token>
sudo -H privatae-bastion stepup-enroll
```
Run as root, it installs the bastion for a **shared machine**: the first Host
you pair is in charge, every terminal and every write on the machine waits for
your approval, and any other Host waits for the one in charge to approve it.
`--personal` or `--shared` chooses either way explicitly.
The installer downloads the binary for your OS and architecture, checks its
SHA-256 against the release manifest, and only then moves it into place; a
failed check installs nothing. That manifest comes from the same place as the
binary. To check its signature too, see [Verify a release](#verify-a-release).
How the service runs on Linux, macOS and Windows, and where the release
binaries and the signed release manifest are served from:
`privatae-bastion uninstall` removes the service and the program and keeps the
state, the audit log included; `--delete-data` deletes that too. How the
service runs on each system, and where the release files are served from:
[docs/install.md](docs/install.md).
## Quick start
## Your own computer or a shared machine
On your own computer. For a server, see [Install](#install): the installer
run as root sets it up networked, and `run` is the service's job.
```sh
privatae-bastion pair <token> # token: your host's "Set up a bastion", one use
privatae-bastion stepup-enroll # your second factor, straight away
privatae-bastion run # tunnel + lanes up; one daemon serves every host you pair
privatae-bastion status # or: privatae-bastion ui, the local console
privatae-bastion add-folder ~/projects/notes "Notes"
```
Pairing generates this machine's keys locally and pulls only its certificate
and routing from the host. Until you enrol a second factor, folder grants and
file moves are gated by the signed grant alone (the host verifies its own
factor), and model downloads and root shells are refused outright. After you
do, every one of those waits for your approval here. Each paired host gets its
own loopback control port, starting at `127.0.0.1:7878`. A shared folder's
files stay on this machine: the host keeps only an index, and you confirm the
share on the host with your code.
## Local and networked modes
| | **local** | **networked** |
| | **your own computer** (`local` in the code) | **a shared machine** (`networked`) |
|---|---|---|
| Where | your own computer | a server, or a machine several people share |
| Installed by | the installer run as yourself | the installer run as root (Windows: `-Networked`), or `sudo -H privatae-bastion set-mode networked` |
| Runs as | your user is fine | the system's service: root under systemd or launchd, LocalSystem on Windows |
| A terminal the host opens | opens on the pairing's capabilities | waits for your approval, for that one session |
| Installed by | the installer run as yourself | the installer run as root, or `sudo -H privatae-bastion set-mode networked` |
| Runs as | the system's service (or your account, with `--just-me`) | the system's service: root under systemd or launchd, LocalSystem on Windows |
| A terminal the Host opens | opens on the pairing's capabilities | waits for your approval, for that one session |
| A write (an op, a ship step) | runs on the pairing's capabilities | waits for your approval, bound to that op and its arguments |
| Second pairing | asked on this computer's terminal, added only on a yes | waits for the **anchor** host's approval |
| The anchor | none | the first host paired, or `claim-anchor` |
| A second Host | asked on this computer's terminal, added only on a yes | waits for the approval of the Host in charge |
| The Host in charge | none | the first Host paired, or `claim-anchor` |
In networked mode one host is the **anchor**: the one host that approves every
later pairing on this machine. Claiming the anchor requires root, because there
is no authority above the box to ask. On a networked machine with no anchor,
the first `pair` run as root anchors it to that host by itself. A machine
paired first and switched to networked afterwards is anchored with
`claim-anchor` and no token: it is a choice among pairings the box already
holds, and running it again for the same host changes nothing. A token is needed only to name a host this box
does not serve yet (a fresh machine, or `--rotate`). The anchor can be moved to
another host only with the current anchor's signed consent, or after 14 days of
silence from it.
On a shared machine one Host is **in charge** (the code calls it the anchor):
the one Host that approves every later pairing on this machine. Putting a Host
in charge requires root, because there is no authority above the machine to
ask. On a shared machine with none in charge, the first `pair` run as root puts
that Host in charge by itself. A machine paired first and switched to shared
afterwards gets its Host in charge with `claim-anchor` and no token: it is a
choice among pairings the machine already holds. A token is needed only to name
a Host this machine does not serve yet (a fresh machine, or `--rotate`). The
Host in charge can be changed only with its own signed consent, or after 14
days of silence from it.
```sh
sudo -H privatae-bastion claim-anchor [--owner <name>] [--host <host>] # anchor to a host already paired here
sudo -H privatae-bastion claim-anchor [--owner <name>] <token> # anchor a fresh machine to your host
sudo -H privatae-bastion claim-anchor --rotate <token> # move the anchor to a different host
sudo -H privatae-bastion claim-anchor --repin # same host, new address
sudo -H privatae-bastion claim-anchor [--owner <name>] [--host <host>] # put a Host already paired here in charge
sudo -H privatae-bastion claim-anchor [--owner <name>] <token> # a fresh machine, and your Host in charge
sudo -H privatae-bastion claim-anchor --rotate <token> # put a different Host in charge
sudo -H privatae-bastion claim-anchor --repin # same Host, new address
sudo -H privatae-bastion claim-owner <name> # state who owns the machine
```
`claim-owner` is optional. The name shows on this machine's card in every host
`claim-owner` is optional. The name shows on this machine's card in every Host
it serves, and it grants nothing anywhere.
## Replacing an agent's SSH keys
@@ -293,7 +294,7 @@ Each point in full: [docs/security-model.md](docs/security-model.md); the link a
anchor host, until the time you chose and at most 30 days; a revoke or
expiry stops a running command within 2 seconds. ([session lane](docs/lanes.md#session-lane))
- **Root only where there is no one else to ask**: claiming ownership or the
anchor. ([modes](#local-and-networked-modes))
anchor. ([modes](#your-own-computer-or-a-shared-machine))
- **On macOS and Windows, a networked bastion is the system's service**, which
a signed-in user who does not administer the machine cannot stop. ([install](docs/install.md))
- **Observation changes nothing on the machine.** Observe ops read and report;