Skip to main content
В self-hosted-режиме Veriqa работает как собственный процесс: standalone OIDC-сервер аутентификации в контейнере (внутри — OpenIddict, но вашему приложению это знать не нужно). Ваше приложение общается с ним по стандартному OpenID Connect и не ссылается ни на один пакет Veriqa.
Путь, удобный для compliance. Поскольку ваше приложение — обычный OIDC-клиент, ни один компонент Veriqa не попадает в вашу сборку и SBOM. Если в организации сканеры лицензий проходят по зависимостям — выбирайте этот режим: вы интегрируетесь по протоколу, а не по пакету. Встроенная альтернатива — .NET quickstart.

1. Получите образ

Каждый релиз публикуется в реестр контейнеров публичного репозитория:
Без тега Docker берёт latest — самый свежий релиз. Чтобы закрепить релиз в продакшене, укажите его версию тегом (registry.gitlab.com/veriqa/veriqa:<версия>).
Опубликованный образ — linux/amd64. На другой архитектуре соберите его из корня репозитория — Dockerfile там публикует тот же standalone-хост — и подставьте свой локальный тег вместо имени образа в командах ниже:
Образ многостадийный (сборка на SDK → рантайм ASP.NET), запускается под non-root пользователем (UID 1654), экспонирует порт 8080 по HTTP и несёт HEALTHCHECK против /health/live. TLS ожидается терминированным перед ним — см. шаг 6.

2. Настройте его

Контейнер читает ту же конфигурацию, что и любое ASP.NET Core приложение, поэтому каждая настройка может быть передана переменной окружения. Вложенные ключи используют двойное подчёркивание как разделитель: Veriqa:OpenIddict:Database:Provider превращается в Veriqa__OpenIddict__Database__Provider.
docker run
Секреты в командной строке попадают в историю оболочки и в docker inspect. Используйте --env-file, секреты Docker/Kubernetes или ваш secret-manager — пример выше расписан лишь для показа имён ключей.
Хранилища настраиваются независимо и могут указывать на одну и ту же БД:
  • Veriqa:OpenIddict:Database — OIDC-клиенты, авторизации и токены. Умолчания нет: контейнер с пустой секцией не стартует — проверка на старте отказывает и называет лечение (в Development тот же текст пишется как Warning). Для реального использования задайте Provider = PostgreSQL или SqlServer и укажите ConnectionString; Provider = InMemory — это явный выбор волатильного хранилища: каждый рестарт сбрасывает выданные токены — подходит для разработки и для коннектора на одном инстансе.
  • Veriqa:TransactionEngine:Store — сами транзакции входа. Задание ConnectionString переключает его на EF Core; пустое значение оставляет хранилище в памяти. Provider — PostgreSQL (он же при пустом ключе) или SqlServer.
  • Veriqa:Logging:Store — журнал аудита, необязательно: по умолчанию он делит БД с транзакциями. См. Сток аудита.
Значения провайдера регистр не учитывают. Для SQL Server строка подключения — в обычной форме SqlClient: Server=sqlserver;Database=veriqa;User Id=veriqa;Password=…. Резолверы — fail-fast: нераспознанное значение провайдера или реляционный провайдер без строки подключения останавливают контейнер на старте, а не молча падают в фолбэк.

Сток аудита

Записи аудита пишутся только при Veriqa:Logging:Mode = Audit. Куда они попадают, решают два необязательных ключа, и каждый откатывается на своё значение независимо: Оставьте оба незаданными — журнал живёт в БД транзакций. Отдельная БД нужна, когда:
  • учётной записью, под которой хранятся записи, должен владеть аудитор — отдельно от той, которой пишет путь входа;
  • профили нагрузки и бэкапа разные: транзакции исчезают за минуты, записи аудита живут RetentionDays;
  • транзакции остаются в памяти, а журнал должен пережить рестарт — задайте только Veriqa:Logging:Store:ConnectionString.
