hub v0.167.0 source (not built, not deployed): D31 — the F-14 PBS auto re-issue reads the newest deletion of a host that held a key, a keyless install between no longer blocks it (R-1205); D30 — the new key's handshake clears wg_tunnel_stale (pinned), wording; wire gate entry; Part 1 stray-copy 9202 evidence
gates / gates (push) Successful in 15m46s
gates / gates (push) Successful in 15m46s
This commit is contained in:
1 parent
96ac07e168
commit
84751cb0ce
18 files changed
+517
-13
No files matched your search
@@ -1,3 +1,19 @@
|
||||
## v0.167.0 (unreleased — committed 2026-10-22/23 night, NOT built, NOT deployed) — D31 and the D30 alarm clear
|
||||
|
||||
- **D31 (operator 2026-10-22, R-1205).** The F-14 PBS auto-re-issue now reads the customer's newest deletion of a host
|
||||
that HELD A KEY (`store.LatestKeyHoldingHostDeletion`, `escrow_acked = 1`) instead of the newest deletion of any
|
||||
host. A later install deleted before it made a key (an un-acked record — `DeleteHost` refuses a host with an escrow
|
||||
row unless acknowledged) owned no tenancy and no longer leaves the next install's whole-box tier waiting for the
|
||||
operator's press (seen live 2026-10-21). Only keyless deletions, or none → the manual refusal, byte-unchanged. Tests
|
||||
`TestLatestKeyHoldingHostDeletion_SkipsAKeylessInstall`, `TestPBSDR_D31_AKeylessInstallBetweenDoesNotBlockTheReissue`,
|
||||
each red-proved.
|
||||
- **D30 (R-1204).** The agent's re-key (felhom-agent `ae69ad4`) registers the new key on `POST /hosts/<id>/wg` (re-key
|
||||
in place, unchanged); the new key's first handshake clears `wg_tunnel_stale` once — pinned by
|
||||
`TestD30_TheAlarmClearsWhenTheNewKeyShakesHands` (red-proved). The stale text says an agent with D30 re-keys by itself
|
||||
within about 15 minutes; the `wg_tunnel_ok` log line is INFO; the source comment no longer says the ok event is
|
||||
mailed (it is stored, not mailed — review 2026-10-22 night).
|
||||
- `scripts/wire_contract_gate.py`: `wireguard.last_handshake_age_s` left the unread list (the hub reads it since 0.166.0).
|
||||
|
||||
## v0.166.0 (unreleased — built 2026-10-22 night, NOT deployed; the deploy is the operator's yes) — the clock-ahead tunnel alarm (R-1204)
|
||||
|
||||
- **`wg_tunnel_stale` / `wg_tunnel_ok` (operator-only).** A box that ran once with its clock ahead stamps a WireGuard
|
||||
|
||||
@@ -10,8 +10,11 @@ import (
|
||||
// than the last one accepted from that peer), so the box's tunnel to ep0 — its PBS whole-box copies and the
|
||||
// operator's door — stays dead until the real date passes the wrong one, while the box itself reports normally.
|
||||
// Nothing said why. The hub now tells the OPERATOR once per host, edge-triggered from the host's newest tunnel event
|
||||
// (the host_swap shape), and once when it is healthy again. Operator-only (notify.operatorOnlyEvents): the household
|
||||
// cannot fix it. The repair (re-key the peer, or ep0 forgets the stored timestamp) is a separate decision.
|
||||
// (the host_swap shape), and records once when it is healthy again (wg_tunnel_ok is an info event: stored, not
|
||||
// mailed). Operator-only (notify.operatorOnlyEvents): the household cannot fix it. The repair is D30 (operator
|
||||
// 2026-10-22): an agent with it makes a new key after 15 minutes of a future-stamped handshake and registers it here
|
||||
// (re-key in place, the full-list push to ep0); the new key's first handshake clears this alarm — pinned by
|
||||
// TestD30_TheAlarmClearsWhenTheNewKeyShakesHands.
|
||||
const (
|
||||
eventWGTunnelStale = "wg_tunnel_stale"
|
||||
eventWGTunnelOK = "wg_tunnel_ok"
|
||||
@@ -36,7 +39,7 @@ func wgTunnelStale(w *hostWireguard) (bool, string) {
|
||||
return false, ""
|
||||
}
|
||||
if w.HandshakeFutureS != nil && *w.HandshakeFutureS > 0 {
|
||||
return true, fmt.Sprintf("its last handshake is stamped %s in the FUTURE by the box's own clock — the clock ran ahead once, and ep0 refuses every newer handshake until that time passes", humanDuration(*w.HandshakeFutureS))
|
||||
return true, fmt.Sprintf("its last handshake is stamped %s in the FUTURE by the box's own clock — the clock ran ahead once, and ep0 refuses every newer handshake until that time passes (an agent with the D30 repair makes a new key by itself within about 15 minutes)", humanDuration(*w.HandshakeFutureS))
|
||||
}
|
||||
if w.LastHandshakeAgeS != nil && *w.LastHandshakeAgeS > wgTunnelStaleAfterS {
|
||||
return true, fmt.Sprintf("it has not shaken hands for %s while the box reports — the box's clock may have run ahead (then ep0 refuses it), or ep0 dropped the peer", humanDuration(*w.LastHandshakeAgeS))
|
||||
@@ -78,7 +81,7 @@ func (h *Handler) checkWGTunnel(hostID, custID string, w *hostWireguard) {
|
||||
default:
|
||||
return
|
||||
}
|
||||
h.logger.Printf("[WARN] host %s: %s", hostID, typ)
|
||||
h.logger.Printf("[%s] host %s: %s", map[bool]string{true: "WARN", false: "INFO"}[stale], hostID, typ)
|
||||
det := map[string]any{"host_id": hostID}
|
||||
if w.LastHandshakeAgeS != nil {
|
||||
det["last_handshake_age_s"] = *w.LastHandshakeAgeS
|
||||
|
||||
@@ -76,3 +76,32 @@ func TestR1204_WhatIsJudged(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// D30 (R-1204): after the agent re-keys, the box reports a restarted tunnel with no handshake yet (nothing to judge),
|
||||
// then the new key's first handshake — which clears the alarm once, and raises nothing new.
|
||||
// RED-PROOF (2026-10-22 night): with the `!stale && wasStale` case removed from checkWGTunnel, this fails (no ok event).
|
||||
func TestD30_TheAlarmClearsWhenTheNewKeyShakesHands(t *testing.T) {
|
||||
h, st, _ := newTestHandler(t)
|
||||
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"})
|
||||
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
|
||||
post := func(wg string) {
|
||||
t.Helper()
|
||||
if rr := do(h, http.MethodPost, "/host-report", "HKEY", tunnelReportBody("h1", wg)); rr.Code != http.StatusOK {
|
||||
t.Fatalf("host-report = %d %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
count := func(typ string) int {
|
||||
evs, _ := st.GetEventsByType("c1", typ, time.Time{})
|
||||
return len(evs)
|
||||
}
|
||||
post(`{"pubkey":"OLD","registered":true,"active":true,"last_handshake_age_s":0,"handshake_future_s":10800}`) // 3 h ahead
|
||||
post(`{"pubkey":"NEW","registered":true,"active":true}`) // re-keyed, no handshake yet
|
||||
if count(eventWGTunnelStale) != 1 || count(eventWGTunnelOK) != 0 {
|
||||
t.Fatalf("stale %d ok %d, want 1 / 0 before the new key shakes hands", count(eventWGTunnelStale), count(eventWGTunnelOK))
|
||||
}
|
||||
post(`{"pubkey":"NEW","registered":true,"active":true,"last_handshake_age_s":20}`)
|
||||
post(`{"pubkey":"NEW","registered":true,"active":true,"last_handshake_age_s":110}`)
|
||||
if count(eventWGTunnelOK) != 1 || count(eventWGTunnelStale) != 1 {
|
||||
t.Fatalf("stale %d ok %d, want 1 / 1 — the new key's handshake clears the alarm once", count(eventWGTunnelStale), count(eventWGTunnelOK))
|
||||
}
|
||||
}
|
||||
@@ -281,3 +281,40 @@ func TestCountHostArtifacts(t *testing.T) {
|
||||
t.Errorf("unknown host artifacts = %+v, want zero value", empty)
|
||||
}
|
||||
}
|
||||
|
||||
// D31 (R-1205): the F-14 gate's row is the newest deletion of a host that HELD A KEY; a later keyless install's
|
||||
// un-acked deletion is skipped. RED-PROOF (2026-10-22 night): with the `AND escrow_acked = 1` filter removed, this
|
||||
// fails (it returns the keyless gen2-host).
|
||||
func TestLatestKeyHoldingHostDeletion_SkipsAKeylessInstall(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
seedHostWithArtifacts(t, s, "gen1-host", "cust-d31")
|
||||
if err := s.DeleteHost("gen1-host", true); err != nil { // held a key → acked
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.UpsertHost(&Host{HostID: "gen2-host", CustomerID: "cust-d31", APIKey: "k2"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.DeleteHost("gen2-host", true); err != nil { // never made a key → un-acked even with the box ticked
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec, err := s.LatestKeyHoldingHostDeletion("cust-d31")
|
||||
if err != nil || rec == nil {
|
||||
t.Fatalf("LatestKeyHoldingHostDeletion = %+v, %v", rec, err)
|
||||
}
|
||||
if rec.HostID != "gen1-host" || !rec.EscrowAcked {
|
||||
t.Errorf("row = %+v, want the key-holding gen1-host", rec)
|
||||
}
|
||||
if rec, err := s.LatestKeyHoldingHostDeletion("cust-never"); err != nil || rec != nil {
|
||||
t.Errorf("no deletions → nil, got %+v, %v", rec, err)
|
||||
}
|
||||
s2 := newTestStore(t)
|
||||
if err := s2.UpsertHost(&Host{HostID: "only-keyless", CustomerID: "cust-k", APIKey: "k"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s2.DeleteHost("only-keyless", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec, err := s2.LatestKeyHoldingHostDeletion("cust-k"); err != nil || rec != nil {
|
||||
t.Errorf("only a keyless deletion → nil (the manual path stays), got %+v, %v", rec, err)
|
||||
}
|
||||
}
|
||||
@@ -3336,6 +3336,31 @@ func (s *Store) LatestHostDeletion(customerID string) (*HostDeletion, error) {
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// LatestKeyHoldingHostDeletion returns the customer's most recent deletion of a host that HELD A KEY — escrow_acked=1,
|
||||
// the escrow-ack flow over a real escrow row (nil when there is none). D31 (operator 2026-10-22, R-1205): the F-14
|
||||
// gate reads THIS row. An un-acked deletion is a host that never made a key (DeleteHost refuses a host with an escrow
|
||||
// row unless acknowledged), so it orphaned no tenancy and must not block the re-issue for the next install — the
|
||||
// escrow carry's own reasoning (escrow_carry.go). Pinned by TestLatestKeyHoldingHostDeletion_SkipsAKeylessInstall
|
||||
// and TestPBSDR_D31_AKeylessInstallBetweenDoesNotBlockTheReissue.
|
||||
func (s *Store) LatestKeyHoldingHostDeletion(customerID string) (*HostDeletion, error) {
|
||||
var d HostDeletion
|
||||
var deletedAt string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT host_id, customer_id, deleted_at
|
||||
FROM host_deletions WHERE customer_id = ? AND escrow_acked = 1
|
||||
ORDER BY id DESC LIMIT 1`, customerID,
|
||||
).Scan(&d.HostID, &d.CustomerID, &deletedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.DeletedAt = parseSQLiteTime(deletedAt)
|
||||
d.EscrowAcked = true
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// UpsertHost creates or updates a host identity (used by the admin mint). On
|
||||
// conflict it updates only operator-settable identity fields + updated_at; it does
|
||||
// NOT touch the reality columns (agent_version/last_report_at) or the inert intent
|
||||
|
||||
@@ -235,11 +235,13 @@ func (s *Server) pbsdrProvisionAtom(ctx context.Context, customerID string, host
|
||||
// half-torn earlier attempt, or the F-14 shape: host deleted, tenancy survived).
|
||||
//
|
||||
// F-14 gate (operator ruling 2026-07-13): auto-re-issue is permitted ONLY when the
|
||||
// hub's own deletion record shows the tenancy's owning host — the customer's most
|
||||
// recent host deletion — was removed through the escrow-ack flow. Acknowledged
|
||||
// destruction is not silent re-keying; the old secret went down with the acked host.
|
||||
// No record / un-acked record → the refusal below, byte-unchanged (manual path).
|
||||
rec, derr := s.store.LatestHostDeletion(customerID)
|
||||
// hub's own deletion record shows the tenancy's owning host was removed through the
|
||||
// escrow-ack flow. Acknowledged destruction is not silent re-keying; the old secret went
|
||||
// down with the acked host. D31 (operator 2026-10-22, R-1205): the owning host is the
|
||||
// newest deleted host that HELD A KEY — a later install deleted before it made a key
|
||||
// (an un-acked record) owned nothing and no longer blocks this. No key-holding record →
|
||||
// the refusal below, byte-unchanged (manual path).
|
||||
rec, derr := s.store.LatestKeyHoldingHostDeletion(customerID)
|
||||
if derr != nil {
|
||||
return "", fmt.Errorf("pbsdr: deletion-provenance lookup for %s: %w", customerID, derr)
|
||||
}
|
||||
|
||||
@@ -457,7 +457,7 @@ func TestPBSDR_F14AutoReissueOnAckedDeletion(t *testing.T) {
|
||||
}
|
||||
|
||||
// F-14 scenario B (the never-silently-re-key law): a surviving tenancy WITHOUT an acked
|
||||
// deletion record — none at all, or the latest one un-acked — keeps the current refusal and
|
||||
// deletion record — none at all, or only un-acked ones (D31: a keyless install) — keeps the current refusal and
|
||||
// records ZERO re-issue calls (the exact non-effect).
|
||||
// RED-PROOF (§10 B): removing the record check in pbsdrProvisionAtom (auto-reissue
|
||||
// unconditionally) → the zero-reissue assertions here fail.
|
||||
@@ -499,6 +499,40 @@ func TestPBSDR_F14NoRecordNeverRekeys(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// D31 (operator 2026-10-22, R-1205 — seen live 2026-10-21): the tenancy's owning host was deleted through the escrow-ack
|
||||
// flow, then a SECOND install was deleted before it made a key. The re-issue for the third install proceeds.
|
||||
// RED-PROOF (2026-10-22 night): with the gate reading LatestHostDeletion again (the newest row, un-acked), this fails
|
||||
// with the 502 refusal and zero re-issue calls.
|
||||
func TestPBSDR_D31_AKeylessInstallBetweenDoesNotBlockTheReissue(t *testing.T) {
|
||||
fake := &fakeTenancy{provisionErr: tenantsync.ErrTokenExists, secret: "REISSUED-SECRET"}
|
||||
s, st, _ := newPBSDRServer(t, fake)
|
||||
if err := st.UpsertHost(&store.Host{HostID: "peti-00-dead", CustomerID: "peti", APIKey: "oldkey"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := st.SaveHostEscrow("peti-00-dead", []byte("opaque"), "fp", "posture", "2026-07-01T00:00:00Z", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.DeleteHost("peti-00-dead", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.UpsertHost(&store.Host{HostID: "peti-00-short", CustomerID: "peti", APIKey: "shortkey"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.DeleteHost("peti-00-short", true); err != nil { // no escrow row → an un-acked record
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr := postUpdate(t, s, url.Values{"dr_tier": {"on"}})
|
||||
if rr.Code != 303 {
|
||||
t.Fatalf("enable = %d (%s), want 303 — a keyless install between must not block the re-issue", rr.Code, rr.Body.String())
|
||||
}
|
||||
if fake.reissueCalls != 1 {
|
||||
t.Errorf("reissue calls = %d, want 1", fake.reissueCalls)
|
||||
}
|
||||
if desc, _, _ := hostState(t, st); desc == nil || !desc.Enabled {
|
||||
t.Fatalf("descriptor after the re-issue = %+v", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPBSDR_Reissue(t *testing.T) {
|
||||
fake := &fakeTenancy{secret: "OLD-SECRET"}
|
||||
s, st, logBuf := newPBSDRServer(t, fake)
|
||||
|
||||
Reference in new issue
Block a user