bastion 0.63.1
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 4m58s
ci / fuzz (push) Successful in 3m33s

This commit is contained in:
Privatae
2026-11-03 22:56:18 +00:00
parent 2043fa5e9e
commit 48899b41a9
78 changed files with 10366 additions and 307 deletions
+101 -52
View File
@@ -195,16 +195,18 @@ func pairPrecheck(cfgs []*Config, handle string, replace, forceActive bool) (exi
if boxMode() == modeLocal && len(cfgs) > 0 {
return nil, "", &localSecondHostError{serving: cfgs[0].HostName}
}
return pairPrecheckState(forceActive)
return pairPrecheckState(forceActive, handle)
}
// pairPrecheckState is the state a new pairing gets once pairPrecheck's walls
// are passed: active for claim-anchor, pending behind an anchor, else "".
func pairPrecheckState(forceActive bool) (*Config, string, error) {
// are passed: active for claim-anchor, pending behind an anchor (or, on a
// machine an organisation's policy governs and no Host holds yet, behind the
// Host the policy names: anchor_local.go), else "".
func pairPrecheckState(forceActive bool, handle string) (*Config, string, error) {
if forceActive {
return nil, pairingActive, nil
}
if currentAnchor() != nil {
if currentAnchor() != nil || pendingUnderPolicy(handle) {
return nil, pairingPending, nil
}
return nil, "", nil
@@ -280,29 +282,29 @@ func cmdPair(token, redeemURL, hostCAFile string, replace bool) error {
if err != nil {
return err
}
// A networked machine is a server. Left unanchored it refuses every op to
// every host (capsAllowIn: unanchoredNetworkedRefusal) until someone runs
// claim-anchor, a step people missed. So the first pairing on a networked
// box with no anchor anchors it, as `claim-anchor` with no token would, and
// from then on every terminal and write waits for the person's approval and
// every later pairing for this host's. Only with machine authority, which
// claim-anchor needs anyway.
// A networked machine is a server, with one Host in charge of it: the
// anchor (anchor.go). Left unanchored it refuses every op to every host
// (capsAllowIn: unanchoredNetworkedRefusal) until someone runs
// claim-anchor, a step people missed. So with machine authority, which
// claim-anchor needs anyway, `pair` puts the right Host in charge by
// itself (anchor_local.go anchorAfterPair): the Host an organisation's
// policy names, else a Host on this machine, else the first Host paired.
// The pairing itself succeeded, so a failed anchor is reported, not
// returned: `pair` failing here would read as "could not pair" to a caller
// (npx cseries exits on it) when the pairing exists. claim-anchor, which
// such a caller runs next, reports the anchor's own failure.
anchored, err := anchorFirstNetworkedPairing(cfg, all)
took, err := anchorAfterPair(cfg, all)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: %v\n", err)
}
if anchored {
if took.Taken {
if fresh, lerr := loadPairings(); lerr == nil {
if c := pairingFor(fresh, cfg.HostName); c != nil {
cfg, all = c, fresh
}
}
}
pairReport(os.Stdout, cfg, all, anchored)
pairReport(os.Stdout, cfg, all, took)
return nil
}
@@ -327,45 +329,77 @@ func pairOptsFor(token, redeemURL, hostCAFile string, replace bool) (string, pai
}
// pairReport is what `pair` prints once the pairing stands. Whatever the form
// (anchored just now, over TCP, over a unix socket, pending, or over the
// tunnel), it ends by saying to enrol a second factor when this pairing has
// none and no approval device is pinned: without one, a local machine installs
// a folder grant on the host's signed grant alone, and an anchored one has no
// way to approve anything the host asks for.
func pairReport(w io.Writer, cfg *Config, all []*Config, anchored bool) {
// (put in charge just now, over TCP, over a unix socket, waiting to be
// admitted, or over the tunnel), it ends by saying to enrol a second factor
// when this pairing has none and no approval device is pinned: without one, a
// local machine installs a folder grant on the host's signed grant alone, and
// an anchored one has no way to approve anything the host asks for.
//
// It never tells the person to start a bastion that is already running: a
// running bastion serves a new pairing by itself within supervisePeriod
// (run.go superviseLoop), and on a machine whose bastion is a system service
// the command that starts it is the service manager's (pairServeNote).
func pairReport(w io.Writer, cfg *Config, all []*Config, took anchorTake) {
enrol := ""
if !secondFactorAvailable(cfg.dcnDir()) {
enrol = stepupEnrollHint(cfg.HostName, len(all))
}
defer func() { pairEnrolNote(w, cfg.HostName, enrol) }()
if anchored {
fmt.Fprintf(w, "\nThis machine is networked and anchored to %s: every terminal and every write\n", cfg.HostName)
fmt.Fprintln(w, "waits for your approval, and any later host waits for this one to approve it.")
if took.Taken {
fmt.Fprintf(w, "\n%s is now the Host in charge of this machine: %s.\n", cfg.HostName, took.Why)
if took.Replaced != "" {
fmt.Fprintf(w, "%s was in charge before. It is told so, and waits for %s to admit it, as any other Host does.\n",
took.Replaced, cfg.HostName)
}
if others := without(took.Waiting, took.Replaced); len(others) > 0 {
fmt.Fprintf(w, "Waiting for %s to admit them: %s.\n", cfg.HostName, strings.Join(others, ", "))
}
fmt.Fprintln(w, "Every terminal and every write waits for your approval, and any other Host waits")
fmt.Fprintf(w, "until %s admits it: %s asks you to approve it.\n", cfg.HostName, cfg.HostName)
if enrol != "" {
fmt.Fprintf(w, "Enrol your second factor now, so you can approve here:\n %s\n\n", enrol)
enrol = ""
}
}
if cfg.tcpTransport() && cfg.active() {
if !cfg.active() {
who := admitter()
if who == "" {
fmt.Fprintf(w, "Paired with %s — PENDING. Nothing runs for it until it is admitted.\n", cfg.HostName)
return
}
why := "the Host in charge of this computer"
if p := currentAnchorPolicy(); p != nil && boxMode() == modeNetworked && policyNames(p, who) {
why += ", named by an organisation's policy (" + p.Source + ")"
}
fmt.Fprintf(w, "Paired with %s — WAITING for %s to admit it.\n\n", cfg.HostName, who)
fmt.Fprintf(w, "The code was used: %s now lists this computer, but nothing runs for it yet (no lanes).\n", cfg.HostName)
fmt.Fprintf(w, "%s is %s, and it admits every other Host:\n", who, why)
fmt.Fprintf(w, "%s asks you to approve %s. Once you do, %s is served at once, with no restart.\n", who, cfg.HostName, cfg.HostName)
fmt.Fprintf(w, "Until then `privatae-bastion status` shows it as waiting for %s.\n", who)
return
}
if cfg.hostDials() {
fmt.Fprintf(w, "Paired with %s. Your Host connects to this server; this server never connects to it.\n\n", cfg.HostName)
fmt.Fprintf(w, " lanes: over the connections your Host opens to port %s (mutual TLS, the Host's CA pinned, name %s)\n coder runs: none over TCP\n",
doorPort(cfg.doorAddr()), tcpServerName)
fmt.Fprintf(w, " firewall: this server needs TCP port %s reachable inbound; the bastion keeps its own rule for it in a firewall it manages "+
"here (status shows it), and nothing on your Host's network needs opening\n", doorPort(cfg.doorAddr()))
fmt.Fprintf(w, " local: http://%s\n\n%s\n", cfg.ListenAddr, pairServeNote())
return
}
if cfg.tcpTransport() {
fmt.Fprintf(w, "Paired with %s over TCP.\n\n", cfg.HostName)
fmt.Fprintf(w, " lanes: %s (mutual TLS, the Host's CA pinned, name %s)\n coder runs: none over TCP\n", cfg.LaneAddr, tcpServerName)
fmt.Fprintf(w, " local: http://%s\n\nStart (or restart) the bastion:\n privatae-bastion run\n", cfg.ListenAddr)
fmt.Fprintf(w, " local: http://%s\n\n%s\n", cfg.ListenAddr, pairServeNote())
return
}
if cfg.unixTransport() && cfg.active() {
if cfg.unixTransport() {
fmt.Fprintf(w, "Paired with %s, a Host on this machine.\n\n", cfg.HostName)
fmt.Fprintf(w, " lanes: %s (mutual TLS, the Host's CA pinned)\n coder runs: a socket per run in %s\n", cfg.LaneSocket, cfg.RunsDir)
fmt.Fprintf(w, " local: http://%s\n\nStart (or restart) the bastion:\n privatae-bastion run\n", cfg.ListenAddr)
fmt.Fprintf(w, " local: http://%s\n\n%s\n", cfg.ListenAddr, pairServeNote())
return
}
if !cfg.active() {
fmt.Fprintf(w, "Paired with %s — PENDING.\n\n", cfg.HostName)
fmt.Fprintln(w, "This machine is anchored, so a new host waits until the anchor host approves it.")
fmt.Fprintln(w, "Nothing is started for it yet: no tunnel, no lanes. It shows on the anchor host's")
fmt.Fprintln(w, "Network app as a pending pairing; once approved the daemon opens its lanes by itself.")
return
}
fmt.Fprintf(w, "Paired with %s.\n\nOne `privatae-bastion run` serves every pairing:\n", cfg.HostName)
fmt.Fprintf(w, "Paired with %s.\n\nOne bastion serves every pairing:\n", cfg.HostName)
for _, c := range all {
// The listen address is the CLI's loopback control port for that host
// (add-folder, git-credential). The local secret that guards it is read
@@ -373,10 +407,37 @@ func pairReport(w io.Writer, cfg *Config, all []*Config, anchored bool) {
// operator client to hand it to since the MCP relay was removed.
fmt.Fprintf(w, "\n %s\n local: http://%s\n", c.HostName, c.ListenAddr)
}
fmt.Fprintf(w, "\nStart (or restart) the tunnels:\n privatae-bastion run\n")
fmt.Fprintf(w, "\n%s\n", pairServeNote())
fmt.Fprintf(w, "\ngit to your forge goes through this bastion — one-time setup:\n privatae-bastion git-setup\n")
}
// pairDaemonRunning: a bastion is serving on this machine (its console
// answers, or the system service runs, waiting for its first pairing). A seam
// for the suite.
var pairDaemonRunning = func() bool { return uiDaemonListening() || systemServiceRunning() }
// pairServeNote is `pair`'s last line about serving the new pairing: a running
// bastion picks it up by itself; otherwise the command that starts this
// machine's bastion (the service manager's on a service machine).
func pairServeNote() string {
if pairDaemonRunning() {
return fmt.Sprintf("The bastion running on this machine serves it within %d seconds; nothing needs restarting.",
int(supervisePeriod.Seconds()))
}
return "Start the bastion so it serves it:\n " + runCommandHint()
}
// without is list less drop.
func without(list []string, drop string) []string {
var out []string
for _, s := range list {
if s != drop {
out = append(out, s)
}
}
return out
}
// pairEnrolNote says what a pairing with no second factor means on this kind
// of machine, and how to enrol one. hint "" says nothing.
func pairEnrolNote(w io.Writer, host, hint string) {
@@ -393,19 +454,6 @@ func pairEnrolNote(w io.Writer, host, hint string) {
"sessions are refused. Enrol it now, so each needs a code from this machine:\n %s\n", host, hint)
}
// anchorFirstNetworkedPairing anchors a networked machine that has no anchor
// to the pairing just made, when it is the machine's only one and the caller
// holds machine authority (see cmdPair). Reports whether it anchored.
func anchorFirstNetworkedPairing(cfg *Config, all []*Config) (bool, error) {
if boxMode() != modeNetworked || currentAnchor() != nil || len(all) != 1 || !machineAuthorityHeld() {
return false, nil
}
if err := cmdClaimAnchor("", false, "", cfg.HostName); err != nil {
return false, fmt.Errorf("paired with %s, but anchoring this networked machine to it failed: %w\n anchor it with: %s", cfg.HostName, err, anchorCommandHint())
}
return true, nil
}
// redeemURLBase is the platform's enrollment ingress. A variable so a test can
// serve a redeem response on loopback; nothing outside tests assigns it.
var redeemURLBase = "https://api.privatae.ai"
@@ -509,7 +557,7 @@ func pairWith(token string, opts pairOpts) (*Config, []*Config, error) {
if !yes {
return nil, nil, fmt.Errorf("nothing was paired: %s stays the only Host this computer serves", second.serving)
}
existing, state, err = pairPrecheckState(forceActive)
existing, state, err = pairPrecheckState(forceActive, handle)
}
// The CA this pair trusts, as a fingerprint: the pairing code's, or that
// of the --host-ca file. "" for a platform or unix-socket pairing, whose
@@ -738,8 +786,9 @@ func commitPairing(cfg, existing *Config, replace bool, state string) (*Config,
// host renamed when its address moved). unpair() is deliberately not
// reused: it refuses to remove the anchor's own pairing, which is exactly
// the case here — the anchor is re-paired like every other host. The anchor
// RECORD is untouched by design; anchor.json still names the old host and
// only `claim-anchor --rotate` may re-sign it.
// RECORD is untouched here; anchor.json still names the old host until
// `claim-anchor --repin` re-signs it, or, for a Host paired again through a
// window under its pinned CA, carryAnchorToNewName (pair_window.go) does.
if replace && existing != nil && existing.HostName != cfg.HostName {
if err := removeStalePairingFile(existing.HostName); err != nil {
return nil, nil, fmt.Errorf("paired as %s, but the superseded pairing file for %s "+