Собственный провайдер поверх заимствованной строки подключения отклоняется на старте сообщением, называющим оба ключа Provider: задайте и строку стока, либо уберите провайдер. Если строка стока отличается от строки транзакций, у readiness появляется отдельная проверка (postgresql-audit или sqlserver-audit). Существующая установка. У установки, работавшей без этих ключей, AuditRecords лежат в БД транзакций. Задание Veriqa:Logging:Store:ConnectionString их не переносит: новая БД начинает с пустой таблицы, а оставшиеся строки ретенция больше не вычищает. Перенесите их или удалите в рамках переключения. Режиму Audit нужен долговечный сток. При Veriqa:Logging:Mode = Audit и незаданных обеих строках подключения записи хранились бы в памяти и терялись при рестарте — поэтому вне Development контейнер отказывается стартовать:
В Development тот же текст пишется как Warning. Проверка выполняется на старте: переключение в Audit позже, без рестарта, повторно не проверяется.

3. Предоставьте сертификаты токенов

Вне Development сервер отказывается стартовать без обоих сертификатов OpenIddict — подписи и шифрования: авто-генерируемых dev-ключей, на которые можно откатиться, нет. Передайте каждый либо путём к файлу, либо инлайн как Base64 PFX. Подойдёт любой сертификат RSA-2048 (или сильнее), самоподписанного достаточно. Один из способов выпустить оба через OpenSSL:
Переменная PFX_PASSWORD хранит пароль, который затем передаётся как …CertificatePassword. Файлы .key не кладите ни в образ, ни в репозиторий. Оба файла .pfx openssl создаёт с правами -rw-------, владельцем становится запустивший его пользователь — это важно, как только вы монтируете их в контейнер, см. предупреждение ниже.
Варианты …Path (SigningCertificatePath / EncryptionCertificatePath) принимают путь к PFX, смонтированному в контейнер, — обычно вариант лучше, так как Base64-форма даёт очень длинные значения переменных окружения.
Смонтированный PFX должен быть читаем пользователем контейнера. Образ работает под UID 1654 (шаг 1), а файлы, которые даёт рецепт выше, читает только выпустивший их пользователь хоста. Смонтированные как есть, они валят контейнер на старте с CryptographicException: Error occurred during a cryptographic operation — сообщением, которое не называет ни путь, ни отказ в доступе. Перед монтированием передайте файлы пользователю образа:
chown — лучший из двух способов: файлы остаются закрыты для всех, кроме пользователя образа. Но он требует root на хосте — там, где его нет, читать файлы контейнеру даст и chmod 644 signing.pfx encryption.pfx, только тогда PFX становится читаем любому локальному пользователю, а пароль, которым он открывается, лежит рядом — в окружении контейнера. Применяйте chmod только на хосте, который принадлежит вам одному. Файлы .key в контейнер не монтируются вовсе, а Base64-формы это не касается — там сертификат едет значением переменной окружения, а не файлом.
Этими сертификатами подписываются и шифруются ваши выданные токены. Держите их стабильными между рестартами и общими между инстансами — их ротация инвалидирует все живые токены — и относитесь к паролям как к секретам.

4. Объявите свой клиент

Приложение, которое выполняет вход пользователей, регистрируется как OIDC-клиент в Veriqa:OpenIddict:Clients. Образ не несёт клиентов, поэтому этот шаг обязателен — и каждый клиент валидируется на старте: отсутствие ClientId, пустой AllowedRedirectUris или пустой AllowedScopes останавливают контейнер с сообщением, называющим проблемную запись. С env-файлом это удобнее выразить как JSON — смонтируйте файл настроек veriqa.json в контейнер по пути /app/config/veriqa.json (переменная VERIQA_SETTINGS_FILE переносит его в другое место; если по заданному в ней пути файла нет, контейнер не стартует). В файле допустимы любые настройки Veriqa, не только клиенты; ключ, заданный через окружение или --env-file, перекрывает тот же ключ из файла:
veriqa.json
redirect_uri сверяется точным совпадением с AllowedRedirectUris — wildcard’ы не поддерживаются, URI должны быть абсолютными, а http принимается только для loopback-адресов (localhost, 127.0.0.1, [::1]) в разработке.

5. Направьте своё приложение на него

