agentpki

package
v4.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Overview

Package agentpki issues the mTLS client certificates of AnixOps Agents (node-ops-service.md, section 5.3).

A certificate names one node, spiffe://anixops/<cluster>/agent/proxy-<id> or .../agent/forward-<id>, and is signed by the module PKI's CA (internal/modulepki) with its current/next rotation. Agents get their first certificate with a bootstrap credential: the node credential they already have (a proxy node's API key or a forward node's token), or a one-time anixagt_ enrollment credential bound to a node. They renew with the current certificate at two thirds of its lifetime.

Every certificate is recorded in v4_kernel_agent_certificate and every way in in v4_kernel_agent_enrollment. Revoking, replacing or disabling a node's credentials, or deleting the node, revokes its certificates (RevokeNode), and the agent listener refuses revoked serials through a cache of at most RevocationCacheTTL.

The package also runs the forward link CA (link.go, owner decision H28): a separate root that issues nodes whose Agent negotiated forward.v1 the link certificates their forward engines present to each other, recorded in v4_kernel_forward_link_certificate and revoked with the node's Agent credentials.

Index

Constants

View Source
const (
	// DefaultCertificateLifetime is the lifetime of agent certificates
	// (decision D9): nodes can be offline longer than modules.
	DefaultCertificateLifetime = 7 * 24 * time.Hour
	// MaxEnrollmentCredentialTTL bounds a one-time enrollment credential.
	MaxEnrollmentCredentialTTL = 7 * 24 * time.Hour
	// DefaultEnrollmentCredentialTTL is used when a request names none.
	DefaultEnrollmentCredentialTTL = 24 * time.Hour
	// RevocationCacheTTL is the longest the listener trusts a cached
	// revocation answer.
	RevocationCacheTTL = 30 * time.Second
)
View Source
const (
	AuditActionTokenIssue = "agent_enrollment_token_issue"
	AuditActionEnroll     = "agent_enroll"
)

Audit actions written to the operation log (v2_operation_log).

View Source
const (
	RevokeReasonNodeDisabled        = "node_disabled"
	RevokeReasonNodeDeleted         = "node_deleted"
	RevokeReasonCredentialsReplaced = "credentials_replaced"
	RevokeReasonCredentialsRevoked  = "credentials_revoked"
	RevokeReasonCredentialsRotated  = "credentials_rotated"
)

Revocation reasons recorded with revoked enrollments and certificates.

View Source
const AuditActionCredentialsRotate = "agent_credentials_rotate" // #nosec G101 -- names an operation log action, not a credential.

AuditActionCredentialsRotate is the operation log action of a credential rotation (RotateCredentials). Its entry carries the node, the reason and what was revoked; never a credential.

View Source
const AuditActionLinkIssue = "forward_link_certificate_issue"

AuditActionLinkIssue is the operation log action of an issued link certificate.

View Source
const (
	// DefaultLinkCertificateLifetime is the lifetime of link certificates:
	// the Agent certificate's.
	DefaultLinkCertificateLifetime = DefaultCertificateLifetime
)
View Source
const MaxRotationReasonLength = 200

MaxRotationReasonLength bounds the free-text reason an administrator gives for a rotation.

Variables

