agents

package
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package agents is the server side of myownpki-agent (docs/PHASE3.md): join tokens, enrollment with proof of control (ADR-18), the agents' 7-day certificates from the agent CA, their names, disable and revoke.

Everything an agent sends is attacker input. Before it is admitted, an agent can only create a pending enrollment – limited in number, expiring, never an agent – and nothing it reports is ever used as a path or a command here.

Index

Constants

View Source
const (
	ActionEnroll      = "agent.enroll"       // an enrollment arrived (anonymous, with the source address)
	ActionAdmit       = "agent.admit"        // admitted by the mode or an admin
	ActionDeny        = "agent.deny"         // refused by an admin
	ActionDisable     = "agent.disable"      // and agent.enable
	ActionEnable      = "agent.enable"       //
	ActionRevoke      = "agent.revoke"       // its certificates on the CRL, cut off
	ActionNames       = "agent.names"        // a name added or removed
	ActionTokenCreate = "agent.token.create" // a join token made (shown once)
	ActionTokenDelete = "agent.token.delete" // an unused join token withdrawn
)

Audit actions.

View Source
const (
	// CertificateDays is the validity of an agent certificate; the agent
	// renews it at 2/3.
	CertificateDays = 7
	// MaxOpenWithoutToken: pending enrollments without a valid join token
	// per tenant. An enrollment with a valid token is never refused by it.
	MaxOpenWithoutToken = 100
	// ShortExpiry: pending without a valid token and without a passed check.
	ShortExpiry = 24 * time.Hour
	// LongExpiry: every other pending enrollment.
	LongExpiry = 7 * 24 * time.Hour
	// MaxTokenTTL and DefaultTokenTTL bound a join token's life.
	MaxTokenTTL     = 30 * 24 * time.Hour
	DefaultTokenTTL = 24 * time.Hour
)

Limits (docs/PHASE3.md §6.1).

View Source
const (
	StatusActive   = "active"
	StatusDisabled = "disabled"
	StatusRevoked  = "revoked"
)

Agent states.

View Source
const (
	StateWaitingForCheck  = "waiting_for_check"
	StatePending          = "pending_approval"
	StateAdmitted         = "admitted"
	StateDenied           = "denied"
	StateExpired          = "expired"
	StateWaitingForUnseal = "waiting_for_unseal"
)

Enrollment states (agent_enrollments.state); StateWaitingForUnseal is only reported, never stored: the enrollment is admitted, the certificate follows the unseal.

View Source
const (
	MethodStandalone = "standalone"
	MethodWebroot    = "webroot"
)

Check methods: how the agent answers the http-01 check.

View Source
const (
	// MaxDeployments bounds the configured inventory per agent.
	MaxDeployments = 100
	// MaxReportDeployments also admits one removed result to drain per report.
	MaxReportDeployments = MaxDeployments + 1
)
View Source
const ActionPredecessor = "agent.predecessor"
View Source
const MaxWatches = 1024
View Source
const TokenPrefix = "mjt_"

TokenPrefix starts every join token, so secret scanners and people can tell one when it ends up somewhere it shouldn't.

Variables

View Source
var (
	ErrNotFound     = ca.ErrNotFound
	ErrInvalid      = errors.New("invalid request")
	ErrQueueFull    = errors.New("too many enrollments wait for approval – ask an admin for a join token")
	ErrRateLimited  = errors.New("too many requests from this address – try again later")
	ErrUnknownAgent = errors.New("this certificate belongs to no active agent")
	ErrConflict     = errors.New("conflict")
)

Errors the transport maps to its codes.

View Source
var (
	ErrWatchClosed   = errors.New("the agent service is shutting down")
	ErrWatchReplaced = errors.New("a newer watch replaced this agent's watch")
)

Functions

func CheckHostname

func CheckHostname(s string) (string, error)

CheckHostname checks a host name an agent claims: a fully qualified DNS name (at least two labels), lower case, letters, digits and hyphens, no wildcard, no IP address. It returns the name in lower case.

func CheckPattern

