Безопасность
Настройки безопасности задаются переменными окружения в файле конфигурации панели —
/etc/gameap/config.env в Linux, C:\gameap\web\config.env в Windows. После изменения файла панель
нужно перезапустить: gameapctl panel restart.
Двухфакторная аутентификация
Обязательная 2FA для администраторов
По умолчанию включена. Администратор без двухфакторной аутентификации сначала видит напоминание, а через 30 дней — требование её включить, без которого работать с панелью не получится.
| Переменная | По умолчанию | Назначение |
|---|---|---|
AUTH_REQUIRE_MFA_FOR_ADMINS |
true |
Требовать 2FA от администраторов. false полностью отключает механизм |
AUTH_MFA_HARD_FAIL_DAYS |
30 |
Сколько дней даётся на подключение. 0 — только напоминание, без блокировки |
AUTH_MFA_ENROLLMENT_TOKEN_TTL |
15m |
Время жизни ограниченной сессии, которая выдаётся после дедлайна |
Администратором считается пользователь с глобально выданным разрешением
admin roles & permissions — неважно, через роль или напрямую. Отдельной «роли администратора»
в панели нет, и пользователь с id = 1 никаких привилегий по умолчанию не получает.
Как отсчитываются 30 дней
Отсчёт начинается с первого успешного входа администратора без 2FA — не с момента установки панели, не с даты обновления и не с момента включения настройки.
Дата первого напоминания сохраняется в поле metadata таблицы users под ключом
mfa_first_shown_at. Если администратор ни разу не входил в панель, отсчёт для него ещё не начат.
Из этого следует, что после обновления существующей установки каждый администратор получает полные 30 дней начиная со своего следующего входа.
Кнопка «Напомнить позже» откладывает показ окна на 24 часа, но дедлайн не сдвигает. Закрытие окна крестиком или клавишей Esc работает так же.
Что происходит после дедлайна
Вход по-прежнему проходит успешно, но вместо обычной сессии выдаётся ограниченный токен на 15 минут. С ним доступны только пять маршрутов, нужных для подключения 2FA:
GET /api/config/publicPOST /api/auth/logoutGET /api/profilePOST /api/profile/2fa/setupPOST /api/profile/2fa/confirm
Все остальные запросы возвращают 403 с сообщением session is restricted to two-factor enrollment.
В интерфейсе показывается модальное окно без кнопки закрытия.
Это не блокировка учётной записи. Подключите 2FA, войдите заново — и работа продолжится. Ограниченный токен нельзя «повысить» до полноценного, поэтому после подключения панель сама выполнит выход и попросит войти снова.
Два исключения, о которых полезно знать заранее:
- Полноценная сессия, выданная до дедлайна, продолжает работать до истечения своего срока. Токен «запомнить меня», полученный на 29-й день, останется рабочим ещё 7 дней.
- Персональные токены доступа (PAT) под ограничение не попадают вовсе. Токен, выпущенный заранее, продолжит работать с API и после дедлайна — это разумная страховка для автоматизации.
Подключение 2FA
Профиль → двухфакторная аутентификация. Панель покажет QR-код и секрет для ручного ввода, после чего нужно ввести код из приложения-аутентификатора.
Параметры фиксированы и не настраиваются: TOTP по RFC 6238, HMAC-SHA1, 6 цифр, период 30 секунд,
допуск ±1 шаг (то есть примерно ±30 секунд расхождения часов). Такое сочетание поддерживают все
распространённые приложения — Google Authenticator, Authy, 1Password и другие. В приложении запись
будет называться GameAP, имя учётной записи — ваш логин.
Использованный код нельзя применить повторно даже в пределах его 30-секундного окна.
Если часы на сервере или на телефоне уходят больше чем на полминуты, коды перестают подходить — проверьте синхронизацию времени с обеих сторон.
Коды восстановления
При подключении 2FA выдаётся 10 кодов восстановления вида abcde-fghjk. В алфавите нет гласных
и символов, которые легко перепутать (0, o, 1, l, i).
- Каждый код одноразовый.
- Коды показываются ровно один раз — при подключении. В базе хранятся только их хеши, посмотреть коды повторно невозможно.
- Кодом восстановления можно и войти, и отключить 2FA.
- Перевыпуск: профиль → перевыпустить коды восстановления, потребуется ввести пароль. Все прежние коды при этом аннулируются.
Сохраните коды сразу и не в том же менеджере паролей, где лежит пароль от панели.
Потерян доступ
Штатной команды в CLI для сброса 2FA нет: ни gameapctl, ни сама панель не умеют отключать
двухфакторную аутентификацию другому пользователю. Варианты по порядку:
1. Код восстановления. Введите его вместо кода из приложения.
2. Дедлайн прошёл, но 2FA ещё не подключена. Это не потеря доступа: войдите обычным способом и завершите подключение — нужные для этого страницы доступны.
3. Снять требование целиком. В config.env:
AUTH_REQUIRE_MFA_FOR_ADMINS=false
и gameapctl panel restart. Ограничение снимется, а уже выданные ограниченные токены станут
полноценными.
Этот способ не поможет тому, кто уже подключил TOTP и потерял устройство: проверка второго фактора выполняется раньше, чем проверка требования, и продолжит запрашивать код.
4. Оставить напоминание, но убрать блокировку.
AUTH_MFA_HARD_FAIL_DAYS=0
5. Правка базы данных. Единственный путь, когда потеряны и устройство, и коды восстановления. Остановите панель, сделайте резервную копию базы и выполните запрос.
Во всех запросах ниже
login = 'admin'— это пример. Подставьте логин нужной учётной записи и убедитесь, что он существует и единственный:SELECT id, login FROM users WHERE login = '...';Запрос без совпадений выполнится успешно и молча ничего не изменит.
PostgreSQL:
UPDATE users
SET two_factor_enabled = false,
two_factor_secret = NULL,
two_factor_recovery_codes = NULL,
two_factor_last_used_step = NULL
WHERE login = 'admin';
MySQL и SQLite — то же самое, но two_factor_enabled = 0.
Чтобы заодно обнулить 30-дневный отсчёт, удалите ключ mfa_first_shown_at из поля metadata.
В PostgreSQL, где это поле имеет тип JSONB:
UPDATE users SET metadata = metadata - 'mfa_first_shown_at' WHERE login = 'admin';
В MySQL поле хранится как текст с JSON, ключ удаляется так:
UPDATE users SET metadata = JSON_REMOVE(metadata, '$.mfa_first_shown_at') WHERE login = 'admin';
В SQLite — начиная с версии 3.38:
UPDATE users SET metadata = json_remove(metadata, '$.mfa_first_shown_at') WHERE login = 'admin';
Не очищайте поле
metadataцеликом (SET metadata = NULL): кроме отсчёта 2FA в нём могут храниться другие сведения о пользователе, и они будут потеряны.
После этого запустите панель и подключите 2FA заново.
Пароли
Требования к паролю: не меньше 12 и не больше 128 байт. Требований к составу (заглавные буквы, цифры, спецсимволы) намеренно нет — вместо них используется проверка по списку скомпрометированных паролей.
Ограничение считается в байтах, а не в символах. Пароль из 12 кириллических букв занимает 24 байта и проходит проверку с запасом.
Список распространённых паролей взят из SecLists
(xato-net-10-million-passwords), отфильтрован по длине и содержит около 46 000 записей. Он
встроен в бинарник — панель никуда не обращается при проверке пароля и ничего о нём не передаёт.
| Переменная | По умолчанию | Назначение |
|---|---|---|
AUTH_ALLOW_WEAK_PASSWORDS |
false |
Отключает только проверку по списку. Ограничения длины остаются |
AUTH_BCRYPT_COST |
13 |
Стоимость bcrypt. Допустимо от 10 до 14, иначе панель не запустится |
Пароли хешируются bcrypt поверх предварительного SHA-256, поэтому ограничение bcrypt в 72 байта не обрезает длинные пароли. При входе хеш со стоимостью ниже текущей автоматически перехешируется; понизить стоимость уже сохранённых хешей нельзя, даже если уменьшить значение переменной.
Проверка применяется при создании пользователя администратором, при изменении пользователя и при смене собственного пароля. При входе она не применяется — иначе пользователи со старыми слабыми паролями потеряли бы доступ.
Пароль первого администратора, заданный переменной
ADMIN_PASSWORDпри первоначальном заполнении базы, проверку не проходит. Задавайте его осознанно.
CAPTCHA
По умолчанию выключена. Защищает только форму входа (POST /api/auth/login); проверка второго
фактора капчей не закрыта.
| Переменная | По умолчанию | Назначение |
|---|---|---|
CAPTCHA_PROVIDER |
"" |
recaptcha_v2, recaptcha_v3 или turnstile. Пусто — выключена |
CAPTCHA_SITE_KEY |
"" |
Публичный ключ, отдаётся в браузер |
CAPTCHA_SECRET_KEY |
"" |
Секретный ключ, наружу не отдаётся |
CAPTCHA_MIN_SCORE |
0.5 |
Порог только для reCAPTCHA v3, остальными провайдерами игнорируется |
CAPTCHA_FAIL_OPEN |
false |
Пускать ли вход, если сервис проверки недоступен |
CAPTCHA_VERIFY_URL |
"" |
Свой адрес проверки — для прокси исходящего трафика |
При CAPTCHA_FAIL_OPEN=false недоступность сервиса проверки означает 503 на вход в панель.
Если задать
CAPTCHA_PROVIDER, но не задатьCAPTCHA_SECRET_KEY, виджет в форме входа появится, а серверная проверка молча не включится. Задавайте обе переменные вместе.
Домены выбранного провайдера панель сама добавляет в политику CSP — дополнительно настраивать
SECURITY_CSP_EXTRA_SCRIPT_SRC не нужно.
reCAPTCHA v3
Ключи выдаются в консоли reCAPTCHA: зарегистрируйте сайт, выберите тип reCAPTCHA v3 и укажите домен панели.
CAPTCHA_PROVIDER=recaptcha_v3
CAPTCHA_SITE_KEY=6LcExampleSiteKeyExampleSiteKeyExam
CAPTCHA_SECRET_KEY=6LcExampleSecretKeyExampleSecretKeyEx
CAPTCHA_MIN_SCORE=0.5
reCAPTCHA v3 ничего не спрашивает у пользователя: она возвращает оценку от 0.0 до 1.0, где
единица — почти наверняка человек. Вход отклоняется, если оценка ниже CAPTCHA_MIN_SCORE.
Начните со значения по умолчанию 0.5 и меняйте его по обстановке: если жалуются на невозможность
войти — понижайте, если подбор пароля продолжается — повышайте. Значение выше 0.7 заметно мешает
пользователям в браузерах с блокировщиками и в режиме инкогнито.
Панель должна открываться по тому домену, который указан в настройках ключа, иначе проверка не пройдёт. Для нескольких доменов перечислите их все в консоли reCAPTCHA.
Проверка выполняется запросом к https://www.google.com/recaptcha/api/siteverify — этот адрес должен
быть доступен с сервера панели.
Turnstile
Ключи выдаются в панели Cloudflare в разделе Turnstile. Учётной записи достаточно бесплатной, домен не обязан быть делегирован в Cloudflare.
CAPTCHA_PROVIDER=turnstile
CAPTCHA_SITE_KEY=0x4AAAAAAAExampleSiteKey
CAPTCHA_SECRET_KEY=0x4AAAAAAAExampleSecretKey
CAPTCHA_MIN_SCORE для Turnstile не применяется — провайдер возвращает только «прошёл» или
«не прошёл», задавать эту переменную не нужно.
В большинстве случаев Turnstile проходится незаметно для пользователя, а при подозрении показывает короткую проверку. Режим виджета (Managed, Non-interactive или Invisible) выбирается на стороне Cloudflare при создании ключа, со стороны панели он не настраивается.
Проверка выполняется запросом к https://challenges.cloudflare.com/turnstile/v0/siteverify.
reCAPTCHA v2 настраивается так же, как v3, но без CAPTCHA_MIN_SCORE.
Ограничение частоты входов
Ограничения жёстко заданы в коде, переменных окружения для них нет:
- окно — 15 минут;
- не больше 20 неудачных попыток с одного IP-адреса;
- не больше 5 неудачных попыток на один логин.
Ограничение стоит на двух маршрутах: POST /api/auth/login и POST /api/auth/2fa/verify. Клиент
получает 429 и заголовок Retry-After: 900. Счётчик увеличивают только ответы 401; успешный вход
обнуляет счётчик по логину, но не по IP-адресу.
Счётчики хранятся в кэше панели. При CACHE_DRIVER=memory (значение по умолчанию) они сбрасываются
при перезапуске и не общие между несколькими экземплярами панели — для отказоустойчивой установки
используйте CACHE_DRIVER=redis.
Отдельно от этого проверка второго фактора допускает не больше 5 попыток ввода кода на одну попытку входа, после чего нужно вводить пароль заново.
Блокировки учётных записей в панели нет — перебор ограничивается только описанным выше.
HTTP-заголовки и Content Security Policy
Заголовки безопасности включены по умолчанию.
| Переменная | По умолчанию | Заголовок |
|---|---|---|
SECURITY_HEADERS_ENABLED |
true |
Общий выключатель |
SECURITY_CONTENT_TYPE_OPTIONS |
true |
X-Content-Type-Options: nosniff |
SECURITY_FRAME_OPTIONS |
SAMEORIGIN |
X-Frame-Options, пустое значение убирает заголовок |
SECURITY_REFERRER_POLICY |
strict-origin-when-cross-origin |
Referrer-Policy |
SECURITY_HSTS_ENABLED |
true |
Strict-Transport-Security |
SECURITY_HSTS_MAX_AGE |
31536000 |
Срок в секундах, по умолчанию год |
SECURITY_HSTS_INCLUDE_SUBDOMAINS |
false |
Добавляет includeSubDomains |
SECURITY_HSTS_PRELOAD |
false |
Добавляет preload |
HSTS отдаётся только при обращении по HTTPS — по факту TLS-соединения, по заголовку
X-Forwarded-Proto: https или при TLS_FORCE_HTTPS=true. Работа по обычному HTTP при разработке
браузер не «залипает».
Политика CSP
| Переменная | По умолчанию | Назначение |
|---|---|---|
SECURITY_CSP_ENABLED |
true |
Включение политики |
SECURITY_CSP_REPORT_ONLY |
false |
Отдавать Content-Security-Policy-Report-Only вместо блокирующей политики |
SECURITY_CSP_POLICY |
"" |
Полностью заменяет генерируемую политику |
SECURITY_CSP_REPORT_URI |
"" |
Добавляет report-uri |
SECURITY_CSP_EXTRA_SCRIPT_SRC |
"" |
Дополняет script-src, значения через запятую |
SECURITY_CSP_EXTRA_STYLE_SRC |
"" |
Дополняет style-src |
SECURITY_CSP_EXTRA_CONNECT_SRC |
"" |
Дополняет connect-src |
SECURITY_CSP_EXTRA_IMG_SRC |
"" |
Дополняет img-src |
SECURITY_CSP_EXTRA_FRAME_SRC |
"" |
Дополняет frame-src |
SECURITY_CSP_EXTRA_FONT_SRC |
"" |
Дополняет font-src |
Генерируемая политика:
default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self';
script-src 'self' blob: 'wasm-unsafe-eval' <хеши встроенных скриптов>;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self';
frame-src 'self';
worker-src 'self' blob:
Почему в ней есть послабления:
'wasm-unsafe-eval'— при загрузке файлов контрольная сумма SHA-256 считается в браузере через WebAssembly. Без этого разрешения загрузка файлов перестанет работать.blob:вscript-src— так загружаются фронтенд-части плагинов.'unsafe-inline'вstyle-src— стили плагинов и Vue добавляются в страницу как встроенные.
Адреса выбранного провайдера CAPTCHA добавляются в script-src и frame-src автоматически.
Плагин, который подгружает скрипты со стороннего CDN, будет заблокирован политикой. Добавьте нужный домен в
SECURITY_CSP_EXTRA_SCRIPT_SRC. ПеременнуюSECURITY_CSP_POLICYдля этого лучше не использовать: она заменяет политику целиком, вместе с автоматическим разрешением доменов CAPTCHA и хешами встроенных скриптов панели.
Шифрование секретов
| Переменная | Обязательна | Назначение |
|---|---|---|
AUTH_SECRET |
да | Ключ подписи токенов сессий. Без него панель не запустится |
ENCRYPTION_KEY |
нет | Ключ шифрования секретов в базе |
Оба значения должны быть случайными, а не парольной фразой. Но требования к длине у них разные — панель обрабатывает их по-разному.
AUTH_SECRET используется как есть и приводится ровно к 32 байтам: более короткое значение
дополняется, более длинное обрезается, и в журнал попадает предупреждение. Поэтому давайте
ровно 32 символа:
openssl rand -base64 24
Не используйте здесь openssl rand -hex 32: получится 64 символа, панель отбросит половину,
а стойкость останется прежней.
ENCRYPTION_KEY хешируется целиком через SHA-256, длина не ограничена и ничего не теряется.
Здесь можно взять значение подлиннее:
openssl rand -hex 32
Хеширование сохраняет энтропию исходного значения, но не увеличивает её, поэтому ключ должен быть случайным. Парольная фраза здесь небезопасна: её можно подобрать перебором, если утечёт зашифрованное значение.
AUTH_SECRETприводится к 32 байтам молча: более короткое значение дополняется, более длинное обрезается, в журнал попадает только предупреждение. Короткий или предсказуемыйAUTH_SECRETозначает, что токены сессий можно подделать.
ENCRYPTION_KEY используется для шифрования секретов TOTP и пароля подключения к демону в базе.
Если он не задан, секреты TOTP шифруются ключом, производным от AUTH_SECRET, а пароль демона
хранится открытым текстом — при старте панель об этом предупреждает.
Не задавайте
ENCRYPTION_KEYвпервые на работающей установке, где уже подключена 2FA. Ключ шифрования секретов TOTP переключится сAUTH_SECRETнаENCRYPTION_KEY, ранее сохранённые секреты станут нечитаемыми, и всем пользователям придётся подключать 2FA заново. Коды восстановления при этом остаются рабочими — они хранятся отдельно.То же самое произойдёт при потере или смене
ENCRYPTION_KEY. Храните его вместе с резервной копией базы: без него часть данных из копии восстановить не получится.
Пароли пользователей хешируются bcrypt, персональные токены и ключ демона хранятся как SHA-256 —
это необратимые преобразования, ENCRYPTION_KEY к ним отношения не имеет.
Журнал аудита
| Переменная | По умолчанию | Назначение |
|---|---|---|
AUDIT_ENABLED |
true |
Запись событий безопасности |
AUDIT_CLIENT_IP_HEADER |
"" |
Заголовок с реальным IP клиента, например X-Real-IP |
Записываются: успешные и неудачные входы, срабатывания ограничения частоты, отказы в доступе, подключение и отключение 2FA, перевыпуск кодов восстановления, изменение пользователей и назначение ролей, создание и отзыв токенов, изменение и удаление выделенных серверов, операции с файлами, установка и удаление плагинов.
В каждой записи: тип события, категория, исход, идентификатор и логин действующего пользователя, способ аутентификации, IP-адрес, User-Agent, метод и путь запроса, идентификатор запроса.
Журнал аудита — это строки структурированного лога приложения с полем
component=audit. Отдельной таблицы в базе, отдельного файла, ротации, интерфейса просмотра и API для чтения нет. Если записи нужно хранить и искать, настройте сбор журнала панели штатными средствами системы — например, черезjournaldи внешний сборщик логов.
AUDIT_CLIENT_IP_HEADERдоверяет указанному заголовку от любого отправителя — списка доверенных прокси в панели нет. Включайте эту переменную только если обратный прокси гарантированно перезаписывает заголовок в приходящих запросах. Иначе IP-адрес можно подделать, а вместе с ним — обойти ограничение частоты входов по IP.
Сессии и токены
Сессии выдаются в формате PASETO v4.local (AUTH_SERVICE=paseto, альтернатива — jwt). Обычная
сессия живёт 24 часа, с отметкой «запомнить меня» — 7 дней. При выходе токен попадает в список
отозванных, который проверяется на каждом запросе.
Для случаев, когда токен вынужден передаваться в адресе страницы — подключение по WebSocket, скачивание
файлов — выдаются одноразовые короткоживущие токены с префиксом glst_. Их время жизни ограничено
10 секундами независимо от значения AUTH_SHORT_LIVED_TOKEN_TTL.
Защиты от CSRF в панели нет и она не требуется: аутентификация идёт заголовком Authorization,
а не куками.
Список источников, которым разрешено обращаться к API из браузера, задаётся переменной
HTTP_ALLOWED_ORIGINS (значения через запятую). Если она пуста, разрешается единственный источник,
вычисленный из HTTP_HOST. Символ * не поддерживается.
Загрузка файлов
Тип загружаемого файла определяется по его содержимому, а не по расширению и не по заголовку от клиента. По умолчанию разрешены изображения, текстовые файлы, JSON, XML, CSV, YAML и PDF. SVG и HTML намеренно запрещены — они могут содержать скрипты.
| Переменная | По умолчанию | Назначение |
|---|---|---|
FILES_UPLOAD_ALLOWED_MIMES |
"" |
Дополняет список разрешённых типов, не заменяет его |
FILES_UPLOAD_ALLOW_ARCHIVES |
false |
Разрешить архивы: zip, tar, gzip, bzip2, 7z, xz |
FILES_UPLOAD_ALLOW_BINARY |
false |
Разрешить произвольные двоичные файлы |
Запрет на архивы и двоичные файлы — самая частая причина вопроса «почему не загружается файл». Архив может содержать исполняемые файлы, которые распакуются уже на выделенном сервере, поэтому по умолчанию загрузка запрещена. Включайте эти настройки осознанно.
Отклонённые загрузки попадают в журнал аудита с указанием определённого типа файла и причины отказа. Предельный размер одного файла — 100 МБ, он не настраивается.
Плагины
Плагины выполняются в песочнице WebAssembly и не имеют прямого доступа к системе. Сетевые запросы плагинов ограничены отдельно:
| Переменная | По умолчанию | Назначение |
|---|---|---|
PLUGINS_DISABLED |
false |
Полностью отключить механизм плагинов |
PLUGIN_HTTP_BLOCK_PRIVATE_IPS |
true |
Запрет обращений к внутренним адресам сети |
PLUGIN_HTTP_ALLOWED_SCHEMES |
https |
Разрешённые схемы |
PLUGIN_HTTP_ALLOWED_HOSTS |
"" |
Список разрешённых хостов, пусто — без ограничений по хостам |
PLUGIN_HTTP_MAX_TIMEOUT_SECONDS |
30 |
Предельное время запроса |
PLUGIN_HTTP_MAX_REDIRECTS |
5 |
Предельное число перенаправлений, каждое проверяется заново |
Адреса служб метаданных облачных провайдеров заблокированы всегда и разблокировать их нельзя.
Заголовки Set-Cookie, Authorization, WWW-Authenticate и Clear-Site-Data плагину не передаются.
Подробнее — Плагины.
Что настраивается только в коде
Часть переменных присутствует в конфигурации, но на поведение панели не влияет. Не рассчитывайте на них:
AUTH_SESSION_IDLE_TIMEOUTиAUTH_SESSION_IDLE_UPDATE_FREQ— завершения сессии по бездействию сейчас нет. Сессия живёт ровно свой срок: 24 часа или 7 дней.GRPC_ENABLED— такой настройки не существует, gRPC-сервер работает всегда. Старые версииgameapctlдописывают эту строку вconfig.env; она безвредна, но ничего не делает.- Ограничения частоты входов задаются константами в коде, переменных
RATE_LIMIT*нет.
Проценты соответствия OWASP ASVS из файлов docs/security/ASVS.md и ASVS_L2.md в репозитории
панели устарели — сверяйтесь с этой страницей.
Сообщить об уязвимости
Уязвимости принимаются через
GitHub Security Advisory — это
предпочтительный канал — либо письмом на security@gameap.com.
Сроки: подтверждение получения — 72 часа, оценка — 14 дней, публичное раскрытие — 90 дней. Исправления: критические — 14 дней, высокие — 30 дней, средние — 60 дней, низкие — в следующем релизе.
Ненамеренная небезопасная настройка самим администратором (например, AUTH_ALLOW_WEAK_PASSWORDS=true
или SECURITY_HEADERS_ENABLED=false) уязвимостью не считается.