Compare commits

..
8 Commits
Author SHA1 Message Date
Ruslan PetrovichandCursor 1ec315d96c Align Postfix identity with system hostname on PMG nodes.
Add an idempotent baseline step that enforces myhostname/mydomain from hostname -f and reloads postfix only when drift is detected.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 21:31:53 +03:00
Ruslan PetrovichandCursor 664a544aa9 Fix PMG custom-check log ownership to avoid permission denied errors.
Updated the custom_check state to manage /var/log/mail_av and checked file with pmg-smtp-filter ownership and configurable modes, and documented new pillar settings for log owner/group/modes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 21:26:13 +03:00
Ruslan Piatrovich 7373d5520f 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. 2026-06-10 17:06:22 +03:00
Ruslan Piatrovich 7230484b44 Add additional PMG mail gateway hosts and enhance SMTP metrics script with state management and new metrics. Updated metrics output to include queue drain rate and total SMTP connections. 2026-06-09 14:01:00 +03:00
Ruslan Piatrovich 86dd59e462 Enhance Kaspersky integration in PMG by adding configurable excluded paths for KESL scanning. Updated README to reflect new functionality and modified Salt states to implement the changes, including a restart command for KESL when exclusions are modified. 2026-06-08 12:48:15 +03:00
Ruslan Piatrovich c492399300 Add configuration for Prometheus node exporter and SMTP monitoring in PMG. Updated README and added verification checks for new metrics. 2026-06-05 16:33:57 +03:00
Ruslan PiatrovichandCursor 85cecf78f6 Add Salt baseline states for Proxmox Mail Gateway.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:27:44 +03:00
Ruslan Piatrovich 35d81332fa first commit 2026-06-01 15:35:45 +03:00
32 changed files with 3239 additions and 1 deletions
+13
View File
@@ -0,0 +1,13 @@
# Python / virtualenv
.venv/
venv/
__pycache__/
*.pyc
# Salt runtime artifacts
.salt/
# IDE / OS files
.idea/
.vscode/
.DS_Store
+16
View File
@@ -0,0 +1,16 @@
extends: default
ignore: |
.git/
.venv/
venv/
rules:
comments-indentation: disable
document-start:
present: false
line-length:
max: 120
level: warning
truthy:
check-keys: false
+43
View File
@@ -0,0 +1,43 @@
SHELL := /usr/bin/env bash
.RECIPEPREFIX := >
SALT_CALL ?= salt-call
SALT_LINT ?= salt-lint
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 cluster-bootstrap
lint:
>@if command -v $(SALT_LINT) >/dev/null 2>&1; then \
> $(SALT_LINT) pmg/*.sls pillar/*.sls top.sls; \
>else \
> echo "salt-lint не найден, пропускаю"; \
>fi
>@if command -v $(YAMLLINT) >/dev/null 2>&1; then \
> $(YAMLLINT) .; \
>else \
> echo "yamllint не найден, пропускаю"; \
>fi
check:
>sudo $(SALT_CALL) --local --id $(MINION_ID) --file-root=$(STATE_ROOT) --pillar-root=$(PILLAR_ROOT) state.apply pmg.init test=True
apply:
>sudo $(SALT_CALL) --local --id $(MINION_ID) --file-root=$(STATE_ROOT) --pillar-root=$(PILLAR_ROOT) state.apply pmg.init
verify:
>sudo $(SALT_CALL) --local --id $(MINION_ID) --file-root=$(STATE_ROOT) --pillar-root=$(PILLAR_ROOT) state.apply pmg.verify
update-check:
>sudo $(SALT_CALL) --local --id $(MINION_ID) --file-root=$(STATE_ROOT) --pillar-root=$(PILLAR_ROOT) state.apply pmg.update test=True
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)"
+142 -1
View File
@@ -1,2 +1,143 @@
# mailgw-salt
# Salt baseline для PMG
Репозиторий содержит автономный набор Salt states для базовой эксплуатации и hardening Proxmox Mail Gateway.
## Что внутри
- `top.sls` — привязка состояний к minion ID.
- `pillar/pmg.sls` — переменные и параметры PMG.
- `pmg/*.sls` — состояния:
- `common` (базовые пакеты),
- `node_exporter` (установка и включение Prometheus node exporter),
- `smtp_monitoring` (SMTP-метрики: blackbox probe для 25/26 и postfix queue/connection gauges),
- `ssh_hardening`,
- `custom_check` (кастомная AV-проверка PMG через KESL),
- `security_agents` (Wazuh/Qualys),
- `update` (пакетные обновления + pre/post проверки PMG),
- `auto_updates` (security-only, без auto reboot),
- `audit_logging`,
- `pmg_baseline` (проверка/поддержание сервисов PMG),
- `verify` (проверки состояния).
- `pmg/files/*` — шаблоны конфигов.
## Требования
- Salt (рекомендуется актуальная LTS-ветка).
- Debian/PMG узлы с systemd.
- root-доступ на узел.
## Быстрый запуск
```bash
make lint
make check
make apply
make verify
make update-check
make update
```
По умолчанию `MINION_ID=p-mailgw-x2-01.lmru.tech`. Можно переопределить:
```bash
make check MINION_ID=p-mailgw-x1-01.lmru.tech
```
## Основные переменные
Все параметры задаются в `pillar/pmg.sls`:
- `pmg.base_packages`
- `pmg.ssh_allow_users`
- `pmg.ssh_password_authentication`
- `pmg.journald_system_max_use`
- `pmg.journald_max_file_sec`
- `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`
- `pmg.custom_check.*`
- `pmg.custom_check.log_dir_user|log_dir_group|log_dir_mode|log_file_mode`
- `pmg.manage_hosts_file`
- `pmg.hosts_file_entries`
- `pmg.wazuh.*`
- `pmg.kesl.*`
- `pmg.klnagent.*`
- `pmg.qualys.*`
`pmg.node_exporter.args` по умолчанию включает textfile collector:
`--collector.textfile.directory=/var/lib/prometheus/node-exporter`.
`pmg.smtp_monitoring` публикует расширенные SMTP метрики в node_exporter:
- queue total/deferred + age oldest message;
- SMTP listen/established для портов 25/26;
- 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
Перед обновлением:
```bash
make update-check MINION_ID=p-mailgw-x1-01.lmru.tech
```
Применение:
```bash
make update MINION_ID=p-mailgw-x1-01.lmru.tech
make verify MINION_ID=p-mailgw-x1-01.lmru.tech
```
State `pmg.update` выполняет:
- precheck кластера (`pmgcm status`);
- precheck критичных PMG сервисов;
- пакетное обновление (`pkg.uptodate`, с `dist_upgrade` по `pmg.update_dist_upgrade=true`);
- postcheck кластера и сервисов;
- уведомление, если требуется reboot (`/var/run/reboot-required`).
- опциональный автоперезапуск по `pmg.update_auto_reboot=true` (через `shutdown -r +N`).
## PMG custom check (Kaspersky)
Коротко про `pmg.custom_check`:
- готовит окружение (`pwgen`, `/var/log/mail_av`, `/usr/local/bin/pmg-custom-check`);
- готовит Kaspersky-учетку (`pmg.custom_check.ksc_superuser.*`; сейчас `superuser` + `NOPASSWD`);
- настраивает исключения KESL из сканирования через `pmg.custom_check.kesl_excluded_paths`
(по умолчанию: `/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`.
+129
View File
@@ -0,0 +1,129 @@
pmg:
base_packages:
- curl
- vim-tiny
- rsync
- ca-certificates
- jq
ssh_allow_users: []
ssh_password_authentication: true
journald_system_max_use: 1G
journald_max_file_sec: 1month
node_exporter:
enabled: true
package: prometheus-node-exporter
service: prometheus-node-exporter
textfile_directory: /var/lib/prometheus/node-exporter
args: --collector.textfile.directory=/var/lib/prometheus/node-exporter
smtp_monitoring:
enabled: true
log_window_seconds: 300
ports:
- 25
- 26
blackbox:
package: prometheus-blackbox-exporter
service: prometheus-blackbox-exporter
pmg_services:
- postfix
- pmg-smtp-filter
- pmgpolicy
- 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
custom_check:
enabled: true
script_path: /usr/local/bin/pmg-custom-check
log_dir: /var/log/mail_av
log_dir_user: pmg-smtp-filter
log_dir_group: pmg
log_dir_mode: "0770"
log_file_mode: "0660"
managed_packages:
- pwgen
ksc_superuser:
enabled: true
name: superuser
manage_sudoers: true
required_services:
- kesl
- klnagent64
kesl_excluded_paths:
- /var/spool/pmg
- /var/lib/clamav
- /tmp/.proxdump_*
manage_hosts_file: false
hosts_file_entries:
- ip: 10.80.38.152
hostname: p-mailgw-x1-01
aliases:
- p-mailgw-x1-01.lmru.tech
- ip: 10.80.38.153
hostname: p-mailgw-x1-02
aliases:
- p-mailgw-x1-02.lmru.tech
- ip: 10.220.41.152
hostname: p-mailgw-x2-01
aliases:
- p-mailgw-x2-01.lmru.tech
- ip: 10.220.41.153
hostname: p-mailgw-x2-02
aliases:
- p-mailgw-x2-02.lmru.tech
wazuh:
enabled: true
version: 4.14.0-1
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
package_path: /opt/QualysCloudAgent.deb
activation_id: 5eb9ad8e-02cc-431a-9afb-b8caad175ec1
customer_id: 4d868109-dad3-5536-82b4-9f3a6f3010bc
server_uri: https://qagpublic.qg1.apps.qualys.eu/CloudAgent/
+3
View File
@@ -0,0 +1,3 @@
base:
'*':
- pmg
+56
View File
@@ -0,0 +1,56 @@
{% set journald_system_max_use = salt['pillar.get']('pmg:journald_system_max_use', '1G') %}
{% set journald_max_file_sec = salt['pillar.get']('pmg:journald_max_file_sec', '1month') %}
pmg-journald-dropin-dir:
file.directory:
- name: /etc/systemd/journald.conf.d
- user: root
- group: root
- mode: '0755'
pmg-journald-retention:
file.managed:
- name: /etc/systemd/journald.conf.d/99-retention.conf
- source: salt://pmg/files/journald-retention.conf.jinja
- template: jinja
- context:
journald_system_max_use: {{ journald_system_max_use }}
journald_max_file_sec: {{ journald_max_file_sec }}
- user: root
- group: root
- mode: '0644'
pmg-restart-journald:
cmd.run:
- name: systemctl restart systemd-journald
- onchanges:
- file: pmg-journald-retention
pmg-auditd-package:
pkg.installed:
- name: auditd
pmg-audit-rules:
file.managed:
- name: /etc/audit/rules.d/99-pmg-baseline.rules
- source: salt://pmg/files/99-pmg-baseline.rules.jinja
- template: jinja
- user: root
- group: root
- mode: '0640'
pmg-reload-audit-rules:
cmd.run:
- name: augenrules --load
- onchanges:
- file: pmg-audit-rules
pmg-auditd-service:
service.running:
- name: auditd
- enable: true
pmg-rsyslog-service:
service.running:
- name: rsyslog
- enable: true
+28
View File
@@ -0,0 +1,28 @@
pmg-updates-packages:
pkg.installed:
- pkgs:
- unattended-upgrades
- apt-listchanges
pmg-apt-auto-upgrades:
file.managed:
- name: /etc/apt/apt.conf.d/20auto-upgrades
- source: salt://pmg/files/20auto-upgrades.jinja
- template: jinja
- user: root
- group: root
- mode: '0644'
pmg-unattended-policy:
file.managed:
- name: /etc/apt/apt.conf.d/50unattended-upgrades
- source: salt://pmg/files/50unattended-upgrades.jinja
- template: jinja
- user: root
- group: root
- mode: '0644'
pmg-unattended-service:
service.running:
- name: unattended-upgrades
- enable: true
+30
View File
@@ -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 %}
+5
View File
@@ -0,0 +1,5 @@
{% set base_packages = salt['pillar.get']('pmg:base_packages', []) %}
pmg-base-packages:
pkg.installed:
- pkgs: {{ base_packages }}
+151
View File
@@ -0,0 +1,151 @@
{% set custom_check_enabled = salt['pillar.get']('pmg:custom_check:enabled', True) %}
{% set custom_check_script_path = salt['pillar.get']('pmg:custom_check:script_path', '/usr/local/bin/pmg-custom-check') %}
{% set custom_check_log_dir = salt['pillar.get']('pmg:custom_check:log_dir', '/var/log/mail_av') %}
{% set custom_check_log_dir_user = salt['pillar.get']('pmg:custom_check:log_dir_user', 'pmg-smtp-filter') %}
{% set custom_check_log_dir_group = salt['pillar.get']('pmg:custom_check:log_dir_group', 'pmg') %}
{% set custom_check_log_dir_mode = salt['pillar.get']('pmg:custom_check:log_dir_mode', '0770') %}
{% set custom_check_log_file_mode = salt['pillar.get']('pmg:custom_check:log_file_mode', '0660') %}
{% set custom_check_managed_packages = salt['pillar.get']('pmg:custom_check:managed_packages', ['pwgen']) %}
{% set custom_check_required_services = salt['pillar.get']('pmg:custom_check:required_services', ['kesl', 'klnagent64']) %}
{% set custom_check_kesl_excluded_paths = salt['pillar.get']('pmg:custom_check:kesl_excluded_paths', ['/var/spool/pmg', '/var/lib/clamav', '/tmp/.proxdump_*']) %}
{% set ksc_superuser_enabled = salt['pillar.get']('pmg:custom_check:ksc_superuser:enabled', False) %}
{% set ksc_superuser_name = salt['pillar.get']('pmg:custom_check:ksc_superuser:name', '') %}
{% set ksc_superuser_manage_sudoers = salt['pillar.get']('pmg:custom_check:ksc_superuser:manage_sudoers', True) %}
{% set kesl_installed = salt['pkg.version']('kesl') != '' %}
{% set klnagent64_installed = salt['pkg.version']('klnagent64') != '' %}
{% set custom_check_kaspersky_ready = kesl_installed and klnagent64_installed %}
{% if custom_check_enabled %}
{% if ksc_superuser_enabled and ksc_superuser_name %}
pmg-custom-check-ksc-superuser:
user.present:
- name: {{ ksc_superuser_name }}
- shell: /bin/bash
- createhome: true
{% if ksc_superuser_manage_sudoers %}
pmg-custom-check-ksc-superuser-sudo-package:
pkg.installed:
- name: sudo
pmg-custom-check-ksc-superuser-sudoers:
file.managed:
- name: /etc/sudoers.d/90-kaspersky-superuser
- user: root
- group: root
- mode: '0440'
- contents:
- '{{ ksc_superuser_name }} ALL = (ALL) NOPASSWD: ALL'
- check_cmd: visudo -cf
- require:
- pkg: pmg-custom-check-ksc-superuser-sudo-package
- user: pmg-custom-check-ksc-superuser
{% endif %}
{% elif ksc_superuser_enabled %}
pmg-custom-check-ksc-superuser-missing-name:
test.fail_without_changes:
- name: "pmg.custom_check.ksc_superuser.enabled=true but pmg.custom_check.ksc_superuser.name is empty."
{% endif %}
pmg-custom-check-managed-packages:
pkg.installed:
- pkgs: {{ custom_check_managed_packages }}
pmg-custom-check-log-dir:
file.directory:
- name: {{ custom_check_log_dir }}
- user: {{ custom_check_log_dir_user }}
- group: {{ custom_check_log_dir_group }}
- mode: '{{ custom_check_log_dir_mode }}'
- recurse:
- user
- group
pmg-custom-check-log-file:
file.managed:
- name: {{ custom_check_log_dir }}/checked
- user: {{ custom_check_log_dir_user }}
- group: {{ custom_check_log_dir_group }}
- mode: '{{ custom_check_log_file_mode }}'
- replace: false
- require:
- file: pmg-custom-check-log-dir
pmg-custom-check-script:
file.managed:
- name: {{ custom_check_script_path }}
- source: salt://pmg/files/pmg-custom-check.jinja
- template: jinja
- user: root
- group: root
- mode: '0755'
- context:
custom_check_log_dir: {{ custom_check_log_dir }}
- require:
- pkg: pmg-custom-check-managed-packages
- file: pmg-custom-check-log-dir
{% if custom_check_kaspersky_ready %}
pmg-custom-check-kaspersky-ready:
test.nop:
- name: "KESL and klnagent64 are installed."
{% else %}
pmg-custom-check-kaspersky-missing-note:
test.show_notification:
- text: "KESL/klnagent64 не найдены. Установите через Kaspersky Administration Center (сервисный superuser/NOPASSWD), затем повторите state.apply pmg.custom_check."
{% endif %}
{% if custom_check_kaspersky_ready %}
{% for service_name in custom_check_required_services %}
pmg-custom-check-service-{{ service_name }}:
service.running:
- name: {{ service_name }}
- enable: true
- require:
- test: pmg-custom-check-kaspersky-ready
{% endfor %}
{% for excluded_path in custom_check_kesl_excluded_paths %}
pmg-custom-check-kesl-excluded-scope-{{ loop.index0 }}:
cmd.run:
- name: >-
kesl-control --set-settings File_Threat_Protection
ExcludedFromScanScope.item_{{ "%04d" | format(loop.index0) }}.Path='{{ excluded_path }}'
ExcludedFromScanScope.item_{{ "%04d" | format(loop.index0) }}.UseScanArea=Yes
ExcludedFromScanScope.item_{{ "%04d" | format(loop.index0) }}.AreaMask.item_0000='*'
- unless: >-
kesl-control --get-settings File_Threat_Protection 2>/dev/null
| grep -Fq "Path={{ excluded_path }}"
- require:
- test: pmg-custom-check-kaspersky-ready
{% for service_name in custom_check_required_services %}
- service: pmg-custom-check-service-{{ service_name }}
{% endfor %}
{% endfor %}
pmg-custom-check-kesl-restart:
cmd.wait:
- name: systemctl restart kesl
- watch:
{% for excluded_path in custom_check_kesl_excluded_paths %}
- cmd: pmg-custom-check-kesl-excluded-scope-{{ loop.index0 }}
{% endfor %}
- require:
- test: pmg-custom-check-kaspersky-ready
pmg-custom-check-enable-in-pmg:
cmd.run:
- name: pmgsh set /config/admin --custom_check 1
- unless: "pmgsh get /config/admin | grep -q '\"custom_check\" : 1'"
- require:
- file: pmg-custom-check-script
- pkg: pmg-custom-check-managed-packages
{% for service_name in custom_check_required_services %}
- service: pmg-custom-check-service-{{ service_name }}
{% endfor %}
{% endif %}
{% else %}
pmg-custom-check-disabled-note:
test.show_notification:
- text: "PMG custom_check disabled by pillar (pmg.custom_check.enabled=false)."
{% endif %}
+4
View File
@@ -0,0 +1,4 @@
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
+14
View File
@@ -0,0 +1,14 @@
Unattended-Upgrade::Origins-Pattern {
"origin=Debian,codename=${distro_codename},label=Debian-Security";
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::InstallOnShutdown "false";
Unattended-Upgrade::MailOnlyOnError "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::SyslogEnable "true";
+10
View File
@@ -0,0 +1,10 @@
# Managed by Salt.
PasswordAuthentication {{ "yes" if ssh_password_authentication else "no" }}
PermitRootLogin prohibit-password
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 30
{% if ssh_allow_users %}
AllowUsers {{ " ".join(ssh_allow_users) }}
{% endif %}
+14
View File
@@ -0,0 +1,14 @@
## Managed by Salt.
-D
-b 8192
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k privilege-escalation
-w /etc/sudoers.d/ -p wa -k privilege-escalation
-w /etc/ssh/sshd_config -p wa -k ssh
-w /etc/ssh/sshd_config.d/ -p wa -k ssh
+12
View File
@@ -0,0 +1,12 @@
modules:
{% for port in smtp_ports %}
smtp_{{ port }}:
prober: tcp
timeout: 5s
tcp:
preferred_ip_protocol: ip4
query_response:
- expect: "^220[ -]"
- send: "EHLO blackbox.local"
- expect: "^250[ -]"
{% endfor %}
+3
View File
@@ -0,0 +1,3 @@
[Journal]
SystemMaxUse={{ journald_system_max_use }}
MaxFileSec={{ journald_max_file_sec }}
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Managed by Salt: PMG custom check via Kaspersky KESL.
set -u
LOG_DIR="{{ custom_check_log_dir }}"
KESL_BIN="/opt/kaspersky/kesl/bin/kesl-control"
SCAN_TARGET="${2:-}"
if [ -z "${SCAN_TARGET}" ]; then
echo "v1"
echo "ERROR: empty scan target"
exit 0
fi
mkdir -p "${LOG_DIR}"
POLICY_NAME="$(pwgen 10 -1 -s -0)"
POLICY_FILE="${LOG_DIR}/${POLICY_NAME}.pol"
cat > "${POLICY_FILE}" <<EOF
ActionOnThreat=Inform
ReportCleanObjects=No
ScanFiles=Yes
ScanMailBases=Yes
ScanPlainMail=Yes
UseAnalyzer=Yes
HeuristicLevel=Medium
[ScanScope.item_0000]
Path=${SCAN_TARGET}
EOF
"${KESL_BIN}" --create-task "${POLICY_NAME}" --type ODS --file "${POLICY_FILE}" >/dev/null
VIRUS_NAME="$("${KESL_BIN}" --start-task "${POLICY_NAME}" -W | grep 'DetectName' | awk -F'=' '{print $2}' | head -1)"
"${KESL_BIN}" --delete-task "${POLICY_NAME}"
if [ -z "${VIRUS_NAME}" ]; then
rm -f "${POLICY_FILE}"
echo "Mail ${SCAN_TARGET} was checked successfully" >> "${LOG_DIR}/checked"
echo "v1"
echo "OK"
else
echo "In mail ${SCAN_TARGET} find ${VIRUS_NAME}" >> "${POLICY_FILE}"
echo "Mail ${SCAN_TARGET} was blocked. More info in ${POLICY_FILE}" >> "${LOG_DIR}/checked"
echo "v1"
echo "VIRUS: ${VIRUS_NAME}"
fi
exit 0
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=Generate PMG SMTP metrics for node_exporter textfile collector
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/pmg-smtp-metrics.sh
+219
View File
@@ -0,0 +1,219 @@
#!/usr/bin/env bash
set -euo pipefail
outfile="/var/lib/prometheus/node-exporter/pmg_smtp.prom"
tmpfile="$(mktemp)"
statefile="/var/lib/prometheus/node-exporter/pmg_smtp.state"
statetmp="$(mktemp)"
now_epoch="$(date +%s)"
window_seconds={{ smtp_log_window_seconds | int }}
if [ "${window_seconds}" -lt 60 ]; then
window_seconds=60
fi
to_int() {
local value="${1:-0}"
if [[ "${value}" =~ ^[0-9]+$ ]]; then
echo "${value}"
else
echo "0"
fi
}
get_established_count() {
local port="$1"
ss -Htn state established "( sport = :${port} )" 2>/dev/null | awk 'END { print NR + 0 }'
}
get_listen_up() {
local port="$1"
if ss -Hln "( sport = :${port} )" 2>/dev/null | awk 'END { exit(NR > 0 ? 0 : 1) }'; then
echo "1"
else
echo "0"
fi
}
service_up() {
local service="$1"
if systemctl is-active --quiet "${service}" 2>/dev/null; then
echo "1"
else
echo "0"
fi
}
last_cursor=""
total_smtp_connections=0
total_smtp_received=0
total_smtp_sent=0
total_smtp_deferred=0
total_smtp_bounced=0
total_smtp_rejected=0
total_smtp_tls_failures=0
prev_queue_total=0
prev_queue_ts=0
if [ -f "${statefile}" ]; then
while IFS='=' read -r key value; do
case "${key}" in
last_cursor) last_cursor="${value}" ;;
total_smtp_connections) total_smtp_connections="$(to_int "${value}")" ;;
total_smtp_received) total_smtp_received="$(to_int "${value}")" ;;
total_smtp_sent) total_smtp_sent="$(to_int "${value}")" ;;
total_smtp_deferred) total_smtp_deferred="$(to_int "${value}")" ;;
total_smtp_bounced) total_smtp_bounced="$(to_int "${value}")" ;;
total_smtp_rejected) total_smtp_rejected="$(to_int "${value}")" ;;
total_smtp_tls_failures) total_smtp_tls_failures="$(to_int "${value}")" ;;
prev_queue_total) prev_queue_total="$(to_int "${value}")" ;;
prev_queue_ts) prev_queue_ts="$(to_int "${value}")" ;;
esac
done < "${statefile}"
fi
queue_total="$(postqueue -j 2>/dev/null | awk 'END { print NR + 0 }')"
queue_deferred="$(postqueue -j 2>/dev/null | awk -F'"' '/"queue_name":/ { if ($4 == "deferred") c++ } END { print c + 0 }')"
if command -v jq >/dev/null 2>&1; then
oldest_queue_epoch="$(postqueue -j 2>/dev/null | jq -sr 'map(.arrival_time // empty) | if length == 0 then 0 else min end' 2>/dev/null || echo "0")"
else
oldest_queue_epoch="0"
fi
if [ "${oldest_queue_epoch}" -gt 0 ] 2>/dev/null; then
queue_oldest_age_seconds="$(( now_epoch - oldest_queue_epoch ))"
if [ "${queue_oldest_age_seconds}" -lt 0 ]; then
queue_oldest_age_seconds=0
fi
else
queue_oldest_age_seconds=0
fi
if [ "${prev_queue_ts}" -gt 0 ] && [ "${now_epoch}" -gt "${prev_queue_ts}" ]; then
queue_delta_seconds="$(( now_epoch - prev_queue_ts ))"
queue_drain_per_minute="$(awk -v prev="${prev_queue_total}" -v curr="${queue_total}" -v dt="${queue_delta_seconds}" 'BEGIN { d = prev - curr; if (dt <= 0 || d <= 0) print 0; else printf "%.3f", (d * 60.0) / dt }')"
else
queue_drain_per_minute="0"
fi
log_slice="$(journalctl --no-pager -o cat --since "-${window_seconds} sec" 2>/dev/null || true)"
smtp_connections_recent="$(printf '%s\n' "${log_slice}" | grep -Ec 'postfix/smtpd.*connect from' || true)"
smtp_received_recent="$(printf '%s\n' "${log_slice}" | grep -Ec 'postfix/qmgr.*: .*: from=<' || true)"
smtp_sent_recent="$(printf '%s\n' "${log_slice}" | grep -Ec 'status=sent' || true)"
smtp_deferred_recent="$(printf '%s\n' "${log_slice}" | grep -Ec 'status=deferred' || true)"
smtp_bounced_recent="$(printf '%s\n' "${log_slice}" | grep -Ec 'status=bounced' || true)"
smtp_rejected_recent="$(printf '%s\n' "${log_slice}" | grep -Ec 'NOQUEUE: reject:| reject:' || true)"
smtp_tls_fail_recent="$(printf '%s\n' "${log_slice}" | grep -Eci '(tls|starttls).*(fail|error|warning|lost)' || true)"
journal_cursor_args=()
if [ -n "${last_cursor}" ]; then
journal_cursor_args+=(--after-cursor "${last_cursor}")
fi
journal_delta="$(journalctl --no-pager -o cat "${journal_cursor_args[@]}" 2>/dev/null || true)"
delta_smtp_connections="$(printf '%s\n' "${journal_delta}" | grep -Ec 'postfix/smtpd.*connect from' || true)"
delta_smtp_received="$(printf '%s\n' "${journal_delta}" | grep -Ec 'postfix/qmgr.*: .*: from=<' || true)"
delta_smtp_sent="$(printf '%s\n' "${journal_delta}" | grep -Ec 'status=sent' || true)"
delta_smtp_deferred="$(printf '%s\n' "${journal_delta}" | grep -Ec 'status=deferred' || true)"
delta_smtp_bounced="$(printf '%s\n' "${journal_delta}" | grep -Ec 'status=bounced' || true)"
delta_smtp_rejected="$(printf '%s\n' "${journal_delta}" | grep -Ec 'NOQUEUE: reject:| reject:' || true)"
delta_smtp_tls_failures="$(printf '%s\n' "${journal_delta}" | grep -Eci '(tls|starttls).*(fail|error|warning|lost)' || true)"
total_smtp_connections="$(( total_smtp_connections + delta_smtp_connections ))"
total_smtp_received="$(( total_smtp_received + delta_smtp_received ))"
total_smtp_sent="$(( total_smtp_sent + delta_smtp_sent ))"
total_smtp_deferred="$(( total_smtp_deferred + delta_smtp_deferred ))"
total_smtp_bounced="$(( total_smtp_bounced + delta_smtp_bounced ))"
total_smtp_rejected="$(( total_smtp_rejected + delta_smtp_rejected ))"
total_smtp_tls_failures="$(( total_smtp_tls_failures + delta_smtp_tls_failures ))"
current_cursor="$(journalctl --no-pager -n 1 -o cat --show-cursor 2>/dev/null | awk -F': ' '/^-- cursor: / { c = $2 } END { print c }')"
if [ -z "${current_cursor}" ]; then
current_cursor="${last_cursor}"
fi
{
echo "# HELP pmg_postfix_queue_messages_total Number of messages in Postfix queue."
echo "# TYPE pmg_postfix_queue_messages_total gauge"
echo "pmg_postfix_queue_messages_total ${queue_total}"
echo "# HELP pmg_postfix_queue_deferred_messages Number of deferred messages in Postfix queue."
echo "# TYPE pmg_postfix_queue_deferred_messages gauge"
echo "pmg_postfix_queue_deferred_messages ${queue_deferred}"
echo "# HELP pmg_postfix_queue_oldest_age_seconds Age of oldest queued message in seconds."
echo "# TYPE pmg_postfix_queue_oldest_age_seconds gauge"
echo "pmg_postfix_queue_oldest_age_seconds ${queue_oldest_age_seconds}"
echo "# HELP pmg_postfix_queue_drain_per_minute Queue drain rate in messages per minute (only positive drain)."
echo "# TYPE pmg_postfix_queue_drain_per_minute gauge"
echo "pmg_postfix_queue_drain_per_minute ${queue_drain_per_minute}"
echo "# HELP pmg_smtp_metrics_window_seconds Time window in seconds used for recent SMTP counters."
echo "# TYPE pmg_smtp_metrics_window_seconds gauge"
echo "pmg_smtp_metrics_window_seconds ${window_seconds}"
echo "# HELP pmg_smtp_connections_recent Number of SMTP connections observed in recent window."
echo "# TYPE pmg_smtp_connections_recent gauge"
echo "pmg_smtp_connections_recent ${smtp_connections_recent}"
echo "# HELP pmg_smtp_received_recent Number of received messages observed in recent window."
echo "# TYPE pmg_smtp_received_recent gauge"
echo "pmg_smtp_received_recent ${smtp_received_recent}"
echo "# HELP pmg_smtp_sent_recent Number of sent messages observed in recent window."
echo "# TYPE pmg_smtp_sent_recent gauge"
echo "pmg_smtp_sent_recent ${smtp_sent_recent}"
echo "# HELP pmg_smtp_deferred_recent Number of deferred deliveries observed in recent window."
echo "# TYPE pmg_smtp_deferred_recent gauge"
echo "pmg_smtp_deferred_recent ${smtp_deferred_recent}"
echo "# HELP pmg_smtp_bounced_recent Number of bounced deliveries observed in recent window."
echo "# TYPE pmg_smtp_bounced_recent gauge"
echo "pmg_smtp_bounced_recent ${smtp_bounced_recent}"
echo "# HELP pmg_smtp_rejected_recent Number of rejected SMTP attempts observed in recent window."
echo "# TYPE pmg_smtp_rejected_recent gauge"
echo "pmg_smtp_rejected_recent ${smtp_rejected_recent}"
echo "# HELP pmg_smtp_tls_failures_recent Number of TLS/STARTTLS failures observed in recent window."
echo "# TYPE pmg_smtp_tls_failures_recent gauge"
echo "pmg_smtp_tls_failures_recent ${smtp_tls_fail_recent}"
echo "# HELP pmg_smtp_connections_total Total SMTP connections seen in logs since metrics script state start."
echo "# TYPE pmg_smtp_connections_total counter"
echo "pmg_smtp_connections_total ${total_smtp_connections}"
echo "# HELP pmg_smtp_messages_total Total SMTP message events by direction/status since metrics script state start."
echo "# TYPE pmg_smtp_messages_total counter"
echo "pmg_smtp_messages_total{direction=\"received\",status=\"accepted\"} ${total_smtp_received}"
echo "pmg_smtp_messages_total{direction=\"sent\",status=\"sent\"} ${total_smtp_sent}"
echo "pmg_smtp_messages_total{direction=\"delivery\",status=\"deferred\"} ${total_smtp_deferred}"
echo "pmg_smtp_messages_total{direction=\"delivery\",status=\"bounced\"} ${total_smtp_bounced}"
echo "pmg_smtp_messages_total{direction=\"inbound\",status=\"rejected\"} ${total_smtp_rejected}"
echo "# HELP pmg_smtp_tls_failures_total Total TLS/STARTTLS failures seen in logs since metrics script state start."
echo "# TYPE pmg_smtp_tls_failures_total counter"
echo "pmg_smtp_tls_failures_total ${total_smtp_tls_failures}"
echo "# HELP pmg_smtp_port_listen_up SMTP listen status on port (1 up, 0 down)."
echo "# TYPE pmg_smtp_port_listen_up gauge"
echo "# HELP pmg_smtp_port_established_connections Established SMTP connections on port."
echo "# TYPE pmg_smtp_port_established_connections gauge"
echo "# HELP pmg_service_up PMG-related service status (1 up, 0 down)."
echo "# TYPE pmg_service_up gauge"
{% for port in smtp_ports %}
echo "pmg_smtp_port_listen_up{port=\"{{ port }}\"} $(get_listen_up {{ port }})"
echo "pmg_smtp_port_established_connections{port=\"{{ port }}\"} $(get_established_count {{ port }})"
{% endfor %}
echo "pmg_service_up{service=\"postfix\"} $(service_up postfix)"
echo "pmg_service_up{service=\"pmg-smtp-filter\"} $(service_up pmg-smtp-filter)"
echo "pmg_service_up{service=\"pmgpolicy\"} $(service_up pmgpolicy)"
echo "pmg_service_up{service=\"pmgdaemon\"} $(service_up pmgdaemon)"
echo "pmg_service_up{service=\"pmgmirror\"} $(service_up pmgmirror)"
echo "pmg_service_up{service=\"pmgtunnel\"} $(service_up pmgtunnel)"
} > "${tmpfile}"
mv "${tmpfile}" "${outfile}"
chmod 0644 "${outfile}"
{
echo "last_cursor=${current_cursor}"
echo "total_smtp_connections=${total_smtp_connections}"
echo "total_smtp_received=${total_smtp_received}"
echo "total_smtp_sent=${total_smtp_sent}"
echo "total_smtp_deferred=${total_smtp_deferred}"
echo "total_smtp_bounced=${total_smtp_bounced}"
echo "total_smtp_rejected=${total_smtp_rejected}"
echo "total_smtp_tls_failures=${total_smtp_tls_failures}"
echo "prev_queue_total=${queue_total}"
echo "prev_queue_ts=${now_epoch}"
} > "${statetmp}"
mv "${statetmp}" "${statefile}"
chmod 0644 "${statefile}"
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Run PMG SMTP metrics exporter script every minute
[Timer]
OnBootSec=30s
OnUnitActiveSec=1min
AccuracySec=15s
Unit=pmg-smtp-metrics.service
[Install]
WantedBy=timers.target
+11
View File
@@ -0,0 +1,11 @@
include:
- pmg.common
- pmg.node_exporter
- pmg.smtp_monitoring
- pmg.ssh_hardening
- pmg.custom_check
- pmg.security_agents
- pmg.cluster_join
- pmg.auto_updates
- pmg.audit_logging
- pmg.pmg_baseline
+39
View File
@@ -0,0 +1,39 @@
{% set node_exporter_enabled = salt['pillar.get']('pmg:node_exporter:enabled', True) %}
{% set node_exporter_package = salt['pillar.get']('pmg:node_exporter:package', 'prometheus-node-exporter') %}
{% set node_exporter_service = salt['pillar.get']('pmg:node_exporter:service', 'prometheus-node-exporter') %}
{% set node_exporter_textfile_dir = salt['pillar.get']('pmg:node_exporter:textfile_directory', '/var/lib/prometheus/node-exporter') %}
{% set node_exporter_args = salt['pillar.get']('pmg:node_exporter:args', '--collector.textfile.directory=' ~ node_exporter_textfile_dir) %}
{% if node_exporter_enabled %}
pmg-node-exporter-package:
pkg.installed:
- name: {{ node_exporter_package }}
pmg-node-exporter-textfile-dir:
file.directory:
- name: {{ node_exporter_textfile_dir }}
- user: root
- group: root
- mode: "0755"
pmg-node-exporter-defaults:
file.managed:
- name: /etc/default/prometheus-node-exporter
- user: root
- group: root
- mode: "0644"
- contents:
- "# Managed by Salt."
- "ARGS=\"{{ node_exporter_args }}\""
pmg-node-exporter-service:
service.running:
- name: {{ node_exporter_service }}
- enable: true
- require:
- pkg: pmg-node-exporter-package
- file: pmg-node-exporter-textfile-dir
- file: pmg-node-exporter-defaults
- watch:
- file: pmg-node-exporter-defaults
{% endif %}
+50
View File
@@ -0,0 +1,50 @@
{% 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', []) %}
{% for svc in pmg_services %}
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:
cmd.run:
- name: pmgcm status
pmg-postfix-identity-from-system-hostname:
cmd.run:
- name: >-
HN="$(hostname -f)" &&
DM="${HN#*.}" &&
postconf -e "myhostname=${HN}" "mydomain=${DM}" &&
systemctl reload postfix
- unless: >-
HN="$(hostname -f)" &&
DM="${HN#*.}" &&
[ "$(postconf -h myhostname)" = "${HN}" ] &&
[ "$(postconf -h mydomain)" = "${DM}" ]
- require:
- service: pmg-service-postfix
{% if manage_hosts_file %}
pmg-managed-hosts:
file.blockreplace:
- name: /etc/hosts
- marker_start: "#-- SALT PMG HOSTS START --"
- marker_end: "#-- SALT PMG HOSTS END --"
- append_if_not_found: true
- content: |
{% for host in hosts_file_entries %}
{% if host.get('aliases', []) %}
{{ host['ip'] }} {{ host['hostname'] }} {{ ' '.join(host['aliases']) }}
{% else %}
{{ host['ip'] }} {{ host['hostname'] }}
{% endif %}
{% endfor %}
{% endif %}
+208
View File
@@ -0,0 +1,208 @@
{% set wazuh_enabled = salt['pillar.get']('pmg:wazuh:enabled', True) %}
{% set wazuh_version = salt['pillar.get']('pmg:wazuh:version', '4.14.0-1') %}
{% set wazuh_manager = salt['pillar.get']('pmg:wazuh:manager', '10.80.120.205') %}
{% set wazuh_group = salt['pillar.get']('pmg:wazuh:group', 'test_auditd_unix') %}
{% set wazuh_pkg_name = 'wazuh-agent_' ~ wazuh_version ~ '_amd64.deb' %}
{% 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') %}
{% set qualys_activation_id = salt['pillar.get']('pmg:qualys:activation_id', '') %}
{% 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:
- name: wget -qO {{ wazuh_pkg_path }} {{ wazuh_pkg_url }}
- unless: test -f {{ wazuh_pkg_path }}
pmg-wazuh-install:
cmd.run:
- name: WAZUH_MANAGER='{{ wazuh_manager }}' WAZUH_AGENT_GROUP='{{ wazuh_group }}' WAZUH_AGENT_NAME="$(hostname -f)" dpkg -i {{ wazuh_pkg_path }}
- unless: dpkg-query -W -f='${Status}' wazuh-agent 2>/dev/null | grep -q 'install ok installed'
- require:
- cmd: pmg-wazuh-download
pmg-wazuh-service:
service.running:
- name: wazuh-agent
- enable: true
- require:
- cmd: pmg-wazuh-install
{% endif %}
{% if qualys_enabled and qualys_package_path and qualys_activation_id and qualys_customer_id %}
pmg-qualys-download:
cmd.run:
- name: wget -qO {{ qualys_package_path }} {{ qualys_package_url }}
- unless: test -f {{ qualys_package_path }}
pmg-qualys-install:
cmd.run:
- name: dpkg --install {{ qualys_package_path }}
- onlyif: test -f {{ qualys_package_path }}
- unless: dpkg-query -W -f='${Status}' qualys-cloud-agent 2>/dev/null | grep -q 'install ok installed'
- require:
- cmd: pmg-qualys-download
pmg-qualys-activate:
cmd.run:
- name: /usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh ActivationId={{ qualys_activation_id }} CustomerId={{ qualys_customer_id }} ServerUri={{ qualys_server_uri }}
- onchanges:
- cmd: pmg-qualys-install
- onlyif: test -x /usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh
pmg-qualys-service:
service.running:
- name: qualys-cloud-agent
- enable: true
- onlyif: test -f {{ qualys_package_path }}
- require:
- cmd: pmg-qualys-install
{% elif qualys_enabled %}
pmg-qualys-skipped:
test.show_notification:
- text: "Qualys agent skipped: set package_url + package_path + activation_id + customer_id in pillar."
{% endif %}
+94
View File
@@ -0,0 +1,94 @@
{% set smtp_monitoring_enabled = salt['pillar.get']('pmg:smtp_monitoring:enabled', True) %}
{% set blackbox_package = salt['pillar.get']('pmg:smtp_monitoring:blackbox:package', 'prometheus-blackbox-exporter') %}
{% set blackbox_service = salt['pillar.get']('pmg:smtp_monitoring:blackbox:service', 'prometheus-blackbox-exporter') %}
{% set smtp_ports = salt['pillar.get']('pmg:smtp_monitoring:ports', [25, 26]) %}
{% set smtp_log_window_seconds = salt['pillar.get']('pmg:smtp_monitoring:log_window_seconds', 300) %}
{% if smtp_monitoring_enabled %}
pmg-smtp-monitoring-packages:
pkg.installed:
- pkgs:
- {{ blackbox_package }}
- iproute2
- jq
pmg-smtp-monitoring-metrics-dir:
file.directory:
- name: /var/lib/prometheus/node-exporter
- user: root
- group: root
- mode: "0755"
pmg-smtp-monitoring-script:
file.managed:
- name: /usr/local/bin/pmg-smtp-metrics.sh
- source: salt://pmg/files/pmg-smtp-metrics.sh.jinja
- template: jinja
- context:
smtp_ports: {{ smtp_ports }}
smtp_log_window_seconds: {{ smtp_log_window_seconds }}
- user: root
- group: root
- mode: "0755"
- require:
- file: pmg-smtp-monitoring-metrics-dir
pmg-smtp-monitoring-service:
file.managed:
- name: /etc/systemd/system/pmg-smtp-metrics.service
- source: salt://pmg/files/pmg-smtp-metrics.service.jinja
- user: root
- group: root
- mode: "0644"
pmg-smtp-monitoring-timer:
file.managed:
- name: /etc/systemd/system/pmg-smtp-metrics.timer
- source: salt://pmg/files/pmg-smtp-metrics.timer.jinja
- user: root
- group: root
- mode: "0644"
pmg-smtp-monitoring-blackbox-config:
file.managed:
- name: /etc/prometheus/blackbox.yml
- source: salt://pmg/files/blackbox.yml.jinja
- template: jinja
- context:
smtp_ports: {{ smtp_ports }}
- user: root
- group: root
- mode: "0644"
- require:
- pkg: pmg-smtp-monitoring-packages
pmg-smtp-monitoring-systemd-reload:
cmd.run:
- name: systemctl daemon-reload
- onchanges:
- file: pmg-smtp-monitoring-service
- file: pmg-smtp-monitoring-timer
pmg-smtp-monitoring-blackbox-service:
service.running:
- name: {{ blackbox_service }}
- enable: true
- require:
- pkg: pmg-smtp-monitoring-packages
- watch:
- file: pmg-smtp-monitoring-blackbox-config
pmg-smtp-monitoring-timer-running:
service.running:
- name: pmg-smtp-metrics.timer
- enable: true
- require:
- cmd: pmg-smtp-monitoring-systemd-reload
- file: pmg-smtp-monitoring-script
pmg-smtp-monitoring-run-once:
cmd.run:
- name: /usr/local/bin/pmg-smtp-metrics.sh
- require:
- file: pmg-smtp-monitoring-script
{% endif %}
+35
View File
@@ -0,0 +1,35 @@
{% set ssh_allow_users = salt['pillar.get']('pmg:ssh_allow_users', []) %}
{% set ssh_password_authentication = salt['pillar.get']('pmg:ssh_password_authentication', True) %}
pmg-sshd-dropin-dir:
file.directory:
- name: /etc/ssh/sshd_config.d
- user: root
- group: root
- mode: '0755'
pmg-sshd-hardening-config:
file.managed:
- name: /etc/ssh/sshd_config.d/99-hardening.conf
- source: salt://pmg/files/99-hardening.conf.jinja
- template: jinja
- context:
ssh_allow_users: {{ ssh_allow_users }}
ssh_password_authentication: {{ ssh_password_authentication }}
- user: root
- group: root
- mode: '0644'
pmg-validate-sshd-config:
cmd.run:
- name: sshd -t
- onchanges:
- file: pmg-sshd-hardening-config
pmg-sshd-service:
service.running:
- name: ssh
- enable: true
- reload: true
- watch:
- cmd: pmg-validate-sshd-config
+50
View File
@@ -0,0 +1,50 @@
{% set pmg_services = salt['pillar.get']('pmg:pmg_services', []) %}
{% set pmg_update_dist_upgrade = salt['pillar.get']('pmg:update_dist_upgrade', True) %}
{% set pmg_update_auto_reboot = salt['pillar.get']('pmg:update_auto_reboot', False) %}
{% set pmg_update_reboot_delay_minutes = salt['pillar.get']('pmg:update_reboot_delay_minutes', 1) %}
pmg-update-precheck-cluster:
cmd.run:
- name: pmgcm status
{% for svc in pmg_services %}
pmg-update-precheck-service-{{ svc }}:
cmd.run:
- name: systemctl is-active --quiet {{ svc }}
{% endfor %}
pmg-update-packages:
pkg.uptodate:
- refresh: true
- dist_upgrade: {{ pmg_update_dist_upgrade }}
pmg-update-postcheck-cluster:
cmd.run:
- name: pmgcm status
- require:
- pkg: pmg-update-packages
{% for svc in pmg_services %}
pmg-update-postcheck-service-{{ svc }}:
cmd.run:
- name: systemctl is-active --quiet {{ svc }}
- require:
- pkg: pmg-update-packages
{% endfor %}
pmg-update-reboot-required-note:
test.show_notification:
- text: "После обновления требуется reboot: /var/run/reboot-required"
- onlyif: test -f /var/run/reboot-required
{% if pmg_update_auto_reboot %}
pmg-update-schedule-reboot:
cmd.run:
- name: "shutdown -r +{{ pmg_update_reboot_delay_minutes }} \"Salt pmg.update: reboot required\""
- onlyif: test -f /var/run/reboot-required
- require:
- cmd: pmg-update-postcheck-cluster
{% for svc in pmg_services %}
- cmd: pmg-update-postcheck-service-{{ svc }}
{% endfor %}
{% endif %}
+31
View File
@@ -0,0 +1,31 @@
{% 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:
cmd.run:
- name: pmgcm status
pmg-verify-sshd-config:
cmd.run:
- name: sshd -t
pmg-verify-blackbox-service:
cmd.run:
- name: systemctl is-active --quiet prometheus-blackbox-exporter
pmg-verify-smtp-metrics-timer:
cmd.run:
- name: systemctl is-active --quiet pmg-smtp-metrics.timer
pmg-verify-smtp-metrics-exposed:
cmd.run:
- name: "curl -fsS http://127.0.0.1:9100/metrics | awk 'BEGIN { ok=0 } /^pmg_smtp_sent_recent / { ok=1 } END { exit(ok ? 0 : 1) }'"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 3 ]]; then
echo "usage: $0 <target_host> <master_host> <master_ip>" >&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'"
+3
View File
@@ -0,0 +1,3 @@
base:
'p-mailgw-x*-0*':
- pmg.init