Безопасность

Настройки безопасности задаются переменными окружения в файле конфигурации панели — /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/public
  • POST /api/auth/logout
  • GET /api/profile
  • POST /api/profile/2fa/setup
  • POST /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) уязвимостью не считается.