bastion 0.60.0
This commit is contained in:
@@ -5,59 +5,25 @@
|
||||
[](#supported-platforms)
|
||||
[](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;
|
||||
|
||||
Reference in New Issue
Block a user