Documentation
¶
Overview ¶
Package ligand is the Ligand Zero Trust microservice framework.
Call Init to initialize the framework before accepting any connections. Init wires together the pluggable providers, enforces the non-negotiable security invariants (mTLS, fail-closed policy evaluation, rate limiting before policy evaluation), and returns a configured Server ready to serve.
Ligand is an in-process library, not a sidecar or proxy. Once initialized, the service and its enforcement layer are a single binary.
Index ¶
- Constants
- Variables
- func AttrBackoff(d time.Duration) slog.Attr
- func AttrComponent(name string) slog.Attr
- func AttrErr(err error) slog.Attr
- func AttrLatency(d time.Duration) slog.Attr
- func AttrPanic(r any) slog.Attr
- func AttrSpiffeID(id string) slog.Attr
- func AttrStack() slog.Attr
- func AttrThreshold(d time.Duration) slog.Attr
- func InboundTransactionToken(ctx context.Context) string
- func Latest[T any](ctx context.Context, ch LatestRecv[T]) (v T, ok bool)
- func Set[T any](ch LatestSend[T], v T)
- func SleepWithContext(ctx context.Context, clock quartz.Clock, d time.Duration) bool
- func WithInboundChain(ctx context.Context, chain Chain) context.Context
- func WithInboundTransactionToken(ctx context.Context, token string) context.Context
- type Action
- type Binding
- type BindingArgs
- type BindingFactory
- type BundleFetchEvent
- type BundleFetchOutcome
- type BundleFetcher
- type BundleFetcherArgs
- type BundleFetcherFactory
- type BundleUpdate
- type CallOutcome
- type CallOutcomeType
- type Chain
- type CircuitAllowRequest
- type CircuitBreaker
- type CircuitState
- type CircuitStateEvent
- type CircuitStateRequest
- type Config
- type ContextServiceClient
- type ContextServiceClientArgs
- type ContextServiceClientFactory
- type DecisionEvent
- type DeregisterRequest
- type EjectionEvent
- type Endpoint
- type EndpointSelector
- type EndpointUpdate
- type EnforceFunc
- type EvalRequest
- type EvalResponse
- type EventStream
- type FetchBundleRequest
- type FetchBundleResponse
- type FetchWITRequest
- type FetchWITResponse
- type FetchX509ContextRequest
- type FetchX509ContextResponse
- type ForPoolRequest
- type GetSecretRequest
- type GetSecretResponse
- type GracefulDrainer
- type HealthChecker
- type HealthReport
- type HealthSignalExposer
- type HealthSignalExposerArgs
- type HealthSignalExposerFactory
- type HealthTransitionEvent
- type InboundRequest
- type IsEjectedRequest
- type JTICache
- type LatestRecv
- type LatestSend
- type Link
- type LoadBalancer
- type Locality
- type MetricsSnapshot
- type MissingRequiredProvidersError
- type Monitor
- type NoopRateLimiter
- type NoopTelemetry
- func (NoopTelemetry) Close() error
- func (NoopTelemetry) RecordBundleFetch(_ context.Context, _ *BundleFetchEvent) error
- func (NoopTelemetry) RecordCircuitState(_ context.Context, _ *CircuitStateEvent) error
- func (NoopTelemetry) RecordDecision(_ context.Context, _ *DecisionEvent) error
- func (NoopTelemetry) RecordEjection(_ context.Context, _ *EjectionEvent) error
- func (NoopTelemetry) RecordHealthTransition(_ context.Context, _ *HealthTransitionEvent) error
- func (NoopTelemetry) RecordMetrics(_ context.Context, _ *MetricsSnapshot) error
- func (NoopTelemetry) RecordOutboundCall(_ context.Context, _ *OutboundCallEvent) error
- func (NoopTelemetry) RecordRateLimit(_ context.Context, _ *RateLimitEvent) error
- type OutboundCallEvent
- type OutboundOutcome
- type OutboundTransport
- type OutboundTransportArgs
- type OutboundTransportFactory
- type OutlierDetector
- type PolicyEngine
- type PoolSelector
- type ProviderKind
- type RateLimitEvent
- type RateLimitRequest
- type RateLimitResponse
- type RateLimiter
- type RateLimiterArgs
- type RateLimiterFactory
- type RecordCircuitOutcomeRequest
- type RecordLoadBalancerOutcomeRequest
- type RecordOutlierOutcomeRequest
- type RecordRetryRequest
- type RegisterRequest
- type ReleaseRequest
- type ResolveRequest
- type ResolveResponse
- type Resource
- type RetryAllowRequest
- type RetryBudget
- type RetryCondition
- type RetryConfig
- type RetryPolicyProvider
- type RetryPolicyRequest
- type RevocationEvent
- type RevocationUpdate
- type SVIDSnapshot
- type SecretRevocationUpdate
- type Secrets
- type SecretsArgs
- type SecretsFactory
- type SelectRequest
- type SelectResponse
- type Server
- type ServiceRegistrar
- type ServiceRegistrarArgs
- type ServiceRegistrarFactory
- type ServiceRegistration
- type ServiceRegistry
- type ServiceRegistryArgs
- type ServiceRegistryFactory
- type SessionStore
- type State
- type Telemetry
- type TokenExchangeRequest
- type TransactionToken
- type TransactionTokenClaims
- type TrustBoundaryConfig
- type UnimplementedBinding
- type UnimplementedBundleFetcher
- type UnimplementedCircuitBreaker
- func (UnimplementedCircuitBreaker) Allow(_ context.Context, _ *CircuitAllowRequest) (bool, error)
- func (UnimplementedCircuitBreaker) Close() error
- func (UnimplementedCircuitBreaker) RecordOutcome(_ context.Context, _ *RecordCircuitOutcomeRequest)
- func (UnimplementedCircuitBreaker) State(_ context.Context, _ *CircuitStateRequest) CircuitState
- type UnimplementedContextServiceClient
- type UnimplementedEndpointSelector
- type UnimplementedGracefulDrainer
- type UnimplementedHealthChecker
- type UnimplementedHealthSignalExposer
- type UnimplementedLoadBalancer
- type UnimplementedOutboundTransport
- type UnimplementedOutlierDetector
- func (UnimplementedOutlierDetector) Close() error
- func (UnimplementedOutlierDetector) EjectedEndpoints(_ context.Context) []Endpoint
- func (UnimplementedOutlierDetector) IsEjected(_ context.Context, _ *IsEjectedRequest) bool
- func (UnimplementedOutlierDetector) RecordOutcome(_ context.Context, _ *RecordOutlierOutcomeRequest)
- type UnimplementedPolicyEngine
- type UnimplementedPoolSelector
- type UnimplementedRateLimiter
- type UnimplementedRetryBudget
- type UnimplementedRetryPolicyProvider
- type UnimplementedSecrets
- type UnimplementedServiceRegistrar
- type UnimplementedServiceRegistry
- type UnimplementedSessionStore
- type UnimplementedTelemetry
- func (UnimplementedTelemetry) RecordBundleFetch(_ context.Context, _ *BundleFetchEvent) error
- func (UnimplementedTelemetry) RecordCircuitState(_ context.Context, _ *CircuitStateEvent) error
- func (UnimplementedTelemetry) RecordEjection(_ context.Context, _ *EjectionEvent) error
- func (UnimplementedTelemetry) RecordHealthTransition(_ context.Context, _ *HealthTransitionEvent) error
- func (UnimplementedTelemetry) RecordMetrics(_ context.Context, _ *MetricsSnapshot) error
- type UnimplementedWorkloadIdentity
- type WPTSigner
- type WatchEndpointsRequest
- type WorkloadIdentity
- type X509ContextUpdate
- type X509SVID
Examples ¶
Constants ¶
const ( // ChainHeader carries a JSON-encoded [Chain] propagated between services. // Intentionally transitional: WIMSE WPT/TT do not carry the full // intermediate hop chain, so X-Ligand-Chain is used for the mTLS path. // The header is currently unauthenticated; policy must not trust upstream // chain links beyond the immediate peer until C3 from security_audit_2026-05-21.md is addressed. ChainHeader = "X-Ligand-Chain" // WITHeader carries a Workload-Identity-Token per draft-ietf-wimse-workload-creds-00. // WITs must be signed by the Identity Server (SPIRE) using its JWT authority key, // which is distributed via the SPIFFE trust bundle JWTAuthorities field. Verification // is fail-closed: requests with WIT/WPT headers are rejected unless valid JWT // authorities are present in the trust bundle. // // Full support requires SPIRE to implement WIMSE WIT issuance (SPIFFE issue #315). // Until then, [WorkloadIdentity.FetchWIT] returns [ErrNotSupported] and // the app-layer auth path is effectively disabled. // // HTTP Message Signatures (draft-ietf-wimse-http-signature-01) for body integrity // at external trust domain boundaries are not yet implemented. WITHeader = "Workload-Identity-Token" // WPTHeader carries a Workload-Proof-Token per draft-ietf-wimse-wpt-00. WPTHeader = "Workload-Proof-Token" // TxnTokenHeader carries a Transaction Token per draft-ietf-oauth-transaction-tokens. TxnTokenHeader = "Txn-Token" )
WIMSE header names. Single point of change when the drafts finalise.
const ComponentLigand = "ligand"
ComponentLigand is the value used for the "component" log attribute injected by the framework on every logger it uses. Callers can use this constant to filter or route log records emitted by the framework.
const DefaultSecretRenewalLeadTime = 10 * time.Minute
DefaultSecretRenewalLeadTime is the default duration before a secret's TTL expires at which the framework begins proactive renewal. The actual trigger is min(DefaultSecretRenewalLeadTime, ttl/2), so short-lived secrets (TTL < 2×lead time) are renewed at their halfway point instead.
Variables ¶
var ( // DefaultPreStopTimeout is the default wall-clock budget for all pre-stop // hooks when Config.PreStopTimeout is zero. DefaultPreStopTimeout = 5 * time.Second // ErrIdentityGraceExpired is the cause passed to Monitor.Set when the // identity rotation grace period expires without a successful renewal. // It indicates the framework is transitioning KindIdentity to Unavailable // as a security-driven hard failure rather than a transient error. ErrIdentityGraceExpired = errors.New("ligand: identity rotation grace period expired without SVID renewal") // DefaultPolicyLatencyWarnThreshold is the policy evaluation duration above // which the framework logs a latency warning. Config.PolicyLatencyWarnThreshold // defaults to this value when zero. DefaultPolicyLatencyWarnThreshold = 50 * time.Millisecond // DefaultPolicyEvalTimeout is the maximum time policy.Evaluate may run before // the framework cancels the call and denies the request fail-closed. // Config.PolicyEvalTimeout defaults to this value when zero. DefaultPolicyEvalTimeout = 5 * time.Second // ErrNoSVIDs is returned by Init when the identity provider supplies an empty // SVID list on startup. ErrNoSVIDs = errors.New("ligand: identity provider returned no SVIDs") // ErrEmptyInitialBundle is returned by Init when the bundle fetcher returns // a nil or empty bundle on the initial fetch. The framework requires a // non-empty bundle to proceed. ErrEmptyInitialBundle = errors.New("ligand: bundle fetcher returned empty bundle on initial fetch") // ErrPolicyEngineReloadRequired is returned by Init when a BundleFetcher is // configured but the PolicyEngine does not support Reload (returns // ErrNotSupported). Configure a policy engine that implements Reload, or // remove the BundleFetcher from Config. ErrPolicyEngineReloadRequired = errors.New("ligand: policy engine does not support Reload; required when BundleFetcher is configured") // ErrNotSupported is returned by [Unimplemented] stub methods to indicate // that the provider does not implement an optional capability. The framework // distinguishes this sentinel from genuine failures: on ErrNotSupported it // applies the conservative safe default for that capability; on any other // error it treats the capability as failed. ErrNotSupported = errors.New("ligand: capability not supported by this provider") // ErrServiceRegistrationRequired is returned by [Init] when // [Config.ServiceRegistrar] is set but [Config.ServiceRegistration] is nil. ErrServiceRegistrationRequired = errors.New("ligand: ServiceRegistrar is configured but ServiceRegistration is nil") // ErrNoEndpoints is returned by [PoolSelector.Pick] when the // candidate list is empty, and by outbound transports when the service // registry resolves to zero endpoints. Callers may use errors.Is to detect // this condition and apply service-specific fallback logic. ErrNoEndpoints = errors.New("ligand: no endpoints available") )
var ( // ErrRateLimitDenial is returned by [Server.Enforce] and // [Server.EnforceInbound] when the rate limiter rejects a request within // normal operating parameters (Allowed: false). It is distinct from // [ErrPolicyDenial] so callers can map the two cases to different response // codes (e.g. HTTP 429 vs HTTP 403). ErrRateLimitDenial = errors.New("ligand: request denied by rate limiter") // ErrRateLimiterFailure is returned by [Server.Enforce] and // [Server.EnforceInbound] when the rate limiter returns an unexpected error // (e.g. its backend is unreachable). It is distinct from [ErrRateLimitDenial] // so callers can map it to a server-error response code (e.g. HTTP 503) // rather than a retryable rate-limit response (HTTP 429). ErrRateLimiterFailure = errors.New("ligand: rate limiter internal error") // ErrPolicyDenial is returned by [Server.Enforce] and // [Server.EnforceInbound] when the policy engine rejects a request. It is // distinct from [ErrRateLimitDenial] so callers can map the two cases to // different response codes (e.g. HTTP 403 vs HTTP 429). ErrPolicyDenial = errors.New("ligand: request denied by policy") )
var ( // ErrNoPeerCertificate is returned by verifyPeerChain when the peer presents // no certificate. RequireAnyClientCert normally prevents this, but we guard // defensively in case the callback is invoked outside that context. ErrNoPeerCertificate = errors.New("ligand: peer presented no certificate") // ErrNoServerCertificate is returned by verifyServerChain when the server // presents no certificate. TLS requires servers to present a certificate, // so this indicates a protocol violation; the guard exists for defensive // completeness. ErrNoServerCertificate = errors.New("ligand: server presented no certificate") // ErrChosenSVIDAbsent is wrapped by newTLSStateFor when the rotation update // does not contain the SVID that was selected at Init time. ErrChosenSVIDAbsent = errors.New("ligand: chosen SVID absent from rotation update") )
var ErrBulkheadFull = errors.New("ligand: bulkhead concurrency limit reached")
ErrBulkheadFull is returned by [PoolSelector.Pick] when healthy endpoints exist but the concurrency limit for the pool has been reached.
var ErrCircuitOpen = errors.New("ligand: circuit breaker open")
ErrCircuitOpen is returned by [CircuitBreaker.Allow] when the circuit for the requested service is open or the half-open call limit has been reached. Callers should treat this as a fast-fail: the outbound transport returns it immediately without attempting a connection.
var ( // ErrEmptySVIDList is returned when the identity provider supplies an empty // SVID list during a rotation update, which is treated as an identity expiry. ErrEmptySVIDList = errors.New("ligand: SVID rotation returned empty SVID list") )
var ErrRetryBudgetExhausted = errors.New("ligand: retry budget exhausted")
ErrRetryBudgetExhausted is returned by the outbound transport when a retry is needed but [RetryBudget.Allow] has denied it.
var ErrWPTKeyNotECDSA = errors.New("ligand: WPT signing requires an ECDSA private key")
ErrWPTKeyNotECDSA is returned by WPTSigner.BuildWIMSETokens when the current SVID private key is not an *ecdsa.PrivateKey. SPIFFE SVIDs use ECDSA, so this indicates an unsupported identity provider configuration.
Functions ¶
func AttrBackoff ¶
AttrBackoff returns a "backoff" slog.Attr for a retry delay duration.
func AttrComponent ¶
AttrComponent returns a "component" slog.Attr identifying the framework subsystem. Use ComponentLigand as the value for framework-emitted logs.
func AttrErr ¶
AttrErr returns an "err" slog.Attr for the given error. Uses slog.Any to preserve the error type for handlers that inspect it.
func AttrLatency ¶
AttrLatency returns a "latency" slog.Attr for a wall-clock elapsed time.
func AttrPanic ¶
AttrPanic returns a "panic" slog.Attr for a value recovered by recover(). Always pair with AttrStack.
func AttrSpiffeID ¶
AttrSpiffeID returns a "spiffe_id" slog.Attr for a SPIFFE ID string.
func AttrStack ¶
AttrStack returns a "stack" slog.Attr containing the current goroutine stack trace. Call immediately after recover() alongside AttrPanic.
func AttrThreshold ¶
AttrThreshold returns a "threshold" slog.Attr for a configured duration limit that is being compared against.
func InboundTransactionToken ¶
InboundTransactionToken returns the Transaction Token stored by WithInboundTransactionToken, or "" if none is present.
func Latest ¶
func Latest[T any](ctx context.Context, ch LatestRecv[T]) (v T, ok bool)
Latest blocks until ch delivers a value or ctx is cancelled, then drains any additional pending values and returns only the most recent. The second return value is false if ctx was cancelled or ch was closed before any value was received.
Latest and Set are both required even when used together on the same channel: Latest drains values that arrive in the gap between waking up and reading, while Set guarantees at most one value is buffered at send time.
Only valid for LatestRecv channels, where skipping intermediate values is safe. For EventStream channels, use range.
func Set ¶
func Set[T any](ch LatestSend[T], v T)
Set publishes v to ch, evicting any unseen prior value so the consumer always sees the most recent update. ch must be a buffered channel of size 1.
Set and Latest are both required even when used together on the same channel: Set guarantees at most one value is buffered at send time, while Latest drains any value that arrives in the gap between waking up and reading, ensuring the consumer always returns the most recent value.
func SleepWithContext ¶
SleepWithContext sleeps for d using clock, returning true when the sleep completes or false immediately if ctx is cancelled. Callers should return when SleepWithContext returns false.
func WithInboundChain ¶
WithInboundChain stores chain in ctx so the outbound transport prepends it to the X-Ligand-Chain header on downstream calls. The HTTP binding calls this automatically; application code only needs it to override the chain in unusual cases.
func WithInboundTransactionToken ¶
WithInboundTransactionToken stores the verified/minted Transaction Token in ctx so the outbound transport forwards it unmodified on downstream calls, per draft-ietf-oauth-transaction-tokens §13.2. The HTTP binding calls this automatically after Handle succeeds; application code does not need to call it.
Types ¶
type Action ¶
type Action struct {
// Name is the operation, e.g. "GET", "POST", "read", "invoke".
Name string
}
Action identifies the operation being performed on a Resource.
type Binding ¶
type Binding interface {
// Serve starts accepting requests and blocks until [Binding.Shutdown] or
// [Binding.Close] is called, or a fatal error occurs. ctx governs listener
// establishment only: implementations should use it for the underlying
// Listen call so that a cancelled or timed-out context aborts startup
// cleanly. Cancelling ctx after the listener is established has no effect;
// callers must use [Binding.Shutdown] or [Binding.Close] to stop a running
// server.
Serve(ctx context.Context) error
// Shutdown gracefully drains in-flight requests within the deadline
// imposed by ctx, then stops accepting new ones. Called by the framework
// when a drain signal is received.
Shutdown(ctx context.Context) error
io.Closer
// Listening returns a channel that is closed once the binding's listener
// is established and accepting connections. The framework waits on this
// channel before transitioning [KindBinding] to [Ready], so the health
// signal reflects actual listener readiness rather than the end of [Init].
//
// Implementations must also close the channel (via the same mechanism) if
// [Binding.Serve] returns an error before establishing the listener, and in
// [Binding.Close] in case Serve was never called — so the framework goroutine
// waiting on this channel does not leak.
//
// [UnimplementedBinding] returns a pre-closed channel so test fakes that do
// not model a real listener are marked [Ready] synchronously at [Init] return.
Listening() <-chan struct{}
// Addr returns the address the binding is listening on. It blocks until
// [Binding.Listening] is closed, making it safe to call from a goroutine
// that races with [Binding.Serve] startup. Returns an empty string if Serve
// failed before establishing the listener or if the binding does not
// establish a real listener.
//
// The framework calls Addr to determine the address to publish to the
// service registry when [Config.ServiceRegistrar] is configured alongside
// a [Config.Binding].
Addr() string
}
Binding is the seam between the transport layer and the enforcement pipeline. Each implementation accepts inbound connections on a specific transport (HTTP, gRPC, etc.), applies the framework-provided TLS configuration, and calls [Server.Enforce] for each request. If Enforce returns nil the binding invokes the application handler; if it returns an error the binding translates it to an appropriate protocol-level denial (e.g. HTTP 429 or 403).
All methods are required. Embed UnimplementedBinding to remain forward-compatible if optional methods are added in the future.
type BindingArgs ¶
type BindingArgs struct {
// Enforce is the enforcement pipeline entry point. Call it once per
// inbound request; translate the returned error to the appropriate
// protocol-level denial (e.g. HTTP 429 for [ErrRateLimitDenial], 403
// for all other non-nil errors).
Enforce EnforceFunc
// InboundTLSConfig is the framework-managed *tls.Config for the inbound
// mTLS listener. Apply it to the transport listener in
// [Binding.Serve]; do not construct a separate TLS config.
InboundTLSConfig *tls.Config
// GetJWTAuthorities returns the current map of trust domain → JWT
// authority public keys from the SPIFFE trust bundle. Used by bindings
// to verify Workload Identity Token (WIT) signatures on the app-layer
// auth path. Returns nil when the Identity Server has not yet populated
// JWT authorities (e.g. before SPIRE implements SPIFFE issue #315), in
// which case the WIT/WPT path is fail-closed.
GetJWTAuthorities func() map[string][]crypto.PublicKey
// IsTrustBoundaryPath reports whether path is a configured trust-boundary
// entry point. The HTTP binding uses this to strip the Authorization header
// after Exchange has converted the bearer token into a Transaction Token,
// preventing the raw credential from reaching the application handler.
// Returns false for all paths when no trust boundary is configured.
IsTrustBoundaryPath func(path string) bool
}
BindingArgs holds the ligand-internal values that BindingFactory implementations receive from Init. Only values that are contractually guaranteed to be present when Init calls the factory are included; adding a field to this struct is a considered API decision because it constrains the construction order inside Init.
type BindingFactory ¶
type BindingFactory func(*BindingArgs) Binding
BindingFactory is a constructor called by Init after the workload identity and TLS config are established. It returns a ready-to-use Binding. The BindingArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
Implementations should expose a factory function alongside their concrete constructor; see package httpbinding for the canonical example.
type BundleFetchEvent ¶
type BundleFetchEvent struct {
// Timestamp is the time the fetch result was received by the framework.
// Always populated.
Timestamp time.Time `json:"timestamp"`
// SPIFFEID is the workload SPIFFE ID of the service that fetched the bundle.
// Always populated.
SPIFFEID string `json:"spiffe_id"`
// ETag is the version identifier of the fetched bundle. Empty on error or
// when the server does not support conditional fetches.
ETag string `json:"etag,omitempty"`
// Version is the human-readable bundle version string, if provided by the
// server. Empty when the server does not expose version metadata.
Version string `json:"version,omitempty"`
// Outcome describes the result of the fetch attempt. Always populated.
Outcome BundleFetchOutcome `json:"outcome"`
// Err is set when Outcome is [BundleFetchOutcomeError]. Excluded from
// default JSON marshaling; providers that need a string representation
// should emit err.Error() as a separate attribute.
Err error `json:"-"`
}
BundleFetchEvent is emitted after each bundle fetch attempt — both the initial fetch during Init and each update delivered by the watch loop.
type BundleFetchOutcome ¶
type BundleFetchOutcome string
BundleFetchOutcome describes the result of a bundle fetch attempt emitted in a BundleFetchEvent.
const ( // BundleFetchOutcomeSuccess indicates a new bundle was returned and loaded. BundleFetchOutcomeSuccess BundleFetchOutcome = "success" // BundleFetchOutcomeNotModified indicates the server confirmed the bundle // has not changed since the caller's last known ETag (HTTP 304). BundleFetchOutcomeNotModified BundleFetchOutcome = "not_modified" // BundleFetchOutcomeError indicates the fetch failed with an error. BundleFetchOutcomeError BundleFetchOutcome = "error" )
type BundleFetcher ¶
type BundleFetcher interface {
// Fetch retrieves the current bundle. Returns the bundle bytes, a version
// identifier (ETag or equivalent), and an error. The caller passes
// req.KnownETag to enable conditional fetches; the implementation may
// return a response with nil Bundle and the same ETag to indicate that
// the bundle has not changed since req.KnownETag.
//
// The framework calls Fetch once during Init with an empty KnownETag to
// obtain the initial bundle. A nil or empty Bundle in the response is
// treated as an error by the framework.
Fetch(ctx context.Context, req *FetchBundleRequest) (*FetchBundleResponse, error)
// WatchBundles returns a channel delivering a [BundleUpdate] whenever a
// new bundle version is available. The channel follows the
// [LatestRecv]/[LatestSend] pattern: only the most recent update matters
// and intermediate values may be discarded.
//
// The channel is closed when ctx is cancelled or when the provider
// encounters an unrecoverable error (in which case the framework
// re-establishes the watch with exponential backoff). Error updates
// ([BundleUpdate.Err] non-nil) cause the framework to set
// [KindBundleFetcher] to [Unavailable] and retry.
//
// The framework calls WatchBundles after Init returns, using the ETag
// from the initial Fetch as the baseline. Providers should track their
// own ETag state so that the first WatchBundles call only delivers an
// update when the bundle has changed since the initial Fetch.
WatchBundles(ctx context.Context) LatestRecv[BundleUpdate]
io.Closer
}
BundleFetcher retrieves signed policy bundles from the bundle server and delivers them to the PolicyEngine via the framework's hot-reload path.
Required methods: [BundleFetcher.Fetch], [BundleFetcher.WatchBundles], and io.Closer.
The framework calls [BundleFetcher.Fetch] once during Init to obtain the initial bundle. After Init returns, the framework calls [BundleFetcher.WatchBundles] to receive subsequent bundle updates and passes each update's bytes to [PolicyEngine.Reload].
Failure mode: while the bundle server is unreachable the last successfully loaded policy remains active. The health monitor transitions KindBundleFetcher to Unavailable on the first watch error and back to Ready when a successful update is delivered. Grace window: indefinite — the framework never evicts a loaded policy due to bundle-fetcher unavailability alone. Recovery: the bundle watch loop retries with exponential backoff; no operator action required.
type BundleFetcherArgs ¶
type BundleFetcherArgs struct {
// OutboundTLSConfig is a *tls.Config for outbound mTLS connections from
// this workload to the bundle server. It presents the current X.509 SVID
// via GetClientCertificate (updated atomically on each SVID rotation) and
// verifies the server certificate against the SPIFFE trust bundle.
OutboundTLSConfig *tls.Config
// SPIFFEID is the workload SPIFFE ID of this service. Bundle fetcher
// implementations use it to request only the policy bundle scoped to this
// service (e.g. URL-encoded and appended as a path segment to the base URL).
SPIFFEID string
}
BundleFetcherArgs holds the ligand-internal values that BundleFetcherFactory implementations receive from Init. Only values that are contractually guaranteed to be present when Init calls the factory are included; adding a field to this struct is a considered API decision because it constrains the construction order inside Init.
type BundleFetcherFactory ¶
type BundleFetcherFactory func(*BundleFetcherArgs) (BundleFetcher, error)
BundleFetcherFactory is a constructor called by Init after the workload identity and TLS config are established. The BundleFetcherArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
Providers expose both a Factory function (for use in Config) and a New constructor (for tests and advanced use).
type BundleUpdate ¶
type BundleUpdate struct {
Response *FetchBundleResponse
Err error
}
BundleUpdate is delivered on the LatestRecv returned by [BundleFetcher.WatchBundles]. Err is non-nil if the watch encountered an error; in that case Response is nil.
type CallOutcome ¶
type CallOutcome struct {
// Type is the result class of the call.
Type CallOutcomeType
// Latency is the elapsed time from the start of the call attempt to when
// the outcome was determined. Available for latency-based ejection (S9).
Latency time.Duration
}
CallOutcome is the result of one outbound call attempt, including its latency. Passed to [PoolSelector.Release] and [CircuitBreaker.RecordOutcome] after every attempt.
type CallOutcomeType ¶
type CallOutcomeType int
CallOutcomeType is the result class of an outbound call, used by CircuitBreaker, PoolSelector, and the outbound transport to drive ejection and state-machine transitions.
const ( // CallOutcomeSuccess indicates the call completed and the response status // was below 5xx. CallOutcomeSuccess CallOutcomeType = iota // CallOutcomeGatewayError indicates the upstream returned HTTP 502, 503, // or 504. Tracked separately from other 5xx responses because Envoy's // outlier detection counts consecutive gateway errors independently. CallOutcomeGatewayError // CallOutcomeServerError indicates the upstream returned a non-gateway // 5xx response (e.g. 500, 501, 505). CallOutcomeServerError // CallOutcomeConnectionError indicates the call failed at the TCP/TLS // level or timed out before a response was received. CallOutcomeConnectionError )
func (CallOutcomeType) String ¶
func (t CallOutcomeType) String() string
String returns a human-readable name for the outcome type.
type Chain ¶
type Chain []Link
Chain is the ordered sequence of hops that have handled a request, from the most distal (the originating user or device) to the most proximate (the immediate upstream caller). Each element represents one hop; len(chain) is the number of hops. An empty Chain indicates that provenance information was not available or has not yet been propagated.
func InboundChain ¶
InboundChain returns the Chain stored by WithInboundChain, or nil.
type CircuitAllowRequest ¶
type CircuitAllowRequest struct {
// SPIFFEID is the SPIFFE ID of the destination service.
SPIFFEID string
}
CircuitAllowRequest is the argument to [CircuitBreaker.Allow].
type CircuitBreaker ¶
type CircuitBreaker interface {
// Allow reports whether an outbound call to req.SPIFFEID is permitted.
// Returns false with [ErrCircuitOpen] when the circuit is open or the
// half-open concurrent call limit has been reached. Returns false with
// another error only on infrastructure failure. Returns true, nil when the
// call should proceed.
Allow(ctx context.Context, req *CircuitAllowRequest) (bool, error)
// RecordOutcome records the result of an outbound call to req.SPIFFEID and
// drives state-machine transitions.
RecordOutcome(ctx context.Context, req *RecordCircuitOutcomeRequest)
// State returns the current [CircuitState] for req.SPIFFEID. Unknown SPIFFE
// IDs return [CircuitStateClosed].
State(ctx context.Context, req *CircuitStateRequest) CircuitState
io.Closer
}
CircuitBreaker stops outbound calls to a service that has exceeded its error threshold, giving the service time to recover before traffic resumes.
The OutboundTransport calls [CircuitBreaker.Allow] before each attempt and [CircuitBreaker.RecordOutcome] after each attempt. The state machine transitions: Closed → Open (on threshold breach) → HalfOpen (after OpenDuration) → Closed (on probe success) or Open (on probe failure).
All three methods are optional in enforcement semantics: if no CircuitBreaker is configured, the framework skips circuit-breaking silently. Implementations that do not need all three methods should embed UnimplementedCircuitBreaker.
type CircuitState ¶
type CircuitState int
CircuitState is the state of a per-service circuit breaker.
const ( // CircuitStateClosed is the normal operating state: calls are forwarded. CircuitStateClosed CircuitState = iota // CircuitStateOpen means the circuit has tripped: calls are rejected // immediately without attempting to reach the service. CircuitStateOpen // CircuitStateHalfOpen means the circuit is probing for recovery: a // limited number of calls are forwarded; a success closes the circuit, // a failure reopens it. CircuitStateHalfOpen )
func (CircuitState) String ¶
func (s CircuitState) String() string
String returns a human-readable name for the state.
type CircuitStateEvent ¶
type CircuitStateEvent struct {
// Timestamp is the time the transition was recorded. Always populated.
Timestamp time.Time `json:"timestamp"`
// SPIFFEID is the SPIFFE ID of the service whose circuit changed state.
// Always populated.
SPIFFEID string `json:"spiffe_id"`
// From is the state before the transition; To is the state after.
// Always populated.
From CircuitState `json:"from"`
To CircuitState `json:"to"`
}
CircuitStateEvent is emitted by the CircuitBreaker when a per-service circuit transitions between states.
type CircuitStateRequest ¶
type CircuitStateRequest struct {
// SPIFFEID is the SPIFFE ID of the destination service.
SPIFFEID string
}
CircuitStateRequest is the argument to [CircuitBreaker.State].
type Config ¶
type Config struct {
// Identity provides the service's workload identity for mTLS. Required.
Identity WorkloadIdentity
// Policy evaluates access control policy for inbound requests. Required.
// The enforcement pipeline is fail-closed: Init returns an error if
// Policy is nil, and any evaluation error denies the request.
Policy PolicyEngine
// Binding constructs the [Binding] that accepts inbound connections and
// calls [Server.Enforce] for each request. [Init] calls the factory with
// a [BindingArgs] once workload identity and TLS are established. Required.
Binding BindingFactory
// GracefulDrainer signals the framework to begin shutting down. When
// Wait returns nil, Init's background goroutine calls [Server.Close].
// Optional; nil means the caller is responsible for calling [Server.Close] directly.
GracefulDrainer GracefulDrainer
// RateLimiter constructs the [RateLimiter] that enforces rate limits before
// policy evaluation. [Init] calls the factory after workload identity and TLS
// are established, providing the outbound mTLS config for the rate limit
// backend connection via [RateLimiterArgs.OutboundTLSConfig]. Unlike other
// factory types, [RateLimiterFactory] returns an error; Init fails if the
// factory returns an error (e.g. because the Valkey backend is unreachable).
//
// Optional; nil defaults to [NoopRateLimiterFactory] (all requests allowed).
RateLimiter RateLimiterFactory
// Telemetry receives structured events and operational metrics emitted by
// the enforcement pipeline. Optional; nil defaults to [NoopTelemetry]
// (all events discarded).
Telemetry Telemetry
// BundleFetcher constructs the [BundleFetcher] that retrieves signed policy
// bundles from the bundle server. [Init] calls the factory with a
// [BundleFetcherArgs] after workload identity and TLS are established,
// providing the outbound mTLS config for the bundle server connection. On startup, Init calls
// [BundleFetcher.Fetch] to load the initial bundle and passes the bytes to
// [PolicyEngine.Reload]; Init fails if either call fails. After Init
// returns, a background goroutine calls [BundleFetcher.WatchBundles] and
// forwards each update to [PolicyEngine.Reload].
//
// Configuring BundleFetcher alongside a [PolicyEngine] that returns
// [ErrNotSupported] from Reload is a misconfiguration; Init returns
// [ErrPolicyEngineReloadRequired] in that case.
//
// Optional; nil means no bundle-based hot-reload — the policy engine is
// responsible for its own policy loading.
BundleFetcher BundleFetcherFactory
// Secrets constructs the [Secrets] that retrieves secrets from
// a backing store. [Init] calls the factory after workload identity and
// TLS are established, providing the outbound mTLS config for the secrets
// backend connection via [SecretsArgs.OutboundTLSConfig]. [Init]
// starts a background goroutine that watches for revocation events and
// evicts affected cache entries. Application code accesses secrets via
// [Server.Secrets], never by holding a reference to this provider
// directly.
//
// Optional; nil means no secrets provider is configured and
// [Server.Secrets].Get always returns [ErrNotSupported].
Secrets SecretsFactory
// SecretRenewalLeadTime is how long before a secret's TTL expires the
// framework begins proactive background renewal. The actual trigger is
// min(SecretRenewalLeadTime, ttl/2) so that short-lived secrets are
// renewed at their halfway point rather than before they are meaningfully
// used. Zero uses [DefaultSecretRenewalLeadTime].
//
// Only meaningful when [Config.Secrets] is non-nil.
SecretRenewalLeadTime time.Duration
// ServiceRegistry constructs the [ServiceRegistry] used to resolve
// logical service names to endpoint sets for outbound calls. [Init] calls
// the factory after workload identity and TLS are established, providing
// the outbound TLS config via [ServiceRegistryArgs]. Required when
// [Config.Outbound] is non-nil; Init returns an error if ServiceRegistry
// is set without EndpointSelector and Outbound.
//
// Optional; nil means the framework cannot make outbound calls.
ServiceRegistry ServiceRegistryFactory
// EndpointSelector creates per-pool selectors for outbound calls. Each pool
// combines load balancing, outlier detection, and concurrency limiting for
// one fixed endpoint set. Required when [Config.Outbound] is non-nil; Init
// returns an error if EndpointSelector is set without ServiceRegistry and
// Outbound.
//
// Optional; nil means the framework cannot make outbound calls.
EndpointSelector EndpointSelector
// Outbound constructs the [OutboundTransport] that manages outbound mTLS
// connections to other services. [Init] calls the factory after workload
// identity and TLS are established, providing service discovery and WPT
// signing material via [OutboundTransportArgs]. Requires both
// [Config.ServiceRegistry] and [Config.EndpointSelector] to be non-nil.
//
// Optional; nil means the framework cannot make outbound calls.
Outbound OutboundTransportFactory
// CircuitBreaker stops outbound calls to a service that has exceeded its
// error threshold. The outbound transport checks the breaker before each
// call attempt and records outcomes after each attempt. Optional; nil means
// circuit breaking is disabled.
CircuitBreaker CircuitBreaker
// RetryBudget gates retry attempts with a sliding-window budget across all
// services. Optional; nil means no budget cap.
RetryBudget RetryBudget
// RetryPolicy looks up the retry configuration for a named service.
// Optional; nil means no retry for any service. The static implementation
// ([provider/retrypolicy/static]) wraps a fixed map; a dynamic
// implementation may reload policies from etcd without restarting.
RetryPolicy RetryPolicyProvider
// ContextServiceClient constructs the [ContextServiceClient] used at
// trust-domain entry points to exchange incoming OAuth access tokens for
// Transaction Tokens. [Init] calls the factory after workload identity is
// established.
//
// Optional; nil means no Context Service is configured and the framework
// never calls [ContextServiceClient.Exchange].
ContextServiceClient ContextServiceClientFactory
// TrustBoundary configures the paths at which the framework calls
// [ContextServiceClient.Exchange]. Ignored when [Config.ContextServiceClient]
// is nil.
TrustBoundary TrustBoundaryConfig
// ServiceRegistrar constructs the [ServiceRegistrar] that publishes this
// service instance to the registry backend at startup and withdraws it at
// graceful shutdown. [Init] calls the factory after workload identity and
// TLS are established.
//
// Required when [Config.ServiceRegistration] is non-nil; [Init] returns
// [ErrServiceRegistrationRequired] if ServiceRegistrar is set without
// ServiceRegistration. The converse ([Config.ServiceRegistration] set
// without ServiceRegistrar) returns [MissingRequiredProvidersError].
//
// Optional; nil means this service does not self-register.
ServiceRegistrar ServiceRegistrarFactory
// ServiceRegistration describes the endpoint to publish to the registry at
// startup. The framework populates [Endpoint.SPIFFEID] from the current
// SVID before calling [ServiceRegistrar.Register].
//
// Required when [Config.ServiceRegistrar] is non-nil.
ServiceRegistration *ServiceRegistration
// HealthSignalExposer constructs the [HealthSignalExposer] that exposes the
// framework's aggregate health state to external observers (e.g. a
// Kubernetes readiness probe). [Init] calls the factory with a
// [HealthSignalExposerArgs] once the monitor is created. Optional; nil
// Optional; nil means health state is not exposed externally.
HealthSignalExposer HealthSignalExposerFactory
// PolicyLatencyWarnThreshold is the policy evaluation duration above which
// the framework logs a warning. Zero uses [DefaultPolicyLatencyWarnThreshold].
PolicyLatencyWarnThreshold time.Duration
// PolicyEvalTimeout is the maximum time allowed for a single policy
// evaluation. Exceeding this deadline cancels the evaluation context and
// denies the request fail-closed. Zero uses [DefaultPolicyEvalTimeout].
PolicyEvalTimeout time.Duration
// Clock is the time source used for timestamps and backoff timers. Optional;
// nil defaults to [quartz.NewReal]. Inject [quartz.NewMock] in tests to
// control time deterministically.
Clock quartz.Clock
// Logger receives structured log events from the framework. Optional;
// nil defaults to [slog.Default]. Callers can bridge any third-party
// logger (zap, zerolog, etc.) by supplying a [slog.Handler] adapter.
Logger *slog.Logger
// PreStopHooks are called sequentially after the service transitions to
// [Draining] and before [Binding.Shutdown]. Each hook receives a context
// bounded by [Config.PreStopTimeout]. Errors are logged at warn but do not
// abort shutdown — hooks must not be able to delay shutdown indefinitely.
//
// Optional; nil means no pre-stop hooks are configured.
PreStopHooks []func(context.Context) error
// PreStopTimeout bounds the total wall-clock time allowed for all
// [Config.PreStopHooks] to complete. Zero defaults to 5s.
//
// Only meaningful when [Config.PreStopHooks] is non-nil.
PreStopTimeout time.Duration
// IdentityGracePeriod is how long the framework continues using the last
// valid SVID after a rotation error before transitioning [KindIdentity] to
// [Unavailable]. During the grace window [KindIdentity] is [Degraded] and
// the existing SVID remains in use for mTLS.
//
// Zero uses the current SVID's NotAfter as the implicit deadline — the
// framework keeps serving until the certificate actually expires.
IdentityGracePeriod time.Duration
// SPIFFEID, if non-empty, selects the SVID with this SPIFFE ID from the
// list returned by [Config.Identity] at Init. When empty, the framework
// uses the first SVID in the list. Init returns an error if this ID is
// set but absent from the identity provider's response.
SPIFFEID string
// MetricsInterval is the period at which the framework calls
// [Telemetry.RecordMetrics] with a snapshot of aggregated framework health
// metrics. Zero defaults to 30s. The metrics loop is skipped entirely when
// the configured [Telemetry] provider returns [ErrNotSupported] from
// [Telemetry.RecordMetrics] (e.g. when using the slog provider).
MetricsInterval time.Duration
}
Config holds the providers and settings passed to Init.
The zero value is not a valid configuration: Identity, Policy, and Binding are required. All other fields have safe zero-value defaults.
type ContextServiceClient ¶
type ContextServiceClient interface {
// Exchange performs RFC 8693 token exchange: validates the incoming subject
// token against the configured IdP, then issues a signed Transaction Token
// carrying the verified caller identity and scope.
//
// If the subject token is invalid or the Context Service is unavailable,
// Exchange returns an error. The framework is fail-closed: an Exchange
// error at a configured trust-boundary path denies the request rather
// than forwarding the raw credential downstream.
Exchange(ctx context.Context, req *TokenExchangeRequest) (*TransactionToken, error)
// VerifyTransactionToken validates the signature and expiry of a
// Transaction Token received from an upstream service leg. Returns the
// decoded claims on success. On error, the framework treats the token as
// absent (fail-safe) rather than denying the request, because internal
// services that receive a forwarded token should not fail if the upstream
// leg used a different authentication path.
VerifyTransactionToken(ctx context.Context, token string) (*TransactionTokenClaims, error)
io.Closer
}
ContextServiceClient exchanges incoming credentials for Transaction Tokens and verifies inbound Transaction Tokens at trust-domain boundaries.
A trust-domain boundary is an API gateway that terminates mTLS and receives an OAuth 2.0 access token from an external caller. The framework calls [ContextServiceClient.Exchange] at configured boundary paths to convert the incoming credential into a signed Transaction Token that can be propagated across all subsequent internal hops.
Required methods: [ContextServiceClient.Exchange], [ContextServiceClient.VerifyTransactionToken], and io.Closer.
There are no optional methods today. Embed UnimplementedContextServiceClient to remain forward-compatible when optional methods are added.
type ContextServiceClientArgs ¶
type ContextServiceClientArgs struct {
// OutboundTLSConfig is the framework-managed *tls.Config for outbound
// mTLS connections, e.g. for fetching JWKS over mTLS.
OutboundTLSConfig *tls.Config
// GetSnapshot returns an atomic snapshot of the workload's current SVID
// material. Callers must invoke it once per operation and read all needed
// fields from the returned value to guarantee consistency across a rotation.
GetSnapshot func() SVIDSnapshot
// Monitor is the framework health monitor. Providers use it to transition
// [KindContextServiceClient] between [Ready] and [Unavailable] when their
// external dependencies (e.g. a JWKS endpoint) become unavailable or
// recover. Nil when no [Config.ContextServiceClient] is configured.
Monitor *Monitor
}
ContextServiceClientArgs holds the ligand-internal values that ContextServiceClientFactory implementations receive from Init.
type ContextServiceClientFactory ¶
type ContextServiceClientFactory func(*ContextServiceClientArgs) ContextServiceClient
ContextServiceClientFactory is a constructor called by Init after workload identity and TLS are established. It returns a ready-to-use ContextServiceClient. The ContextServiceClientArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
type DecisionEvent ¶
type DecisionEvent struct {
// Timestamp is the time the request arrived at the enforcement pipeline.
// Always populated.
Timestamp time.Time `json:"timestamp"`
// Chain is the full verified identity chain as received and evaluated,
// with the immediate peer as the last element. Always populated; contains
// at least the immediate peer link.
Chain Chain `json:"chain"`
// Resource and Action identify the access being requested. Always populated.
Resource Resource `json:"resource"`
Action Action `json:"action"`
// Allowed reports whether the request was permitted by policy.
// Always populated.
Allowed bool `json:"allowed"`
// Latency is the wall-clock time from request arrival to the enforcement
// decision, including time spent in the rate limiter. Always populated.
Latency time.Duration `json:"latency"`
// PolicyLatency is the wall-clock time spent in policy evaluation only,
// excluding rate-limiter time. Always populated.
PolicyLatency time.Duration `json:"policy_latency"`
// PolicyVersion is the version identifier of the policy bundle active
// during evaluation, as reported by the policy engine. Optional; empty if
// the engine does not expose version information.
PolicyVersion string `json:"policy_version,omitempty"`
// Reason is the human-readable explanation returned by the policy engine.
// Optional; empty when the engine does not provide one.
Reason string `json:"reason,omitempty"`
// AuditMetadata contains arbitrary key-value pairs emitted by the policy
// engine for audit purposes. Optional; nil when not provided. Not currently set.
AuditMetadata map[string]any `json:"audit_metadata,omitempty"`
// Err is set when policy evaluation itself failed (as opposed to returning
// an explicit denial). Optional; nil for normal allowed or denied decisions.
// Providers should serialize this as err.Error() if they need a string
// representation; the field is excluded from default JSON marshaling.
Err error `json:"-"`
}
DecisionEvent is emitted by the framework for every request that reaches policy evaluation, whether the request was ultimately allowed or denied. It is the tamper-evident decision audit record required by Section 7.
type DeregisterRequest ¶
type DeregisterRequest struct {
// SPIFFEID is the SPIFFE ID under which this endpoint was registered.
SPIFFEID string
// Address is the host:port to remove. The framework uses the same
// address that was passed to [RegisterRequest.Endpoint.Address].
Address string
}
DeregisterRequest is the argument to [ServiceRegistrar.Deregister].
type EjectionEvent ¶
type EjectionEvent struct {
// Timestamp is the time the ejection or reinstatement was recorded.
// Always populated.
Timestamp time.Time `json:"timestamp"`
// Endpoint is the affected endpoint. Always populated.
Endpoint Endpoint `json:"endpoint"`
// Ejected is true when the endpoint was ejected, false when reinstated.
Ejected bool `json:"ejected"`
// EjectionCount is the total number of times this endpoint has been
// ejected, including this event. Used to compute the exponential backoff
// duration. Zero on reinstatement.
EjectionCount int `json:"ejection_count,omitempty"`
// Duration is the length of the ejection window. Zero on reinstatement.
Duration time.Duration `json:"duration,omitempty"`
}
EjectionEvent is emitted by the OutlierDetector when an endpoint is ejected from or reinstated into the load-balancing pool.
type Endpoint ¶
type Endpoint struct {
// Address is the host:port to connect to.
Address string
// HealthAddr is the host:port of the plain-HTTP health endpoint (/healthz).
// Empty when the instance does not expose a health signal.
HealthAddr string
// SPIFFEID is the SPIFFE ID the endpoint's TLS certificate must present.
// The framework verifies this before establishing a connection; a mismatch
// is treated as a refused connection regardless of TLS handshake outcome.
SPIFFEID string
// Locality groups endpoints by failure domain. Used by locality-aware
// load-balancing algorithms to prefer same-zone endpoints.
Locality Locality
// Metadata carries arbitrary key-value pairs from the registry. Consumers
// must not assume any specific keys are present.
Metadata map[string]string
}
Endpoint is a single healthy instance of a logical service that the framework may route outbound calls to.
type EndpointSelector ¶
type EndpointSelector interface {
// ForPool returns a [PoolSelector] bound to the endpoint set in req.
// The caller owns the returned PoolSelector and must Close it when the
// endpoint set is replaced.
ForPool(ctx context.Context, req *ForPoolRequest) PoolSelector
io.Closer
}
EndpointSelector is a factory that creates per-pool selectors. Configured as [Config.EndpointSelector]. Implementations embed any combination of ejection algorithm, load balancing policy, and concurrency limiting.
Why a factory: per-pool state (round-robin position, in-flight counters, consecutive error counts, ejection timers) lives exactly as long as the endpoint set. When the set changes, the old PoolSelector is closed and a fresh one starts clean — which is what blue/green deployment requires. A single stateful selector would need complex delta reconciliation on endpoint set changes; the factory eliminates that entirely.
type EndpointUpdate ¶
type EndpointUpdate struct {
// Endpoints is the new complete set of healthy endpoints. Replaces any
// previously received set.
Endpoints []Endpoint
// Err is set when the registry encountered an error delivering the update.
// Consumers should continue watching; the registry will recover and send
// a fresh update when connectivity is restored.
Err error
}
EndpointUpdate is delivered on the channel returned by [ServiceRegistry.WatchEndpoints] whenever the endpoint set changes.
type EnforceFunc ¶
EnforceFunc is the enforcement pipeline entry point passed to Binding implementations via BindingArgs. It has the same signature as [Server.Enforce]: call it once per inbound request, use the returned context for the application handler (it carries the inbound chain and, when present, the Transaction Token), and translate the returned error to the appropriate protocol-level denial.
type EvalRequest ¶
type EvalRequest struct {
// Chain is the ordered sequence of verified principals that have handled
// the request before it reached this service. The policy engine may
// evaluate any or all links when making an authorization decision.
// The immediate caller is the last link whose Workload field is non-empty.
// See the chain package for the full identity model.
Chain Chain
// Resource identifies the resource being accessed.
Resource Resource
// Action identifies the operation being performed on the resource.
Action Action
// PIPs holds signal data contributed by Policy Information Points before
// policy evaluation. Reserved for future PIP composition; always nil in the
// current implementation.
PIPs map[string]any
}
EvalRequest is the request type for [PolicyEngine.Evaluate].
type EvalResponse ¶
type EvalResponse struct {
// Allowed indicates whether the request is permitted by policy.
// The framework denies the request if Allowed is false.
Allowed bool
// Reason is a human-readable explanation of the policy decision, intended
// for operator logs and audit records. It is empty when the policy engine
// does not provide one.
Reason string
// PolicyVersion is the version identifier of the policy bundle that was
// active during evaluation. Empty when the engine does not expose version
// information. The framework records this in every [DecisionEvent].
PolicyVersion string
// AuditMetadata contains arbitrary key-value pairs emitted by the policy
// engine for audit and observability. The framework records these alongside
// the decision but does not interpret them. Nil when not provided. Not currently set.
AuditMetadata map[string]any
}
EvalResponse is the response type for [PolicyEngine.Evaluate].
type EventStream ¶
type EventStream[T any] <-chan T
EventStream carries events that must all be processed in order. No events may be discarded. Consume with range. The channel is closed when the watch ends or the context passed to the Watch method is cancelled.
type FetchBundleRequest ¶
type FetchBundleRequest struct {
// KnownETag is the ETag of the last bundle fetched by this caller. The
// server may return a response with the same ETag and nil Bundle to
// indicate no change. Empty on the first call.
KnownETag string
}
FetchBundleRequest is the request type for [BundleFetcher.Fetch].
type FetchBundleResponse ¶
type FetchBundleResponse struct {
// Bundle contains the raw bundle bytes (typically an OPA bundle tar.gz).
// Nil when the bundle has not changed since KnownETag.
Bundle []byte
// ETag is the version identifier for this bundle, suitable for use as
// KnownETag in a subsequent request. Empty if the server does not support
// conditional fetches.
ETag string
// Version is a human-readable bundle version string for logs and
// telemetry. Empty if the server does not expose version metadata.
Version string
}
FetchBundleResponse is the response type for [BundleFetcher.Fetch].
type FetchWITRequest ¶
type FetchWITRequest struct {
// Audience is the intended recipient's SPIFFE ID or URI.
Audience string
// CNFPublicKey is the workload's public key to embed in the WIT's
// cnf.jwk claim. The Identity Server signs the WIT but never holds the
// corresponding private key; the workload retains it for WPT signing.
CNFPublicKey crypto.PublicKey
}
FetchWITRequest is the request type for [WorkloadIdentity.FetchWIT].
type FetchWITResponse ¶
type FetchWITResponse struct {
// WIT is the signed Workload Identity Token JWT string, ready to send
// in the Workload-Identity-Token HTTP header.
WIT string
// ExpiresAt is when the WIT expires, derived from the JWT exp claim.
ExpiresAt time.Time
}
FetchWITResponse is the response type for [WorkloadIdentity.FetchWIT].
type FetchX509ContextRequest ¶
type FetchX509ContextRequest struct{}
FetchX509ContextRequest is the request type for [WorkloadIdentity.FetchX509Context].
type FetchX509ContextResponse ¶
type FetchX509ContextResponse struct {
// SVIDs is the list of X.509 SVIDs issued to this workload. There is
// typically one, but a workload may hold multiple — for example during
// key-rotation overlap or when operating across multiple trust domains.
SVIDs []*X509SVID
// TrustBundles maps each trust domain name to its CA certificates.
// A workload must include at least its own trust domain; federation
// requires entries for each federated domain.
TrustBundles map[string][]*x509.Certificate
// JWTAuthorities maps each trust domain name to the public keys that
// its Identity Server uses to sign Workload Identity Tokens (WITs).
// These are the SPIFFE trust bundle's jwt_authorities, populated by the
// Identity Server (SPIRE) once it implements draft-ietf-wimse-workload-creds
// (tracked in SPIFFE issue #315). Until then, providers may return nil.
// [FakeWorkloadIdentity] populates this with the SVID public key
// so that the full WIT/WPT verification path is exercisable in tests.
JWTAuthorities map[string][]crypto.PublicKey
}
FetchX509ContextResponse is the response type for [WorkloadIdentity.FetchX509Context] and the value carried by X509ContextUpdate.
type ForPoolRequest ¶
type ForPoolRequest struct {
// SPIFFEID is the SPIFFE ID of the service pool being created.
SPIFFEID string
// Endpoints is the fixed set of endpoints for this pool.
Endpoints []Endpoint
}
ForPoolRequest is the argument to [EndpointSelector.ForPool].
type GetSecretRequest ¶
type GetSecretRequest struct {
// Name is the full backend path for the secret, e.g.
// "secret/data/myapp/db-password" for a Vault/OpenBao KV v2 mount.
Name string
}
GetSecretRequest is the request type for [Secrets.Get].
type GetSecretResponse ¶
type GetSecretResponse struct {
// Value is the raw secret bytes returned by the backend.
Value []byte
// TTL is how long the framework should consider this value valid. The
// framework schedules a proactive renewal before TTL expires. A zero TTL
// means the backend did not specify an expiry; the framework treats it as
// a non-renewable secret and does not schedule renewal.
TTL time.Duration
// LeaseID is an opaque backend identifier used by the framework to match
// revocation events from [Secrets.WatchRevocations] to cached
// entries. Providers set this to whatever token the backend uses to
// identify the lease (e.g. a Vault lease ID). Empty means the backend
// does not issue leases for this secret.
LeaseID string
}
GetSecretResponse is the response type for [Secrets.Get].
This type is the provider-to-framework communication layer. Application code never holds a GetSecretResponse directly — they receive only the raw bytes via Server.GetSecret. All fields are therefore safe to export for use by provider implementations in other packages.
type GracefulDrainer ¶
type GracefulDrainer interface {
// Wait blocks until a drain signal is received or ctx is cancelled.
// It returns nil when a signal is received and the framework should begin
// draining. It returns ctx.Err() when ctx is cancelled.
Wait(ctx context.Context) error
io.Closer
}
GracefulDrainer signals to the framework that a shutdown is imminent, triggering orderly connection draining before the process exits. The framework stops accepting new connections and waits for in-flight requests to complete within a configured deadline.
type HealthChecker ¶
HealthChecker probes an endpoint to determine whether it is healthy and eligible to receive traffic. Results feed the OutlierDetector and the ServiceRegistry's view of available endpoints.
type HealthReport ¶
type HealthReport struct {
// SPIFFEID is the SPIFFE ID under which this endpoint is registered.
SPIFFEID string
// Address is the host:port of the endpoint to update.
Address string
// Healthy controls whether this endpoint appears in [ServiceRegistry.Resolve]
// results. When false the endpoint is removed from the registry but the
// backend lease is kept alive for fast recovery. The framework calls
// ReportHealth with Healthy false during graceful shutdown.
Healthy bool
}
HealthReport is the argument to [ServiceRegistrar.ReportHealth].
type HealthSignalExposer ¶
type HealthSignalExposer interface {
// Serve starts the exposer and blocks until ctx is cancelled or [io.Closer]
// is called. It returns nil when stopped cleanly and a non-nil error for
// unexpected failures.
Serve(ctx context.Context) error
// Listening returns a channel that is closed once the exposer's listener is
// bound and its address is known. [Init] blocks on this channel so that
// [HealthSignalExposer.Addr] is guaranteed to return the final address before
// Init returns.
Listening() <-chan struct{}
// Addr returns the host:port the exposer is listening on. It blocks until
// [Listening] is closed. Returns "" for implementations that do not listen
// on a network address.
Addr() string
io.Closer
}
HealthSignalExposer exposes the framework's aggregate health state to external observers. The most common implementation is an HTTP endpoint used as a Kubernetes readiness or liveness probe.
All methods are required. Embed UnimplementedHealthSignalExposer to remain forward-compatible if optional methods are added in the future.
type HealthSignalExposerArgs ¶
type HealthSignalExposerArgs struct {
// Monitor is the framework health monitor. Call [Monitor.Overall] to
// determine the aggregate health state to expose to external observers.
Monitor *Monitor
}
HealthSignalExposerArgs holds the ligand-internal values that HealthSignalExposerFactory implementations receive from Init. Only values that are contractually guaranteed to be present when Init calls the factory are included; adding a field to this struct is a considered API decision because it constrains the construction order inside Init.
type HealthSignalExposerFactory ¶
type HealthSignalExposerFactory func(*HealthSignalExposerArgs) HealthSignalExposer
HealthSignalExposerFactory is a constructor called by Init after the framework monitor is created. It returns a ready-to-use HealthSignalExposer. The HealthSignalExposerArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
Implementations should expose a factory function alongside their concrete constructor; see package httphealth for the canonical example.
type HealthTransitionEvent ¶
type HealthTransitionEvent struct {
// Timestamp is the time the transition was recorded by the monitor.
// Always populated.
Timestamp time.Time `json:"timestamp"`
// Kind identifies the provider whose state changed. Always populated.
Kind ProviderKind `json:"kind"`
// From is the state before the transition; To is the state after.
// Always populated.
From State `json:"from"`
To State `json:"to"`
// Err is the error that caused the transition. Optional; nil for normal
// lifecycle transitions (e.g. Initializing → Ready on startup).
// Providers should serialize this as err.Error() if they need a string
// representation; the field is excluded from default JSON marshaling.
Err error `json:"-"`
}
HealthTransitionEvent is emitted when a provider's health state changes.
type InboundRequest ¶
type InboundRequest struct {
// Chain is the upstream principal chain propagated from callers that
// handled the request before the immediate peer. May be empty if the
// peer did not propagate chain context.
//
// For [Server.EnforceInbound] callers: populate this field directly with
// whatever identity signals are available for the originating caller.
// [Server.EnforceInbound] uses Chain as-is without appending a peer link.
Chain Chain
// Peer is the immediate upstream caller, represented as a [Link].
// [Binding] implementations set Peer from the mTLS peer certificate:
//
// Peer: ligand.Link{Workload: spiffeID}
//
// [Server.Enforce] appends Peer to Chain to form the evaluation chain
// delivered to rate limiting and policy. Leave Peer as the zero [Link]{}
// when calling [Server.EnforceInbound] — that method uses Chain directly.
Peer Link
// Resource and Action describe what the caller is requesting.
Resource Resource
Action Action
// SubjectToken is an OAuth 2.0 access token extracted by the binding from
// the inbound request (e.g. "Authorization: Bearer <token>"). Present only
// at trust-domain entry points where an external caller presents a bearer
// token instead of a client TLS certificate. The framework exchanges it for
// a [TransactionToken] when the request path matches a configured
// [TrustBoundaryConfig.Paths] entry and a [ContextServiceClient] is
// configured. Application code must never read this field directly.
SubjectToken string
// TransactionToken is a signed JWT forwarded from an upstream service leg,
// extracted by the binding from the inbound request. The framework verifies
// its signature via [ContextServiceClient.VerifyTransactionToken]; on
// failure it is cleared (fail-safe) and the request continues without it.
// Application code must never read this field directly.
TransactionToken string
}
InboundRequest carries the per-request metadata extracted by a Binding from an inbound connection and passed to [Server.Enforce].
type IsEjectedRequest ¶
type IsEjectedRequest struct {
// Endpoint is the endpoint to check.
Endpoint *Endpoint
}
IsEjectedRequest is the argument to [OutlierDetector.IsEjected].
type JTICache ¶
type JTICache struct {
// contains filtered or unexported fields
}
JTICache tracks JWT ID (jti) values to detect replay attacks within a token's validity window. Entries are pruned when their exp time has passed — once a token expires its replay cannot succeed via the exp check, so there is no security benefit to retaining old entries indefinitely.
func NewJTICache ¶
NewJTICache returns a new JTICache backed by clock.
func (*JTICache) Seen ¶
Seen returns true if jti has already been recorded and its exp has not yet passed, indicating a replay. Otherwise it records the jti and returns false. Up to maxPrunePerSeen expired entries are pruned per call; expiry is also checked at lookup time so uncapped stale entries never cause false replays.
type LatestRecv ¶
type LatestRecv[T any] <-chan T
LatestRecv is the consumer side of a latest-value channel. Only the most recent value matters; intermediate values may be safely discarded. Consume with Latest. The channel is closed when the watch ends or the context passed to the Watch method is cancelled.
type LatestSend ¶
type LatestSend[T any] chan T
LatestSend is the publisher side of a latest-value channel. Publish with Set, which evicts any unseen prior value before sending. Return it to consumers as LatestRecv[T].
type Link ¶
type Link struct {
// Workload is the SPIFFE ID of the workload (microservice, batch job,
// serverless function, CI/CD step, or AI agent) that handled this hop.
// Empty if this hop did not originate from a workload.
Workload string
// User is the IdP subject string of the human principal associated with
// this hop (e.g. an email address or an opaque subject claim). Empty if no
// user identity is associated with this hop.
User string
// Device is the hardware or hypervisor-level attestation identity of the
// execution environment that handled this hop. Rooted in hardware
// attestation where possible (e.g. TPM). Empty if not attested.
// Distinct from [Link.Host]: a device may have a valid hardware identity
// while its host OS is out of compliance, and policy must evaluate each
// independently.
Device string
// Host is the OS and software-stack identity of the host running on the
// [Link.Device] at the time of this hop — the specific kernel, OS
// configuration, and installed software. Empty if not attested.
// Unlike device identity, host identity is mutable: it changes across
// reboots, OS updates, and configuration changes.
Host string
// ExternalParty is the credential subject of an entity outside the
// organisation's direct control (a third-party SaaS vendor, a customer
// system, a partner API, or an external service). Empty if this hop is not
// from an external party.
ExternalParty string
}
Link represents one hop in the request Chain — a single principal that has handled the request. Each field captures a distinct identity dimension for that hop, corresponding to the identity categories in §3.3. Fields are empty strings when the identity was not attested or is not applicable to the hop.
All fields are singular. The multi-principal case (e.g. a support agent acting on behalf of a customer) is a known limitation of this model; it will be revisited when the WIMSE standard matures.
MAINTENANCE: When adding support for a new identity dimension, add a field here and update pipeline in serve.go to populate it. AI agents are currently represented via the Workload field per §3.3 (non-adoption note).
type LoadBalancer ¶
type LoadBalancer interface {
// Select chooses one endpoint from req.Candidates for the current request.
// Returns an error if candidates is empty or selection fails. Must not
// modify the candidates slice.
Select(ctx context.Context, req *SelectRequest) (*SelectResponse, error)
// RecordOutcome informs the load balancer of the result of a call to the
// previously selected endpoint. Algorithms that track in-flight count
// (e.g. least-request) use this to decrement their counter.
//
// RecordOutcome is notification-style: it has no return value and must
// not block. Embed [UnimplementedLoadBalancer] to receive a no-op
// implementation for algorithms that do not need outcome feedback.
RecordOutcome(ctx context.Context, req *RecordLoadBalancerOutcomeRequest)
io.Closer
}
LoadBalancer selects an endpoint from the set of candidates supplied by the ServiceRegistry for each outbound call.
type Locality ¶
type Locality struct {
// Zone is the availability zone within Region (e.g. "us-east-1a").
Zone string
// Region is the geographic region (e.g. "us-east-1"). Empty when
// locality-aware load balancing is not in use.
Region string
}
Locality describes the physical or logical placement of an Endpoint.
type MetricsSnapshot ¶
type MetricsSnapshot struct {
// Timestamp is the time the snapshot was collected. Always populated.
Timestamp time.Time `json:"timestamp"`
// SVIDAge is the wall-clock age of the current SVID leaf certificate,
// computed as clock.Now() minus the certificate's NotBefore time.
// Zero when the SVID has not yet been loaded.
SVIDAge time.Duration `json:"svid_age,omitempty"`
// SVIDTimeToNextRotation is the remaining valid lifetime of the current
// SVID leaf certificate, computed as the certificate's NotAfter time
// minus clock.Now(). Zero when the SVID has not yet been loaded.
SVIDTimeToNextRotation time.Duration `json:"svid_time_to_next_rotation,omitempty"`
// PolicyBundleVersion is the version identifier of the most recently
// loaded policy bundle, as reported by the bundle server. Empty when no
// [Config.BundleFetcher] is configured or no bundle has been loaded yet.
PolicyBundleVersion string `json:"policy_bundle_version,omitempty"`
// PolicyBundleAge is the wall-clock time since the current policy bundle
// was loaded or reloaded. Zero when no [Config.BundleFetcher] is configured.
PolicyBundleAge time.Duration `json:"policy_bundle_age,omitempty"`
// PolicyEvalP50, PolicyEvalP95, PolicyEvalP99 are the 50th, 95th, and
// 99th percentile policy evaluation latencies computed over a rolling
// window of the most recent 1000 evaluations. Zero until at least one
// evaluation has been recorded.
PolicyEvalP50 time.Duration `json:"policy_eval_p50,omitempty"`
PolicyEvalP95 time.Duration `json:"policy_eval_p95,omitempty"`
PolicyEvalP99 time.Duration `json:"policy_eval_p99,omitempty"`
// SecretsCacheHits is the cumulative number of [Server.GetSecret] calls
// served from the in-process secrets cache since [Init]. Zero when no
// [Config.Secrets] is configured.
SecretsCacheHits int64 `json:"secrets_cache_hits,omitempty"`
// SecretsCacheMisses is the cumulative number of [Server.GetSecret] calls
// that required a synchronous fetch from the secrets backend since [Init].
// Zero when no [Config.Secrets] is configured.
SecretsCacheMisses int64 `json:"secrets_cache_misses,omitempty"`
}
MetricsSnapshot carries a point-in-time snapshot of aggregated framework health metrics, delivered to [Telemetry.RecordMetrics] at each [Config.MetricsInterval] tick. Fields are zero when the corresponding subsystem is not configured or has not yet produced data.
type MissingRequiredProvidersError ¶
type MissingRequiredProvidersError struct {
// Missing lists each required [ProviderKind] that was nil in the Config.
Missing []ProviderKind
}
MissingRequiredProvidersError is returned by Init when one or more required providers are nil in the supplied Config. All missing providers are reported at once so the caller can fix every gap in a single iteration.
func (*MissingRequiredProvidersError) Error ¶
func (e *MissingRequiredProvidersError) Error() string
type Monitor ¶
type Monitor struct {
// contains filtered or unexported fields
}
Monitor tracks the health state of all framework providers and derives an aggregate service state. It is safe for concurrent use.
All provider states start as Initializing. Required providers that become Unavailable cause the overall state to become Unavailable; required providers that are still Initializing cause it to remain Initializing. A required provider may be Degraded during a grace window after a transient error — the service continues accepting traffic but reports Degraded. Optional providers that become Unavailable or Degraded cause the overall state to become Degraded but do not prevent traffic.
func NewMonitor ¶
NewMonitor returns a new Monitor with all providers in the Initializing state.
func (*Monitor) Overall ¶
Overall returns the aggregate service health state derived from all provider states.
Priority order: Unavailable > Initializing > Draining > Degraded > Ready.
func (*Monitor) Provider ¶
func (m *Monitor) Provider(kind ProviderKind) (State, bool)
Provider returns the current state of the given provider kind, and whether kind is valid. Returns Initializing and false for unknown kinds.
func (*Monitor) Set ¶
Set updates the health state of the given provider kind. cause is the error responsible for the transition, or nil for normal lifecycle transitions (e.g. Initializing → Ready on startup). If the state changes, or there is an error, [Telemetry.RecordHealthTransition] is called synchronously after the lock is released.
type NoopRateLimiter ¶
type NoopRateLimiter struct{}
NoopRateLimiter is a RateLimiter that allows all requests and does nothing on Close. It is used as the default when [Config.RateLimiter] is nil and may be used directly in tests or development environments where rate limiting is not yet needed.
func (NoopRateLimiter) Allow ¶
func (NoopRateLimiter) Allow(_ context.Context, _ *RateLimitRequest) (*RateLimitResponse, error)
Allow always returns Allowed: true.
type NoopTelemetry ¶
type NoopTelemetry struct{}
NoopTelemetry is a Telemetry that silently discards all events and metrics. It is used as the default when [Config.Telemetry] is nil and may be used directly in tests or environments where observability is not needed.
func (NoopTelemetry) RecordBundleFetch ¶
func (NoopTelemetry) RecordBundleFetch(_ context.Context, _ *BundleFetchEvent) error
RecordBundleFetch discards the event and returns nil.
func (NoopTelemetry) RecordCircuitState ¶
func (NoopTelemetry) RecordCircuitState(_ context.Context, _ *CircuitStateEvent) error
RecordCircuitState discards the event and returns nil.
func (NoopTelemetry) RecordDecision ¶
func (NoopTelemetry) RecordDecision(_ context.Context, _ *DecisionEvent) error
RecordDecision discards the event and returns nil.
func (NoopTelemetry) RecordEjection ¶
func (NoopTelemetry) RecordEjection(_ context.Context, _ *EjectionEvent) error
RecordEjection discards the event and returns nil.
func (NoopTelemetry) RecordHealthTransition ¶
func (NoopTelemetry) RecordHealthTransition(_ context.Context, _ *HealthTransitionEvent) error
RecordHealthTransition discards the event and returns nil.
func (NoopTelemetry) RecordMetrics ¶
func (NoopTelemetry) RecordMetrics(_ context.Context, _ *MetricsSnapshot) error
RecordMetrics discards the snapshot and returns nil.
func (NoopTelemetry) RecordOutboundCall ¶
func (NoopTelemetry) RecordOutboundCall(_ context.Context, _ *OutboundCallEvent) error
RecordOutboundCall discards the event and returns nil.
func (NoopTelemetry) RecordRateLimit ¶
func (NoopTelemetry) RecordRateLimit(_ context.Context, _ *RateLimitEvent) error
RecordRateLimit discards the event and returns nil.
type OutboundCallEvent ¶
type OutboundCallEvent struct {
// Timestamp is the time the outbound call was initiated.
// Always populated.
Timestamp time.Time `json:"timestamp"`
// LocalSPIFFEID is the SPIFFE ID of the workload making the call.
// Always populated.
LocalSPIFFEID string `json:"local_spiffe_id"`
// DestinationSPIFFEID is the SPIFFE ID of the target service, as passed
// to [OutboundTransport.Transport] and verified against both the registry
// and the peer TLS certificate. Always populated.
DestinationSPIFFEID string `json:"destination_spiffe_id"`
// Method is the request method (e.g. "GET", "POST" for HTTP).
// May be empty if the call was refused before a request was sent.
Method string `json:"method,omitempty"`
// Outcome describes the result of the call. Always populated.
Outcome OutboundOutcome `json:"outcome"`
// Latency is the wall-clock time from call initiation to completion.
// Always populated.
Latency time.Duration `json:"latency"`
// Err is the error returned by the outbound call, if any. Optional; nil
// for successful calls. Excluded from default JSON marshaling.
Err error `json:"-"`
}
OutboundCallEvent is emitted by the OutboundTransport on every outbound service call. Aggregated fleet-wide, these records form the service call graph required by Section 7 (Structured Telemetry).
type OutboundOutcome ¶
type OutboundOutcome string
OutboundOutcome describes the result of an outbound service call emitted in an OutboundCallEvent.
const ( // OutboundOutcomeSuccess indicates the call completed without a // connection-level error and the response status was below 5xx. OutboundOutcomeSuccess OutboundOutcome = "success" // OutboundOutcomeError indicates the call failed due to a connection-level // error, timeout, or a 5xx response status. OutboundOutcomeError OutboundOutcome = "error" // OutboundOutcomeRefused indicates the call was rejected before a // connection was established — for example, because the endpoint's // SPIFFE ID did not match the registry record, or the outbound transport // is shutting down. OutboundOutcomeRefused OutboundOutcome = "refused" )
type OutboundTransport ¶
type OutboundTransport interface {
// Shutdown gracefully drains in-flight outbound calls within the deadline
// imposed by ctx, then stops accepting new ones. Called by the framework
// between [Binding.Shutdown] and SVID rotation cancellation so that live
// outbound requests still see a valid certificate during drain. Symmetric
// with [Binding.Shutdown].
Shutdown(ctx context.Context) error
io.Closer
}
OutboundTransport manages the lifecycle of outbound service connectivity: mTLS connection management, service discovery integration, and protocol-level security (SPIFFE ID verification, WPT/chain attachment).
The method for obtaining a transport handle is intentionally NOT in this interface, because different protocol implementations return incompatible types (http.RoundTripper for HTTP, *grpc.ClientConn for gRPC). Each concrete implementation exposes its own typed method:
// HTTP implementation: func (p *Provider) Transport(ctx context.Context, spiffeID string) (http.RoundTripper, error)
Callers that need the concrete implementation use [Capture] on the provider:
var p *outboundhttp.Provider
srv, _ := ligand.Init(ctx, &ligand.Config{
Outbound: outboundhttp.Capture(nil, &p),
})
rt, _ := p.Transport(ctx, "my-service")
client := &http.Client{Transport: rt}
type OutboundTransportArgs ¶
type OutboundTransportArgs struct {
// OutboundTLSConfig is the framework-managed *tls.Config for
// outbound mTLS connections. The returned config reads [tlsState]
// atomically so SVID and trust-bundle rotations take effect for new
// connections without restart.
OutboundTLSConfig *tls.Config
// GetSnapshot returns an atomic snapshot of the workload's current SVID
// material. Callers must invoke it once per operation and read all needed
// fields from the returned value to guarantee consistency across a rotation.
GetSnapshot func() SVIDSnapshot
// ServiceRegistry resolves logical service names to endpoint sets.
// Populated when [Config.ServiceRegistry] is non-nil.
ServiceRegistry ServiceRegistry
// EndpointSelector creates per-pool selectors that combine load balancing,
// outlier detection, and concurrency limiting. Nil when
// [Config.EndpointSelector] is not set; the transport uses basic resolution
// without ejection or concurrency limiting in that case.
EndpointSelector EndpointSelector
// WPTSigner builds WIT and WPT tokens for outbound WIMSE authentication.
// Always populated when the factory is called.
WPTSigner *WPTSigner
// Telemetry receives structured events emitted by the outbound transport
// (e.g. [OutboundCallEvent]). Never nil; defaults to [NoopTelemetry].
Telemetry Telemetry
// Clock is the time source used for token expiry and latency measurement.
// Never nil; defaults to [quartz.NewReal].
Clock quartz.Clock
// CircuitBreaker stops calls to a service that has exceeded its error
// threshold. Nil when [Config.CircuitBreaker] is not set; the transport
// skips circuit-breaking in that case.
CircuitBreaker CircuitBreaker
// RetryBudget gates retry attempts with a sliding-window budget. Nil means
// no budget cap; retries are bounded only by [RetryConfig.MaxAttempts].
RetryBudget RetryBudget
// RetryPolicy looks up the retry configuration for a named service.
// Nil means no retry for any service.
RetryPolicy RetryPolicyProvider
// Logger receives structured log events. Never nil.
Logger *slog.Logger
}
OutboundTransportArgs holds the ligand-internal values that OutboundTransportFactory implementations receive from Init.
type OutboundTransportFactory ¶
type OutboundTransportFactory func(*OutboundTransportArgs) OutboundTransport
OutboundTransportFactory is a constructor called by Init after workload identity and TLS are established. It returns a ready-to-use OutboundTransport. The OutboundTransportArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
type OutlierDetector ¶
type OutlierDetector interface {
// RecordOutcome reports the result of an outbound call to req.Endpoint.
// The detector uses this signal to decide whether to eject the endpoint.
RecordOutcome(ctx context.Context, req *RecordOutlierOutcomeRequest)
// IsEjected reports whether req.Endpoint is currently ejected and should
// be excluded from load-balancer selection. An ejected endpoint whose
// ejection duration has expired is considered reinstated.
IsEjected(ctx context.Context, req *IsEjectedRequest) bool
// EjectedEndpoints returns the current set of ejected endpoints whose
// ejection duration has not yet expired. Used for observability and by
// active health-check loops.
EjectedEndpoints(ctx context.Context) []Endpoint
io.Closer
}
OutlierDetector identifies endpoints that are performing significantly worse than their peers and ejects them from the load balancing pool until they recover.
The OutboundTransport calls [OutlierDetector.RecordOutcome] after every outbound call and filters ejected endpoints via [OutlierDetector.IsEjected] before load-balancer selection. Ejections are time-limited with exponential backoff; endpoints auto-reinstate when their ejection duration expires.
All three methods are optional in enforcement semantics: if no OutlierDetector is configured, the framework skips outlier detection silently. Implementations that do not need all three methods should embed UnimplementedOutlierDetector.
type PolicyEngine ¶
type PolicyEngine interface {
// Evaluate evaluates policy for a single inbound request. The framework
// calls Evaluate after rate limiting and identity verification have
// completed. If Evaluate returns an error or EvalResponse.Allowed is
// false, the request is denied.
Evaluate(ctx context.Context, req *EvalRequest) (*EvalResponse, error)
// Reload loads a new policy bundle into the engine. The framework calls
// Reload whenever [BundleFetcher] delivers a new bundle version, passing
// the raw bundle bytes received from the fetcher. In-flight [Evaluate]
// calls complete against the current policy; subsequent calls use the
// newly loaded policy.
//
// The framework treats [ErrNotSupported] as a misconfiguration error when
// a [BundleFetcher] is configured: Init will return an error if the
// engine does not support Reload but a fetcher is present.
//
// Optional: embed [UnimplementedPolicyEngine] if not supported.
Reload(ctx context.Context, bundle []byte) error
// WatchRevocations returns an [EventStream] that delivers a
// [RevocationUpdate] each time the policy engine detects that a
// previously-issued credential has been actively revoked. The policy
// engine is the source of revocation events because it has direct
// visibility into the policy store; the framework is the consumer and
// uses the stream to invalidate cached decisions. The channel is closed
// when ctx is cancelled. Every event must be processed; use range to
// consume.
//
// Optional: embed [UnimplementedPolicyEngine] if not supported. The
// framework falls back to TTL-based expiry on [ErrNotSupported].
WatchRevocations(ctx context.Context) EventStream[RevocationUpdate]
io.Closer
}
PolicyEngine evaluates access control policy for inbound requests. Policy evaluation is fail-closed: if the engine is unavailable or returns an error, the request is denied.
Required methods: [PolicyEngine.Evaluate] and io.Closer.
Optional methods: [PolicyEngine.Reload] and [PolicyEngine.WatchRevocations]. Providers that do not support these should embed UnimplementedPolicyEngine, which returns ErrNotSupported. The framework applies conservative safe defaults: no hot reload (TTL expiry only), no active revocation push.
type PoolSelector ¶
type PoolSelector interface {
// Pick selects one endpoint. Implementations may filter ejected endpoints,
// enforce concurrency limits, and apply any load balancing algorithm.
//
// [ErrNoEndpoints] is returned when no healthy endpoint is available.
// [ErrBulkheadFull] is returned when healthy endpoints exist but the
// concurrency limit is reached.
Pick(ctx context.Context) (Endpoint, error)
// Release records the outcome and releases any concurrency resources
// (semaphore slots, in-flight counters) acquired by Pick. Must be called
// after every successful Pick regardless of call outcome. No-op after Close.
Release(ctx context.Context, req *ReleaseRequest)
io.Closer
}
PoolSelector handles endpoint selection for one fixed endpoint set. Lifetime matches the endpoint set: created by [EndpointSelector.ForPool], closed when the set changes. Release on a closed pool is a no-op so that in-flight calls completing after a pool replacement do not corrupt new-pool state.
type ProviderKind ¶
type ProviderKind uint8
ProviderKind identifies a pluggable provider tracked by the health monitor.
MAINTENANCE: When adding a new pluggable interface to the framework, add a corresponding Kind constant to the required or optional block below. When changing a provider's required/optional status, move its constant across the numRequiredKinds boundary and update its doc comment. Failure to do so will cause the new provider to be silently ignored by the health monitor.
const ( KindIdentity ProviderKind = iota // WorkloadIdentity KindPolicy // PolicyEngine KindBinding // Binding )
Required provider kinds. The service denies all traffic while any required provider is not Ready. Add new required kinds above numRequiredKinds.
const ( KindBundleFetcher ProviderKind = numRequiredKinds + iota // BundleFetcher KindRateLimiter // RateLimiter KindTelemetry // Telemetry KindSecrets // Secrets KindServiceRegistry // ServiceRegistry KindServiceRegistrar // ServiceRegistrar KindSession // SessionStore KindDrain // GracefulDrainer KindEndpointSelector // EndpointSelector KindRetryBudget // RetryBudget KindRetryPolicy // RetryPolicyProvider KindCircuitBreaker // CircuitBreaker KindHealthChecker // HealthChecker KindHealthSignalExposer // HealthSignalExposer KindOutbound // OutboundTransport KindContextServiceClient // ContextServiceClient )
Optional provider kinds. The service becomes Degraded but continues accepting traffic when an optional provider is Unavailable. Add new optional kinds above numKinds.
func (ProviderKind) String ¶
func (k ProviderKind) String() string
String returns a human-readable name for the provider kind.
type RateLimitEvent ¶
type RateLimitEvent struct {
// Timestamp is the time the request arrived at the enforcement pipeline.
// Always populated.
Timestamp time.Time `json:"timestamp"`
// Chain is the identity chain as received at the enforcement boundary,
// with the immediate peer as the last element. Always populated; contains
// at least the immediate peer link.
Chain Chain `json:"chain"`
// Resource and Action identify the access being requested. Always populated.
Resource Resource `json:"resource"`
Action Action `json:"action"`
// Allowed reports whether the rate limiter permitted the request.
// Always populated.
Allowed bool `json:"allowed"`
// Latency is the wall-clock time spent in the rate limiter. Always populated.
Latency time.Duration `json:"latency"`
// CurrentRPS is the observed request rate for the calling identity over
// the current window, as reported by the rate limiter. Optional; zero
// when the implementation does not report utilisation.
CurrentRPS float64 `json:"current_rps,omitempty"`
// LimitRPS is the configured rate limit applied to the calling identity.
// Optional; zero when the implementation does not report the configured limit.
LimitRPS float64 `json:"limit_rps,omitempty"`
// Err is set when the rate limiter returned an error rather than a normal
// allow or deny decision. Optional; nil for normal outcomes.
// Providers should serialize this as err.Error() if they need a string
// representation; the field is excluded from default JSON marshaling.
Err error `json:"-"`
}
RateLimitEvent is emitted by the framework on every call to [RateLimiter.Allow], whether the request was allowed or denied. It carries utilisation information when the rate limiter implementation reports it.
type RateLimitRequest ¶
type RateLimitRequest struct {
// Chain is the ordered sequence of verified principals that have handled
// the request. The rate limiter may use it to apply per-identity limits.
Chain Chain
// Resource and Action identify what is being requested, allowing the
// limiter to apply resource-scoped or action-scoped limits.
Resource Resource
Action Action
}
RateLimitRequest is the request type for [RateLimiter.Allow].
type RateLimitResponse ¶
type RateLimitResponse struct {
// Allowed indicates whether the request is within the rate limit.
// The framework denies the request if Allowed is false.
Allowed bool
// Reason is a human-readable explanation of the rate limit decision,
// intended for operator logs. It is empty when the limiter does not
// provide one.
Reason string
// CurrentRPS is the observed request rate for the calling identity over
// the current window, as measured by the implementation. Zero means the
// implementation does not report utilisation.
CurrentRPS float64
// LimitRPS is the configured rate limit for the calling identity. Zero
// means the implementation does not report the configured limit.
LimitRPS float64
}
RateLimitResponse is the response type for [RateLimiter.Allow].
type RateLimiter ¶
type RateLimiter interface {
// Allow reports whether the request identified by req is within the
// configured rate limits. The framework calls Allow before policy
// evaluation; a denial short-circuits the pipeline and returns a distinct
// rejection to the caller.
//
// A nil error with Allowed false is a normal rate-limit denial.
// A non-nil error is treated as a transient failure; the framework
// applies the conservative safe default (deny).
Allow(ctx context.Context, req *RateLimitRequest) (*RateLimitResponse, error)
io.Closer
}
RateLimiter enforces request rate limits before policy evaluation. The framework applies rate limiting as the first enforcement step so that policy evaluation is never reached for requests that exceed configured limits.
Required methods: [RateLimiter.Allow] and io.Closer.
type RateLimiterArgs ¶
type RateLimiterArgs struct {
// OutboundTLSConfig is a *tls.Config for outbound mTLS connections from
// this workload to the rate limit backend. It presents the current X.509
// SVID via GetClientCertificate (updated atomically on each SVID rotation)
// and verifies the server certificate against the SPIFFE trust bundle.
// Nil when the framework has no outbound TLS configured.
OutboundTLSConfig *tls.Config
}
RateLimiterArgs holds the ligand-internal values that RateLimiterFactory implementations receive from Init. Only values that are contractually guaranteed to be present when Init calls the factory are included; adding a field to this struct is a considered API decision because it constrains the construction order inside Init.
type RateLimiterFactory ¶
type RateLimiterFactory func(*RateLimiterArgs) (RateLimiter, error)
RateLimiterFactory is a constructor called by Init after the workload identity and TLS config are established. Unlike other factory types in Ligand, RateLimiterFactory returns an error because rate limiter providers verify connectivity at construction time — the Valkey provider issues a PING and returns an error if Valkey is unreachable. Other factory types (BindingFactory, BundleFetcherFactory, HealthSignalExposerFactory, OutboundTransportFactory) perform no network I/O in their constructors; blocking operations are deferred to Serve/Fetch/WatchBundles. Init fails if the factory returns an error, satisfying the fail-closed guarantee.
The RateLimiterArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
Providers expose both a Factory function (for use in Config) and a New constructor (for tests and advanced use).
func NoopRateLimiterFactory ¶
func NoopRateLimiterFactory() RateLimiterFactory
NoopRateLimiterFactory returns a RateLimiterFactory that creates a NoopRateLimiter. It is used as the default when [Config.RateLimiter] is nil.
type RecordCircuitOutcomeRequest ¶
type RecordCircuitOutcomeRequest struct {
// SPIFFEID is the SPIFFE ID of the destination service.
SPIFFEID string
// Outcome is the result of the outbound call.
Outcome CallOutcome
}
RecordCircuitOutcomeRequest is the argument to [CircuitBreaker.RecordOutcome].
type RecordLoadBalancerOutcomeRequest ¶
type RecordLoadBalancerOutcomeRequest struct {
// Endpoint is the endpoint that was called.
Endpoint *Endpoint
// Success is true when the call completed without a connection-level error
// and the response status was below 5xx.
Success bool
}
RecordLoadBalancerOutcomeRequest is the argument to [LoadBalancer.RecordOutcome].
type RecordOutlierOutcomeRequest ¶
type RecordOutlierOutcomeRequest struct {
// Endpoint is the endpoint that was called.
Endpoint *Endpoint
// Outcome is the result of the outbound call.
Outcome CallOutcome
}
RecordOutlierOutcomeRequest is the argument to [OutlierDetector.RecordOutcome].
type RecordRetryRequest ¶
type RecordRetryRequest struct {
// SPIFFEID is the SPIFFE ID of the service being retried.
SPIFFEID string
}
RecordRetryRequest is the argument to [RetryBudget.RecordRetry].
type RegisterRequest ¶
type RegisterRequest struct {
// SPIFFEID is the SPIFFE ID of the workload registering itself. Used as
// the registry key so callers can discover endpoints by SPIFFE ID.
// The framework derives this from the SVID; callers must not set it
// directly.
SPIFFEID string
// Endpoint is the endpoint data to publish. The framework populates
// Endpoint.SPIFFEID from the current SVID before calling Register.
Endpoint Endpoint
}
RegisterRequest is the argument to [ServiceRegistrar.Register].
type ReleaseRequest ¶
type ReleaseRequest struct {
// Endpoint is the endpoint that was called.
Endpoint *Endpoint
// Outcome is the result of the outbound call.
Outcome CallOutcome
}
ReleaseRequest is the argument to [PoolSelector.Release].
type ResolveRequest ¶
type ResolveRequest struct {
// SPIFFEID is the SPIFFE ID of the service to resolve.
SPIFFEID string
}
ResolveRequest is the argument to [ServiceRegistry.Resolve].
type ResolveResponse ¶
type ResolveResponse struct {
// Endpoints is the current set of healthy endpoints for the requested
// service. May be empty if no healthy endpoints are registered.
Endpoints []Endpoint
}
ResolveResponse is returned by [ServiceRegistry.Resolve].
type Resource ¶
type Resource struct {
// Kind scopes the Value to a namespace, e.g. "http.path",
// "grpc.method", or "resource.arn".
Kind string
// Value is the resource identifier within its Kind, e.g.
// "/api/v1/users", "/package.Service/Method".
Value string
}
Resource identifies the target of a request for policy evaluation. Using both Kind and Value allows the policy engine to handle multiple resource namespaces (HTTP paths, gRPC methods, custom resource types) without conflating them.
type RetryAllowRequest ¶
type RetryAllowRequest struct {
// SPIFFEID is the SPIFFE ID of the service being retried.
SPIFFEID string
}
RetryAllowRequest is the argument to [RetryBudget.Allow].
type RetryBudget ¶
type RetryBudget interface {
// Allow reports whether a retry attempt is permitted for req.SPIFFEID.
// Returns false when the retry/request ratio has exceeded the configured
// cap. The caller must call [RetryBudget.RecordRetry] before attempting
// the retry if Allow returns true.
Allow(ctx context.Context, req *RetryAllowRequest) bool
// RecordRetry records that a retry was consumed for req.SPIFFEID. Called
// only after [RetryBudget.Allow] returns true.
RecordRetry(ctx context.Context, req *RecordRetryRequest)
io.Closer
}
RetryBudget gates retry attempts with a sliding-window budget, preventing a single caller from multiplying load on an already-degraded downstream through unbounded retries.
Configured in [Config.RetryBudget]. Nil means no budget cap.
The inprocess implementation tracks (retryCount, requestCount) per service in a sliding time window and rejects retries once retryCount/requestCount exceeds the configured ratio, with a floor of at least one retry regardless of ratio. A Valkey-backed implementation (T7.7) shares counters across all instances so the budget applies to aggregate traffic, preventing the N-instance amplification that per-instance budgets cannot address.
type RetryCondition ¶
type RetryCondition int
RetryCondition names a condition under which a failed outbound call is eligible for retry.
const ( // RetryOnConnectionError retries when the call failed at the TCP/TLS level // or timed out. Safe for all HTTP methods. RetryOnConnectionError RetryCondition = iota // RetryOnServerError retries when the endpoint returned a 5xx response. // Only applied to idempotent methods (GET, HEAD, PUT, OPTIONS) unless the // caller has explicitly enabled it for non-idempotent methods. RetryOnServerError )
type RetryConfig ¶
type RetryConfig struct {
// MaxAttempts is the maximum number of additional attempts after the first
// failure. 0 means no retry (one attempt total).
MaxAttempts int
// RetryOn lists the conditions under which a failed call is retried.
// Empty means no retry regardless of MaxAttempts.
RetryOn []RetryCondition
// BackoffBase is the initial backoff duration before the second attempt.
// Subsequent backoffs double up to [RetryConfig.BackoffMax].
// Zero means no backoff (immediate retry).
BackoffBase time.Duration
// BackoffMax caps the exponential backoff. Zero means no cap (unbounded
// doubling, which is rarely desirable).
BackoffMax time.Duration
// BackoffJitter is the fraction of the computed backoff to randomise,
// expressed as a value in [0, 1]. For example, 0.25 adds or subtracts up
// to 25% of the backoff duration. Zero means no jitter.
BackoffJitter float64
// PerCallTimeout is the per-attempt deadline. Zero means use the caller's
// context deadline only. Non-zero implements the two-timeout model: a hung
// first attempt is cancelled after PerCallTimeout so a retry can still
// reach a healthy endpoint before the overall deadline expires.
PerCallTimeout time.Duration
}
RetryConfig controls retry behaviour for outbound calls to a single logical service. Returned by [RetryPolicyProvider.RetryPolicy].
A zero-value RetryConfig means no retry: [RetryConfig.MaxAttempts] is 0.
type RetryPolicyProvider ¶
type RetryPolicyProvider interface {
// RetryPolicy returns the retry configuration for req.SPIFFEID.
// The bool reports whether an entry exists; false means no retry.
RetryPolicy(ctx context.Context, req *RetryPolicyRequest) (RetryConfig, bool)
io.Closer
}
RetryPolicyProvider looks up the retry configuration for a named service. Configured as [Config.RetryPolicy] and [OutboundTransportArgs.RetryPolicy]. Nil means no retry for any service.
The static implementation wraps a fixed map; a dynamic implementation may reload policies from etcd or another config store without restarting the service. Both satisfy this interface.
type RetryPolicyRequest ¶
type RetryPolicyRequest struct {
// SPIFFEID is the SPIFFE ID of the destination service.
SPIFFEID string
}
RetryPolicyRequest is the argument to [RetryPolicyProvider.RetryPolicy].
type RevocationEvent ¶
type RevocationEvent struct{}
RevocationEvent carries information about an actively revoked credential.
type RevocationUpdate ¶
type RevocationUpdate struct {
Event *RevocationEvent
Err error
}
RevocationUpdate is delivered on the EventStream returned by [PolicyEngine.WatchRevocations]. Err is non-nil if the watch encountered an error; in that case Event is nil.
type SVIDSnapshot ¶
type SVIDSnapshot struct {
PrivateKey crypto.PrivateKey
SPIFFEID string
Certificates []*x509.Certificate // leaf-first, no root CA; matches x5c header format
TrustBundles map[string][]*x509.Certificate
}
SVIDSnapshot is an immutable view of the workload's current SVID material captured from a single atomic tlsState load. Providers that need multiple fields (key + identity, certificates + trust bundle) must call GetSnapshot once per operation and read all fields from the returned value — never call GetSnapshot more than once per operation, as a rotation between calls would produce mismatched fields.
type SecretRevocationUpdate ¶
type SecretRevocationUpdate struct {
// LeaseID is the opaque identifier of the revoked lease, matching the
// leaseID returned in the original [GetSecretResponse]. The framework uses
// this to identify and evict the affected cache entry.
LeaseID string
// Err is non-nil when the revocation watch itself encountered an error.
// The framework logs the error and continues watching; the stream is not
// closed on transient errors.
Err error
}
SecretRevocationUpdate is delivered on the EventStream returned by [Secrets.WatchRevocations]. Err is non-nil if the watch encountered an error; in that case LeaseID is empty.
type Secrets ¶
type Secrets interface {
// Get retrieves the named secret from the backend. name is the full path
// as understood by the backend (e.g. "secret/data/myapp/db-password" for
// a Vault/OpenBao KV v2 mount). The framework caches the returned value
// for the duration of TTL and calls Get again proactively before expiry
// so callers rarely block on a cold fetch.
//
// Get must be safe for concurrent use. The framework may call it from
// multiple goroutines simultaneously for different names.
Get(ctx context.Context, req *GetSecretRequest) (*GetSecretResponse, error)
// WatchRevocations returns an [EventStream] that delivers a
// [SecretRevocationUpdate] each time the backend reports that a
// previously-issued credential has been actively revoked. The channel is
// closed when ctx is cancelled. Every event must be processed; use range
// to consume.
//
// Optional: embed [UnimplementedSecrets] if not supported. The
// framework falls back to TTL-based expiry when the stream is closed
// immediately.
WatchRevocations(ctx context.Context) EventStream[SecretRevocationUpdate]
io.Closer
}
Secrets retrieves secrets (API keys, database credentials, TLS certificates) from a backing secrets store such as OpenBao. The framework caches each secret until its TTL expires and renews transparently before expiry. Application code accesses secrets via Server.GetSecret and never holds a reference to the backend client.
Required methods: [Secrets.Get] and io.Closer.
Optional methods: [Secrets.WatchRevocations]. Providers that do not support push revocation should embed UnimplementedSecrets, which returns a closed EventStream. The framework falls back to TTL-based expiry on the closed stream.
Failure mode: while the backend is unreachable the framework serves cached values until each entry's TTL expires naturally; once an entry expires and the backend is still unreachable, Server.GetSecret returns an error. Grace window: each secret's individual TTL. Recovery: automatic — the next [Secrets.Get] call after the backend recovers succeeds and repopulates the cache. Health transitions: KindSecrets moves to Unavailable when [Secrets.Get] fails and back to Ready when a subsequent call succeeds. A revocation event moves KindSecrets to Degraded while the affected entry is evicted and re-fetched; it returns to Ready once re-fetch succeeds.
type SecretsArgs ¶
type SecretsArgs struct {
// OutboundTLSConfig is a *tls.Config for outbound mTLS connections from
// this workload to the secrets backend. It presents the current X.509
// SVID via GetClientCertificate (updated atomically on each SVID rotation)
// and verifies the server certificate against the SPIFFE trust bundle.
// Nil when the framework has no outbound TLS configured.
OutboundTLSConfig *tls.Config
}
SecretsArgs holds the ligand-internal values that SecretsFactory implementations receive from Init. Only values that are contractually guaranteed to be present when Init calls the factory are included; adding a field to this struct is a considered API decision because it constrains the construction order inside Init.
type SecretsFactory ¶
type SecretsFactory func(*SecretsArgs) (Secrets, error)
SecretsFactory is a constructor called by Init after the workload identity and TLS config are established. Init fails if the factory returns an error, satisfying the fail-closed guarantee.
The SecretsArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
Providers expose both a Factory function (for use in Config) and a New constructor (for tests and advanced use).
type SelectRequest ¶
type SelectRequest struct {
// Candidates is the set of available endpoints to choose from. Must not
// be modified by the implementation.
Candidates []Endpoint
}
SelectRequest is the argument to [LoadBalancer.Select].
type SelectResponse ¶
type SelectResponse struct {
// Endpoint is the chosen endpoint.
Endpoint Endpoint
}
SelectResponse is returned by [LoadBalancer.Select].
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is an initialized Ligand framework instance. Call [Server.Enforce] to run an inbound request through the enforcement pipeline from a Binding, or Server.EnforceInbound from a trust-boundary service's own HTTP handlers. Obtain a Server by calling Init or InitTrustBoundary. Close the Server when the process is shutting down.
func Init ¶
Init initializes the Ligand framework. It must be called once at process startup before any listener is started. Init validates all required providers, establishes workload identity, and configures the TLS stack.
Init is fail-closed: if any required provider is nil, or if establishing initial identity or loading the initial policy bundle fails, Init returns an error. The caller must not proceed to accept connections on error.
Example ¶
ExampleInit shows the minimal Config required to initialize Ligand for a service that accepts mTLS traffic from peer services. Identity, Policy, and Binding are required; all other providers are optional and default to safe no-op behaviour. Init is fail-closed: any error means the process must not start.
In production, Identity is a SPIFFE/SPIRE provider, Policy is an OPA engine, and Binding is a real HTTP or gRPC binding. This example uses testkit fakes for brevity; see examples/bank/ for a fully wired multi-service deployment.
package main
import (
"context"
"fmt"
"codeberg.org/arichard/ligand"
"codeberg.org/arichard/ligand/testkit"
)
func main() {
identity, err := testkit.NewFakeWorkloadIdentity("spiffe://example.org/service/accounts")
if err != nil {
fmt.Println("identity setup failed:", err)
return
}
cfg := &ligand.Config{
Identity: identity,
Policy: testkit.NewFakePolicyEngine(),
Binding: testkit.FakeBindingFactory(),
}
srv, err := ligand.Init(context.Background(), cfg)
if err != nil {
fmt.Println("init failed:", err)
return
}
defer func() {
if err := srv.Close(); err != nil {
fmt.Println("close error:", err)
}
}()
fmt.Println("running as", srv.SPIFFEID())
}
Output: running as spiffe://example.org/service/accounts
func InitTrustBoundary ¶
InitTrustBoundary initialises a Server for services that accept external traffic (browsers, partner APIs, etc.) and manage their own inbound port rather than using a Ligand-managed mTLS Binding.
All providers initialise identically to Init. [Config.Binding] may be nil when the service has no Ligand-managed inbound port (batch jobs, CLI tools, event consumers), or non-nil when the service also accepts mTLS calls from other Ligand services. The returned Server exposes Server.EnforceInbound for use in the service's own HTTP handlers.
InitTrustBoundary is fail-closed: Identity and Policy are still required.
Example ¶
ExampleInitTrustBoundary shows how a trust-boundary service initializes Ligand. A trust boundary accepts external traffic (browsers, partner APIs) rather than peer mTLS, so Config.Binding may be nil — the service uses its own HTTP handlers and calls Server.EnforceInbound directly per request.
Pre-authentication requests use Peer: Link{ExternalParty: "..."}; post- authentication requests use Peer: Link{User: accountID}. The returned context carries the verified chain and must be threaded through any subsequent outbound call so chain propagation works correctly.
package main
import (
"context"
"fmt"
"codeberg.org/arichard/ligand"
"codeberg.org/arichard/ligand/testkit"
)
func main() {
identity, err := testkit.NewFakeWorkloadIdentity("spiffe://example.org/service/gateway")
if err != nil {
fmt.Println("identity setup failed:", err)
return
}
cfg := &ligand.Config{
Identity: identity,
Policy: testkit.NewFakePolicyEngine(),
// Binding intentionally nil — this service has no Ligand-managed
// inbound port. It accepts external HTTP and calls EnforceInbound
// from its own handlers.
}
srv, err := ligand.InitTrustBoundary(context.Background(), cfg)
if err != nil {
fmt.Println("init failed:", err)
return
}
defer func() {
if err := srv.Close(); err != nil {
fmt.Println("close error:", err)
}
}()
fmt.Println("trust boundary running as", srv.SPIFFEID())
}
Output: trust boundary running as spiffe://example.org/service/gateway
func (*Server) Close ¶
Close shuts down the framework and releases all resources held by the providers registered with Init.
Shutdown order:
- The drain goroutine (if any) is cancelled so it cannot call Close again.
- KindBinding transitions to Draining; all other configured providers transition to Unavailable. Health checks see Draining so load balancers stop routing new traffic while in-flight requests complete.
- [Config.PreStopHooks] run sequentially within [Config.PreStopTimeout]. Errors are logged at warn but never abort shutdown.
- [ServiceRegistrar.ReportHealth] marks this instance unhealthy (5 s deadline) so callers stop routing new traffic here before draining.
- [ServiceRegistrar.Deregister] removes the endpoint from the registry entirely (10 s deadline).
- [Binding.Shutdown] drains in-flight inbound requests (30 s deadline). The SVID rotation loop intentionally remains running during drain so that live requests still see a valid certificate.
- [OutboundTransport.Shutdown] drains in-flight outbound calls (same deadline). The rotation loop remains running here too.
- The SVID rotation goroutine and bundle watch goroutine are cancelled now that no live requests or outbound calls remain.
- [Binding.Close] releases the listener and any remaining connections. KindBinding transitions from Draining to Unavailable.
- Providers close in reverse dependency order: bundle fetcher, policy, identity, rate limiter, secrets, context service client, outbound transport, service registry, service registrar, drainer.
- Health and telemetry providers close last so they can observe the full shutdown sequence.
Close is idempotent; subsequent calls return nil.
func (*Server) EnforceInbound ¶
EnforceInbound runs req through the enforcement pipeline for trust-boundary services initialised with InitTrustBoundary. It is the entry point for services that accept external traffic and manage their own inbound port rather than using a Ligand-managed mTLS Binding.
The pipeline is identical to [Enforce] — rate limiting then policy evaluation, fail-closed on errors. evalChain is req.Chain + req.Peer, matching the invariant established by [Enforce]: the context always carries the full principal chain including the immediate caller. The caller sets [InboundRequest.Chain] with upstream hops (empty for a first-hop request) and [InboundRequest.Peer] with the immediate trust-boundary caller's identity (e.g. Link{User: accountID} after authentication, or Link{ExternalParty: "browser/unauthenticated"} before it).
On success EnforceInbound returns a new context with the chain stored via WithInboundChain. Callers must use the returned context for all subsequent work including outbound calls — discarding it silently breaks chain propagation to downstream services.
SubjectToken and TransactionToken are not applicable at a trust-boundary entry point — the service is responsible for authenticating callers before building the chain. Both fields are zeroed on a local copy of req so that the shared pipeline in [Enforce] skips the token exchange and verify steps.
Example ¶
ExampleServer_EnforceInbound shows how a trust-boundary handler calls EnforceInbound to run an external request through rate limiting and policy evaluation. The returned context carries the verified chain. Discarding it silently breaks chain propagation to downstream services — always use the returned context for subsequent outbound work.
package main
import (
"context"
"fmt"
"codeberg.org/arichard/ligand"
"codeberg.org/arichard/ligand/testkit"
)
func main() {
identity, err := testkit.NewFakeWorkloadIdentity("spiffe://example.org/service/gateway")
if err != nil {
fmt.Println("identity setup failed:", err)
return
}
srv, err := ligand.InitTrustBoundary(context.Background(), &ligand.Config{
Identity: identity,
Policy: testkit.NewFakePolicyEngine(),
})
if err != nil {
fmt.Println("init failed:", err)
return
}
defer srv.Close() //nolint:errcheck // example brevity
// Inside an external-facing HTTP handler:
req := &ligand.InboundRequest{
Peer: ligand.Link{ExternalParty: "browser/unauthenticated"},
Resource: ligand.Resource{Kind: "http.path", Value: "/login"},
Action: ligand.Action{Name: "GET"},
}
ctx, err := srv.EnforceInbound(context.Background(), req)
if err != nil {
fmt.Println("denied:", err)
return
}
_ = ctx // pass to downstream calls for chain propagation
fmt.Println("allowed")
}
Output: allowed
func (*Server) GetSecret ¶
GetSecret returns the current value of the named secret. name is the full backend path (e.g. "secret/data/myapp/db-password"). The framework serves the value from its cache when a valid non-expired entry exists, and fetches from the backend otherwise.
Returns ErrNotSupported when no Secrets is configured. Returns an error when the cache entry has expired and the backend is unreachable.
func (*Server) Monitor ¶
Monitor returns the health monitor for this server. Callers can use it to observe provider state for health endpoints, degradation logic, and tests.
func (*Server) SPIFFEID ¶
SPIFFEID returns the SPIFFE ID of this workload's current X.509 SVID. The value is updated atomically each time the SVID rotates, so callers always see the identity that is currently in use for mTLS connections.
Applications should use this when constructing outbound chain links or logging the service's own identity.
type ServiceRegistrar ¶
type ServiceRegistrar interface {
// Register writes req.Endpoint to the registry backend as a healthy
// endpoint. Idempotent: a second call replaces the prior registration.
Register(ctx context.Context, req *RegisterRequest) error
// ReportHealth marks the endpoint as healthy or unhealthy. When Healthy
// is false the endpoint is removed from [ServiceRegistry.Resolve] results
// but the backend lease is kept alive, enabling fast recovery when Healthy
// is set back to true without a full Register call. The framework calls
// ReportHealth with Healthy false during graceful shutdown before Deregister.
ReportHealth(ctx context.Context, req *HealthReport) error
// Deregister removes the endpoint from the registry backend and releases
// any associated backend resources (e.g. a lease). Called once during
// graceful shutdown after ReportHealth.
Deregister(ctx context.Context, req *DeregisterRequest) error
io.Closer
}
ServiceRegistrar registers and deregisters a service endpoint in the registry backend. It is separate from ServiceRegistry because registration is a write operation that not all backends support. Backends that support both implement both interfaces in the same concrete type.
The framework calls [ServiceRegistrar.Register] once at startup after the SVID is available, and [ServiceRegistrar.ReportHealth] followed by [ServiceRegistrar.Deregister] during graceful shutdown. Application code must not call these methods directly.
Implementations must keep the registration alive for the process lifetime. For backends that use TTL leases the implementation is responsible for renewing the lease in the background and signalling the framework via [ServiceRegistrarArgs.OnLeaseExpiry] if renewal fails unrecoverably after exhausting the configured recovery grace period.
type ServiceRegistrarArgs ¶
type ServiceRegistrarArgs struct {
// OutboundTLSConfig is the mTLS config for connections to the registry
// backend. Updated atomically on SVID rotation. Nil when no outbound TLS
// is configured.
OutboundTLSConfig *tls.Config
// OnLeaseExpiry is called by the registrar if the registration lease
// expires AND all internal recovery attempts within the configured grace
// period fail. The framework binds this to a [KindServiceRegistrar]
// monitor transition to [Unavailable]. Nil-safe; must not block. Not
// called for transient expiries that self-heal within the grace period.
OnLeaseExpiry func(err error)
}
ServiceRegistrarArgs holds the ligand-internal values that ServiceRegistrarFactory implementations receive from Init.
type ServiceRegistrarFactory ¶
type ServiceRegistrarFactory func(*ServiceRegistrarArgs) (ServiceRegistrar, error)
ServiceRegistrarFactory is a constructor called by Init after workload identity and TLS config are established. It returns a ServiceRegistrar that the framework uses for the lifetime of the Server.
Init fails if the factory returns an error, satisfying the fail-closed guarantee.
type ServiceRegistration ¶
type ServiceRegistration struct {
// Address is the host:port that remote callers should connect to.
// Typically the address the [Binding] is listening on.
Address string
// Locality optionally describes the failure domain of this instance.
// Zero value means no locality preference.
Locality Locality
// Metadata is arbitrary key-value data published alongside the endpoint.
// May be nil.
Metadata map[string]string
}
ServiceRegistration describes the endpoint this service instance publishes to the registry at startup. Set once in Config; does not change for the lifetime of the Server. The framework derives the registry key from the workload SVID and populates [Endpoint.SPIFFEID] from the current SVID before calling [ServiceRegistrar.Register].
type ServiceRegistry ¶
type ServiceRegistry interface {
// Resolve returns the current set of healthy endpoints for the workload
// with the given SPIFFE ID. Returns an empty slice (not an error) when
// no healthy endpoints are registered.
Resolve(ctx context.Context, req *ResolveRequest) (*ResolveResponse, error)
// WatchEndpoints returns a channel that delivers an [EndpointUpdate]
// whenever the endpoint set for the given SPIFFE ID changes. The channel
// follows the [LatestRecv]/[LatestSend] pattern: only the most recent
// update matters and intermediate values may be discarded. The channel is
// closed when ctx is cancelled or the registry shuts down.
WatchEndpoints(ctx context.Context, req *WatchEndpointsRequest) LatestRecv[EndpointUpdate]
io.Closer
}
ServiceRegistry resolves SPIFFE IDs to sets of healthy endpoints. The framework uses it to populate the load balancer with candidate endpoints for each outbound call.
Security note: the registry is an address directory, not an identity authority. The outbound transport holds the expected SPIFFE ID independently (via [OutboundTransport.Transport]) and verifies it against both the registry record and the peer TLS certificate. Write access to the registry backend (e.g. etcd) causes connection failures (wrong address), not silent traffic redirection through a forged identity. Operators must still treat the registry backend as security-critical and restrict write access accordingly.
type ServiceRegistryArgs ¶
type ServiceRegistryArgs struct {
// OutboundTLSConfig is a *tls.Config for outbound mTLS connections from
// this workload to the service registry backend. It presents the current
// X.509 SVID via GetClientCertificate (updated atomically on each SVID
// rotation) and verifies the server certificate against the SPIFFE trust
// bundle. Nil when the framework has no outbound TLS configured.
OutboundTLSConfig *tls.Config
}
ServiceRegistryArgs holds the ligand-internal values that ServiceRegistryFactory implementations receive from Init. Only values that are contractually guaranteed to be present when Init calls the factory are included; adding a field to this struct is a considered API decision because it constrains the construction order inside Init.
type ServiceRegistryFactory ¶
type ServiceRegistryFactory func(*ServiceRegistryArgs) (ServiceRegistry, error)
ServiceRegistryFactory is a constructor called by Init after workload identity and TLS config are established. It returns a ServiceRegistry that the framework uses for the lifetime of the Server.
The ServiceRegistryArgs carries exactly the ligand-internal values the factory needs; no partially-initialized Server is exposed.
Providers expose both a Factory function (for use in Config) and a New constructor (for tests and advanced use).
type SessionStore ¶
SessionStore persists and retrieves session state across requests. It is used by the framework when the application requires session continuity beyond the lifetime of a single mTLS connection.
type State ¶
type State uint8
State describes the health of a provider or the overall service.
const ( // Initializing indicates the component has not yet completed startup. // The service denies all requests while any required provider is in // this state. Initializing State = iota // Ready indicates the component is operating normally. Ready // Degraded indicates one or more providers are operating with reduced // capability — either an optional provider is unavailable, or a required // provider is within its configured grace period after a transient error. // The service continues accepting traffic. Degraded // Draining indicates the service has received a shutdown signal and is // completing in-flight requests before closing. New requests are rejected. // This is a transitional state between Ready and Unavailable during // graceful shutdown. Draining // resource. The service denies all requests while any required provider // is in this state. Unavailable )
type Telemetry ¶
type Telemetry interface {
// RecordDecision records an enforcement decision for every request that
// reaches policy evaluation, whether allowed or denied. It forms the
// tamper-evident decision audit log required by Section 7.
//
// RecordDecision must not block the enforcement pipeline. Implementations
// that perform I/O should buffer writes and return promptly.
RecordDecision(ctx context.Context, event *DecisionEvent) error
// RecordRateLimit records the outcome of every rate limit check, whether
// the request was allowed or denied. Consumers that only want rejection
// events may filter on [RateLimitEvent.Allowed]. The event carries
// utilisation fields ([RateLimitEvent.CurrentRPS], [RateLimitEvent.LimitRPS])
// when the implementation reports them; both are zero otherwise.
//
// RecordRateLimit must not block the enforcement pipeline.
RecordRateLimit(ctx context.Context, event *RateLimitEvent) error
// RecordOutboundCall records the outcome of every outbound service call
// made via the [OutboundTransport]. Aggregated fleet-wide, these records
// form the service call graph required by Section 7 (Structured Telemetry).
//
// RecordOutboundCall must not block the outbound call path.
RecordOutboundCall(ctx context.Context, event *OutboundCallEvent) error
// RecordHealthTransition is called whenever a provider's health state
// changes. It is optional: embed [UnimplementedTelemetry] to
// receive a stub that returns [ErrNotSupported].
RecordHealthTransition(ctx context.Context, event *HealthTransitionEvent) error
// RecordEjection is called whenever an endpoint is ejected from or
// reinstated into the load-balancing pool by the [OutlierDetector].
// It is optional: embed [UnimplementedTelemetry] to receive a
// stub that returns [ErrNotSupported].
RecordEjection(ctx context.Context, event *EjectionEvent) error
// RecordCircuitState is called whenever a [CircuitBreaker] transitions
// between states (Closed ↔ Open ↔ HalfOpen).
// It is optional: embed [UnimplementedTelemetry] to receive a
// stub that returns [ErrNotSupported].
RecordCircuitState(ctx context.Context, event *CircuitStateEvent) error
// RecordBundleFetch is called after each bundle fetch attempt — both the
// initial fetch during [Init] and each update delivered by the watch loop.
// It is optional: embed [UnimplementedTelemetry] to receive a
// stub that returns [ErrNotSupported].
RecordBundleFetch(ctx context.Context, event *BundleFetchEvent) error
// RecordMetrics is called periodically by the framework at
// [Config.MetricsInterval] to deliver a snapshot of aggregated framework
// health metrics. It is optional: embed [UnimplementedTelemetry] to
// receive a stub that returns [ErrNotSupported], which causes the framework
// to stop the metrics loop for this provider.
//
// Implementations must not block; the framework calls RecordMetrics from
// its own background goroutine.
RecordMetrics(ctx context.Context, snapshot *MetricsSnapshot) error
io.Closer
}
Telemetry receives structured events emitted by the framework. Implementations may write to stdout, export to OpenTelemetry, or forward to any observability backend.
Required methods: [Telemetry.RecordDecision], [Telemetry.RecordRateLimit], [Telemetry.RecordOutboundCall], and io.Closer.
Optional methods: [Telemetry.RecordHealthTransition], [Telemetry.RecordEjection], [Telemetry.RecordCircuitState], [Telemetry.RecordBundleFetch], [Telemetry.RecordMetrics]. Providers that do not need these events should embed UnimplementedTelemetry, which returns ErrNotSupported.
type TokenExchangeRequest ¶
type TokenExchangeRequest struct {
// SubjectToken is the incoming credential to exchange.
SubjectToken string
// SubjectTokenType is the token type URI per RFC 8693
// (e.g. "urn:ietf:params:oauth:token-type:access_token").
SubjectTokenType string
// Resource is the downstream service URI the resulting Transaction Token
// will be presented to, per RFC 8693 §2.1. Intended to scope the token's
// aud claim per draft-ietf-oauth-transaction-tokens. Currently passed
// through by the framework but not yet consumed by the inline provider,
// which omits aud from minted TTs pending resolution of the multi-hop
// propagation design (see inline/inline.go TODO).
Resource string
}
TokenExchangeRequest is the argument to [ContextServiceClient.Exchange].
type TransactionToken ¶
type TransactionToken struct {
// Token is the signed JWT string.
Token string
// ExpiresAt is the token's expiry time. Callers must not cache or reuse
// tokens past this time.
ExpiresAt time.Time
}
TransactionToken is a signed JWT issued by the Context Service or the inline provider. It carries verified caller identity and scope and is propagated across all subsequent internal hops per draft-ietf-oauth-transaction-tokens.
type TransactionTokenClaims ¶
type TransactionTokenClaims struct {
// CallerID is the identity of the original external caller (the IdP subject
// claim from the exchanged access token).
CallerID string
// Scope is the OAuth scope granted to the original caller.
Scope string
// IssuedAt and ExpiresAt are the token's validity window.
IssuedAt time.Time
ExpiresAt time.Time
}
TransactionTokenClaims carries the verified claims extracted from a Transaction Token by [ContextServiceClient.VerifyTransactionToken].
type TrustBoundaryConfig ¶
type TrustBoundaryConfig struct {
// Paths is the set of route prefixes that are external entry points.
// The framework calls [ContextServiceClient.Exchange] only on requests
// whose resource value has a matching prefix. Empty means no paths are
// configured as trust-boundary entry points.
Paths []string
// IdPJWKSURL is the JWKS endpoint of the IdP whose tokens arrive at this
// boundary. Required by the inline Context Service implementation for
// subject token validation.
IDPJWKSURL string
}
TrustBoundaryConfig declares the entry points at which the framework calls [ContextServiceClient.Exchange] to convert incoming OAuth access tokens into Transaction Tokens.
type UnimplementedBinding ¶
type UnimplementedBinding struct{}
UnimplementedBinding is embedded in concrete Binding implementations to satisfy optional Binding methods with safe defaults.
func (UnimplementedBinding) Addr ¶
func (UnimplementedBinding) Addr() string
Addr returns an empty string. Override in bindings that establish a real listener and need to expose their bound address to the service registry.
func (UnimplementedBinding) Listening ¶
func (UnimplementedBinding) Listening() <-chan struct{}
Listening returns a pre-closed channel, causing the framework to mark KindBinding Ready synchronously at the end of Init. This is the correct default for test fakes and other bindings that do not establish a deferred TCP listener. Override this method in bindings (such as the HTTP provider) that only become ready after [Binding.Serve] completes its Listen call.
type UnimplementedBundleFetcher ¶
type UnimplementedBundleFetcher struct{}
UnimplementedBundleFetcher is embedded in concrete BundleFetcher implementations to provide stub implementations of future optional methods. There are no optional methods today; embedding this struct ensures forward compatibility when they are added.
Required methods ([BundleFetcher.Fetch], [BundleFetcher.WatchBundles], and io.Closer) are not stubbed — failing to implement them is a compile error.
type UnimplementedCircuitBreaker ¶
type UnimplementedCircuitBreaker struct{}
UnimplementedCircuitBreaker is embedded in concrete CircuitBreaker implementations to provide no-op stubs for optional methods and forward compatibility when new optional methods are added.
func (UnimplementedCircuitBreaker) Allow ¶
func (UnimplementedCircuitBreaker) Allow(_ context.Context, _ *CircuitAllowRequest) (bool, error)
Allow always returns true, nil (all calls permitted).
func (UnimplementedCircuitBreaker) Close ¶
func (UnimplementedCircuitBreaker) Close() error
Close is a no-op stub.
func (UnimplementedCircuitBreaker) RecordOutcome ¶
func (UnimplementedCircuitBreaker) RecordOutcome(_ context.Context, _ *RecordCircuitOutcomeRequest)
RecordOutcome is a no-op stub.
func (UnimplementedCircuitBreaker) State ¶
func (UnimplementedCircuitBreaker) State(_ context.Context, _ *CircuitStateRequest) CircuitState
State always returns CircuitStateClosed.
type UnimplementedContextServiceClient ¶
type UnimplementedContextServiceClient struct{}
UnimplementedContextServiceClient is embedded in concrete ContextServiceClient implementations to provide stub implementations of optional methods. Required methods — [ContextServiceClient.Exchange], [ContextServiceClient.VerifyTransactionToken], and io.Closer — are never stubbed; failing to implement them remains a compile error.
There are no optional methods today; embedding this struct ensures forward compatibility when they are added.
type UnimplementedEndpointSelector ¶
type UnimplementedEndpointSelector struct{}
UnimplementedEndpointSelector is embedded in concrete EndpointSelector implementations for forward compatibility when new optional methods are added.
func (UnimplementedEndpointSelector) Close ¶
func (UnimplementedEndpointSelector) Close() error
Close is a no-op stub.
type UnimplementedGracefulDrainer ¶
type UnimplementedGracefulDrainer struct{}
UnimplementedGracefulDrainer is embedded in concrete GracefulDrainer implementations to provide stub implementations of future optional methods. There are no optional methods today; embedding this struct ensures forward compatibility when they are added.
type UnimplementedHealthChecker ¶
type UnimplementedHealthChecker struct{}
UnimplementedHealthChecker is embedded in concrete HealthChecker implementations to provide stub implementations of future optional methods. There are no optional methods today; embedding this struct ensures forward compatibility when they are added.
type UnimplementedHealthSignalExposer ¶
type UnimplementedHealthSignalExposer struct{}
UnimplementedHealthSignalExposer is embedded in concrete HealthSignalExposer implementations to remain forward-compatible when optional methods are added to HealthSignalExposer in the future.
Embedding is recommended. Providers that choose not to embed it accept responsibility for tracking new optional methods when they are added.
func (UnimplementedHealthSignalExposer) Addr ¶
func (UnimplementedHealthSignalExposer) Addr() string
Addr returns "" because this implementation does not bind a network address.
func (UnimplementedHealthSignalExposer) Listening ¶
func (UnimplementedHealthSignalExposer) Listening() <-chan struct{}
Listening returns a pre-closed channel, signalling that the address is immediately known (or that no address is exposed).
type UnimplementedLoadBalancer ¶
type UnimplementedLoadBalancer struct{}
UnimplementedLoadBalancer is embedded in concrete LoadBalancer implementations to provide no-op stubs of notification-style optional methods. [LoadBalancer.Select] and io.Closer are required and never stubbed.
func (UnimplementedLoadBalancer) RecordOutcome ¶
func (UnimplementedLoadBalancer) RecordOutcome(_ context.Context, _ *RecordLoadBalancerOutcomeRequest)
RecordOutcome is a no-op. Embed UnimplementedLoadBalancer in load balancer implementations that do not track per-endpoint call outcomes.
type UnimplementedOutboundTransport ¶
type UnimplementedOutboundTransport struct{}
UnimplementedOutboundTransport is embedded in concrete OutboundTransport implementations for forward compatibility when new optional methods are added. [OutboundTransport.Shutdown] and io.Closer are required and never stubbed here — omitting them is a compile error.
type UnimplementedOutlierDetector ¶
type UnimplementedOutlierDetector struct{}
UnimplementedOutlierDetector is embedded in concrete OutlierDetector implementations to provide no-op stubs for optional methods and forward compatibility when new optional methods are added.
func (UnimplementedOutlierDetector) Close ¶
func (UnimplementedOutlierDetector) Close() error
Close is a no-op stub.
func (UnimplementedOutlierDetector) EjectedEndpoints ¶
func (UnimplementedOutlierDetector) EjectedEndpoints(_ context.Context) []Endpoint
EjectedEndpoints always returns nil (no endpoints ejected).
func (UnimplementedOutlierDetector) IsEjected ¶
func (UnimplementedOutlierDetector) IsEjected(_ context.Context, _ *IsEjectedRequest) bool
IsEjected always returns false (no endpoints ejected).
func (UnimplementedOutlierDetector) RecordOutcome ¶
func (UnimplementedOutlierDetector) RecordOutcome(_ context.Context, _ *RecordOutlierOutcomeRequest)
RecordOutcome is a no-op stub.
type UnimplementedPolicyEngine ¶
type UnimplementedPolicyEngine struct{}
UnimplementedPolicyEngine is embedded in concrete PolicyEngine implementations to provide stub implementations of optional methods. Required methods ([PolicyEngine.Evaluate] and io.Closer) are not stubbed — failing to implement them remains a compile error.
Embedding this struct is recommended. Providers that choose not to embed it accept responsibility for tracking new optional methods when they are added to the interface.
func (UnimplementedPolicyEngine) Reload ¶
func (UnimplementedPolicyEngine) Reload(_ context.Context, _ []byte) error
Reload returns ErrNotSupported. The framework skips hot reload and relies on TTL-based bundle expiry instead. Note: configuring a BundleFetcher alongside a PolicyEngine that returns ErrNotSupported from Reload is a misconfiguration; Init will return an error in that case.
func (UnimplementedPolicyEngine) WatchRevocations ¶
func (UnimplementedPolicyEngine) WatchRevocations(_ context.Context) EventStream[RevocationUpdate]
WatchRevocations returns a closed EventStream with no events. TTL-based credential expiry is always active and requires no sentinel to activate.
type UnimplementedPoolSelector ¶
type UnimplementedPoolSelector struct{}
UnimplementedPoolSelector is embedded in concrete PoolSelector implementations for forward compatibility when new optional methods are added.
func (UnimplementedPoolSelector) Close ¶
func (UnimplementedPoolSelector) Close() error
Close is a no-op stub.
func (UnimplementedPoolSelector) Release ¶
func (UnimplementedPoolSelector) Release(_ context.Context, _ *ReleaseRequest)
Release is a no-op stub.
type UnimplementedRateLimiter ¶
type UnimplementedRateLimiter struct{}
UnimplementedRateLimiter is embedded in concrete RateLimiter implementations to remain forward-compatible when optional methods are added to RateLimiter in the future. Currently RateLimiter has no optional methods. Embedding the struct now ensures that when optional methods are added, existing implementations continue to satisfy the interface without any code changes.
Providers that choose not to embed it accept responsibility for tracking new optional methods when they are added.
type UnimplementedRetryBudget ¶
type UnimplementedRetryBudget struct{}
UnimplementedRetryBudget is embedded in concrete RetryBudget implementations for forward compatibility when new optional methods are added.
func (UnimplementedRetryBudget) Allow ¶
func (UnimplementedRetryBudget) Allow(_ context.Context, _ *RetryAllowRequest) bool
Allow always returns true (retries permitted).
func (UnimplementedRetryBudget) Close ¶
func (UnimplementedRetryBudget) Close() error
Close is a no-op stub.
func (UnimplementedRetryBudget) RecordRetry ¶
func (UnimplementedRetryBudget) RecordRetry(_ context.Context, _ *RecordRetryRequest)
RecordRetry is a no-op stub.
type UnimplementedRetryPolicyProvider ¶
type UnimplementedRetryPolicyProvider struct{}
UnimplementedRetryPolicyProvider is embedded in concrete RetryPolicyProvider implementations for forward compatibility when new optional methods are added. Both current methods have safe no-op defaults, so this struct fully satisfies the interface and the compile-time check is present.
func (UnimplementedRetryPolicyProvider) Close ¶
func (UnimplementedRetryPolicyProvider) Close() error
Close is a no-op.
func (UnimplementedRetryPolicyProvider) RetryPolicy ¶
func (UnimplementedRetryPolicyProvider) RetryPolicy(_ context.Context, _ *RetryPolicyRequest) (RetryConfig, bool)
RetryPolicy returns a zero RetryConfig and false (no retry for any SPIFFE ID).
type UnimplementedSecrets ¶
type UnimplementedSecrets struct{}
UnimplementedSecrets is embedded in concrete Secrets implementations to provide stub implementations of optional methods. Embedding this struct ensures forward compatibility when new optional methods are added.
Required methods ([Secrets.Get] and io.Closer) are not stubbed — failing to implement them is a compile error.
func (UnimplementedSecrets) WatchRevocations ¶
func (UnimplementedSecrets) WatchRevocations(_ context.Context) EventStream[SecretRevocationUpdate]
WatchRevocations returns a closed EventStream carrying a single SecretRevocationUpdate with ErrNotSupported. The revocation loop treats this sentinel as "provider does not support push revocation" and stops watching without retrying, falling back to TTL-based expiry only.
type UnimplementedServiceRegistrar ¶
type UnimplementedServiceRegistrar struct{}
UnimplementedServiceRegistrar is embedded in concrete ServiceRegistrar implementations to provide stub implementations of future optional methods. There are no optional methods today; embedding this struct ensures forward compatibility when they are added.
type UnimplementedServiceRegistry ¶
type UnimplementedServiceRegistry struct{}
UnimplementedServiceRegistry is embedded in concrete ServiceRegistry implementations to provide stub implementations of future optional methods. There are no optional methods today; embedding this struct ensures forward compatibility when they are added.
type UnimplementedSessionStore ¶
type UnimplementedSessionStore struct{}
UnimplementedSessionStore is embedded in concrete SessionStore implementations to provide stub implementations of future optional methods. There are no optional methods today; embedding this struct ensures forward compatibility when they are added.
type UnimplementedTelemetry ¶
type UnimplementedTelemetry struct{}
UnimplementedTelemetry is embedded in concrete Telemetry implementations to provide stub implementations of optional methods. Required methods — [Telemetry.RecordDecision], [Telemetry.RecordRateLimit], and io.Closer — are never stubbed; failing to implement them remains a compile error.
Embedding this struct is recommended. Providers that choose not to embed it accept responsibility for tracking new optional methods when they are added.
func (UnimplementedTelemetry) RecordBundleFetch ¶
func (UnimplementedTelemetry) RecordBundleFetch(_ context.Context, _ *BundleFetchEvent) error
RecordBundleFetch returns ErrNotSupported.
func (UnimplementedTelemetry) RecordCircuitState ¶
func (UnimplementedTelemetry) RecordCircuitState(_ context.Context, _ *CircuitStateEvent) error
RecordCircuitState returns ErrNotSupported.
func (UnimplementedTelemetry) RecordEjection ¶
func (UnimplementedTelemetry) RecordEjection(_ context.Context, _ *EjectionEvent) error
RecordEjection returns ErrNotSupported.
func (UnimplementedTelemetry) RecordHealthTransition ¶
func (UnimplementedTelemetry) RecordHealthTransition(_ context.Context, _ *HealthTransitionEvent) error
RecordHealthTransition returns ErrNotSupported. The framework logs health transitions through its structured logger when this method returns ErrNotSupported.
func (UnimplementedTelemetry) RecordMetrics ¶
func (UnimplementedTelemetry) RecordMetrics(_ context.Context, _ *MetricsSnapshot) error
RecordMetrics returns ErrNotSupported. The framework stops the metrics loop for this provider when this method returns ErrNotSupported.
type UnimplementedWorkloadIdentity ¶
type UnimplementedWorkloadIdentity struct{}
UnimplementedWorkloadIdentity is embedded in concrete WorkloadIdentity implementations to provide stub implementations of optional methods. Required methods ([WorkloadIdentity.FetchX509Context] and io.Closer) are not stubbed — failing to implement them remains a compile error.
Embedding this struct is recommended. Providers that choose not to embed it accept responsibility for tracking new optional methods when they are added to the interface.
func (UnimplementedWorkloadIdentity) FetchWIT ¶
func (UnimplementedWorkloadIdentity) FetchWIT(_ context.Context, _ *FetchWITRequest) (*FetchWITResponse, error)
FetchWIT returns ErrNotSupported. WIT issuance requires the Identity Server to implement draft-ietf-wimse-workload-creds (tracked in SPIFFE issue #315). Until then, providers embed this stub and WIT/WPT headers are omitted from outbound requests; the inbound WPT path is fail-closed.
func (UnimplementedWorkloadIdentity) WatchX509Context ¶
func (UnimplementedWorkloadIdentity) WatchX509Context(_ context.Context) LatestRecv[X509ContextUpdate]
WatchX509Context returns a closed LatestRecv containing a single X509ContextUpdate with ErrNotSupported. The framework falls back to polling [WorkloadIdentity.FetchX509Context] for SVID rotation.
type WPTSigner ¶
type WPTSigner struct {
// contains filtered or unexported fields
}
WPTSigner builds Workload Identity Token (WIT) and Workload Proof Token (WPT) JWTs for outbound WIMSE authentication. It reads the current SVID key and SPIFFE ID atomically on each call so SVID rotations propagate to new tokens without restart.
WIT issuance is delegated to the Identity Server via identity.FetchWIT per draft-ietf-wimse-workload-creds-00. The Identity Server signs the WIT and binds the workload's public key (cnf.jwk) to its identity; the workload retains the private key for WPT signing. If identity.FetchWIT returns ErrNotSupported, [WPTSigner.BuildWIT] returns that error and the caller should omit WIMSE headers rather than attaching unverifiable tokens.
WPT replay protection is per-instance: JTI values are cached in the inbound binding's in-process JTI cache, so the same WPT replayed against a different instance behind the same load balancer is not detected. This is an accepted tradeoff per draft-ietf-wimse-wpt-00 §5, which notes that preventing cross-instance replay requires shared replay state (e.g. a distributed cache). Do not rely solely on JTI uniqueness for replay protection in deployments with multiple instances of the same service.
WPT: draft-ietf-wimse-wpt-00
func NewWPTSigner ¶
func NewWPTSigner(getSnapshot func() SVIDSnapshot, identity WorkloadIdentity, clock quartz.Clock) *WPTSigner
NewWPTSigner creates a WPTSigner. getSnapshot is called once per Build* invocation; it must read atomically from the Server's tlsState so rotations take effect without requiring a new WPTSigner. All fields used within a single Build* call are read from the same snapshot to avoid rotation skew. identity is used to issue WITs via [WorkloadIdentity.FetchWIT].
func (*WPTSigner) BuildWIMSETokens ¶
func (s *WPTSigner) BuildWIMSETokens(ctx context.Context, audience, txnToken string) (wit, wpt string, err error)
BuildWIMSETokens issues a WIT and WPT for the given audience in a single atomic snapshot. The WIT is obtained from the Identity Server and binds the workload's current SVID public key as cnf.jwk; the WPT is signed with the same key so that recipients can verify the WPT against the WIT's cnf.jwk. txnToken is an optional Transaction Token to bind in the WPT via the oth claim; pass an empty string when there is no Transaction Token to forward.
Returns ErrWPTKeyNotECDSA if the current SVID key is not ECDSA. Returns ErrNotSupported if the Identity Server does not yet implement WIT issuance (SPIFFE issue #315); callers should omit WIMSE headers in that case.
func (*WPTSigner) BuildWPT ¶
BuildWPT creates a WPT JWT per draft-ietf-wimse-wpt-00 signed with an explicitly supplied key. Use WPTSigner.BuildWIMSETokens for normal outbound request signing — it issues the WIT and WPT atomically with the same key and is the correct call for all production paths.
BuildWPT exists solely for tests that construct WIT strings out-of-band (e.g. with crafted expiry timestamps) and need to sign a WPT against them. Do not use it in production code: callers that supply a key unrelated to the WIT's cnf.jwk will produce tokens that fail verification at the peer.
wit is the raw WIT JWT string. txnToken is an optional Transaction Token to bind via the oth claim; pass an empty string when there is none.
type WatchEndpointsRequest ¶
type WatchEndpointsRequest struct {
// SPIFFEID is the SPIFFE ID of the service to watch.
SPIFFEID string
}
WatchEndpointsRequest is the argument to [ServiceRegistry.WatchEndpoints].
type WorkloadIdentity ¶
type WorkloadIdentity interface {
// FetchX509Context returns the current X.509-SVID and trust bundle for
// this workload. The result is used to configure mTLS listeners and
// dialers. Callers that need automatic rotation should use
// WatchX509Context instead of polling FetchX509Context.
FetchX509Context(ctx context.Context, req *FetchX509ContextRequest) (*FetchX509ContextResponse, error)
// WatchX509Context returns a [LatestRecv] that delivers an
// [X509ContextUpdate] each time the SVID rotates or the trust bundle
// changes. The first update is delivered before any request is served.
// The channel is closed when ctx is cancelled. Only the most recent
// update matters; use [Latest] to consume.
//
// Optional: embed [UnimplementedWorkloadIdentity] if not
// supported. The framework falls back to polling
// [WorkloadIdentity.FetchX509Context] on [ErrNotSupported].
WatchX509Context(ctx context.Context) LatestRecv[X509ContextUpdate]
// FetchWIT requests a Workload Identity Token (WIT) per
// draft-ietf-wimse-workload-creds from the Identity Server. The returned
// JWT is signed by the Identity Server's JWT authority key (carried in
// [FetchX509ContextResponse.JWTAuthorities]) and binds
// req.CNFPublicKey to the workload's identity in the cnf.jwk claim.
//
// Optional: embed [UnimplementedWorkloadIdentity] to return
// [ErrNotSupported]. When [ErrNotSupported] is returned, [WPTSigner]
// will not attach WIT/WPT headers on outbound requests, and the inbound
// WPT verification path is fail-closed (no JWT authorities → rejected).
// Full support requires the Identity Server to implement SPIFFE issue #315.
FetchWIT(ctx context.Context, req *FetchWITRequest) (*FetchWITResponse, error)
io.Closer
}
WorkloadIdentity supplies the service's cryptographic workload identity and keeps it current through automatic rotation. The framework uses it to establish mTLS connections for all inbound and outbound traffic.
Required methods: [WorkloadIdentity.FetchX509Context] and io.Closer. These must be implemented; failing to do so is a compile error.
Optional methods: [WorkloadIdentity.WatchX509Context] and [WorkloadIdentity.FetchWIT]. Providers that do not support these should embed UnimplementedWorkloadIdentity.
type X509ContextUpdate ¶
type X509ContextUpdate struct {
Response *FetchX509ContextResponse
Err error
}
X509ContextUpdate is delivered on the LatestRecv returned by [WorkloadIdentity.WatchX509Context]. Err is non-nil if the watch encountered an error; in that case Response is nil.
type X509SVID ¶
type X509SVID struct {
// SPIFFEID is the SPIFFE ID encoded in this SVID, e.g.
// "spiffe://trust-domain/service/name".
SPIFFEID string
// Certificates is the certificate chain for this SVID, leaf first.
// The leaf carries the SPIFFE ID in its URI SAN; intermediates follow.
Certificates []*x509.Certificate
// PrivateKey is the private key corresponding to the leaf certificate.
// It is held in memory only and never serialised by the framework.
// Implementations must support at least ECDSA P-256; RSA and Ed25519
// are also accepted.
PrivateKey crypto.PrivateKey
}
X509SVID is a single X.509 SVID (SPIFFE Verifiable Identity Document) held by this workload.
Source Files
¶
- attrs.go
- binding.go
- bundle.go
- bundle_fetcher.go
- chain.go
- chan.go
- circuit_breaker.go
- context_service_client.go
- endpoint_selector.go
- graceful_drainer.go
- health.go
- health_checker.go
- health_signal_exposer.go
- init.go
- jti_cache.go
- load_balancer.go
- metrics.go
- outbound_transport.go
- outlier_detector.go
- policy_engine.go
- provider.go
- rate_limiter.go
- retry.go
- retry_budget.go
- retry_policy.go
- rotate.go
- secrets.go
- secrets_cache.go
- serve.go
- service_registrar.go
- service_registry.go
- session_store.go
- telemetry.go
- tls.go
- workload_identity.go
- wpt.go