РУЧНАЯ УСТАНОВКА АСР КАЗНА-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 \ 
      postfix mailutils \
      perl libdbd-mysql-perl libdbi-perl \
      libnet-snmp-perl libsnmp-perl librrds-perl rrdtool \
      libcgi-pm-perl libcgi-fast-perl \
      libdate-manip-perl libdate-calc-perl \
      libconfig-inifiles-perl libclass-makemethods-perl \
      libdigest-hmac-perl libdigest-sha-perl \
      libnet-ip-perl libnet-dns-perl \
      libio-socket-ssl-perl libnet-ssleay-perl \
      libjson-perl libjson-xs-perl \
      libxml-simple-perl libxml-libxml-perl \
      libtext-iconv-perl libexpat1 ssl-cert \
      build-essential gcc make libperl-dev libtalloc-dev \
      arping curl wget mc nano htop net-tools \
      git openssl ca-certificates logrotate cron \
      libarchive-zip-perl libmime-base64-perl \
      libemail-valid-perl libmailtools-perl \
      libfile-tail-perl libproc-processtable-perl \
      libsocket6-perl libio-multiplex-perl \
      libnet-server-perl libnet-telnet-perl \
      libterm-readkey-perl libterm-readpassword-perl \
      libtimedate-perl liburi-perl libwww-perl libhtml-parser-perl \
      liblwp-protocol-https-perl \
      libcrypt-rc4-perl libcrypt-des-perl \
      libconvert-binhex-perl libmime-tools-perl \
      libauthen-sasl-perl libnet-smtp-ssl-perl \
      libmath-round-perl libyaml-perl libyaml-libyaml-perl \
      libdb-file-lock-perl libfile-copy-recursive-perl \
      libfile-slurp-perl libfile-which-perl \
      libipc-run-perl libipc-sharelite-perl \
      libparallel-forkmanager-perl libsnmp-session-perl \
      libcrypt-openssl-rsa-perl libcrypt-openssl-random-perl \
      libcrypt-openssl-bignum-perl libnetaddr-ip-perl \
      libtext-csv-perl libtext-csv-xs-perl \
      libspreadsheet-writeexcel-perl libexcel-writer-xlsx-perl \
      libpdf-api2-perl libimage-size-perl libmime-lite-perl \
      libnet-cidr-perl libnet-cidr-lite-perl \
      libsys-syslog-perl libunix-syslog-perl \
      libnet-ssh2-perl libexpect-perl libio-pty-perl libio-stty-perl \
      libterm-readline-gnu-perl libdata-dump-perl libhash-merge-perl \
      libclone-perl libparams-validate-perl libmodule-build-perl \
      liblocal-lib-perl libversion-perl libtry-tiny-perl \
      libsub-exporter-perl libsub-install-perl libsub-name-perl \
      libsub-quote-perl librole-tiny-perl libmoo-perl \
      libnamespace-clean-perl libnamespace-autoclean-perl \
      libstrictures-perl libbareword-filehandles-perl \
      libindirect-perl libmultidimensional-perl libimport-into-perl \
      libmodule-runtime-perl libmodule-implementation-perl \
      libpackage-stash-perl libpackage-stash-xs-perl \
      libclass-method-modifiers-perl libclass-xsaccessor-perl \
      libdevel-globaldestruction-perl libdevel-caller-perl \
      libdevel-lexalias-perl libdevel-stacktrace-perl \
      libvariable-magic-perl libb-hooks-op-check-perl \
      libb-hooks-endofscope-perl libparams-classify-perl \
      libparams-util-perl libparams-check-perl libdata-optlist-perl \
      libdata-section-perl libdata-validate-ip-perl \
      libdata-validate-domain-perl libdata-validate-email-perl \
      libdata-validate-uri-perl libdatetime-perl \
      libdatetime-timezone-perl libdatetime-locale-perl \
      libdatetime-format-strptime-perl libdatetime-format-dateparse-perl \
      libdatetime-format-iso8601-perl libdatetime-format-http-perl \
      libdatetime-format-mail-perl libdatetime-format-mysql-perl \
      libdatetime-format-sqlite-perl libdatetime-format-builder-perl \
      libdatetime-event-cron-perl libdatetime-set-perl \
      libdatetime-event-recurrence-perl libspecio-perl \
      liblog-dispatch-perl liblog-log4perl-perl liblog-any-perl \
      liblog-any-adapter-log4perl-perl liblog-contextual-perl \
      liblog-dispatchouli-perl liblog-report-perl liblog-report-optional-perl \
      libstring-rewriteprefix-perl libstring-formatter-perl \
      libstring-escape-perl libstring-print-perl libstring-shellquote-perl \
      libstring-random-perl libstring-crc32-perl libstring-diff-perl \
      libstring-truncate-perl libstring-util-perl libstring-flogger-perl \
      libstring-tagged-perl libstring-tagged-terminal-perl \
      libstring-trim-perl libstring-trim-more-perl libstring-interpolate-perl \
      libstring-license-perl libstring-copyright-perl libstring-format-perl \
      libstring-errf-perl libio-socket-inet6-perl libio-socket-ip-perl \
      libio-socket-multicast-perl libio-socket-portstate-perl \
      libio-socket-socks-perl libio-socket-timeout-perl libio-string-perl \
      libio-stringy-perl libio-tiecombine-perl libio-compress-perl \
      libio-compress-lzma-perl libio-interactive-perl libio-lockedfile-perl \
      libio-prompt-perl libio-prompt-tiny-perl libio-sessiondata-perl \
      libio-tee-perl libio-tty-perl libio-async-perl \
      libio-async-loop-epoll-perl libio-async-loop-glib-perl \
      libio-async-ssl-perl libio-capture-perl libio-captureoutput-perl \
      libio-compress-brotli-perl libio-digest-perl libio-dirent-perl \
      libio-event-perl libio-fdpass-perl libio-file-withpath-perl \
      libio-handle-util-perl libio-html-perl libio-lcdproc-perl \
      libio-pager-perl libio-pipely-perl libio-prompter-perl \
      libipc-run3-perl libipc-signal-perl libipc-system-simple-perl \
      libipc-run-safehandles-perl libipc-sharedcache-perl \
      libipc-shareable-perl libipc-filter-perl libipc-pubsub-perl \
      libsocket-perl libfile-rsync-perl libtime-hires-perl \
      libgd-perl libauthen-captcha-perl libimager-qrcode-perl \
      libdigest-md4-perl libcrypt-openssl-x509-perl \
      libtext-autoformat-perl libdate-simple-perl libdate-range-perl \
      libauthen-radius-perl libjson-rpc-perl libmail-pop3client-perl


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. ПУСТЫЕ ФАЙЛЫ ЛОГОВ — ОБЯЗАТЕЛЬНО

