РУЧНАЯ УСТАНОВКА АСР КАЗНА-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 и все модули биллинга, затем форкает 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 |
5) Число одновременных воркеров задаётся в юните переменной ACP_FCGI_MAX_WORKERS (по умолчанию 100, совпадает с числом воркеров fcgiwrap). Чтобы изменить — отредактируйте юнит:
| Блок кода | ||
|---|---|---|
| ||
systemctl edit acp-fcgi-index |
и добавьте:
| Блок кода | ||
|---|---|---|
| ||
[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. |
Результат: прямой 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/acp-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/acp-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://*.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 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.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';";
}
# Платёжные 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';";
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/;
}
# Админские 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 -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 {
database = "mysql"
driver = "rlm_sql_${database}"
server = "localhost"
#port = 3306
login = "acp"
password = "ВАШ_ПАРОЛЬ_БД"
radius_db = "acp"
secretkey = "ВАШ_SECRET_KEY"
acct_table1 = "radacct"
acct_table2 = "radacct"
postauth_table = "radpostauth"
authcheck_table = "radcheck"
authreply_table = "radreply"
groupcheck_table = "radgroupcheck"
groupreply_table = "radgroupreply"
usergroup_table = "radusergroup"
delete_stale_sessions = yes
sqltrace = no
sqltracefile = ${logdir}/sqltrace.sql
num_sql_socks = 50
connect_failure_retry_delay = 60
lifetime = 0
max_queries = 0
read_clients = yes
nas_table = "nas"
#$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"
} |
Ключевые моменты:
- 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 {
filename = /usr/acp/libexec/rlm_perl.pl
} |
Два критичных момента:
- 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 в 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 User-Password == "cisco", Auth-Type = Accept # 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 == "ISG", Auth-Type = Accept # Cisco-Service-Info = "NPREPAID_INTERNET", # Cisco-Control-Info += "QV1000000", # Service-Type = Dialout-Framed-User, # Cisco-Account-Info += "QU;1024000;256000;D;1024000;256000", # Cisco-Account-Info += "APREPAID_INTERNET", # Cisco-Account-Info += "NPREPAID_INTERNET", # Idle-Timeout = 120, # Cisco-Account-Info += "ABILLING_ACCESS" DEFAULT Auth-Type = Perl Fall-Through = 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
# 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
}
}
listen {
type = auth
ipv6addr = :: # any. ::1 == localhost
port = 0
# interface = eth0
# clients = per_socket_clients
limit {
max_connections = 65536
lifetime = 0
idle_timeout = 60
}
}
listen {
ipv6addr = ::
port = 0
type = acct
# interface = eth0
# clients = per_socket_clients
limit {
# max_pps = 0
}
}
authorize {
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/acp-kazna-39 cp -p /etc/freeradius/3.0/sites-available/acp-kazna-39 /etc/freeradius/3.0/sites-enabled/acp-kazna-39 |
Штатный default не нужен
| Блок кода | ||
|---|---|---|
| ||
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. ПРОВЕРКА И ЗАПУСК
| Блок кода | ||
|---|---|---|
| ||
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
Файл: /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
}
/usr/acp/var/log/nas_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/sql_errors {
daily
rotate 10
maxage 10
missingok
notifempty
compress
delaycompress
dateext
dateformat -%Y%m%d
copytruncate
su root root
}
/usr/acp/var/log/ipn/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, 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
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 |
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_<таймстамп>/.