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'"