документы: сведены расхождения, найденные сверкой канона
- один факт — один дом: рецепт локального входа, правило чтения X-Forwarded-For, уровень строки журнала и опись опор изъятия сведены к своим домам, копии заменены ссылками - форма [auth.test_headers] выровнена по образцу конфига в семи местах; сценарии intake и archive перестали ссылаться на сессию, которой сервис не выдаёт - поправлены протухшие факты: ключ объекта строит ULID, а не UUID; сверку адреса пира зовут трое, а не двое; обзор capability access знает о задаче 2026-08-23
This commit is contained in:
@@ -278,11 +278,9 @@ Node на машину **не ставится**: шаг сборки прило
|
|||||||
пришедшего по заголовку, который на сервере ставит Caddy, а браузер заголовков
|
пришедшего по заголовку, который на сервере ставит Caddy, а браузер заголовков
|
||||||
не ставит. Заголовок подставляет сам сервис — настройками, а не вторым
|
не ставит. Заголовок подставляет сам сервис — настройками, а не вторым
|
||||||
процессом: рецепт из трёх правок записан связным блоком в
|
процессом: рецепт из трёх правок записан связным блоком в
|
||||||
`config.example.toml`, под перечнем доверенных адресов. Коротко: пара петлевых
|
`config.example.toml`, под перечнем доверенных адресов. Приложение при этом
|
||||||
адресов в перечень, `[server] debug = true`, раскомментированная секция
|
открывают по адресу сервиса, второго порта нет. Заполненная имитация при
|
||||||
`[auth.test_headers]` с ключом `Remote-User`. Приложение при этом открывают по
|
выключенном предохранителе роняет старт с именем ключа. Ключей боевого
|
||||||
адресу сервиса, второго порта нет. Заполненная имитация при выключенном
|
|
||||||
предохранителе роняет старт с именем ключа. Ключей боевого
|
|
||||||
провайдера на машине разработчика не нужно вовсе — их больше нет и в конфиге.
|
провайдера на машине разработчика не нужно вовсе — их больше нет и в конфиге.
|
||||||
- **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage
|
- **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage
|
||||||
оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён —
|
оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён —
|
||||||
|
|||||||
@@ -52,15 +52,10 @@
|
|||||||
сервис по своим настройкам. Второго процесса для этого не нужно: приложение
|
сервис по своим настройкам. Второго процесса для этого не нужно: приложение
|
||||||
открывают по адресу сервиса.
|
открывают по адресу сервиса.
|
||||||
|
|
||||||
Рецепт — три правки `config.toml` сверху вниз:
|
Рецепт целиком — связным блоком в `config.example.toml`, под перечнем доверенных
|
||||||
|
адресов: там названы три правки, принимаемые имена заголовков и цена включения.
|
||||||
|
|
||||||
1. добавить в перечень доверенных адресов пару петлевых — `127.0.0.1` и `::1`;
|
Заполненная секция имитации при выключенном предохранителе роняет
|
||||||
2. поставить в секции `[server]` ключ `debug = true`;
|
|
||||||
3. раскомментировать секцию `[auth.test_headers]` и назвать в ней `Remote-User`.
|
|
||||||
|
|
||||||
Тот же рецепт записан связным блоком в `config.example.toml`, под перечнем
|
|
||||||
доверенных адресов, — там же названы принимаемые имена заголовков и цена
|
|
||||||
включения. Заполненная секция имитации при выключенном предохранителе роняет
|
|
||||||
старт с именем ключа: сервис с включённым предохранителем называет пришедшего
|
старт с именем ключа: сервис с включённым предохранителем называет пришедшего
|
||||||
сам, никого не спросив, и в бою этот ключ стоит `false`.
|
сам, никого не спросив, и в бою этот ключ стоит `false`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Пришедшего называет заголовок доверенного прокси, а не собственный вход OIDC
|
# Пришедшего называет заголовок доверенного прокси, а не собственный вход OIDC
|
||||||
|
|
||||||
- **Дата:** 2026-08-22
|
- **Дата:** 2026-08-22
|
||||||
- **Источник:** openspec/changes/archive/trusted-header-login/design.md
|
- **Источник:** [../../openspec/changes/archive/2026-08-22-trusted-header-login/design.md](../../openspec/changes/archive/2026-08-22-trusted-header-login/design.md), разделы Р1 и Р3
|
||||||
|
|
||||||
## Решение
|
## Решение
|
||||||
|
|
||||||
|
|||||||
+17
-6
@@ -59,7 +59,13 @@
|
|||||||
`trusted-header-login` — вместе с куками, сессией и её сроком. Здесь же разграничение записей по владельцу: принятая запись
|
`trusted-header-login` — вместе с куками, сессией и её сроком. Здесь же разграничение записей по владельцу: принятая запись
|
||||||
принадлежит тому, кто её принёс, чужая неотличима от несуществующей, а ничьей
|
принадлежит тому, кто её принёс, чужая неотличима от несуществующей, а ничьей
|
||||||
записи не бывает вовсе — колонка владельца пустого значения не принимает.
|
записи не бывает вовсе — колонка владельца пустого значения не принимает.
|
||||||
Задачи `record-ownership` и `remove-telegram-intake` 2026-08-14.
|
Задачи `record-ownership` и `remove-telegram-intake` 2026-08-14. Здесь же
|
||||||
|
изъятие отладочного запуска: при включённом предохранителе `[server] debug`
|
||||||
|
заголовки входа подставляет сам сервис значениями из `[auth.test_headers]` — с
|
||||||
|
отказами старта, строкой журнала и закрытым перечнем следствий ключа. Задача
|
||||||
|
`config-test-headers-login` 2026-08-23; решения —
|
||||||
|
[ADR-2026-08-23-test-headers-substituted-by-service](adr/ADR-2026-08-23-test-headers-substituted-by-service.md)
|
||||||
|
и [ADR-2026-08-23-no-address-guard-for-debug-login](adr/ADR-2026-08-23-no-address-guard-for-debug-login.md).
|
||||||
|
|
||||||
Поведение узла, которого нет в перечне выше, по-прежнему живёт только в коде.
|
Поведение узла, которого нет в перечне выше, по-прежнему живёт только в коде.
|
||||||
Задача, которая его трогает, дописывает спеку своей capability.
|
Задача, которая его трогает, дописывает спеку своей capability.
|
||||||
@@ -84,7 +90,7 @@
|
|||||||
- **Подставной собеседник в боевом бинарнике объявлен своим ключом.** Дом ему —
|
- **Подставной собеседник в боевом бинарнике объявлен своим ключом.** Дом ему —
|
||||||
код или оснастка; в боевом бинарнике он появляется только отдельным решением
|
код или оснастка; в боевом бинарнике он появляется только отдельным решением
|
||||||
владельца и только под ключом, названным своим предметом: имитацию заголовков
|
владельца и только под ключом, названным своим предметом: имитацию заголовков
|
||||||
входа объявляет секция `[auth] test_headers`, подмену распознавания — правка
|
входа объявляет секция `[auth.test_headers]`, подмену распознавания — правка
|
||||||
кода (`internal/adapter/recognizer/memory.go`). Ключ, названный общим словом,
|
кода (`internal/adapter/recognizer/memory.go`). Ключ, названный общим словом,
|
||||||
обрастает следствиями молча, и выключить его перестаёт означать «сервис ведёт
|
обрастает следствиями молча, и выключить его перестаёт означать «сервис ведёт
|
||||||
себя как в бою». Предохранитель `[server] debug` вторым именем собеседнику при
|
себя как в бою». Предохранитель `[server] debug` вторым именем собеседнику при
|
||||||
@@ -161,7 +167,7 @@
|
|||||||
|
|
||||||
| Компонент | Где | Что делает |
|
| Компонент | Где | Что делает |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| HTTP API | `internal/controller/http` | Адреса приложения под корнем `/app/` на `net/http`: приём записи, страница своих записей, карточка, текст названного вида, файл записи, пределы сервера и «кто вошёл». Слои — свои: журнал, восстановление после паники, ограничитель частоты, подстановка заголовков входа отладочного запуска, узнавание, требование учётной записи. Подстановка — звено необязательное: при выключенном предохранителе `[server] debug` и при пустой имитации она в цепочку не встаёт вовсе |
|
| HTTP API | `internal/controller/http` | Адреса приложения под корнем `/app/` на `net/http`: приём записи, страница своих записей, карточка, текст названного вида, файл записи, пределы сервера и «кто вошёл». Слои — свои: журнал, восстановление после паники, ограничитель частоты, подстановка заголовков входа отладочного запуска, узнавание, требование учётной записи. Условия, при которых звено подстановки встаёт в цепочку, нормирует [access](../openspec/specs/access/spec.md), «Отладочный запуск называет пришедшего настройками» |
|
||||||
| Воркеры | `internal/controller/worker` | Пул одинаковых потоков: каждый берёт любую пригодную запись и опрашивает базу. Число — настройкой, ноль законен |
|
| Воркеры | `internal/controller/worker` | Пул одинаковых потоков: каждый берёт любую пригодную запись и опрашивает базу. Число — настройкой, ноль законен |
|
||||||
| Сервис расшифровки | `internal/service` | Конвейер: приём, приведение, отправка, опрос, завершение. Шаг выбирается по рубежу записи |
|
| Сервис расшифровки | `internal/service` | Конвейер: приём, приведение, отправка, опрос, завершение. Шаг выбирается по рубежу записи |
|
||||||
| Конвертер и метаданные | `internal/adapter/{converter,metaviewer}/ffmpeg` | `ffmpeg` в ogg/vorbis, `ffprobe` для длительности |
|
| Конвертер и метаданные | `internal/adapter/{converter,metaviewer}/ffmpeg` | `ffmpeg` в ogg/vorbis, `ffprobe` для длительности |
|
||||||
@@ -184,7 +190,8 @@
|
|||||||
## Внешние границы и форматы
|
## Внешние границы и форматы
|
||||||
|
|
||||||
- **Yandex Object Storage.** S3-совместимый, клиент `aws-sdk-go-v2` с
|
- **Yandex Object Storage.** S3-совместимый, клиент `aws-sdk-go-v2` с
|
||||||
`UsePathStyle`. Ключ объекта — имя файла, то есть UUID с расширением.
|
`UsePathStyle`. Ключ объекта — имя файла записи, то есть её идентификатор с
|
||||||
|
расширением; идентификаторы строит `internal/ident` и они ULID, а не UUID.
|
||||||
- **Yandex SpeechKit v3.** gRPC, `stt.api.cloud.yandex.net:443`, модель
|
- **Yandex SpeechKit v3.** gRPC, `stt.api.cloud.yandex.net:443`, модель
|
||||||
`deferred-general`, авторизация заголовком `Api-Key`. Распознавание
|
`deferred-general`, авторизация заголовком `Api-Key`. Распознавание
|
||||||
асинхронное: запрос возвращает идентификатор операции, готовность опрашивается
|
асинхронное: запрос возвращает идентификатор операции, готовность опрашивается
|
||||||
@@ -192,6 +199,10 @@
|
|||||||
- **ffmpeg и ffprobe.** Внешние процессы, ищутся в `PATH`.
|
- **ffmpeg и ffprobe.** Внешние процессы, ищутся в `PATH`.
|
||||||
- **SQLite через `modernc.org/sqlite`.** Драйвер на чистом Go: CGO сборке не
|
- **SQLite через `modernc.org/sqlite`.** Драйвер на чистом Go: CGO сборке не
|
||||||
нужен. База и файлы записей лежат под одним каталогом данных.
|
нужен. База и файлы записей лежат под одним каталогом данных.
|
||||||
|
- **`github.com/BurntSushi/toml`.** Формат единственного источника настроек.
|
||||||
|
Негодный TOML останавливает старт; незнакомый ключ разбор не судит и молча
|
||||||
|
отбрасывает — наблюдение и чем оно проверено, в
|
||||||
|
[research/toml-unknown-keys.md](research/toml-unknown-keys.md).
|
||||||
- **`github.com/pressly/goose/v3`.** Шаги схемы — библиотекой, а не командной
|
- **`github.com/pressly/goose/v3`.** Шаги схемы — библиотекой, а не командной
|
||||||
строкой: перечень шагов приходит провайдеру доводом, накат идёт при старте.
|
строкой: перечень шагов приходит провайдеру доводом, накат идёт при старте.
|
||||||
Исключающей блокировки под SQLite библиотека не даёт, и замок каталога данных
|
Исключающей блокировки под SQLite библиотека не даёт, и замок каталога данных
|
||||||
@@ -284,8 +295,8 @@
|
|||||||
| Адресное пространство сервиса | `internal/controller/http.ServiceMounts` — перечень корней и адресов наблюдения. Он **порождает** регистрацию наших маршрутов, а не описывает её, и из него же выводятся правило неизвестного пути, уровень журнала и область действия узнавания |
|
| Адресное пространство сервиса | `internal/controller/http.ServiceMounts` — перечень корней и адресов наблюдения. Он **порождает** регистрацию наших маршрутов, а не описывает её, и из него же выводятся правило неизвестного пути, уровень журнала и область действия узнавания |
|
||||||
| Узнавание предъявителя | `sqlite.UserRepository.EnsureUser` — поиск учётной записи по логину у провайдера и заведение при первом обращении. Дом правила один и лежит в хранилище, а не в транспорте: второй способ представиться (личные токены) возьмёт этот же метод, а уложенное куском в слой оно разошлось бы двумя копиями. Транспорт читает заголовок, судит адрес пира и зовёт метод интерфейсом `contract.UserRepository` — `internal/controller/http.TrustedHeaderIdentity` |
|
| Узнавание предъявителя | `sqlite.UserRepository.EnsureUser` — поиск учётной записи по логину у провайдера и заведение при первом обращении. Дом правила один и лежит в хранилище, а не в транспорте: второй способ представиться (личные токены) возьмёт этот же метод, а уложенное куском в слой оно разошлось бы двумя копиями. Транспорт читает заголовок, судит адрес пира и зовёт метод интерфейсом `contract.UserRepository` — `internal/controller/http.TrustedHeaderIdentity` |
|
||||||
| Приём значения заголовка | `internal/entity.AcceptProviderLogin`, `AcceptDisplayName`, `AcceptEmail` — правило одно на все способы представиться |
|
| Приём значения заголовка | `internal/entity.AcceptProviderLogin`, `AcceptDisplayName`, `AcceptEmail` — правило одно на все способы представиться |
|
||||||
| Имена заголовков входа | `internal/controller/http.IdentityHeaderNames` вместе с константами рядом — тройка `Remote-*` перечисляется отсюда, а не по месту. она же порождает набор имён, принимаемых секцией `[auth] test_headers`: ключ, не совпавший ни с одним, роняет старт |
|
| Имена заголовков входа | `internal/controller/http.IdentityHeaderNames` вместе с константами рядом — тройка `Remote-*` перечисляется отсюда, а не по месту. она же порождает набор имён, принимаемых секцией `[auth.test_headers]`; что делает старт с ключом вне набора, нормирует [access](../openspec/specs/access/spec.md), «Настройка, открывающая вход всем, роняет старт» |
|
||||||
| Сверка адреса пира с перечнем доверенных | `internal/controller/http`, `identity.go` — `peerAddress` и `isTrusted`. Зовут их двое: узнавание и подстановка заголовков отладочного запуска. Второй сверщик разошёлся бы с первым молча — разбор разворачивает IPv4 в оболочке IPv6, и разница пришлась бы ровно на те адреса, ради которых он заводится |
|
| Сверка адреса пира с перечнем доверенных | `internal/controller/http`, `identity.go` — `peerAddress` и `isTrusted`. Зовут их узнавание, подстановка заголовков отладочного запуска и ограничитель частоты. Свой сверщик разошёлся бы с общим молча — разбор разворачивает IPv4 в оболочке IPv6, и разница пришлась бы ровно на те адреса, ради которых он заводится |
|
||||||
| Ограничитель частоты | `internal/controller/http.RateLimit` — бюджет по адресу спрашивающего под корнем приложения; из его чисел выводится объявляемая частота опроса |
|
| Ограничитель частоты | `internal/controller/http.RateLimit` — бюджет по адресу спрашивающего под корнем приложения; из его чисел выводится объявляемая частота опроса |
|
||||||
|
|
||||||
Единых точек, которых **нет** и которые ожидались бы, сегодня не осталось.
|
Единых точек, которых **нет** и которые ожидались бы, сегодня не осталось.
|
||||||
|
|||||||
@@ -63,12 +63,10 @@ force_shutdown_timeout = <N> # ждать остановки ворке
|
|||||||
Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным
|
Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным
|
||||||
входом.
|
входом.
|
||||||
|
|
||||||
Там же, комментарием под секцией, стоит **рецепт локального входа одним связным
|
Там же, комментарием под секцией, стоит рецепт локального входа **одним связным
|
||||||
блоком**: три правки сверху вниз — пара петлевых адресов в перечень доверенных,
|
блоком**, а не тремя комментариями по месту: правки связаны между собой, и
|
||||||
`[server] debug = true`, раскомментированная секция `[auth.test_headers]` с
|
применённая порознь любая из них роняет старт либо оставляет сервис никого не
|
||||||
ключом `Remote-User`. Блок один, а не три комментария по месту: правки связаны
|
узнающим. Рабочей строкой в образце стоит боевое значение —
|
||||||
между собой, и применённая порознь любая из них роняет старт либо оставляет
|
|
||||||
сервис никого не узнающим. Рабочей строкой в образце стоит боевое значение —
|
|
||||||
перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована
|
перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована
|
||||||
целиком: образец описывает боевую выкладку, а локальный вход — способ до неё
|
целиком: образец описывает боевую выкладку, а локальный вход — способ до неё
|
||||||
дойти, и два рабочих значения в одном файле читались бы как выбор без указания,
|
дойти, и два рабочих значения в одном файле читались бы как выбор без указания,
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ stdlib-логом в поток ошибок. Это выбор, а не дол
|
|||||||
*Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем.
|
*Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем.
|
||||||
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
|
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
|
||||||
|
|
||||||
*Изъятие:* строка о подставленных заголовках входа
|
*Изъятие:* строка о подставленных заголовках входа адресована разработчику, а
|
||||||
(`internal/controller/http/substitute.go`) адресована разработчику, а идёт на
|
идёт на `INFO` — уровень и его довод нормирует спека
|
||||||
`INFO`: `DEBUG` включить нечем, а в бою она не пишется вовсе — подстановку
|
[access](../../openspec/specs/access/spec.md), «Отладочный запуск виден в
|
||||||
держит выключенный умолчанием предохранитель `[server] debug`.
|
журнале».
|
||||||
|
|
||||||
## Время
|
## Время
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -424,9 +424,8 @@ Crockford. Выдаёт их приложение единой точкой `int
|
|||||||
и тот же — обратный прокси; бюджет тогда становится общим на весь сервис, и
|
и тот же — обратный прокси; бюджет тогда становится общим на весь сервис, и
|
||||||
восемь одновременно открытых карточек выбирают его целиком. Обратная ошибка —
|
восемь одновременно открытых карточек выбирают его целиком. Обратная ошибка —
|
||||||
верить заголовку без сверки пира — отдаёт обход ограничителя ровно тому, кого он
|
верить заголовку без сверки пира — отдаёт обход ограничителя ровно тому, кого он
|
||||||
ограничивает. Сама цепочка читается справа налево с отбрасыванием доверенных
|
ограничивает. Как читается цепочка — спека
|
||||||
адресов, поэтому дописывающий прокси правилом покрыт; почему так —
|
[archive](../openspec/specs/archive/spec.md); здесь только числа бюджета.
|
||||||
[security.md](security.md), «Периметр».
|
|
||||||
|
|
||||||
Числа, ушедшие отсюда со встроенным хранилищем: потолок сохранённого ответа
|
Числа, ушедшие отсюда со встроенным хранилищем: потолок сохранённого ответа
|
||||||
провайдера и потолок структуры реплик — их держало поле коллекции, а теперь ответ
|
провайдера и потолок структуры реплик — их держало поле коллекции, а теперь ответ
|
||||||
|
|||||||
+1
-1
@@ -345,7 +345,7 @@ API и имя не откатываются обратной правкой по
|
|||||||
|
|
||||||
**Вход живой прогон теперь проверяет целиком, и это сдвиг 2026-08-22.** Прежде
|
**Вход живой прогон теперь проверяет целиком, и это сдвиг 2026-08-22.** Прежде
|
||||||
сессию в прогоне выдать было нечем; теперь заголовок ставит сам сервис по
|
сессию в прогоне выдать было нечем; теперь заголовок ставит сам сервис по
|
||||||
секции `[auth] test_headers` под предохранителем `[server] debug` — прежде
|
секции `[auth.test_headers]` под предохранителем `[server] debug` — прежде
|
||||||
`cmd/devtools proxy`, убранный 2026-08-23, — и живьём проверяются узнавание,
|
`cmd/devtools proxy`, убранный 2026-08-23, — и живьём проверяются узнавание,
|
||||||
заведение учётной записи первым обращением, отказ с недоверенного адреса и
|
заведение учётной записи первым обращением, отказ с недоверенного адреса и
|
||||||
отказ старта на пустом перечне.
|
отказ старта на пустом перечне.
|
||||||
|
|||||||
+12
-13
@@ -85,10 +85,12 @@ Telegram — связи чата с учётной записью сервис
|
|||||||
**Изъятие из барьера одно — отладочный запуск, и заведено оно 2026-08-23**
|
**Изъятие из барьера одно — отладочный запуск, и заведено оно 2026-08-23**
|
||||||
задачей `config-test-headers-login`. При включённом предохранителе
|
задачей `config-test-headers-login`. При включённом предохранителе
|
||||||
`[server] debug` заголовки входа ставит не прокси, а сам сервис значениями из
|
`[server] debug` заголовки входа ставит не прокси, а сам сервис значениями из
|
||||||
секции `[auth] test_headers`: на машине разработчика прокси нет, а браузер
|
секции `[auth.test_headers]`: на машине разработчика прокси нет, а браузер
|
||||||
заголовков не ставит. Узнавание при этом остаётся тем же и подставленного
|
заголовков не ставит. Узнавание при этом остаётся тем же и подставленного
|
||||||
заголовка от пришедшего не отличает — отлаживается боевая ветка. Нормирует
|
заголовка от пришедшего не отличает — отлаживается боевая ветка. Нормирует
|
||||||
изъятие спека [access](../openspec/specs/access/spec.md).
|
изъятие спека [access](../openspec/specs/access/spec.md), решение о подстановке
|
||||||
|
самим сервисом —
|
||||||
|
[ADR-2026-08-23-test-headers-substituted-by-service](adr/ADR-2026-08-23-test-headers-substituted-by-service.md).
|
||||||
|
|
||||||
Держится оно тремя вещами, и других нет: умолчание предохранителя —
|
Держится оно тремя вещами, и других нет: умолчание предохранителя —
|
||||||
«выключено»; заполненная имитация при выключенном предохранителе роняет старт с
|
«выключено»; заполненная имитация при выключенном предохранителе роняет старт с
|
||||||
@@ -103,19 +105,16 @@ Telegram — связи чата с учётной записью сервис
|
|||||||
перечне доверенных адресов и назовёт своим именем всякого, чей запрос пришёл
|
перечне доверенных адресов и назовёт своим именем всякого, чей запрос пришёл
|
||||||
через обратный прокси, — то есть всякого, кто пришёл обычным путём. Адресного
|
через обратный прокси, — то есть всякого, кто пришёл обычным путём. Адресного
|
||||||
предохранителя у изъятия нет: требование петлевого перечня рассматривалось и
|
предохранителя у изъятия нет: требование петлевого перечня рассматривалось и
|
||||||
снято решением владельца на чекпоинте задачи.
|
снято — [ADR-2026-08-23-no-address-guard-for-debug-login](adr/ADR-2026-08-23-no-address-guard-for-debug-login.md).
|
||||||
|
|
||||||
**`X-Forwarded-For` сервис читает сам, и правило чтения закрывает дописывание.**
|
**`X-Forwarded-For` сервис читает сам, и правило чтения закрывает дописывание.**
|
||||||
С 2026-08-22 адрес спрашивающего ограничитель частоты берёт из этого заголовка:
|
Как именно читается цепочка, нормирует спека
|
||||||
иначе счётчик ведётся по адресу пира, а пир теперь всегда один — прокси, — и
|
[archive](../openspec/specs/archive/spec.md), «Адреса приложения живут своим
|
||||||
бюджет становится общим на весь сервис. Цепочка читается **справа налево**,
|
пространством». Отсюда периметровое следствие: прокси, дописывающий
|
||||||
доверенные адреса отбрасываются, и ключом становится первый недоверенный: левым
|
`X-Forwarded-For` к присланному, этим правилом покрыт, и требования
|
||||||
значением распоряжается сам спрашивающий, а правое приписал ближайший к нам
|
«перезаписывать, а не дописывать» у сервиса к нему нет — в отличие от `Remote-*`.
|
||||||
прокси. Заголовок читается всеми строками, а не одной: цепочка законно приходит
|
Барьером узнавания заголовок при этом не служит: кто пришёл, решает адрес самого
|
||||||
несколькими. Прокси, дописывающий `X-Forwarded-For` к присланному, этим правилом
|
соединения.
|
||||||
покрыт, и требования «перезаписывать, а не дописывать» у сервиса к нему нет — в
|
|
||||||
отличие от `Remote-*`. Барьером узнавания заголовок при этом не служит: кто
|
|
||||||
пришёл, решает адрес самого соединения.
|
|
||||||
|
|
||||||
**Ширина перечня доверенных адресов — тоже цена, и она принимается сознательно.**
|
**Ширина перечня доверенных адресов — тоже цена, и она принимается сознательно.**
|
||||||
Перечень задаёт, чьему `Remote-User` верить, и всякий, кто дотянулся до сервиса
|
Перечень задаёт, чьему `Remote-User` верить, и всякий, кто дотянулся до сервиса
|
||||||
|
|||||||
@@ -81,12 +81,10 @@
|
|||||||
«Предохранитель отладки включает только подстановку заголовков»; снимать их
|
«Предохранитель отладки включает только подстановку заголовков»; снимать их
|
||||||
поодиночке нельзя: барьер держится всеми разом.
|
поодиночке нельзя: барьер держится всеми разом.
|
||||||
|
|
||||||
Держится изъятие **умолчанием, а не машиной**: предохранитель по умолчанию
|
Держится изъятие умолчанием предохранителя «выключено» и отказом старта при
|
||||||
выключен, а заполненная имитация без него роняет старт. Боевой перечень
|
заполненной имитации без него. Что остаётся между боевой выкладкой и открытым
|
||||||
доверенных адресов включению предохранителя не мешает — сервис, поднятый в бою с
|
входом целиком, включая опору вне репозитория, перечисляет модель угроз —
|
||||||
включённым предохранителем и заполненной имитацией, назовёт своим именем всякого,
|
`docs/security.md`, «Периметр».
|
||||||
чей запрос пришёл через обратный прокси, то есть всякого, кто пришёл обычным
|
|
||||||
путём.
|
|
||||||
|
|
||||||
#### Scenario: Названный провайдером получает доступ
|
#### Scenario: Названный провайдером получает доступ
|
||||||
|
|
||||||
@@ -275,14 +273,15 @@ MUST отвечать отказом `401`, когда пришедший не
|
|||||||
|
|
||||||
**Изъятие одно — отладочный запуск.** При включённом предохранителе
|
**Изъятие одно — отладочный запуск.** При включённом предохранителе
|
||||||
`[server] debug` заголовок входа ставит не прокси, а сам сервис значением из
|
`[server] debug` заголовок входа ставит не прокси, а сам сервис значением из
|
||||||
секции `[auth] test_headers`; узнавание при этом остаётся тем же и подставленного
|
секции `[auth.test_headers]`; узнавание при этом остаётся тем же и подставленного
|
||||||
заголовка от пришедшего не отличает. Условия, при которых источник этот законен,
|
заголовка от пришедшего не отличает. Условия, при которых источник этот законен,
|
||||||
и проверки старта, которыми он держится, стоят требованиями «Отладочный запуск
|
и проверки старта, которыми он держится, перечислены требованием «Кого пускать,
|
||||||
называет пришедшего настройками», «Настройка, открывающая вход всем, роняет
|
решает провайдер».
|
||||||
старт», «Отладочный запуск виден в журнале» и «Предохранитель отладки включает
|
|
||||||
только подстановку заголовков». Держится изъятие умолчанием предохранителя
|
Держится изъятие умолчанием предохранителя «выключено» и отказом старта при
|
||||||
«выключено» и отказом старта при заполненной имитации без него; боевой перечень
|
заполненной имитации без него. Что остаётся между боевой выкладкой и открытым
|
||||||
доверенных адресов включению предохранителя не мешает.
|
входом целиком, включая опору вне репозитория, перечисляет модель угроз —
|
||||||
|
`docs/security.md`, «Периметр».
|
||||||
|
|
||||||
Заголовку сервис MUST верить только тогда, когда запрос пришёл с адреса из
|
Заголовку сервис MUST верить только тогда, когда запрос пришёл с адреса из
|
||||||
объявленного перечня доверенных, и адрес этот MUST браться у самого соединения,
|
объявленного перечня доверенных, и адрес этот MUST браться у самого соединения,
|
||||||
@@ -591,7 +590,7 @@ MUST не выдавать вовсе — ни куки, ни токена се
|
|||||||
### Requirement: Отладочный запуск называет пришедшего настройками
|
### Requirement: Отладочный запуск называет пришедшего настройками
|
||||||
|
|
||||||
Сервис SHALL подставлять запросу заголовки входа значениями из секции
|
Сервис SHALL подставлять запросу заголовки входа значениями из секции
|
||||||
`[auth] test_headers`, когда включён предохранитель `[server] debug`, и MUST
|
`[auth.test_headers]`, когда включён предохранитель `[server] debug`, и MUST
|
||||||
делать это так, чтобы узнавание не отличало подставленный заголовок от
|
делать это так, чтобы узнавание не отличало подставленный заголовок от
|
||||||
поставленного обратным прокси. Ветка кода, которой узнаётся пришедший, обязана
|
поставленного обратным прокси. Ветка кода, которой узнаётся пришедший, обязана
|
||||||
быть той же, что работает в бою: отладке подлежит боевой путь, а не его
|
быть той же, что работает в бою: отладке подлежит боевой путь, а не его
|
||||||
@@ -685,7 +684,7 @@ MUST не выдавать вовсе — ни куки, ни токена се
|
|||||||
|
|
||||||
Умолчания названы нормой, а не образцом конфига. Отсутствие ключа `[server]
|
Умолчания названы нормой, а не образцом конфига. Отсутствие ключа `[server]
|
||||||
debug` MUST читаться как выключенный предохранитель, а отсутствие секции
|
debug` MUST читаться как выключенный предохранитель, а отсутствие секции
|
||||||
`[auth] test_headers` — как пустая секция: конфиг сегодняшнего дня, не тронутый
|
`[auth.test_headers]` — как пустая секция: конфиг сегодняшнего дня, не тронутый
|
||||||
ни на байт, обязан вести себя ровно как вёл. Ошибка разбора значения MUST
|
ни на байт, обязан вести себя ровно как вёл. Ошибка разбора значения MUST
|
||||||
кончаться отказом старта, а не прочтением «включено».
|
кончаться отказом старта, а не прочтением «включено».
|
||||||
|
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ MUST называть в нём потолок размера одной зап
|
|||||||
|
|
||||||
#### Scenario: Потолок размера равен тому, которым сервис отвергает
|
#### Scenario: Потолок размера равен тому, которым сервис отвергает
|
||||||
|
|
||||||
- **GIVEN** человек вошёл и предъявил сессию
|
- **GIVEN** человек узнан
|
||||||
- **WHEN** он спрашивает пределы сервиса
|
- **WHEN** он спрашивает пределы сервиса
|
||||||
- **THEN** потолок размера в ответе равен потолку, которым сервис ограничивает
|
- **THEN** потолок размера в ответе равен потолку, которым сервис ограничивает
|
||||||
тело запроса приёма
|
тело запроса приёма
|
||||||
|
|||||||
@@ -302,19 +302,19 @@ MUST ограничивать его длину и MUST убирать из не
|
|||||||
|
|
||||||
#### Scenario: Имя доходит до записи
|
#### Scenario: Имя доходит до записи
|
||||||
|
|
||||||
- **GIVEN** отправитель предъявил сессию
|
- **GIVEN** отправитель узнан
|
||||||
- **WHEN** он шлёт запись с именем `разговор.mp3`
|
- **WHEN** он шлёт запись с именем `разговор.mp3`
|
||||||
- **THEN** колонка имени файла у заведённой записи несёт `разговор.mp3`
|
- **THEN** колонка имени файла у заведённой записи несёт `разговор.mp3`
|
||||||
|
|
||||||
#### Scenario: Заголовок принятой записи пуст
|
#### Scenario: Заголовок принятой записи пуст
|
||||||
|
|
||||||
- **GIVEN** отправитель предъявил сессию
|
- **GIVEN** отправитель узнан
|
||||||
- **WHEN** он шлёт запись с именем `разговор.mp3`
|
- **WHEN** он шлёт запись с именем `разговор.mp3`
|
||||||
- **THEN** колонка заголовка у заведённой записи пуста
|
- **THEN** колонка заголовка у заведённой записи пуста
|
||||||
|
|
||||||
#### Scenario: Длинное и грязное имя приходит обрезанным и очищенным
|
#### Scenario: Длинное и грязное имя приходит обрезанным и очищенным
|
||||||
|
|
||||||
- **GIVEN** отправитель предъявил сессию
|
- **GIVEN** отправитель узнан
|
||||||
- **WHEN** он шлёт запись, чьё имя длиннее предела и несёт управляющие знаки
|
- **WHEN** он шлёт запись, чьё имя длиннее предела и несёт управляющие знаки
|
||||||
- **THEN** колонка имени файла несёт имя не длиннее предела
|
- **THEN** колонка имени файла несёт имя не длиннее предела
|
||||||
- **AND** управляющих знаков в нём нет
|
- **AND** управляющих знаков в нём нет
|
||||||
|
|||||||
Reference in New Issue
Block a user