func CheckPattern(s string) (string, error)

CheckPattern checks an entry of a name list or of a join token: an exact name or "*.suffix" – every name below suffix.

func Matches

func Matches(pattern, name string) bool

Matches reports whether name is covered by pattern: the same name, or below the suffix of "*.suffix". A wildcard name ("*.x") is only covered by the identical pattern – an agent never gets a wildcard by accident.

func MatchesAny

func MatchesAny(patterns []string, name string) bool

MatchesAny reports whether one of the patterns covers name.

Types

type Agent

type Agent struct {
	ID              string
	Hostname        string
	Status          string // active | disabled | revoked
	EnrollmentMode  string
	Proof           string // JSON
	CertificateID   string
	CertSerial      string
	OS, Arch        string
	Version         string
	PackageFormat   string
	LastSeenAt      time.Time
	CreatedAt       time.Time
	ApprovedAt      time.Time
	ApprovedBy      string
	Predecessor     *predecessor.Policy
	EffectivePolicy predecessor.Policy
	Names           []Name
	Deployments     []Deployment
}

Agent is an admitted agent.

func (Agent) Silent

func (a Agent) Silent(now time.Time) bool

Silent reports three missed five-minute reports, including a host that has never reported since admission.

type Caller

type Caller struct {
	Agent pkidb.Agent
	Who   keys.Who
}

Caller is an authenticated agent request.

type Checker

type Checker interface {
	Check(ctx context.Context, host, token, want, policy string) error
}

Checker proves control of host: it fetches http://host/.well-known/acme-challenge/<token> and compares the answer with want, under the address policy of the domain (acme.HTTP01Checker).

type Deployment

type Deployment struct {
	Name, Profile                                                   string
	Names                                                           []string
	Challenge, State, Serial, CertificateID, Fingerprint, LastError string
	NotAfter, DeployedAt, LastReportAt, RenewRequestedAt            time.Time
	RenewRequestedBy                                                string
	RenewalVersion                                                  int64
	Removed                                                         bool
	Predecessor                                                     *predecessor.Policy
	EffectivePolicy                                                 predecessor.Policy
	RevocationPlannedAt                                             *time.Time
}

Deployment is the current reported state and an optional renewal request.

type DeploymentReport

type DeploymentReport struct {
	Name, Profile                                                          string
	Names                                                                  []string
	Challenge, State, Serial, NotAfter, Fingerprint, DeployedAt, LastError string
	WrittenFingerprint                                                     string
	CheckConfigured                                                        bool
	HandledRenewalVersion                                                  int64
}

type DesiredDeployment

type DesiredDeployment struct {
	Name, RequestedAt string
	RenewalVersion    int64
}

type DesiredState

type DesiredState struct {
	Version      int64
	Changed      bool
	AllowedNames []string
	Deployments  []DesiredDeployment
}

type EnrollInput

type EnrollInput struct {
	Hostname, Token, Method, OS, Arch, Version, SourceIP string
	CSRDER                                               []byte
}

EnrollInput is an enrollment as the transport received it.

type EnrollOutput

type EnrollOutput struct {
	ID, Handle, CheckToken string
}

EnrollOutput is what the agent needs for the check and the polls.

type Enrollment

type Enrollment struct {
	ID, Hostname, State         string
	AgentID                     string // set when admitted, and for a known agent
	CheckMethod, CheckState     string
	CheckError                  string
	TokenValid                  bool
	TokenProblem                string
	KnownAgent                  bool
	OS, Arch, Version, SourceIP string
	CreatedAt, ExpiresAt        time.Time
	DecidedAt                   time.Time // zero = open
	DecidedBy                   string
}

Enrollment is an enrollment as the approval queue shows it.

type JoinToken

type JoinToken struct {
	ID        string
	Label     string
	Patterns  []string
	ExpiresAt time.Time
	UsedAt    time.Time // zero = unused
	UsedBy    string    // the enrollment
	CreatedBy string
	CreatedAt time.Time
}

