agents

package
v0.3.9 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: Apache-2.0 Imports: 39 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 or pass made (shown once)
	ActionTokenDelete = "agent.token.delete" // an unused join token withdrawn
	ActionTokenRevoke = "agent.token.revoke" // #nosec G101 -- an action name (ends a join token or pass at once), not a credential
)

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 (
	TokenPrefix = "mjt_"
	PassPrefix  = "mjp_"
)

Prefixes of the secrets, so secret scanners and people can tell one when it ends up somewhere it shouldn't, and tell a pass from a token.

View Source
const (
	KindToken = "token"
	KindPass  = "pass"
)

Kinds of join token (join_tokens.kind): a plain token admits one host, an enrollment pass admits up to MaxPassUses hosts (docs/PHASE3.md section 11.2).

View Source
const (
	MinPassUses     = 2
	MaxPassUses     = 25
	DefaultPassUses = 10
	MaxPassPatterns = 5
	MinPassTTL      = 5 * time.Minute
	MaxPassTTL      = 7 * 24 * time.Hour
	DefaultPassTTL  = 24 * time.Hour
)

Limits of an enrollment pass (docs/PHASE3.md section 11.2). They are constants, not configuration: a leaked pass admits at most MaxPassUses hosts matching at most MaxPassPatterns patterns until it expires, and a larger fleet takes two passes.

View Source
const ActionPredecessor = "agent.predecessor"
View Source
const MaxWatches = 1024
View Source
const PackagesTTL = time.Minute

PackagesTTL: the packages of a release are immutable, but hashing six of them is not free and any viewer, API tokens included, may ask.

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 CheckDNSName added in v0.3.4

func CheckDNSName(h string) error

CheckDNSName checks a host name that is already lower case and has no trailing dot, by the rules of CheckHostname: a fully qualified DNS name, letters, digits and hyphens, no wildcard, no IP address. The TLS scan (internal/tlsscan) shares it so that both accept the same names.

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 InstallFacts added in v0.3.5

type InstallFacts struct {
	ServerURL     string
	ACMEDirectory string // "" = ACME is off
	Version       string // the agent release that matches this server
	Root          Trust
	Packages      []Package
}

InstallFacts is everything a script needs to install the agent on a host without a browser: the independent values of the guided installation.

type InstallSource added in v0.3.5

type InstallSource struct {
	Dist      *agentdist.Store
	BaseURL   string // agent.base_url
	TLSIssuer string // agent.tls_issuer
	// ACMEDirectory returns the directory URL of the ACME server, "" while ACME is off.
	ACMEDirectory func() string
	Clock         clock.Clock
	// contains filtered or unexported fields
}

InstallSource holds what the install facts are made of: the server's agent address, the packages of this release, the agent TLS issuer and the ACME directory. Everything in it is already visible to a viewer (the packages and the root are served without a login); the facts only gather it.

func (*InstallSource) Facts added in v0.3.5

func (s *InstallSource) Facts(ctx context.Context, engine *ca.Engine, who keys.Who) (*InstallFacts, error)

Facts gathers the install facts. It fails with ErrNotFound while there is no agent TLS root or no package directory for this release.

type JoinToken

type JoinToken struct {
	ID        string
	Label     string
	Kind      string // KindToken | KindPass
	Patterns  []string
	MaxUses   int
	UseCount  int
	ExpiresAt time.Time
	UsedAt    time.Time // the last use was taken; zero = uses left
	UsedBy    string    // the enrollment of the last use
	RevokedAt time.Time
	RevokedBy string
	CreatedBy string
	CreatedAt time.Time
	// Hosts are the enrollments a pass admitted or queued, oldest first
	// (ListJoinTokens fills them for passes only).
	Hosts []PassHost
}

JoinToken is a join token or enrollment pass without its secret (security.md "Agent trust": short-lived, scoped to name patterns, stored hashed; a plain token is single-use, a pass has a bounded number of uses).

func (JoinToken) State added in v0.3.5

func (t JoinToken) State(now time.Time) TokenState

State reports the state of the token at now.

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 NewToken added in v0.3.5

type NewToken struct {
	Kind     string // "" = KindToken
	Label    string
	Patterns []string
	TTL      time.Duration // 0 = the default of the kind
	MaxUses  int           // pass only; 0 = DefaultPassUses
}

NewToken is a join token or pass to create.

type Package added in v0.3.5

type Package struct {
	Name, Format, Arch string // Format "checksums" for checksums.txt
	Size               int64
	SHA256             string // hex
	URL                string
}

Package is one file of the agent distribution.

type PassHost added in v0.3.5

type PassHost struct {
	Enrollment string
	Hostname   string
	State      string // the enrollment state
	AgentID    string // set once admitted
	At         time.Time
}

PassHost is one use of a pass: the enrollment it started and where that stands now.

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
	// Install serves the install facts (InstallFacts); nil = no agent distribution.
	Install *InstallSource
	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) CreateEnrollmentPass added in v0.3.5

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

CreateEnrollmentPass makes a pass that admits up to maxUses hosts whose name matches one of patterns, valid for ttl (0 = DefaultPassTTL). It needs the same fresh second factor as a join token and, unlike one, only names that a domain covers.

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 single-use 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) CreateToken added in v0.3.5

func (s *Service) CreateToken(ctx context.Context, who keys.Who, in NewToken) (string, *JoinToken, error)

CreateToken creates a join token or an enrollment pass; the secret is returned once.

func (*Service) DeleteJoinToken

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

DeleteJoinToken withdraws a token or pass nobody has used; 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) InstallFacts added in v0.3.5

func (s *Service) InstallFacts(ctx context.Context, who keys.Who) (*InstallFacts, error)

InstallFacts returns the install facts for anyone who may read the PKI (viewer and up, API tokens included): they hold no secret, only what the agent distribution and the public root already show.

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 and pass, newest first, without secrets. A pass comes with the enrollments it admitted or queued.

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) RevokeJoinToken added in v0.3.5

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

RevokeJoinToken ends the use of a token or pass at once. It does not touch the enrollments and agents the token already produced: they stay, listed on the pass, for an admin to refuse or disable (docs/PHASE3.md section 11.2).

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.

type TokenState added in v0.3.5

type TokenState string

TokenState is where a join token or pass stands at a given time.

const (
	TokenActive  TokenState = "active"
	TokenUsedUp  TokenState = "used_up"
	TokenExpired TokenState = "expired"
	TokenRevoked TokenState = "revoked"
)

Token states. A revoked token reports revoked whatever else is true, a used up one used_up before it reports expired.

type Trust added in v0.3.5

type Trust struct {
	SHA256, SHA1, URL string
}

Trust is the root the agents are told to trust before anything else: its DER fingerprints and the plain-HTTP URL of its PEM. Only SHA-256 authorizes an installation; SHA-1 is for comparing with the Windows certificate dialog.

func TLSTrust added in v0.3.5

func TLSTrust(ctx context.Context, engine *ca.Engine, who keys.Who, issuerName string) (Trust, error)

TLSTrust finds the root of the agent TLS issuer (agent.tls_issuer) among the CAs who may read. The guided installation (web) and the install facts (API) share it, so both name the same root.

Jump to

Keyboard shortcuts

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