← Harvester

Security

harvester · workzone

Harvester тянет данные из внешних сетей, и защищать нужно сам канал приёма — два вектора: проверка входящих webhook'ов и сдерживание частоты обращений к чужим API. Узко про канал: шифрование учётных данных держит sources, а перенос прав доступа из источника — acl-identity.

1 Webhook'и Принять только то, что реально от источника.
Ядро применяет — коннектор объявляет
Не зашивать if на каждого провайдера: ядро держит единый механизм проверки, а коннектор приносит свой подключаемый верификатор — он объявляет схему подписи, способ извлечь ключ дедупликации и есть ли в доставке метка времени. Ядро прогоняет три шага в строгом порядке свежесть → подпись → дедуп (дёшево → дорого): сначала отсеять по метке времени и заголовкам, и лишь то, что прошло подпись, пускать в дедуп — иначе неаутентичный поток сам станет DoS на хранилище дедупа. Новый источник = новый верификатор; ядро не трогаем.
Подлинность вызова
Каждый входящий вызов нужно доказать как пришедший от источника — иначе в конвейер попадёт чужое. Схему подписи объявляет коннектор: HMAC над телом (Slack, GitHub), статичный токен в заголовке (GitLab) или JWT Connect-приложения (Atlassian); где источник не подписывает вовсе — откат на секретный неугадываемый endpoint. Подлинность не подтверждена — вызов отклонён.
Свежесть метки
Где провайдер шлёт метку времени (Slack, GitLab), вызов старше окна свежести (единообразно 5 минут) отбрасываем сразу, до подписи — это и первый барьер от переигрывания, и самая дешёвая отбраковка мусора. У источников без метки (GitHub, Atlassian) шаг пропускается, защиту от повтора несёт целиком дедуп.
Один вызов — один раз
Подпись не спасает от повторного проигрывания перехваченного вызова. Ключ дедупликации каждой доставки (его извлекает верификатор коннектора) кладём в Redis атомарным SET key NX EX — первый раз ставит, повтор видит ключ и отбрасывается. TTL зависит от метки: 15 минут там, где есть timestamp (свежесть уже отрезала старое), и 24 часа там, где метки нет — чтобы ловить и ручные redelivery. Сам канал — только по TLS (терминирует reverse-proxy).
Декларации сегодняшних коннекторов
  • Slack — HMAC над v0:ts:body, метка есть (окно 5 мин), дедуп по event_id.
  • GitHubX-Hub-Signature-256, метки нет, дедуп по X-GitHub-Delivery.
  • GitLab — статичный токен (план миграции на HMAC signing token), дедуп по Idempotency-Key / Event-UUID.
  • AtlassianX-Hub-Signature (admin) или JWT (Connect), метки нет, дедуп по X-Atlassian-Webhook-Identifier.
Отбить мусорv2
Зная адрес endpoint'а, его можно завалить мусором — каждый вызов стоит проверки подписи. Поэтому проверки и идут от дешёвых к дорогим (свежесть и заголовки — раньше HMAC, а неаутентичное в дедуп вообще не попадает), на endpoint висит лимит частоты на reverse-proxy, а сам путь неугадываем (секрет в адресе).
Отказ — в лог и алерт
Каждый отклонённый вызов уходит в лог: одиночный — шум, всплеск — сигнал, что секрет разъехался или endpoint щупают. На всплеск поднимается уведомление типа Security — тем же каналом, что и перебор пароля.
Где webhook'и настраиваются
Webhook привязан к конкретному источнику — секрет и адрес заводятся там же, где подключение; какие события слать, выбирается на стороне источника, не у нас. Эту механику держит sources; здесь — только проверка того, что по каналу пришло.
2 Rate limiting per scope → Cache & Workers Не забанили нас — и не положили источник.
Лимит по scope, не «на источник»
Провайдеры лимитируют не «источник» вообще, а аккаунт, токен, воркспейс или сайт — и пороги у всех разные, со своими тарифными (tier) потолками. Поэтому ключ лимитера строится по rate_limit_scope из манифеста коннектора (tenant / account_token / workspace_method / site), а не по абстракции «источник» — общего числа на всех нет. Манифест же объявляет стартовый и максимальный темп: безопасная отправная точка под типовой тариф, реальную же границу рантайм нащупывает по ответам самого источника.
Считаем стоимость, не запросы
«Один запрос» — не единица нагрузки: у Atlassian поиск стоит 51 point, у Slack лимит свой на каждый метод. Поэтому лимитер списывает не запросы, а единицы стоимости (cost per request, по умолчанию 1) — а во сколько единиц обходится конкретный запрос, объявляет коннектор. Так разнородные биллинги провайдеров ложатся на один счётчик без переделки ядра.
Адаптивный темп (AIMD)
Темп подстраивается под источник AIMD-контроллером: при спокойных ответах разгон +10% от нижней границы за окно оценки (≈10 c / 20 запросов), на 429 — резкий откат ×0.5 (additive-increase / multiplicative-decrease). Пол и старт — темп из манифеста, потолок — его же максимум. Текущий темп не сбрасываем между прогонами — это выученная ёмкость API; в Redis он живёт с TTL в часы. Исполнитель — распределённый token-bucket в Redis, списание атомарно (Lua), ключ per-scope, переживает рестарт воркера.
Сигналы провайдера сильнее AIMD
Когда источник говорит прямо — слушаем его, а не свою догадку. Retry-After — жёсткая пауза на весь scope, её уважают все воркеры, она перекрывает AIMD. А если X-RateLimit-Remaining мал — тормозим заранее: safe_rps = Remaining / (Reset − now), берём min с темпом AIMD.
Стык с ретраями
Throttling и повторы — одна линия: отбили по лимиту — повтор уходит уже с выдержкой. Сами ретраи и их backoff держит reliability.