Documentation
¶
Overview ¶
Package workloadidentity implements OIDC workload identity tokens for sandbox containers, following the GitHub Actions OIDC pattern.
Trust Model ¶
Each cluster is its own OIDC issuer with an independent signing key and JWKS endpoint. New clusters sign with RS256 (RSA), the universally supported default; clusters provisioned before that default keep their EdDSA key advertised in JWKS for verification while new tokens are signed with a freshly generated RS256 key. Miren Cloud is not in the trust path — it only contributes organization_id and cluster_id as claim metadata during registration. This per-cluster model means external verifiers (e.g., AWS IAM OIDC) must configure trust per cluster rather than once for all of Miren. A future central issuer could reduce that to one trust config scoped by claims, but would introduce a single point of compromise for all clusters.
Issuer URL (iss claim) ¶
The issuer URL is the cluster's cryptographic identity anchor — it's baked into every token and pinned in external trust configurations. For cloud-registered clusters, this is the provisioned DNS hostname (e.g., https://cluster-abc.miren.systems). For bare-metal clusters without registration, it falls back to cfg.TLS.AdditionalNames[0], meaning the identity anchor is determined by config list order. This fallback is intentionally simple for v1; a more deliberate selection mechanism (e.g., explicit --issuer-url flag) may be warranted if bare-metal OIDC federation sees adoption.
A cluster with no hostname at all still gets an issuer, anchored at LocalIssuerURL. Identity is not conditional on being externally addressable: the cluster's own services authenticate to each other with these tokens and verify them in-process against the signing keys, which needs no DNS, no reachability, and no publicly trusted certificate. Only federation to an outside party needs those, and that is a property of the anchor rather than a mode the cluster is in.
Index ¶
- Constants
- type IdentityType
- type Issuer
- func (iss *Issuer) DiscoveryDocument() []byte
- func (iss *Issuer) Hostname() string
- func (iss *Issuer) IssueSystemWorkloadToken(workload SystemWorkload, opts TokenOptions) (string, error)
- func (iss *Issuer) IssueToken(app, sandboxID string) (string, error)
- func (iss *Issuer) IssueTokenWithOptions(app, sandboxID string, opts TokenOptions) (string, error)
- func (iss *Issuer) IssuerURL() string
- func (iss *Issuer) JWKSDocument() ([]byte, error)
- func (iss *Issuer) PublicKey() any
- func (iss *Issuer) VerifySystemWorkloadToken(tokenString, expectedAudience string, expectedWorkload SystemWorkload) (*WorkloadClaims, error)
- func (iss *Issuer) VerifyToken(tokenString, expectedAudience string) (*WorkloadClaims, error)
- type IssuerConfig
- type Subject
- type SystemWorkload
- type TokenIssuer
- type TokenOptions
- type WorkloadClaims
Constants ¶
const ( DefaultTTL = 1 * time.Hour MaxTTL = 24 * time.Hour MinTTL = 60 * time.Second // DefaultAudience is the audience stamped on a token whose caller did not // ask for a specific one. Callers guarding a particular service should // always request their own audience and verify it, so that a token minted // for one service cannot be replayed against another. DefaultAudience = "miren" )
const LocalIssuerURL = "https://cluster.local"
LocalIssuerURL anchors a cluster that has no hostname to advertise.
It is deliberately a name the cluster already owns rather than a synthetic placeholder: cluster.local is how the cluster refers to itself internally, resolved to whatever is locally appropriate (the registry router on the coordinator, the coordinator's address on a runner). Nothing outside can resolve it at all, which is the honest signal that such a cluster cannot federate to an external party until it is given a real hostname.
Note that this is an identity anchor, not an endpoint. It is compared as a string when verifying, never fetched, so it does not matter that the resolution it names is set up later in startup or means different things in different processes.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type IdentityType ¶ added in v0.13.0
type IdentityType string
IdentityType names the kind of principal a token represents.
The value travels as its own claim rather than being inferred from the subject. This lets verifiers authorize an identity class without reparsing the subject grammar. Tokens issued before this claim existed carry no identity_type, which reads as "not a system workload" and so fails closed for system-only resources.
It is a defined type so that switches over it are exhaustiveness-checked, the way rpc.AuthMethod already is. Note that this buys nothing at the trust boundary itself: a token arriving from a caller decodes into whatever string it contained, so verification still has to compare the value rather than assume it is one of the constants below.
const ( IdentityTypeSandbox IdentityType = "sandbox" IdentityTypeSystem IdentityType = "system" )
type Issuer ¶
type Issuer struct {
// contains filtered or unexported fields
}
func NewIssuer ¶
func NewIssuer(cfg IssuerConfig) (*Issuer, error)
func (*Issuer) DiscoveryDocument ¶
func (*Issuer) IssueSystemWorkloadToken ¶ added in v0.13.0
func (iss *Issuer) IssueSystemWorkloadToken(workload SystemWorkload, opts TokenOptions) (string, error)
IssueSystemWorkloadToken mints a token identifying a Miren-owned workload (the sandbox controller, a telemetry writer) rather than a customer workload.
System workload tokens let Miren's own code authenticate to cluster-internal services without a second credential system: they are minted by the same issuer, verified the same way, and inherit the short lifetimes that make revocation tractable. They are deliberately distinguishable from sandbox tokens by the identity_type claim, because the services they open are precisely the ones a sandbox must not reach.
Callers choose an audience scoped to the service they intend to reach, since one workload may call several services. The receiving service verifies that audience and the expected workload together so a token minted for one workload or service cannot be replayed against another.
func (*Issuer) IssueTokenWithOptions ¶
func (iss *Issuer) IssueTokenWithOptions(app, sandboxID string, opts TokenOptions) (string, error)
func (*Issuer) JWKSDocument ¶
func (*Issuer) VerifySystemWorkloadToken ¶ added in v0.13.0
func (iss *Issuer) VerifySystemWorkloadToken(tokenString, expectedAudience string, expectedWorkload SystemWorkload) (*WorkloadClaims, error)
VerifySystemWorkloadToken verifies a token and additionally requires that it identifies the expected Miren-owned system workload rather than a customer workload or a different system workload.
Services that only system workloads may reach should call this rather than VerifyToken, so neither the identity type nor workload authorization can be forgotten at a call site. Sandbox tokens are signed by the same key and will verify cleanly; these claims are the only thing separating them.
func (*Issuer) VerifyToken ¶ added in v0.13.0
func (iss *Issuer) VerifyToken(tokenString, expectedAudience string) (*WorkloadClaims, error)
VerifyToken checks a token minted by this issuer and returns its claims.
This is the in-process verification path, for callers that live in the same process as the signing key (the coordinator). It reads the public keys directly and performs no network I/O, so it neither depends on nor races the JWKS endpoint. Verifiers in other processes should fetch JWKS over the issuer's discovery document instead — see pkg/oidcauth's Validator.
expectedAudience is required. A token is only accepted if it names that audience, which is what stops a token minted for one service being replayed against another.
type IssuerConfig ¶
type Subject ¶ added in v0.13.0
type Subject struct {
// contains filtered or unexported fields
}
Subject is the structured identity rendered into a workload token's sub claim. Its segments are private so every subject the issuer accepts has passed through the delimiter validation in newSubject.
type SystemWorkload ¶ added in v0.13.0
type SystemWorkload string
SystemWorkload names a Miren-owned workload that may receive an identity. System workloads are a closed set: adding one is a code change, not a runtime configuration operation.
const ( SystemWorkloadSandboxController SystemWorkload = "sandboxcontroller" SystemWorkloadTelemetryWriter SystemWorkload = "telemetrywriter" SystemWorkloadBuildKit SystemWorkload = "buildkit" )
func ParseSystemWorkload ¶ added in v0.13.0
func ParseSystemWorkload(value string) (SystemWorkload, error)
ParseSystemWorkload converts the string representation used on the wire into a known system workload.
type TokenIssuer ¶
type TokenIssuer interface {
IssueToken(app, sandboxID string) (string, error)
IssueTokenWithOptions(app, sandboxID string, opts TokenOptions) (string, error)
IssueSystemWorkloadToken(workload SystemWorkload, opts TokenOptions) (string, error)
IssuerURL() string
}
TokenIssuer is the minting surface the sandbox controller depends on. The concrete *Issuer satisfies it directly (the coordinator holds the signing key). Distributed runners have no signing key, so they supply an implementation that proxies minting to the coordinator over RPC.
type TokenOptions ¶
type WorkloadClaims ¶
type WorkloadClaims struct {
jwt.RegisteredClaims
OrganizationID string `json:"organization_id,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
App string `json:"app,omitempty"`
// SandboxID deliberately keeps its unconditional encoding: it predates
// system workload tokens and external verifiers may already federate on its
// presence. System workload tokens therefore carry an empty sandbox_id
// rather than omitting it.
SandboxID string `json:"sandbox_id"`
IdentityType IdentityType `json:"identity_type,omitempty"`
SystemWorkload SystemWorkload `json:"system_workload,omitempty"`
}