eb227486d0
- internal/poke: pinned-host-key SSH poke sender (wgsync sibling) + fire-and-forget Notifier (PokeHost/PokeAllHosts). Poke refuses non-WG targets pre-dial; contentless via ep0 forced command to the box WG /32:51822. - wiring: Server.SetPoke; applyPBSDR pokes the host after each descriptor gen-bump; handleSetArtifacts (MinAgent floor) pokes all hosts. main.go env POKE_SSH_KEY_FILE (reuses peersync endpoint/hostkey). - scripts/felhom-poke.sh (non-root forced command) + offsite-endpoint.md §11; manifests/hub.yaml Secret/agent-poke + POKE_SSH_KEY_FILE (image tag bump follows the build).
135 lines
5.4 KiB
Go
135 lines
5.4 KiB
Go
// Package poke is the agent-plane immediate-sync SENDER (Direction-2a,
|
||
// SPIKE-immediate-sync-transport-2026-07-16). It is the third structural sibling of
|
||
// internal/wgsync and internal/tenantsync: the hub holds a forced-command SSH credential to the
|
||
// offsite endpoint (ep0) and drives it; the endpoint stays a dumb, runbook-provisioned box.
|
||
//
|
||
// A poke is a CONTENTLESS "sync now" nudge: the hub SSHes to ep0's `felhom-poke` forced command
|
||
// with the target box's WireGuard /32 as the command string (→ SSH_ORIGINAL_COMMAND); the forced
|
||
// command validates it and sends ONE empty UDP datagram from wg0 to that /32:51822. The agent's
|
||
// poke listener (felhom-agent internal/poke) fires an immediate desired-state cycle. Measured
|
||
// ~0.42 s per fresh SSH session in the spike (target ≤2–3 s).
|
||
//
|
||
// It carries NO payload semantics, NO auth handshake, NO retry: a lost or forged poke costs at
|
||
// most one extra debounced tick, and the 15-min report cycle remains the guarantee. The transport
|
||
// is SSH with a PINNED host key (the wgsync posture — exact match or refuse, no insecure
|
||
// fallback); credential theft bounds the attacker to "make registered boxes tick", and WireGuard
|
||
// itself refuses to encrypt to any /32 no registered peer owns (spike P1 EKEYREJECTED).
|
||
package poke
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"net/netip"
|
||
"strings"
|
||
"time"
|
||
|
||
"golang.org/x/crypto/ssh"
|
||
)
|
||
|
||
// wgSubnet confines every poke target to the WG /24 (defence in depth — the ep0 forced command
|
||
// validates independently, and the kernel refuses non-peer /32s).
|
||
var wgSubnet = netip.MustParsePrefix("10.77.0.0/24")
|
||
|
||
// Config configures the SSH poke client. All values come from the deployment env / mounted Secret
|
||
// (operator infra — never a customer record). Addr + HostKeyLine mirror wgsync/tenantsync; the
|
||
// private key is the poke key's OWN (the ep0 authorized_keys line selects the forced command).
|
||
type Config struct {
|
||
Addr string // "host:22"
|
||
User string // "felhom-peersync" (same user; the key selects the forced command)
|
||
PrivateKey []byte // PEM private key (from the mounted Secret file)
|
||
HostKeyLine string // endpoint host pubkey, authorized_keys format (the wgsync pin)
|
||
Timeout time.Duration // default 10s (a poke must be quick or abandoned)
|
||
}
|
||
|
||
// Client is a pinned-host-key SSH poke sender. Construct with New (parses keys up front).
|
||
type Client struct {
|
||
addr string
|
||
user string
|
||
signer ssh.Signer
|
||
hostKey ssh.PublicKey
|
||
timeout time.Duration
|
||
logger *log.Logger
|
||
}
|
||
|
||
// New builds a Client, failing early on an unparsable key or host-key line.
|
||
func New(cfg Config, logger *log.Logger) (*Client, error) {
|
||
if cfg.Addr == "" || cfg.User == "" {
|
||
return nil, fmt.Errorf("poke: Addr and User are required")
|
||
}
|
||
signer, err := ssh.ParsePrivateKey(cfg.PrivateKey)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("poke: parse private key: %w", err)
|
||
}
|
||
hostKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(cfg.HostKeyLine))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("poke: parse host key line: %w", err)
|
||
}
|
||
timeout := cfg.Timeout
|
||
if timeout == 0 {
|
||
timeout = 10 * time.Second
|
||
}
|
||
if logger == nil {
|
||
logger = log.Default()
|
||
}
|
||
return &Client{addr: cfg.Addr, user: cfg.User, signer: signer, hostKey: hostKey, timeout: timeout, logger: logger}, nil
|
||
}
|
||
|
||
// Poke sends one contentless UDP nudge to boxWGIP via the ep0 forced command. boxWGIP is validated
|
||
// to be inside the WG /24 before any connection (the ep0 script validates again). The box IP is
|
||
// the SSH command string (the forced command reads $SSH_ORIGINAL_COMMAND). Returns an error on any
|
||
// failure — but the CALLER treats this fire-and-forget (log, never fail the operator save).
|
||
func (c *Client) Poke(ctx context.Context, boxWGIP string) error {
|
||
addr, err := netip.ParseAddr(boxWGIP)
|
||
if err != nil || !wgSubnet.Contains(addr) {
|
||
return fmt.Errorf("poke: refusing non-WG target %q", boxWGIP)
|
||
}
|
||
sshCfg := &ssh.ClientConfig{
|
||
User: c.user,
|
||
Auth: []ssh.AuthMethod{ssh.PublicKeys(c.signer)},
|
||
HostKeyCallback: ssh.FixedHostKey(c.hostKey),
|
||
HostKeyAlgorithms: []string{c.hostKey.Type()}, // pin the algorithm (wgsync S1 finding)
|
||
Timeout: c.timeout,
|
||
}
|
||
dialer := net.Dialer{Timeout: c.timeout}
|
||
conn, err := dialer.DialContext(ctx, "tcp", c.addr)
|
||
if err != nil {
|
||
return fmt.Errorf("poke: dial %s: %w", c.addr, err)
|
||
}
|
||
if dl, ok := ctx.Deadline(); ok {
|
||
conn.SetDeadline(dl)
|
||
} else {
|
||
conn.SetDeadline(time.Now().Add(c.timeout))
|
||
}
|
||
sconn, chans, reqs, err := ssh.NewClientConn(conn, c.addr, sshCfg)
|
||
if err != nil {
|
||
conn.Close()
|
||
return fmt.Errorf("poke: ssh handshake %s: %w", c.addr, err)
|
||
}
|
||
client := ssh.NewClient(sconn, chans, reqs)
|
||
defer client.Close()
|
||
conn.SetDeadline(time.Time{})
|
||
if dl, ok := ctx.Deadline(); ok {
|
||
conn.SetDeadline(dl)
|
||
}
|
||
|
||
session, err := client.NewSession()
|
||
if err != nil {
|
||
return fmt.Errorf("poke: session: %w", err)
|
||
}
|
||
defer session.Close()
|
||
|
||
var stdout, stderr bytes.Buffer
|
||
session.Stdout = &stdout
|
||
session.Stderr = &stderr
|
||
// The forced command IGNORES this string for execution but reads it as $SSH_ORIGINAL_COMMAND —
|
||
// that is how the target box IP crosses to ep0. The command carries only the IP, nothing else.
|
||
if err := session.Run(boxWGIP); err != nil {
|
||
return fmt.Errorf("poke: remote felhom-poke failed: %w (stderr: %s)", err, strings.TrimSpace(stderr.String()))
|
||
}
|
||
c.logger.Printf("[INFO] poke: sync-poke delivered to %s via %s", boxWGIP, c.addr)
|
||
return nil
|
||
}
|