Help us shape the future of Go! Take the 2026 Go Developer Survey

token

package
v1.36.5-stratum.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

Documentation

Overview

Package token implements `stratum token create`: it mints a bootstrap token, stores the bootstrap-token Secret, wires the node-bootstrap and node-renewal RBAC with upstream bootstrap auto-approval reserved and empty (the operator approves joins), publishes the cluster-info ConfigMap (so agents can discover the CA), and prints the join command. This replaces the pre-signed cert bundle (oss-mkbundle.sh) — the CA private key never leaves the server.

Index

Constants

View Source
const DefaultTokenTTL = 24 * time.Hour

DefaultTokenTTL is the lifetime stratum stamps on a freshly minted bootstrap token unless the operator overrides it with --ttl. tokencleaner — enabled in the rendered controller-manager argv (version.ControllerManagerControllersFlag) — reaps the token Secret once this elapses, so a token pasted into a runbook, CI secret or terraform variable stops being a working join credential (and a CA-injection key) on its own.

Variables

This section is empty.

Functions

func CACertHash

func CACertHash() (string, error)

CACertHash returns the "sha256:<hex>" of the CA's SubjectPublicKeyInfo. During a CA rotation pki.ReadCARotation names the staged root as the pin anchor (it is present in the published cluster-info bundle), so a token is pinned against it — that pin stays valid after finalize, when the staged root becomes the live ca.crt.

func CACertHashFor

func CACertHashFor(name string) (string, error)

CACertHashFor returns the "sha256:<hex>" SubjectPublicKeyInfo pin of the named CA cert basename in version.PKIDir.

func Create

func Create(w io.Writer, cs kubernetes.Interface, serverURL string, ttl time.Duration, controlPlane bool, confirm func(context.Context) error) (string, string, *metav1.OwnerReference, error)

Create mints a token + sets up everything a node needs to join (bootstrap Secret, RBAC, cluster-info), returning the token string "<id>.<secret>", the CA-cert hash, and an OwnerReference to the token's Secret. serverURL is the apiserver the joiner will reach; ttl is the token lifetime (0 = never expires, see bootstrapExpiration). controlPlane mints a control-plane join token, which alone carries k8sutil.ControlPlaneBootstrapGroup and so may read stratum-certs.

cs is the client every call rides — the caller decides the credential, not this function: the CLI hands the admin client from this node's admin.conf, the MCP token_create handler the node connector's. A minting operation the front end cannot scope is one a narrowed server credential could be silently widened to serve.

The writer leads, so the mint's one print — the control-plane signing wait — goes to the caller's stream: the CLI passes os.Stdout byte-identically, the MCP handler the buffer its result is built from.

The returned OwnerReference lets the --control-plane path tie the stratum-certs Secret to this token, so tokencleaner deleting the expired token garbage-collects the shared-cert material too — it must never outlive the tokens that grant read access to it. It is nil when the token's UID could not be established.

confirm is the caller's maintenance-lease recheck (nil when the caller holds no fence): it runs immediately before the bootstrap-Secret write and inside each publication's conflict retry, so a lease lost while an earlier call stalled refuses the next mutation instead of publishing this node's trust snapshot over the successor's.

func RepinCA

func RepinCA() error

RepinCA republishes the cluster-info ConfigMap with the current CA trust bundle (ca-bundle.crt), preserving the currently-advertised apiserver endpoint. Call after a CA-rotation phase changes the bundle so joiners fetch the right trust set (phase trust-new adds the new CA; finalize drops the old one).

func RepinCAWith

func RepinCAWith(ctx context.Context, cs kubernetes.Interface) error

RepinCAWith is RepinCA on the caller's client and context, for a caller that decides the credential and bounds the requests itself.

func RevokeOnFailure

func RevokeOnFailure(tokenStr string, cause error) error

RevokeOnFailure deletes the bootstrap-token Secret for tokenStr because the command that minted it is failing, and returns cause — folding in a warning if the credential could not be removed.

