Documentation
¶
Overview ¶
Package agentserver implements the gRPC Ingest service that remote agents connect to for enrollment and event streaming (Pro only).
Index ¶
- Constants
- Variables
- func CauseForReason(reason string) error
- func GenerateChallenge() (*agentpb.AuthChallenge, error)
- func LoadOrGenerateTLS(certFile, keyFile string, hosts []string, logger *slog.Logger) (*tls.Config, error)
- func LogsCommand(externalID string, lines int, timestamps, follow bool) *agentpb.AgentCommand
- func ResolvePublicURL(req *http.Request, cfg PublicURLConfig) (string, []string)
- func Verify(req *agentpb.AuthResponse, nonce []byte, ag *agent.Agent, serverNow time.Time) error
- type CertificateHandler
- type ContainerHandler
- type ContainerInventoryHandler
- type Deps
- type DispatchDeps
- type Dispatcher
- type EndpointHandler
- type EventBroadcaster
- type EventMeta
- type HeartbeatHandler
- type HostOSHandler
- type KubernetesTopologyHandler
- type LabelSyncFunc
- type Limiter
- type PublicURLConfig
- type ResourceHandler
- type Server
- type Sessions
- func (s *Sessions) Close(agentID, reason string)
- func (s *Sessions) CloseStream(agentID string, tok Token, reason string)
- func (s *Sessions) DeliverResult(agentID string, res *agentpb.CommandResult)
- func (s *Sessions) EventsPerSecond5m() float64
- func (s *Sessions) EventsSeen(agentID string) int64
- func (s *Sessions) FetchLogs(ctx context.Context, agentID, externalID string, lines int, timestamps bool) ([]string, error)
- func (s *Sessions) HasCapability(agentID, capability string) bool
- func (s *Sessions) IncrEvents(agentID string)
- func (s *Sessions) IsConnected(agentID string) bool
- func (s *Sessions) ListConnected() []string
- func (s *Sessions) Open(agentID string, cancel context.CancelCauseFunc, addr string, caps []string, ...) Token
- func (s *Sessions) RecordSpoolStatus(agentID string, st *agentpb.SpoolStatus)
- func (s *Sessions) SendCommand(ctx context.Context, agentID, capability string, cmd *agentpb.AgentCommand) (<-chan *agentpb.CommandResult, func(), error)
- func (s *Sessions) SetLifecycleAlertHook(fn func(agentID, reason string, connected bool))
- func (s *Sessions) SpoolStatus(agentID string) *SpoolState
- func (s *Sessions) StartRingAdvancer(ctx context.Context)
- func (s *Sessions) StartStaleWatcher(ctx context.Context, interval, threshold, grace time.Duration, ...)
- type SpoolState
- type StaleAgentsFn
- type SwarmTopologyHandler
- type Token
Constants ¶
const CapabilityLogs = "logs"
CapabilityLogs is advertised by agents able to serve container logs on demand.
const OfflineReportGrace = 2 * time.Minute
OfflineReportGrace is how long after startup the stale watcher waits before reporting agents that are absent with no live stream. An agent reconnects with a backoff capped at 60s ±25% jitter, so a shorter delay would page for every agent still on its way back after a server restart.
Variables ¶
var ( ErrBadSignature = errors.New("invalid ed25519 signature") ErrClockSkew = errors.New("client clock skew exceeds 300s") ErrAgentRevoked = errors.New("agent is revoked") ErrAgentUnknown = errors.New("agent not found") )
var ( ErrSessionRevoked = errors.New("agent_revoked") ErrSessionReplaced = errors.New("session_replaced") ErrSessionStale = errors.New("session_stale") ErrSessionClosed = errors.New("session_closed") )
Reasons a session is torn down. They travel to the Push handler as the stream context's cancellation cause, so it can tell the agent whether to give up or reconnect — reporting every teardown as a revocation would make an agent that was merely reaped as stale exit for good.
var ( ErrAgentNotConnected = errors.New("agent not connected") ErrAgentCannotServe = errors.New("agent does not support this command") ErrTooManyRequests = errors.New("too many in-flight commands for this agent") )
Errors returned by the command path, mapped to HTTP status by the API layer.
Functions ¶
func CauseForReason ¶ added in v1.3.8
CauseForReason maps a Close reason to the cause handed to the agent.
func GenerateChallenge ¶
func GenerateChallenge() (*agentpb.AuthChallenge, error)
GenerateChallenge creates a 32-byte random nonce for the auth handshake.
func LoadOrGenerateTLS ¶
func LoadOrGenerateTLS(certFile, keyFile string, hosts []string, logger *slog.Logger) (*tls.Config, error)
LoadOrGenerateTLS returns a TLS config for the agent gRPC server.
If both certFile and keyFile are non-empty, the keypair is loaded from disk. Otherwise a self-signed dev certificate is generated in-memory and a loud warning is logged — agents must then connect with --grpc-insecure-skip-tls-verify. Intended for local development only.
hosts seeds the certificate SAN (Subject Alternative Names) so a strict client can still verify it. Empty/unknown hosts default to 127.0.0.1 + localhost.
func LogsCommand ¶ added in v1.3.8
func LogsCommand(externalID string, lines int, timestamps, follow bool) *agentpb.AgentCommand
LogsCommand builds a logs command with a fresh request id. Exported because the SSE follow path drives SendCommand directly to stream chunks as they arrive.
func ResolvePublicURL ¶
func ResolvePublicURL(req *http.Request, cfg PublicURLConfig) (string, []string)
ResolvePublicURL returns the grpcs:// URL that remote agents should use plus a list of warnings. Resolution priority: explicit config > X-Forwarded-Host+Proto headers from the HTTP request > request Host header.
A "public_url_appears_local" warning is appended whenever the resolved host is localhost, 127.x.x.x, ::1, or an RFC-1918 / link-local address.
Types ¶
type CertificateHandler ¶
type CertificateHandler interface {
HandleAgentEvent(ctx context.Context, agentID string, ev *agentpb.CertificateInfo) error
}
CertificateHandler processes a certificate scan result from a remote agent.
type ContainerHandler ¶
type ContainerHandler interface {
HandleAgentEvent(ctx context.Context, agentID string, ev *agentpb.ContainerEvent, meta EventMeta) error
}
ContainerHandler processes a container event from a remote agent.
type ContainerInventoryHandler ¶ added in v1.3.8
type ContainerInventoryHandler interface {
HandleAgentInventory(ctx context.Context, agentID string, ev *agentpb.ContainerInventory, meta EventMeta) error
}
ContainerInventoryHandler reconciles a full container snapshot from an agent.
type Deps ¶
type Deps struct {
AgentStore *store.AgentStore
Broadcaster EventBroadcaster
Sessions *Sessions
Limiter *Limiter
Dispatcher *Dispatcher
Logger *slog.Logger
}
Deps groups the dependencies required by the agent gRPC server.
type DispatchDeps ¶
type DispatchDeps struct {
Container ContainerHandler
Inventory ContainerInventoryHandler
Endpoint EndpointHandler
Heartbeat HeartbeatHandler
Resource ResourceHandler
Certificate CertificateHandler
Swarm SwarmTopologyHandler
Kubernetes KubernetesTopologyHandler
HostOS HostOSHandler
// LabelSync, if set, provisions endpoint/cert monitors from a container's
// labels after each container event. Optional (nil = no label discovery).
LabelSync LabelSyncFunc
}
DispatchDeps groups the optional per-domain handlers the dispatcher calls. A nil handler means that event type is silently ignored.
type Dispatcher ¶
type Dispatcher struct {
// contains filtered or unexported fields
}
Dispatcher routes AgentEvents to the appropriate domain handler.
func NewDispatcher ¶
func NewDispatcher(deps DispatchDeps) *Dispatcher
NewDispatcher creates a Dispatcher with the given handler set.
func (*Dispatcher) Dispatch ¶
func (d *Dispatcher) Dispatch(ctx context.Context, agentID string, evt *agentpb.AgentEvent) error
Dispatch routes evt to the handler matching its body type, attributing every event to agentID — the identity proven by the auth handshake, NOT the client-controlled agent_id carried on the wire. Returns an error if a handler is wired and returns an error. Silently ignores events whose handler is nil.
func (*Dispatcher) RejectedEvents ¶ added in v1.6.0
func (d *Dispatcher) RejectedEvents(agentID string) uint64
RejectedEvents returns how many events the dispatcher refused for an out of range observation time since startup, for agentID.
type EndpointHandler ¶
type EndpointHandler interface {
HandleAgentEvent(ctx context.Context, agentID string, ev *agentpb.EndpointEvent, meta EventMeta) error
}
EndpointHandler processes an endpoint probe result from a remote agent.
type EventBroadcaster ¶
EventBroadcaster is the minimal interface required to publish SSE events to connected clients.
type EventMeta ¶ added in v1.6.0
type EventMeta = agentevent.Meta
EventMeta is the observation time and the replay flag of an agent event.
type HeartbeatHandler ¶
type HeartbeatHandler interface {
HandleAgentEvent(ctx context.Context, agentID string, ev *agentpb.HeartbeatEvent) error
}
HeartbeatHandler processes a heartbeat ping from a remote agent.
type HostOSHandler ¶ added in v1.7.0
type HostOSHandler interface {
HandleAgentHostOS(ctx context.Context, agentID string, ev *agentpb.HostOSMsg) error
}
HostOSHandler records the operating system identity an agent reports for its host.
type KubernetesTopologyHandler ¶
type KubernetesTopologyHandler interface {
HandleAgentEvent(ctx context.Context, agentID string, ev *agentpb.KubernetesTopology) error
}
KubernetesTopologyHandler processes a full Kubernetes topology snapshot from an agent.
type LabelSyncFunc ¶
type LabelSyncFunc func(ctx context.Context, agentID, containerName, externalID string, labels map[string]string)
LabelSyncFunc provisions label-discovered endpoint/cert monitors for a remote agent's container. Invoked for every container event so monitors track label changes: created on first sight, deprovisioned when a label is removed. The agent itself probes them and pushes the results.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter manages per-agent token-bucket rate limiters.
func NewLimiter ¶
NewLimiter creates a per-agent Limiter allowing eventsPerSecond events per second for each agent, with an equal burst. A value <= 0 falls back to the default (1000/s). Driven by MAINTENANT_AGENT_RATE_LIMIT_PER_SECOND.
type PublicURLConfig ¶
type PublicURLConfig struct {
// Explicit override from MAINTENANT_GRPC_URL env or --grpc-url flag.
// If non-empty, it is used as-is (after ensuring the grpcs:// scheme).
Explicit string
// ListenAddr is the address the gRPC server is bound to (e.g. "127.0.0.1:8443").
// Used as fallback when neither Explicit nor request headers are available.
ListenAddr string
// TrustedProxies lists the peers whose X-Forwarded-* headers are believed.
TrustedProxies []netip.Prefix
}
PublicURLConfig holds the inputs for resolving the gRPC public URL.
type ResourceHandler ¶
type ResourceHandler interface {
HandleAgentEvent(ctx context.Context, agentID string, ev *agentpb.ResourceSample, meta EventMeta) error
}
ResourceHandler processes a resource sample from a remote agent.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server wraps the gRPC server lifecycle for the Ingest service.
func (*Server) Start ¶
Start binds to listen and registers the Ingest service, then serves in the background. It returns as soon as the bind succeeds or fails, so call it synchronously and check the error; the server then runs and stops on its own as ctx allows. If tlsCfg is nil the server listens in h2c (plaintext) — only safe behind a trusted reverse proxy (MAINTENANT_GRPC_TLS_INSECURE=true).
func (*Server) StartTokenGC ¶
StartTokenGC launches a background goroutine that purges unconsumed enrollment tokens older than 7 days. It ticks every hour until ctx is cancelled.
type Sessions ¶
type Sessions struct {
// contains filtered or unexported fields
}
Sessions tracks all currently connected agent streams.
func NewSessions ¶
func NewSessions(logger *slog.Logger, broadcaster EventBroadcaster) *Sessions
NewSessions creates an empty Sessions registry. broadcaster may be nil (SSE events will not be emitted).
func (*Sessions) Close ¶
Close removes and cancels whatever active stream currently exists for agentID, regardless of which one opened it. For administrative teardown (revoke, delete) where any live session must go.
func (*Sessions) CloseStream ¶ added in v1.5.0
CloseStream removes and cancels the active stream for agentID only if it is still the one identified by tok. A stale tok (superseded by a reconnect) or a zero Token is a no-op, so a handler unwinding after being replaced can never tear down the session that replaced it.
func (*Sessions) DeliverResult ¶ added in v1.3.8
func (s *Sessions) DeliverResult(agentID string, res *agentpb.CommandResult)
DeliverResult routes a CommandResult from agentID to whoever awaits it.
func (*Sessions) EventsPerSecond5m ¶
EventsPerSecond5m returns the average events/s over the last 5 minutes.
func (*Sessions) EventsSeen ¶
EventsSeen returns the events_seen counter for agentID (0 if not connected).
func (*Sessions) FetchLogs ¶ added in v1.3.8
func (s *Sessions) FetchLogs(ctx context.Context, agentID, externalID string, lines int, timestamps bool) ([]string, error)
FetchLogs performs a one-shot log tail on agentID, collecting the agent's chunks into a single slice. Shared by the REST and MCP read paths.
func (*Sessions) HasCapability ¶ added in v1.3.8
HasCapability reports whether the agent's live stream advertised capability.
func (*Sessions) IncrEvents ¶
IncrEvents increments the events_seen counter for agentID and the ring buffer.
func (*Sessions) IsConnected ¶
IsConnected reports whether agentID currently has an active stream.
func (*Sessions) ListConnected ¶
ListConnected returns all currently connected agent IDs.
func (*Sessions) Open ¶
func (s *Sessions) Open(agentID string, cancel context.CancelCauseFunc, addr string, caps []string, send chan *agentpb.ServerMessage) Token
Open registers an active stream for agentID and cancels any pre-existing one. caps are the command families the agent advertised; send is the queue its Push goroutine drains to write to the stream (both may be nil for a telemetry-only stream, in which case no command can be issued to this agent). The returned Token identifies this stream for CloseStream.
func (*Sessions) RecordSpoolStatus ¶ added in v1.6.0
func (s *Sessions) RecordSpoolStatus(agentID string, st *agentpb.SpoolStatus)
RecordSpoolStatus stores what agentID declared about its spool.
func (*Sessions) SendCommand ¶ added in v1.3.8
func (s *Sessions) SendCommand(ctx context.Context, agentID, capability string, cmd *agentpb.AgentCommand) (<-chan *agentpb.CommandResult, func(), error)
SendCommand issues cmd to agentID and returns a channel carrying its replies, closed once the terminal result arrives or the stream dies. The returned release func MUST be called by the caller (defer): it drops the pending entry and, when the command may still be running, tells the agent to stop.
func (*Sessions) SetLifecycleAlertHook ¶
SetLifecycleAlertHook registers a callback fired on connect/disconnect transitions. Must be called once at wiring time, before any agent connects.
func (*Sessions) SpoolStatus ¶ added in v1.6.0
func (s *Sessions) SpoolStatus(agentID string) *SpoolState
SpoolStatus returns what agentID last declared, or nil when it is not connected or has never reported (an agent older than the spool).
func (*Sessions) StartRingAdvancer ¶
StartRingAdvancer runs the ring buffer advance tick every 5s until ctx is done.
func (*Sessions) StartStaleWatcher ¶
func (s *Sessions) StartStaleWatcher(ctx context.Context, interval, threshold, grace time.Duration, staleAgents StaleAgentsFn)
StartStaleWatcher emits agent.disconnected SSE for agents that have not been seen within threshold but still appear in the sessions map (dead stream), and reports the outage of stale agents that have no session at all.
That second half is what makes an absent agent visible across a restart: the connect/disconnect hook only fires on a transition, so an agent that went down while the server was stopped (or during the shutdown storm, where alerts are suppressed on purpose) would otherwise stay silently disconnected forever. grace holds that reporting back after startup, giving healthy agents time to reconnect; each outage is announced once, until the agent is back. Runs until ctx is done.
type SpoolState ¶ added in v1.6.0
type SpoolState struct {
Queued int64
Draining bool
DroppedSinceConnect int64
ReportedAt time.Time
}
SpoolState is what an agent last said about its outbound queue.
type StaleAgentsFn ¶
StaleAgentsFn returns agent IDs whose last_seen_at is older than threshold. Passed to StartStaleWatcher so sessions doesn't import the sqlite package.
type SwarmTopologyHandler ¶
type SwarmTopologyHandler interface {
HandleAgentEvent(ctx context.Context, agentID string, ev *agentpb.SwarmTopology) error
}
SwarmTopologyHandler processes a full swarm topology snapshot from an agent.