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) 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 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
LastSeenAt time.Time
CreatedAt time.Time
ApprovedAt time.Time
ApprovedBy string
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
}
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
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)
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 ¶
RequestRenewal records an operator's request, never executable input.
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) 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.