createToken's own rollback disarms the moment it returns successfully, but the COMMAND is not finished then: `stratum token create --control-plane` still has to mint a certificate-key and upload the shared certs. Without this the advertised guarantee — this command owns what it creates until it succeeds — held for token.Create and was merely advertised for the command.

The caller must not have printed the token yet. Revoking a credential already on the operator's terminal would trade a stranded credential for output that lies, which is worse; cmd/stratum therefore prints nothing until every step has succeeded.

func UnpublishKubeletClientCA

func UnpublishKubeletClientCA() error

UnpublishKubeletClientCA removes the published kubelet client-CA bundle, so a joining worker's kubelet follows the cluster root again.

Types

type Grant

type Grant struct {
	ServerURL  string // the apiserver the joiner sends the credential to
	Token      string // "<id>.<secret>" bootstrap token
	CACertHash string // the "sha256:<hex>" pin the joiner verifies the control plane against
	ExpiresIn  time.Duration
	// CertKey is the shared-certificate decryption key, present only for a
	// control-plane token. It is a second, more sensitive credential; how it
	// travels is each front end's handover decision (the CLI writes it to a
	// 0600 file and hands over the path; the MCP tool returns it in its
	// result).
	CertKey   string
	JoinFlags JoinFlags
}

Grant is a minted join credential together with everything a joining node needs alongside it. It travels to the caller's handover callback as data, so a front end cannot hand over a grant that omits a piece — the omission that once kept all four join flags off token_create.

func Issue

func Issue(w io.Writer, deps IssueDeps, serverURL string, ttl time.Duration, controlPlane bool, flags JoinFlags, handover func(Grant) (bool, error)) (Grant, error)

Issue mints a join token and owns it until it has been handed over. The writer leads: it travels into deps.Create, so the mint's progress reaches the caller's stream (the CLI's os.Stdout, the MCP handler's buffer) instead of printing to one the caller does not control.

The sequence: deps.Create mints the credential; for a control-plane token the certificate key is generated and the shared certificates are uploaded under the mint's owner reference (so the uploaded material is garbage-collected with the token); then the assembled grant goes to the caller's handover callback exactly once, which delivers it to whoever asked for it and reports whether the credential bytes actually arrived.

The ownership rule the handover defines:

  • While nothing has been delivered, a failing step takes the minted token back — the caller never saw it, so a live token behind a reported failure would be a stranded credential nobody can name to revoke.
  • Once the handover reports delivery, the token is the recipient's, and Issue never revokes it — not even when the same handover also reports a failure, which is a join command only partially written or a broken pipe after the token reached the terminal. Revoking a credential the caller may already hold would trade a stranded credential for output that lies; the error is returned instead, and the judgement about what was exposed stays with the front end, which knows what was delivered.
  • A handover that reports neither delivery nor an error gave up the grant, so the token is revoked with a cause of its own rather than left behind a call that handed over nothing.
  • A failed mint revokes nothing: no token exists, and token.Create already rolls its own Secret back — a revoke here would run that rollback twice and call back a credential that was never created.

On success it returns the same grant it handed over. On failure it returns a zero grant and the cause, carried through the revoke's warning when one ran.

type IssueDeps

