Отлично, теперь картина ясная:

- **Балансера SMTP нет**
- Публикация идет через **Checkpoint**
- Балансировка — через **MX-записи**
- Хост может иметь один приватный интерфейс (`ens192`), и при этом быть доступным снаружи по DNAT/правилам на периметре — это нормально.

Значит целевая политика firewall на PMG такая:

- `25/tcp` — **разрешить из интернета** (иначе входящая почта сломается)
- `587/tcp` — только если реально нужен submission
- `22/tcp`, `443/tcp`, `8006/tcp` — **только из corp/VPN CIDR**
- остальное inbound — `drop`

Это соответствует текущей логике, которую мы уже заложили в роль.

Если хотите, следующим шагом сделаю «боевую» проверку на одной ноде (`t-mailgw-as-02`):
1. `--check --tags firewall`
2. затем apply только firewall на `--limit t-mailgw-as-02`
3. сразу `verify` + снимок `ss -tulpen` после применения.

**Considering firewall and interface settings**

I’m thinking through host firewall rules, maybe allowing port 25 from anywhere, and possibly port 587 too. Management should only be through the corporate VPN. I wonder if I can restrict port 25 specifically.

For actions in the repository, I could suggest renaming variables and documentation for clarity. There’s a single interface anyway, but I wonder if removing interface binding might improve robustness. However, the user didn’t explicitly request changes right now.

Let’s wrap this up in a concise message in Russian.
**Finalizing network settings**

I want to clarify that the OS displaying a private IP is not a contradiction. It's important to establish a clear rule set. I should also consider using a command to check settings before applying any changes. Maybe it’s a good idea to implement some checks and limits to ensure everything is in order. The nice part is that no specific tools are needed for this process, which simplifies things a bit.


<timestamp>Tuesday, Jun 2, 2026, 11:59 AM (UTC+3)</timestamp>
<user_query>
фаервол нужно убрать вообще, там все работает бай дизайн, в интрнет 25 порт за натом будет по эмикс записям, внутри сети закроют PAM+BASTION
еще сегодя узнал, что эти тачки будут не на нашей поддержке, просто они готовы взять таску через полгода, а надо срочно. Они работают с Saltstack, поэтому надо переделать сейчас под солт, без упоминания ансибла, чисто для меня в автономную единицу солта, потом просто передам код. Папки и файлы оставить только нужные, без всяких gitkeep и пустых папок.

</user_query>


Принял, разворачиваю репозиторий в автономный SaltStack-юнит и полностью убираю Ansible/role/firewall-часть. Сначала очищу структуру от лишних файлов, затем соберу рабочие `states`/`pillar` и обновлю README без упоминаний Ansible.


Пересобираю содержимое в Salt-структуру: сейчас добавлю `top.sls`, `pillar`, `pmg/*.sls` и шаблоны, затем перепишу `README/Makefile/.gitignore` так, чтобы не осталось упоминаний Ansible и firewall-руления.

ApplyPatch

Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/top.sls
+base:
+  't-mailgw-as-*':
+    - pmg.init
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/top.sls
+base:
+  '*':
+    - pmg
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/pmg.sls
+pmg:
+  timezone: Europe/Moscow
+  base_packages:
+    - curl
+    - vim-tiny
+    - rsync
+    - ca-certificates
+    - jq
+
+  ssh_allow_users: []
+
+  smtp_submission_enabled: false
+
+  journald_system_max_use: 1G
+  journald_max_file_sec: 1month
+
+  pmg_services:
+    - postfix
+    - pmg-smtp-filter
+    - pmgpolicy
+    - pmgdaemon
+    - pmgmirror
+    - pmgtunnel
+
+  manage_hosts_file: false
+  hosts_file_entries:
+    - ip: 10.80.38.152
+      hostname: t-mailgw-as-01
+      aliases:
+        - t-mailgw-as-01.local
+    - ip: 10.220.41.152
+      hostname: t-mailgw-as-02
+      aliases:
+        - t-mailgw-as-02.local
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/init.sls
+include:
+  - pmg.common
+  - pmg.ssh_hardening
+  - pmg.auto_updates
+  - pmg.audit_logging
+  - pmg.pmg_baseline
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/common.sls
+{% set timezone = salt['pillar.get']('pmg:timezone', 'Europe/Moscow') %}
+{% set base_packages = salt['pillar.get']('pmg:base_packages', []) %}
+
+pmg-base-packages:
+  pkg.installed:
+    - pkgs: {{ base_packages }}
+
+pmg-timezone-file:
+  file.managed:
+    - name: /etc/timezone
+    - contents: "{{ timezone }}\n"
+    - mode: '0644'
+    - user: root
+    - group: root
+
+pmg-set-timezone:
+  cmd.run:
+    - name: timedatectl set-timezone {{ timezone }}
+    - unless: timedatectl show --property=Timezone --value | grep -Fx {{ timezone }}
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/ssh_hardening.sls
+{% set ssh_allow_users = salt['pillar.get']('pmg:ssh_allow_users', []) %}
+
+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 }}
+    - 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
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/auto_updates.sls
+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
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/audit_logging.sls
+{% 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
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/pmg_baseline.sls
+{% set pmg_services = salt['pillar.get']('pmg:pmg_services', []) %}
+{% 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
+{% endfor %}
+
+pmg-cluster-status:
+  cmd.run:
+    - name: pmgcm status
+
+{% 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 %}
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/verify.sls
+{% set pmg_services = salt['pillar.get']('pmg:pmg_services', []) %}
+
+{% for svc in pmg_services %}
+pmg-verify-service-{{ svc }}:
+  cmd.run:
+    - name: systemctl is-active --quiet {{ svc }}
+{% endfor %}
+
+pmg-verify-cluster:
+  cmd.run:
+    - name: pmgcm status
+
+pmg-verify-sshd-config:
+  cmd.run:
+    - name: sshd -t
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/files/99-hardening.conf.jinja
+# Managed by Salt.
+PasswordAuthentication no
+PermitRootLogin prohibit-password
+PubkeyAuthentication yes
+X11Forwarding no
+MaxAuthTries 3
+LoginGraceTime 30
+{% if ssh_allow_users %}
+AllowUsers {{ " ".join(ssh_allow_users) }}
+{% endif %}
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/files/20auto-upgrades.jinja
+APT::Periodic::Update-Package-Lists "1";
+APT::Periodic::Download-Upgradeable-Packages "1";
+APT::Periodic::Unattended-Upgrade "1";
+APT::Periodic::AutocleanInterval "7";
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/files/50unattended-upgrades.jinja
+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";
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/files/journald-retention.conf.jinja
+[Journal]
+SystemMaxUse={{ journald_system_max_use }}
+MaxFileSec={{ journald_max_file_sec }}
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/files/99-pmg-baseline.rules.jinja
+## 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
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/Makefile
+SHELL := /usr/bin/env bash
+.RECIPEPREFIX := >
+
+SALT_CALL ?= salt-call
+SALT_LINT ?= salt-lint
+YAMLLINT ?= yamllint
+STATE_ROOT ?= .
+PILLAR_ROOT ?= ./pillar
+MINION_ID ?= t-mailgw-as-02
+
+.PHONY: lint check apply verify
+
+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
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/.yamllint
+---
+extends: default
+
+ignore: |
+  .git/
+  .venv/
+  venv/
+
+rules:
+  comments-indentation: disable
+  document-start:
+    present: false
+  line-length:
+    max: 120
+    level: warning
+  truthy:
+    check-keys: false
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/.gitignore
@@
 # Python / virtualenv
 .venv/
 venv/
 __pycache__/
 *.pyc

-# Ansible runtime artifacts
-*.retry
-.ansible/
+# Salt runtime artifacts
+.salt/

 # IDE / OS files
 .idea/
 .vscode/
 .DS_Store
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/README.md
+# Salt baseline для PMG
+
+Репозиторий содержит автономный набор Salt states для базовой эксплуатации и hardening Proxmox Mail Gateway.
+Подход безопасный: без изменения сетевого периметра и без вмешательства в почтовый поток по дизайн-ограничениям.
+
+## Что внутри
+
+- `top.sls` — привязка состояний к minion ID.
+- `pillar/pmg.sls` — переменные и параметры PMG.
+- `pmg/*.sls` — состояния:
+  - `common` (базовые пакеты и timezone),
+  - `ssh_hardening`,
+  - `auto_updates` (security-only, без auto reboot),
+  - `audit_logging`,
+  - `pmg_baseline` (проверка/поддержание сервисов PMG),
+  - `verify` (проверки состояния).
+- `pmg/files/*` — шаблоны конфигов.
+
+## Требования
+
+- Salt (рекомендуется актуальная LTS-ветка).
+- Debian/PMG узлы с systemd.
+- root-доступ на узел.
+- Секреты и ключи хранятся в HashiCorp Vault, не в git.
+
+## Быстрый запуск
+
+```bash
+make lint
+make check
+make apply
+make verify
+```
+
+По умолчанию `MINION_ID=t-mailgw-as-02`. Можно переопределить:
+
+```bash
+make check MINION_ID=t-mailgw-as-01
+```
+
+## Основные переменные
+
+Все параметры задаются в `pillar/pmg.sls`:
+
+- `pmg.base_packages`
+- `pmg.timezone`
+- `pmg.ssh_allow_users`
+- `pmg.smtp_submission_enabled`
+- `pmg.journald_system_max_use`
+- `pmg.journald_max_file_sec`
+- `pmg.pmg_services`
+- `pmg.manage_hosts_file`
+- `pmg.hosts_file_entries`
+
+## Ограничения и принципы
+
+- Хостовый firewall намеренно не управляется из этого набора состояний.
+- Внешний SMTP-поток остается в текущей схеме (MX + периметр/NAT).
+- Управляющий доступ ограничивается сетевым контуром PAM + Bastion вне этого репозитория.
*** End Patch


Shell

Run salt and YAML lint commands


ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/.yamllint
@@
----
 extends: default
*** End Patch


Shell

Re-run lint after yamllint fix


Готово, полностью перевел репозиторий в автономный SaltStack-юнит и убрал firewall-управление.

Что сделал:

- Удалил всю старую Ansible-структуру (`playbooks`, `roles`, `inventories`, `ansible.cfg`, `requirements.yml`, `.ansible-lint`, пустые `.gitkeep`).
- Собрал Salt-структуру:
  - `top.sls`
  - `pillar/top.sls`
  - `pillar/pmg.sls`
  - `pmg/init.sls`
  - `pmg/common.sls`
  - `pmg/ssh_hardening.sls`
  - `pmg/auto_updates.sls`
  - `pmg/audit_logging.sls`
  - `pmg/pmg_baseline.sls`
  - `pmg/verify.sls`
  - `pmg/files/*.jinja` (шаблоны конфигов)
- Обновил `Makefile` под `salt-call --local` (`check/apply/verify/lint`).
- Полностью переписал `README.md` на русский под Salt (без упоминаний Ansible).
- Обновил `.gitignore` и `.yamllint` под новый стек.

По вашей новой политике:

- Firewall **вообще не управляется** из этого кода (убран полностью).
- Упор только на baseline/hardening уровня хоста и проверку PMG-сервисов.
- Указано, что сетевой доступ и ограничения делаются внешним контуром (PAM + Bastion / периметр).

Проверил:

- `make lint` — проходит (в окружении нет `salt-lint`, поэтому он корректно пропускается).
- IDE lint ошибок нет.

Если хотите, следующим шагом прогоню `make check` на текущем хосте (`salt-call --local`) и покажу сухой результат по каждому state.


<timestamp>Tuesday, Jun 2, 2026, 12:07 PM (UTC+3)</timestamp>
<user_query>
для начала, ты удалил не все файлы от ансибла
потом, сходи на оба хоста и переименуй их, остались старые имена от того, что с старых тачек просто перенесли снапшоты на новые

Григорий Наумов
57 минут назад

@Руслан Петрович, имена для машин
10.80.38.152     p-mailgw-x1-01.lmru.tech
10.220.41.152   p-mailgw-x2-01.lmru.tech
</user_query>


Принял, делаю в два шага: сначала дочищу остатки Ansible в репозитории, затем аккуратно переименую обе ноды и проверю, что hostname применился. Начну с инвентаризации оставшихся файлов/упоминаний.


ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "hostnamectl --static"

Get current static hostname on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "hostnamectl --static"

Get current static hostname on second host

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "hostname"

Get current runtime hostname first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "hostname"

Get current runtime hostname second host


Сейчас переименую обе машины в новые FQDN и сразу приведу `/etc/hosts`, чтобы не осталось старых имен после снапшота. Затем дам верификацию по каждой ноде.

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "hostnamectl set-hostname p-mailgw-x1-01.lmru.tech"

Set new hostname on first PMG host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "hostnamectl set-hostname p-mailgw-x2-01.lmru.tech"

Set new hostname on second PMG host


ansible -i "10.80.38.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/hosts regexp=^10\.80\.38\.152\s+ line='10.80.38.152 p-mailgw-x1-01.lmru.tech p-mailgw-x1-01' state=present backup=yes"

Update hosts file IP mapping on first host

ansible -i "10.80.38.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/hosts regexp=^127\.0\.1\.1\s+ line='127.0.1.1 p-mailgw-x1-01.lmru.tech p-mailgw-x1-01' state=present backup=yes"

Update loopback hostname mapping on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/hosts regexp=^10\.220\.41\.152\s+ line='10.220.41.152 p-mailgw-x2-01.lmru.tech p-mailgw-x2-01' state=present backup=yes"

Update hosts file IP mapping on second host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/hosts regexp=^127\.0\.1\.1\s+ line='127.0.1.1 p-mailgw-x2-01.lmru.tech p-mailgw-x2-01' state=present backup=yes"

Update loopback hostname mapping on second host


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; hostname -f; awk '/^(127\.0\.1\.1|10\.80\.38\.152)/ {print}' /etc/hosts"

Verify first host new hostname and hosts entries

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; hostname -f; awk '/^(127\.0\.1\.1|10\.220\.41\.152)/ {print}' /etc/hosts"

Verify second host new hostname and hosts entries

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Check PMG cluster state after first rename

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Check PMG cluster state after second rename


Обновление по факту выполнения:

- Остатков Ansible в рабочем дереве уже нет (по файлам и по контенту проверил).
- Хосты переименованы:
  - `10.80.38.152` -> `p-mailgw-x1-01.lmru.tech`
  - `10.220.41.152` -> `p-mailgw-x2-01.lmru.tech`
- `/etc/hosts` на обеих нодах тоже обновил под новые имена.

⚠ Критично: после переименования `pmgcm status` на обеих нодах теперь возвращает:

- `local node 'p-mailgw-x1-01' not part of cluster`
- `local node 'p-mailgw-x2-01' not part of cluster`

То есть кластерная привязка в PMG была завязана на старые node names, и после смены hostname узлы больше не считаются частью кластера.

Как действуем дальше? Нужно ваше решение перед следующими шагами:

1) **Срочно откатить hostname обратно** на обеих нодах (самый быстрый стабилизирующий вариант),
2) **Оставить новые hostname** и делать корректную процедуру переоформления/реинтеграции PMG-кластера под новыми именами.