Ваше приложение — стандартный OIDC-клиент. Ничего специфичного для Veriqa в него не попадает — это та же конфигурация, что вы написали бы для любого OpenID Connect провайдера. В ASP.NET Core она приезжает одним пакетом:
Program.cs (ваше приложение, не Veriqa)
Аватар пользователя (picture) гейтится Veriqa-специфичным scope avatar: добавьте options.Scope.Add("avatar") и options.GetClaimsFromUserInfoEndpoint = true, а avatar впишите в AllowedScopes клиента из шага 4. В id_token аватар не попадает никогда — см. OIDC + Veriqa: разбор. Discovery, authorization code flow и выдача токенов — обычное поведение OpenIddict, поэтому подходит любая совместимая OIDC-клиентская библиотека — .NET, Node, Go, Java, Python. Блок выше — только регистрация. Чтобы провести вход из шага 8, нужны ещё конвейер (app.UseAuthentication(), app.UseAuthorization()) и конечная точка, выдающая challenge, — полная программа клиента, вместе с ними обоими, лежит в шаге 5 .NET quickstart. Та программа написана для встроенного quickstart, поэтому два значения в ней свои: Authority указывает на https://localhost:7300, и она запрашивает scope email, которого нет в AllowedScopes клиента из шага 4 — либо уберите options.Scope.Add("email"), либо добавьте email в AllowedScopes. Остальное применимо как есть.

6. Поставьте за reverse-proxy

Контейнер говорит по HTTP; TLS терминируется на вашем прокси. Чтобы OpenIddict строил корректные https-URL, прокси должен пробрасывать X-Forwarded-Proto / X-Forwarded-For, а хосту нужно сообщить, каким прокси доверять, — в том же veriqa.json из шага 4:
veriqa.json
В контейнере это не опционально. По умолчанию доверяется только loopback, а запрос, приходящий через контейнерную сеть, идёт с gateway моста — так что ненастроенный хост молча игнорирует X-Forwarded-Proto, продолжает считать запрос http, и даже /.well-known/openid-configuration отвечает invalid_request («This server only accepts HTTPS requests»). Как только сеть прокси перечислена, discovery возвращает https-URL и вход работает. Оставьте прокси запас и на заголовки запроса. Запросы входа несут cookie: собственные cookie Veriqa и, если ваше приложение делит с ней имя хоста или родительский домен (в разработке все порты localhost — один хост), cookie корреляции, nonce и сессии OIDC-клиента. NGINX по умолчанию ограничивает один заголовок 8k и на более длинный отвечает 400 Request Header Or Cookie Too Large — поднимите лимит в блоке server:
Прокси — это и место per-IP rate limiting. Ядро считает свои лимиты на маршрут, на пользователя и на клиента, но не на адрес, поэтому ограничитель по IP клиента (limit_req в NGINX, правило WAF, fail2ban) — часть этого же шага, см. Hardening к продакшну.

7. Включите каналы

Настройка каналов — токены ботов, регистрация вебхуков, Meta Cloud API, SMTP — идентична в обоих режимах поставки и живёт в Настройке каналов. Одна деталь специфична для self-hosting: вебхуки каналов должны быть доступны из интернета по https://auth.your-domain.com/api/channels/…, поэтому reverse-proxy обязан выставить эти пути. Хотя бы один канал обязателен — без него вход подтверждать негде. В команде из шага 2 включён Telegram; вот что значат его ключи, чтобы дойти до рабочего входа, не уходя с этой страницы: Вебхук Veriqa регистрирует в Bot API сама на старте — вручную setWebhook вызывать не нужно. Регистрацию проверьте через Bot API:
url равен https://auth.your-domain.com/api/channels/telegram/webhook, last_error_message отсутствует. В логе контейнера есть и строка Telegram webhook registered: …, но только на уровне Information: образ в Production пишет лог от Warning, поэтому там этой строки нет. Секция валидируется на старте, как и остальные: включённый адаптер без BotToken, без WebhookSecretToken или с относительным либо не-HTTPS WebhookBaseUrl останавливает контейнер сообщением, называющим ключ.
UpdateMode=Polling публичного URL не требует — годится, чтобы проверить вход до того, как настроен прокси, но не для развёрнутой установки. Опрашивать бота может только один процесс: Bot API отдаёт очередь апдейтов единственному клиенту getUpdates, поэтому второй инстанс на том же токене не получает ничего — см. Запуск более одного инстанса.

8. Проверьте

