jwksproxyhttp

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: 20 Imported by: 0

Documentation

Overview

Package jwksproxyhttp — cluster-ВНУТРЕННИЙ публикатор наборов проверочных ключей.

Записей ДВЕ, и у каждой свой объявленный путь

Платформа чеканит свои токены сама (задача #897), поэтому публикуются две записи, а не одна:

  • НАША (keyset.go) — ПРОЕКЦИЯ ключницы: только наши ключи, только публичный материал, собственного кэша нет by construction;
  • ЗЕРКАЛО прежнего издателя (этот файл) — тонкий кэширующий обратный посредник его публичного набора, побайтовое равенство источнику. Оно сохраняется до последней фазы отказа от внешнего OAuth-сервера и уходит вместе с ним: запись, которой больше нечего зеркалить, — находка, а не «оставим на всякий случай».

Объединять наборы в один документ было бы дешевле и уничтожило бы ровно ту защиту, ради которой развязка заводится: ключ одного издателя проверял бы токен, объявляющий другого. Привязка «издатель → путь» — в binding.go.

Что верно ИМЕННО для зеркала

Оно отдаёт чужие идентификаторы ключей и НИКОГДА наши: наши живут в своей записи. Наш идентификатор здесь был бы гарантированным промахом проверки — потребитель этой записи ищет ключи прежнего издателя.

Fail-closed. A cold cache + an unavailable Hydra (network error / non-200 / empty keyset / timeout) yields 502 — never an empty 200, never a substitute keyset. A warm cache within TTL survives a brief Hydra blip (bounded-stale); once the TTL elapses with Hydra still down the endpoint degrades to fail-closed, never indefinitely-stale.

Per-call timeout. The upstream fetch uses a dedicated http.Client WITH a Timeout plus a per-request context deadline — never http.DefaultClient (architecture.md: DefaultClient has no Timeout, so a hung/half-open Hydra would wedge the goroutine forever).

Снятие authN — задокументированное исключение, и его обоснование СМЕНИЛОСЬ

Маршруты НАБОРА КЛЮЧЕЙ не требуют аутентификации осознанно. Прежде это обосновывалось тем, что публикуется ЧУЖОЕ зеркало; после того как платформа стала чеканить сама, обоснование другое и записано здесь, чтобы двух мест об одном предмете не завелось: на проводе — ТОЛЬКО ПУБЛИЧНЫЙ МАТЕРИАЛ проверки подписи (наш в одной записи, прежнего издателя в другой), в форме стандартного документа, на cluster-ВНУТРЕННЕМ слушателе (:9097, никогда внешнем — ban #6), под односторонней TLS с сертификатом внутреннего удостоверяющего центра.

Требовать сертификат у ПОТРЕБИТЕЛЯ набора нельзя: он обязан оставаться origin-agnostic, и взаимная TLS сломала бы ровно это свойство.

Но соседняя поверхность того же слушателя обоснованием НЕ пользуется

Авторитет отзыва (internal/handler/tokenintrospecthttp) стоит на этом же слушателе и принимает ПРЕДЪЯВЛЕННЫЙ ТОКЕН, а не отдаёт публичный материал. Обоснование выше на него не распространяется, поэтому он требует проверенного клиентского сертификата САМ, а слушатель переводится в режим «сертификат запрашивается и верифицируется, если предъявлен» — набор ключей при этом остаётся доступен без сертификата.

Не добавляйте здесь authN-гейт, не пересмотрев это решение (и TLS-клиента потребителя набора).

Index

Constants

View Source
const WellKnownJWKSPath = "/.well-known/jwks.json"

WellKnownJWKSPath — the standard OIDC JWKS well-known path this proxy serves.

Variables

This section is empty.

Functions

func NewMux

func NewMux(b Binding) (*http.ServeMux, error)

NewMux монтирует КАЖДУЮ запись привязки на свой путь.

Возвращённый mux выставляется вызывающим на cluster-ВНУТРЕННЕМ слушателе — никогда на внешнем, и это относится к каждому пути, а не к первому из них.

Types

type Binding

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

Binding — объявленная привязка «издатель → путь → обработчик».

func NewBinding

func NewBinding(records []Record) (Binding, error)

NewBinding строит привязку, ОТКАЗЫВАЯ в вырожденной.

Это и есть страж старта записи источника: издатель, объявленный принимаемым, но не имеющий записи, — отказ в старте, а не молчаливый перебор записей подряд. Отказ здесь — третий экземпляр класса «пустое значение означает „не сужаем“», который дерево уже закрывает на двух других перечнях.

func (Binding) PathOf

func (b Binding) PathOf(issuer string) (string, bool)

PathOf резолвит объявленного издателя в путь его записи.

Издатель употребляется ТОЛЬКО как ключ поиска в объявленной таблице: не резолвится — отказ. Ни одна часть пути, имени файла, ключа кэша или исходящего адреса из него не строится.

func (Binding) Paths

func (b Binding) Paths() []string

Paths возвращает пути привязки.

Всякий, кому нужен перечень путей публикации — проба замка «только внутри», композиционный корень, страница развёртывания, — ВЫВОДИТ его отсюда. Выписанный перечень разошёлся бы с привязкой молча при появлении третьей записи, и утверждение о единственном маршруте осталось бы зелёным, уедь второй на внешнюю поверхность.

func (Binding) Records

func (b Binding) Records() []Record

Records возвращает записи привязки.

type Config

type Config struct {
	// UpstreamURL — the Hydra PUBLIC JWKS URL to mirror (config.ResolveHydraJWKSURL).
	UpstreamURL string
	// Client — optional injectable http.Client. nil → an internal client WITH a
	// per-call Timeout (never http.DefaultClient).
	Client *http.Client
	// TTL — cache lifetime when the upstream advertises no Cache-Control max-age.
	// <=0 → defaultTTL (5m).
	TTL time.Duration
	// Timeout — per-call upstream-fetch timeout. <=0 → defaultTimeout (5s).
	Timeout time.Duration
	// Clock — injectable time source (tests). nil → time.Now.
	Clock func() time.Time
	// Logger — nil → slog.Default().
	Logger *slog.Logger
}

Config — inputs for the JWKS-proxy handler.

type Handler

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

Handler — the caching reverse-proxy handler.

func NewHandler

func NewHandler(cfg Config) *Handler

NewHandler builds the JWKS-proxy handler. The default http.Client carries a per-call Timeout (never http.DefaultClient).

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Handler) Stats

func (h *Handler) Stats() MirrorStats

Stats returns a snapshot of the counters.

type KeySetConfig

type KeySetConfig struct {
	Source KeySetSource
	Logger *slog.Logger
}

KeySetConfig — настройка обработчика нашей записи.

type KeySetHandler

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

KeySetHandler — НАША запись публикуемого набора: ПРОЕКЦИЯ ключницы.

Почему проекция, а не собственный кэш

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

Собственный кэш вернул бы состояние «строка есть, в ответе нет», и тогда вступление ключа в подпись пришлось бы гейтить на видимость eventually-consistent проекции — запрещённый подтверждающий барьер. С несколькими репликами публикатора «ответ эндпоинта» перестал бы быть одной величиной.

Цена решения названа

Каждый запрос публикации становится чтением своей базы. Величина ограничена сверху числом потребителей, помноженным на частоту их перезапроса, — то есть единицами запросов в минуту, а не функцией от пользовательской нагрузки.

func NewKeySetHandler

func NewKeySetHandler(cfg KeySetConfig) *KeySetHandler

NewKeySetHandler — построитель.

func (*KeySetHandler) ServeHTTP

func (h *KeySetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*KeySetHandler) Stats

func (h *KeySetHandler) Stats() KeySetStats

Stats возвращает величины по каждому исходу.

type KeySetSource

type KeySetSource interface {
	PublishedSet(ctx context.Context) ([]domain.PublishedKey, error)
}

KeySetSource — источник НАШЕЙ записи публикуемого набора.

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

type KeySetStats

type KeySetStats struct {
	Served      uint64
	Unavailable uint64
	Empty       uint64
}

KeySetStats — величины по каждому исходу.

«Ноль отказов за всё время жизни» обязано быть отличимо от «контроль не исполнялся», поэтому величины читаются всегда, включая нулевые.

type MirrorStats

type MirrorStats struct {
	// Served — fetches that produced a keyset (a cache hit serves without a fetch
	// and is not counted here; this counts what came off the hop).
	Served uint64
	// Unavailable — fetches refused because the provider did not answer usefully.
	Unavailable uint64
	// Misconfigured — fetches refused because the address does not serve a JWKS.
	Misconfigured uint64
}

MirrorStats — what this mirror has actually done since the process started.

It exists to answer "has this control ever refused anything, and for which reason?". Without the counts, a mirror that has fail-closed on every fetch since the stand came up is indistinguishable from one that never had to — and the difference is "every docker pull is 401" versus "everything is fine".

WHO READS THIS. The composition root registers a scrape collector over these counters (kaname_jwks_mirror_outcomes_total), so Served is published beside the two refusal reasons and "never refused" stays distinguishable from "never reached". Здесь стояло «процесс сообщает их в журнале жизненного цикла» — такого читателя в дереве не было НИ ОДНОГО, то есть комментарий описывал наблюдаемость, которой не существовало, и именно поэтому её отсутствие никого не смутило. Свойство «читатель есть» держит гейт по дереву TestDeclaredAccumulatorsHaveANonTestReader.

type Record

type Record struct {
	// Issuer — принимаемый издатель. Ключ поиска, и ТОЛЬКО ключ поиска.
	Issuer string
	// Path — путь записи. ОБЪЯВЛЯЕТСЯ здесь, а не выводится из издателя.
	//
	// Производная конструкция «взять базовый адрес и приклеить издателя»
	// короче и запрещена по двум причинам сразу. Первая — про безопасность:
	// издатель приходит от предъявителя, то есть это недоверенный вход, и
	// значению от предъявителя не место в построении пути. Вторая — про
	// проверяемость: производный путь получается У ВСЯКОГО издателя, поэтому
	// состояние «записи источника нет» не наступает никогда, и страж старта
	// становится тождественно истинным — проверка остаётся в тексте, не имея
	// возможности упасть.
	Path string
	// Handler — обработчик записи: проекция ключницы для нашей, зеркало для
	// прежнего издателя.
	Handler http.Handler
}

Record — одна запись привязки «издатель → источник набора».

Записей больше одной, и это следствие решения принимать двух издателей: у КАЖДОГО принимаемого издателя своя запись. Объединение наборов рассмотрено и отвергнуто — оно уничтожает ровно ту защиту, ради которой развязка и заводится: ключ одного издателя проверял бы токен, объявляющий другого.

Jump to

Keyboard shortcuts

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