type IssueDeps struct {
	// Confirm rechecks an acquired maintenance lease before the next mutation.
	// RekeyGuard refuses publication while another rotation remains unfinished.
	Confirm    func() error
	RekeyGuard func() error

	// Create mints the bootstrap token: the Secret, its RBAC and the
	// cluster-info publication. It takes the writer Issue was handed, so the
	// mint's signing-wait progress reaches the caller's stream, and returns
	// the token string, the CA pin and the token Secret's owner reference,
	// which the shared-certificate upload uses so the uploaded material is
	// garbage-collected with the token. It takes the same maintenance-lease
	// recheck Confirm carries, and runs it immediately before each write: a
	// lease lost while an earlier call stalled refuses the next publication
	// instead of overwriting the successor's trust.
	Create func(w io.Writer, serverURL string, ttl time.Duration, controlPlane bool, confirm func(context.Context) error) (token, caHash string, owner *metav1.OwnerReference, err error)
	// NewCertKey generates the shared-certificate decryption key, and
	// UploadCerts publishes the shared certificates under the mint's owner
	// reference, rechecking the lease immediately before the Secret write.
	// Both run for a control-plane token only.
	NewCertKey  func() (string, error)
	UploadCerts func(certKey string, owner *metav1.OwnerReference, confirm func(context.Context) error) error
	// Revoke takes back a minted token because the issuance is failing, and
	// returns cause — folding in a warning when the credential could not be
	// removed, because that warning is the only thing that ever tells an
	// operator a live credential is stranded.
	Revoke func(tok string, cause error) error
}

IssueDeps are the cluster effects Issue performs, injectable so the credential lifecycle is testable without a live apiserver. They mirror the seams the two front ends inject today — pkg/cli's tokenCmd and the MCP handler's tokenDeps — because Issue replaces the sequence both of them re-implemented by hand.

type JoinFlags

type JoinFlags struct {
	PodCIDR       string `` /* 203-byte string literal not displayed */
	ServiceCIDR   string `` /* 215-byte string literal not displayed */
	ClusterDomain string `` /* 216-byte string literal not displayed */
	Profile       string `` /* 133-byte string literal not displayed */
}

JoinFlags are the flags a joining node must be given so it matches the cluster it joins. The pod and service ranges and the DNS domain cannot be discovered by a node after it joins — a worker learns them only from its join line — and --profile is the only way a worker learns the profile the cluster runs (with it, the kubelet's seccompDefault and the rest of the profile).

It is data, not a rendered line: a field is empty when the cluster sits at the default for it, so each front end renders --pod-cidr, --service-cidr and --cluster-domain only on a non-default cluster and --profile only on a cluster that runs one, and no front end re-derives the rule to drift from it.

func LoadJoinFlags

func LoadJoinFlags() (JoinFlags, error)

LoadJoinFlags derives the join flags from what this control plane persists: the network config for the ranges and domain, and the node's own state for the profile.

A lost netconfig is not a default cluster. netcfg.Load recovers the ranges and domain a lost file describes from node state — the state the control plane recorded at init — and falls back to the built-in defaults only for a node with neither, so a custom-network control plane prints join lines for the network it actually runs.

A netconfig that exists but cannot be parsed is an error, not a default cluster: treating it as one printed a worker join line without --service-cidr, the join succeeded, and the worker's kubelet pointed pods at the default DNS address on a cluster whose DNS is elsewhere, with nothing saying why.

The same care applies to node state: a state that exists but cannot be read is an error rather than a silent profile-less join line, while a node that has no state yet runs no profile, so the join lines carry no --profile. A profile this binary does not register is refused: the join line would name it while the bundle this node publishes carries none of its shared secrets, a grant no joiner could use.

type TokenInfo

type TokenInfo struct {
	ID      string
	Expires *time.Time // nil = never expires (unless Malformed)
	// Malformed is set when the Secret carries a non-empty but unparseable
	// expiration. The bootstrap-token controller treats such a value as
	// invalid/expired, NOT as non-expiring, so it must be reported distinctly from
	// a genuinely non-expiring token rather than shown as "never".
	Malformed bool
}

TokenInfo is one bootstrap token's identity and lifetime, for `stratum token list`.

func List

func List() ([]TokenInfo, error)

List returns every bootstrap-token Secret in kube-system with its expiry, so an operator can see which join tokens are live and when they age out. Sorted by id for stable output.

func ListPage

func ListPage(ctx context.Context, cs kubernetes.Interface, limit int64, cont string) ([]TokenInfo, string, error)

ListPage lists one bounded page using the caller's client and cancellation. Only token IDs and expiry metadata are returned; secret halves stay internal.

Jump to

Keyboard shortcuts

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