<timestamp>Tuesday, Jun 2, 2026, 12:09 PM (UTC+3)</timestamp>
<user_query>
Оставить новые hostname и делать корректную процедуру переоформления/реинтеграции PMG-кластера под новыми именами.
</user_query>


Принято, тогда делаю реинтеграцию под новыми именами. Сначала сниму текущее состояние кластера и доступные `pmgcm` команды, чтобы выполнить корректную штатную процедуру без риска для почтового потока.

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "pmgcm help || pmgcm --help || true"

Get pmgcm command help on first node

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "pmgcm help || pmgcm --help || true"

Get pmgcm command help on second node

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Check cluster status on first node

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Check cluster status on second node

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "grep -n '.*' /etc/hosts"

Inspect hosts file entries on first node

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "grep -n '.*' /etc/hosts"

Inspect hosts file entries on second node


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "ls -l /etc/pmg && sed -n '1,200p' /etc/pmg/cluster.conf"

Inspect PMG config and cluster file first node

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "ls -l /etc/pmg && sed -n '1,200p' /etc/pmg/cluster.conf"

Inspect PMG config and cluster file second node

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgcm status --list_single_node 1"

Check single-node cluster view first node

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgcm status --list_single_node 1"

Check single-node cluster view second node


Нашел причину: в `/etc/pmg/cluster.conf` на обеих нодах остались старые `name t-mailgw-*`, поэтому `pmgcm` не узнает локальные hostname. Сейчас аккуратно обновлю cluster-конфиг и peer-записи в `/etc/hosts`, затем проверю `pmgcm status`.

ansible -i "10.80.38.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/hosts regexp=^10\.220\.41\.152\s+ line='10.220.41.152 p-mailgw-x2-01.lmru.tech p-mailgw-x2-01' state=present backup=yes"

Update peer hostname mapping on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/hosts regexp=^10\.80\.38\.152\s+ line='10.80.38.152 p-mailgw-x1-01.lmru.tech p-mailgw-x1-01' state=present backup=yes"

Update peer hostname mapping on second host

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "cp -a /etc/pmg/cluster.conf /etc/pmg/cluster.conf.bak.$(date +%Y%m%d%H%M%S)"

Backup PMG cluster config first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "cp -a /etc/pmg/cluster.conf /etc/pmg/cluster.conf.bak.$(date +%Y%m%d%H%M%S)"

Backup PMG cluster config second host


ansible -i "10.80.38.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/pmg/cluster.conf regexp=^\s*name\s+t-mailgw-as-01$ line='\tname p-mailgw-x1-01' state=present"

Update first node name in cluster config

ansible -i "10.80.38.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/pmg/cluster.conf regexp=^\s*name\s+t-mailgw-as-02$ line='\tname p-mailgw-x2-01' state=present"

Update second node name in first config copy

ansible -i "10.220.41.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/pmg/cluster.conf regexp=^\s*name\s+t-mailgw-as-01$ line='\tname p-mailgw-x1-01' state=present"

Update first node name in second config copy

ansible -i "10.220.41.152," all -u root -m ansible.builtin.lineinfile -a "path=/etc/pmg/cluster.conf regexp=^\s*name\s+t-mailgw-as-02$ line='\tname p-mailgw-x2-01' state=present"

Update second node name in cluster config


ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Check cluster status on first host after config update

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Check cluster status on second host after config update

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgcm sync --master_ip 10.80.38.152"

Sync cluster configuration from master IP

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgcm update-fingerprints"

Refresh node certificate fingerprints on master


ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "systemctl is-active postfix pmg-smtp-filter pmgpolicy pmgdaemon pmgmirror pmgtunnel"

Verify PMG services active first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "systemctl is-active postfix pmg-smtp-filter pmgpolicy pmgdaemon pmgmirror pmgtunnel"

Verify PMG services active second host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgcm sync --master_ip 10.80.38.152"

Force sync from second node to cluster master

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Recheck cluster state on first node

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Recheck cluster state on second node


Сделал реинтеграцию на хостах и она уже рабочая; дополнительно сейчас синхронизирую Salt-код под новые имена (в `top.sls` и `pillar`), чтобы дальше не было рассинхрона с уже переименованными нодами.


ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/top.sls
@@
 base:
-  't-mailgw-as-*':
+  'p-mailgw-x*-01*':
     - pmg.init
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/pmg.sls
@@
   hosts_file_entries:
     - ip: 10.80.38.152
-      hostname: t-mailgw-as-01
+      hostname: p-mailgw-x1-01
       aliases:
-        - t-mailgw-as-01.local
+        - p-mailgw-x1-01.lmru.tech
     - ip: 10.220.41.152
-      hostname: t-mailgw-as-02
+      hostname: p-mailgw-x2-01
       aliases:
-        - t-mailgw-as-02.local
+        - p-mailgw-x2-01.lmru.tech
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/Makefile
@@
-MINION_ID ?= t-mailgw-as-02
+MINION_ID ?= p-mailgw-x2-01.lmru.tech
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/README.md
@@
-По умолчанию `MINION_ID=t-mailgw-as-02`. Можно переопределить:
+По умолчанию `MINION_ID=p-mailgw-x2-01.lmru.tech`. Можно переопределить:
@@
-make check MINION_ID=t-mailgw-as-01
+make check MINION_ID=p-mailgw-x1-01.lmru.tech
*** End Patch


Shell

Run lint after hostname updates

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; pmgcm status"

Final verification first host hostname and cluster

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; pmgcm status"

Final verification second host hostname and cluster


Сделано.

### 1) По репозиторию: Ansible-остатков нет

Проверил по дереву и по содержимому: в рабочих файлах нет упоминаний `ansible`, `inventory`, `playbook`, `role` (кроме `.git/config` URL репозитория).
Дополнительно синхронизировал код под новые имена хостов:

- `top.sls` -> таргет теперь `p-mailgw-x*-01*`
- `pillar/pmg.sls` -> `hosts_file_entries` обновлены на:
  - `p-mailgw-x1-01(.lmru.tech)`
  - `p-mailgw-x2-01(.lmru.tech)`
- `Makefile` -> `MINION_ID` по умолчанию `p-mailgw-x2-01.lmru.tech`
- `README.md` -> примеры `MINION_ID` обновлены

---

### 2) По хостам: переименование + реинтеграция кластера выполнены

Сделал на обеих нодах:

- hostname:
  - `10.80.38.152` -> `p-mailgw-x1-01.lmru.tech`
  - `10.220.41.152` -> `p-mailgw-x2-01.lmru.tech`
- обновил `/etc/hosts` (локальные и peer-записи)
- сделал backup `/etc/pmg/cluster.conf` на обеих нодах
- обновил в `cluster.conf` имена нод:
  - `t-mailgw-as-01` -> `p-mailgw-x1-01`
  - `t-mailgw-as-02` -> `p-mailgw-x2-01`
- выполнил `pmgcm sync --master_ip 10.80.38.152`

Проверка сейчас:

- hostname на обеих нодах — новые FQDN, корректно.
- `pmgcm status` на обеих нодах — кластер снова видит обе ноды.
- PMG-сервисы на обеих нодах `active` (`postfix`, `pmg-smtp-filter`, `pmgpolicy`, `pmgdaemon`, `pmgmirror`, `pmgtunnel`).

Примечание по `pmgcm status`: `A/S` в выводе зеркалится в зависимости от того, с какой ноды смотрите (локальная чаще `A`, удаленная `S`) — это ожидаемо.


<timestamp>Tuesday, Jun 2, 2026, 12:14 PM (UTC+3)</timestamp>
<user_query>
http://p-mailgw-x2-01.lmru.tech:8006/
ввел в браузере, ERR_NAME_NOT_RESOLVED
</user_query>


Это чисто DNS-резолв: сейчас быстро проверю, резолвится ли имя у вас локально и на самих PMG-нодах, чтобы понять где именно отсутствует запись (локальный hosts / корпоративный DNS).

getent hosts p-mailgw-x2-01.lmru.tech || true

Check local DNS resolution for PMG hostname

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "getent hosts p-mailgw-x2-01.lmru.tech"

Check hostname resolution on second PMG node

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "getent hosts p-mailgw-x2-01.lmru.tech"

Check hostname resolution on first PMG node

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "ss -ltn sport = :8006"

Verify PMG web port 8006 is listening


Test direct HTTPS connectivity to PMG GUI IP


Да, проблема именно в DNS, не в PMG.

Что проверил:

- У вас локально `getent hosts p-mailgw-x2-01.lmru.tech` ничего не вернуло — имя не резолвится.
- На самих PMG-узлах имя резолвится корректно в `10.220.41.152`.
- Порт `8006` на `10.220.41.152` слушает.
- Прямой доступ по IP живой (`https://10.220.41.152:8006` отвечает).

Что делать:

1. Временно (на вашем рабочем ПК) добавить в `/etc/hosts`:
   - `10.220.41.152 p-mailgw-x2-01.lmru.tech p-mailgw-x2-01`
   - `10.80.38.152 p-mailgw-x1-01.lmru.tech p-mailgw-x1-01`
2. Или правильно — завести A-записи в корпоративном DNS.
3. После этого URL `http://...:8006` лучше открыть как `https://p-mailgw-x2-01.lmru.tech:8006/`.

Если хотите, дам готовую команду под ваш Manjaro для быстрого добавления в `/etc/hosts`.


<timestamp>Tuesday, Jun 2, 2026, 12:16 PM (UTC+3)</timestamp>
<user_query>
@/home/ruslanpi/.cursor/projects/home-ruslanpi-Documents-repos-lmru-devsecops-ansible/terminals/1.txt:496-519
</user_query>


Да, всё подтверждается: в вашем DNS сейчас `NXDOMAIN` для `p-mailgw-x2-01.lmru.tech`.

Ключевые строки из вывода:

- `status: NXDOMAIN`
- `ANSWER: 0`
- authoritative SOA от `lmru.tech`

То есть имя не заведено в DNS-зоне, поэтому браузер и дает `ERR_NAME_NOT_RESOLVED`.

Практически:

- временно: добавить запись в локальный `/etc/hosts`
- правильно: попросить DNS-админов создать A-записи:
  - `p-mailgw-x1-01.lmru.tech -> 10.80.38.152`
  - `p-mailgw-x2-01.lmru.tech -> 10.220.41.152`

Могу сразу дать готовый короткий текст заявки в DNS.


<timestamp>Tuesday, Jun 2, 2026, 12:36 PM (UTC+3)</timestamp>
<user_query>

Руслан Петрович
6 минут назад
я переименовал
но А записей таких нет

Григорий Наумов
2 минуты назад

добавил, 5 минут на репликацию
</user_query>


Отлично, это ровно то, что нужно.

Через ~5 минут проверьте:

```bash
dig +short p-mailgw-x1-01.lmru.tech
dig +short p-mailgw-x2-01.lmru.tech
```

Ожидаемо должны вернуться:

- `10.80.38.152`
- `10.220.41.152`

И потом сразу:

```bash
curl -kI https://p-mailgw-x2-01.lmru.tech:8006/
```

Если DNS уже разошелся, браузерный `ERR_NAME_NOT_RESOLVED` уйдет.
Если не уйдет, обычно помогает подождать TTL и перезапустить браузер/очистить DNS-кэш ОС (`resolvectl flush-caches`).


<timestamp>Tuesday, Jun 2, 2026, 12:43 PM (UTC+3)</timestamp>
<user_query>
@/home/ruslanpi/.cursor/projects/home-ruslanpi-Documents-repos-lmru-devsecops-ansible/terminals/1.txt:544-548
</user_query>


Отлично, DNS уже разошелся корректно:

- `p-mailgw-x1-01.lmru.tech -> 10.80.38.152`
- `p-mailgw-x2-01.lmru.tech -> 10.220.41.152`

Значит по именам всё в порядке.
Дальше можно открывать GUI по HTTPS:

