Documentation
¶
Overview ¶
Package workloadclaims implements the admission-inventory API: the component that admitted a pod's containers (nri-image-policy on node-CVM, policy-monitor in a kata guest) is the arbiter of both what runs in a pod sandbox and which sandbox a process belongs to.
It serves two disjoint surfaces (docs/ratls.md, "Sandbox identity"):
- a node-local token route, where get-cert redeems its identity for a signed sandbox token naming its own sandbox — nothing the caller sends names the pod. On node-CVM that is a Unix socket, whose kernel peer credentials bind the caller; inside a kata guest it is guest loopback, where the single-pod guest boundary does;
- a network endpoint over mutually-attested RA-TLS, where CDS asks which image digests a named sandbox is currently running.
Keeping them apart bounds each: the socket cannot enumerate other sandboxes, and the network endpoint cannot mint identity.
Index ¶
- Constants
- Variables
- func ContainerIDCandidatesForPID(procRoot string, pid int) ([]string, error)
- func DigestsServerTLSConfig(platform string, ...) (*tls.Config, *ratls.CertManager, error)
- func GuestInventoryEndpoint() string
- func InventoryEndpoint() string
- func ListenUnix(socketPath string, gid int) (net.Listener, error)
- func NodeHostCIDRs(nodes []*corev1.Node) (hosts []*net.IPNet, excluded []string)
- func RequesterKeyDigest(pub crypto.PublicKey) ([]byte, error)
- func ResolveAdvertiseHost(ctx context.Context, host, cdsEndpoint string) (string, error)
- func ServeDigests(ctx context.Context, l net.Listener, resolver SandboxResolver, identity []byte) error
- func ServeTokens(ctx context.Context, l net.Listener, resolver SandboxResolver, ...) error
- func StartDigestsEndpoint(ctx context.Context, logger *slog.Logger, resolver SandboxResolver, ...) error
- func UnverifiedInventoryHost(tokenDER []byte) (string, error)
- func ValidateInventoryHost(host string) error
- type AllowlistRefresh
- type AllowlistRefreshReporter
- type CIDRHosts
- type DigestsClient
- func (c *DigestsClient) Fetch(ctx context.Context, host, sandboxID string) ([]string, error)
- func (c *DigestsClient) FetchSandbox(ctx context.Context, host, sandboxID string) (SandboxDigestsResponse, error)
- func (c *DigestsClient) InventoryKey(ctx context.Context, host string) (*ecdsa.PublicKey, error)
- type InventoryHosts
- type InventoryIdentity
- type NodeHosts
- type Peer
- type SandboxContainer
- type SandboxDigestsResponse
- type SandboxResolver
- type SandboxTokenRequest
- type SandboxTokenSigner
- type SignedSandboxToken
- type SignerHolder
- type VerifiedSandbox
Constants ¶
const ( SandboxPath = "/sandbox" SandboxDigestsPrefix = "/digests/" IdentityPath = "/identity" )
SandboxPath is the token route on the local Unix socket; SandboxDigestsPrefix is the digests route on the CDS-facing network endpoint. POST SandboxPath issues a signed sandbox token for the calling process (caller bound by kernel peer credentials; see sandboxtoken.go). GET SandboxDigestsPrefix+<sandboxID> lists the tracked container image digests of that sandbox.
const ( SocketName = "workload-claims.sock" SidecarSocketDir = "/run/c8s/workload-claims" )
SocketName is the fixed filename of the inventory's Unix socket, and SidecarSocketDir is where the socket directory is presented inside the c8s-cert sidecar. Both are compiled constants, not deployment values: get-cert dials InventoryEndpoint (built from them) as a baked path, so the control plane cannot redirect the fetch to a rogue inventory (docs/getcert-workload-binding.md Corner 5). The inventory (nri-image-policy on node-CVM, policy-monitor in the kata guest) creates its socket as SocketName under its configured directory, and the webhook hostPath-mounts that directory at SidecarSocketDir in the pod. node-CVM only: a kata guest serves the token route on loopback instead (GuestTokenPort), with nothing to mount.
const DigestsPort = 1019
DigestsPort is the port every inventory serves its digests endpoint on, and the only port CDS will dial for one. It is fixed rather than carried in the token, and it is privileged (<1024, IANA-unassigned), because that is what makes it an identity: answering on it at the node's address requires hostNetwork or a hostPort, and the chart's deny-host-namespaces policy withholds BOTH from tenant pods. A pod can bind any port inside its own netns, so an unprivileged port would let any workload answer as the inventory.
Both halves are load-bearing. A hostPort needs no host namespace: the CNI publishes the pod on the node's address, which is enough to be dialled here and have CDS accept the responder's key as the inventory's.
const GuestTokenPort = 8401
GuestTokenPort is the in-guest loopback port policy-monitor serves the token route on under kata, alongside the attestation-service on 8400. A kata guest holds exactly one pod and its containers share the guest's network namespace, so loopback reaches the inventory without a shared filesystem — the same transport the in-guest attestation-service already uses. Peer credentials are not needed there: with one pod per guest there is no caller to disambiguate.
const InventorySocketGID = 65532
InventorySocketGID owns the inventory's Unix socket. The inventory runs as root, but get-cert connects as the non-root c8s UID/GID over a read-only mount; a root:root 0660 socket is unreachable by that caller (connect needs write permission on the socket node), so the connect would fail closed and issuance would hang. The inventory chgrps the socket to this group and the webhook injects it as a supplemental group on the get-cert sidecar (pod_mutator.go, ensureSupplementalGroup) — together they let the non-root caller connect. Reuses the c8s distroless nonroot GID, so a default get-cert (RunAsGroup 65532) also reaches it via its primary group. Connecting to a socket is exempt from the read-only-mount write block (sockets are not regular files), so the RO mount still prevents a socket-file swap without blocking the connect.
Variables ¶
var ErrSandboxContainersUnsupported = fmt.Errorf("workloadclaims: inventory does not report per-container detail")
ErrSandboxContainersUnsupported reports an inventory that answers with digests but no per-container detail — one older than the field. Distinguished so a caller that needs (digest, argv) fails closed on it instead of silently matching on digests alone.
var ErrSandboxNotReady = errors.New("workloadclaims: inventory has no sandbox-token signer yet")
ErrSandboxNotReady reports that the inventory serves the route but has no signer yet, because the address it commits to needs a pod network that is still being configured. Distinct from ErrSandboxUnsupported because the answer differs: this one is worth waiting for, and issuing without a sandbox ID instead would bind the sandbox in CDS's ledger — first-write-wins — to a leaf that has none.
var ErrSandboxUnknown = fmt.Errorf("workloadclaims: inventory does not know this sandbox")
ErrSandboxUnknown reports that the inventory does not know the sandbox — it never admitted it, or the pod is already gone. Distinguished from a transport or policy failure so the caller can say which happened; both are fail-closed for issuance.
var ErrSandboxUnsupported = errors.New("workloadclaims: inventory does not serve the sandbox route")
ErrSandboxUnsupported reports that the inventory serves no SandboxPath route — an inventory without sandbox state or without a CDS-attested signing key. Callers proceed without a sandbox ID.
Functions ¶
func ContainerIDCandidatesForPID ¶
ContainerIDCandidatesForPID returns every 64-hex component of a process's cgroup file under procRoot (normally "/proc"), in shallowest-to-deepest path order, deduplicated. This is the kernel-derived half of caller binding: SO_PEERCRED names the PID, the cgroup names the container, and the runtime's own state names the pod — nothing the caller sends is trusted.
The caller (the inventory) MUST resolve the SHALLOWEST candidate that is a tracked container, not the deepest. A process can only ever move itself DEEPER into cgroups it creates, so its runtime-assigned container scope is always an ancestor (shallower) of any child cgroup it nests — including one it maliciously names with a victim's container ID. Picking the shallowest tracked ID therefore returns the caller's own container and skips both a nested victim ID and CRI-O's untracked parent sandbox ID.
func DigestsServerTLSConfig ¶
func DigestsServerTLSConfig(platform string, attestFunc func(ctx context.Context, customData string) (string, error), attestationApiURL string, cdsPins ratls.Pins, certTTL time.Duration) (*tls.Config, *ratls.CertManager, error)
DigestsServerTLSConfig builds the inventory's listener config for ServeDigests: it presents an RA-TLS certificate proving the inventory runs in a TEE, and requires the caller to present a hardware-attested one too. With cdsPins set the caller must satisfy them (launch measurement, and TDX RTMRs when pinned), so the endpoint discloses what a node runs only to a CDS on an expected measurement; zero pins accept any TEE on the network. UNSAFE outside development; callers warn.
func GuestInventoryEndpoint ¶
func GuestInventoryEndpoint() string
GuestInventoryEndpoint is get-cert's compiled inventory endpoint inside a kata guest. Like InventoryEndpoint it is fixed at build time: the control plane selects which of the two shapes applies, never an address, so the worst a wrong selection does is fail closed against a port nothing serves.
func InventoryEndpoint ¶
func InventoryEndpoint() string
InventoryEndpoint is get-cert's compiled inventory endpoint on node-CVM: the in-sidecar Unix socket path, whose peer credentials bind the caller to its pod.
func ListenUnix ¶
ListenUnix binds an inventory's Unix socket at socketPath: it removes a stale socket file first (so an inventory restart does not fail with EADDRINUSE), chmods the socket to 0660, and (when gid > 0) chgrps it to gid so a non-root caller in that group can connect. Caller binding is by kernel peer credentials, so the mode and group gate reachability only, not authorization.
func NodeHostCIDRs ¶
NodeHostCIDRs derives the inventory dial bound from node objects: one host route per InternalIP. A host route per node rather than a covering range, deliberately — on a CNI that assigns pod IPs from the node subnet (AWS VPC CNI, Azure CNI) every range covering the nodes covers the pods too, and the bound would look configured and be absent. A node address inside a pod range is excluded and reported instead: node and pod addresses are not separable there. Where the CNI owns IPAM and leaves podCIDR empty, that check simply does not run. The c8s CLI preflights the same derivation at install time.
func RequesterKeyDigest ¶
RequesterKeyDigest is the caller-key commitment a sandbox token carries: SHA-256 over the canonical PKIX DER of pub. Both sides derive it from a parsed key, so a non-canonical caller encoding cannot split the binding.
func ResolveAdvertiseHost ¶
ResolveAdvertiseHost returns the node IP an inventory signs into its sandbox tokens. An explicit host always wins; the chart supplies one from the installer DaemonSet's downward API (status.hostIP). cdsEndpoint is the enforcer's pull config verbatim — a URL or a bare host[:port]; a URL's host is used when it parses as one, so every enforcer derives the callback host the same way.
Inference is the fallback and is deliberately weak: it asks the routing table which local address would reach the CDS host. That answer is wrong whenever CDS is reached over loopback — which the chart's own default does, since the plugin dials the CDS NodePort on 127.0.0.1 — so it fails loudly rather than advertising something CDS could never dial back. ctx bounds the lookup's name resolution, which net.Dial would otherwise block on without limit.
func ServeDigests ¶
func ServeDigests(ctx context.Context, l net.Listener, resolver SandboxResolver, identity []byte) error
ServeDigests runs the CDS-facing digests endpoint on l until ctx is done. It serves GET SandboxDigestsPrefix+<sandboxID> only, and answers for ANY sandbox — so l MUST be a mutually-attested RA-TLS listener that admits only CDS (BuildDigestsTLSConfig). Over a plain listener this would disclose the node's running images to anyone who can reach the port.
func ServeTokens ¶
func ServeTokens(ctx context.Context, l net.Listener, resolver SandboxResolver, signers *SignerHolder) error
ServeTokens runs the token route on l until ctx is done — a unix listener on node-CVM, a guest-loopback listener under kata. It serves POST SandboxPath only, and l must stay node- or guest-local: on the socket the caller is bound by kernel peer credentials, and in the guest by the single-pod boundary. Errors from the resolver are returned as 500s — get-cert fails closed on them.
The route is registered whatever state signers is in, because under kata the listener has to claim 127.0.0.1:8401 before any workload container starts — containers share the guest's network namespace, so a listener bound late is one a workload can bind first and answer as. What the signer's state changes is the answer: see SignerHolder.
func StartDigestsEndpoint ¶
func StartDigestsEndpoint(ctx context.Context, logger *slog.Logger, resolver SandboxResolver, identity []byte, platform string, attestFunc func(ctx context.Context, customData string) (string, error), attestationApiURL string, cdsPins ratls.Pins) error
StartDigestsEndpoint binds DigestsPort and serves the identity and digests routes on it. Shared by both inventories, which differ only in where their configuration comes from.
The listener is bound before the certificate warm-up so a token never names a port nothing is listening on, and so a port conflict surfaces immediately rather than after the warm-up window. Warm-up failure is logged, not fatal: the endpoint provisions on the first handshake instead, and taking the inventory down would cost far more than a slow first callback.
func UnverifiedInventoryHost ¶
UnverifiedInventoryHost reads the inventory host out of an unverified token.
It exists for one purpose: the host says which endpoint holds the key that would verify the signature, so it must be read before verification can happen. Nothing may be trusted on its basis — a caller uses it only to select a dial target it independently constrains, and a wrong value simply yields a key under which the signature fails. The authenticated host is the one Verify returns.
func ValidateInventoryHost ¶
ValidateInventoryHost reports whether host is a usable inventory host. See parseInventoryHost for the rules.
Types ¶
type AllowlistRefresh ¶
type AllowlistRefresh struct {
Enabled bool `json:"enabled"`
// Reason explains a disabled refresh.
Reason string `json:"reason,omitempty"`
// Entries is the allowlist size actually being enforced.
Entries int `json:"entries"`
}
AllowlistRefresh reports whether an inventory's image allowlist still tracks CDS, or has fallen back to whatever it started with. It is the one channel carrying that state out of a kata guest, whose journal the operator cannot read — kubectl logs on locked-guest pods is empty.
Diagnostic only: no issuance or release decision reads it, so a guest cannot widen its own admission by lying here.
type AllowlistRefreshReporter ¶
type AllowlistRefreshReporter interface {
AllowlistRefresh() (AllowlistRefresh, bool)
}
AllowlistRefreshReporter is the optional half of SandboxResolver: an inventory that can describe its refresh posture. ok=false means it has nothing to report, which stays off the wire rather than serializing as a disabled refresh.
type CIDRHosts ¶
CIDRHosts is the static InventoryHosts parsed from operator-given CIDRs.
func ParseInventoryHosts ¶
ParseInventoryHosts builds the set from CIDR strings.
type DigestsClient ¶
type DigestsClient struct {
// contains filtered or unexported fields
}
DigestsClient is CDS's side of the callback: an RA-TLS client that verifies the inventory's attestation — pinning its launch measurement when one is configured — and presents CDS's own RA-TLS certificate, so the inventory can pin CDS in turn.
func NewDigestsClient ¶
func NewDigestsClient(ctx context.Context, platform string, attestFunc func(ctx context.Context, customData string) (string, error), attestationApiURL string, pins ratls.Pins, timeout time.Duration) (*DigestsClient, error)
NewDigestsClient builds the client. pins hold the launch digests (and any TDX RTMR pins) an inventory may present — the same allowlist CDS pins for the inventory's /attest-key EAR, so a sandbox token and the callback that follows it are held to one standard. Zero pins accept any RA-TLS-attested inventory, matching what an empty allowlist already means for the EAR: UNSAFE outside development; callers warn.
It warms its own RA-TLS certificate before returning: provisioning costs an attestation round-trip, and paying it lazily would put it inside the first pod's issuance deadline.
func (*DigestsClient) Fetch ¶
Fetch asks the inventory on host which image digests sandboxID is running. host comes from the verified sandbox token, so it names the inventory that vouched for the sandbox.
func (*DigestsClient) FetchSandbox ¶
func (c *DigestsClient) FetchSandbox(ctx context.Context, host, sandboxID string) (SandboxDigestsResponse, error)
FetchSandbox is Fetch with the per-container (digest, argv) detail secret release needs. It fails closed when the inventory reports containers it could not fully resolve, and when it reports none at all for a sandbox that has digests.
func (*DigestsClient) InventoryKey ¶
InventoryKey fetches the sandbox-token signing key of the inventory on host.
This is what gives an inventory an identity CDS can check. The key arrives over RA-TLS from DigestsPort — a privileged port in the node's own network namespace — so answering here requires a privilege the chart's deny-host-namespaces policy withholds from tenant pods. Sharing the node's launch measurement, which every pod on a node-CVM does, is not enough.
type InventoryHosts ¶
type InventoryHosts interface {
// Contains reports whether host is inside the set. An empty set contains
// nothing: callers must fail closed rather than dial anywhere.
Contains(host string) bool
// Empty reports whether the set currently holds nothing.
Empty() bool
}
InventoryHosts bounds which addresses an inventory may live at — the operator's node addresses. CDS refuses to dial anything outside the set, which is what keeps a pod (whose IP is in the pod CIDR) from standing in for a node. Implementations: CIDRHosts (operator-given CIDRs, static) and NodeHosts (derived live from the cluster's node objects).
type InventoryIdentity ¶
type InventoryIdentity struct {
PublicKey []byte `json:"public_key"`
}
InventoryIdentity is the IdentityPath answer: the inventory's sandbox-token signing key, PKIX DER. Served on the same privileged-port listener as the digests, which is what makes it an identity rather than an assertion.
type NodeHosts ¶
type NodeHosts struct {
// contains filtered or unexported fields
}
NodeHosts is an InventoryHosts derived from the cluster's node objects and swapped atomically by the caller's informer. Before the first SetNodes it holds nothing, so Contains fails closed.
type Peer ¶
type Peer struct {
// contains filtered or unexported fields
}
Peer identifies the process on the other end of an inventory connection. The PID comes from SO_PEERCRED; SO_PEERPIDFD (Linux 6.5+) additionally returns a pidfd that pins that exact process instance, so IsAlive can detect an exit between the credential read and the resolver's /proc lookup — the window a bare PID leaves open to PID reuse (docs/getcert-workload-binding.md, "Corner 1"). Close releases the pidfd.
func PeerForPID ¶
PeerForPID builds a Peer from a PID obtained out of band, with no pidfd — its liveness check is skipped (pidfdSkip). Production inventory code always uses peerFrom, which pins a pidfd; this exists only for tests exercising the resolver without a real socket.
func PeerFromConn ¶
PeerFromConn captures the peer credentials of a unix connection, for a server outside this package that binds its callers the same way. Exported so that server does not reimplement SO_PEERCRED; the returned Peer's pidfd must be released with Close.
func (Peer) Close ¶
func (p Peer) Close()
Close releases the pidfd, if any. Safe to call on a zero or test Peer.
func (Peer) IsAlive ¶
IsAlive reports whether the pinned peer process is confirmed still running. It MUST be called AFTER the resolver reads /proc for this PID: a peer that exited during resolution may have had its PID recycled, so the read cannot be trusted. It fails closed (returns false) when it cannot confirm liveness — no pidfd, a bad pidfd, or a poll error — so only a positively-confirmed live process passes.
type SandboxContainer ¶
type SandboxContainer struct {
Digest string `json:"digest"`
// Argv is the effective OCI process.args — the merged image-config and
// pod-spec command, which is what the argv policy is written against.
Argv []string `json:"argv,omitempty"`
}
SandboxContainer is one admitted container: the bytes, and what they were told to run.
func (SandboxContainer) Compare ¶
func (c SandboxContainer) Compare(o SandboxContainer) int
Compare orders containers by digest, then argv — the stable order the digests endpoint serves, so identical sandboxes report identical inventories.
func (SandboxContainer) Key ¶
func (c SandboxContainer) Key() string
Key identifies this (digest, argv) pair in an inventory's admission high-water mark, where it is the deduplication key.
The encoding MUST be injective, so it is /proc/cmdline's: NUL after every element, digest included. NUL is the one byte neither field can carry — an execve argument is a NUL-terminated C string and a digest is hex — and terminating rather than separating keeps an empty argv list distinct from a single empty argument.
A non-injective key is not merely untidy. Two distinct admissions that collide onto one key erase each other from the sandbox's record, and the erasure is invisible to CDS: it drops the container from the digests and containers views alike, so the cross-check between them still agrees and the sandbox can then be named for a workload it did not actually run.
type SandboxDigestsResponse ¶
type SandboxDigestsResponse struct {
Digests []string `json:"digests"`
Containers []SandboxContainer `json:"containers,omitempty"`
// AllowlistRefresh is the inventory's enforcement posture. Absent from an
// inventory that predates the field, and from one with nothing to report.
AllowlistRefresh *AllowlistRefresh `json:"allowlist_refresh,omitempty"`
}
SandboxDigestsResponse is the SandboxDigestsPrefix answer. Both fields describe every container ever admitted in the sandbox, not only those running now — see docs/secrets.md, "The report is a high-water mark".
Digests is the deduplicated digest set cert issuance gates on. Containers carries each container's effective argv, deduplicated by SandboxContainer.Key (the whole (digest, argv) pair, not the digest alone), so a consumer can hold a sandbox to the pair each container actually ran with.
Digests is [] (never null) for a known sandbox with no containers. Containers is absent on an inventory that predates it, which consumers must treat as "cannot answer" rather than "no containers".
func (SandboxDigestsResponse) RequireContainers ¶
func (r SandboxDigestsResponse) RequireContainers() ([]SandboxContainer, error)
RequireContainers returns the per-container detail, refusing an answer that cannot support a (digest, argv) decision.
type SandboxResolver ¶
type SandboxResolver interface {
// SandboxForPeer returns the pod sandbox ID of the calling process,
// bound by kernel peer credentials exactly like ContainersForPeer.
SandboxForPeer(peer Peer) (string, error)
// DigestsForSandbox returns every container ever admitted in the named
// sandbox — deduplicated digests for issuance, and the per-container
// (digest, argv) detail for secret release. Admitted means the container
// was let run, not that it passed a check. known=false means no such
// sandbox (a 404 on the wire); a known sandbox with no containers returns
// empty slices.
DigestsForSandbox(sandboxID string) (digests []string, containers []SandboxContainer, known bool, err error)
}
SandboxResolver is the surface an inventory implements — nri-image-policy on node-CVM (runtime sandbox state from NRI pod events) and policy-monitor in the kata guest (the guest's single pod). ServeTokens uses SandboxForPeer; ServeDigests uses DigestsForSandbox. get-cert treats a missing SandboxPath as "no sandbox ID" (ErrSandboxUnsupported).
peer carries the kernel-pinned caller identity (SO_PEERCRED PID plus an SO_PEERPIDFD liveness pin) — the caller never names its own pod. The node-CVM resolver binds peer.PID() to a pod and rechecks peer.IsAlive() after its /proc read to reject PID reuse; the kata resolver ignores it, since the guest holds exactly one pod and no disambiguation is needed.
type SandboxTokenRequest ¶
type SandboxTokenRequest struct {
PublicKey []byte `json:"public_key"`
// Nonce is the single-use CDS challenge get-cert obtained for this
// issuance. The inventory binds it into the signed token so CDS confirms
// freshness against the same challenge it consumes for the evidence — no
// clock (docs/ratls.md, "Sandbox identity").
Nonce []byte `json:"nonce"`
}
SandboxTokenRequest is the SandboxPath request body: the requester's PKIX public-key DER, which the inventory binds into the signed token so only the holder of that key can redeem it at CDS.
type SandboxTokenSigner ¶
type SandboxTokenSigner struct {
// contains filtered or unexported fields
}
SandboxTokenSigner signs sandbox tokens with an in-process P-256 key. The key never leaves the process and is not persisted; an inventory restart mints a new one, which CDS picks up because it reads the key from the inventory's own endpoint on every issuance rather than caching a credential.
func NewSandboxTokenSigner ¶
func NewSandboxTokenSigner(host string) (*SandboxTokenSigner, error)
NewSandboxTokenSigner generates the signing key. host is the IP of the node serving this inventory's digests endpoint, signed into every token so CDS knows which endpoint holds the key that verifies it.
func (*SandboxTokenSigner) PublicKey ¶
func (s *SandboxTokenSigner) PublicKey() *ecdsa.PublicKey
PublicKey is the signing key CDS fetches from the digests endpoint.
func (*SandboxTokenSigner) PublicKeyDER ¶
func (s *SandboxTokenSigner) PublicKeyDER() []byte
PublicKeyDER is PublicKey in PKIX DER, as IdentityPath serves it.
func (*SandboxTokenSigner) Sign ¶
func (s *SandboxTokenSigner) Sign(sandboxID string, requesterKeyDigest, nonce []byte) (*SignedSandboxToken, error)
Sign issues a signed sandbox token binding sandboxID to the requester-key digest and the CDS challenge nonce for this issuance.
type SignedSandboxToken ¶
type SignedSandboxToken struct {
// Token is the DER-encoded sandbox token.
Token []byte `json:"token"`
// Signature is the inventory key's ASN.1 ECDSA-SHA256 signature over the
// domain-separated Token bytes.
Signature []byte `json:"signature"`
}
SignedSandboxToken is the inventory's SandboxPath answer.
It carries no credential for the signing key. CDS resolves that key by dialing the signer's own digests endpoint on a privileged port (DigestsClient.InventoryKey), which is what distinguishes the inventory from any other TEE sharing the node's launch measurement.
func FetchSandboxToken ¶
func FetchSandboxToken(ctx context.Context, endpoint string, timeout time.Duration, requesterPub crypto.PublicKey, nonce []byte) (*SignedSandboxToken, error)
FetchSandboxToken asks the inventory at endpoint for a signed sandbox token bound to requesterPub (the caller's CSR key) and nonce (the CDS challenge for this issuance, which CDS re-checks for freshness). A 404 maps to ErrSandboxUnsupported so callers can distinguish an inventory without the route from a resolution failure, which stays fail-closed.
func (*SignedSandboxToken) Verify ¶
func (s *SignedSandboxToken) Verify(inventoryPub *ecdsa.PublicKey, requesterPub crypto.PublicKey, nonce []byte) (VerifiedSandbox, error)
Verify checks the envelope against the inventory key (which the caller must have resolved from the inventory's own endpoint and trusts), the requester key, and the CDS challenge for this request. It fails closed on a bad signature, a malformed or wrong-version token, a nonce that is not this request's challenge, or a requester-key mismatch. nonce is the single-use challenge CDS is consuming for the issuance, so a token cannot be replayed against a later request or pre-signed against a future one.
type SignerHolder ¶
type SignerHolder struct {
// contains filtered or unexported fields
}
SignerHolder carries the sandbox-token signer for a listener that must serve before the signer exists. Safe for concurrent use.
func NewPendingSignerHolder ¶
func NewPendingSignerHolder() *SignerHolder
NewPendingSignerHolder returns a holder whose signer arrives later. Until it does the route answers 503, so a caller waits rather than issuing a leaf with no sandbox ID — a binding CDS takes first-write-wins and will not revisit.
func NewSignerHolder ¶
func NewSignerHolder(signer *SandboxTokenSigner) *SignerHolder
NewSignerHolder returns a holder that already knows its answer: signer when non-nil, otherwise "this deployment issues no tokens". For a caller that resolves its signer later, use NewPendingSignerHolder.
func (*SignerHolder) Disable ¶
func (h *SignerHolder) Disable()
Disable records that no signer is coming, so callers stop waiting on one. For a failure waiting cannot fix — no CDS configured, measurements that do not parse — as distinct from a network that has not arrived yet.
func (*SignerHolder) Ready ¶
func (h *SignerHolder) Ready() bool
Ready reports whether a signer is installed, for the log line that tells an operator which posture a guest came up in.
func (*SignerHolder) Set ¶
func (h *SignerHolder) Set(signer *SandboxTokenSigner)
Set installs the signer and makes the route answer.
type VerifiedSandbox ¶
type VerifiedSandbox struct {
SandboxID string
// InventoryHost is the IP of the node whose inventory signed this token,
// read from the signed bytes.
InventoryHost string
}
VerifiedSandbox is what a valid token establishes: which sandbox the requester is in, and which node's inventory vouched for it.