View Source
var (
	// ErrDisabled means the kernel does not run the built-in CA, which signs
	// agent certificates. It needs module_runtime.ca_kek with pki builtin;
	// the module listener (module_runtime.enabled) need not run.
	ErrDisabled = errors.New("agent enrollment needs the built-in CA (module_runtime.ca_kek with module_runtime.pki: builtin)")
	// ErrExternalPKI is ErrDisabled with module_runtime.pki external: the
	// kernel holds no CA key, so it cannot issue agent certificates.
	ErrExternalPKI = fmt.Errorf("%w: module_runtime.pki is external, so the kernel holds no CA key to issue agent certificates", ErrDisabled)
	// ErrEnrollmentRejected is the single answer to any unusable bootstrap
	// credential, so credentials cannot be probed.
	ErrEnrollmentRejected = errors.New("agent enrollment rejected")
	// ErrCertificateRevoked means the certificate, its enrollment or its
	// node's credentials were revoked, or the node is gone or disabled.
	ErrCertificateRevoked = errors.New("agent certificate revoked")
	// ErrInvalidCertificate reports a client certificate that does not
	// authenticate an agent of this cluster: unparsable, not chaining to the
	// trust bundle, expired, or carrying another identity.
	ErrInvalidCertificate = errors.New("invalid agent client certificate")
	// ErrCertificateExpired is the ErrInvalidCertificate of an agent
	// certificate of this CA whose not_after has passed.
	ErrCertificateExpired = fmt.Errorf("%w: the certificate has expired", ErrInvalidCertificate)
	// ErrCertificateWrongCluster is the ErrInvalidCertificate of an agent
	// certificate of another cluster.
	ErrCertificateWrongCluster = fmt.Errorf("%w: the certificate belongs to another cluster", ErrInvalidCertificate)
	// ErrInvalidNode reports an enrollment credential request for a node
	// that does not exist or is disabled.
	ErrInvalidNode = errors.New("agent node not found or disabled")
)
View Source
var (
	// ErrLinkDisabled means the kernel does not run the forward link CA. It
	// needs the built-in CA's module_runtime.ca_kek with pki builtin; an
	// external PKI holds no key in the kernel, so it cannot issue link
	// certificates either.
	ErrLinkDisabled = errors.New("forward link certificates need the built-in CA (module_runtime.ca_kek with module_runtime.pki: builtin)")
	// ErrNoLinkAuthority means the cluster has no current link CA yet.
	ErrNoLinkAuthority = errors.New("the forward link PKI has no current CA")
	// ErrLinkNotNegotiated means the node's Agent did not negotiate
	// forward.v1 in its last Hello.
	ErrLinkNotNegotiated = errors.New("the node's Agent has not negotiated forward.v1")
	// ErrInvalidLinkRequest reports an unacceptable link certificate
	// request: malformed, signed with an unsupported key or with the Agent's
	// own key, or asking for a name other than the node's.
	ErrInvalidLinkRequest = errors.New("invalid link certificate request")
)
View Source
var ErrNodeDisabled = errors.New("agent node is disabled")

ErrNodeDisabled reports a rotation of a node an administrator disabled: disabling already revoked its Agent credentials, and a disabled node is not enrolled until it is enabled again.

Functions

func CheckNodeEnabled

func CheckNodeEnabled(ctx context.Context, db *gorm.DB, node agentcontrol.AgentNode) error

CheckNodeEnabled fails with ErrInvalidNode unless node exists and is not disabled.

func ForgetRevocations added in v4.2.0

func ForgetRevocations()

ForgetRevocations drops every cached revocation answer of this process. RevokeNode does it too, but inside the caller's transaction: a reader between it and the commit caches "not revoked". A caller that revoked in a transaction calls this after the commit, so open Agent streams see the revocation at their next heartbeat instead of after the cache lifetime.

func RevokeNode

func RevokeNode(ctx context.Context, db *gorm.DB, node agentcontrol.AgentNode, reason string) error

RevokeNode revokes every certificate and every enrollment (including unused enrollment credentials) of node, and its forward link certificates, in db, which may be a transaction. It is the hook of the kernel paths that revoke, replace or disable a node's credentials or delete the node. A database without the agent PKI tables has nothing to revoke.

Types

type ActiveCredentials added in v4.2.0

type ActiveCredentials struct {
	Certificates     int64 `json:"certificates"`
	Enrollments      int64 `json:"enrollments"`
	LinkCertificates int64 `json:"link_certificates"`
}

ActiveCredentials counts what a revocation of a node's Agent credentials would revoke: certificates, enrollments (including unused enrollment credentials) and forward link certificates that are not revoked yet.

func CountActive added in v4.2.0

func CountActive(ctx context.Context, db *gorm.DB, node agentcontrol.AgentNode) (ActiveCredentials, error)

