Цепочка лицензий и делегирование
Rediacc использует защищённую от подделки хеш-цепочку для выдачи лицензий и модель сертификата делегирования для on-premise развёртываний. На этой странице объясняется, как система защищает от подделки, атак воспроизведения и совместного использования лицензий.
Зачем нужна цепочка?
Каждая лицензия, выданная сервером аккаунтов, записывается в реестр с дозаписью. Каждая запись связана с предыдущей через хеш SHA-256, образуя цепочку. Цепочка обладает тремя свойствами, делающими подделку обнаруживаемой:
- Порядковые номера глобальны и монотонно возрастают для каждой подписки. Пропуск или изменение порядка записей разрывает цепочку.
- Хеши цепочки привязывают каждую запись ко всем предыдущим. Изменение любой прошлой записи делает недействительными все последующие.
- Renet хранит максимальный виденный порядковый номер для каждой подписки. Сервер, откатывающий свой порядковый номер, обнаруживается немедленно.
Как выдаётся лицензия
Когда CLI запрашивает активацию машины или лицензию репозитория, сервер аккаунтов:
- Считывает текущую вершину цепочки (последний порядковый номер + хеш) для подписки.
- Формирует полезную нагрузку лицензии со следующим порядковым номером и предыдущим хешем цепочки.
- Подписывает полезную нагрузку с помощью Ed25519.
- Вычисляет
chainHash = SHA256(prevChainHash + ":" + signedPayload). - Атомарно добавляет запись в реестр выдачи. Если два одновременных запроса конкурируют за один порядковый номер, проигравший повторно получает следующий и переподписывает.
- Возвращает подписанный блоб с хешем цепочки в CLI.
sequence и prevChainHash находятся внутри подписанной полезной нагрузки (поэтому их нельзя изменить без аннулирования подписи). chainHash находится на конверте (вычисляется после подписания во избежание циклической зависимости).
Как Renet выполняет проверку
Каждая машина, работающая с Renet, хранит последнее известное состояние цепочки в {licenseDir}/chain-state.json. При каждой проверке лицензии Renet выполняет:
| Проверка | Неудача означает |
|---|---|
| Подпись Ed25519 действительна | Лицензия подделана или изменена |
sequence > lastKnownSequence | Сервер откатил цепочку (атака воспроизведения) |
chainHash == SHA256(prevChainHash + ":" + payload) | Запись цепочки изменена |
issuedAt >= lastKnownIssuedAt | Манипуляция с часами (часы сервера переведены назад) |
При неудаче любой проверки лицензия отклоняется, и причина сообщается.
Сертификаты делегирования (on-premise)
Для изолированных или самостоятельно размещённых развёртываний вышестоящий сервер аккаунтов выдаёт сертификат делегирования, разрешающий on-premise серверу подписывать лицензии собственным ключом Ed25519. Сертификат ограничивает возможности on-premise сервера.
Структура сертификата
Сертификат делегирования содержит:
subscriptionId- к какой подписке относится этот сертификатplanCode,maxMachines,maxRepositorySizeGb,maxRepoLicenseIssuancesPerMonth- лимиты плана, зафиксированные в сертификатеmaxTotalIssuances- верхняя граница порядкового номера цепочкиdelegatedPublicKey- открытый ключ Ed25519 on-premise сервера (SPKI base64)genesisHash- начальная точка цепочки (продолжение от предыдущего сертификата или “genesis”)genesisSequence- порядковый номер цепочки на момент выдачи. Используется/onprem/cert-uploadдля проверки связи нового сертификата с известной записью в локальном реестре при продвижении цепочки во время транзита. Необязателен для обратной совместимости (трактуется как 0 при отсутствии).validFrom,validUntil- окно валидности (определяется политикой валидности, описанной ниже)- Подписан вышестоящим мастер-ключом Ed25519
Как работает делегирование
- Admin Enterprise генерирует пару ключей Ed25519 на on-premise сервере.
- Admin запрашивает сертификат делегирования у вышестоящего сервера:
POST /admin/delegation-certs { subscriptionId, validDays: 90, delegatedPublicKey: "MCowBQYDK2VwAyEA..." } - Вышестоящий сервер подписывает сертификат своим мастер-ключом и возвращает его.
- On-premise сервер сохраняет сертификат и свой закрытый ключ, готовый к подписанию лицензий.
- Когда CLI запрашивает лицензию у on-premise сервера, тот подписывает её делегированным ключом и включает ссылку на сертификат.
- Renet выполняет двухуровневую проверку:
- Проверяет подпись сертификата по встроенному мастер-ключу вышестоящего сервера.
- Проверяет подпись блоба по делегированному ключу из сертификата.
- Проверяет
blob.sequence <= cert.maxTotalIssuances. - Применяет все стандартные проверки цепочки.
On-premise сервер не может:
- Подделать лицензию за пределами лимитов плана сертификата делегирования (renet отклонит её).
- Выдать более
maxTotalIssuancesопераций суммарно (renet отклонит переполнение порядкового номера). - Изменить сертификат (подпись вышестоящего сервера будет нарушена).
Политика валидности
Окно валидности сертификата делегирования вычисляется вспомогательной функцией общей политики (computeDelegationCertValidity()), которая выполняется как на бэкенде вышестоящего сервера, так и на фронтенде клиентского портала. Одинаковые входные данные всегда дают одинаковый validUntil, поэтому клиенты могут просмотреть эффективный срок действия в модальном окне создания до отправки.
Значения по умолчанию и потолки для каждого плана
| План | Срок по умолчанию | Потолок плана |
|---|---|---|
| COMMUNITY | 15 дней | 30 дней |
| PROFESSIONAL | 60 дней | 120 дней |
| BUSINESS | 90 дней | 180 дней |
| ENTERPRISE | 120 дней | 365 дней |
Значение по умолчанию - то, что выбирает эндпоинт создания, когда вызывающий не указывает validDays. Потолок - верхняя граница, которую может запросить вызывающий.
Переопределение для конкретной подписки
Администраторы могут установить пользовательское значение delegationCertDefaultDays для конкретной подписки на странице Admin Subscription Detail. Переопределение заменяет как значение по умолчанию, так и потолок для этой подписки - это аварийный клапан для особых клиентов (например, корпоративный контракт, которому нужен 200-дневный сертификат на плане COMMUNITY). Схема Zod по-прежнему обеспечивает абсолютный диапазон 1..365.
Жёсткий потолок: дата окончания подписки + 3 дня льготного периода
Независимо от потолка плана и переопределения, каждый сертификат жёстко ограничен значением subscription.expiresAt + 3 дня (существующий SUBSCRIPTION_CONFIG.gracePeriodDays). Это означает:
- Для бессрочных подписок (
expiresAt = null) потолок по сроку не применяется - действует только потолок плана. - Для ежемесячных подписок Stripe потолок примерно равен следующей дате выставления счёта + 3 дня. Когда Stripe каждый месяц продвигает
expiresAtвперёд, потолок смещается вместе с ним. - Для пробных подписок потолок равен дате окончания пробного периода + 3 дня.
Эффективные дни + причина
Каждый ответ на создание/продление включает effectiveDays и reason, чтобы вызывающий мог видеть, почему сертификат получил именно такой срок действия:
| Причина | Значение |
|---|---|
plan_default | Запрос не указан, переопределения нет - использовано значение по умолчанию для плана |
subscription_override | Запрос не указан - использовано переопределение подписки как значение по умолчанию |
requested | Запрос вызывающего принят в рамках всех ограничений |
plan_max_clamp | Запрос вызывающего превысил потолок плана - ограничен |
override_max_clamp | Запрос вызывающего превысил переопределение подписки - ограничен |
subscription_cap_clamp | Иначе допустимый срок превышал бы expiresAt + 3 дня подписки |
Модальное окно создания на клиентском портале использует эти причины для отображения предварительного просмотра в реальном времени (“Вы получите сертификат на 18 дней. Ограничен, так как срок сертификата не может превышать дату окончания подписки более чем на 3 дня.”), чтобы клиенты не отправляли заявку вслепую.
Адаптивный порог продления
Цикл автообновления on-premise использует адаптивный порог по образцу Let’s Encrypt:
effectiveThresholdDays = min(env.RENEW_THRESHOLD_DAYS, ceil(certValidityDays / 3))
15-дневный COMMUNITY сертификат продлевается при 5 оставшихся днях. 90-дневный BUSINESS сертификат продлевается при 14 оставшихся днях (применяется потолок из переменной окружения). 120-дневный ENTERPRISE сертификат продлевается при 14 оставшихся днях. Это предотвращает немедленное срабатывание продления для краткосрочных сертификатов, при этом давая долгосрочным сертификатам достаточный буфер.
Принуждение единственного активного сертификата
Подписка может иметь не более одного активного сертификата делегирования одновременно (MAX_ACTIVE_DELEGATION_CERTS_PER_SUBSCRIPTION = 1).
Почему только один?
Каждая on-premise установка соблюдает maxRepoLicenseIssuancesPerMonth, maxActivations и целостность цепочки согласно собственному локальному реестру выдачи. On-premise сервер не синхронизирует счётчики использования с вышестоящим - в этом и состоит смысл делегирования с поддержкой офлайн-режима.
Если бы подписка имела несколько активных сертификатов (по одному на каждую установку), каждая установка соблюдала бы лимит независимо:
- Подписка на 500 выдач/месяц с 3 активными сертификатами позволяет до 1 500 выдач/месяц на практике.
- Три параллельные цепочки, каждая привязанная к genesis, без возможности аудиторского согласования.
Вышестоящий сервер не может обнаружить этот обход, поскольку on-premise серверы спроектированы для работы в офлайн-режиме. Единственный активный сертификат - единственная применимая на практике модель. Клиентам с несколькими установками (production + staging + DR) необходимо приобретать по одной подписке на каждую установку.
Поведение при коллизии
POST /admin/delegation-certs и POST /portal/delegation-certs отклоняют повторное создание ответом:
HTTP/1.1 409 Conflict
{
"code": "DELEGATION_CERT_ALREADY_ACTIVE",
"existingCertId": "...",
"actions": {
"renew": "POST /portal/delegation-certs/process-renewal-request (preserves chain)",
"revokeAndCreate": "POST /portal/delegation-certs/{existingCertId}/revoke then retry create"
}
}
Клиентский портал отображает это в специальном диалоговом окне с объяснением последствий:
- Продлить (рекомендуется) - расширяет существующую цепочку. Все ранее выданные лицензии репозиториев продолжают работать.
- Отозвать и создать - отбрасывает существующую цепочку и начинает заново с genesis. Ранее выданные лицензии репозиториев становятся непроверяемыми после истечения
validUntilСТАРОГО сертификата. Используйте только при переходе на новый on-premise с другим ключом подписи или при восстановлении после скомпрометированного ключа.
renew() - это атомарная замена, сохраняющая принцип единственного активного сертификата и не подпадающая под проверку коллизии 409.
Ограничение частоты запросов
Даже при наличии единственного активного сертификата злоумышленник может циклически выполнять revoke -> create -> revoke -> create, расходуя циклы подписи мастер-ключом вышестоящего сервера. Оба эндпоинта создания ограничивают частоту до 10 попыток за скользящие 24 часа для каждой подписки через таблицу rateLimits:
HTTP/1.1 429 Too Many Requests
Retry-After: 78234
{ "code": "DELEGATION_CERT_RATE_LIMITED", "retryAfterSec": 78234 }
Счётчик увеличивается при каждой попытке независимо от результата (циклы спама при коллизиях также ограничиваются).
Обнаружение форков
Если клиент делится своим сертификатом делегирования с другой стороной (или запускает два on-premise сервера с одним сертификатом), цепочки расходятся. Вышестоящий сервер обнаруживает это при продлении.
Поток продления
- Admin on-premise вызывает
POST /admin/delegation-certs/renewс текущей вершиной цепочки:{ subscriptionId, currentChainHash, currentSequence, delegatedPublicKey } - Вышестоящий сервер сверяет записи цепочки с собственным реестром.
- Если
currentChainHashне совпадает с записью вышестоящего сервера приcurrentSequence, обнаружен форк:409 { code: 'CHAIN_FORK_DETECTED', divergedAtSequence: N } genesisHashнового сертификата устанавливается равным текущему хешу цепочки, чтобы машины со старым состоянием цепочки могли продолжить с того места, где остановились.
Если сертификат передан лицу, не являющемуся клиентом:
- Они могут использовать его в течение срока действия сертификата.
- При первом продлении вышестоящий сервер видит только одну цепочку (легитимную).
genesisHashнового сертификата совпадает только с легитимной цепочкой.- Машины на общей цепочке немедленно отклонят новые лицензии, так как их сохранённый
chainHashне связан сgenesisHashнового сертификата.
Продление в изолированном окружении
Для on-premise установок без исходящего HTTPS-доступа к вышестоящему серверу поток продления полностью офлайновый. Для замыкания цикла предусмотрены три новых эндпоинта:
На on-premise (auth, root, requireElevated()):
GET /onprem/cert-current- загрузить текущий подписанный сертификат (резервная копия, аудит, повторный импорт)GET /onprem/renewal-request- сгенерировать подписанный манифест с текущей вершиной локальной цепочки + делегированным открытым ключом, подписанный закрытым ключом on-premise
На вышестоящем сервере (admin или портал с областью org):
POST /admin/delegation-certs/process-renewal-request(системный root для работы с несколькими клиентами)POST /portal/delegation-certs/process-renewal-request(владелец/Admin организации)
Манифест запроса на продление
Запрос на продление - небольшой JSON-документ:
{
"manifest": {
"schemaVersion": 1,
"generatedAt": "2026-04-15T12:00:00.000Z",
"subscriptionId": "...",
"currentChainHash": "...",
"currentSequence": 42,
"delegatedPublicKey": "MCowBQYDK2VwAyEA...",
"currentCertValidUntil": "...",
"currentCertPublicKeyId": "...",
"currentCertId": null
},
"signature": "<base64 Ed25519>",
"publicKeyId": "..."
}
Подпись вычисляется по каноническому кодированию манифеста (ключи отсортированы по алфавиту, затем JSON.stringify) с использованием закрытого ключа on-premise. Это гарантирует, что обе стороны вычисляют идентичные байты независимо от порядка построения объекта.
Проверка на вышестоящем сервере
processRenewalManifest() выполняет пять проверок:
- Активный сертификат существует для подписки манифеста. Иначе возвращает
404 NO_ACTIVE_CERT- клиент должен использовать поток создания, а не продления. - Делегированный открытый ключ совпадает с активным сертификатом. Иначе возвращает
400 DELEGATED_KEY_MISMATCH- защита от воспроизведения с другого on-premise сервера. - Подпись манифеста проходит проверку по
delegatedPublicKeyактивного сертификата. Иначе возвращает400 MANIFEST_SIGNATURE_INVALID- доказывает, что манифест исходит от владельца закрытого ключа on-premise. - Возраст манифеста не превышает 7 дней (
RENEWAL_MANIFEST_MAX_AGE_MS). Иначе возвращает400 MANIFEST_EXPIRED- защита от воспроизведения. - Связь хешей цепочки при
currentSequenceманифеста совпадает с реестром вышестоящего сервера. Иначе возвращает409 CHAIN_FORK_DETECTED- защита от форкнутых цепочек.
Если все проверки пройдены, processRenewalManifest вызывает существующий поток renew(), который атомарно истекает старый сертификат и вставляет новый. Не подпадает под проверку коллизии 409 на стороне создания, поскольку является атомарной заменой, а не двухшаговой операцией отзыва+создания.
Продвижение порядкового номера во время транзита
Манифест запроса на продление фиксирует вершину цепочки на момент генерации. Пока манифест находится в пути (доставка через USB, зашифрованная почта), on-premise сервер может продолжать выдавать лицензии репозиториев, продвигая свою локальную цепочку.
Когда новый сертификат загружается обратно на on-premise, /onprem/cert-upload проверяет, что genesisSequence нового сертификата по-прежнему связывается с известной записью в локальном реестре:
- Если
cert.genesisSequence > localHead.sequence- возвращает409 CHAIN_HEAD_BEHIND(вышестоящий сервер находится на форкнутой цепочке). - Если
cert.genesisSequence > 0и запись в локальном реестре при данном порядковом номере имеетchainHash, отличный отcert.genesisHash- возвращает409 CHAIN_FORK_ON_UPLOAD(локальная цепочка разошлась). - В противном случае сертификат принимается. Будущие выдачи продолжаются с
localHead.sequence + 1.
Это означает, что заморозка записей во время транзита не требуется. Цепочка расширяется естественным образом с обеих сторон. Аналогично тому, как продление X.509 обрабатывает серийные номера в процессе выдачи.
Периодический аудит
Вышестоящий сервер предоставляет эндпоинт аудита для проверки целостности цепочки без продления сертификата:
POST /admin/delegation-certs/audit
{ subscriptionId, chainEntries: [{ sequence, chainHash }, ...] }
Вышестоящий сервер обходит записи и возвращает { valid: true } или { valid: false, divergedAtSequence: N, expected, actual }.
On-premise серверы должны периодически вызывать этот эндпоинт (по умолчанию: еженедельно через переменную окружения UPSTREAM_AUDIT_URL) для раннего обнаружения форков.
Доказательства аудита на стороне машины
Renet может локально проверить непрерывность цепочки с помощью VerifyAuditProof. Когда машина продлевает лицензию после долгого перерыва, сервер может вернуть промежуточные записи цепочки в качестве доказательства. Машина обходит доказательство, проверяя, что каждый chainHash получается из предыдущего prevHash + blobHash через SHA-256, обнаруживая любую подделку без обращения к вышестоящему серверу.
Безопасность конкурентного доступа
D1 (база данных Cloudflare) не поддерживает интерактивные транзакции. Одновременная выдача лицензий для одной подписки может привести к коллизии порядкового номера. Сервер аккаунтов обрабатывает это следующим образом:
- Считывает следующий порядковый номер + предыдущий хеш цепочки.
- Формирует и подписывает блоб с зафиксированным порядковым номером.
- Вставляет запись в реестр с
onConflictDoNothing. - Если вставка возвращает 0 изменённых строк, порядковый номер занят другим запросом - повторно получает порядковый номер, повторно формирует, переподписывает и повторяет попытку.
- После 10 неудачных попыток завершается с ошибкой.
Ключевая деталь: при повторной попытке блоб переподписывается. Наивная повторная попытка, только обновляющая запись в реестре, оставила бы подписанный блоб с устаревшим порядковым номером, нарушая цепочку.
Транспорт электронной почты
Сервер аккаунтов может отправлять транзакционные email (магические ссылки, сброс пароля, уведомления безопасности) через два подключаемых транспорта:
| Транспорт | Конфигурация |
|---|---|
ses (по умолчанию) | AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY, AWS_SES_REGION, AWS_SES_FROM |
smtp | EMAIL_TRANSPORT=smtp, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECURE, SMTP_FROM |
Оба транспорта работают для облачных и on-premise развёртываний. Выберите тот, что подходит вашей инфраструктуре: AWS SES с вашим собственным аккаунтом AWS или любой SMTP-сервер (Microsoft Exchange, Postfix, SendGrid, Mailgun и другие).
Транспорт выбирается при запуске через переменную окружения EMAIL_TRANSPORT. SMTP использует пул соединений и ленивую загрузку, поэтому библиотека SMTP-клиента инициализируется только при выборе SMTP.
Все шаблоны email и публичный API email идентичны для всех транспортов.
Связанная документация
- Установка On-Premise - как развернуть on-premise сервер
- Подписка и лицензирование - лимиты плана и слоты машин
- Каналы релизов - каналы edge и stable
- Регионы данных - региональное хранение данных