Коротко
Безопасность интеграции держится на двух вещах: API-ключ живёт только на вашем сервере и каждый входящий вебхук вы проверяете по подписи. Остальные пункты укрепляют эти два.
Ниже — 12 пунктов. У каждого есть объяснение «зачем»: пункт, который вы понимаете, работает лучше, чем пункт, который вы просто скопировали.
1. Ключ только на сервере
Ключ из заголовка X-API-Key должен находиться только в памяти вашего сервера или в файле .env. Не кладите его в JavaScript, исполняемый в браузере, внутрь мобильного приложения (APK/IPA) или во фронтенд-бандл.
Зачем: ключ — это полное право создавать счета, читать их статусы и делать возвраты. Ключ, попавший в браузер, виден любому через DevTools. Декомпиляция APK занимает несколько минут.
Для мобильного приложения порядок такой: приложение → ваш сервер → Qut Pay → Kaspi. Приложение никогда не обращается к нам напрямую.
2. Ключ не попадает в репозиторий
Добавьте .env в .gitignore. Не вписывайте ключ в код — ни в тесты, ни в примеры.
Зачем: из истории git ключ не исчезает. Удалите файл — ключ останется в старом коммите. Откроете репозиторий или передадите его подрядчику — ключ уйдёт вместе с ним.
Если ключ уже попал в коммит, чистки файла недостаточно: удалите сам ключ в кабинете и создайте новый.
3. Отдельный ключ на каждую интеграцию
Сайт, мобильное приложение, CRM, внутренний скрипт — у каждого свой ключ. Давайте ключам понятные имена в кабинете («Сайт», «1С», «Тесты»).
Зачем: если скомпрометирован один контур, вы удаляете один ключ. С общим ключом придётся останавливать всё сразу.
4. Минимальный scope
Выдавайте ключу только то, что ему нужно. Всего доступно шесть: invoices:read, invoices:write, refunds:write, subscriptions:manage, webhooks:manage, partner:manage.
| Что делает контур | Какого scope достаточно |
|---|---|
| Создаёт счета на сайте | invoices:write |
| Панель отчётности | invoices:read |
| Инструмент поддержки | invoices:read, refunds:write |
| Сервис подписок | subscriptions:manage, invoices:read |
Зачем: ключ без refunds:write не сможет вывести деньги возвратами. Даже при утечке ущерб ограничен. Подробно: Права доступа (scopes).
5. Привязка ключа к кассиру
Если кассиров или точек несколько, привяжите ключ к конкретному кассиру. Боевые счета привязанного ключа идут только через этого кассира, а счета других кассиров он не видит — получает 404.
Зачем: это и разделение отчётности по точкам, и снижение радиуса поражения. Подробно: Привязка API-ключа к кассиру.
6. Подпись вебхука проверяется по СЫРОМУ ТЕЛУ
Заголовок X-Webhook-Signature — это префикс sha256= плюс HMAC-SHA256(secret, timestamp + "." + rawBody) в hex.
rawBody — это неизменённые байты пришедшего тела. Берите их до разбора в JSON. Если сделать JSON.parse, а потом JSON.stringify, изменятся пробелы и порядок полей — подпись не сойдётся никогда.
Зачем: подпись — единственное доказательство того, что вебхук действительно от нас. Ваш адрес открыт без авторизации (иначе доставка не пройдёт), поэтому любой, кто его нашёл, может прислать вам поддельное «оплачено». Без проверки подписи вы отгрузите неоплаченный заказ.
Подробно и с примером кода: Безопасность вебхуков.
7. Timestamp не старше 5 минут
Сравните X-Webhook-Timestamp с часами своего сервера. Запрос старше 5 минут не принимайте.
Зачем: даже корректно подписанный запрос можно перехватить и отправить повторно позже (replay). Проверка времени закрывает это окно. Часы сервера должны идти по NTP, иначе все вебхуки окажутся «старыми».
8. HTTPS и только настоящий домен
Адрес вебхука в боевом режиме обязан быть на https и на реальном домене. IP-адрес и временные туннели (ngrok, localtunnel и подобные) не принимаются — вы получите webhook_url_requires_https, webhook_url_requires_domain или webhook_url_tunnel_forbidden.
Зачем: вебхук, отправленный по HTTP, можно прочитать и подменить по дороге. Туннельные адреса временные: завтра этот адрес достанется другому человеку.
9. Непредсказуемый адрес вебхука
Не /webhook, а что-то вроде /hooks/qutpay/8f3c1a9e2b7d4f60 — со случайной частью.
Зачем: это не заменяет подпись, но скрипты, массово сканирующие типовые пути, ваш адрес не найдут. Два слоя защиты вместо одного.
Важно: сам адрес должен быть доступен без авторизации. Basic Auth или фильтр по IP приведут к тому, что вебхуки не дойдут.
10. Логи — да, ключи в логах — нет
Пишите в журнал каждое создание счёта и каждый входящий вебхук: время, invoice.id, externalOrderId, статус, HTTP-код ответа, значение X-Webhook-Delivery.
Никогда не пишите в лог: сам API-ключ, секрет вебхука, полные значения заголовков Authorization и X-API-Key. От ключа достаточно последних четырёх символов.
Зачем: без логов вы не разберёте ни один инцидент — это единственное место, где есть ответ на вопрос «вебхук приходил или нет». Но файлы логов часто уезжают в сторонние системы мониторинга, и ключ уезжает вместе с ними.
11. Ключ утёк — удаляйте сразу
Если ключ мог попасть в репозиторий, скриншот, переписку или лог-файл:
- Кабинет → Интеграции → удалите этот ключ. С этого момента он отвечает 401
- Создайте новый ключ и подмените его на сервере
- Просмотрите счета за последние дни: нет ли чужих счетов и неожиданных возвратов
- Найдите канал утечки и закройте его
Зачем: план «поменяю потом» не работает. Пока ключ действителен, тот, кто его нашёл, выставляет счета от вашего имени. По шагам: API-ключ утёк.
12. Плановая ротация
Меняйте ключ раз в год, а также когда: уволился разработчик, закончился договор с подрядчиком, сервер переехал на другой хостинг.
Ротация делается без простоя: создайте новый ключ → подмените на сервере → убедитесь, что всё работает → удалите старый. Два ключа спокойно живут одновременно.
Зачем: со временем вы перестаёте помнить, у скольких людей побывал ключ. Плановая ротация обнуляет эту неопределённость.
Бонус: берегите номер кассира
Пункт не технический, а организационный, но нарушается чаще остальных.
Kaspi разрешает одно активное устройство на кассира. Если кто-то войдёт с номером кассира в приложение Kaspi Pay, наша сессия оборвётся (код Kaspi -101001) и выставление счетов остановится.
Поэтому: держите SIM кассира у ответственного лица или в сейфе, не входите с этого номера в приложение и не раздавайте номер сотрудникам «раз он всё равно свободен».
Вопросы и ответы
Если я опрашиваю статус, а не жду вебхук, подпись нужна? Нет — когда запрос делаете вы, источник ответа очевиден. Но вебхук всё равно стоит включить: Вебхук или опрос статуса.
Где правильно хранить ключ? В .env или в хранилище секретов вашего хостинга. Не держите его в базе данных открытым текстом.
Тестовый ключ надо защищать так же? С qp_test_… реальные деньги не двигаются, риск ниже. Но не заводите вредную привычку: если ключ вписан прямо в код, однажды кто-то просто заменит его на боевой и так и оставит.
Потерял секрет вебхука. Он показывается один раз. Пересоздайте адрес вебхука в разделе Интеграции — получите новый секрет.
Есть ли единый список для финальной проверки? Да: Чек-лист выхода в прод.