slice 8A (agent half): local-API server + provisioning back-half (v0.10.0)
internal/localapi: per-guest local-API server (doc 03 §6) — 7 self-scoped endpoints, hashed per-guest token store, persisted self-signed leaf with stable SHA-256 pin, optional 6th daemon goroutine. internal/provision: back-half — mint token, render bootstrap.json (no registry cred), write 0600, chown 100000:100000, attach pct-set bind mount (host-side, F3, no pct exec). --selftest=provision. build-golden.sh bakes the controller image + bootstrap unit. sudoers FELHOM_PROVISION; firewall narrowing artifact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,9 +29,83 @@ type Config struct {
|
||||
Storage StorageConfig `json:"storage"`
|
||||
Backup BackupConfig `json:"backup"`
|
||||
Escrow EscrowConfig `json:"escrow"`
|
||||
LocalAPI LocalAPIConfig `json:"local_api"`
|
||||
LogLevel string `json:"log_level"` // debug|info|warn|error (default info)
|
||||
}
|
||||
|
||||
// LocalAPIConfig configures the per-guest local API server (doc 03 §6, slice 8A). The
|
||||
// controller (inside its LXC) reaches the agent over the local bridge; the agent is the
|
||||
// per-guest authorization gate — it maps a per-guest bearer token → VMID and authorizes
|
||||
// every call against THAT guest only (self-scoped; never a caller-supplied id). Disabled
|
||||
// unless Enable is set AND ListenAddr is non-empty (so a host that doesn't yet provision
|
||||
// controllers runs the daemon without it).
|
||||
//
|
||||
// Defense-in-depth (spike gotcha 5): ListenAddr should be the host's BRIDGE IP (not
|
||||
// 0.0.0.0), and a host firewall rule should limit the port to the guest bridge subnet
|
||||
// (configs/felhom-localapi-firewall.example). The per-guest token remains the gate; the
|
||||
// bind + firewall narrow exposure but are not the authorization.
|
||||
type LocalAPIConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
ListenAddr string `json:"listen_addr"` // bridge IP:port, e.g. "192.168.0.162:8443"
|
||||
// CertFile/KeyFile hold the agent's self-signed leaf served to controllers. Generated
|
||||
// (persisted) on first start if absent, so the leaf SHA-256 fingerprint — baked into each
|
||||
// guest's bootstrap for pinning — is STABLE across agent restarts.
|
||||
CertFile string `json:"cert_file"` // default <token_store_dir>/local-api.crt
|
||||
KeyFile string `json:"key_file"` // default <token_store_dir>/local-api.key
|
||||
// TokenStore is the durable, hashed token→guest map (only a HASH of each token is
|
||||
// persisted; the plaintext exists transiently at mint→write-to-mount, then is discarded).
|
||||
TokenStore string `json:"token_store"` // default /var/lib/felhom-agent/local-tokens.log
|
||||
}
|
||||
|
||||
// Default local-API file locations (under the agent's state dir).
|
||||
const (
|
||||
defaultLocalAPITokenStore = "/var/lib/felhom-agent/local-tokens.log"
|
||||
defaultLocalAPICert = "/var/lib/felhom-agent/local-api.crt"
|
||||
defaultLocalAPIKey = "/var/lib/felhom-agent/local-api.key"
|
||||
)
|
||||
|
||||
// Enabled reports whether the local-API server should run.
|
||||
func (l LocalAPIConfig) Enabled() bool {
|
||||
return l.Enable && strings.TrimSpace(l.ListenAddr) != ""
|
||||
}
|
||||
|
||||
// TokenStorePath returns the configured token-store path (default applied).
|
||||
func (l LocalAPIConfig) TokenStorePath() string {
|
||||
if l.TokenStore != "" {
|
||||
return l.TokenStore
|
||||
}
|
||||
return defaultLocalAPITokenStore
|
||||
}
|
||||
|
||||
// CertPath/KeyPath return the configured leaf cert/key paths (defaults applied).
|
||||
func (l LocalAPIConfig) CertPath() string {
|
||||
if l.CertFile != "" {
|
||||
return l.CertFile
|
||||
}
|
||||
return defaultLocalAPICert
|
||||
}
|
||||
|
||||
func (l LocalAPIConfig) KeyPath() string {
|
||||
if l.KeyFile != "" {
|
||||
return l.KeyFile
|
||||
}
|
||||
return defaultLocalAPIKey
|
||||
}
|
||||
|
||||
// Validate checks the local-API config is usable when enabled.
|
||||
func (l LocalAPIConfig) Validate() error {
|
||||
if !l.Enable {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(l.ListenAddr) == "" {
|
||||
return fmt.Errorf("config: local_api.listen_addr is required when local_api.enable is set (use the host bridge IP:port, e.g. 192.168.0.162:8443)")
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(l.ListenAddr); err != nil {
|
||||
return fmt.Errorf("config: local_api.listen_addr %q is not host:port: %w", l.ListenAddr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EscrowConfig configures PBS recovery-code escrow creation (slice 7, doc 03 §8a). Enrollment-time
|
||||
// only (not the steady-state daemon). The default posture is zero-knowledge (Felhom holds the
|
||||
// opaque blob, the customer holds the recovery code).
|
||||
|
||||
Reference in New Issue
Block a user