Создаём пустые файлы заранее, чтобы:

  1. модуль мог писать лог сразу после установки
  2. logrotate не ругался на отсутствующий файл
  3. права были выставлены до первого запуска


    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 не сможет авторизовать коммутаторы.

6.4. ИНДЕКСЫ (ЕСЛИ ЕСТЬ)


    [ -f /usr/acp/db/patches/indexes.sql ] && \

      mysql --default-character-set=utf8 -u acp -p'ВАШ_ПАРОЛЬ_БД' -h localhost acp < /usr/acp/db/patches/indexes.sql


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.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/ktk-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% — путь к ключу

Если ставите вручную — скопируйте шаблон и замените плейсхолдеры:

    cp /usr/acp/misc/nginx/ktk-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';";
        }

        # Обработка 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 "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 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
    } 


Два критичных момента:

  1. filename указывает ПРЯМО на rlm_perl.pl, без симлинка. rlm_perl.pl берёт config.pl из своего каталога через FindBin ($Bin). Если подсунуть симлинк — $Bin будет указывать на каталог симлинка, и config.pl не найдётся.
  2. 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. САЙТ 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 {
    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/ktk-39
    cp -p /etc/freeradius/3.0/sites-available/ktk-39 /etc/freeradius/3.0/sites-enabled/ktk-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

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-сервис.


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 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)




15. АВТОМАТИЧЕСКАЯ УСТАНОВКА

Всё вышеописанное делает инсталлер misc/install.sh:


    cd /usr/acp/misc

    ./install.sh


Инсталлер ИНТЕРАКТИВНЫЙ — аргументы командной строки не используются.

Меню:

  1. Параметры установки
    1. Каталог установки
    2. Git-репозиторий (адрес, ветка, доступ, ключ/логин/пароль)
    3. База данных (имя, пользователь, пароль, хост, пароль root)
    4. Веб-сервер (домен, порт, HTTPS)
    5. FreeRADIUS (вкл/выкл)
    6. Демо-данные (вкл/выкл, файл)
    7. Состав установки (пакеты, nginx, БД, клонирование, force, dry-run)
    8. Сбросить параметры к значениям по умолчанию
  2. Показать план установки
  3. Начать установку
  4. Выход


Инсталлер сам:

  • генерирует пароль БД и 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_<таймстамп>/.