CountActive counts node's unrevoked Agent credentials in db, which may be a transaction. A database without the agent PKI tables has none.

type Bootstrap

type Bootstrap struct {
	// Method is one of model.AgentEnrollmentMethod*.
	Method string
	// Node is the node the agent claims: required with a node API key or a
	// forward token; with an enrollment credential it must match the
	// credential's node when set.
	Node agentcontrol.AgentNode
	// Secret is the API key, the forward token or the enrollment
	// credential. It is never stored or logged.
	Secret string
}

Bootstrap is the credential an agent enrolls with.

type EnrollRequest

type EnrollRequest struct {
	Bootstrap    Bootstrap
	CSRDER       []byte
	AgentVersion string
	InstanceID   string
	// RemoteAddr is recorded in the audit entry.
	RemoteAddr string
}

EnrollRequest is an agent's request for its first certificate.

type Issued

type Issued struct {
	modulepki.SignedLeaf
	Identity     agentcontrol.AgentIdentity
	EnrollmentID string
}

Issued is a signed agent certificate.

type LinkAuthority added in v4.2.0

type LinkAuthority struct {
	// contains filtered or unexported fields
}

LinkAuthority is the forward link CA of one cluster.

func LinkAuthorityFromConfig added in v4.2.0

func LinkAuthorityFromConfig(cfg *config.Config, db *gorm.DB) (*LinkAuthority, error)

LinkAuthorityFromConfig returns the forward link CA of the built-in CA's configuration: module_runtime.ca_kek seals its keys and the module runtime's cluster names it. ErrLinkDisabled without the built-in CA or with an external PKI.

func NewLinkAuthority added in v4.2.0

func NewLinkAuthority(opts LinkAuthorityOptions) (*LinkAuthority, error)

NewLinkAuthority returns the forward link CA of opts.Cluster.

func (*LinkAuthority) CAs added in v4.2.0

CAs returns the cluster's link CAs, oldest first, without their keys' plaintext (SealedKey stays sealed and is not serialized).

func (*LinkAuthority) Cluster added in v4.2.0

func (a *LinkAuthority) Cluster() string

Cluster returns the cluster the link CA serves.

func (*LinkAuthority) Ensure added in v4.2.0

func (a *LinkAuthority) Ensure(ctx context.Context) error

Ensure creates the cluster's first link CA when it has none. Concurrent kernels call it under the bootstrap lock.

func (*LinkAuthority) Lifetime added in v4.2.0

func (a *LinkAuthority) Lifetime() time.Duration

Lifetime returns the lifetime of link certificates, which is also the rotation overlap.

func (*LinkAuthority) Maintain added in v4.2.0

func (a *LinkAuthority) Maintain(ctx context.Context) error

Maintain promotes a next link CA that has been in the trust bundle for one link certificate lifetime and retires the current one.

func (*LinkAuthority) Rotate added in v4.2.0

Rotate creates the next link CA. It joins the trust bundle at once and starts signing one link certificate lifetime later (Maintain), so every node that renewed meanwhile trusts it before any peer presents a certificate it signed. Rotating while a next CA exists returns it.

func (*LinkAuthority) TrustBundle added in v4.2.0

func (a *LinkAuthority) TrustBundle(ctx context.Context) ([]*x509.Certificate, error)

TrustBundle returns every link CA a node must trust to verify its peers' link certificates: the current and next CAs and retired CAs whose link certificates may still be valid.

type LinkAuthorityOptions added in v4.2.0

type LinkAuthorityOptions struct {
	DB      *gorm.DB
	Cluster string
	// KEK seals the CA keys: module_runtime.ca_kek (modulepki.ParseKEK).
	KEK []byte
	// Lifetime of link certificates; DefaultLinkCertificateLifetime when
	// zero.
	Lifetime time.Duration
	// Now defaults to time.Now.
	Now func() time.Time
}

LinkAuthorityOptions configures a LinkAuthority.

type LinkIssued added in v4.2.0

