# Changelog All notable changes to the bastion. The version in `deploy/VERSION` on `main` is the next release, not necessarily the one machines run. A merged change waits there until it is published. The published version is whatever names at the moment, and it is the only version a bastion will update to. Between a merge and a publish, `main` is ahead of the manifest. Two things can keep a version listed here from reaching most machines: - **Superseded before publishing.** When a second release is merged before the first is published, only the later one is published, and it carries the changes of both. - **Held.** Automatic updates move our own machines to a published version first, and everyone else only after it has run on them for a day without a machine going silent. A version that fails that check is held: automatic updates move no other machine to it, and it is never released again, only superseded by a newer version. From 0.39.1 on, a heading marks a version *not published* or *held* once that is settled. The newest entries may not be published yet; the manifest says which version is. Older entries are not marked. ## 0.54.0 - **Every audit row also goes to the system log.** The rows written to `mgmt_audit.jsonl`, `pairing_audit.jsonl` and each pairing's `audit.jsonl` (terminals, root commands, store ops, op runs) are also written to the machine's system log, so a log collector can keep them where no host can write: syslog facility `auth`, tag `privatae-bastion-audit` (the journal on systemd) on Linux and macOS, and the Application event log, source `PrivataeBastionAudit`, on Windows, registered when the system service is installed and removed with it. Each line is the file's row with `audit` (the file) and `pairing` added, and a root command's text left out (its digest and length stay). The lines go through a bounded queue, so a slow or stuck system log never holds up the audited op; a full queue drops lines and the next line carries `system_log_dropped`. The file stays the record. - **A server is networked and anchored by default.** `install.sh` run as root now installs the networked bastion (systemd on Linux, launchd on macOS); `--local` keeps a root install local, a reinstall over a machine already paired in local mode keeps it local unless `--networked` is passed, and run as your own user it is local as before. On a networked machine with no anchor, the first `pair` run with machine authority anchors it to that host. Before, such a machine refused every op until `claim-anchor` was run by hand; now it is anchored at once: every terminal and every write waits for the person's approval, and any later host for the anchor's. `pair` then says to enrol the second factor. `claim-anchor` with no token on a machine already anchored to that host now succeeds and changes nothing (an installer that anchors after pairing finds the work done); naming another host is still refused without `--rotate`. The Windows installer is unchanged: `-Networked` as before. ## 0.53.2 - **No change to what the bastion does.** Two code comments named source files of the Host program; they now describe them instead, so the release's source can be published to the member repository as built. CI checks for such names before a change merges. ## 0.53.1 - **`switch` turns a capability family off for a host from a terminal.** `sudo -H privatae-bastion switch on|off` writes the same local deny list the console page's switch does, so it can only remove reach: off makes this machine refuse that family for that host and tell it why, and on clears the refusal without granting anything past the host's signed grant. Both directions need machine authority. `switch --list` prints every paired host's families, on or off, one per line, for a person or a setup script on a machine with no browser; beside a system service on macOS or Windows it is refused to an ordinary account, whose own state is not the machine's. The deny list is now replaced whole (temp file and rename) under a lock both writers take, so the gate never reads it half written and a switch from the page and one from a terminal cannot drop each other. - **`release-sign` signs C-Series Cloud manifests.** It accepts the kind `cseries-cloud` beside `bastion` and `bastion-agent`, with the same statement and keys, for the manifest a C-Series Cloud machine's updater checks before it moves to new images or a new overlay. - **`release-sign` signs the C-Series App Store catalog.** It accepts the kind `cseries-store`, with the same statement and keys, for the catalog a C-Series Host checks before it lists or installs an app from the public store. A signature for one kind does not verify as another. - **Fixed: `stepup-enroll --console-only` refused root on Linux.** The startup check recorded whether the process administers the machine only when a macOS or Windows system service was installed, and on Linux the service is the systemd unit, so that check never ran there and root was told it does not administer the machine. Root on Linux enrols into its own state, which is the state the unit reads. An ordinary account on Linux is still refused, and so is every account on macOS and Windows until the system service is installed. `approvals-owner` shares the rule and now takes root on Linux the same way. - **`pair` takes the token on standard input.** `pair -` reads it as the first line of standard input, so the token is not in the process list while `pair` runs or in the shell's history after. Every form of `pair` takes `-` in place of ``, and the token as an argument works as before. `pair` with no token is still the usage error, whatever standard input is: some terminals do not register as one, and reading there would wait with nothing said. At a terminal, `pair -` asks for the token on standard error. A first line that is empty, more than one word or longer than 4096 bytes is refused, and no refusal repeats what was read. ## 0.53.0 - **A Host with no platform in a VM on this machine can pair over TCP.** `pair --redeem-url https://127.0.0.1:8810/v1/local/redeem --host-ca ` redeems the token on the Host's enroll port (`POST /v1/local/redeem`, the platform redeem's body) over TLS that trusts that CA and nothing else, no system roots, and checks the name `host.cseries.internal` on the Host's certificate. The URL must be `https://[:]/`, with no user info, query or fragment. `--host-ca` is required with it and refused with a unix or platform pairing, and must hold CA certificates only; all of this is checked before the token is read, so a refusal dials nothing. The redeem follows no redirect: an enroll port that answers with one is refused as it answered. The Host's answer must say transport `tcp`, carry the same CA as `--host-ca` and an operator certificate for this bastion's key that the CA signed for client use, and name a lane on the host the redeem URL names. The pairing records transport `tcp`, the lane address and the CA, which is pinned as for a unix pairing: a `pair --replace` under another CA is refused before the redeem dials, and a pairing does not change transport on a `--replace`. - **Its lanes go to the Host's lane port**, the address its answer names, with the same mutual TLS as the unix lane socket: this bastion's operator certificate, which the Host knows it by, the pinned CA as the only root, and the name `host.cseries.internal`. Then the same bearer and protocol: egress, management, the two parked harness workers, sessions, git, the heartbeat and what its reply directs. `status` and `pair` show the lane address. - **What a TCP pairing does not have yet.** No WireGuard forwards: no ollama, LLM or PII forward, no service slots, no endpoint rotation, as for a unix pairing. No coder runs, on any machine, Linux included: a run's control plane is a tunnel port or a socket in a unix pairing's runs directory, and a TCP pairing has neither, so run-up for a TCP-paired Host is refused before any container starts, and so is every run-up while this machine's pairings cannot be read. Off Linux, coder runs stay refused for every pairing, as before; running them inside the Host's VM is not built. - `unix://` redeem URLs work as before. - **Beside a Windows system service, the signed-in person approves in their own tray.** The service serves a named pipe, `\\.\pipe\privatae-bastion-approvals`. Its ACL admits SYSTEM, Administrators and interactive users. Interactive users get read and write and no right to create an instance of the pipe. Remote clients are refused. The service identifies each client from the pipe (`GetNamedPipeClientProcessId` and that process's token) and serves only a process that runs as the person signed in to its own session, in an active session, and of those only the account that owns this machine's approvals (below); every other account is refused for list, deny and approve alike. Through it a client can list the pending step-up requests, in the words `approvals` prints. It can approve one request id with a code from the authenticator enrolled here, checked by the existing step-up check and bound to the sha256 of the purpose the client was shown. It can deny one, which needs no code. - **`privatae-bastion approvals-owner --sid [--replace]`**, with machine authority, records that account in the service's state (`approvals_owner.json`); an installer's elevated step passes the SID of the account that ran it. The same SID again changes nothing. A different SID is refused and the recorded owner stays, unless `--replace` is given, which is taken only on a console (an Administrator terminal), never with output captured as an installer runs it. A record that cannot be read is treated the same way. With no readable owner, the pipe serves nobody. - **The pipe's name is never free while the service serves it.** A replacement instance is created before a failed connect's instance is closed, as it already was before a connected one is handed over. If another process holds the name when the service starts, the first create fails (it insists on a new name), and the service tries again, waiting 1 s and doubling to 60 s, logging once per series of failed attempts and once when serving again. It never serves under a name it did not create. - **What never crosses the pipe.** The TOTP secret, `ui.secret` and approval ids. Device approvals are not listed, because they need no code. - **Wrong codes on the pipe are limited** (NIST SP 800-63B 5.2.2). Five are free. After that the pipe takes one attempt per 30 seconds. After 100 in a row it takes none. A code accepted on any other lane resets the count. - **`privatae-bastion tray`**: the person's icon and approvals window, run as them and drawn from the pipe. `service install` starts it at every sign-in through the HKLM Run value `PrivataeBastionTray`, and `service uninstall` removes that value. One tray runs per session. The tray checks that the pipe's server is the `PrivataeBastion` service process before it sends anything. The daemon's own tray stays as it was for a bastion that runs as the person; the service still shows none. - **`stepup-enroll --console-only`**, for an installer's elevated step. The secret is shown only when standard input and output are a console. It is saved only in the system service's state: on Windows and macOS when the service is installed and the process has machine authority, on Linux when the process is root. Both checks run before a secret exists. - **`release-anchor --host (--host-ca | --force)`**, with machine authority, for an anchor whose Host is gone, e.g. a Host on this machine deleted with its data and set up again. `--host-ca` is the new Host's CA. It is refused when that is the CA this box pinned for the host, since the same CA means the same Host, alive, and when this box pinned no CA to compare. `--force` skips that check and is taken only on a console; an installer never passes it. All of this is checked before anything changes. It drops the anchor if it is that host and refuses any other. It unpairs that host. It renames that host's DCN state (key pin, factor, grants) to `dcn/.released-