JoinToken is a join token without its secret (security.md "Agent trust": single-use, short-lived, scoped to name patterns, stored hashed).

func (JoinToken) Usable

func (t JoinToken) Usable(now time.Time) bool

Usable reports whether the token can still admit an agent at now.

type Name

type Name struct {
	Pattern string
	Source  string // hostname | token | admin
	AddedBy string
	AddedAt time.Time
}

Name is one entry of an agent's name list.

type ReportInput

type ReportInput struct {
	Version, OS, Arch string
	PackageFormat     string
	Deployments       []DeploymentReport
}

ReportInput is display data, with identity supplied only by mTLS.

type Service

type Service struct {
	PKI    *sql.DB // pki.db writer
	Tenant string  // the tenant these agents belong to
	Engine *ca.Engine
	// CA is agent.ca: the CA that signs the agents' certificates.
	CA string
	// Mode returns the tenant's enrollment mode (tenant.Store).
	Mode func(ctx context.Context) (string, error)
	// AgentPredecessor returns the tenant default for agent deployments.
	AgentPredecessor func(ctx context.Context) (predecessor.Policy, error)
	Checker          Checker
	Clock            clock.Clock
	Log              *slog.Logger
	Wake             func()
	Rand             io.Reader                                  // nil = crypto/rand
	Sleep            func(context.Context, time.Duration) error // nil = clock.Sleep
	// contains filtered or unexported fields
}

Service is the agent service of one tenant.

func (*Service) AddName

func (s *Service) AddName(ctx context.Context, who keys.Who, id, pattern string) error

AddName lets an agent order for one more name or pattern.

func (*Service) Approve

func (s *Service) Approve(ctx context.Context, who keys.Who, enrollmentID string) error

Approve admits an enrollment from the queue (like `salt-key -A`).

func (*Service) ClientCAs

func (s *Service) ClientCAs(ctx context.Context, now time.Time) (*x509.CertPool, int, error)

ClientCAs are the certificates of the agent CA – every generation that is valid at now: the only anchors of the listener's client authentication.

func (*Service) CloseWatches

func (s *Service) CloseWatches()

CloseWatches is called before HTTP shutdown, so long polls release the request budget immediately. A service closed this way is never reused.

func (*Service) CreateJoinToken

func (s *Service) CreateJoinToken(ctx context.Context, who keys.Who, label string, patterns []string, ttl time.Duration) (string, *JoinToken, error)

CreateJoinToken makes a token for hosts whose name matches one of patterns, valid for ttl (0 = DefaultTokenTTL). The secret is returned once; only its SHA-256 is stored, and the audit entry never holds it.

func (*Service) DeleteJoinToken

func (s *Service) DeleteJoinToken(ctx context.Context, who keys.Who, id string) error

DeleteJoinToken withdraws an unused token; a used one stays as a record.

func (*Service) Deny

func (s *Service) Deny(ctx context.Context, who keys.Who, enrollmentID string) error

Deny refuses an enrollment from the queue; the agent learns it at its next poll.

func (*Service) Enroll

func (s *Service) Enroll(ctx context.Context, in EnrollInput) (*EnrollOutput, error)

Enroll stores a pending enrollment. It needs no certificate: nothing but an enrollment that waits, limited in number and expiring, comes of it.

func (*Service) EnrollStatus

func (s *Service) EnrollStatus(ctx context.Context, in StatusInput) (*Status, error)

EnrollStatus reports an enrollment; with CheckReady it first runs the check (once) and decides. An admitted agent gets its certificate here.

func (*Service) GetAgent

func (s *Service) GetAgent(ctx context.Context, who keys.Who, id string) (*Agent, error)

GetAgent returns one agent with its names.

func (*Service) Handler

func (s *Service) Handler() http.Handler

Handler returns the Connect-RPC handler of the agent listener: only the AgentService path, every other path 404. The peer's address and certificate come from the TLS connection – never from a header.

func (*Service) Identify

func (s *Service) Identify(ctx context.Context, leaf *x509.Certificate, sourceIP string) (*Caller, error)