type LinkIssued struct {
	CertificateDER []byte
	// TrustBundleDER is the link trust bundle at issuance.
	TrustBundleDER [][]byte
	Identity       agentcontrol.AgentIdentity
	// DNSName is the certificate's only DNS name: the node's identity name.
	DNSName     string
	Serial      string
	NotAfter    time.Time
	RenewAfter  time.Time
	IssuerKeyID string
}

LinkIssued is a signed link certificate.

type Options

type Options struct {
	DB        *gorm.DB
	Authority *modulepki.Authority
	// Lifetime of issued certificates; DefaultCertificateLifetime when zero.
	Lifetime time.Duration
	// RevocationCacheTTL defaults to RevocationCacheTTL and may not exceed it.
	RevocationCacheTTL time.Duration
	// Now defaults to time.Now.
	Now func() time.Time
	// Link is the forward link CA (link.go); nil serves no link
	// certificates (ErrLinkDisabled).
	Link *LinkAuthority
}

Options configures a Service.

type RotateRequest added in v4.2.0

type RotateRequest struct {
	Node agentcontrol.AgentNode
	// TTL is the new enrollment credential's lifetime; as TokenRequest.
	TTL time.Duration
	// CreatedBy is the administrator's user id; Actor and IP describe the
	// caller in the audit entries.
	CreatedBy uint
	Actor     string
	IP        string
	// Reason is recorded in the audit entry (at most MaxRotationReasonLength
	// bytes, checked by the caller).
	Reason string
	// ReplaceNodeKey, when set, runs first in the rotation's transaction and
	// replaces the node's own credential (a proxy node's API key). It must
	// not revoke Agent credentials itself: the rotation does that, once, with
	// its own reason. Its failure rolls the whole rotation back.
	ReplaceNodeKey func(tx *gorm.DB) error
}

RotateRequest rotates a node's Agent credentials.

type Rotation added in v4.2.0

type Rotation struct {
	// Credential is the new one-time enrollment credential. It is shown
	// once: only its SHA-256 is stored.
	Credential string
	Enrollment model.AgentEnrollment
	// Revoked counts the Agent credentials the rotation revoked.
	Revoked       ActiveCredentials
	APIKeyRotated bool
}

Rotation is what RotateCredentials did.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the agent PKI of one cluster.

func FromConfig

func FromConfig(cfg *config.Config, db *gorm.DB) (*Service, error)

FromConfig returns the agent PKI of the built-in CA, or ErrDisabled (ErrExternalPKI with an external PKI). It does not need the module runtime: module_runtime.ca_kek alone configures the CA.

func New

func New(opts Options) (*Service, error)

New returns the agent PKI signing with opts.Authority.

func (*Service) Cluster

func (s *Service) Cluster() string

Cluster returns the cluster named in every agent SPIFFE ID.

func (*Service) CreateEnrollmentToken

func (s *Service) CreateEnrollmentToken(ctx context.Context, request TokenRequest) (string, model.AgentEnrollment, error)

CreateEnrollmentToken stores a one-time enrollment credential bound to a node and returns it. The credential is shown only here; the kernel keeps its SHA-256.

func (*Service) CreateEnrollmentTokenTx added in v4.2.0

func (s *Service) CreateEnrollmentTokenTx(tx *gorm.DB, request TokenRequest) (string, model.AgentEnrollment, error)

CreateEnrollmentTokenTx is CreateEnrollmentToken in the caller's transaction: the credential, its row and its audit entry commit with whatever else the caller changed (the credential rotation revokes the node's old credentials in the same transaction).

func (*Service) Enroll

func (s *Service) Enroll(ctx context.Context, request EnrollRequest) (Issued, error)

Enroll checks a bootstrap credential and issues the authenticated node's first certificate. The CSR's subject and SANs are ignored. A one-time credential is consumed atomically; a failed issuance leaves it unused.

func (*Service) IsRevoked

func (s *Service) IsRevoked(ctx context.Context, serial string, node agentcontrol.AgentNode) (bool, error)

