#!/bin/sh # privatae-bastion installer (macOS / Linux). # # On your own computer: curl -fsSL https://privatae.ai/bastion/install.sh | sh # It installs the bastion for the WHOLE MACHINE, as a system service (systemd on # Linux, launchd on macOS), so that no program running as you, an AI agent # included, can stop it or change what it allows. sudo asks for your password # once. Your own computer is the default ("personal": one Host at most, chosen at # the machine), and your account opens the console without sudo: # privatae-bastion ui. It watches the AI agents on the machine at once; no Host is # needed. # # On a server: curl -fsSL https://privatae.ai/bastion/install.sh | sudo sh # Run as root, it installs the SHARED bastion (several Hosts, the first one paired # is in charge), as before: sudo -H privatae-bastion pair # # --just-me installs it for your own account only, with no sudo. Any program # running as you can then stop it; the installer says so. Without sudo, or when # sudo refuses, that is what it falls back to, and it says that too. # --personal / --shared choose the kind of machine (also spelled --local / # --networked, their earlier names). # # PRIVATAE_PAIR_CODE='' pairs as well, with the code from your Host's "Set # up a bastion" card, and starts the bastion so it serves now and after a # restart. The one line the card gives is # curl -fsSL https://privatae.ai/bastion/install.sh | PRIVATAE_PAIR_CODE='skp_…' sh # set -e # The pairing code comes from the environment, never from an argument: an # argument sits in this shell's command line for the whole install, where any # user of this machine can read it with ps, and one at the same address could # redeem it first. A process's environment is readable only by its owner and # root. It is read here, before this script starts any program, and unset at # once, so no program it starts inherits it. Never echoed: it holds the token. pair_code=${PRIVATAE_PAIR_CODE-} pair_given=${PRIVATAE_PAIR_CODE+1} unset PRIVATAE_PAIR_CODE if [ -n "$pair_given" ]; then # skp_ and base64url: anything else is a line that lost part of the code. case "$pair_code" in skp_*[!A-Za-z0-9_-]* | skp_) pair_code="" ;; skp_?*) ;; *) pair_code="" ;; esac if [ -z "$pair_code" ]; then echo "ERROR: PRIVATAE_PAIR_CODE is not a pairing code. Copy the whole line from your Host's card again (the code starts skp_). Nothing was installed." >&2 exit 2 fi fi # dl.privatae.ai, not privatae.ai/bastion: the download host is served from each # edge's OWN local disk, so installs survive losing the platform datacentre, and # DNS points it at the edge with the bandwidth for an install wave (~15MB a # binary, and the primary sustains roughly 25x what the secondary does). # Deliberately NOT env-overridable: the digest is checked against the manifest # from this same base, so anything that can redirect the base can redirect both # and the verification buys nothing. Hardcoded is the point. base="https://dl.privatae.ai" os=$(uname -s | tr '[:upper:]' '[:lower:]') arch=$(uname -m); case "$arch" in x86_64|amd64) arch=amd64;; aarch64|arm64) arch=arm64;; esac if [ "$(id -u)" = 0 ]; then bindir=/usr/local/bin; root=1; else bindir="${HOME}/.local/bin"; root=0; mkdir -p "$bindir"; fi bin="$bindir/privatae-bastion" # As root the default is the NETWORKED bastion: a root-owned service, anchored to # the host it is first paired with, so every terminal and every write waits for the # person's approval and every later host for the anchor's. A server left in local # mode would serve its host with no approval at all. --local keeps a root install # local (one host, the pairing's caps are the authorization); run as your own user # it is always local. --networked is accepted and is the root default. networked=0 local=0 just_me=0 code_on_stdin=0 for a in "$@"; do case "$a" in --networked|--shared) networked=1 ;; --local|--personal) local=1 ;; --just-me) just_me=1 ;; # Internal: the whole-machine step this script runs through sudo # (elevate_whole_machine) hands the pairing code on standard input, never # in its arguments. --pair-code-on-stdin) code_on_stdin=1 ;; # An unknown argument is never echoed: it may be a pairing code given in # the wrong place, and a log of the failed install must not hold it. *) echo "ERROR: unknown option. This installer takes --personal, --shared or --just-me; a pairing code goes in PRIVATAE_PAIR_CODE, as your Host's card shows. Nothing was installed." >&2; exit 2 ;; esac done if [ "$networked" = 1 ] && [ "$local" = 1 ]; then echo "ERROR: --shared and --personal contradict each other; pass one" >&2; exit 2 fi if [ "$just_me" = 1 ] && [ "$(id -u)" = 0 ]; then echo "ERROR: --just-me installs the bastion for your own account; run it without sudo. Nothing was installed." >&2; exit 2 fi if [ "$just_me" = 1 ] && [ "$networked" = 1 ]; then echo "ERROR: a shared machine's bastion runs for the whole machine; --just-me and --shared contradict each other" >&2; exit 2 fi if [ "$code_on_stdin" = 1 ]; then if [ "$(id -u)" != 0 ] || [ -n "$pair_given" ]; then echo "ERROR: unknown option. Nothing was installed." >&2; exit 2 fi IFS= read -r pair_code || true pair_given=1 case "$pair_code" in skp_*[!A-Za-z0-9_-]* | skp_) pair_code="" ;; skp_?*) ;; *) pair_code="" ;; esac [ -n "$pair_code" ] || { echo "ERROR: the pairing code did not arrive. Nothing was installed." >&2; exit 2; } fi asked_networked=$networked # paired_local DIR...: 0 when one of these state folders holds a pairing and # its mode is not networked (no mode.json reads as local, mode.go boxMode). # The service's own folder goes first. paired_local() { for d in "$@"; do [ -d "$d" ] || continue if [ -f "$d/mode.json" ] && grep -q '"networked"' "$d/mode.json"; then return 1; fi [ -f "$d/config.json" ] && return 0 for f in "$d"/pairings/*.json; do [ -f "$f" ] && return 0; done done return 1 } # pair_as ROOT OS BIN ARGS...: run the bastion as the state it serves. As # root that is the service's HOME (the unit's /root, launchd's /var/root), # whatever HOME `sudo` left behind; as yourself it is your own. pair_as() { pa_root=$1; pa_os=$2; shift 2 if [ "$pa_root" = 1 ]; then if [ "$pa_os" = darwin ]; then HOME=/var/root "$@"; else HOME=/root "$@"; fi else "$@" fi } # bastion_copy PID: 0 when PID is a process of this user running a program # named privatae-bastion, from any path, including one replaced on disk since # it started. A process that has exited, or another user's, is not. bastion_copy() { [ -O "/proc/$1" ] || return 1 bc_exe=$(readlink "/proc/$1/exe" 2>/dev/null) || return 1 case "${bc_exe##*/}" in "privatae-bastion" | "privatae-bastion (deleted)") return 0 ;; esac return 1 } # stop_user_copies: stop every copy of this user's bastion (Linux), as # install.ps1 does on Windows: one left running in a terminal, from an older # install or another path. `run` holds the machine's run lock, so the copy the # service starts would only wait behind it, and that copy never reads the new # pairing; an older build would fail on 127.0.0.1:7878 and restart for ever. # Asked to stop, then killed after 10 seconds. Says on stderr how many. stop_user_copies() { suc_pids="" for suc_d in /proc/[0-9]*; do suc_pid=${suc_d#/proc/} bastion_copy "$suc_pid" || continue kill "$suc_pid" 2>/dev/null && suc_pids="$suc_pids $suc_pid" done [ -n "$suc_pids" ] || return 0 set -- $suc_pids echo "Stopped $# copy(ies) of the bastion running outside its service (a terminal, or another install)." >&2 for suc_i in 1 2 3 4 5 6 7 8 9 10; do suc_left="" for suc_pid in $suc_pids; do bastion_copy "$suc_pid" && suc_left="$suc_left $suc_pid"; done [ -n "$suc_left" ] || return 0 sleep 1 done for suc_pid in $suc_left; do kill -9 "$suc_pid" 2>/dev/null || true; done } # user_unit_stays_up: 0 when the user unit privatae-bastion is running and # has kept the same process across a few seconds, longer than its # RestartSec, so a copy that fails at start and is restarted is caught. # Otherwise says what systemd shows, on stderr. user_unit_stays_up() { sleep 1 uu_first=$(systemctl --user show -p MainPID --value privatae-bastion 2>/dev/null) || uu_first="" sleep 4 uu_pid=$(systemctl --user show -p MainPID --value privatae-bastion 2>/dev/null) || uu_pid="" uu_state=$(systemctl --user show -p ActiveState --value privatae-bastion 2>/dev/null) || uu_state="" uu_sub=$(systemctl --user show -p SubState --value privatae-bastion 2>/dev/null) || uu_sub="" if [ "$uu_state" = active ] && [ "$uu_sub" = running ] && [ -n "$uu_pid" ] && [ "$uu_pid" != 0 ] && [ "$uu_pid" = "$uu_first" ]; then return 0 fi echo "The bastion's user service privatae-bastion is not staying up (systemd shows it ${uu_state:-unknown}/${uu_sub:-unknown}, and it has restarted)." >&2 echo "Its log says why: journalctl --user -u privatae-bastion" >&2 return 1 } # start_bastion BIN ROOT OS: start, or restart, the bastion so it serves every # pairing now and starts again by itself. A running bastion reads a new # pairing at its next start, so a restart is what a fresh pairing needs. # Prints one plain sentence saying how it will run. start_bastion() { sb_bin=$1; sb_root=$2; sb_os=$3 if [ "$sb_root" = 1 ] && [ "$sb_os" = linux ] && command -v systemctl >/dev/null 2>&1; then systemctl restart privatae-bastion || return 1 echo "The bastion runs as the system service privatae-bastion, and starts at boot." return 0 fi if [ "$sb_root" = 1 ] && [ "$sb_os" = darwin ] && [ -f /Library/LaunchDaemons/ai.privatae.bastion.plist ]; then /bin/launchctl kickstart -k system/ai.privatae.bastion || return 1 echo "The bastion runs as the system service ai.privatae.bastion, and starts at boot." return 0 fi if [ "$sb_root" != 1 ] && [ "$sb_os" = linux ] && command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then # A systemd user unit: yours, no root. It starts when you sign in; with # lingering on, at boot and after you sign out as well. sb_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" mkdir -p "$sb_dir" cat > "$sb_dir/privatae-bastion.service" </dev/null 2>&1 || return 1 # Its own copy first, then every other: what is left running after this # is only what the unit starts. systemctl --user stop privatae-bastion >/dev/null 2>&1 || true stop_user_copies systemctl --user restart privatae-bastion || return 1 user_unit_stays_up || return 1 if loginctl enable-linger "$(id -un)" >/dev/null 2>&1; then echo "The bastion runs as your systemd user service privatae-bastion, and starts at boot." else echo "The bastion runs as your systemd user service privatae-bastion while you are signed in." echo "To keep it running at boot and after you sign out: sudo loginctl enable-linger $(id -un)" fi return 0 fi if [ "$sb_root" != 1 ] && [ "$sb_os" = darwin ]; then # A LaunchAgent: yours, no root, started each time you log in. sb_plist="$HOME/Library/LaunchAgents/ai.privatae.bastion.user.plist" mkdir -p "$HOME/Library/LaunchAgents" cat > "$sb_plist" < Labelai.privatae.bastion.user ProgramArguments$sb_binrun RunAtLoad KeepAlive PLIST /bin/launchctl bootout "gui/$(id -u)/ai.privatae.bastion.user" >/dev/null 2>&1 || true /bin/launchctl bootstrap "gui/$(id -u)" "$sb_plist" || return 1 echo "The bastion runs as your LaunchAgent ai.privatae.bastion.user, and starts each time you log in." return 0 fi # No service manager this script can use: run it now, detached, once every # other copy of it has stopped (stop_user_copies says why). if [ "$sb_os" = linux ] && [ "$sb_root" != 1 ]; then stop_user_copies; fi nohup "$sb_bin" run >/dev/null 2>&1 & sb_pid=$! sleep 3 if [ "$sb_os" = linux ] && ! bastion_copy "$sb_pid"; then echo "The bastion stopped as soon as it started. Run it in a terminal to see why: $sb_bin run" >&2 return 1 fi echo "The bastion is running, until this machine restarts. Start it again with: $sb_bin run" } # record_owner BIN ROOT OS: run through sudo from a person's account on their # own computer, record that account as the machine's owner, whose own account # then opens the console without sudo (privatae-bastion ui). SUDO_UID is set by # sudo itself. Not on a shared machine: there the console stays the # administrator's (sudo -H privatae-bastion ui), so no agent running as the # account that ran sudo can reach it. A second administrator's reinstall leaves # the first owner in place; the bastion says so. record_owner() { ro_bin=$1; ro_root=$2; ro_os=$3 [ "$ro_root" = 1 ] || return 0 [ "$networked" != 1 ] || return 0 case "${SUDO_UID-}" in ""|0|*[!0-9]*) return 0 ;; esac pair_as "$ro_root" "$ro_os" "$ro_bin" approvals-owner --account "$SUDO_UID" &2 } # whole_machine_next BIN: what to do next on a person's own computer, the # bastion installed for the whole machine and no Host connected. whole_machine_next() { echo "The bastion runs for the whole machine and starts at boot. It is watching the AI agents on this computer now; no Host is needed." echo " Open its console: privatae-bastion ui" case "$(uname -s)" in Darwin) wmn_state="/var/root/Library/Application Support/privatae-bastion" ;; *) wmn_state=/root/.config/privatae-bastion ;; esac [ -f "$wmn_state/machine-factor/stepup.json" ] && return 0 echo " Set up your code, to stop an agent or answer what agents ask (an authenticator app on your phone):" echo " sudo -H privatae-bastion stepup-enroll --machine" } # pair_and_start BIN CODE ROOT OS: pair with CODE, handed to `pair -` on # standard input and never as an argument, where the process list would show # it. Then start the bastion and show its status. `pair` exits 3 when this # machine is already paired with that Host, which is success here too. pair_and_start() { ps_bin=$1; ps_code=$2; ps_root=$3; ps_os=$4 ps_rc=0 printf '%s\n' "$ps_code" | pair_as "$ps_root" "$ps_os" "$ps_bin" pair - || ps_rc=$? case "$ps_rc" in 0) ps_done="This machine is paired." ;; 3) ps_done="This machine was already paired with this Host; nothing changed there." ;; *) echo "ERROR: pairing did not finish (the reason is above). The bastion is installed; nothing else changed." >&2 return 1 ;; esac ps_how=$(start_bastion "$ps_bin" "$ps_root" "$ps_os") || { echo "ERROR: $ps_done But the bastion did not start. Start it with: $ps_bin run" >&2 return 1 } sleep 2 pair_as "$ps_root" "$ps_os" "$ps_bin" status || true echo "" echo "$ps_done $ps_how" } # whole_machine_script is what the whole-machine step runs as root: this same # installer, fetched by root from the one place it is published. A script # piped to sh cannot hand itself on, and a copy this account wrote would be a # file this account could change before root ran it. Hardcoded, like $base. whole_machine_script="https://privatae.ai/bastion/install.sh" # account_install_paired: 0 when this account already has a bastion of its own # that is paired. An upgrade keeps it where it is: moved to the whole machine, # its pairings would stay behind in this account. account_install_paired() { case "$(uname -s)" in Darwin) aip_dir="${HOME}/Library/Application Support/privatae-bastion" ;; *) aip_dir="${XDG_CONFIG_HOME:-$HOME/.config}/privatae-bastion" ;; esac [ -f "$aip_dir/config.json" ] && return 0 for aip_f in "$aip_dir"/pairings/*.json; do [ -f "$aip_f" ] && return 0; done return 1 } # elevate_whole_machine: install for the whole machine through sudo. Returns 3 # when sudo is missing or refuses (no admin rights, no terminal to ask on, a # wrong password): nothing was installed. Otherwise the root step's own exit # status. The pairing code reaches the root step on standard input, never in # an argument, where the process list would show it. elevate_whole_machine() { command -v sudo >/dev/null 2>&1 || return 3 echo "Installing the bastion for the whole machine, so that no program running as you, an AI agent included, can stop it or change what it allows." echo "sudo asks for your password once." # `sudo true`, not `sudo -v`: -v asks for a password unless every sudo rule # of the account is NOPASSWD (sudoers verifypw), so an account that may run # anything without one was asked anyway, and fell back to itself where no # terminal could answer. sudo -p '[bastion] sudo password for %u: ' true /dev/null 2>&1 || true rm -f "$acc_unit" systemctl --user daemon-reload >/dev/null 2>&1 || true echo "Removed this account's own bastion service; the machine's replaces it." fi acc_plist="$HOME/Library/LaunchAgents/ai.privatae.bastion.user.plist" if [ -f "$acc_plist" ]; then /bin/launchctl bootout "gui/$(id -u)/ai.privatae.bastion.user" >/dev/null 2>&1 || true rm -f "$acc_plist" echo "Removed this account's own bastion LaunchAgent; the machine's replaces it." fi if [ -f "$HOME/.local/bin/privatae-bastion" ] && [ ! -L "$HOME/.local/bin/privatae-bastion" ]; then rm -f "$HOME/.local/bin/privatae-bastion" echo "Removed $HOME/.local/bin/privatae-bastion, an earlier copy that would come first on PATH." fi if [ "$(uname -s)" = Linux ]; then stop_user_copies; fi } # machine_install_present: 0 when this machine's bastion is already installed # for the whole machine. A second one for an account beside it would only be # refused (the bastion's own guard), so none is installed. machine_install_present() { case "$(uname -s)" in Darwin) [ -f /Library/LaunchDaemons/ai.privatae.bastion.plist ] ;; *) [ -f /etc/systemd/system/privatae-bastion.service ] ;; esac } # Not root: the whole machine, through sudo, unless this account's own bastion # is asked for (--just-me) or is already paired. Where sudo cannot be used, # this account, and the installer says what that means; but never beside a # bastion installed for the whole machine. kept_account=0 fell_back=0 if [ "$root" != 1 ] && [ "$just_me" = 1 ] && machine_install_present; then echo "ERROR: this machine's bastion is installed for the whole machine, and a second one for your account would be refused beside it. Its console opens from its owner's account with: privatae-bastion ui. Nothing was installed." >&2 exit 1 fi if [ "$root" != 1 ] && [ "$just_me" != 1 ]; then if account_install_paired && ! machine_install_present; then kept_account=1 else ew_rc=0 elevate_whole_machine || ew_rc=$? case "$ew_rc" in 0) account_copy_cleanup; exit 0 ;; 3) if machine_install_present; then echo "ERROR: this machine's bastion is installed for the whole machine, and updating it needs an account that may use sudo, in a terminal. Nothing was installed." >&2 exit 1 fi fell_back=1 echo "sudo could not be used here, so the bastion is installed for your account only." >&2 ;; *) exit "$ew_rc" ;; esac fi fi if [ "$root" = 1 ] && [ "$local" != 1 ]; then case "$(uname -s)" in Linux) if command -v systemctl >/dev/null 2>&1; then networked=1; fi ;; Darwin) networked=1 ;; esac fi # A reinstall (an upgrade) of a machine already paired in local mode keeps # local mode unless --networked is passed. Switched by default, its pairing # would sit on an unanchored networked machine, which refuses every op until # claim-anchor is run, and `pair` would accept a second host. kept_local=0 if [ "$networked" = 1 ] && [ "$asked_networked" != 1 ]; then case "$(uname -s)" in Darwin) state_dirs="/var/root/Library/Application Support/privatae-bastion|${HOME}/Library/Application Support/privatae-bastion" ;; *) state_dirs="/root/.config/privatae-bastion|${XDG_CONFIG_HOME:-$HOME/.config}/privatae-bastion" ;; esac if paired_local "${state_dirs%%|*}" "${state_dirs#*|}"; then networked=0; kept_local=1; fi fi if [ "$networked" = 1 ] && [ "$root" != 1 ]; then echo "ERROR: --networked needs root — a networked bastion is a root-owned service (run with sudo)" >&2; exit 1 fi key="$os-$arch" tmp=$(mktemp); man=$(mktemp); vdir=$(mktemp -d) # Any exit before the final mv must leave nothing behind and install nothing: # this script is piped to `sudo sh`, so a partial run is a root-privileged # partial install. trap 'rm -f "$tmp" "$man"; rm -rf "$vdir"' EXIT INT TERM die() { echo "ERROR: $*" >&2; exit 1; } # Pick the digest tool ONCE, here — not inside the command substitution that # computes the hash. `exit` inside `$(...)` only leaves the subshell, so a # missing-tool check there would print an error and then carry on installing # an unverified binary, which is the exact failure this whole change exists to # prevent. Linux ships sha256sum; macOS ships shasum. Neither: refuse. if command -v sha256sum >/dev/null 2>&1; then SHACMD="sha256sum" elif command -v shasum >/dev/null 2>&1; then SHACMD="shasum -a 256" else die "no sha256sum or shasum on this system — cannot verify the download, refusing to install" fi sha_of() { $SHACMD "$1" | cut -d' ' -f1; } # ── the release signature ────────────────────────────────────────────────── # # The manifest names each binary's digest, and the download is checked against # it. The manifest itself is signed with a release key whose public half is # pinned here, the same keys a running bastion checks every update with # (client-go/release_sign.go productionReleaseKeys; install_sig_sh_test.go # holds the two to each other). So a copy of this script read on GitHub and run # from there as root, or with --just-me, installs only a release we signed # (possibly an older one), whatever the download host serves. Run without # sudo, the copy does not install for the whole machine itself: its root step # fetches this installer from the website again (whole_machine_script) and # runs that. The signature is over one statement, as verifyReleaseSignature # builds it: privatae-release-signature-v1, the kind, the version, and the # manifest's sha256, a line each; it sits beside the manifest at # version.json..sig, in base64. # # Checked with an openssl that does Ed25519 (OpenSSL 3, on most Linux; the # openssl@3 Homebrew installs on a Mac). A probe signature made for this tells # "this openssl cannot check" from "the signature is wrong": the first prints a # note and keeps the digest check alone, which is what the script did before; # the second refuses to install. RELEASE_KEYS='MCowBQYDK2VwAyEAtWOyZm38DwTwbawSnvExQG/CQsVJBKYWc46arCBZj9E= MCowBQYDK2VwAyEACKL+J0VSdqrfY4AmxChvylJ1ha4e8GvcrYF2o17Ah+g=' SIG_PROBE_KEY='MCowBQYDK2VwAyEA1TRy5NeI2n7tieFf6cHe5VE69SV0XL7Vx4k6EwFlFFk=' SIG_PROBE_SIG='Do3fnwjyv8QgR+9sJcc0A3Sr0XzSKqBW8DK8kzaJL29tS8KxuStc2tp+HheJvoGwXmrH2kg7YN+yDLKVDnDqDQ==' # release_signature_check : returns when the # manifest's signature verifies, or when no openssl here can check one (said); # exits when it does not verify. release_signature_check() { rs_man=$1 rs_dir=$2 rs_pem() { printf -- '-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n' "$1" > "$2"; } rs_ssl="" rs_pem "$SIG_PROBE_KEY" "$rs_dir/probe.pem" printf 'privatae-installer-probe-v1\n' > "$rs_dir/probe.msg" printf 'privatae-installer-probe-v0\n' > "$rs_dir/probe.other" # The probe signature must verify over its message and not over another: an # openssl that passes every signature is no check, and is set aside like one # that cannot check, never taken for one. for c in openssl /opt/homebrew/opt/openssl@3/bin/openssl /usr/local/opt/openssl@3/bin/openssl; do command -v "$c" >/dev/null 2>&1 || continue printf '%s' "$SIG_PROBE_SIG" | "$c" base64 -d -A -out "$rs_dir/probe.sig" 2>/dev/null || continue if "$c" pkeyutl -verify -pubin -inkey "$rs_dir/probe.pem" -rawin -in "$rs_dir/probe.msg" \ -sigfile "$rs_dir/probe.sig" >/dev/null 2>&1 && ! "$c" pkeyutl -verify -pubin -inkey "$rs_dir/probe.pem" -rawin -in "$rs_dir/probe.other" \ -sigfile "$rs_dir/probe.sig" >/dev/null 2>&1; then rs_ssl=$c; break fi done if [ -z "$rs_ssl" ]; then echo "NOTE: no openssl here can check an Ed25519 signature, so the release's signature is not checked;" echo " the download is still checked against the manifest's digest, fetched over HTTPS." if [ "$os" = darwin ]; then echo " To check the signature too, install OpenSSL 3 (brew install openssl@3) and run this again." else echo " To check the signature too, install OpenSSL 3 from your distribution and run this again." fi return 0 fi rs_sum=$(sha_of "$rs_man") case "$rs_sum" in *[!0-9a-f]* | "") die "cannot take the manifest's digest — refusing to install unverified" ;; esac rs_ver=$(sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$rs_man" | head -n 1) case "$rs_ver" in "" | *[!0-9A-Za-z.+-]*) die "the manifest names no usable version — refusing to install unverified" ;; esac rs_short=$(printf '%s' "$rs_sum" | cut -c1-16) curl -fsSL --max-filesize 4096 "$base/version.json.$rs_short.sig" -o "$rs_dir/sig.b64" \ || die "cannot fetch the release's signature ($base/version.json.$rs_short.sig) — refusing to install unverified" tr -d ' \r\n' < "$rs_dir/sig.b64" | "$rs_ssl" base64 -d -A -out "$rs_dir/sig" 2>/dev/null \ || die "the release's signature is malformed — refusing to install" [ "$(wc -c < "$rs_dir/sig" | tr -d ' ')" = 64 ] || die "the release's signature is malformed — refusing to install" printf 'privatae-release-signature-v1\nbastion\n%s\n%s\n' "$rs_ver" "$rs_sum" > "$rs_dir/statement" for k in $RELEASE_KEYS; do rs_pem "$k" "$rs_dir/key.pem" if "$rs_ssl" pkeyutl -verify -pubin -inkey "$rs_dir/key.pem" -rawin -in "$rs_dir/statement" \ -sigfile "$rs_dir/sig" >/dev/null 2>&1; then echo "Release signature verified (version $rs_ver)." return 0 fi done die "the release's signature does not verify against the pinned release keys — refusing to install" } echo "Fetching manifest..." curl -fsSL "$base/version.json" -o "$man" || die "cannot fetch $base/version.json — refusing to install unverified" release_signature_check "$man" "$vdir" # Pull this platform's expected digest out of version.json without needing jq or # python (neither is guaranteed on a clean macOS or a minimal container): find the # stanza for our key, then take the quoted value on its "sha256" line. expected=$(awk -v k="\"$key\"" ' index($0, k) { f = 1 } f && /"sha256"/ { n = split($0, a, "\""); print a[n-1]; exit } ' "$man") case "$expected" in *[!0-9a-f]* | "") die "manifest has no usable sha256 for $key — refusing to install unverified" ;; esac [ ${#expected} -eq 64 ] || die "manifest sha256 for $key is malformed — refusing to install unverified" # Take the URL FROM THE MANIFEST, never build one. The manifest's url now carries # ?v=, which is what stops Cloudflare handing back the PREVIOUS release's # binary against the CURRENT release's digest — a stale cache that surfaces as # "checksum MISMATCH" and reads like a corrupt or tampered download. Same awk # shape as the digest read above (no jq, no python on a clean macOS). Falls back # to the constructed path for a manifest that predates the field. url=$(awk -v k="\"$key\"" ' index($0, k) { f = 1 } f && /"url"/ { n = split($0, a, "\""); print a[n-1]; exit } ' "$man") case "$url" in https://*) ;; *) url="$base/dl/privatae-bastion-$key" ;; esac echo "Downloading privatae-bastion ($key)..." curl -fsSL "$url" -o "$tmp" || die "download failed for $key" actual=$(sha_of "$tmp") if [ "$actual" != "$expected" ]; then die "checksum MISMATCH for privatae-bastion-$key expected $expected actual $actual The downloaded binary is not the one the platform published. Nothing was installed." fi echo "Checksum verified ($expected)." darwin_service=0 if [ "$os" = darwin ] && [ "$root" = 1 ]; then # The launchd daemon runs this program as root, so only root may be able to # replace it: a root-owned folder, not /usr/local/bin, which a common package # manager hands to the signed-in user on many Macs. `service install` checks # the whole path and refuses otherwise. darwin_service=1 appdir="/Library/Application Support/privatae" mkdir -p "$appdir/bin" chown root:wheel "$appdir" "$appdir/bin" chmod 755 "$appdir" "$appdir/bin" bin="$appdir/bin/privatae-bastion" fi chmod +x "$tmp" mv "$tmp" "$bin" if [ "$darwin_service" = 1 ]; then chown root:wheel "$bin" chmod 755 "$bin" # The command people type still works: a link on PATH to the real program. # -n replaces an existing link instead of following it: /usr/local/bin may # belong to the signed-in user, who could otherwise point the name at a # directory and have this root shell create the link inside it. mkdir -p /usr/local/bin && ln -sfn "$bin" /usr/local/bin/privatae-bastion fi echo "Installed: $bin" if [ "$os" = linux ] && [ "$root" = 1 ] && command -v systemctl >/dev/null 2>&1; then # This unit has NO User= line: the service runs as root. Measured on real # installs: `ps` shows root running the bastion, and every such install came # from this same script. `run` itself needs no root — the # tunnel is userspace — but the networked bastion's ops (observe:*, session:*) # do, and a root install is what makes claim-anchor's root check mean # something. A single-user machine that wants the local, non-root shape runs # the installer WITHOUT sudo and gets the --user path below instead. cat > /etc/systemd/system/privatae-bastion.service </dev/null 2>&1 || true # restart, not start: a reinstall must run the new program. systemctl restart privatae-bastion >/dev/null 2>&1 || true record_owner "$bin" "$root" "$os" echo "" if [ -n "$pair_code" ]; then : elif [ "$networked" = 1 ]; then echo "Networked bastion. Pair it with YOUR host; the first host paired becomes its anchor:" echo " sudo -H privatae-bastion pair " echo "Then enrol your second factor, so you can approve here:" echo " sudo -H privatae-bastion stepup-enroll" echo "Every terminal and write waits for your approval, and every later host for your host's." echo "(Already paired before this install? Anchor with: sudo -H privatae-bastion claim-anchor)" elif [ "$kept_local" = 1 ]; then echo "This machine is already paired as a LOCAL bastion, and reinstalling keeps it local." echo "To make it a networked server, anchored to the host it serves:" echo " sudo -H privatae-bastion set-mode networked" echo " sudo -H privatae-bastion claim-anchor" echo " sudo -H privatae-bastion stepup-enroll" echo " sudo systemctl restart privatae-bastion" else whole_machine_next "$bin" echo " Connect a Host, if you run one (optional): sudo -H privatae-bastion pair " fi elif [ "$darwin_service" = 1 ]; then # The mode is box state under root's home, where the daemon keeps its state # (the daemon's HOME is /var/root), written by the binary itself so the path # is resolved exactly one way. Before the service starts, so its first run # already sees it. if [ "$networked" = 1 ]; then HOME=/var/root "$bin" set-mode networked || die "could not set networked mode" fi "$bin" service install || die "could not register the system service" record_owner "$bin" "$root" "$os" echo "" if [ -z "$pair_code" ] && [ "$networked" != 1 ]; then whole_machine_next "$bin" elif [ -z "$pair_code" ]; then echo "Networked bastion, running as a system service (launchd, as root)." echo "Pair it with YOUR host; the first host paired becomes its anchor:" # The full path, not the name on PATH: /usr/local/bin may belong to the # signed-in user, who could point the link at their own program, and this # line is run as root. echo " sudo -H '$bin' pair " echo "Then enrol your second factor, so you can approve here:" echo " sudo -H '$bin' stepup-enroll" echo "Every terminal and write waits for your approval, and every later host for your host's." echo "(Already paired before this install? Anchor with: sudo -H '$bin' claim-anchor)" fi elif [ "$networked" = 1 ]; then die "--networked needs a system service manager: systemd on Linux, launchd on macOS" else case ":$PATH:" in *":$bindir:"*) ;; *) echo "Add to PATH: export PATH=\"\$PATH:$bindir\"" ;; esac echo "" if [ "$kept_account" = 1 ]; then echo "This account's own bastion is already paired, so it stays installed for this account." echo "To move it to the whole machine: disconnect its Host (privatae-bastion unpair ), run this" echo "installer again, and connect the Host again with: sudo -H privatae-bastion pair " fi echo "WARNING: installed for your account only. Any program running as you, an AI agent included, can stop" echo " the bastion or change what it allows." if [ "$just_me" = 1 ]; then echo " To install it for the whole machine instead, run this installer again without --just-me." elif [ "$fell_back" = 1 ]; then echo " To install it for the whole machine, run this installer again in a terminal, from an account that may use sudo." fi if [ -z "$pair_code" ]; then if start_how=$(start_bastion "$bin" "$root" "$os"); then echo "$start_how" echo "It is watching the AI agents in this account now; no Host is needed. Open its console: $bin ui" else echo "Start it with: $bin run" >&2 fi echo "Connect a Host, if you run one (optional): $bin pair " fi fi if [ -n "$pair_code" ]; then echo "" pair_and_start "$bin" "$pair_code" "$root" "$os" || exit 1 fi echo "privatae-bastion is free software under the GNU GPL v3 or later: https://privatae.ai/bastion/LICENSE.txt"