- [https://p-mailgw-x1-01.lmru.tech:8006/](https://p-mailgw-x1-01.lmru.tech:8006/)
- [https://p-mailgw-x2-01.lmru.tech:8006/](https://p-mailgw-x2-01.lmru.tech:8006/)

Если вдруг браузер еще держит старый кэш/сессию — просто новый инкогнито таб или перезапуск браузера.


[Image]
<image_files>
The following images were provdied by the user and saved to the workspace for future use:
1. /home/ruslanpi/.cursor/projects/home-ruslanpi-Documents-repos-lmru-devsecops-ansible/assets/image-45f856cb-725d-49d3-ad97-019ee48416cf.png

These images can be copied for use in other locations.
</image_files>
<timestamp>Tuesday, Jun 2, 2026, 12:45 PM (UTC+3)</timestamp>
<user_query>
во вкладках старое имя
</user_query>


Да, вижу — это уже не DNS, а внутреннее отображение имени в PMG UI. Сейчас найду, откуда именно оно подхватывается (конфиг/кэш/статистика), и дам точечный фикс без риска для почты.

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; cat /etc/hostname; cat /etc/mailname; grep -n '^hostname\|^domain' /etc/pmg/pmg.conf || true"

Check hostname-related PMG identity files node1

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; cat /etc/hostname; cat /etc/mailname; grep -n '^hostname\|^domain' /etc/pmg/pmg.conf || true"

Check hostname-related PMG identity files node2

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "rg -n 't-mailgw-as|p-mailgw-x' /etc/pmg /var/lib/pmg /etc/postfix 2>/dev/null | sed -n '1,120p'"

Find old hostname references on node1

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "rg -n 't-mailgw-as|p-mailgw-x' /etc/pmg /var/lib/pmg /etc/postfix 2>/dev/null | sed -n '1,120p'"

Find old hostname references on node2


ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgsh get /nodes"

Check PMG API node names on node1

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgsh get /nodes"

Check PMG API node names on node2

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "systemctl is-active pmgproxy pmgdaemon pmgmirror"

Confirm PMG web-related services active node1

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "systemctl is-active pmgproxy pmgdaemon pmgmirror"

Confirm PMG web-related services active node2


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "ls -1 /var/lib/rrdcached/db 2>/dev/null | sed -n '1,120p'; ls -1 /var/lib/pmg 2>/dev/null | sed -n '1,120p'"

Inspect PMG RRD data directories on node1

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "rg -n 't-mailgw-as|p-mailgw-x' /var/lib/rrdcached /var/lib/pmg 2>/dev/null | sed -n '1,120p'"

Search old/new hostnames in PMG data on node1


Да, это похоже на кэш веб-интерфейса, не на реальное состояние кластера.

Что уже проверено на серверах:
- `pmgcm status` показывает новые имена (`p-mailgw-x1-01`, `p-mailgw-x2-01`).
- `pmgsh get /nodes` тоже возвращает только новые имена.

То есть backend уже переехал, «старое имя во вкладке» — фронт/браузер.

Что сделать:
1. Полный hard refresh страницы (`Ctrl+Shift+R`).
2. Выйти из PMG и зайти снова.
3. Открыть в инкогнито (быстрая проверка без кэша).
4. Если в обычном окне всё равно старое — удалить site data для `p-mailgw-x1-01.lmru.tech:8006` и `p-mailgw-x2-01.lmru.tech:8006`.

Если после этого останется старое отображение, скажите — перезапущу только web-слой PMG (`pmgproxy`/`pmgdaemon`) без влияния на SMTP-трафик.


<timestamp>Tuesday, Jun 2, 2026, 12:48 PM (UTC+3)</timestamp>
<user_query>
в инкогнито тоже старое имя, почта на них не летит, можно все ребутать)
</user_query>


Принято, тогда перезагружаю обе ноды сейчас и после возврата сразу проверю hostname, кластер и ключевые PMG-сервисы.

ansible -i "10.80.38.152," all -u root -m ansible.builtin.reboot -a "reboot_timeout=900 test_command='hostnamectl --static'"

Reboot first PMG host and wait online

ansible -i "10.220.41.152," all -u root -m ansible.builtin.reboot -a "reboot_timeout=900 test_command='hostnamectl --static'"

Reboot second PMG host and wait online


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; pmgcm status; systemctl is-active postfix pmg-smtp-filter pmgpolicy pmgdaemon pmgmirror pmgtunnel"

Verify first host name cluster and PMG services

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "hostnamectl --static; pmgcm status; systemctl is-active postfix pmg-smtp-filter pmgpolicy pmgdaemon pmgmirror pmgtunnel"

Verify second host name cluster and PMG services

ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgsh get /nodes"

Check node names via PMG API after reboot


ansible -i "10.80.38.152," all -u root -m ansible.builtin.command -a "pmgsh get /config/admin"

Inspect PMG admin config values node1

ansible -i "10.220.41.152," all -u root -m ansible.builtin.command -a "pmgsh get /config/admin"

Inspect PMG admin config values node2


Сделал полный ребут обеих нод.

Результат после перезапуска:

- `10.80.38.152` -> `p-mailgw-x1-01.lmru.tech`
- `10.220.41.152` -> `p-mailgw-x2-01.lmru.tech`
- `pmgcm status` на обеих нодах показывает новые имена нод.
- `pmgsh get /nodes` возвращает только:
  - `p-mailgw-x1-01`
  - `p-mailgw-x2-01`
- Критичные сервисы PMG на обеих нодах `active`:
  - `postfix`, `pmg-smtp-filter`, `pmgpolicy`, `pmgdaemon`, `pmgmirror`, `pmgtunnel`.

То есть на серверной стороне старых имен уже нет в рабочем состоянии.

Если во вкладке браузера всё еще старое имя, это уже фронт-кеш/сессия вкладки:
- закройте именно эту вкладку и откройте URL заново;
- либо удалите site data для `https://p-mailgw-x1-01.lmru.tech:8006` и `https://p-mailgw-x2-01.lmru.tech:8006`;
- потом зайдите снова.

Если хотите, могу еще проверить конкретный API-эндпоинт dashboard, который отдает label в шапку UI, чтобы добить до 100% понимания.


<timestamp>Tuesday, Jun 2, 2026, 12:54 PM (UTC+3)</timestamp>
<user_query>
так, удалил таймзон
проверь теперь все стейты, все ли правильно, все ли реально нужно, прогони драй ран
</user_query>


Принял. Сейчас сделаю полный ревью текущих Salt states (после удаления timezone), отмечу лишнее/риски и прогоню реальный dry-run на обеих PMG-нодах. Сначала читаю актуальные файлы, потом запускаю проверку на хостах.


Сейчас прогоню именно «реальный» dry-run на обеих нодах: упакую текущие states, закину на хосты и выполню `salt-call --local ... test=True` с соответствующим `minion id`. Это даст честную проверку на целевых PMG, а не на локальной машине.

Run salt/yaml lint before dry-run

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "salt-call --version"

Check salt-call installed on both PMG hosts


Check local salt-call availability


Check salt-ssh availability for agentless dry-run


salt-ssh --help | rg "file-root|pillar-root|state-root|roster-file|ssh-run"

Inspect salt-ssh options for roots and roster


ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/pmg.sls
@@
-  smtp_submission_enabled: false
-
   journald_system_max_use: 1G
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/README.md
@@
 - `pmg.base_packages`
 - `pmg.ssh_allow_users`
-- `pmg.smtp_submission_enabled`
 - `pmg.journald_system_max_use`
*** End Patch


Shell

Run lint after state cleanup

salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Mock dry-run compile for node x1 state init

salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Mock dry-run compile for node x2 state init

salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.verify test=True mock=True

Mock dry-run compile for verify state


sudo salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Run mock dry-run with sudo for node x1

sudo salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Run mock dry-run with sudo for node x2

sudo salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.verify test=True mock=True

Run mock dry-run verify with sudo


salt-call --help | rg "config-dir|cachedir|pki|log-file|root-dir"

Find salt-call flags for writable local config


mkdir -p /tmp/saltcfg-agent && salt-call --local --config-dir=/tmp/saltcfg-agent --cachedir=/tmp/saltcfg-agent/cache --log-file=/tmp/saltcfg-agent/salt-call.log --id p-mailgw-x1-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Run non-root mock dry-run with custom salt config


ApplyPatch

Input:
*** Begin Patch
*** Add File: /tmp/saltcfg-agent/minion
+root_dir: /tmp/saltcfg-agent
+cachedir: /tmp/saltcfg-agent/cache
+pki_dir: /tmp/saltcfg-agent/pki
+sock_dir: /tmp/saltcfg-agent/run/minion
+log_file: /tmp/saltcfg-agent/salt-call.log
+id: local-check
*** End Patch


Нашел способ обойти `sudo`: запускаю `salt-call` с отдельным writable config-dir в `/tmp`, чтобы сделать mock dry-run без прав root. Сейчас прогоню `init` и `verify`.

Shell

salt-call --local --config-dir=/tmp/saltcfg-agent --id p-mailgw-x1-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Run mock dry-run init for node x1 with tmp config

salt-call --local --config-dir=/tmp/saltcfg-agent --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Run mock dry-run init for node x2 with tmp config

salt-call --local --config-dir=/tmp/saltcfg-agent --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.verify test=True mock=True

Run mock dry-run verify for node x2 with tmp config


salt-call --local --config-dir=/tmp/saltcfg-agent --id p-mailgw-x1-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Run mock dry-run init for x1 sequentially


salt-call --local --config-dir=/tmp/saltcfg-agent --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.init test=True mock=True

Run mock dry-run init for x2 sequentially

salt-call --local --config-dir=/tmp/saltcfg-agent --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.verify test=True mock=True

Run mock dry-run verify for x2 sequentially


salt-call --local --config-dir=/tmp/saltcfg-agent --id p-mailgw-x2-01.lmru.tech --file-root="$(pwd)" --pillar-root="$(pwd)/pillar" state.apply pmg.verify test=True mock=True

Run mock dry-run verify sequentially


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "systemctl is-active postfix pmg-smtp-filter pmgpolicy pmgdaemon pmgmirror pmgtunnel"

Validate critical PMG services are active

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "sshd -t"

Validate SSH daemon config on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "pmgcm status"

Check PMG cluster status on both hosts


Show repository changes after review


Проверил после удаления timezone — в целом всё ок.

Что проверил и что поправил:

- Удаление timezone не ломает states: `pmg/common.sls` теперь чисто про базовые пакеты — это корректно.
- Нашел и убрал лишнее:
  - из `pillar/pmg.sls` удалил `pmg.smtp_submission_enabled` (переменная больше нигде не используется),
  - из `README.md` убрал этот параметр из списка.
- Линт:
  - `make lint` проходит (с `salt-lint` fallback “не найден, пропускаю”).

Dry-run:

- На самих PMG-хостах `salt-call` не установлен, поэтому **прямой dry-run на узлах Salt-ом** сейчас невозможен.
- Прогнал `salt-call --local ... test=True mock=True` локально с вашими `state/pillar roots`:
  - `state.apply pmg.init` для `p-mailgw-x1-01.lmru.tech` — OK
  - `state.apply pmg.init` для `p-mailgw-x2-01.lmru.tech` — OK
  - `state.apply pmg.verify` — OK
  - компиляция/граф реквизитов валидные.

Проверка «по факту на продовых узлах» (через ad-hoc команды):

- `pmgcm status` — кластер видит новые имена и обе ноды.
- `sshd -t` — OK на обеих.
- Критичные PMG-сервисы `active` на обеих:
  - `postfix`, `pmg-smtp-filter`, `pmgpolicy`, `pmgdaemon`, `pmgmirror`, `pmgtunnel`.

Итог:
- States сейчас корректные и нужны для вашего кейса.
- Лишнего после удаления timezone не осталось.
- Единственное ограничение для «настоящего Salt dry-run на узлах» — отсутствие `salt-call` на самих PMG (это нормально, если пока только подготовка к передаче команде Salt).


<timestamp>Tuesday, Jun 2, 2026, 1:00 PM (UTC+3)</timestamp>
<user_query>
надо установить на хосты все, что нужно, для продовой работы солта
</user_query>


Ок, ставлю на обе PMG-ноды базовый production-набор для Salt minion: пакет `salt-minion`, автозапуск сервиса и проверку `salt-call --local test.ping`. После установки дам, что еще нужно от команды Salt (только адрес master/pki).

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.apt -a "update_cache=true name=salt-minion state=present"

Install salt-minion package on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.systemd_service -a "name=salt-minion enabled=true state=started"

Enable and start salt-minion service on hosts


Пакета `salt-minion` в текущих APT-репозиториях PMG нет, поэтому сервис не найден. Сейчас определю версию Debian/список репо на узлах и добавлю корректный репозиторий Salt для установки minion.

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "source /etc/os-release && echo ID=$ID VERSION_ID=$VERSION_ID VERSION_CODENAME=$VERSION_CODENAME"

Read Debian release metadata on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "apt-cache policy salt-minion || true; apt-cache search '^salt-' | sed -n '1,20p'"

Check salt packages in current apt repos

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "ls /etc/apt/sources.list.d && sed -n '1,200p' /etc/apt/sources.list"

Inspect configured apt source lists


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "curl -fsSL https://bootstrap.saltproject.io -o /tmp/install_salt.sh && sh /tmp/install_salt.sh -P -x python3 stable"

Install Salt via official bootstrap on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.systemd_service -a "name=salt-minion enabled=true state=started"

Ensure salt-minion service enabled and running


Check bootstrap URL response headers


WebSearch

Salt Project install Debian 12 salt-minion packages.broadcom.com apt repository

Explanation: Need current official Salt apt repository URL for package install


Shell

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.apt -a "update_cache=true name=gnupg state=present"

Install gnupg prerequisite on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "mkdir -p /etc/apt/keyrings && curl -fsSL https://packages.broadcom.com/artifactory/api/security/keypair/SaltProjectKey/public | gpg --dearmor -o /etc/apt/keyrings/salt-archive-keyring.pgp"

Install Salt repository keyring on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.get_url -a "url=https://github.com/saltstack/salt-install-guide/releases/latest/download/salt.sources dest=/etc/apt/sources.list.d/salt.sources mode=0644"

Install Salt apt source file on both hosts


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.apt -a "update_cache=true"

Refresh apt metadata after adding Salt repo

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.apt -a "name=salt-minion state=present"

Install salt-minion from new repository

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.systemd_service -a "name=salt-minion enabled=true state=started"

Enable and start salt-minion service


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "sed -n '1,200p' /etc/apt/sources.list.d/salt.sources"

Inspect installed salt.sources file

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "apt-cache policy | sed -n '1,260p' | rg -n 'saltproject|broadcom|packages.broadcom|repo.salt' || true"

Check apt policy contains salt repository

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "apt-cache search '^salt' | sed -n '1,80p'"

List salt-related package names after repo add


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.apt -a "name=salt-minion state=present update_cache=true"

Install salt-minion package after repository sync

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.systemd_service -a "name=salt-minion enabled=true state=started"

Enable and start installed salt-minion service

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "salt-call --version"

Verify salt-call version on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "salt-call --local test.ping"

Verify local salt-call execution on both hosts


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -l | awk '$2 ~ /^salt-(minion|common)$/ {print $1,$2,$3}'; dpkg -L salt-minion | sed -n '1,80p'; ls -l /usr/bin/salt* /opt/saltstack/salt/bin/salt* 2>/dev/null"

Inspect installed Salt files and binary paths

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "systemctl list-unit-files | sed -n '1,400p' | awk '/salt/ {print}'"

List salt-related systemd units on hosts


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -l salt-minion salt-common | sed -n '1,20p'"

Check salt package installation status

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -L salt-minion | sed -n '1,120p'"

List files installed by salt-minion package

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "command -v salt-call || true; command -v salt-minion || true; ls -l /usr/bin/salt* 2>/dev/null"

Locate salt executable binaries on hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "systemctl is-active salt-minion"

Check salt-minion service active state


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --version && salt-call --local test.ping"

Verify salt-call and local test.ping on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local config.get id; salt-call --local config.get master"

Show minion id and master settings on hosts


Готово, установил всё необходимое для Salt на **обе PMG-ноды** (`10.80.38.152`, `10.220.41.152`).

Сделано:

- Подключил официальный DEB-репозиторий Salt (новый `packages.broadcom.com`):
  - ключ: `/etc/apt/keyrings/salt-archive-keyring.pgp`
  - source: `/etc/apt/sources.list.d/salt.sources`
- Установил пакеты:
  - `salt-minion` `3008.0`
  - `salt-common` и зависимости
- Проверил бинарники:
  - `salt-call` доступен
  - `salt-minion` доступен
- Проверил сервис:
  - `salt-minion.service` — `enabled`, `active` на обеих нодах
- Проверил локальный вызов:
  - `salt-call --local test.ping` -> `True` на обеих нодах
- Проверил id:
  - `p-mailgw-x1-01.lmru.tech`
  - `p-mailgw-x2-01.lmru.tech`

Что осталось для полноценной prod-интеграции с Salt master:

- Сейчас `master` в minion-конфиге = `salt` (дефолт).
- Нужен фактический адрес Salt master от команды эксплуатации, после чего:
  - прописать `master` в `/etc/salt/minion` (или `minion.d/*.conf`),
  - перезапустить `salt-minion`,
  - принять ключи на master.

Если дадите адрес master, сразу докручу конфиг на обеих нодах и проверю коннект end-to-end.


<timestamp>Tuesday, Jun 2, 2026, 1:06 PM (UTC+3)</timestamp>
<user_query>
мастер потом сами настроят, проверь драй ран
</user_query>


Ок, делаю именно боевой dry-run `test=True` на самих нодах. Для этого сначала закину текущий Salt-пакет репозитория на обе машины, затем запущу `state.apply pmg.init` и `pmg.verify` локально на каждой ноде.

List repo root before creating salt bundle


tar -czf /tmp/pmg-salt-bundle.tar.gz Makefile README.md top.sls pillar pmg && ls -lh /tmp/pmg-salt-bundle.tar.gz

Create archive with Salt states and pillar


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.file -a "path=/root/salt-pmg state=directory mode=0755"

Create salt working directory on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.copy -a "src=/tmp/pmg-salt-bundle.tar.gz dest=/root/pmg-salt-bundle.tar.gz mode=0644"

Copy salt bundle archive to both hosts


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.unarchive -a "src=/root/pmg-salt-bundle.tar.gz dest=/root/salt-pmg remote_src=true"

Extract Salt bundle into working directory on hosts


Архив закинут, запускаю `test=True` на обеих нодах: сначала `pmg.init`, потом `pmg.verify`, и соберу короткий итог по ошибкам/планируемым изменениям.

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.init test=True"

Run real dry-run of pmg.init on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.init test=True"

Run real dry-run of pmg.init on second host


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.verify test=True"

Run real dry-run of pmg.verify on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.verify test=True"

Run real dry-run of pmg.verify on second host


Прогнал dry-run на **обеих** нодах, как просили — реальный `salt-call --local ... test=True` на самих PMG-хостах.

### Что сделал

- Развернул текущий bundle states/pillar на обе ноды в `/root/salt-pmg`.
- Выполнил:
  - `state.apply pmg.init test=True`
  - `state.apply pmg.verify test=True`
- Для:
  - `p-mailgw-x1-01.lmru.tech`
  - `p-mailgw-x2-01.lmru.tech`

### Результат dry-run

- **Ошибок нет** (`Failed: 0`) на обеих нодах.
- `pmg.init`:
  - `Succeeded: 24`, `Failed: 0`
  - Планируемые изменения одинаковые на обеих:
    - установить пакеты: `jq`, `unattended-upgrades`, `auditd`
    - создать/обновить файлы:
      - `/etc/ssh/sshd_config.d/99-hardening.conf`
      - `/etc/apt/apt.conf.d/20auto-upgrades`
      - `/etc/apt/apt.conf.d/50unattended-upgrades`
      - `/etc/systemd/journald.conf.d/99-retention.conf`
      - `/etc/audit/rules.d/99-pmg-baseline.rules`
    - выполнить команды (по onchanges): `sshd -t`, `augenrules --load`, `pmgcm status`
- `pmg.verify`:
  - `Succeeded: 8`, `Failed: 0` на обеих
  - Проверки сервисов/кластера/`sshd -t` компилируются корректно.

### Вывод

- После удаления timezone states в порядке.
- Набор состояний рабочий и dry-run чистый.
- Можно безопасно идти в `apply`, когда дадите команду.


<timestamp>Tuesday, Jun 2, 2026, 1:09 PM (UTC+3)</timestamp>
<user_query>
ssh захарденим, а я зайти смогу потом?
</user_query>


Да, сможете, **если входите по SSH-ключу**.

С текущим `ssh_hardening` будет так:

- `PasswordAuthentication no` — вход по паролю отключится.
- `PermitRootLogin prohibit-password` — `root` по ключу останется рабочим.
- `PubkeyAuthentication yes` — ключевая авторизация включена.
- `AllowUsers` применяется только если `ssh_allow_users` не пустой. Сейчас у вас `[]`, значит по users не режем.

То есть риск только один: если у вас нет валидного ключа на хосте (или ключ не тот), тогда после применения не зайдете.

Как безопасно делать:
1. Держите **текущую root-сессию** открытой.
2. Перед apply проверьте вход новым SSH-подключением по ключу.
3. Применяйте сначала на одной ноде.
4. Сразу тестируйте повторный вход.

Если хотите, могу прямо сейчас сделать precheck по обеим нодам: проверить, что ваши текущие сессии реально идут по ключу и что конфиг после применения пройдет `sshd -t`.


<timestamp>Tuesday, Jun 2, 2026, 1:13 PM (UTC+3)</timestamp>
<user_query>
Установка wazuh-agent на Unix-подбные системы
Обновлено 19 марта 2026, 12:55

Установка wazuh-agent для централизованной отправки событий в SIEM
Для Debian/Ubuntu
1)Запуск команды в CLI

wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_4.14.0-1_amd64.deb && sudo WAZUH_MANAGER='10.80.120.205' WAZUH_AGENT_GROUP='test_auditd_unix' WAZUH_AGENT_NAME="$(hostname -f)" dpkg -i ./wazuh-agent_4.14.0-1_amd64.deb
Запуск агента и добавление его в автозагрузку
systemctl enable wazuh-agent
systemctl start wazuh-agent
systemctl status wazuh-agent

Для Red hat/CentOS
1)Запуск команды в CLI

curl -o wazuh-agent-4.14.0-1.x86_64.rpm https://packages.wazuh.com/4.x/yum/wazuh-agent-4.14.0-1.x86_64.rpm && sudo WAZUH_MANAGER='10.80.120.205' WAZUH_AGENT_GROUP='test_auditd_unix' WAZUH_AGENT_NAME="$(hostname -f)" rpm -ihv wazuh-agent-4.14.0-1.x86_64.rpm
2)Запуск агента и добавление его в автозагрузку

systemctl enable wazuh-agent
systemctl start wazuh-agent
systemctl status wazuh-agent

Пример Ansible task для установки агента централизовано:

 - name: install wazuh-agent
  shell: wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_4.14.0-1_amd64.deb && sudo WAZUH_MANAGER='10.80.120.205' WAZUH_AGENT_GROUP='test_auditd_unix' dpkg -i ./wazuh-agent_4.14.0-1_amd64.deb

- name: restart wazuh
  systemd:
    name: wazuh-agent
    state: restarted
    enabled: yes
    daemon_reload: yes
when:
  - ansible_os_family == 'Debian'
  - auditd_status.status.ActiveState is defined and auditd_status.status.ActiveState != "active"

Qualys agent
Обновлено 9 апреля 2026, 18:54

Агент Qualys - ПО устанавливаемое на активы инфраструктуры команд(серверы в датацентрах ЛМ и облачной инфраструктуре) для переодического проведения аудита безопасности систем на соответствие конфигурации, наличие уязвимостей инфраструктуры и ПО.

Сетевой доступ

Для работы агента необходим доступ по порту TCP/443 к сети 64.39.96.0/20 - ИСХОДЯЩИЙ трафик
Агент подключается к управляющему серверу, который находится в облаке с этим диапазоном адресов.

Значения url серверов:

  https://qagpublic.qg1.apps.qualys.eu - основной используется всегда по-умолчанию

  https://cask.qg1.apps.qualys.eu -* резерв для нештатных ситуаций

Для сканирования доступ к слушающим портам агента TCP/10001-10005 на самом хосте из сети 10.0.0.0/8 - ВХОДЯЩИЙ трафик
Сканеры безопасности размещены в нашей инфраструктуре и при проверках сопоставляют наличие агентов на слушающих портах 10001-10005

То есть сетевые правила доступа

Разрешающее правило

Источник

Назначение

Порты назначения

Комментарий

Исходящий трафик

ХОСТ

64.39.96.0/20

443/TCP

Разрешить исходящие подключения к управляющему серверу во вне в облаке

Входящий трафик

10.0.0.0/8

ХОСТ

10001-10005/TCP

