Documentation
¶
Overview ¶
Package awsparamstore implements ligand.WITIssuer backed by AWS Systems Manager (SSM) Parameter Store. It is the AWS-plane counterpart to provider/witissuer/openbao: a public-key REGISTRY plus local signing. The workload's SVID public key is published (public keys are not secrets, so the Standard tier — free — is the role-correct home), and WITs are signed locally by the provider's own ephemeral ECDSA key. Registered keys are cached in-process, so Parameter Store is not on the hot enforcement path.
Authentication — the deliberate mTLS exception ¶
Like provider/secrets/awssm and unlike every self-hosted dependency provider, this provider does NOT dial its backend over SVID-pinned mTLS and does NOT use ligand.WITIssuerArgs.OutboundTLSConfig. Parameter Store authenticates the SERVER via web PKI and the CLIENT via SigV4 from instance-role credentials the AWS SDK resolves from IMDS. There is no SPIFFE identity to pin, so a ServerID field is deliberately absent, and SigV4's stateless per-request signing means there is no login token to renew on a months-long node.
Layout and per-service write isolation (the C3-analog) ¶
Each public key is stored at "<Prefix>/<service>/<kid>", where <kid> is the RFC 7638 JWK thumbprint of the key and <service> is the final path segment of the workload's SPIFFE ID (e.g. "accounts" for spiffe://…/accounts). The value is JSON {spiffe_id, jwk}.
Splitting by service lets IAM grant each workload ssm:PutParameter / ssm:DeleteParameter only under "<Prefix>/<service>/*" — the per-service write-path isolation the OpenBao provider's C3 hardening prescribes, enforced by IAM instead of policy. It also makes the stored spiffe_id checkable: Provider.ResolveKey derives the expected service from the parameter's PATH and rejects any entry whose stored spiffe_id maps to a different service, so a workload cannot register a key under another workload's identity even with a lying payload. This requires each workload's SPIFFE ID to have a unique, SSM-name-safe (matching [A-Za-z0-9_.-]) final path segment within the fleet — true for the flat VM-plane SPIFFE IDs the EC2 plane assigns.
Resolution ¶
ligand.WITIssuer.ResolveKey receives only a kid, so per-service paths cannot be addressed directly. On a cache miss the provider does one recursive GetParametersByPath over <Prefix>, validates each entry (path-vs-payload service match, then RFC 7638 self-certifying thumbprint), and builds a kid→key index cached in-process. The scanned population is tiny and bounded (≤2 live keys per instance plus shutdown leftovers). The local service's own keys never miss — they are cached by Provider.FetchWIT — so a scan happens only the first time a given peer key is verified.
Key lifecycle ¶
Provider.FetchWIT runs on each SVID rotation (~hourly): fresh key → new kid → PutParameter, and the previous kid's parameter is deleted after Config.WITDuration (the window during which chain links referencing it remain valid). Steady state is ≤2 live parameters per instance regardless of node lifetime. On graceful shutdown the current kid is intentionally left in Parameter Store so in-flight chains stay verifiable; that leftover accumulates one parameter per instance per deployment and is cleared by the EC2 plane's teardown sweep.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrKeyNotFound is returned by [Provider.ResolveKey] when a kid is absent // from both the in-process cache and Parameter Store. ErrKeyNotFound = errors.New("awsparamstore: key not found") // ErrUnsupportedVerifyingKey is returned when a stored JWK is not a public key // in the framework's signing-algorithm allowlist (a private key, a symmetric // key, or an off-allowlist type/curve/size). A verifying-key registry must // hold only keys usable for chain-link verification; anything else fails closed. ErrUnsupportedVerifyingKey = errors.New("awsparamstore: unsupported verifying key") // ErrUnsafeServiceSegment is returned by [Provider.FetchWIT] when the SPIFFE // ID yields no SSM-name-safe service segment. ErrUnsafeServiceSegment = errors.New("awsparamstore: SPIFFE ID has no SSM-name-safe final path segment") )
Functions ¶
func Capture ¶
func Capture(cfg *Config, out **Provider) ligand.WITIssuerFactory
Capture is like Factory but also stores the constructed Provider in *out, for callers that need the concrete type after ligand.Init returns.
func Factory ¶
func Factory(cfg *Config) ligand.WITIssuerFactory
Factory returns a ligand.WITIssuerFactory that builds a Provider from cfg, for use in ligand.Config.WITIssuer. A nil cfg uses all defaults.
Types ¶
type Config ¶
type Config struct {
// AWSConfig, when non-nil, is used verbatim as the SDK configuration for the
// SSM client. Supply it to pin a region/endpoint, inject a custom credential
// provider, or instrument the SDK HTTP client (e.g. otelaws via APIOptions).
// Nil (the default) loads the SDK default configuration chain (environment,
// IMDS).
AWSConfig *aws.Config
// Prefix is the SSM parameter path prefix under which workload public keys are
// stored, as "<Prefix>/<service>/<kid>". It must begin with "/". Defaults to
// "/ligand/witkeys".
Prefix string
// WITDuration is the validity window for issued WITs and the delay before a
// rotated-out key's parameter is deleted. Defaults to 5 minutes.
WITDuration time.Duration
// CacheTTL is the revocation-propagation window: how long a key resolved by a
// prefix scan is served from the in-process cache before re-resolution. Keys
// written by [Provider.FetchWIT] (the local service's own) use WITDuration
// instead so they stay valid for the full WIT window. Defaults to 5 minutes.
CacheTTL time.Duration
}
Config holds the configuration for the SSM Parameter Store WIT issuer.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider implements ligand.WITIssuer backed by SSM Parameter Store.
func New ¶
New constructs a Provider. It loads the AWS SDK configuration and builds an SSM client authenticated by instance-role SigV4. args is accepted for factory-signature uniformity; its OutboundTLSConfig is deliberately unused (web-PKI TLS + SigV4, not SVID-pinned mTLS — see the package doc). A nil cfg uses all defaults.
func (*Provider) Close ¶
Close stops the background goroutines (cache sweep and any pending old-key deletions) and waits for them to exit. The current kid is intentionally left in Parameter Store on shutdown — see the package doc. Idempotent.
func (*Provider) FetchWIT ¶
func (p *Provider) FetchWIT(ctx context.Context, req *ligand.FetchWITRequest) (*ligand.FetchWITResponse, error)
FetchWIT registers req.CNFPublicKey in Parameter Store under "<Prefix>/<service>/<kid>", caches it locally, and returns a locally-signed WIT binding the key to the workload's SPIFFE identity. The returned KID is the RFC 7638 JWK thumbprint of CNFPublicKey.
func (*Provider) ResolveKey ¶
ResolveKey returns the public key and bound SPIFFE ID registered under kid. It first checks the in-process cache (populated by FetchWIT and prior scans); an expired entry is treated as a miss. On a miss it scans <Prefix> once, validates every entry (path-vs-payload service match, then RFC 7638 self-certifying thumbprint), repopulates the cache, and returns the requested kid or ErrKeyNotFound.
func (*Provider) VerifyingKeys ¶
VerifyingKeys returns the public key corresponding to the ephemeral signing key used to sign WITs. The framework merges this into GetJWTAuthorities so VerifyWPT can verify WITs produced by this issuer.