From 7373d5520fa5911e88a0e966b2d6e3efff6ae3da Mon Sep 17 00:00:00 2001 From: Ruslan Piatrovich Date: Wed, 10 Jun 2026 17:06:22 +0300 Subject: [PATCH] Add cluster join functionality and Kaspersky agent configuration to PMG. Updated Makefile for cluster bootstrap command, enhanced README with new service details, and modified pillar states for KESL and KLN agent integration. Added verification and management for cluster-only services in Salt states. --- Makefile | 7 +- README.md | 35 +++++++++ pillar/pmg.sls | 33 +++++++++ pmg/cluster_join.sls | 30 ++++++++ pmg/custom_check.sls | 2 +- pmg/init.sls | 1 + pmg/pmg_baseline.sls | 4 + pmg/security_agents.sls | 139 +++++++++++++++++++++++++++++++++++ pmg/verify.sls | 4 + scripts/cluster-bootstrap.sh | 19 +++++ 10 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 pmg/cluster_join.sls create mode 100755 scripts/cluster-bootstrap.sh diff --git a/Makefile b/Makefile index 655b06f..130f529 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,10 @@ YAMLLINT ?= yamllint STATE_ROOT ?= . PILLAR_ROOT ?= ./pillar MINION_ID ?= p-mailgw-x2-01.lmru.tech +MASTER_IP ?= 10.80.38.152 +MASTER_HOST ?= p-mailgw-x1-01.lmru.tech -.PHONY: lint check apply verify update-check update +.PHONY: lint check apply verify update-check update cluster-bootstrap lint: >@if command -v $(SALT_LINT) >/dev/null 2>&1; then \ @@ -36,3 +38,6 @@ update-check: update: >sudo $(SALT_CALL) --local --id $(MINION_ID) --file-root=$(STATE_ROOT) --pillar-root=$(PILLAR_ROOT) state.apply pmg.update + +cluster-bootstrap: +>bash "./scripts/cluster-bootstrap.sh" "$(MINION_ID)" "$(MASTER_HOST)" "$(MASTER_IP)" diff --git a/README.md b/README.md index b770aa1..f0b253d 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ make check MINION_ID=p-mailgw-x1-01.lmru.tech - `pmg.node_exporter.*` - `pmg.smtp_monitoring.*` - `pmg.pmg_services` +- `pmg.pmg_cluster_only_services` +- `pmg.cluster_join.*` - `pmg.update_dist_upgrade` - `pmg.update_auto_reboot` - `pmg.update_reboot_delay_minutes` @@ -62,6 +64,8 @@ make check MINION_ID=p-mailgw-x1-01.lmru.tech - `pmg.manage_hosts_file` - `pmg.hosts_file_entries` - `pmg.wazuh.*` +- `pmg.kesl.*` +- `pmg.klnagent.*` - `pmg.qualys.*` `pmg.node_exporter.args` по умолчанию включает textfile collector: @@ -73,6 +77,27 @@ make check MINION_ID=p-mailgw-x1-01.lmru.tech - recent window counters (`pmg.smtp_monitoring.log_window_seconds`) по логам postfix: sent, received, deferred, bounced, rejected, tls failures, smtp connections. +`pmg.cluster_join` делает: +- precheck passwordless SSH с новой ноды на `pmg.cluster_join.master_ip`; +- non-interactive `pmgcm join` при заданном `pmg.cluster_join.password`. + +Важно: `pmgcm join` использует пароль PMG (`root@pam` на master) для получения ticket +(это не SSH-пароль). Без `pmg.cluster_join.password` state делает skip-уведомление. + +Рекомендуется передавать пароль через переменную окружения (без сохранения в `pillar/pmg.sls`): + +```bash +export PMG_CLUSTER_JOIN_PASSWORD='***' +PILLAR_B64="$(python3 -c 'import json, os, base64; print(base64.b64encode(json.dumps({\"pmg\": {\"cluster_join\": {\"password\": os.environ[\"PMG_CLUSTER_JOIN_PASSWORD\"]}}}).encode()).decode())')" +ssh root@p-mailgw-x1-02.lmru.tech "cd /root/mailgw-salt && salt-call --local --id p-mailgw-x1-02.lmru.tech --file-root=. --pillar-root=./pillar state.apply pmg.cluster_join pillar=\"\$(python3 -c \"import base64; print(base64.b64decode(\\\"${PILLAR_B64}\\\").decode())\")\"" +``` + +Для полностью автоматического bootstrap SSH-доверия (без паролей) с управляющей машины: + +```bash +make cluster-bootstrap MINION_ID=p-mailgw-x1-02.lmru.tech MASTER_HOST=p-mailgw-x1-01.lmru.tech MASTER_IP=10.80.38.152 +``` + ## Отдельное обновление PMG Перед обновлением: @@ -105,3 +130,13 @@ State `pmg.update` выполняет: (по умолчанию: `/var/spool/pmg`, `/var/lib/clamav`, `/tmp/.proxdump_*`) и делает `systemctl restart kesl` только при изменении этих настроек; - если `kesl/klnagent64` установлены — включает сервисы и `custom_check` в PMG, если нет — выводит уведомление "ставится через Kaspersky Administration Center"; сервисы включатся вторым прогоном после установки пакетов. + +`pmg.security_agents` дополнительно ставит/настраивает `kesl` и `klnagent64`: +- для `kesl`: + - скачивает `.deb` из `pmg.kesl.package_url`; + - формирует `autoinstall.ini` и запускает `/opt/kaspersky/kesl/bin/kesl-setup.pl --autoinstall=...`; + - поднимает сервис `kesl`; +- скачивает `.deb` из `pmg.klnagent.package_url`; +- формирует файл ответов `KLAUTOANSWERS` (минимум: `KLNAGENT_SERVER`, + `KLNAGENT_AUTOINSTALL`, `EULA_ACCEPTED`); +- устанавливает/переустанавливает пакет в тихом режиме и поднимает сервис `klnagent64`. diff --git a/pillar/pmg.sls b/pillar/pmg.sls index e8420ca..8433b38 100644 --- a/pillar/pmg.sls +++ b/pillar/pmg.sls @@ -36,6 +36,14 @@ pmg: - pmgdaemon - pmgmirror - pmgtunnel + pmg_cluster_only_services: + - pmgmirror + - pmgtunnel + cluster_join: + enabled: true + master_ip: 10.80.38.152 + fingerprint: "4C:63:ED:BB:01:F0:63:5B:92:AC:37:D8:52:D9:0A:CB:5B:78:EE:3A:36:83:86:29:C8:19:D2:EC:CE:AC:88:EC" + password: "" update_dist_upgrade: true update_auto_reboot: false update_reboot_delay_minutes: 1 @@ -83,6 +91,31 @@ pmg: manager: 10.80.120.205 group: test_auditd_unix + kesl: + enabled: true + package_url: https://products.s.kaspersky-labs.com/endpoints/keslinux10/12.4.0.1225/multilanguage-INT-12.4.0.1225/167a2f9c5bdb4e869f4603fbc3a829c1/kesl_12.4.0-1225_amd64.deb + package_path: /opt/kesl_12.4.0-1225_amd64.deb + autoinstall_path: /etc/opt/kaspersky/kesl/autoinstall.ini + ksvla_mode: "No" + endpoint_agent_mode: "No" + eula_agreed: "Yes" + privacy_policy_agreed: "Yes" + use_ksn: "No" + group_clean: "No" + locale: "" + install_license: "" + updater_source: SCServer + update_execute: "No" + + klnagent: + enabled: true + package_url: https://products.s.kaspersky-labs.com/administrationkit/ksc10/16.2.0.1023/russian-INT-29664057-ru/2129c97dba254c9fb8a4161f29a2241c/klnagent64_16.2.0-1023_amd64.deb + package_path: /opt/klnagent64_16.2.0-1023_amd64.deb + administration_server: 10.80.38.76 + autoanswers_path: /etc/opt/kaspersky/klnagent64/autoanswers.conf + autoinstall: true + eula_accepted: true + qualys: enabled: true package_url: https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb diff --git a/pmg/cluster_join.sls b/pmg/cluster_join.sls new file mode 100644 index 0000000..5a5597a --- /dev/null +++ b/pmg/cluster_join.sls @@ -0,0 +1,30 @@ +{% set cluster_join_enabled = salt['pillar.get']('pmg:cluster_join:enabled', False) %} +{% set cluster_join_master_ip = salt['pillar.get']('pmg:cluster_join:master_ip', '') %} +{% set cluster_join_fingerprint = salt['pillar.get']('pmg:cluster_join:fingerprint', '') %} +{% set cluster_join_password = salt['pillar.get']('pmg:cluster_join:password', '') %} + +{% if cluster_join_enabled and cluster_join_master_ip and cluster_join_fingerprint %} +pmg-cluster-join-passwordless-ssh-check: + cmd.run: + - name: ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@{{ cluster_join_master_ip }} "exit 0" + - timeout: 20 + +{% if cluster_join_password %} +pmg-cluster-join: + cmd.run: + - name: pmgcm join {{ cluster_join_master_ip }} --fingerprint {{ cluster_join_fingerprint }} + - stdin: "{{ cluster_join_password }}" + - timeout: 60 + - require: + - cmd: pmg-cluster-join-passwordless-ssh-check + - unless: "pmgcm status 2>/dev/null | awk 'BEGIN { ok=1 } /no cluster defined/ { ok=0 } END { exit(ok ? 0 : 1) }'" +{% else %} +pmg-cluster-join-skipped-password: + test.show_notification: + - text: "Cluster join skipped: set pmg.cluster_join.password (PMG root@pam password on master) for non-interactive join." +{% endif %} +{% elif cluster_join_enabled %} +pmg-cluster-join-skipped: + test.show_notification: + - text: "Cluster join skipped: set pmg.cluster_join.master_ip and pmg.cluster_join.fingerprint in pillar." +{% endif %} diff --git a/pmg/custom_check.sls b/pmg/custom_check.sls index e6a62a4..1f60a30 100644 --- a/pmg/custom_check.sls +++ b/pmg/custom_check.sls @@ -104,7 +104,7 @@ pmg-custom-check-kesl-excluded-scope-{{ loop.index0 }}: ExcludedFromScanScope.item_{{ "%04d" | format(loop.index0) }}.AreaMask.item_0000='*' - unless: >- kesl-control --get-settings File_Threat_Protection 2>/dev/null - | grep -Fq "ExcludedFromScanScope.item_{{ "%04d" | format(loop.index0) }}.Path={{ excluded_path }}" + | grep -Fq "Path={{ excluded_path }}" - require: - test: pmg-custom-check-kaspersky-ready {% for service_name in custom_check_required_services %} diff --git a/pmg/init.sls b/pmg/init.sls index 4b16f86..4190f9c 100644 --- a/pmg/init.sls +++ b/pmg/init.sls @@ -5,6 +5,7 @@ include: - pmg.ssh_hardening - pmg.custom_check - pmg.security_agents + - pmg.cluster_join - pmg.auto_updates - pmg.audit_logging - pmg.pmg_baseline diff --git a/pmg/pmg_baseline.sls b/pmg/pmg_baseline.sls index 8c90ad1..eecfe6d 100644 --- a/pmg/pmg_baseline.sls +++ b/pmg/pmg_baseline.sls @@ -1,4 +1,5 @@ {% set pmg_services = salt['pillar.get']('pmg:pmg_services', []) %} +{% set pmg_cluster_only_services = salt['pillar.get']('pmg:pmg_cluster_only_services', ['pmgmirror', 'pmgtunnel']) %} {% set manage_hosts_file = salt['pillar.get']('pmg:manage_hosts_file', False) %} {% set hosts_file_entries = salt['pillar.get']('pmg:hosts_file_entries', []) %} @@ -7,6 +8,9 @@ pmg-service-{{ svc }}: service.running: - name: {{ svc }} - enable: true +{% if svc in pmg_cluster_only_services %} + - onlyif: "pmgcm status 2>/dev/null | awk 'BEGIN { ok=1 } /no cluster defined/ { ok=0 } END { exit(ok ? 0 : 1) }'" +{% endif %} {% endfor %} pmg-cluster-status: diff --git a/pmg/security_agents.sls b/pmg/security_agents.sls index 15e4409..a1ea459 100644 --- a/pmg/security_agents.sls +++ b/pmg/security_agents.sls @@ -6,6 +6,32 @@ {% set wazuh_pkg_url = 'https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/' ~ wazuh_pkg_name %} {% set wazuh_pkg_path = '/var/tmp/' ~ wazuh_pkg_name %} +{% set kesl_enabled = salt['pillar.get']('pmg:kesl:enabled', True) %} +{% set kesl_package_url = salt['pillar.get']('pmg:kesl:package_url', 'https://products.s.kaspersky-labs.com/endpoints/keslinux10/12.4.0.1225/multilanguage-INT-12.4.0.1225/167a2f9c5bdb4e869f4603fbc3a829c1/kesl_12.4.0-1225_amd64.deb') %} +{% set kesl_package_path = salt['pillar.get']('pmg:kesl:package_path', '/opt/kesl_12.4.0-1225_amd64.deb') %} +{% set kesl_autoinstall_path = salt['pillar.get']('pmg:kesl:autoinstall_path', '/etc/opt/kaspersky/kesl/autoinstall.ini') %} +{% set kesl_eula_agreed = salt['pillar.get']('pmg:kesl:eula_agreed', 'Yes') %} +{% set kesl_privacy_policy_agreed = salt['pillar.get']('pmg:kesl:privacy_policy_agreed', 'Yes') %} +{% set kesl_use_ksn = salt['pillar.get']('pmg:kesl:use_ksn', 'No') %} +{% set kesl_group_clean = salt['pillar.get']('pmg:kesl:group_clean', 'No') %} +{% set kesl_locale = salt['pillar.get']('pmg:kesl:locale', '') %} +{% set kesl_updater_source = salt['pillar.get']('pmg:kesl:updater_source', 'SCServer') %} +{% set kesl_install_license = salt['pillar.get']('pmg:kesl:install_license', '') %} +{% set kesl_update_execute = salt['pillar.get']('pmg:kesl:update_execute', 'No') %} +{% set kesl_ksvla_mode = salt['pillar.get']('pmg:kesl:ksvla_mode', 'No') %} +{% set kesl_endpoint_agent_mode = salt['pillar.get']('pmg:kesl:endpoint_agent_mode', 'No') %} +{% set kesl_setup_bin = '/opt/kaspersky/kesl/bin/kesl-setup.pl' %} +{% set kesl_setup_marker = '/var/lib/salt/pmg-kesl-autoinstall.done' %} + +{% set klnagent_enabled = salt['pillar.get']('pmg:klnagent:enabled', True) %} +{% set klnagent_package_url = salt['pillar.get']('pmg:klnagent:package_url', 'https://products.s.kaspersky-labs.com/administrationkit/ksc10/16.2.0.1023/russian-INT-29664057-ru/2129c97dba254c9fb8a4161f29a2241c/klnagent64_16.2.0-1023_amd64.deb') %} +{% set klnagent_package_path = salt['pillar.get']('pmg:klnagent:package_path', '/opt/klnagent64_16.2.0-1023_amd64.deb') %} +{% set klnagent_server = salt['pillar.get']('pmg:klnagent:administration_server', '10.80.38.76') %} +{% set klnagent_autoanswers_path = salt['pillar.get']('pmg:klnagent:autoanswers_path', '/etc/opt/kaspersky/klnagent64/autoanswers.conf') %} +{% set klnagent_autoinstall = salt['pillar.get']('pmg:klnagent:autoinstall', True) %} +{% set klnagent_eula_accepted = salt['pillar.get']('pmg:klnagent:eula_accepted', True) %} +{% set klnagent_config_marker = '/var/lib/salt/pmg-klnagent-autoinstall.done' %} + {% set qualys_enabled = salt['pillar.get']('pmg:qualys:enabled', True) %} {% set qualys_package_url = salt['pillar.get']('pmg:qualys:package_url', 'https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb') %} {% set qualys_package_path = salt['pillar.get']('pmg:qualys:package_path', '/opt/QualysCloudAgent.deb') %} @@ -13,6 +39,119 @@ {% set qualys_customer_id = salt['pillar.get']('pmg:qualys:customer_id', '') %} {% set qualys_server_uri = salt['pillar.get']('pmg:qualys:server_uri', 'https://qagpublic.qg1.apps.qualys.eu/CloudAgent/') %} +{% if kesl_enabled and kesl_package_path %} +pmg-kesl-download: + cmd.run: + - name: wget -qO {{ kesl_package_path }} {{ kesl_package_url }} + - unless: test -f {{ kesl_package_path }} + +pmg-kesl-autoinstall-config: + file.managed: + - name: {{ kesl_autoinstall_path }} + - user: root + - group: root + - mode: '0600' + - makedirs: true + - contents: + - "KSVLA_MODE={{ kesl_ksvla_mode }}" + - "ENDPOINT_AGENT_MODE={{ kesl_endpoint_agent_mode }}" + - "EULA_AGREED={{ kesl_eula_agreed }}" + - "PRIVACY_POLICY_AGREED={{ kesl_privacy_policy_agreed }}" + - "USE_KSN={{ kesl_use_ksn }}" + - "GROUP_CLEAN={{ kesl_group_clean }}" +{% if kesl_locale %} + - "LOCALE={{ kesl_locale }}" +{% endif %} +{% if kesl_install_license %} + - "INSTALL_LICENSE={{ kesl_install_license }}" +{% endif %} + - "UPDATER_SOURCE={{ kesl_updater_source }}" + - "UPDATE_EXECUTE={{ kesl_update_execute }}" + - require: + - cmd: pmg-kesl-install + +pmg-kesl-install: + cmd.run: + - name: apt-get install -y {{ kesl_package_path }} + - onlyif: test -f {{ kesl_package_path }} + - unless: dpkg-query -W -f='${Status}' kesl 2>/dev/null | grep -q 'install ok installed' + - require: + - cmd: pmg-kesl-download + +pmg-kesl-setup: + cmd.run: + - name: mkdir -p /var/lib/salt && {{ kesl_setup_bin }} --autoinstall={{ kesl_autoinstall_path }} && touch {{ kesl_setup_marker }} + - onlyif: test -x {{ kesl_setup_bin }} + - unless: test -f {{ kesl_setup_marker }} + - require: + - cmd: pmg-kesl-install + - file: pmg-kesl-autoinstall-config + +pmg-kesl-service: + service.running: + - name: kesl + - enable: true + - require: + - cmd: pmg-kesl-install + - cmd: pmg-kesl-setup +{% elif kesl_enabled %} +pmg-kesl-skipped: + test.show_notification: + - text: "Kaspersky Endpoint Security skipped: set package_url + package_path in pillar." +{% endif %} + +{% if klnagent_enabled and klnagent_package_path and klnagent_server %} +pmg-klnagent-download: + cmd.run: + - name: wget -qO {{ klnagent_package_path }} {{ klnagent_package_url }} + - unless: test -f {{ klnagent_package_path }} + +pmg-klnagent-autoanswers: + file.managed: + - name: {{ klnagent_autoanswers_path }} + - user: root + - group: root + - mode: '0600' + - makedirs: true + - contents: + - "KLNAGENT_SERVER={{ klnagent_server }}" + - "KLNAGENT_AUTOINSTALL={{ '1' if klnagent_autoinstall else '0' }}" + - "EULA_ACCEPTED={{ '1' if klnagent_eula_accepted else '0' }}" + +pmg-klnagent-install: + cmd.run: + - name: DEBIAN_FRONTEND=noninteractive KLAUTOANSWERS={{ klnagent_autoanswers_path }} apt-get install -y {{ klnagent_package_path }} + - onlyif: test -f {{ klnagent_package_path }} + - unless: dpkg-query -W -f='${Status}' klnagent64 2>/dev/null | grep -q 'install ok installed' + - require: + - cmd: pmg-klnagent-download + - file: pmg-klnagent-autoanswers + +pmg-klnagent-autoinstall-configure: + cmd.run: + - name: >- + mkdir -p /var/lib/salt && + DEBIAN_FRONTEND=noninteractive KLAUTOANSWERS={{ klnagent_autoanswers_path }} apt-get install -y --reinstall {{ klnagent_package_path }} && + touch {{ klnagent_config_marker }} + - onlyif: dpkg-query -W -f='${Status}' klnagent64 2>/dev/null | grep -q 'install ok installed' + - unless: test -f {{ klnagent_config_marker }} + - require: + - cmd: pmg-klnagent-install + - file: pmg-klnagent-autoanswers + +pmg-klnagent-service: + service.running: + - name: klnagent64 + - enable: true + - require: + - cmd: pmg-klnagent-install + - cmd: pmg-klnagent-autoinstall-configure +{% elif klnagent_enabled %} +pmg-klnagent-skipped: + test.show_notification: + - text: "Kaspersky Administration Agent skipped: set package_url + package_path + administration_server in pillar." +{% endif %} + {% if wazuh_enabled %} pmg-wazuh-download: cmd.run: diff --git a/pmg/verify.sls b/pmg/verify.sls index abc7ab9..f50c60d 100644 --- a/pmg/verify.sls +++ b/pmg/verify.sls @@ -1,9 +1,13 @@ {% set pmg_services = salt['pillar.get']('pmg:pmg_services', []) %} +{% set pmg_cluster_only_services = salt['pillar.get']('pmg:pmg_cluster_only_services', ['pmgmirror', 'pmgtunnel']) %} {% for svc in pmg_services %} pmg-verify-service-{{ svc }}: cmd.run: - name: systemctl is-active --quiet {{ svc }} +{% if svc in pmg_cluster_only_services %} + - onlyif: "pmgcm status 2>/dev/null | awk 'BEGIN { ok=1 } /no cluster defined/ { ok=0 } END { exit(ok ? 0 : 1) }'" +{% endif %} {% endfor %} pmg-verify-cluster: diff --git a/scripts/cluster-bootstrap.sh b/scripts/cluster-bootstrap.sh new file mode 100755 index 0000000..2064927 --- /dev/null +++ b/scripts/cluster-bootstrap.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 3 ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +target_host="$1" +master_host="$2" +master_ip="$3" + +ssh "root@${target_host}" "mkdir -p /root/.ssh && chmod 700 /root/.ssh && [ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519 >/dev/null" + +pubkey_b64="$(ssh "root@${target_host}" "python3 -c 'import base64, pathlib; print(base64.b64encode(pathlib.Path(\"/root/.ssh/id_ed25519.pub\").read_bytes()).decode())'")" + +ssh "root@${master_host}" "python3 -c 'import base64, pathlib; pub = base64.b64decode(\"${pubkey_b64}\").decode().strip(); ssh_dir = pathlib.Path(\"/root/.ssh\"); ssh_dir.mkdir(mode=0o700, parents=True, exist_ok=True); auth = ssh_dir / \"authorized_keys\"; lines = auth.read_text().splitlines() if auth.exists() else []; auth.write_text((\"\\n\".join(lines + [pub]) + \"\\n\") if pub not in lines else ((\"\\n\".join(lines) + \"\\n\") if lines else \"\")); auth.chmod(0o600)'" + +ssh "root@${target_host}" "ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@${master_ip} 'exit 0'"