Разрешить входящие подключения со сканеров безопасности внутри сети

Актуальный список поддерживаемых операционных систем -   https://success.qualys.com/customersupport/s/cloud-agent-pam

Linux
image.png

sudo rpm -ivh QualysCloudAgent.rpm
sudo /usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh ActivationId=5eb9ad8e-02cc-431a-9afb-b8caad175ec1 CustomerId=4d868109-dad3-5536-82b4-9f3a6f3010bc ServerUri=https://qagpublic.qg1.apps.qualys.eu/CloudAgent

Скачать .rpm

image.png

sudo dpkg --install QualysCloudAgent.deb
sudo /usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh ActivationId=5eb9ad8e-02cc-431a-9afb-b8caad175ec1 CustomerId=4d868109-dad3-5536-82b4-9f3a6f3010bc ServerUri=https://qagpublic.qg1.apps.qualys.eu/CloudAgent/

Скачать .deb

Windows
image.png

QualysCloudAgent.exe CustomerId={4d868109-dad3-5536-82b4-9f3a6f3010bc} ActivationId={3213648c-c806-4168-947d-8001a281abc3} WebServiceUri=https://qagpublic.qg1.apps.qualys.eu/CloudAgent/
Скачать .exe

Удаление агента Qualys

При выводе хоста из эксплуатации, перед его удалением\отключением необходимо удалить агента qualys. Так как он потребляет лицензию и возникает дублирование при использовании одинаковых ip- или fqdn-адресов. Это может сказываться на долге информационной безопасности.

Windows
При наличии агента сканера безопасности Qualys Cloud Agent необходимо удалить его используя следующую команду
"%programfiles%\qualys\qualysagent\uninstall.exe" Uninstall=True Force=True

Хост при этом должен иметь доступ в Интернет для отключения лицензии.

Linux
"sudo rpm -e qualys-cloud-agent"

или

"sudo dpkg --purge qualys-cloud-agent"

По любым возникающим вопросам пишите в loop: @lmru-itsec-support группа @itsec-vulnerabilities .

вот еще надо поставить агенты на тачки, добавь в стейты в том числе и прогоним солт как раз по этим таскам
PasswordAuthentication надо пока оставить
</user_query>


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "ls -l /root/QualysCloudAgent.deb /tmp/QualysCloudAgent.deb 2>/dev/null || true"

Check if Qualys DEB already exists on hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "dpkg -l wazuh-agent qualys-cloud-agent"

Check current installation status of security agents


Вношу изменения в states: оставляю `PasswordAuthentication` включенным, добавляю установку `wazuh-agent` и `qualys-agent` (с безопасным fallback, если `.deb` Qualys еще не передан), затем прогоню dry-run и apply только нового state.

ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/ssh_hardening.sls
@@
-{% set ssh_allow_users = salt['pillar.get']('pmg:ssh_allow_users', []) %}
+{% set ssh_allow_users = salt['pillar.get']('pmg:ssh_allow_users', []) %}
+{% set ssh_password_authentication = salt['pillar.get']('pmg:ssh_password_authentication', True) %}
@@
     - template: jinja
     - context:
         ssh_allow_users: {{ ssh_allow_users }}
+        ssh_password_authentication: {{ ssh_password_authentication }}
     - user: root
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/files/99-hardening.conf.jinja
@@
 # Managed by Salt.
-PasswordAuthentication no
+PasswordAuthentication {{ "yes" if ssh_password_authentication else "no" }}
 PermitRootLogin prohibit-password
 PubkeyAuthentication yes
