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 ¶
const MaxTTL = 15 * time.Minute
MaxTTL — срок бутстрап-удостоверения.
С переводом на свою чеканку это решение стало НАШИМ: подпись наша, значит и срок наш, и он не выводится из чужого ответа. Величина мала намеренно — токен несёт cluster-admin и существует ради того, чтобы получить ПЕРВЫЙ доступ, а не чтобы им пользоваться.
Variables ¶
ErrMintingUnavailable — выпустить сейчас нечем: подписывающего ключа нет либо ключница недоступна.
ЗАМЕНА, А НЕ ПЕРЕИМЕНОВАНИЕ прежнего `ErrIssuerUnavailable`. Прежний признак назывался недоступностью ИЗДАТЕЛЯ, и на пути, где издателя нет, он послал бы оператора чинить сторону, до которой процесс не достаёт ни разу. Предмет у признака теперь свой: наша ключница.
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 ¶
func (h *Handler) MintBootstrapToken(ctx context.Context, req *iamv1.MintBootstrapTokenRequest) (*iamv1.MintBootstrapTokenResponse, error)
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 ¶
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.