f8fc09e5cc
store.GetHostOOBStates parses the agent oob heartbeat stanza. monitor/host_oob: transition-based oob_degraded/oob_recovered warning (felhom-sshd down while the operator peer is configured, OR config invalid) — proactive "can the operator get in right now" signal; unconfigured OOB never alerts. Wired into the 60s sweep. Non-hollow tests + transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
114 lines
4.3 KiB
Go
114 lines
4.3 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"net/netip"
|
|
)
|
|
|
|
// OperatorOOBNote marks the single fleet-wide operator OOB peer among wg_peers (TASK H1). It is an
|
|
// UNBOUND peer (host_id ''), so it rides the existing empty-host_id support + the partial-unique
|
|
// idx_wg_peers_host (which only constrains non-empty host_id) without collision [OF-4]. It is pushed
|
|
// to the endpoint by peersync like any peer (so the endpoint accepts the operator's handshake), and
|
|
// its assigned_ip becomes each box's desired-state oob_peer_ip (rendered into wg-felhom AllowedIPs).
|
|
const OperatorOOBNote = "operator-oob"
|
|
|
|
// GetOperatorOOBPeer returns the fleet operator OOB peer, or (nil, nil) if none is registered.
|
|
func (s *Store) GetOperatorOOBPeer() (*WGPeer, error) {
|
|
var p WGPeer
|
|
err := s.db.QueryRow(
|
|
`SELECT pubkey, assigned_ip, host_id, note, created_at FROM wg_peers WHERE note = ? LIMIT 1`,
|
|
OperatorOOBNote).
|
|
Scan(&p.Pubkey, &p.AssignedIP, &p.HostID, &p.Note, &p.CreatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
// SetOperatorOOBPeer registers/rotates THE operator OOB peer at an EXPLICIT tunnel address (so the
|
|
// endpoint's STATIC forward chain can hardcode the operator /32). Last-write-wins: any prior
|
|
// operator-oob row is replaced (key rotation / IP change). Validated: assignedIP is an IPv4 host
|
|
// address inside the endpoint's tunnel_subnet, not the network/broadcast/pbs_tunnel_ip, and not
|
|
// already taken by a DIFFERENT (customer) peer. host_id stays '' (unbound) so it is never counted as
|
|
// a customer host.
|
|
func (s *Store) SetOperatorOOBPeer(pubkey, assignedIP string) error {
|
|
ip, err := netip.ParseAddr(assignedIP)
|
|
if err != nil || !ip.Is4() {
|
|
return fmt.Errorf("operator oob: assigned_ip %q is not an IPv4 address", assignedIP)
|
|
}
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
var subnet, pbsIP string
|
|
err = tx.QueryRow(`SELECT tunnel_subnet, pbs_tunnel_ip FROM wg_endpoints ORDER BY endpoint_id LIMIT 1`).
|
|
Scan(&subnet, &pbsIP)
|
|
if err == sql.ErrNoRows {
|
|
return ErrWGEndpointUnset
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pfx, err := netip.ParsePrefix(subnet)
|
|
if err != nil {
|
|
return fmt.Errorf("operator oob: endpoint tunnel_subnet %q unparseable: %w", subnet, err)
|
|
}
|
|
if !pfx.Contains(ip) {
|
|
return fmt.Errorf("operator oob: assigned_ip %s is not inside tunnel_subnet %s", assignedIP, subnet)
|
|
}
|
|
if ip == pfx.Masked().Addr() || ip.String() == pbsIP || ip == lastAddr(pfx) {
|
|
return fmt.Errorf("operator oob: assigned_ip %s is a reserved address (network/broadcast/pbs)", assignedIP)
|
|
}
|
|
// The IP must be free OR already this operator peer's (idempotent re-set at the same IP).
|
|
var otherPubkey string
|
|
err = tx.QueryRow(`SELECT pubkey FROM wg_peers WHERE assigned_ip = ? AND note != ?`, assignedIP, OperatorOOBNote).
|
|
Scan(&otherPubkey)
|
|
if err == nil {
|
|
return fmt.Errorf("operator oob: assigned_ip %s is already taken by peer %s", assignedIP, shortKey(otherPubkey))
|
|
}
|
|
if err != sql.ErrNoRows {
|
|
return err
|
|
}
|
|
// The pubkey must not be bound to a host (an operator peer is never a customer key).
|
|
var boundHost string
|
|
err = tx.QueryRow(`SELECT host_id FROM wg_peers WHERE pubkey = ? AND host_id != ''`, pubkey).Scan(&boundHost)
|
|
if err == nil {
|
|
return fmt.Errorf("operator oob: pubkey is already bound to host %s — refusing", boundHost)
|
|
}
|
|
if err != sql.ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.Exec(`DELETE FROM wg_peers WHERE note = ?`, OperatorOOBNote); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(`INSERT INTO wg_peers (pubkey, assigned_ip, host_id, note) VALUES (?, ?, '', ?)`,
|
|
pubkey, assignedIP, OperatorOOBNote); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// oobOperatorSSHKeyKey is the hub_settings key for the fleet operator SSH public key (H1).
|
|
const oobOperatorSSHKeyKey = "oob_operator_ssh_pubkey"
|
|
|
|
// SetOOBOperatorSSHKey stores the operator's SSH PUBLIC key (an authorized_keys line, delivered to
|
|
// every box via the desired-state so felhom-sshd honours the operator login). "" clears it.
|
|
func (s *Store) SetOOBOperatorSSHKey(pubkey string) error { return s.setSetting(oobOperatorSSHKeyKey, pubkey) }
|
|
|
|
// GetOOBOperatorSSHKey returns the operator SSH public key ("" if unset).
|
|
func (s *Store) GetOOBOperatorSSHKey() string { return s.getSetting(oobOperatorSSHKeyKey) }
|
|
|
|
func shortKey(k string) string {
|
|
if len(k) > 12 {
|
|
return k[:12] + "…"
|
|
}
|
|
return k
|
|
}
|