РУЧНАЯ УСТАНОВКА АСР КАЗНА-39 (ПОШАГОВО)
Инструкция описывает установку системы вручную на чистый Debian 12/13.
Она полностью соответствует тому, что делает автоматический инсталлер misc/install.sh.
Все примеры даны для каталога /usr/acp, базы acp, пользователя БД acp.
Если у вас другие значения — замените их по всему тексту.
| Оглавление |
|---|
1. ОБЩИЕ СВЕДЕНИЯ
| Параметр | Значение по умолчанию | Где задаётся |
|---|---|---|
| Каталог установки | $base_dir='/usr/acp'; | libexec/config.pl |
| База данных | $conf{dbname} ='acp'; | libexec/config.pl |
| Пользователь БД | $conf{dbuser} = 'acp'; | libexec/config.pl |
| Хост БД | $conf{dbhost} = 'localhost'; | libexec/config.pl |
| Секретный ключ | $conf{secretkey} = 'test12345678901234567890'; | libexec/config.pl |
| Веб-сервер | nginx + fcgiwrap | /etc/nginx/sites-available/acp-kazna-39.conf |
| FreeRADIUS из пакетов или исходников | /etc/freeradius/3.0 или /usr/local/freeradius/etc/raddb | |
ВАЖНО про $base_dir.
Это базовая переменная путей. Все остальные пути в системе строятся от неё. Если вы ставите систему не в /usr/acp, поменяйте только $base_dir — остальное подхватится автоматически.
ВАЖНО про secretkey.
Это ключ шифрования MySQL ENCODE/DECODE. Им зашифрованы:
- пароли администраторов (admins.password)
- пароли абонентов (users.password)
- пароли NAS (nas.mng_password)
- карты, пароли почты и т.д.
Значение должно совпадать в config.pl и в БД. Если оставить значение по умолчанию (test12345678901234567890) — система будет небезопасной.
2. ТРЕБОВАНИЯ
- Debian 12 (bookworm) или Debian 13 (trixie), x86_64
- Минимум 2 ГБ RAM, 20 ГБ диска
- Доступ root (или sudo)
- MariaDB/MySQL
- Perl 5.30+
3. ШАГ 1. СИСТЕМНЫЕ ПАКЕТЫ
Обновите индексы пакетов:| Блок кода | ||
|---|---|---|
| ||
apt-get update |
Установите базовый набор пакетов:
| Раскрыть | ||||
|---|---|---|---|---|
| ||||
apt-get install -y nginx fcgiwrap mariadb-server mariadb-client \ |
3.1. FREERADIUS
Есть два варианта. Выберите один.
Вариант А — из пакетов (проще, рекомендуется для новых установок):
| Блок кода | ||
|---|---|---|
| ||
apt-get install -y freeradius freeradius-mysql freeradius-utils |
Конфиги будут в /etc/freeradius/3.0, бинарник /usr/sbin/freeradius, сервис freeradius.
В Debian 13 бинарник называется freeradius, а не radiusd.
Создайте привычный симлинк:
| Блок кода | ||
|---|---|---|
| ||
ln -sf /usr/sbin/freeradius /usr/sbin/radiusd |
Вариант Б — из исходников:
Конфиги будут в /usr/local/freeradius/etc/raddb, бинарник /usr/sbin/radiusd, сервис radiusd. В этом случае пакеты freeradius* НЕ устанавливайте — apt перезапишет существующую сборку.
Инсталлер определяет вариант автоматически: если найден каталог
/usr/local/freeradius/etc/raddb/mods-available — пакеты не ставятся.
3.2. MTA (SENDMAIL) — ОБЯЗАТЕЛЬНО
Система массово использует функцию sendmail() из ACP::Base. Она вызывает бинарник /usr/sbin/sendmail -t. Без MTA:
- не уходят письма: уведомления абонентам, отчёты администратору, регистрация, счета, рассылки
- Paysys и плагины billd ТОРМОЗЯТ: open(...) || die на отсутствующий бинарник, плюс ожидание таймаутов при каждой попытке отправки
MTA ставится пакетами postfix + mailutils. Пакет mailutils даёт /usr/sbin/sendmail, /usr/bin/mail, /usr/bin/mailx.
| Блок кода | ||
|---|---|---|
| ||
apt-get install -y postfix mailutils |
Проверка:
| Блок кода | ||
|---|---|---|
| ||
ls -la /usr/sbin/sendmail which mail mailx |
Настройте postfix в режиме "Local only" — слушать только 127.0.0.1, чтобы не принимать почту извне и не конфликтовать с другими сервисами:
| Блок кода | ||
|---|---|---|
| ||
echo 'postfix postfix/main_mailer_type select Local only' | debconf-set-selections echo "postfix postfix/mailname string $(hostname -f)" | debconf-set-selections |
| Блок кода | ||
|---|---|---|
| ||
postconf -e 'inet_interfaces = loopback-only' postconf -e 'mydestination = localhost, localhost.localdomain' |
| Блок кода | ||
|---|---|---|
| ||
systemctl enable postfix systemctl restart postfix systemctl is-active postfix |
Проверка отправки:
| Блок кода | ||
|---|---|---|
| ||
printf "To: root@localhost\nFrom: test@localhost\nSubject: test\n\nTest\n" | /usr/sbin/sendmail -t mailq |
Порт 25 должен слушаться только на 127.0.0.1:
| Блок кода | ||
|---|---|---|
| ||
ss -ltnp | grep ':25 ' |
4. ШАГ 2. ПОЛУЧЕНИЕ КОДА
По SSH:
| Блок кода | ||
|---|---|---|
| ||
git clone -b master git@git.it39.su:releases/ACP.git /usr/acp |
По HTTPS с логином/паролем:
| Блок кода | ||
|---|---|---|
| ||
git clone -b master https://git.it39.su/releases/ACP.git /usr/acp |
Если репозиторий уже есть — обновите:
| Блок кода | ||
|---|---|---|
| ||
cd /usr/acp git fetch --all git checkout master git pull origin master |
5. ШАГ 3. КАТАЛОГИ И ПРАВА
5.1. БАЗОВЫЕ КАТАЛОГИ
| Блок кода | ||
|---|---|---|
| ||
mkdir -p /usr/acp/var/log /usr/acp/var/db/rrd /usr/acp/var/run /usr/acp/var/q /usr/acp/backup /usr/acp/Certs |
5.2. КАТАЛОГИ ЛОГОВ - ОБЯЗАТЕЛЬНО
Система пишет логи в отдельные подкаталоги var/log/<Модуль>. Если каталога нет — модуль не сможет открыть файл лога и упадёт (open(...) || die). Поэтому создаём ВЕСЬ набор каталогов заранее.
| Блок кода | ||
|---|---|---|
| ||
for _ld in Abon Accident Acct ACP Api Auth billd Cmd DaDaTa Debug Docs \
Equipment Extreceipt Intercom Internet ipn Iptv MAX Msgs \
Nas_logs Paysys Periodic SMS SQL Storage Telegram Update Ureports; do
mkdir -p /usr/acp/var/log/${_ld}
done |
# Каталог для логов диалогов MAX (по chat_id)
| Блок кода | ||
|---|---|---|
| ||
mkdir -p /usr/acp/var/log/MAX/dialogs |
5.3. ПУСТЫЕ ФАЙЛЫ ЛОГОВ — ОБЯЗАТЕЛЬНО
Создаём пустые файлы заранее, чтобы:
- модуль мог писать лог сразу после установки
- logrotate не ругался на отсутствующий файл
- права были выставлены до первого запуска
| Блок кода | ||
|---|---|---|
| ||
for _lf in \
"Debug/acp.log" "Debug/acp.debug" "Debug/bp.log" \
"SQL/sql_errors" \
"ACP/acp.log" \
"Nas_logs/nas.log" \
"Paysys/paysys_check.log" "Paysys/YKassa.log" "Paysys/TBank.log" "Paysys/user_fn.log" \
"SMS/sms_sender.log" "SMS/sms_result.log" \
"Telegram/telegram.log" "Telegram/telegram_debug.log" \
"Telegram/telegram_api.log" "Telegram/telegram_msgs.log" \
"MAX/max.log" "MAX/webhook.log" "MAX/script.log" "MAX/menu_debug.log" \
"MAX/max_dialog_plugins.log" \
"Msgs/msgs_delivery.log" "Msgs/msgs_autoclose.log" \
"Accident/accident_push.log" \
"Extreceipt/extreceipt_request.log" "Extreceipt/extreceipt_answer.log" \
"Equipment/equipment_mac.log" "Equipment/equipment_mac_grabber.log" \
"Intercom/manual_mapping_sync.log" "Intercom/manual_mapping_batch.log" \
"Abon/gitlab.log" \
"DaDaTa/acp_dadata.log" \
"event_asterisk.log" "gps_tracker.log" "hotspot.log" \
"websocket.log" "websocket_internal.log" \
; do
touch /usr/acp/var/log/${_lf}
done |
5.4. ПРАВА
Права 777 на var. Логи пишут и root (billd, periodic), и www-data (веб-интерфейс), поэтому нужен общий доступ.
| Блок кода | ||
|---|---|---|
| ||
chmod -R 777 /usr/acp/var |
Каталоги, доступные веб-серверу:
| Блок кода | ||
|---|---|---|
| ||
chown -R www-data:www-data /usr/acp/cgi-bin chown -R www-data:www-data /usr/acp/ACP/templates chown -R www-data:www-data /usr/acp/backup |
Исполняемые скрипты:
| Блок кода | ||
|---|---|---|
| ||
chmod +x /usr/acp/libexec/billd chmod +x /usr/acp/libexec/periodic chmod +x /usr/acp/cgi-bin/index.cgi chmod +x /usr/acp/cgi-bin/admin/index.cgi |
5.5. ПРОВЕРКА
| Блок кода | ||
|---|---|---|
| ||
find /usr/acp/var/log -maxdepth 1 -type d | wc -l # должно быть ~29 find /usr/acp/var/log -type f | wc -l # должно быть ~70 |
6. ШАГ 4. БАЗА ДАННЫХ
6.1. СОЗДАНИЕ БД И ПОЛЬЗОВАТЕЛЯ
| Блок кода | ||
|---|---|---|
| ||
mysql -u root -e "CREATE DATABASE IF NOT EXISTS \`acp\` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;" mysql -u root -e "CREATE USER IF NOT EXISTS 'acp'@'localhost' IDENTIFIED BY 'ВАШ_ПАРОЛЬ_БД';" mysql -u root -e "GRANT ALL PRIVILEGES ON \`acp\`.* TO 'acp'@'localhost';" mysql -u root -e "FLUSH PRIVILEGES;" |
6.2. ГЕНЕРАЦИЯ СЕКРЕТНОГО КЛЮЧА
Сгенерируйте ключ (32 символа):
| Блок кода | ||
|---|---|---|
| ||
SECRET_KEY=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-32) echo "SECRET_KEY=$SECRET_KEY" |
| Примечание |
|---|
| Сохраните это значение — оно понадобится и в БД, и в config.pl. |
6.3. ИМПОРТ СХЕМЫ
В схеме db/acp.sql есть значение по умолчанию test12345678901234567890.
Его нужно заменить на сгенерированный ключ ПЕРЕД импортом:
| Блок кода | ||
|---|---|---|
| ||
cd /usr/acp
sed "s/test12345678901234567890/${SECRET_KEY}/g" db/acp.sql > /tmp/acp_import.sql
mysql --default-character-set=utf8 -u acp -p'ВАШ_ПАРОЛЬ_БД' -h localhost acp < /tmp/acp_import.sql
rm -f /tmp/acp_import.sql |
Почему это важно.
Ключ шифрует пароли NAS (nas.mng_password) через ENCODE(..., secretkey). Если ключ в БД и в config.pl не совпадут — пароли NAS не расшифруются, и FreeRADIUS не сможет авторизовать коммутаторы.
Все необходимые индексы уже включены в основную схему db/acp.sql и создаются автоматически при её импорте (см. 6.3). Отдельный файл индексов больше не используется.
7. ШАГ 5. CONFIG.PL
Файл: /usr/acp/libexec/config.pl
Если его нет — создайте из шаблона:
| Блок кода | ||
|---|---|---|
| ||
cp /usr/acp/libexec/config.pl.default /usr/acp/libexec/config.pl |
Отредактируйте следующие параметры:
| Блок кода | ||
|---|---|---|
| ||
# --- База данных ---
$conf{dbhost}='localhost';
$conf{dbname}='acp';
$conf{dbuser}='acp';
$conf{dbpasswd}='ВАШ_ПАРОЛЬ_БД';
# --- Базовая директория (все пути строятся от неё) ---
$base_dir='/usr/acp/';
# --- Секретный ключ кодирования (тот же, что подставили в acp.sql) ---
$conf{secretkey}="ВАШ_SECRET_KEY"; |
Обратите внимание на кавычки:
| Блок кода | ||
|---|---|---|
| ||
- $conf{dbpasswd} — в ОДИНАРНЫХ кавычках
- $conf{secretkey} — в ДВОЙНЫХ кавычках |
Проверка:
| Блок кода | ||
|---|---|---|
| ||
grep -E '^\$(base_dir|conf\{(dbhost|dbname|dbuser|dbpasswd|secretkey)\})' /usr/acp/libexec/config.pl |
Должно вывести ваши значения.
8. ШАГ 6. NGINX + FCGIWRAP
8.1. FCGIWRAP — ПУЛ ПРОЦЕССОВ
| Предупреждение |
|---|
| ВАЖНО: штатный fcgiwrap.service запускает ОДИН процесс fcgiwrap. При нагрузке (много одновременных CGI-запросов) это узкое место: запросы встают в очередь, админка тормозит. |
| Подсказка |
|---|
| Улучшение: пул из 100 процессов через override.conf. systemd сам распределяет соединения из сокета между процессами. |
Создайте каталог и файл:
| Блок кода | ||
|---|---|---|
| ||
mkdir -p /etc/systemd/system/fcgiwrap.service.d |
| Блок кода | ||||
|---|---|---|---|---|
| ||||
[Service]
Environment=DAEMON_PROCS=100
ExecStart=
ExecStart=/usr/sbin/fcgiwrap ${DAEMON_OPTS} -c ${DAEMON_PROCS} |
Применение:
| Блок кода | ||
|---|---|---|
| ||
systemctl daemon-reload systemctl enable fcgiwrap.socket systemctl restart fcgiwrap.socket systemctl is-active fcgiwrap.socket |
Проверка, что процессов несколько:
| Блок кода | ||
|---|---|---|
| ||
ps aux | grep -c '[f]cgiwrap' |
8.1.1. РЕКОМЕНДУЕТСЯ: FastCGI-ПУЛ БИЛЛИНГА (fcgi_runner.pl)
Более эффективная альтернатива fcgiwrap — собственный FastCGI-пул биллинга (модель prefork, как Apache mod_perl prefork). Родительский процесс ОДИН РАЗ компилирует index.cgi и все модули биллинга, а на каждый запрос делает fork(); дочерний процесс затем форкает N воркеров; каждый воркер наследует уже скомпилированный код (copy-on-write) и обслуживает запросы. Это устраняет повторную компиляцию ~45 модулей и чтение локализации на каждый HTTP-запрос.
| Примечание |
|---|
| ВАЖНО: число воркеров пула ДОЛЖНО совпадать с числом воркеров fcgiwrap (DAEMON_PROCS=100 в /etc/systemd/system/fcgiwrap.service.d/override.conf, см. 8.1). Иначе при переходе между пулом и fcgiwrap (например, если пул не поднялся) пропускная способность меняется. В инсталлере misc/install.sh для этого заведена ОДНА переменная FCGI_WORKERS=100, которая используется и для fcgiwrap, и для юнитов пула. |
Установка (выполняется автоматически инсталлером misc/install.sh, вручную — так):
1) Установите модуль FCGI:
| Блок кода | ||
|---|---|---|
| ||
apt-get install -y libfcgi-perl |
2) Убедитесь, что раннер на месте и исполняем:
| Блок кода | ||
|---|---|---|
| ||
chmod 755 /usr/acp/cgi-bin/fcgi_runner.pl chown www-data:www-data /usr/acp/cgi-bin/fcgi_runner.pl |
3) Установите два systemd-юнита (клиентский веб и админка):
| Блок кода | ||
|---|---|---|
| ||
cp /usr/acp/cgi-bin/fcgi/acp-fcgi-index.service /etc/systemd/system/ cp /usr/acp/cgi-bin/fcgi/acp-fcgi-admin.service /etc/systemd/system/ systemctl daemon-reload systemctl enable acp-fcgi-index acp-fcgi-admin systemctl restart acp-fcgi-index acp-fcgi-admin |
4) Проверьте, что сокеты созданы:
| Блок кода | ||
|---|---|---|
| ||
ls -l /run/acp-fcgi-index/index.sock /run/acp-fcgi-admin/index.sock systemctl is-active acp-fcgi-index acp-fcgi-admin |
В nginx (см. 8.4) запросы CGI направляются на сокеты пула5) Число одновременных воркеров задаётся в юните переменной ACP_FCGI_MAX_WORKERS (по умолчанию 100, совпадает с числом воркеров fcgiwrap). Чтобы изменить — отредактируйте юнит:
| Блок кода | ||
|---|---|---|
| ||
location ~* \.cgi|pm$ { fastcgi_pass unix:/run/systemctl edit acp-fcgi-index.sock; ... } location /admin/ { fastcgi_pass unix:/run/acp-fcgi-admin.sock; ... } |
и добавьте:
| Блок кода | ||
|---|---|---|
| ||
[Service]
Environment=ACP_FCGI_MAX_WORKERS=16 |
Затем:
| Блок кода | ||
|---|---|---|
| ||
systemctl daemon-reload
systemctl restart acp-fcgi-index acp-fcgi-admin |
Проверить число воркеров:
| Блок кода | ||
|---|---|---|
| ||
pgrep -fc fcgi_runner |
Для 100 воркеров на пул будет ~202 процесса (2 родителя + 100 index + 100 admin). Память: реальное потребление (PSS) ~1.5 ГБ на 200 воркеров — код общий через copy-on-write.
В nginx (см. 8.4) запросы CGI направляются на сокеты пула:
| Блок кода | ||
|---|---|---|
| ||
location ~* ^/admin/.*\.cgi$ { fastcgi_pass unix:/run/acp-fcgi-admin/index.sock; ... }
location ~* \.cgi|pm$ { fastcgi_pass unix:/run/acp-fcgi-index/index.sock; ... } |
ОБРАТНАЯ СОВМЕСТИМОСТЬ: если ОБРАТНАЯ СОВМЕСТИМОСТЬ: если модуль FCGI не установлен или раннер отсутствует — инсталлер НЕ ломает установку и система продолжает работать через fcgiwrap (как раньше). Apache у клиента — раннер просто не используется.
| Примечание |
|---|
| ВАЖНО: платёжный callback paysys_check.cgi в FastCGI-пул НЕ направляется — он всегда обслуживается fcgiwrap (см. 8.4, отдельный location). Отдельного пула acp-fcgi-paysys НЕ существует. Причина: callback рассчитан на обычный CGI-запуск и при отсутствии совпадений по IP завершается без вывода; fcgiwrap отдаёт это как 200 OK, а fcgi_runner.pl — как 502 Bad Gateway. |
Результат: Результат (замер на боевом, 3 запроса к index.cgi): прямой CGI 0.597/0.465/0.429 сек (разброс из-за компиляции), FCGI-пул 0.316/0.316/0.320 сек (стабильно). Ускорение ~25–45% на лёгкой странице, на тяжёлых админских выше.
8.2. SSL-СЕРТИФИКАТ (САМОПОДПИСАННЫЙ, ЕСЛИ НЕТ СВОЕГО)
| Блок кода | ||
|---|---|---|
| ||
mkdir -p /etc/nginx/ssl openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/nginx-selfsigned.key \ -out /etc/nginx/ssl/nginx-selfsigned.crt \ -subj '/C=RU/ST=Kaliningrad/L=Kaliningrad/O=ASR Kazna-39/CN=stat.example.ru' |
8.3. КАТАЛОГ ЛОГОВ NGINX
| Блок кода | ||
|---|---|---|
| ||
mkdir -p /var/log/KTK-39 |
8.4. КОНФИГ NGINX
Файл: /etc/nginx/sites-available/acp-kazna-39.conf
| Примечание |
|---|
| ВАЖНО: конфиг берётся из шаблона misc/nginx/ktkacp-kazna-39.conf.tpl. В шаблоне есть плейсхолдеры, которые нужно заменить: |
| Блок кода | ||
|---|---|---|
| ||
%SERVER_NAME% — домен (например stat.example.ru) %SERVER_IP% — IP прослушивания (например 10.0.0.1) %BASE_DIR% — каталог установки (например /usr/acp) %LOG_DIR% — каталог логов nginx (/var/log/KTK-39) %SSL_CERT% — путь к сертификату %SSL_KEY% — путь к ключу %FCGI_INDEX_SOCKET% — сокет FastCGI-пула для клиентского веба (по умолчанию unix:/run/acp-fcgi-index/index.sock, при отсутствии пула — unix:/var/run/fcgiwrap.socket) %FCGI_ADMIN_SOCKET% — сокет FastCGI-пула для админки (по умолчанию unix:/run/acp-fcgi-admin/index.sock, при отсутствии пула — unix:/var/run/fcgiwrap.socket) %FCGI_PAYSYS_SOCKET% — сокет для платёжных callback (paysys_check.cgi). ВСЕГДА unix:/var/run/fcgiwrap.socket (отдельный FastCGI-пул для платёжных callback НЕ используется) |
Плейсхолдеры %FCGI_INDEX_SOCKET% и %FCGI_ADMIN_SOCKET% подставляются автоматически инсталлером: если FastCGI-пул биллинга установлен (см. 8.1.1) — указываются сокеты пула, иначе — сокет fcgiwrap. При ручной установке без пула замените их на unix:/var/run/fcgiwrap.socket.
Плейсхолдер %FCGI_PAYSYS_SOCKET% ВСЕГДА подставляется как unix:/var/run/fcgiwrap.socket. Платёжный callback paysys_check.cgi НЕ идёт в FastCGI-пул биллинга (fcgi_runner.pl) — он обслуживается fcgiwrap, как было до внедрения пула. Причина: callback рассчитан на обычный CGI-запуск. При отсутствии совпадений по IP он завершается через exit БЕЗ вывода, и fcgiwrap отдаёт это как 200 OK. Через fcgi_runner.pl пустой ответ превращается в 502 Bad Gateway. Поэтому для paysys_check.cgi в конфиге nginx есть ОТДЕЛЬНЫЙ location с fastcgi_pass на fcgiwrap.
Если ставите вручную — скопируйте шаблон и замените плейсхолдеры:
| Блок кода | ||
|---|---|---|
| ||
cp /usr/acp/misc/nginx/ktkacp-kazna-39.conf.tpl /etc/nginx/sites-available/acp-kazna-39.conf |
Затем отредактируйте файл, заменив %...% на свои значения
Ниже приведён ИТОГОВЫЙ конфиг (плейсхолдеры уже заменены на примерные значения). Это полный боевой конфиг с CSP, HSTS, защитой от сканеров и всеми локациями.
| Блок кода | ||
|---|---|---|
| ||
# Блок сервера для отклонения запросов с неправильным или отсутствующим Host server { listen 10.0.0.1:80; server_name ""; return 444; } # Блок сервера для перенаправления HTTP -> HTTPS server { if ($host = stat.example.ru) { return 301 https://$host$request_uri; } listen 10.0.0.1:80; access_log off; error_log off; server_name stat.example.ru; location / { add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob:; connect-src 'self' data: blob:; worker-src 'self' data: blob:"; try_files $uri $uri/ =404; return 301 https://$host$request_uri; } } # Основной HTTPS сервер с полной конфигурацией безопасности server { # ВАЖНО: default_server НЕ указываем — иначе конфликтует с другими # сайтами nginx на этом же IP (a duplicate default server) listen 10.0.0.1:443 ssl; http2 on; charset utf-8; client_max_body_size 20m; # Расширенная политика безопасности контента (CSP) add_header Content-Security-Policy " default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https://*.yandex.ru https://*.yandex.net https://*.google.com https://*.googleapis.com https://*.gstatic.com https://stackpath.bootstrapcdn.com https://code.jquery.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://*.yandex.ru https://*.yandex.net https://*.google.com https://*.gstatic.com https://stackpath.bootstrapcdn.com; img-src 'self' data: https://*.yandex.ru https://*.yandex.net https://*.google.com https://*.googleapis.com https://*.gstatic.com https://*.maps.yandex.net; connect-src 'self' data: blob: https://*.yandex.ru https://*.yandex.net https://*.google.com https://*.googleapis.com; font-src 'self' data: https: https://*.gstatic.com; worker-src 'self' data: blob:; frame-src https://*.yandex.ru https://*.yandex.net https://*.google.com https://yoomoney.ru; frame-ancestors 'self' https://yandex.ru https://*.yandex.ru; object-src 'none'; base-uri 'self'; form-action 'self'"; # HSTS (HTTP Strict Transport Security) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; # Безопасные куки add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; # Защита от MIME-спуфинга add_header X-Content-Type-Options "nosniff"; # Защита от кликджекинга add_header X-Frame-Options "SAMEORIGIN"; # Защита от XSS-атак add_header X-XSS-Protection "1; mode=block"; # Проверка допустимых доменных имен if ( $host !~* ^(stat.example.ru|www.stat.example.ru)$ ) { return 444; } if ( $http_host !~* ^(stat.example.ru|www.stat.example.ru)$ ) { return 444; } # Настройка параметров Host для FastCGI и прокси fastcgi_param HTTP_HOST $http_host; proxy_set_header Host $server_name; # Страницы ошибок error_page 403 /error/403.html; error_page 404 /error/404.html; # Локация для страниц ошибок location ^~ /error/ { internal; root /usr/acp/cgi-bin/; add_header Content-Type text/html; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; } # Корневая директория сервера root "/usr/acp/cgi-bin"; index index.cgi; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # Пути к SSL сертификатам ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt; ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key; # Отключение OCSP Stapling (так как он недоступен) ssl_stapling ssl_stapling off; ssl_stapling_verify off; # Настройки SSL/TLS ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; ssl_prefer_server_ciphers on; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; # Пути к лог-файлам access_log /var/log/KTK-39/access.log; error_log /var/log/KTK-39/error.log; # Безопасные общие ускорения (без изменения API-логики/авторизации) sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65s; keepalive_requests 1000; reset_timedout_connection on; open_file_cache max=10000 inactive=60s; open_file_cache_valid 120s; open_file_cache_min_uses 2; open_file_cache_errors on; # Основная локация сервера location / { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; root "/usr/acp/cgi-bin/"; index index.cgi; } # Обработка PHP-файлов location ~* \.php$ { fastcgi_pass 127 fastcgi_pass 127.0.0.1:9000; fastcgi_param NO_BUFFERING 1; fastcgi_index index.php; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; include fastcgi_params; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_buffering off; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; } # Обработка CGI и Perl файлов location ~* \.cgi|pm$ { fastcgi_pass unix:/var/run/fcgiwrap.socket; fastcgi_param NO_BUFFERING 1; fastcgi_index index.cgi; fastcgi_pass_header Authorization; fastcgi_param HTTP_CGI_AUTHORIZATION $http_authorization; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; include fastcgi_params; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_buffering off; fastcgi_param HTTP_HOST $host:$server_port; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "ALLOW-FROM https://yandex.ru https://*.yandex.ru https://yandex.net https://*.yandex.net"; } # Локация для стилей location /styles/ { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; add_header Last-Modified $date_gmt; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; if_modified_since off; expires off; etag off; root /usr/acp/cgi-bin/; } # Локация для изображений калькулятора location /img/calculator/ { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; root /usr/acp/cgi-bin/; } # Административная панель location /admin/ { # Защита от сканеров уязвимостей и вредоносных ботов if ($http_user_agent ~* (nikto|wpscan|sqlmap|dirbuster|acunetix|nessus|metasploit|burpsuite|owasp|zap|nmap|openvas|arachni|w3af|skipfish|netsparker|appscan|havij|hydra|medusa|patator|john|aircrack|sqlsus|sqlninja|darkmysql|absinthe|bbqsql|firesheep|wce|ratproxy|beef|bettercap|yersinia|dsniff|ettercap|kismet|reaver|wash|mdk3|wifite|fern|airgeddon|hak5|pwn|exploit|rce|lfi|xxe|xsrf|xss|brute|force|browserling|browsershots|shodan|censys|zoomeye|masscan|gobuster|ffuf|dirb|dirsearch|wfuzz|amass|sublist3r|knockpy|theharvester|altdns|dnsrecon|dnsenum|fierce|sn1per|recon-ng|maltego|spiderfoot|osmedeus|xsstrike|xsser|weevely|webshell|shellshock|struts|joomscan|drupwn|drupalgeddon|wpseku|wprecon|joomlavs|magescan|sentrymb|umbraco|yaws)) { access_log /var/log/KTK-39/blocked_scanners.log; return 403; } add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy " # Платёжные callback (paysys_check.cgi) — ОТДЕЛЬНЫЙ location на fcgiwrap. # ВАЖНО: callback рассчитан на обычный CGI-запуск. При отсутствии совпадений # по IP он завершается через exit БЕЗ вывода — fcgiwrap отдаёт это как 200 OK, # тогда как FastCGI-пул биллинга (fcgi_runner.pl) превращает пустой ответ в # 502 Bad Gateway. Regex-locations проверяются в порядке появления — этот # блок ДОЛЖЕН быть ВЫШЕ общего location ~* \.cgi|pm$. location ~* ^/paysys_check\.cgi$ { fastcgi_pass unix:/var/run/fcgiwrap.socket; fastcgi_param NO_BUFFERING 1; fastcgi_index index.cgi; fastcgi_pass_header Authorization; fastcgi_param HTTP_CGI_AUTHORIZATION $http_authorization; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; include fastcgi_params; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_buffering off; fastcgi_param HTTP_HOST $host:$server_port; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; } # Обработка CGI и Perl файлов location ~* \.cgi|pm$ { fastcgi_pass unix:/run/acp-fcgi-index/index.sock; fastcgi_param NO_BUFFERING 1; fastcgi_index index.cgi; fastcgi_pass_header Authorization; fastcgi_param HTTP_CGI_AUTHORIZATION $http_authorization; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; include fastcgi_params; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_buffering off; fastcgi_param HTTP_HOST $host:$server_port; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "ALLOW-FROM https://yandex.ru https://*.yandex.ru https://yandex.net https://*.yandex.net"; } # Локация для стилей location /styles/ { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; alias "/usr/acp/cgi-bin/admin/"; index index.cgi; fastcgi_index index.cgi; fastcgi_param NO_BUFFERING 1; fastcgi_pass_header Authorization; fastcgi_param HTTP_CGI_AUTHORIZATION $http_authorization; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; include fastcgi_params; fastcgi_buffering off; } # API обработчик location ^~ /api.cgi { add_header Last-Modified $date_gmt; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; if_modified_since off; expires off; etag off; root /usr/acp/cgi-bin/; } # Локация для изображений калькулятора location /img/calculator/ { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/admin; Secure; HttpOnly; SameSite=StrictLax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; try_files $uri /api.cgi$is_args$args; allow all; gzip off; fastcgi_param HTTPS on; fastcgi_pass unix:/var/run/fcgiwrap.socket; fastcgi_index index.cgi; fastcgi_param NO_BUFFERING 1; fastcgi_param HTTP_CGI_AUTHORIZATION $http_authorization; fastcgi_param SCRIPT_FILENAME /usr/acp/cgi-bin$fastcgi_script_name; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; fastcgi_request_buffering off; include fastcgi_params; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_buffering off; } # Обработка изображений location ^~ /img/ { alias /usr/acp/cgi-bin/img/; location /img/logo/ { allow all; add_header Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()"; try_files $uri =404; } location = /img/error.jpg { allow all; try_files /error.jpg =404; } location ~* \.(ico|jpg|jpeg|gif|png|webp|css|js|JPG|GIF|PNG)$ { allow all; } deny all; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; } # Специальные CGI-скрипты location /usr/acp/cgi-bin/(auth|packet|delete_subscription) { root /usr/acp/cgi-bin/; } # Админские CGI — отдельный FastCGI-пул (ВАЖНО: regex-location имеет # приоритет над prefix-location /admin/, поэтому этот блок должен идти # ВЫШЕ общего location ~* \.cgi|pm$) location ~* ^/admin/.*\.cgi$ { fastcgi_pass unix:/run/acp-fcgi-admin/index.sock; fastcgi_param NO_BUFFERING 1; fastcgi_index index.cgi; fastcgi_pass_header Authorization; fastcgi_param HTTP_CGI_AUTHORIZATION $http_authorization; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; include fastcgi_params; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_buffering off; fastcgi_param HTTP_HOST $host:$server_port; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; } # Административная панель location /admin/ { # Защита от сканеров уязвимостей и вредоносных ботов if ($http_user_agent ~* (nikto|wpscan|sqlmap|dirbuster|acunetix|nessus|metasploit|burpsuite|owasp|zap|nmap|openvas|arachni|w3af|skipfish|netsparker|appscan|havij|hydra|medusa|patator|john|aircrack|sqlsus|sqlninja|darkmysql|absinthe|bbqsql|firesheep|wce|ratproxy|beef|bettercap|yersinia|dsniff|ettercap|kismet|reaver|wash|mdk3|wifite|fern|airgeddon|hak5|pwn|exploit|rce|lfi|xxe|xsrf|xss|brute|force|browserling|browsershots|shodan|censys|zoomeye|masscan|gobuster|ffuf|dirb|dirsearch|wfuzz|amass|sublist3r|knockpy|theharvester|altdns|dnsrecon|dnsenum|fierce|sn1per|recon-ng|maltego|spiderfoot|osmedeus|xsstrike|xsser|weevely|webshell|shellshock|struts|joomscan|drupwn|drupalgeddon|wpseku|wprecon|joomlavs|magescan|sentrymb|umbraco|yaws)) { access_log /var/log/KTK-39/blocked_scanners.log; return 403; } add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; alias "/usr/acp/cgi-bin/admin/"; index index.cgi; } # API обработчик location ^~ /api.cgi { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/admin; Secure; HttpOnly; SameSite=Strict" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; try_files $uri /api.cgi$is_args$args; allow all; gzip off; fastcgi_param HTTPS on; fastcgi_pass unix:/var/run/fcgiwrap.socket; fastcgi_index index.cgi; fastcgi_param NO_BUFFERING 1; fastcgi_param HTTP_CGI_AUTHORIZATION $http_authorization; fastcgi_param SCRIPT_FILENAME /usr/acp/cgi-bin$fastcgi_script_name; fastcgi_connect_timeout 60s; fastcgi_read_timeout 120s; fastcgi_send_timeout 120s; fastcgi_request_buffering off; include fastcgi_params; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_buffering off; } # Обработка изображений location ^~ /img/ { alias /usr/acp/cgi-bin/img/; location /img/logo/ { allow all; add_header Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()"; try_files $uri =404; } location = /img/error.jpg { allow all; try_files /error.jpg =404; } location ~* \.(ico|jpg|jpeg|gif|png|webp|css|js|JPG|GIF|PNG)$ { allow all; } deny all; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; } # Специальные CGI-скрипты location /usr/acp/cgi-bin/(auth|packet|delete_subscription) { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; rewrite /get_pl.cgi?$args last; } # Обработка изображений из ACP templates location ^~ /images/ { alias /usr/acp/ACP/templates/; location ~* \.(jpg|gif|png|css|js|JPG|GIF)$ { allow all; } deny all; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"; } } |
Активация и проверка:
| Блок кода | ||
|---|---|---|
| ||
ln rewrite /get_pl.cgi?$args last;-sf /etc/nginx/sites-available/acp-kazna-39.conf /etc/nginx/sites-enabled/acp-kazna-39.conf }nginx -t systemctl reload nginx |
Замените stat.example.ru на ваш домен, 10.0.0.1 — на ваш IP.
| Предупреждение |
|---|
| ВАЖНО: если HTTPS не нужен — уберите блоки с listen 443 и редирект, оставьте только listen 80. |
| Примечание |
|---|
| ВАЖНО: default_server в listen 443 НЕ указывайте — иначе получите "a duplicate default server", если на этом IP уже есть другой сайт nginx. |
9. ШАГ 7. FREERADIUS
Каталог конфигов определяется автоматически:
| Блок кода | ||
|---|---|---|
| ||
- из пакетов — /etc/freeradius/3.0
- из исходников — /usr/local/freeradius/etc/raddb |
Ниже примеры для пакетной установки. Для сборки из исходников замените путь
| Блок кода | ||
|---|---|---|
| ||
/etc/freeradius/3.0 на /usr/local/freeradius/etc/raddb. |
9.1. МОДУЛЬ SQL
Файл: /etc/freeradius/3.0/mods-available/sql
Файл генерируется ЦЕЛИКОМ, а не правится sed'ом. Причина: в штатном файле FreeRADIUS 3.2+ параметры подключения закомментированы, а в блоке mysql{} есть подблок tls{} с ca_file, которого нет в системе — из-за него модуль sql не инстанцируется. Правка sed'ом оставляет дубли строк и не переключает dialect.
| Блок кода | ||
|---|---|---|
| ||
sql # Обработка изображений из ACP templates location ^~ /images/ { alias /usr/acp/ACP/templates/; location ~* \.(jpg|gif|png|css|js|JPG|GIF)$ { database allow all;= "mysql" driver }= "rlm_sql_${database}" server deny all;= "localhost" add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;#port = 3306 add_header Set-Cookie "Path=/; Secure; HttpOnly; SameSite=Lax" always;login = "acp" password = "ВАШ_ПАРОЛЬ_БД" add_header X-Frame-Options "SAMEORIGIN"; radius_db = "acp" secretkey add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' data: blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' data: blob:; worker-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';";= "ВАШ_SECRET_KEY" acct_table1 = "radacct" acct_table2 = "radacct" postauth_table = "radpostauth" authcheck_table = "radcheck" authreply_table = "radreply" }groupcheck_table = "radgroupcheck" } |
Активация и проверка:
| Блок кода | ||
|---|---|---|
| ||
ln -sf /etc/nginx/sites-available/acp-kazna-39.conf /etc/nginx/sites-enabled/acp-kazna-39.conf groupreply_table = "radgroupreply" nginx -t usergroup_table = "radusergroup" systemctl reload nginx |
Замените stat.example.ru на ваш домен, 10.0.0.1 — на ваш IP.
| Предупреждение |
|---|
| ВАЖНО: если HTTPS не нужен — уберите блоки с listen 443 и редирект, оставьте только listen 80. |
| Примечание |
|---|
| ВАЖНО: default_server в listen 443 НЕ указывайте — иначе получите |
"a duplicate default server", если на этом IP уже есть другой сайт nginx.
9. ШАГ 7. FREERADIUS
Каталог конфигов определяется автоматически:
| Блок кода | ||
|---|---|---|
| ||
- из пакетов — /etc/freeradius/3.0
- из исходников — /usr/local/freeradius/etc/raddb |
Ниже примеры для пакетной установки. Для сборки из исходников замените путь
| Блок кода | ||
|---|---|---|
| ||
/etc/freeradius/3.0 на /usr/local/freeradius/etc/raddb. |
9.1. МОДУЛЬ SQL
Файл: /etc/freeradius/3.0/mods-available/sql
Файл генерируется ЦЕЛИКОМ, а не правится sed'ом. Причина: в штатном файле FreeRADIUS 3.2+ параметры подключения закомментированы, а в блоке mysql{} есть подблок tls{} с ca_file, которого нет в системе — из-за него модуль sql не инстанцируется. Правка sed'ом оставляет дубли строк и не переключает dialect.
| Блок кода | ||
|---|---|---|
| ||
sql { delete_stale_sessions = yes sqltrace = no sqltracefile = ${logdir}/sqltrace.sql num_sql_socks = 50 databaseconnect_failure_retry_delay = "mysql"60 driverlifetime = "rlm_sql_${database}"0 max_queries = 0 read_clients = yes servernas_table = "localhostnas" #port = 3306 login = "acp"#$INCLUDE sql/${database}/dialup.conf client_query = "SET sql_mode=''; SELECT ip, INET_NTOA(ip), CONCAT('NAS_', id), nas_type, DECODE(mng_password, '${secretkey}') FROM ${nas_table} WHERE disable=0 GROUP BY ip" password = "ВАШ_ПАРОЛЬ_БД" radius_db = "acp" secretkey = "ВАШ_SECRET_KEY" acct_table1 = "radacct" acct_table2 = "radacct" postauth_table = "radpostauth" authcheck_table = "radcheck" authreply_table = "radreply"} |
Ключевые моменты:
- secretkey — тот же ключ, что в config.pl и в БД
- read_clients = yes — клиенты (NAS) читаются из таблицы nas
- client_query — КАСТОМНЫЙ запрос под схему АСР Казна-39. Штатный запрос FreeRADIUS ищет колонку nasname, которой в АСР Казна-39 нет -> ошибка Unknown column 'nasname'. Пароль NAS (mng_password) хранится зашифрованным, расшифровывается через DECODE(mng_password, '${secretkey}').
- Блок tls{} в файле ОТСУТСТВУЕТ — для локального подключения к MariaDB TLS не нужен
Активация:
| Блок кода | ||
|---|---|---|
| ||
rm -f /etc/freeradius/3.0/mods-enabled/sql
cp -p /etc/freeradius/3.0/mods-available/sql /etc/freeradius/3.0/mods-enabled/sql |
9.2. МОДУЛЬ PERL (RLM_PERL)
Файл: /etc/freeradius/3.0/mods-available/perl
| Блок кода | ||
|---|---|---|
| ||
# АСР Казна-39 Freeradius Config perl { groupcheck_table = "radgroupcheck" groupreply_table = "radgroupreply" usergroup_table = "radusergroup"filename = /usr/acp/libexec/rlm_perl.pl delete_stale_sessions = yes sqltrace = no sqltracefile = ${logdir}/sqltrace.sql num_sql_socks = 50 connect_failure_retry_delay = 60 lifetime = 0 max_queries = 0 } |
Два критичных момента:
- filename указывает ПРЯМО на rlm_perl.pl, без симлинка.rlm_perl.pl берёт config.pl из своего каталога через FindBin ($Bin). Если подсунуть симлинк — $Bin будет указывать на каталог симлинка, и config.pl не найдётся.
- perl_flags = "-T" НЕ задаём. Флаг taint mode ломает загрузку config.pl: Insecure dependency in require while running with -T switch.
Активация:
| Блок кода | ||
|---|---|---|
| ||
rm -f /etc/freeradius/3.0/mods-enabled/perl
cp -p /etc/freeradius/3.0/mods-available/perl /etc/freeradius/3.0/mods-enabled/perl |
9.3. CLIENTS.CONF
Файл: /etc/freeradius/3.0/clients.conf
Очистите файл. Штатный client localhost { ipaddr = 127.0.0.1 } конфликтует с записью NAS_Server (127.0.0.1) из таблицы nas, и FreeRADIUS падает с ошибкой Failed to add duplicate client NAS_1. Клиенты (NAS) читаются из БД (read_clients = yes).
| Блок кода | ||
|---|---|---|
| ||
# АСР Казна-39 Freeradius Config # Клиенты (NAS) читаются из БД: read_clients = yes nas_table = "nas" #$INCLUDE sql/${database}/dialup.confв mods-available/sql # Штатные client_query{} =блоки "SET sql_mode=''; SELECT ip, INET_NTOA(ip), CONCAT('NAS_', id), nas_type, DECODE(mng_password, '${secretkey}') FROM ${nas_table} WHERE disable=0 GROUP BY ip" } удалены, чтобы не конфликтовать с записями таблицы nas |
9.4. MODS-CONFIG/FILES/AUTHORIZE
Файл: /etc/freeradius/3.0/mods-config/files/authorize
Это КРИТИЧНЫЙ файл для IPoE-авторизации (безлогиновой, по MAC). В authorize{} сайта вызывается модуль files, который читает этот файл.
Эталонная строка АСР Казна-39:
| Блок кода | ||
|---|---|---|
| ||
DEFAULT Auth-Type = Perl
Fall-Through = 1 |
Она выставляет Auth-Type = Perl для ЛЮБОГО запроса, у которого Auth-Type ещё не определён. Далее в authenticate{} срабатывает Auth-Type Perl { perl } и вызывается rlm_perl.pl::authenticate() -> auth_() -> Mac_auth2::auth(). БЕЗ этой строки IPoE-запрос (без пароля) не получает Auth-Type, и FreeRADIUS отклоняет его с ошибкой
Ключевые моменты:
- secretkey — тот же ключ, что в config.pl и в БД
- read_clients = yes — клиенты (NAS) читаются из таблицы nas
- client_query — КАСТОМНЫЙ запрос под схему АСР Казна-39. Штатный запрос FreeRADIUS ищет колонку nasname, которой в АСР Казна-39 нет -> ошибка Unknown column 'nasname'. Пароль NAS (mng_password) хранится зашифрованным, расшифровывается через DECODE(mng_password, '${secretkey}').
- Блок tls{} в файле ОТСУТСТВУЕТ — для локального подключения к MariaDB TLS не нужен
Активация:
| Блок кода | ||
|---|---|---|
| ||
ERROR: rmNo -f /etc/freeradius/3.0/mods-enabled/sql cp -p /etc/freeradius/3.0/mods-available/sql /etc/freeradius/3.0/mods-enabled/sql |
9.2. МОДУЛЬ PERL (RLM_PERL)
Auth-Type found: rejecting the user via Post-Auth-Type = Reject |
PPPoE работает и без неё, т.к. Auth-Type выставляет модуль pap/chap — поэтому проблема проявляется только на IPoE.
Штатный файл FreeRADIUS содержит DEFAULT Framed-Protocol == PPP и НЕ подходит.
Содержимое файла:Файл: /etc/freeradius/3.0/mods-available/perl
| Блок кода | ||
|---|---|---|
| ||
# АСР Казна-39 Freeradius Config #SERVICE_L4R User-Password == "cisco", Auth-Type = Accept perl { filename = /usr/acp/libexec/rlm_perl.pl } |
Два критичных момента:
filename указывает ПРЯМО на rlm_perl.pl, без симлинка.rlm_perl.pl берёт config.pl из своего каталога через FindBin ($Bin). Если подсунуть симлинк — $Bin будет указывать на каталог симлинка, и config.pl не найдётся.Активация:
| Блок кода | ||
|---|---|---|
| ||
rm -f /etc/freeradius/3.0/mods-enabled/perl
cp -p /etc/freeradius/3.0/mods-available/perl /etc/freeradius/3.0/mods-enabled/perl |
9.3. CLIENTS.CONF
Файл: /etc/freeradius/3.0/clients.conf
Очистите файл. Штатный client localhost { ipaddr = 127.0.0.1 } конфликтует с записью NAS_Server (127.0.0.1) из таблицы nas, и FreeRADIUS падает с ошибкой Failed to add duplicate client NAS_1. Клиенты (NAS) читаются из БД (read_clients = yes).
| Блок кода | ||
|---|---|---|
| ||
# АСР Казна-39 Freeradius Config
# Клиенты (NAS) читаются из БД: read_clients = yes в mods-available/sql
# Штатные client{} блоки удалены, чтобы не конфликтовать с записями таблицы nas |
9.4. MODS-CONFIG/FILES/AUTHORIZE
Файл: /etc/freeradius/3.0/mods-config/files/authorize
Это КРИТИЧНЫЙ файл для IPoE-авторизации (безлогиновой, по MAC). В authorize{} сайта вызывается модуль files, который читает этот файл.
Эталонная строка АСР Казна-39:
| Блок кода | ||
|---|---|---|
| ||
DEFAULT Auth-Type = Perl
Fall-Through = 1 |
Она выставляет Auth-Type = Perl для ЛЮБОГО запроса, у которого Auth-Type ещё не определён. Далее в authenticate{} срабатывает Auth-Type Perl { perl } и вызывается rlm_perl.pl::authenticate() -> auth_() -> Mac_auth2::auth(). БЕЗ этой строки IPoE-запрос (без пароля) не получает Auth-Type, и FreeRADIUS отклоняет его с ошибкой:
| Блок кода | ||
|---|---|---|
| ||
ERROR: No Auth-Type found: rejecting the user via Post-Auth-Type = Reject |
PPPoE работает и без неё, т.к. Auth-Type выставляет модуль pap/chap — поэтому проблема проявляется только на IPoE.
Штатный файл FreeRADIUS содержит DEFAULT Framed-Protocol == PPP и НЕ подходит.
Содержимое файла:
| Блок кода | ||
|---|---|---|
| ||
#SERVICE_L4R# Cisco-AVPair += "ip:l4redirect=redirect list 197 to group REDIRECT_NOPAY", # Cisco-AVpair += "traffic-class=input access-group 198", # Cisco-AVpair += "traffic-class=output access-group 198", # Cisco-AVPair += "ip:traffic-class=out default drop", # Cisco-AVPair += "ip:traffic-class=in default drop", # Idle-Timeout = "600" #BILLING_ACCESS Password == "cisco", Auth-Type = Accept # Cisco-AVPair += "ip:traffic-class=in access-group 195 priority 4", # Cisco-AVPair += "ip:traffic-class=out access-group 195 priority4", # Cisco-Account-Info += "QU;512000;256000;D;512000;256000" #PREPAID_INTERNET Password == "cisco", Auth-Type = Accept # Cisco-Account-Info += "QU;128000;64000;D;128000;64000", # Cisco-AVPair += "ip:traffic-class=in access-group 196 priority 6", # Cisco-AVPair += "ip:traffic-class=out access-group 196 priority 6", # Cisco-AVPair += "ip:traffic-class=out default drop", # Cisco-AVPair += "ip:traffic-class=in default drop", # Cisco-AVPair += "prepaid-config=TRAFFIC_PREPAID", #192.168.1.1 User-Password == "ciscoISG", Auth-Type = Accept # Cisco-Service-AVPairInfo += "ip:l4redirect=redirect list 197 to group REDIRECT_NOPAYNPREPAID_INTERNET", # Cisco-AVpairControl-Info += "traffic-class=input access-group 198QV1000000", # CiscoService-AVpairType += "traffic-class=output access-group 198"Dialout-Framed-User, # Cisco-Account-AVPairInfo += "ip:traffic-class=out default dropQU;1024000;256000;D;1024000;256000", # Cisco-AVPairAccount-Info += "ip:traffic-class=in default dropAPREPAID_INTERNET", # IdleCisco-Account-TimeoutInfo += "600NPREPAID_INTERNET", #BILLING_ACCESS Password == "cisco", Auth-Type# Idle-Timeout = Accept120, # Cisco-AVPairAccount-Info += "ip:traffic-class=in access-group 195 priority 4", "ABILLING_ACCESS" DEFAULT Auth-Type = Perl # CiscoFall-AVPairThrough += "ip:traffic-class=out access-group 195 priority4", # Cisco-Account-Info += "QU;512000;256000;D;512000;256000" #PREPAID_INTERNET Password == "cisco", Auth-Type = Accept # Cisco-Account-Info += "QU;128000;64000;D;128000;64000", # Cisco-AVPair += "ip:traffic-class=in access-group 196 priority 6",1 |
9.5. САЙТ acp-kazna-39
Файл: /etc/freeradius/3.0/sites-available/acp-kazna-39
Штатный sites-available/default FreeRADIUS НЕ подходит для АСР Казна-39 — в нём нет вызовов perl в authorize/accounting/post-auth и нет Auth-Type Perl.
Генерируем свой сайт.
| Блок кода | ||
|---|---|---|
| ||
server default { listen { type = auth ipaddr = * port = 0 # Cisco-AVPairinterface += "ip:traffic-class=out access-group 196 priority 6",eth0 # Cisco-AVPairclients += "ip:traffic-class=out default drop", per_socket_clients # Cisco-AVPair += "ip:traffic-class=in default drop", limit { # Cisco-AVPair += "prepaid-config=TRAFFIC_PREPAID", max_connections = 65536 #192.168.1.1 User-Password == "ISG", Auth-Type lifetime = Accept0 # Cisco-Service-Info = "NPREPAID_INTERNET", # Cisco-Control-Info += "QV1000000",idle_timeout = 60 # Service-Type = Dialout-Framed-User, } # Cisco-Account-Info += "QU;1024000;256000;D;1024000;256000", } listen { # Cisco-Account-Info += "APREPAID_INTERNET", ipaddr = * # Cisco-Account-Infoipv6addr += "NPREPAID_INTERNET",:: # Idle-Timeout port = 120,0 # Cisco-Account-Info += "ABILLING_ACCESS" type = acct DEFAULT# Auth-Typeinterface = Perleth0 # clients Fall-Through = 1 |
9.5. САЙТ KTK-39
Файл: /etc/freeradius/3.0/sites-available/ktk-39
Штатный sites-available/default FreeRADIUS НЕ подходит для АСР Казна-39 — в нём нет вызовов perl в authorize/accounting/post-auth и нет Auth-Type Perl.
Генерируем свой сайт.
| Блок кода | ||
|---|---|---|
| ||
server default= per_socket_clients limit { # max_pps = 0 } } listen { type = auth ipaddr ipv6addr = :: # any. ::1 == *localhost port = 0 # interface = eth0 # clients = per_socket_clients limit { max_connections = 65536 lifetime = 0 idle_timeout = 60 } } listen { ipaddr = * # ipv6addr = :: port = 0 type = acct # interface = eth0 # clients = per_socket_clients limit { # max_pps = 0 } } listenauthorize { preprocess mschap files perl } type =authenticate auth{ Auth-Type PAP { ipv6addr = :: # any. ::1 == localhost pap } portAuth-Type =MSCHAP 0{ # interface = eth0mschap # clients = per_socket_clients } limitAuth-Type CHAP { chap max_connections = 65536 } eap lifetimeAuth-Type =Perl 0{ idle_timeout = 60perl } } listenpreacct { ipv6addr = ::preprocess } portaccounting = 0{ typeperl = acct} # interface = eth0post-auth { # clients = per_socket_clients perl limitPost-Auth-Type REJECT { # max_pps = 0 perl } } } |
Активация и отключение штатных сайтов:
| Блок кода | ||
|---|---|---|
| ||
rm authorize {-f /etc/freeradius/3.0/sites-enabled/acp-kazna-39 cp -p /etc/freeradius/3.0/sites-available/acp-kazna-39 /etc/freeradius/3.0/sites-enabled/acp-kazna-39 |
Штатный default не нужен
| Блок кода | ||
|---|---|---|
| ||
preprocess
mschap
files
perl
}
authenticate {
Auth-Type PAP {
pap
}
Auth-Type MSCHAP {
mschap
}
Auth-Type CHAP {
chap
}
eap
Auth-Type Perl {
perl
}
}
preacct {
preprocess
}
accounting {
perl
}
post-auth {
perl
Post-Auth-Type REJECT {
perl
}
}
} |
rm -f /etc/freeradius/3.0/sites-enabled/default |
inner-tunnel не нужен:
он слушает 127.0.0.1:18120 и при занятом порте FreeRADIUS падает с "Failed binding to auth address 127.0.0.1 port 18120"
| Блок кода | ||
|---|---|---|
| ||
rm -f /etc/freeradius/3.0/sites-enabled/inner-tunnel |
Перезапуск RADIUS при изменении списка NAS
FreeRADIUS читает список RADIUS-клиентов (серверов доступа, NAS) из таблицы nas ТОЛЬКО при старте (read_clients = yes). Убрать клиента «на лету» невозможно: команда radmin 'del client ipaddr' работает только для динамически созданных клиентов, а 'add client file' создаёт статического. Поэтому при добавлении, удалении NAS или изменении критичных полей (IP, секрет, disable) веб-интерфейс перезапускает FreeRADIUS автоматически (функция ACP::Base::radius_restart). Остальные параметры NAS (auth/acct-модуль, interim и т.п.) применяются «на лету» через hot-reload в rlm_perl.pl и рестарта не требуют.
Чтобы веб-интерфейс (пользователь www-data) мог перезапускать RADIUS, нужно sudoers-правило. Имя сервиса зависит от установки: radiusd (сборка из исходников) или freeradius (пакетный).
| Блок кода | ||
|---|---|---|
| ||
# Разрешить www-data перезапуск RADIUS без пароля
# (имя сервиса: radiusd для сборки из исходников, freeradius для пакетного)
cat > /etc/sudoers.d/acp_kazna_39_radius <<'EOF'
www-data ALL = NOPASSWD: /bin/systemctl restart radiusd
www-data ALL = NOPASSWD: /usr/bin/systemctl restart radiusd
EOF
chmod 0440 /etc/sudoers.d/acp_kazna_39_radius
visudo -cf /etc/sudoers.d/acp_kazna_39_radius |
Проверка, что www-data может перезапустить RADIUS:
| Блок кода | ||
|---|---|---|
| ||
su -s /bin/bash www-data -c 'sudo -n /bin/systemctl restart radiusd && /bin/systemctl is-active radiusd' |
9.6. ПРОВЕРКА И ЗАПУСК
| Блок кода | ||
|---|---|---|
| ||
rmfreeradius -f /etc/freeradius/3.0/sites-enabled/ktk-39XC systemctl enable freeradius cpsystemctl -prestart /etc/freeradius/3.0/sites-available/ktk-39 /etc/freeradius/3.0/sites-enabled/ktk-39 |
Штатный default не нужен
| Блок кода | ||
|---|---|---|
| ||
freeradius rmsystemctl is-factive /etc/freeradius/3.0/sites-enabled/default |
inner-tunnel не нужен:
freeradius |
Должно быть Configuration appears to be OK и active.
Проверка, что NAS читаются из БД:он слушает 127.0.0.1:18120 и при занятом порте FreeRADIUS падает с "Failed binding to auth address 127.0.0.1 port 18120"
| Блок кода | ||
|---|---|---|
| ||
rmjournalctl -fu /etc/freeradius/3.0/sites-enabled/inner-tunnel |
Перезапуск RADIUS при изменении списка NAS
FreeRADIUS читает список RADIUS-клиентов (серверов доступа, NAS) из таблицы nas ТОЛЬКО при старте (read_clients = yes). Убрать клиента «на лету» невозможно: команда radmin 'del client ipaddr' работает только для динамически созданных клиентов, а 'add client file' создаёт статического. Поэтому при добавлении, удалении NAS или изменении критичных полей (IP, секрет, disable) веб-интерфейс перезапускает FreeRADIUS автоматически (функция ACP::Base::radius_restart). Остальные параметры NAS (auth/acct-модуль, interim и т.п.) применяются «на лету» через hot-reload в rlm_perl.pl и рестарта не требуют.
Чтобы веб-интерфейс (пользователь www-data) мог перезапускать RADIUS, нужно sudoers-правило. Имя сервиса зависит от установки: radiusd (сборка из исходников) или freeradius (пакетный).
| Блок кода | ||
|---|---|---|
| ||
# Разрешить www-data перезапуск RADIUS без пароля
# (имя сервиса: radiusd для сборки из исходников, freeradius для пакетного)
cat > /etc/sudoers.d/acp_kazna_39_radius <<'EOF'
www-data ALL = NOPASSWD: /bin/systemctl restart radiusd
www-data ALL = NOPASSWD: /usr/bin/systemctl restart radiusd
EOF
chmod 0440 /etc/sudoers.d/acp_kazna_39_radius
visudo -cf /etc/sudoers.d/acp_kazna_39_radius |
Проверка, что www-data может перезапустить RADIUS:
| Блок кода | ||
|---|---|---|
| ||
su -s /bin/bash www-data -c 'sudo -n /bin/systemctl restart radiusd && /bin/systemctl is-active radiusd' |
9.6. ПРОВЕРКА И ЗАПУСК
| Блок кода | ||
|---|---|---|
| ||
freeradius -XC
systemctl enable freeradius
systemctl restart freeradius
systemctl is-active freeradius |
Должно быть Configuration appears to be OK и active.
Проверка, что NAS читаются из БД:
| Блок кода | ||
|---|---|---|
| ||
journalctl -u freeradius -n 50 --no-pager | grep 'to global clients list' |
Должны быть строки вида Adding client 198.18.1.5 (NAS_16) to global clients list.
10. ШАГ 8. LOGROTATE
freeradius -n 50 --no-pager | grep 'to global clients list' |
Должны быть строки вида Adding client 198.18.1.5 (NAS_16) to global clients list.
10. ШАГ 8. LOGROTATE
Файл: /etc/logrotate.d/acp_kazna_39_logs
| Блок кода | ||
|---|---|---|
| ||
# АСР Казна-39 — ротация логов
# Ежедневно, хранение 10 дней, сжатие
/usr/acp/var/log/Debug/*.log {
daily
rotate 10
maxage 10
missingok
notifempty
compress
delaycompress
dateext
dateformat -%Y%m%d
copytruncate
su root root
}
/usr/acp/var/log/SQL/*.log {
daily
rotate 10
maxage 10
missingok
notifempty
compress
delaycompress
dateext
dateformat -%Y%m%d
copytruncate
su root root
}
|
Файл: /etc/logrotate.d/acp_kazna_39_logs
| Блок кода | ||
|---|---|---|
| ||
# АСР Казна-39 — ротация логов # Ежедневно, хранение 10 дней, сжатие /usr/acp/var/log/Debugnas_logs/*.log { daily rotate 10 maxage 10 missingok notifempty compress delaycompress dateext dateformat -%Y%m%d copytruncate su root root } # ВАЖНО: файлы логов БЕЗ расширения .log — их не покрывает маска *.log /usr/acp/var/log/SQL/*.logsql_errors { daily rotate 10 maxage 10 missingok notifempty compress delaycompress dateext dateformat -%Y%m%d copytruncate su root root } /usr/acp/var/log/nas_logs/*.logipn/mikrotik_ipoe_data { daily rotate 10 maxage 10 missingok notifempty compress delaycompress dateext dateformat -%Y%m%d copytruncate su root root } # ВАЖНО:Логи файлыв логовкорне БЕЗ расширения var/log (event_asterisk.log, gps_tracker.log — их не покрывает маска *.log, hotspot.log, # websocket.log, websocket_internal.log) /usr/acp/var/log/SQL/sql_errors*.log { daily rotate 10 maxage 10 missingok notifempty compress delaycompress dateext dateformat -%Y%m%d copytruncate su root root } |
| Примечание |
|---|
| Каталог /usr/acp/var/log/Periodic ИСКЛЮЧЁН — там файлы с датой в имени, ротация им не нужна. Для них отдельный скрипт очистки. |
Скрипт очистки Periodic — файл /etc/cron.daily/acp_kazna_39_periodic_cleanup:
| Блок кода | ||
|---|---|---|
| ||
#!/bin/shipn/mikrotik_ipoe_data { daily # АСР rotate 10 maxage 10 missingok notifempty compress delaycompressКазна-39 — очистка старых логов Periodic (файлы с датой в имени) dateext dateformat -%Y%m%d copytruncatefind /usr/acp/var/log/Periodic -maxdepth 1 -type f ! -name ".*" -mtime +10 -delete chmod su root root } # Логи в корне var/log (event_asterisk.log, gps_tracker.log, hotspot.log, # websocket.log, websocket_internal.log) /usr/acp/var/log/*.log { daily rotate 10 maxage 10 missingok notifempty compress delaycompress dateext dateformat -%Y%m%d copytruncate su root root } |
| Примечание |
|---|
| Каталог /usr/acp/var/log/Periodic ИСКЛЮЧЁН — там файлы с датой в имени, ротация им не нужна. Для них отдельный скрипт очистки. |
Скрипт очистки Periodic — файл /etc/cron.daily/acp_kazna_39_periodic_cleanup:
| Блок кода | ||
|---|---|---|
| ||
#!/bin/sh
# АСР Казна-39 — очистка старых логов Periodic (файлы с датой в имени)
find /usr/acp/var/log/Periodic -maxdepth 1 -type f ! -name ".*" -mtime +10 -delete
chmod +x /etc/cron.daily/acp_kazna_39_periodic_cleanup |
11. ШАГ 9. BILLD (ДЕМОН) + CRON
+x /etc/cron.daily/acp_kazna_39_periodic_cleanup |
11. ШАГ 9. BILLD (ДЕМОН) + CRON
11.1. BILLD КАК SYSTEMD-СЕРВИС
billd — это ДЕМОН обработки событий биллинга. Он должен работать постоянно (флаг -d), а не запускаться по cron каждую минуту. systemd следит за процессом и перезапускает его при падении (Restart=always).
Файл: /etc/systemd/system/billd.service
| Блок кода | ||
|---|---|---|
| ||
[Unit]
Description="АСР Казна-39" - Accounting Control Daemon
After=network.target mysql.service mariadb.service
StartLimitIntervalSec=0
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/usr/acp
ExecStart=/usr/acp/libexec/billd -d
Restart=always
RestartSec=3
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=billd
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=off
ProtectHome=false
# Resource limits
LimitNOFILE=65536
TasksMax=10000
[Install]
WantedBy=multi-user.target |
Запуск
| Блок кода | ||
|---|---|---|
| ||
chmod 644 /etc/systemd/system/billd.service
systemctl daemon-reload
systemctl enable billd
systemctl restart billd
systemctl is-active billd |
11.2. ПЕРИОДИЧЕСКИЕ ЗАДАЧИ (CRONTAB)
Задачи пишем в ЛИЧНЫЙ crontab пользователя root (crontab -e), а не в /etc/cron.d/. Так их привычно редактировать.
| Блок кода | ||
|---|---|---|
| ||
crontab -e |
Добавьте:
| Блок кода | ||
|---|---|---|
| ||
# BEGIN ACP Kazna-39
# Ежедневные + ежемесячные задачи биллинга (в 00:10)
10 0 * * * /usr/acp/libexec/periodic daily NO_USERS_WARNINGS=1; /usr/acp/libexec/periodic monthly NO_USERS_WARNINGS=1
# Резервное копирование БД (в 01:02)
2 1 * * * /usr/acp/libexec/periodic backup
# END ACP Kazna-39 |
ВАЖНО про periodic. Набор команд соответствует боевому эталону:
- daily — списания (credit_operation, reduction_operation), отчёты, sheduler
- monthly — месячные операции, logrotate, backup
- backup — резервное копирование БД
| Примечание |
|---|
| billd в cron НЕ добавляем — он работает как systemd-сервис. |
11.3. TELEGRAM-ПУЛЛЕР (МОСТ TELEGRAM BOT API <-> CGI)
Telegram-боты биллинга работают БЕЗ webhook: отдельный сервис kazna-telegram-poll сам опрашивает Telegram (long-polling, метод getUpdates) и передаёт полученные апдейты в CGI-скрипты бота (telegram_bot.cgi, telegram_admin_bot.cgi). Это позволяет работать без публичного HTTPS-порта и внешнего доступа к серверу.
Файлы:
- Эталон пуллера: misc/telegram/telegram_poll_to_cgi.pl
- Инсталлер: misc/telegram/install.sh
- systemd-юнит: misc/telegram/kazna-telegram-poll.service
- Рабочая копия (устанавливается инсталлером): <PREFIX>/cgi-bin/telegram_poll_to_cgi.pl
Установка (идемпотентно, можно запускать повторно):
| Блок кода | ||
|---|---|---|
| ||
sudo bash misc/telegram/install.sh |
Инсталлер сам: определяет каталог установки, копирует пуллер в cgi-bin, ставит systemd-юнит kazna-telegram-poll (с подстановкой реального пути) и запускает сервис.
Проверка:
| Блок кода | ||
|---|---|---|
| ||
systemctl status kazna-telegram-poll
journalctl -u kazna-telegram-poll -n 30 |
ВАЖНО (как работает доставка апдейтов):
Пуллер отправляет апдейт как FastCGI-запрос в сокет FastCGI-пула (acp-fcgi-index, /run/acp-fcgi-index/index.sock) — точно так же, как это делает nginx. В пуле код биллинга уже скомпилирован в воркерах, поэтому НЕТ повторной компиляции биллинга на каждый апдейт (раньше пуллер запускал perl telegram_bot.cgi — это была заметная постоянная нагрузка на CPU). Если FastCGI-пул недоступен (нет сокета) — пуллер автоматически откатывается на прямой запуск perl, бот продолжает работать.
Требования:
- Модуль Perl JSON (libjson-perl) и IO::Socket::UNIX.
- Настроенный FastCGI-пул (acp-fcgi-index) — для работы без повторной компиляции.
- Токены ботов в config.pl: $conf{TELEGRAM_TOKEN} (бот абонентов) и $conf{TELEGRAM_ADMIN_TOKEN} (бот админов/монтажников).
- Параметр $conf{BILLING_URL} — используется для формирования HTTP_HOST при доставке.
Отключение (при необходимости):
| Блок кода | ||
|---|---|---|
| ||
systemctl disable --now kazna-telegram-poll |
11.1. BILLD КАК SYSTEMD-СЕРВИС
billd — это ДЕМОН обработки событий биллинга. Он должен работать постоянно (флаг -d), а не запускаться по cron каждую минуту. systemd следит за процессом и перезапускает его при падении (Restart=always).
Файл: /etc/systemd/system/billd.service
| Блок кода | ||
|---|---|---|
| ||
[Unit]
Description="АСР Казна-39" - Accounting Control Daemon
After=network.target mysql.service mariadb.service
StartLimitIntervalSec=0
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/usr/acp
ExecStart=/usr/acp/libexec/billd -d
Restart=always
RestartSec=3
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=billd
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=off
ProtectHome=false
# Resource limits
LimitNOFILE=65536
TasksMax=10000
[Install]
WantedBy=multi-user.target |
Запуск
| Блок кода | ||
|---|---|---|
| ||
chmod 644 /etc/systemd/system/billd.service
systemctl daemon-reload
systemctl enable billd
systemctl restart billd
systemctl is-active billd |
11.2. ПЕРИОДИЧЕСКИЕ ЗАДАЧИ (CRONTAB)
Задачи пишем в ЛИЧНЫЙ crontab пользователя root (crontab -e), а не в /etc/cron.d/. Так их привычно редактировать.
| Блок кода | ||
|---|---|---|
| ||
crontab -e |
Добавьте:
| Блок кода | ||
|---|---|---|
| ||
# BEGIN ACP Kazna-39
# Ежедневные + ежемесячные задачи биллинга (в 00:10)
10 0 * * * /usr/acp/libexec/periodic daily NO_USERS_WARNINGS=1; /usr/acp/libexec/periodic monthly NO_USERS_WARNINGS=1
# Резервное копирование БД (в 01:02)
2 1 * * * /usr/acp/libexec/periodic backup
# END ACP Kazna-39 |
ВАЖНО про periodic. Набор команд соответствует боевому эталону:
- daily — списания (credit_operation, reduction_operation), отчёты, sheduler
- monthly — месячные операции, logrotate, backup
- backup — резервное копирование БД
| Примечание |
|---|
| billd в cron НЕ добавляем — он работает как systemd-сервис. |
12. ШАГ 10. ДЕМО-ДАННЫЕ (ОПЦИОНАЛЬНО)
Загрузка демо-данных добавит тестовых абонентов, тарифы, компании в вашу БД.
Делайте это только на тестовом стенде.
| Блок кода | ||
|---|---|---|
| ||
mysql --force --default-character-set=utf8 -u acp -p'ВАШ_ПАРОЛЬ_БД' -h localhost acp < /usr/acp/db/demo_data.sql |
Флаг --force нужен, чтобы импорт не прерывался на некритичных ошибках (дубли, отсутствующие колонки).
13. ПРОВЕРКА УСТАНОВКИ
| Блок кода | ||
|---|---|---|
| ||
systemctl status nginx fcgiwrap.socket acp-fcgi-index acp-fcgi-admin mariadb billd freeradius
curl -sk -o /dev/null -w '%{http_code}\n' https://stat.example.ru/ |
Ожидается 200.
Проверка RADIUS:
| Блок кода | ||
|---|---|---|
| ||
freeradius -XC journalctl -u freeradius -n 30 --no-pager |
14. ЧАСТЫЕ ПРОБЛЕМЫ
| Симптом | Причина | Решение |
|---|---|---|
| No Auth-Type found: rejecting the user | Нет строки DEFAULT Auth-Type = Perl | Установить файл (см. п. 9.4) mods-config/files/authorize |
| Insecure dependency in require while running with -T | В mods-available/perl задан perl_flags = "-T" | Убрать perl_flags (см. п. 9.2) |
| failed to load config.pl | filename в perl указывает на симлинк | Указать прямой путь на rlm_perl.pl (см. п. 9.2) |
| Failed to add duplicate client NAS_1 | Штатный client localhost в clients.conf | Очистить clients.conf (см. п. 9.3) |
| Unknown column 'nasname' | Штатный client_query FreeRADIUS | Задать кастомный client_query (см. п. 9.1) |
| Failed parsing configuration item ca_file | Блок tls{} в mods-available/sql | Убрать блок tls{} (см. п. 9.1) |
| Failed binding to auth address 127.0.0.1 port 18120 | Активен sites-enabled/inner-tunnel | Удалить inner-tunnel (см. п. 9.5) |
| 502 Bad Gateway в браузере | Ошибка Perl в CGI | Смотреть /var/log/nginx/acp_error.log |
| Пароли NAS не расшифровываются | secretkey в config.pl не равен ключу в БД | Синхронизировать ключ (см. п. 6.2, 6.3, 7) |
| Письма не уходят, Paysys/billd тормозят | Нет MTA (/usr/sbin/sendmail) | Установить postfix + mailutils (см. п. 3.2) |
| Модуль падает: Can't open log file ... No such file | Не создан каталог/файл лога | Создать полный набор (см. п. 5.2, 5.3) |
| logrotate: stat of ... failed: No such file | Не создан файл лога | Создать пустые файлы логов (см. п. 5.3) |
| 502 Bad Gateway на paysys_check.cgi | Платёжный callback направлен в FastCGI-пул биллинга | Направить paysys_check.cgi на fcgiwrap отдельным location (см. п. 8.4) |
15. АВТОМАТИЧЕСКАЯ УСТАНОВКА
Всё вышеописанное делает инсталлер misc/install.sh:
| Блок кода | ||
|---|---|---|
| ||
cd /opt/ wget https://git.it39.su/releases/installer/-/raw/main/install.sh chmod +x install.sh ./installsh |
Инсталлер ИНТЕРАКТИВНЫЙ — аргументы командной строки не используются.
Меню:
- Параметры установки
- Каталог установки
- Git-репозиторий (адрес, ветка, доступ, ключ/логин/пароль)
- База данных (имя, пользователь, пароль, хост, пароль root)
- Веб-сервер (домен, порт, HTTPS)
- FreeRADIUS (вкл/выкл)
- Демо-данные (вкл/выкл, файл)
- Состав установки (пакеты, nginx, БД, клонирование, force, dry-run)
- Сбросить параметры к значениям по умолчанию
- Показать план установки
- Начать установку
- Выход
Инсталлер сам:
- генерирует пароль БД и secretkey
- подставляет secretkey в acp.sql при импорте и в config.pl
- определяет вариант FreeRADIUS (пакеты / исходники)
- настраивает nginx, logrotate, billd.service, crontab
16. ПЕРЕНОС УСТАНОВКИ В ДРУГОЙ КАТАЛОГ (Экспериментально)
Если нужно перенести систему (например, /usr/ACP -> /usr/acp), используйте misc/move_install.sh:
| Блок кода | ||
|---|---|---|
| ||
cd /usr/acp/misc ./move_install.sh |
Скрипт ИНТЕРАКТИВНЫЙ. Переносит установку вместе со всеми настройками:
- каталог установки
- $base_dir в config.pl
- конфиги nginx (sites-available / sites-enabled)
- systemd-сервис billd
- crontab root
- logrotate
- cron.daily
- FreeRADIUS (mods-available/sql, mods-enabled/sql, perl, mods-config/perl)
Перед переносом создаётся бэкап в /root/move_install_bak_<таймстамп>/.