IsRevoked reports whether the agent certificate serial of node may no longer authenticate. A serial without a record, or recorded for another node, counts as revoked: every agent certificate is recorded when it is issued, and records are pruned only after the certificate expired. Answers are cached for at most the revocation cache lifetime.

func (*Service) IssueLinkCertificate added in v4.2.0

func (s *Service) IssueLinkCertificate(ctx context.Context, peer *x509.Certificate, csrDER []byte, remoteAddr string) (LinkIssued, error)

IssueLinkCertificate issues the node of the verified Agent certificate peer (VerifyPeer) a link certificate for the key of csrDER. The Agent certificate's record must be unrevoked, the node enabled, and its Agent must have negotiated forward.v1 in its last Hello.

func (*Service) Lifetime

func (s *Service) Lifetime() time.Duration

Lifetime returns the lifetime of issued certificates.

func (s *Service) Link() *LinkAuthority

Link returns the forward link CA, nil when the agent PKI runs without it.

func (*Service) LinkTrustBundle added in v4.2.0

func (s *Service) LinkTrustBundle(ctx context.Context) ([]*x509.Certificate, error)

LinkTrustBundle returns the link trust bundle: ErrLinkDisabled without the link CA.

func (*Service) Prune

func (s *Service) Prune(ctx context.Context, cutoff time.Time) error

Prune deletes the records of certificates (Agent and, with the link CA, forward link certificates) that expired before cutoff and of enrollment credentials that expired unused before it.

func (*Service) Renew

func (s *Service) Renew(ctx context.Context, peer *x509.Certificate, csrDER []byte) (Issued, error)

Renew issues a new certificate to the holder of a valid agent certificate. peer must already be verified (VerifyPeer).

func (*Service) Roots

func (s *Service) Roots(ctx context.Context) (*x509.CertPool, error)

Roots returns TrustBundle as a pool, reloaded at most every minute; a failed reload keeps the last good pool.

func (*Service) RotateCredentials added in v4.2.0

func (s *Service) RotateCredentials(ctx context.Context, req RotateRequest) (Rotation, error)

RotateCredentials rotates a node's Agent credentials in one transaction: it optionally replaces the node's API key (req.ReplaceNodeKey), revokes every Agent certificate, enrollment (unused enrollment credentials included) and forward link certificate of the node (RevokeNode, reason credentials_rotated), and then issues a fresh one-time enrollment credential, the same code path as the enrollment token administration (CreateEnrollmentTokenTx). The revocation comes first because it also revokes unused enrollment credentials, and the new one must survive it.

It never dials the node: an offline node is rotated like any other, and its Agent finds out when it next presents the revoked certificate. Open Agent streams end at their next heartbeat. The node row is locked, so concurrent rotations of one node run one after the other, and a repeated rotation leaves exactly one valid enrollment credential: the last. ErrInvalidNode and ErrNodeDisabled report a node that is gone or disabled; nothing is changed then.

func (*Service) TrustBundle

func (s *Service) TrustBundle(ctx context.Context) ([]*x509.Certificate, error)

TrustBundle returns every CA that may have signed a valid agent certificate.

func (*Service) VerifyPeer

func (s *Service) VerifyPeer(ctx context.Context, rawCerts [][]byte) (agentcontrol.AgentIdentity, *x509.Certificate, error)

VerifyPeer verifies a client certificate chain presented on the agent listener: it must chain to the trust bundle, be valid now, carry the agent identity of a node of this cluster, and not be revoked. Module and kernel certificates of the same CA are refused.

type TokenRequest

type TokenRequest struct {
	Node agentcontrol.AgentNode
	// TTL defaults to DefaultEnrollmentCredentialTTL and may not exceed
	// MaxEnrollmentCredentialTTL.
	TTL time.Duration
	// CreatedBy is the administrator's user id; 0 for the command line.
	CreatedBy uint
	// Actor and IP describe the caller in the audit entry.
	Actor string
	IP    string
}

TokenRequest describes a new one-time enrollment credential.

Jump to

Keyboard shortcuts

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