bootstrap_token

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

handler.go — thin gRPC transport for InternalBootstrapTokenService.

ban #6: registered ONLY on the cluster-internal listener (:9091), never on the external TLS endpoint (the `Internal…Service` name is 404'd on the public listener by HasInternalSuffix). No business logic here — parse → use-case → format.

Package bootstrap_token — the InternalBootstrapTokenService use-case (#58): idempotently provision the singleton bootstrap-admin ServiceAccount's Hydra OAuth client + mapping, then broker a short-lived RS256 access-token for it via the existing Hydra client_credentials exchange (aud = https://{API_DOMAIN}).

The bootstrap SA row + its cluster system_admin grant are seeded by migration 0058 (deterministic id → DB-singleton). This use-case provisions only the runtime Hydra-backed halves (the OAuth client + its 1:1 service_account_oauth_clients mapping), gated by the UNIQUE(sva_id) mapping index + a transaction-scoped advisory lock (winner-only external create, IBT-03), and mints the token. The signing key is env-held (k8s Secret), NEVER persisted in the DB — security.md secrets-at-rest posture.

iface.go — narrow port interfaces for the bootstrap-token use-case.

Clean Architecture: the use-case depends on these; concrete adapters live in internal/repo/kaname/pg (BootstrapStore) and internal/bootstraptokenwire (LocalMinter, over the platform token signer), wired in cmd/kaname. No pgx / grpc imports here (only the domain DTOs, mirroring sa_keys).

───────────────────────────────────────────────────────────────────────────── ЧЕГО ЗДЕСЬ БОЛЬШЕ НЕТ — И ПОЧЕМУ ЭТО СНЯТО, А НЕ ОТСТАВЛЕНО (задача #1119)

Здесь стояли два порта к внешнему поставщику удостоверений: заведение его OAuth-клиента (`OAuthClientAdmin.CreateOAuthClient`) и обмен подписанного утверждения на токен (`TokenExchanger.Exchange`), плюс признак его недоступности. Оба сняты вместе со своим предметом: удостоверение чеканит НАШ подписант (`LocalMinter`), и дороги к поставщику на этом пути нет вовсе.

Отставить их «на случай посадки без своей чеканки» было бы хуже, чем снять. Порт без вызывающего неотличим от исправного, а страж старта, требующий пригодного адреса и якоря для стороны, с которой процесс не разговаривает, отправляет оператора чинить дорогу, по которой никто не пойдёт.

Техника, ради которой эти порты стоило перечитать перед снятием, — обёртывание причины отказа так, чтобы наружу ушёл фиксированный текст, а в журнал приехало то, что ответила сеть, — ПЕРЕНЕСЕНА на нашу полосу (`bootstraptokenwire.localMint`, проба `cause_survives_test.go`), а не потеряна вместе с ними.

keys.go — derive the PUBLIC key of the bootstrap client from the env-held bootstrap ES256 private key. The private half is supplied at wire-time from a k8s Secret and is NEVER persisted — iam records only the public half (parity with the SA-key posture).

───────────────────────────────────────────────────────────────────────────── ЧТО ЗДЕСЬ СНЯТО ВМЕСТЕ СО СВОИМ ПРЕДМЕТОМ (задача #1119)

Файл проецировал наш открытый ключ в ФОРМУ ПОСТАВЩИКА (его JWK с его `kid`), чтобы тот принял нашу подпись. Формы поставщика на этом пути больше нет: удостоверение подписывает наш подписант своим ключом из ключницы, и `kid` проставляет он.

Осталось то, у чего предмет НЕ исчез: разбор ключа и его проверка (PKCS#8, ECDSA, кривая P-256) плюс открытая половина в форме SPKI — она пишется в строку соответствия и остаётся НАШЕЙ записью о ключе бутстрап-клиента, которой пользуется приёмная сторона выдачи по предъявленному ключу.

local_mint.go — НАША чеканка бутстрап-удостоверения (задача #1119, Ф4б эпика #896).

───────────────────────────────────────────────────────────────────────────── ПОЧЕМУ ЭТО НЕ «ЕЩЁ ОДИН ПОРТ», А СМЫСЛ ВСЕЙ ФАЗЫ

Бутстрап — ЕДИНСТВЕННОЕ удостоверение, которым кластер поднимают с нуля. Пока его выдавал внешний поставщик, свежий клон не поднимался в боевой посадке, пока тот не поднят и не знает нашего ключа: самодостаточность упиралась в чужую сторону ровно в той точке, где своей ещё нет ничего.

───────────────────────────────────────────────────────────────────────────── ЧТО ПРИЕХАЛО ВМЕСТЕ С ПЕРЕВОДОМ — И ЭТО НЕ ПОБОЧНЫЙ ЭФФЕКТ

Удостоверение стало НАШИМ токеном по всем осям, включая отзыв: наш авторитет отзыва читает его на предъявлении (`tokenintrospecthttp`), потому что издатель у него теперь наш. Пока подписывал поставщик, отозвать выданное можно было только у него — то есть у стороны, недоступность которой и есть тот случай, ради которого бутстрап существует.

───────────────────────────────────────────────────────────────────────────── ЧЕГО ЗДЕСЬ НЕТ НАМЕРЕННО

Ни одного поля, которое выбирал бы вызывающий. Принципал выведен детерминированно (`DeriveIdentity`), адресат — настройкой контура, срок — политикой. Вход, позволяющий назвать субъекта, был бы не бутстрапом, а универсальным ключом.

Index

Constants

View Source
const MaxTTL = 15 * time.Minute

MaxTTL — срок бутстрап-удостоверения.

С переводом на свою чеканку это решение стало НАШИМ: подпись наша, значит и срок наш, и он не выводится из чужого ответа. Величина мала намеренно — токен несёт cluster-admin и существует ради того, чтобы получить ПЕРВЫЙ доступ, а не чтобы им пользоваться.

Variables

View Source
var ErrMintingUnavailable = errors.New("bootstrap token: minting is unavailable")

ErrMintingUnavailable — выпустить сейчас нечем: подписывающего ключа нет либо ключница недоступна.

ЗАМЕНА, А НЕ ПЕРЕИМЕНОВАНИЕ прежнего `ErrIssuerUnavailable`. Прежний признак назывался недоступностью ИЗДАТЕЛЯ, и на пути, где издателя нет, он послал бы оператора чинить сторону, до которой процесс не достаёт ни разу. Предмет у признака теперь свой: наша ключница.

View Source
var ErrSigningKeyNotConfigured = errors.New("bootstrap token: signing key not configured")

ErrSigningKeyNotConfigured — the bootstrap signing key env/Secret is absent. The mint path fails closed (no token) rather than fabricating a credential.

Functions

This section is empty.

Types

type BootstrapStore

type BootstrapStore interface {
	// LockAndGet takes the bootstrap provisioning advisory lock within tx, then
	// returns the existing mapping (found=false when not yet provisioned).
	LockAndGet(ctx context.Context, tx service.Tx) (c domain.ServiceAccountOAuthClient, found bool, err error)
	// InsertMapping persists the mapping row within tx (public key only; the
	// private half is env-held, never stored).
	InsertMapping(ctx context.Context, tx service.Tx, c domain.ServiceAccountOAuthClient) error
}

BootstrapStore — the singleton provisioning port for the bootstrap OAuth-client mapping (a service_account_oauth_clients row). LockAndGet serialises concurrent first-callers via a transaction-scoped advisory lock (released on commit/ rollback) so the mapping row is written exactly once and the losers read the winner's row instead of colliding on it (IBT-03); UNIQUE(sva_id) on the mapping is the DB backstop.

Строка соответствия остаётся и после перевода на свою чеканку: это НАША запись о бутстрап-клиенте — его идентификатор, его открытый ключ и его алгоритм, — а не зеркало чужой регистрации. Ею же резолвится состав утверждений выпускаемого токена.

type Config

type Config struct {
	// SigningKeyPEM — the bootstrap ES256 (P-256, PKCS#8) private key PEM,
	// supplied from a k8s Secret. Empty → mint disabled (fail-closed,
	// ErrSigningKeyNotConfigured).
	//
	// Ключ ОСТАЁТСЯ и после перевода на свою чеканку, но роль у него одна из
	// двух прежних: он больше не подписывает утверждение поставщику — им
	// заводится открытая половина, записываемая в строку соответствия как наша
	// запись о ключе бутстрап-клиента. И он же остаётся ручкой, которой контур
	// включают: страж старта читает ЕЁ, поэтому «включено» у стража и у рантайма
	// не может разойтись.
	SigningKeyPEM string
	// GatewayAudience — адресат выпускаемого удостоверения (https://{API_DOMAIN}):
	// то, что принимает боевой край.
	GatewayAudience string
	// MaxTTL overrides the package default lifetime (zero → MaxTTL).
	MaxTTL time.Duration
}

Config — mint policy + the env-held bootstrap key.

type Handler

type Handler struct {
	iamv1.UnimplementedInternalBootstrapTokenServiceServer
	// contains filtered or unexported fields
}

Handler implements iamv1.InternalBootstrapTokenServiceServer.

func NewHandler

func NewHandler(mint *MintUseCase) *Handler

NewHandler assembles the handler. Composition root: cmd/kaname/wiring.go.

func (*Handler) MintBootstrapToken

MintBootstrapToken mints the bootstrap RS256 Bearer (synchronous — not an Operation, D-2).

type Identity

type Identity struct {
	// SvaID — the bootstrap ServiceAccount id (`sva…`, seeded by 0058).
	SvaID string
	// SocID — the service_account_oauth_clients mapping id (`soc_…`); also the
	// JWK `kid` registered with Hydra and stamped in the client_assertion header.
	SocID string
	// ClientID — the Hydra OAuth2 client_id (`kacho-bootstrap-admin`).
	ClientID string
	// CreatedByUserID — the system owner user (`usr…`, FK for the mapping row).
	CreatedByUserID string
}

Identity — the deterministic bootstrap identity (derived; matches migration 0058's seeded rows).

func DeriveIdentity

func DeriveIdentity() Identity

DeriveIdentity computes the deterministic bootstrap identity. Pure; no I/O.

type LocalMinter

type LocalMinter interface {
	MintToken(ctx context.Context, in MintInput) (MintOutput, error)
}

LocalMinter — НАШ подписант с точки зрения контура бутстрапа.

Порт объявлен здесь, у вызывающего: контур не знает ни про ключницу, ни про обёртку приватной половины, ни про то, каким алгоритмом подписано и какой состав утверждений кладётся в токен.

type MintInput

type MintInput struct {
	// SAKeyID — НАШ идентификатор строки реестра бутстрап-клиента (`soc_…`).
	// Им резолвится состав утверждений — тем же объявлением, каким он
	// собирается на всех прочих полосах выдачи, чтобы токен, выданный этим
	// путём, не отличался от прежнего у ПРИНЦИПАЛА.
	SAKeyID string
	// PrincipalID — принципал, за которого говорит токен (`sva…`). Резолвится
	// строкой реестра, а не запросом.
	PrincipalID string
	// Audience — адресат: край, которому удостоверение предъявляют.
	// Незаданный адресат означал бы «любой».
	Audience string
	// TTL — срок выпускаемого удостоверения. Решение НАШЕ, потому что подпись
	// наша: величина объявлена контуром, а не выведена из чужого ответа.
	TTL time.Duration
}

MintInput — запрос на выпуск бутстрап-удостоверения НАШИМ подписантом.

type MintOutput

type MintOutput struct {
	AccessToken string
	IssuedAt    time.Time
	ExpiresAt   time.Time
}

MintOutput — выпущенное удостоверение.

Моменты выпуска и истечения приходят ИЗ ПОДПИСАННОГО токена, а не считаются заново вызывающим. Второй расчёт того же предмета разошёлся бы с первым молча — и разошёлся бы в ту сторону, где предъявитель считает cluster-admin удостоверение умершим, пока край его ещё принимает.

type MintUseCase

type MintUseCase struct {
	// contains filtered or unexported fields
}

MintUseCase idempotently provisions the bootstrap client mapping (if absent) and mints a short-lived token for the bootstrap SA with OUR signer.

func NewMintUseCase

func NewMintUseCase(store BootstrapStore, txb service.TxBeginner, minter LocalMinter, cfg Config) *MintUseCase

NewMintUseCase constructs. MaxTTL falls back to the package default.

func (*MintUseCase) Execute

func (u *MintUseCase) Execute(ctx context.Context) (*Result, error)

Execute provisions (idempotently) and mints. Fail-closed: no signing key → UNAVAILABLE; nothing to sign with → UNAVAILABLE (no token, no leak).

Срок НЕ является параметром запроса и никогда им не был: удостоверение подписано, и число в ответе не может укоротить подписанный предъявитель. Оно сообщает то, что стоит В ТОКЕНЕ, и берётся из него же — заниженный срок оставил бы живое cluster-admin удостоверение в обращении, потому что его никто не ищет: все считают, что оно умерло.

func (*MintUseCase) WithLogger

func (u *MintUseCase) WithLogger(l *slog.Logger) *MintUseCase

WithLogger wires the failure logger (composition root). nil → no logging.

type Result

type Result struct {
	AccessToken string
	TokenType   string
	ExpiresIn   int64
	ExpiresAt   time.Time
	PrincipalID string
	IssuedAt    time.Time
}

Result — the minted bootstrap token (transport-agnostic).

Jump to

Keyboard shortcuts

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