Identify maps the client certificate of a request to its agent – per request, not per handshake, so a revocation or a "disable" takes effect at the next call even on a resumed TLS session. Only the agent's current certificate and the one before it count (renewal overlap), and only as exactly the stored certificate.

func (*Service) ListAgents

func (s *Service) ListAgents(ctx context.Context, who keys.Who) ([]Agent, error)

ListAgents returns every agent, by hostname.

func (*Service) ListEnrollments

func (s *Service) ListEnrollments(ctx context.Context, who keys.Who, openOnly bool) ([]Enrollment, error)

ListEnrollments returns the open enrollments (the approval queue), or the last 200 of any state.

func (*Service) ListJoinTokens

func (s *Service) ListJoinTokens(ctx context.Context, who keys.Who) ([]JoinToken, error)

ListJoinTokens returns every token, newest first (without secrets).

func (*Service) RegisterACMEAccount

func (s *Service) RegisterACMEAccount(ctx context.Context, c *Caller, thumbprint string) error

RegisterACMEAccount reserves the account key for an authenticated agent. The ACME request itself later proves possession of that key and binds the account. A reservation never transfers another agent's account.

func (*Service) RemoveName

func (s *Service) RemoveName(ctx context.Context, who keys.Who, id, pattern string) error

RemoveName takes a name or pattern from an agent's list.

func (*Service) RenewCertificate

func (s *Service) RenewCertificate(ctx context.Context, c *Caller, csrDER []byte) (der []byte, chain [][]byte, err error)

RenewCertificate gives an agent a new 7-day certificate for a new key (ADR-11: the same key twice is refused). The current certificate becomes the previous one and stays accepted until it expires.

func (*Service) Report

func (s *Service) Report(ctx context.Context, caller *Caller, in ReportInput) error

Report replaces only this agent's inventory. No agent ID, path or hook can be provided by the sender. Heartbeats do not fill the audit log.

func (*Service) RequestRenewal

func (s *Service) RequestRenewal(ctx context.Context, who keys.Who, id, name string, overrides ...predecessor.Policy) error

RequestRenewal records an operator's request and snapshots its predecessor policy, never executable input. An omitted override uses the effective policy.

func (*Service) Revoke

func (s *Service) Revoke(ctx context.Context, who keys.Who, id string) error

Revoke cuts an agent off for good: the status first (it takes effect at the agent's next call), then its agent certificates go on the agent CA's CRL. The certificates it deployed stay valid – revoking them would take services down; that is a separate step (owner 2026-10-24).

func (*Service) SetDisabled

func (s *Service) SetDisabled(ctx context.Context, who keys.Who, id string, disabled bool) error

SetDisabled disables an active agent or enables a disabled one. A disabled agent is cut off at its next call; its certificates stay valid.

func (*Service) SetPredecessorPolicy added in v0.3.1

func (s *Service) SetPredecessorPolicy(ctx context.Context, who keys.Who, id, deployment string, policy *predecessor.Policy) error

SetPredecessorPolicy sets or clears an agent or deployment override. An empty deployment selects the agent-wide override.

func (*Service) WatchDesiredState

func (s *Service) WatchDesiredState(ctx context.Context, c *Caller, known int64) (*DesiredState, error)

WatchDesiredState subscribes before reading the database: a commit between the read and the wait can never be lost.

func (*Service) WithdrawRenewal

func (s *Service) WithdrawRenewal(ctx context.Context, who keys.Who, id, name string) error

WithdrawRenewal removes an open request and wakes a waiting agent. A result already in flight is harmless: its version no longer matches an open row.

type Status

type Status struct {
	State       string
	Message     string
	AgentID     string
	TokenValid  bool
	CheckPassed bool
	CertDER     []byte
	Chain       [][]byte // issuing CA first, without the root
}

Status is where an enrollment stands, for the agent.

type StatusInput

type StatusInput struct {
	ID, Handle, SourceIP string
	CheckReady           bool
}

StatusInput is a poll of EnrollStatus.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL