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
- Variables
- func CheckHostname(s string) (string, error)
- func CheckPattern(s string) (string, error)
- func Matches(pattern, name string) bool
- func MatchesAny(patterns []string, name string) bool
- type Agent
- type Caller
- type Checker
- type Deployment
- type DeploymentReport
- type DesiredDeployment
- type DesiredState
- type EnrollInput
- type EnrollOutput
- type Enrollment
- type JoinToken
- type Name
- type ReportInput
- type Service
- func (s *Service) AddName(ctx context.Context, who keys.Who, id, pattern string) error
- func (s *Service) Approve(ctx context.Context, who keys.Who, enrollmentID string) error
- func (s *Service) ClientCAs(ctx context.Context, now time.Time) (*x509.CertPool, int, error)
- func (s *Service) CloseWatches()
- func (s *Service) CreateJoinToken(ctx context.Context, who keys.Who, label string, patterns []string, ...) (string, *JoinToken, error)
- func (s *Service) DeleteJoinToken(ctx context.Context, who keys.Who, id string) error
- func (s *Service) Deny(ctx context.Context, who keys.Who, enrollmentID string) error
- func (s *Service) Enroll(ctx context.Context, in EnrollInput) (*EnrollOutput, error)
- func (s *Service) EnrollStatus(ctx context.Context, in StatusInput) (*Status, error)
- func (s *Service) GetAgent(ctx context.Context, who keys.Who, id string) (*Agent, error)
- func (s *Service) Handler() http.Handler
- func (s *Service) Identify(ctx context.Context, leaf *x509.Certificate, sourceIP string) (*Caller, error)
- func (s *Service) ListAgents(ctx context.Context, who keys.Who) ([]Agent, error)
- func (s *Service) ListEnrollments(ctx context.Context, who keys.Who, openOnly bool) ([]Enrollment, error)
- func (s *Service) ListJoinTokens(ctx context.Context, who keys.Who) ([]JoinToken, error)
- func (s *Service) RegisterACMEAccount(ctx context.Context, c *Caller, thumbprint string) error
- func (s *Service) RemoveName(ctx context.Context, who keys.Who, id, pattern string) error
- func (s *Service) RenewCertificate(ctx context.Context, c *Caller, csrDER []byte) (der []byte, chain [][]byte, err error)
- func (s *Service) Report(ctx context.Context, caller *Caller, in ReportInput) error
- func (s *Service) RequestRenewal(ctx context.Context, who keys.Who, id, name string, ...) error
- func (s *Service) Revoke(ctx context.Context, who keys.Who, id string) error
- func (s *Service) SetDisabled(ctx context.Context, who keys.Who, id string, disabled bool) error
- func (s *Service) SetPredecessorPolicy(ctx context.Context, who keys.Who, id, deployment string, ...) error
- func (s *Service) WatchDesiredState(ctx context.Context, c *Caller, known int64) (*DesiredState, error)
- func (s *Service) WithdrawRenewal(ctx context.Context, who keys.Who, id, name string) error
- type Status
- type StatusInput
Constants ¶
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.
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).
const ( StatusActive = "active" StatusDisabled = "disabled" StatusRevoked = "revoked" )
Agent states.
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.
const ( MethodStandalone = "standalone" MethodWebroot = "webroot" )
Check methods: how the agent answers the http-01 check.
const ( // MaxDeployments bounds the configured inventory per agent. MaxDeployments = 100 // MaxReportDeployments also admits one removed result to drain per report. MaxReportDeployments = MaxDeployments + 1 )
const ActionPredecessor = "agent.predecessor"
const MaxWatches = 1024
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 ¶
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.
var ( ErrWatchClosed = errors.New("the agent service is shutting down") ErrWatchReplaced = errors.New("a newer watch replaced this agent's watch") )
Functions ¶
func CheckHostname ¶
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 ¶
CheckPattern checks an entry of a name list or of a join token: an exact name or "*.suffix" – every name below suffix.
func Matches ¶
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 ¶
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.
type Checker ¶
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 DesiredDeployment ¶
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).
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) ClientCAs ¶
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 ¶
DeleteJoinToken withdraws an unused token; a used one stays as a record.
func (*Service) Deny ¶
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 ¶
EnrollStatus reports an enrollment; with CheckReady it first runs the check (once) and decides. An admitted agent gets its certificate here.
func (*Service) 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 ¶
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 ¶
ListJoinTokens returns every token, newest first (without secrets).
func (*Service) RegisterACMEAccount ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
StatusInput is a poll of EnrollStatus.