Критерий готовности: зелёные шаги 1–4. Процесс жив, зависимости отвечают, discovery отдаёт https-URL, канал зарегистрирован — установка развёрнута. Шаг 5 проверяет уже вашу интеграцию (клиента и сценарий), а не сам сервер.
1

Проверьте liveness

curl -f http://localhost:8080/health/live — процесс поднят.
2

Проверьте readiness

curl -f http://localhost:8080/health/ready — настроенные зависимости (БД, Redis, RabbitMQ) отвечают. Именно этот probe стоит завести в оркестратор.
3

Проверьте discovery

curl https://auth.your-domain.com/.well-known/openid-configuration возвращает OIDC-метаданные с https-URL. Если они выходят как http, вернитесь к шагу 6.
4

Проверьте, что канал поднялся

Для режима Webhook: getWebhookInfo бота отдаёт в url ваш /api/channels/telegram/webhook без last_error_message — см. шаг 7. url пуст — подтверждать вход будет нечем. В режиме Polling такого вызова нет: читайте /health/channels, помня, на какие вопросы он не отвечает (ниже).
5

Проведите вход

Инициируйте вход из вашего приложения и подтвердите его в доверенном канале на телефоне. У Email — и у любого канала при Veriqa:LoginConfirmation:Mode = OnWebPage — страница входа затем спрашивает ещё раз: подтвердите и там.
На что /health/channels отвечает, а на что — нет. Эндпоинт сообщает состояние каждого включённого канала отдельно, и читать нужно именно значение канала: healthy, unhealthy или timeout.
  • HTTP-код всегда 200. Отказ канала отдаётся как Degraded, но никогда как Unhealthy, и это сделано намеренно: недоступный мессенджер не должен выбивать работающую реплику из ротации. Поэтому curl -f успешен даже когда лежат все каналы — алертить нужно по телу ответа, а не по коду. По той же причине каналов нет в /health/ready.
  • Отчёт может быть устаревшим. Это снимок, обновляемый не чаще раза в 15 секунд, и двигает его только чтение: запрос, заставший снимок просроченным, всё равно отвечает по старому, а обновление запускает в фоне. Первое чтение после падения канала ещё может сказать healthy, правду скажет следующее, — поэтому, если вы по нему алертите, опрашивайте его периодически.
  • Он не знает, кому принадлежит очередь апдейтов. Зонд — аутентифицированный вызов платформы, так что неверный или отозванный токен действительно выйдет как unhealthy; но токен, который валиден и уже занят другим опрашивающим, ответит безупречно. См. Запуск более одного инстанса.

9. Типичные ошибки первого запуска

Почти всё, что ломается на первом запуске, ломается на старте — контейнер не остаётся жить в полусломанном состоянии, а завершается с сообщением, называющим ключ. Поэтому первое действие при любом сбое — docker logs veriqa.

Запуск более одного инстанса

Одному контейнеру не нужно ничего дополнительно. Масштабирование требует двух общих кусков состояния: Оставьте оба незаданными — и каждый инстанс держит свои ключи в памяти: нормально для одного контейнера, сломано для нескольких. Общий том для KeysDirectory работает как альтернатива Redis. Само собой не масштабируется ещё одно: доставка апдейтов в режиме Polling. Bot API отдаёт очередь апдейтов единственному клиенту getUpdates, поэтому второй инстанс на том же токене бота не получает вообще ничего — и при этом считает себя healthy, ведь токен валиден и платформа ему отвечает. Выдаёт его только лог: цикл опроса повторяет отказ 409 Bot API, записанный как Conflict: terminated by other getUpdates request — самого номера в строке нет. Масштабируйтесь на режиме Webhook, где каждый инстанс кормит прокси, либо дайте каждой установке своего бота — последнее касается staging-установки рядом с боевой ровно так же, как второй реплики.

Дальше

Настройка каналов

MAX, Telegram, WhatsApp, другие мессенджеры и Email — от начала до конца.

Справочник конфигурации

Каждая секция, ключ и дефолт в одном месте.

Продакшн-харденинг

Что проверить перед выкаткой.

Хранилища

Хранилище транзакций, ретенция и миграции.

Установка службой ОС

Тот же сервер без Docker — systemd или служба Windows, из архива.