*** End Patch


Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/security_agents.sls
+{% 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 qualys_enabled = salt['pillar.get']('pmg:qualys:enabled', True) %}
+{% set qualys_package_path = salt['pillar.get']('pmg:qualys:package_path', '/root/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 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-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'
+
+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
+    - require:
+      - cmd: pmg-qualys-install
+{% elif qualys_enabled %}
+pmg-qualys-skipped:
+  test.show_notification:
+    - text: "Qualys agent skipped: set package_path + activation_id + customer_id in pillar."
+{% endif %}
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/init.sls
@@
 include:
   - pmg.common
   - pmg.ssh_hardening
+  - pmg.security_agents
   - pmg.auto_updates
   - pmg.audit_logging
   - pmg.pmg_baseline
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/pmg.sls
@@
   ssh_allow_users: []
+  ssh_password_authentication: true
@@
   manage_hosts_file: false
@@
     - ip: 10.220.41.152
       hostname: p-mailgw-x2-01
       aliases:
         - p-mailgw-x2-01.lmru.tech
+
+  wazuh:
+    enabled: true
+    version: 4.14.0-1
+    manager: 10.80.120.205
+    group: test_auditd_unix
+
+  qualys:
+    enabled: true
+    package_path: /root/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/
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/README.md
@@
 - `pmg/*.sls` — состояния:
   - `common` (базовые пакеты),
   - `ssh_hardening`,
+  - `security_agents` (Wazuh/Qualys),
   - `auto_updates` (security-only, без auto reboot),
@@
 - `pmg.ssh_allow_users`
+- `pmg.ssh_password_authentication`
 - `pmg.journald_system_max_use`
 - `pmg.journald_max_file_sec`
 - `pmg.pmg_services`
 - `pmg.manage_hosts_file`
 - `pmg.hosts_file_entries`
+- `pmg.wazuh.*`
+- `pmg.qualys.*`
*** End Patch


Shell

Run lint after adding new states


tar -czf /tmp/pmg-salt-bundle.tar.gz Makefile README.md top.sls pillar pmg

Rebuild bundle with updated states


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.copy -a "src=/tmp/pmg-salt-bundle.tar.gz dest=/root/pmg-salt-bundle.tar.gz mode=0644"

Upload refreshed state bundle to both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.unarchive -a "src=/root/pmg-salt-bundle.tar.gz dest=/root/salt-pmg remote_src=true"

Extract refreshed bundle on both hosts


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents test=True"

Dry-run security agents state on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents test=True"

Dry-run security agents state on second host


Нашел проблему: на хосты не распаковалась новая версия bundle (поэтому `pmg.security_agents` не найден). Сейчас принудительно обновлю `/root/salt-pmg` на обеих нодах и повторю dry-run/apply.

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.file -a "path=/root/salt-pmg state=absent"

Remove old Salt bundle directory on hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.file -a "path=/root/salt-pmg state=directory mode=0755"

Recreate clean Salt bundle directory on hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.unarchive -a "src=/root/pmg-salt-bundle.tar.gz dest=/root/salt-pmg remote_src=true"

Extract latest Salt bundle to clean directory


ansible -i "10.80.38.152," all -u root -m ansible.builtin.file -a "path=/root/salt-pmg state=directory mode=0755"

Ensure bundle directory exists on first host

ansible -i "10.80.38.152," all -u root -m ansible.builtin.unarchive -a "src=/root/pmg-salt-bundle.tar.gz dest=/root/salt-pmg remote_src=true"

Extract latest Salt bundle on first host


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "test -f /root/salt-pmg/pmg/security_agents.sls && echo present"

Verify security_agents state file on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "test -f /root/salt-pmg/pmg/security_agents.sls && echo present"

Verify security_agents state file on second host

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents test=True"

Dry-run security_agents on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents test=True"

Dry-run security_agents on second host


ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/security_agents.sls
@@
 pmg-qualys-service:
   service.running:
     - name: qualys-cloud-agent
     - enable: true
+    - onlyif: test -f {{ qualys_package_path }}
     - require:
       - cmd: pmg-qualys-install
*** End Patch


Shell

Lint after qualys service condition fix


tar -czf /tmp/pmg-salt-bundle.tar.gz Makefile README.md top.sls pillar pmg

Repack states bundle after qualys state fix


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.copy -a "src=/tmp/pmg-salt-bundle.tar.gz dest=/root/pmg-salt-bundle.tar.gz mode=0644"

Upload patched bundle to both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.file -a "path=/root/salt-pmg state=absent"

Clear old extracted bundle on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.file -a "path=/root/salt-pmg state=directory mode=0755"

Create clean bundle directory on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.unarchive -a "src=/root/pmg-salt-bundle.tar.gz dest=/root/salt-pmg remote_src=true"

Extract patched bundle on both hosts


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "ls -ld /root/salt-pmg; stat -c '%F %n' /root/salt-pmg"

Inspect salt-pmg path type on both hosts


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "mkdir -p /root/salt-pmg && tar -xzf /root/pmg-salt-bundle.tar.gz -C /root/salt-pmg && test -f /root/salt-pmg/pmg/security_agents.sls"

Extract bundle using tar and verify state file


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents test=True"

Dry-run security agents after bundle sync on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents test=True"

Dry-run security agents after bundle sync on second host


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents"

Apply security agents state on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents"

Apply security agents state on second host


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "systemctl is-active wazuh-agent"

Check wazuh-agent service active on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -l wazuh-agent | sed -n '1,10p'"

Confirm wazuh-agent package installed on hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "grep -E '(^address|^agent_name|^groups)' /var/ossec/etc/ossec.conf || true"

Check wazuh config manager and agent identity

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id $(hostname -f) --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.ssh_hardening test=True"

Dry-run ssh_hardening to confirm password auth kept


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.command -a "systemctl is-active wazuh-agent"

Recheck wazuh-agent service state after startup

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "grep -n '10.80.120.205\|test_auditd_unix\|p-mailgw' /var/ossec/etc/ossec.conf || true"

Check wazuh manager and identity values in agent config


ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/README.md
@@
 - `pmg.wazuh.*`
 - `pmg.qualys.*`
+
+Примечание по Qualys: state ожидает локальный пакет `.deb` на целевом хосте
+(`pmg.qualys.package_path`, по умолчанию `/root/QualysCloudAgent.deb`).
*** End Patch


Shell

Show pending repository changes after agent integration


Сделал. Добавил в Salt states установку агентов и прогнал по хостам.

Что изменил в states:

- `ssh_hardening`:
  - добавил параметр `pmg.ssh_password_authentication` в pillar,
  - в шаблоне `99-hardening.conf.jinja` теперь:
    - `PasswordAuthentication yes`, если `pmg.ssh_password_authentication: true`.
- новый state `pmg/security_agents.sls`:
  - установка `wazuh-agent` 4.14.0-1 с параметрами:
    - `WAZUH_MANAGER=10.80.120.205`
    - `WAZUH_AGENT_GROUP=test_auditd_unix`
    - `WAZUH_AGENT_NAME=$(hostname -f)`
  - запуск/автозапуск `wazuh-agent`
  - блок Qualys:
    - установка из `pmg.qualys.package_path` (по умолчанию `/root/QualysCloudAgent.deb`)
    - активация через `qualys-cloud-agent.sh`
    - сервис запускается только если пакет реально есть
- подключил `pmg.security_agents` в `pmg/init.sls`
- обновил `pillar/pmg.sls` и `README.md`.

Что прогнал:

1. **Dry-run** `pmg.security_agents` на обеих нодах — `Failed: 0`
2. **Apply** `pmg.security_agents` на обеих нодах — успешно
3. Проверки после apply:

- `wazuh-agent` пакет установлен:
  - `ii wazuh-agent 4.14.0-1 amd64`
- `wazuh-agent` сервис:
  - `active` на обеих
- в `/var/ossec/etc/ossec.conf` корректные значения:
  - manager: `10.80.120.205`
  - groups: `test_auditd_unix`
  - agent_name: `p-mailgw-x1-01.lmru.tech` / `p-mailgw-x2-01.lmru.tech`

По Qualys:

- в state добавлен, но **не установлен**, потому что на хостах нет файла `/root/QualysCloudAgent.deb` (state его корректно пропускает, без падения).
- как только положите `.deb` по этому пути (или смените `pmg.qualys.package_path`), можно сразу повторить:
  - `salt-call --local ... state.apply pmg.security_agents`

Итог: Wazuh уже встал и работает, SSH-пароль в hardening оставлен включенным (через pillar), Qualys подготовлен к установке через тот же state.


<uploaded_documents>
The following documents have been saved to your filesystem. You can read them using your file-reading tool or other tools:
- /home/ruslanpi/.cursor/projects/home-ruslanpi-Documents-repos-lmru-devsecops-ansible/uploads/QualysCloudAgent.deb-0.md
</uploaded_documents>
<timestamp>Tuesday, Jun 2, 2026, 1:26 PM (UTC+3)</timestamp>
<user_query>
блок Qualys:
вот ссылка для скачивания https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb
/root/QualysCloudAgent.deb неправильно, надо, конечно, в /opt устанавливать
</user_query>


Принял, переделаю блок Qualys на скачивание `.deb` по вашей ссылке и установку из `/opt` (вместо `/root`). После правки сразу прогоню dry-run/apply `pmg.security_agents` на обеих нодах.


ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/security_agents.sls
@@
 {% set qualys_enabled = salt['pillar.get']('pmg:qualys:enabled', True) %}
-{% set qualys_package_path = salt['pillar.get']('pmg:qualys:package_path', '/root/QualysCloudAgent.deb') %}
+{% 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 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-skipped:
   test.show_notification:
-    - text: "Qualys agent skipped: set package_path + activation_id + customer_id in pillar."
+    - text: "Qualys agent skipped: set package_url + package_path + activation_id + customer_id in pillar."
 {% endif %}
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/pmg.sls
@@
   qualys:
     enabled: true
-    package_path: /root/QualysCloudAgent.deb
+    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/
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/README.md
@@
-Примечание по Qualys: state ожидает локальный пакет `.deb` на целевом хосте
-(`pmg.qualys.package_path`, по умолчанию `/root/QualysCloudAgent.deb`).
+Примечание по Qualys: state скачивает `.deb` по `pmg.qualys.package_url`
+в `pmg.qualys.package_path` (по умолчанию `/opt/QualysCloudAgent.deb`).
*** End Patch


Shell

Run lint after Qualys source/path updates


Обновил стейты под `/opt` и URL. Сейчас прокину новую версию на обе ноды и прогоню dry-run + apply именно `pmg.security_agents`, чтобы поставить Qualys из ссылки.

tar -czf /tmp/pmg-salt-bundle.tar.gz Makefile README.md top.sls pillar pmg

Build updated salt bundle archive


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.copy -a "src=/tmp/pmg-salt-bundle.tar.gz dest=/root/pmg-salt-bundle.tar.gz mode=0644"

Upload updated bundle to both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "rm -rf /root/salt-pmg && mkdir -p /root/salt-pmg && tar -xzf /root/pmg-salt-bundle.tar.gz -C /root/salt-pmg && test -f /root/salt-pmg/pmg/security_agents.sls"

Refresh extracted bundle directory on both hosts


<timestamp>Tuesday, Jun 2, 2026, 1:27 PM (UTC+3)</timestamp>
<user_query>
sudo dpkg --purge qualys-cloud-agent
не забудь поудалять лицензии
</user_query>


Сделаю на обеих нодах: остановка агента, `dpkg --purge qualys-cloud-agent`, зачистка хвостов (`/usr/local/qualys`, `/etc/qualys`) и проверка, что пакет удален. После этого дам итог по статусу.

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -l qualys-cloud-agent 2>/dev/null || true"

Check current Qualys package state on both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "systemctl stop qualys-cloud-agent 2>/dev/null || true; dpkg --purge qualys-cloud-agent 2>/dev/null || true; rm -rf /usr/local/qualys /etc/qualys /var/lib/qualys 2>/dev/null || true; echo done"

Purge Qualys agent and remove local license traces

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -l qualys-cloud-agent 2>/dev/null || true; test ! -d /usr/local/qualys && echo qualys_dir_removed"

Verify Qualys package and directories are removed


Чтобы Qualys не ставился обратно при следующем `state.apply`, сейчас отключу его в `pillar` (`pmg.qualys.enabled: false`).

ApplyPatch

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/pmg.sls
@@
   qualys:
-    enabled: true
+    enabled: false
     package_url: https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb
     package_path: /opt/QualysCloudAgent.deb
*** End Patch


Сделано на **обеих** нодах:

- выполнил удаление Qualys (`dpkg --purge qualys-cloud-agent`)
- остановил сервис (если был)
- удалил локальные хвосты:
  - `/usr/local/qualys`
  - `/etc/qualys`
  - `/var/lib/qualys`

Проверка прошла: каталогов Qualys нет, пакет не числится установленным.

Дополнительно я отключил Qualys в states, чтобы он не поставился обратно при следующем запуске:

- `pillar/pmg.sls` -> `pmg.qualys.enabled: false`

По лицензии:
- локальный агент удален корректно.
- если лицензия была активна на стороне Qualys Cloud, иногда освобождение видно с задержкой; при необходимости можно сверить в консоли Qualys (asset/agent list).


<timestamp>Tuesday, Jun 2, 2026, 1:29 PM (UTC+3)</timestamp>
<user_query>
хорошо, тепрь снова включи кволис
и примени теперь полный солт, как при настройке нового хоста и дай отчет для чата
</user_query>


Ок, включаю Qualys обратно в `pillar`, затем выполню **полный** `state.apply pmg.init` на обеих нодах (как первичная настройка), после — `pmg.verify` и соберу готовый отчет для чата.

Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/pillar/pmg.sls
@@
   qualys:
-    enabled: false
+    enabled: true
     package_url: https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb
     package_path: /opt/QualysCloudAgent.deb
*** End Patch


Shell

Run lint before full Salt apply


tar -czf /tmp/pmg-salt-bundle.tar.gz Makefile README.md top.sls pillar pmg

Package current Salt states for deployment


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.copy -a "src=/tmp/pmg-salt-bundle.tar.gz dest=/root/pmg-salt-bundle.tar.gz mode=0644"

Upload latest Salt bundle to both hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "rm -rf /root/salt-pmg && mkdir -p /root/salt-pmg && tar -xzf /root/pmg-salt-bundle.tar.gz -C /root/salt-pmg && test -f /root/salt-pmg/pmg/init.sls"

Refresh extracted Salt state tree on hosts


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.init"

Apply full pmg.init state on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.init"

Apply full pmg.init state on second host


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "sed -n '35,70p' /root/salt-pmg/pillar/pmg.sls"

Inspect deployed pillar Qualys settings on hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -l qualys-cloud-agent 2>/dev/null || true; systemctl is-active qualys-cloud-agent 2>/dev/null || true; ls -l /opt/QualysCloudAgent.deb /root/QualysCloudAgent.deb 2>/dev/null || true"

Check Qualys package and installer file status


tar -xOf /tmp/pmg-salt-bundle.tar.gz pillar/pmg.sls | sed -n '35,70p'

Inspect bundled pillar content in tarball


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "tar -xOf /root/pmg-salt-bundle.tar.gz pillar/pmg.sls | sed -n '35,70p'"

Inspect pillar content inside remote bundle archives


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "rm -rf /root/salt-pmg && mkdir -p /root/salt-pmg && tar -xzf /root/pmg-salt-bundle.tar.gz -C /root/salt-pmg && sed -n '35,70p' /root/salt-pmg/pillar/pmg.sls"

Re-extract bundle and verify deployed pillar values

ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents"

Apply security agents with updated Qualys settings on host1

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.security_agents"

Apply security agents with updated Qualys settings on host2


ansible -i "10.80.38.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x1-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.verify"

Run verify state on first host

ansible -i "10.220.41.152," all -u root -m ansible.builtin.shell -a "salt-call --local --id p-mailgw-x2-01.lmru.tech --file-root=/root/salt-pmg --pillar-root=/root/salt-pmg/pillar state.apply pmg.verify"

Run verify state on second host

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "systemctl is-active wazuh-agent qualys-cloud-agent salt-minion; grep -n '^PasswordAuthentication' /etc/ssh/sshd_config.d/99-hardening.conf"

Check security agents and SSH password auth setting


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg -l wazuh-agent qualys-cloud-agent | awk '/^ii/ {print $2, $3}'"

Collect installed Wazuh and Qualys package versions

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "grep -n '<address>\|<agent_name>\|<groups>' /var/ossec/etc/ossec.conf | sed -n '1,6p'; /usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh Status 2>/dev/null | sed -n '1,10p' || true"

Collect Wazuh and Qualys runtime identity details


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg-query -W -f='${Package} ${Version}\n' wazuh-agent qualys-cloud-agent"

Collect versions for wazuh and qualys packages


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "dpkg-query -W -f='\${Package} \${Version}\\n' wazuh-agent qualys-cloud-agent"

Collect package versions with escaped dpkg format


Сделано.

Что выполнено:

- Qualys снова включен в `pillar`:
  - `pmg.qualys.enabled: true`
  - `pmg.qualys.package_url: https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb`
  - `pmg.qualys.package_path: /opt/QualysCloudAgent.deb`
- Выполнен полный `state.apply pmg.init` на обеих нодах.
- Выполнен `state.apply pmg.verify` на обеих нодах.
- После этого отдельно прогнан `pmg.security_agents` с актуальным bundle, чтобы Qualys точно ставился из `/opt` по URL.

Итог по хостам:

- `10.80.38.152` (`p-mailgw-x1-01.lmru.tech`)
- `10.220.41.152` (`p-mailgw-x2-01.lmru.tech`)

Статусы:

- PMG verify: `Failed: 0` на обеих.
- Кластер PMG: обе ноды в статусе `A/S`, master/node определяются корректно.
- SSH hardening: `PasswordAuthentication yes` (как просили).
- Wazuh:
  - пакет `wazuh-agent 4.14.0-1`
  - сервис `active`
  - manager `10.80.120.205`, group `test_auditd_unix`
- Qualys:
  - пакет `qualys-cloud-agent 6.4.0-45`
  - установлен из `/opt/QualysCloudAgent.deb` (скачивание по URL)
  - сервис `active`

---

Готовый отчет для чата (можно отправлять):

```text
Обновление по PMG-хостам:

1) Salt baseline применен полностью на обе ноды:
- p-mailgw-x1-01.lmru.tech (10.80.38.152)
- p-mailgw-x2-01.lmru.tech (10.220.41.152)

2) Проверки после применения:
- pmg.verify: ошибок нет (Failed: 0)
- pmgcm status: кластер видит обе ноды, master/node в норме
- SSH: PasswordAuthentication оставлен включенным (yes)

3) Security agents:
- Wazuh:
  - wazuh-agent 4.14.0-1
  - сервис активен
  - manager: 10.80.120.205
  - group: test_auditd_unix
  - agent_name = hostname -f (новые FQDN)
- Qualys:
  - qualys-cloud-agent 6.4.0-45
  - .deb скачивается по URL
    https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb
  - установка из /opt/QualysCloudAgent.deb
  - сервис активен

Итог: оба хоста в целевом состоянии, baseline и агенты применены.
```


<timestamp>Tuesday, Jun 2, 2026, 1:37 PM (UTC+3)</timestamp>
<user_query>
отчет для чата надо шире, они не знает, что мы тут с тобой придумали по харденингу и настройкам)
</user_query>


Вот расширенный вариант отчета для чата, чтобы был понятен без контекста:

```text
Коллеги, отчет по срочной подготовке PMG-хостов под передачу в команду Salt.

Контур:
- 10.80.38.152 -> p-mailgw-x1-01.lmru.tech
- 10.220.41.152 -> p-mailgw-x2-01.lmru.tech
- PMG-кластер: 2 ноды

Что сделано по инфраструктуре и доступу:
1) Переименование хостов
- Хосты переименованы в новые FQDN.
- Обновлены /etc/hosts на обеих нодах.
- После переименования была выполнена корректировка PMG cluster config (cluster.conf) под новые node names.
- Кластерная связность восстановлена, pmgcm status на обеих нодах корректный.

2) DNS
- Имена p-mailgw-x1-01.lmru.tech и p-mailgw-x2-01.lmru.tech добавлены в DNS.
- Резолв с клиентской стороны подтвержден.

3) Salt readiness на самих хостах
- Подключен официальный репозиторий Salt (packages.broadcom.com).
- Установлен salt-minion 3008.0.
- Сервис salt-minion включен и запущен на обеих нодах.
- Локальный тест salt-call --local test.ping проходит.

Что сделано по baseline/hardening (через Salt states):
1) Базовый слой
- Установлены эксплуатационные пакеты (curl, vim-tiny, rsync, ca-certificates, jq).

2) SSH hardening (без риска потерять доступ)
- Применен drop-in /etc/ssh/sshd_config.d/99-hardening.conf.
- Зафиксировано:
  - PermitRootLogin prohibit-password
  - PubkeyAuthentication yes
  - PasswordAuthentication оставлен ВКЛЮЧЕННЫМ (yes), по договоренности
  - X11Forwarding no
  - MaxAuthTries 3
  - LoginGraceTime 30
- Конфиг валидируется через sshd -t до reload.

3) Автообновления
- Настроен unattended-upgrades (security-only),
- Авто reboot отключен.

4) Логирование и аудит
- Настроено ограничение journald retention.
- Установлен и включен auditd.
- Применены базовые audit rules.
- rsyslog включен.

5) PMG baseline
- Критичные сервисы PMG приведены/проверены в running state:
  postfix, pmg-smtp-filter, pmgpolicy, pmgdaemon, pmgmirror, pmgtunnel.
- Проверка кластера pmgcm status встроена в verify.

Security agents:
1) Wazuh
- Установлен wazuh-agent 4.14.0-1 на обе ноды.
- Сервис активен.
- Параметры:
  - manager: 10.80.120.205
  - group: test_auditd_unix
  - agent_name: hostname -f (новые FQDN)

2) Qualys
- Установка реализована через Salt state.
- Источник .deb:
  https://storage.yandexcloud.net/qualys-agent/QualysCloudAgent.deb
- Путь установки .deb на хосте: /opt/QualysCloudAgent.deb
- Агент установлен: qualys-cloud-agent 6.4.0-45
- Сервис активен.

Firewall:
- Хостовый firewall из этого набора не применяется (осознанно).
- Текущая схема трафика сохраняется по дизайн-решению:
  публикация 25 порта через периметр/NAT, балансировка через MX.
- Управляющие ограничения доступа реализуются сетевым контуром (PAM/Bastion/периметр), не на уровне данного state-пакета.

Проверки после применения:
- Полный state.apply pmg.init выполнен на обеих нодах без ошибок (Failed: 0).
- state.apply pmg.verify выполнен на обеих нодах без ошибок (Failed: 0).
- PMG кластер в рабочем состоянии.
- Wazuh/Qualys/salt-minion сервисы активны.

Итог:
- Хосты приведены в целевое состояние для передачи в команду Salt.
- Базовый hardening + аудит + security agents применены.
- Риски потери доступа минимизированы (PasswordAuthentication оставлен включенным).
```

Если хотите, подготовлю вторую версию этого же отчета в более коротком формате “для руководителя” (5-7 пунктов).


<timestamp>Tuesday, Jun 2, 2026, 1:48 PM (UTC+3)</timestamp>
<user_query>
короткая версия для тимлида
</user_query>


```text
Короткий статус по PMG:

- Подняли и проверили 2 ноды:
  - p-mailgw-x1-01.lmru.tech (10.80.38.152)
  - p-mailgw-x2-01.lmru.tech (10.220.41.152)

- После переименования восстановили кластер PMG под новыми именами, pmgcm status корректный.

- Подготовили хосты под эксплуатацию через Salt:
  - salt-minion установлен и активен на обеих нодах.

- Применили полный baseline/hardening (через state.apply pmg.init):
  - базовые пакеты,
  - SSH hardening (root по ключу разрешен, PasswordAuthentication оставлен включенным),
  - unattended-upgrades (security-only, без auto reboot),
  - journald + auditd + базовые audit rules,
  - контроль PMG-сервисов.

- Установили security-агенты:
  - Wazuh-agent 4.14.0-1 (manager 10.80.120.205, group test_auditd_unix),
  - Qualys Cloud Agent 6.4.0-45 (установка из /opt/QualysCloudAgent.deb, скачивание по корпоративной ссылке).

- Финальные проверки:
  - state.apply pmg.verify на обеих нодах без ошибок,
  - PMG-кластер и критичные сервисы в рабочем состоянии.
```


<timestamp>Tuesday, Jun 2, 2026, 1:53 PM (UTC+3)</timestamp>
<user_query>
так, вот мы еще обновляли ПМГ
добавь новый стейт для обновления чтоб с проверками и т.д.
и потом пробегись по ридми, проверь
</user_query>


Сделаю: добавлю отдельный state обновления PMG с pre/post-проверками (кластер, сервисы, факт reboot-required), подключу удобный запуск через `Makefile` и обновлю `README`, чтобы это было прозрачно для команды. Сейчас внесу изменения и прогоню lint/syntax-проверку.


ApplyPatch

Input:
*** Begin Patch
*** Add File: /home/ruslanpi/Documents/repos/mailgw-salt/pmg/update.sls
+{% set pmg_services = salt['pillar.get']('pmg:pmg_services', []) %}
+
+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
+
+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
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/Makefile
@@
-.PHONY: lint check apply verify
+.PHONY: lint check apply verify update-check update
@@
 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
*** End Patch


Input:
*** Begin Patch
*** Update File: /home/ruslanpi/Documents/repos/mailgw-salt/README.md
@@
 - `pmg/*.sls` — состояния:
   - `common` (базовые пакеты),
   - `ssh_hardening`,
   - `security_agents` (Wazuh/Qualys),
+  - `update` (пакетные обновления + pre/post проверки PMG),
   - `auto_updates` (security-only, без auto reboot),
   - `audit_logging`,
   - `pmg_baseline` (проверка/поддержание сервисов PMG),
@@
 make check
 make apply
 make verify
+make update-check
+make update
 ```
@@
 - `pmg.qualys.*`
+
+## Отдельное обновление 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`);
+- postcheck кластера и сервисов;
+- уведомление, если требуется reboot (`/var/run/reboot-required`).
*** End Patch


Добавил новый `pmg.update` state и обновил `Makefile/README`. Сейчас прогоняю lint и dry-run `pmg.update` на обеих нодах, чтобы убедиться, что state компилируется и проверки отрабатывают.

Shell

Run lint after adding update state


tar -czf /tmp/pmg-salt-bundle.tar.gz Makefile README.md top.sls pillar pmg

Pack latest states bundle including update state


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.copy -a "src=/tmp/pmg-salt-bundle.tar.gz dest=/root/pmg-salt-bundle.tar.gz mode=0644"

Upload refreshed bundle to both PMG hosts

ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "rm -rf /root/salt-pmg && mkdir -p /root/salt-pmg && tar -xzf /root/pmg-salt-bundle.tar.gz -C /root/salt-pmg && test -f /root/salt-pmg/pmg/update.sls"

Deploy extracted state tree including update state


tar -tzf /tmp/pmg-salt-bundle.tar.gz | sed -n '1,120p'

List archive contents to verify update state path


ansible -i "10.80.38.152,10.220.41.152," all -u root -m ansible.builtin.shell -a "rm -rf /root/salt-pmg && mkdir -p /root/salt-pmg && tar -xzf /root/pmg-salt-bundle.tar.gz -C /root/salt-pmg && ls -l /root/salt-pmg/pmg/update.sls"

Re-extract bundle and verify update state file on hosts