diff --git a/decisions/2026-03-03-mailserver-setup-scenario.md b/decisions/2026-03-03-mailserver-setup-scenario.md new file mode 100644 index 0000000..1aa377f --- /dev/null +++ b/decisions/2026-03-03-mailserver-setup-scenario.md @@ -0,0 +1,97 @@ +# Сценарий настройки почтового сервера (Mailcow) + +> Отработан на niikn.com (2026-03-03). Следующий: dttb.ru. + +## Предусловия +- Mailcow установлен и запущен (docker compose) +- Белый IP есть +- Домен на Spaceweb DNS + +## Шаги + +### 1. MikroTik — проброс почтовых портов +``` +/ip firewall nat add chain=dstnat dst-port=25 protocol=tcp action=dst-nat to-addresses= comment="SMTP" +/ip firewall nat add chain=dstnat dst-port=465 protocol=tcp action=dst-nat to-addresses= comment="SMTPS" +/ip firewall nat add chain=dstnat dst-port=587 protocol=tcp action=dst-nat to-addresses= comment="Submission" +/ip firewall nat add chain=dstnat dst-port=993 protocol=tcp action=dst-nat to-addresses= comment="IMAPS" +/ip firewall nat add chain=dstnat dst-port=995 protocol=tcp action=dst-nat to-addresses= comment="POP3S" +/ip firewall nat add chain=dstnat dst-port=4190 protocol=tcp action=dst-nat to-addresses= comment="Sieve" +``` +> Порты 80/443 НЕ пробрасываем если NPM уже занимает их. + +### 2. NPM — proxy host для mail.domain.com +- Создать proxy host: `mail.` → `https://:443` +- Scheme: https (Mailcow сам обслуживает HTTPS) +- SSL: Let's Encrypt через NPM +- ssl_forced: true + +### 3. DNS записи через Spaceweb API +Скрипт: `snippets/spaceweb-dns-api.py` + +```bash +# Получить текущие записи +python3 spaceweb-dns-api.py info + +# A запись для mail (если нет) +python3 spaceweb-dns-api.py add-a mail + +# MX +python3 spaceweb-dns-api.py add-mx mail. 10 + +# SPF +python3 spaceweb-dns-api.py add-txt @ "v=spf1 mx a:mail. ~all" + +# DMARC +python3 spaceweb-dns-api.py add-txt _dmarc "v=DMARC1; p=none; rua=mailto:admin@" + +# DKIM (получить ключ из Mailcow → Configuration → ARC/DKIM) +python3 spaceweb-dns-api.py add-txt dkim._domainkey "" +``` + +> ⚠️ Капча: после 2-3 запросов подряд нужна новая сессия (перезапустить скрипт). + +### 4. Mailcow — домен и ящики +Через API (X-API-Key) или веб-интерфейс: +1. Добавить домен +2. Получить DKIM ключ (Configuration → ARC/DKIM) +3. Создать ящики + +### 5. Пароль admin +```bash +# Через API: +curl -X POST https://mail./api/v1/edit/admin \ + -H "Content-Type: application/json" \ + -H "X-API-Key: " \ + -d '{"items":["admin"],"attr":{"password":"","password2":""}}' +``` + +### 6. PTR (rDNS) +Обратиться к провайдеру: `` → `mail.` + +### 7. Nextcloud SMTP (если есть) +Администрирование → Основные параметры → Email: +- SMTP, хост: ``, порт: 587 +- STARTTLS, LOGIN auth +- Логин: noreply@, пароль + +### Проверка +```bash +dig mail. A +short # → +dig MX +short # → mail. +dig TXT +short # → v=spf1 ... +dig _dmarc. TXT +short # → v=DMARC1 ... +dig dkim._domainkey. TXT +short # → v=DKIM1 ... +``` + +https://www.mail-tester.com/ — отправить письмо, проверить оценку. + +## Применение для dttb.ru + +| Параметр | niikn.com (выполнено) | dttb.ru (план) | +|----------|----------------------|----------------| +| Mailcow IP | 192.168.1.128 | ? (новая VM или LXC) | +| MikroTik | 192.168.1.1 (AI/OL260380eg) | 10.0.0.250 или роутер dttb | +| NPM | 192.168.1.22 | 10.0.0.195 | +| Публичный IP | 85.235.181.190 | ? | +| DNS домен | niikn.com | dttb.ru | diff --git a/projects/dttb/credentials.md b/projects/dttb/credentials.md index 91e0ff9..8eb8f76 100644 --- a/projects/dttb/credentials.md +++ b/projects/dttb/credentials.md @@ -2,7 +2,7 @@ > ⚠️ **КОНФИДЕНЦИАЛЬНО** — не распространять за пределы команды > -> Последнее обновление: 2026-02-27 +> Последнее обновление: 2026-03-03 --- @@ -42,6 +42,34 @@ | Email | `it5870@yandex.ru` | | Пароль | `1qaz!QAZ` | +## Spaceweb (DNS-хостинг: niikn.com, dttb.ru, itilegent.ru) + +| Параметр | Значение | +|----------|----------| +| URL | https://vps.sweb.ru | +| Логин | `it5870yand` | +| Пароль | `1qaz!QAZ` | +| API | `https://api.sweb.ru/domains/dns` (JSON-RPC) | +| Домены | niikn.com, dttb.ru, itilegent.ru | + +## Mailcow НИИКН (VM106 — 192.168.1.128) + +| Параметр | Значение | +|----------|----------| +| URL | https://mail.niikn.com | +| Admin | `admin` / `1qaz!QAZ` | +| API Key | `niikn-mailcow-api-2026` | +| DBPASS | `8VcUSgpKEOoxNojIZBRJx0FzMxzm` | +| Ящик | `noreply@niikn.com` / `NiIkN-NoReply-2026!` | + +## NPM НИИКН (LXC 102 — 192.168.1.22:81) + +| Параметр | Значение | +|----------|----------| +| URL | http://192.168.1.22:81 | +| Email | `it5870@yandex.ru` | +| Пароль | `1qaz!QAZ` | + ## SSH-ключи и доступы | Хост | Порт | Метод | diff --git a/projects/niikn/README.md b/projects/niikn/README.md index 16a7869..b66f66c 100644 --- a/projects/niikn/README.md +++ b/projects/niikn/README.md @@ -15,7 +15,7 @@ ## Сервисы - **Nextcloud AIO** — `https://new.niikn.com`, управление: `https://new.niikn.com:8080` -- **Mailcow** — `https://mail.niikn.com`, admin / moohoo (сменить!) +- **Mailcow** — `https://mail.niikn.com`, admin / 1qaz!QAZ - **SMB** — `//192.168.1.79/share`, смонтирован на VM108 как `/mnt/ncsmb` ## Ключевые файлы на VM108 @@ -24,12 +24,41 @@ - `/etc/samba/smb-niikn.creds` — учётные данные SMB - `/home/cloud/setup-firewall.sh` — скрипт UFW -## Ключевые файлы на VM106 (Mailcow) +## Mailcow (VM106, 192.168.1.128) -- `/opt/mailcow-dockerized/mailcow.conf` — конфиг Mailcow - - DBPASS=8VcUSgpKEOoxNojIZBRJx0FzMxzm +- **Web UI:** `https://mail.niikn.com` (через NPM proxy host #17) +- **Admin:** admin / 1qaz!QAZ +- **API key:** `niikn-mailcow-api-2026` +- **DKIM:** selector=dkim, 2048-bit RSA +- **Ящик:** noreply@niikn.com (пароль: NiIkN-NoReply-2026!) +- **SMTP:** 192.168.1.128:587, STARTTLS, LOGIN auth +- **SSL:** Let's Encrypt через NPM (cert ID 50) +- **Конфиг:** `/opt/mailcow-dockerized/mailcow.conf` (DBPASS=8VcUSgpKEOoxNojIZBRJx0FzMxzm) -## Сброс пароля admin в Mailcow +### MikroTik проброс портов → 192.168.1.128 +| Порт | Протокол | Назначение | +|------|----------|------------| +| 25 | TCP | SMTP (приём почты) | +| 465 | TCP | SMTPS | +| 587 | TCP | Submission (отправка) | +| 993 | TCP | IMAPS | +| 995 | TCP | POP3S | +| 4190 | TCP | Sieve | + +### DNS записи (niikn.com → Spaceweb) +| Тип | Имя | Значение | +|-----|-----|----------| +| A | mail | 85.235.181.190 | +| MX | @ | mail.niikn.com. (приоритет 10) | +| TXT | @ | v=spf1 mx a:mail.niikn.com ~all | +| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:admin@niikn.com | +| TXT | dkim._domainkey | v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjAN... (2048-bit) | + +### TODO +- [ ] PTR запись: 85.235.181.190 → mail.niikn.com (запросить у Комстар/МТС) +- [ ] Настроить SMTP в Nextcloud + +### Сброс пароля admin в Mailcow ```bash cd /opt/mailcow-dockerized diff --git a/projects/niikn/changelog.md b/projects/niikn/changelog.md index 4cd66fb..91573f4 100644 --- a/projects/niikn/changelog.md +++ b/projects/niikn/changelog.md @@ -371,6 +371,47 @@ VM 192.168.1.200 также добавлен cloud-init диск (ide0: local-lv --- +## 2026-03-03 — Настройка почтового сервера mail.niikn.com + +### Выполненные шаги + +1. **MikroTik — проброс портов (192.168.1.1)** + - SSH: AI / OL260380eg + - Проброшены порты 25, 465, 587, 993, 995, 4190 → 192.168.1.128 (Mailcow) + - Порты 80/443 не тронуты — идут на NPM + +2. **NPM — proxy host #17 (192.168.1.22)** + - Создан: `mail.niikn.com` → `https://192.168.1.128:443` + - SSL: Let's Encrypt (cert ID 50), ssl_forced=true + - Mailcow сам обслуживает HTTPS — NPM проксирует на https backend + +3. **DNS записи — Spaceweb API (vps.sweb.ru)** + - Логин: it5870yand / 1qaz!QAZ + - API: POST https://api.sweb.ru/domains/dns (JSON-RPC, URL-encoded body) + - Добавлены: MX (mail.niikn.com, pri 10), SPF, DMARC, DKIM + - Все записи распространились и видны через Google/Cloudflare DNS + +4. **Mailcow — домен и ящики (192.168.1.128)** + - Домен niikn.com — был уже добавлен + - DKIM — был уже сгенерирован (selector=dkim, 2048-bit) + - Ящик noreply@niikn.com — был уже создан, пароль установлен: NiIkN-NoReply-2026! + - SMTP (587/STARTTLS) проверен — работает, тестовое письмо отправлено + +5. **Пароль admin Mailcow** + - Изменён через API: POST /api/v1/edit/admin (X-API-Key: niikn-mailcow-api-2026) + - Новый пароль: 1qaz!QAZ + +### Не выполнено +- **Nextcloud SMTP** — нет SSH к VM 108, NC API не поддерживает mail_smtp* настройки + - Настроить вручную: Администрирование → Email → SMTP 192.168.1.128:587, noreply@niikn.com +- **PTR (rDNS)** — текущий: Aquatern.access.comstar.ru → нужен mail.niikn.com + - Обратиться к провайдеру Комстар/МТС + +### Spaceweb DNS API — сценарий для повторного использования (dttb.ru) +Подробный скрипт: `snippets/spaceweb-dns-api.py` + +--- + ## Что ещё нужно сделать - [x] ~~Перезапустить AIO контейнеры Talk → восстановить signaling_servers~~ @@ -378,7 +419,8 @@ VM 192.168.1.200 также добавлен cloud-init диск (ide0: local-lv - [ ] **После rsync:** создать Groupfolders, назначить права, scan (см. groupfolders-migration.md) - [ ] **После rsync:** удалить External Storage ID 4 и 5 (SMB пока не удалять) - [ ] Настроить AIO Backup на VM108 -- [ ] Настроить SMTP для уведомлений Nextcloud (использовать mail.niikn.com после настройки DNS) +- [ ] Настроить SMTP для уведомлений Nextcloud (mail.niikn.com готов, нужно вписать в NC вручную) +- [ ] PTR запись: 85.235.181.190 → mail.niikn.com (провайдер Комстар/МТС) - [ ] Проверить работу пользователей на new.niikn.com - [ ] Решить судьбу Linkwarden и FileBrowser (переносить или нет) - [ ] Зафиксировать статический IP для VM100 (сейчас DHCP 192.168.1.245) diff --git a/snippets/spaceweb-dns-api.py b/snippets/spaceweb-dns-api.py new file mode 100644 index 0000000..ae72356 --- /dev/null +++ b/snippets/spaceweb-dns-api.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Spaceweb DNS API — управление DNS записями через JSON-RPC. +Панель: https://vps.sweb.ru +Домены: niikn.com, dttb.ru, itilegent.ru + +Использование: + python3 spaceweb-dns-api.py info niikn.com + python3 spaceweb-dns-api.py add-mx niikn.com mail.niikn.com 10 + python3 spaceweb-dns-api.py add-txt niikn.com @ "v=spf1 mx ~all" + python3 spaceweb-dns-api.py add-txt niikn.com _dmarc "v=DMARC1; p=none" + python3 spaceweb-dns-api.py del niikn.com TXT 1 + python3 spaceweb-dns-api.py zone niikn.com +""" + +import urllib.request +import urllib.parse +import json +import http.cookiejar +import sys + +LOGIN = "it5870yand" +PASSWORD = "1qaz!QAZ" +UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + + +def create_session(): + """Логин в Spaceweb, возвращает opener с куками.""" + cj = http.cookiejar.CookieJar() + opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) + + # GET login page + req = urllib.request.Request("https://mcp.sweb.ru/") + req.add_header("User-Agent", UA) + opener.open(req) + + # POST auth + login_data = urllib.parse.urlencode({ + "login": LOGIN, "password": PASSWORD, + "new_panel": "1", "to": "//mcp.sweb.ru/main/index/", "savepref": "" + }).encode() + req = urllib.request.Request("https://mcp.sweb.ru/main/auth_submit/", + data=login_data, method="POST") + req.add_header("Content-Type", "application/x-www-form-urlencoded") + req.add_header("User-Agent", UA) + req.add_header("Referer", "https://mcp.sweb.ru/main/auth/") + resp = opener.open(req) + if "vps.sweb.ru" not in resp.url and "mcp.sweb.ru/main/index" not in resp.url: + raise RuntimeError(f"Login failed, redirected to: {resp.url}") + return opener + + +def api_call(opener, method, params): + """Вызов JSON-RPC API Spaceweb.""" + payload = { + "jsonrpc": "2.0", + "id": 1, + "user": LOGIN, + "method": method, + "params": params + } + body = urllib.parse.quote(json.dumps(payload, separators=(",", ":")), safe="").encode() + req = urllib.request.Request("https://api.sweb.ru/domains/dns", + data=body, method="POST") + req.add_header("Content-Type", "application/x-www-form-urlencoded") + req.add_header("User-Agent", UA) + req.add_header("Origin", "https://vps.sweb.ru") + req.add_header("Referer", "https://vps.sweb.ru/") + resp = opener.open(req) + result = json.loads(resp.read().decode()) + if "error" in result: + raise RuntimeError(f"API error: {result['error']['message']}") + return result.get("result") + + +def cmd_info(opener, domain): + """Показать все DNS записи.""" + records = api_call(opener, "info", {"domain": domain}) + print(f"DNS записи для {domain}:") + for r in records: + t = r.get("type", "?") + n = r.get("name", "@") or "@" + v = r.get("value", "") + idx = r.get("index", "?") + extra = f" (pri: {r.get('priority')})" if r.get("priority") else "" + print(f" [{idx:>2}] {t:5s} {n:30s} → {v[:100]}{extra}") + + +def cmd_zone(opener, domain): + """Показать zone file.""" + result = api_call(opener, "getFile", {"domain": domain}) + print(result["content"]) + + +def cmd_add_mx(opener, domain, value, priority="10"): + """Добавить MX запись.""" + if not value.endswith("."): + value += "." + result = api_call(opener, "editMx", { + "domain": domain, "subDomain": "", "action": "add", + "priority": str(priority), "value": value + }) + print(f"MX добавлен: {domain} → {value} (pri {priority}): {result}") + + +def cmd_add_txt(opener, domain, subdomain, value): + """Добавить TXT запись.""" + result = api_call(opener, "editTxt", { + "domain": domain, "action": "add", + "subDomain": subdomain, "value": value + }) + print(f"TXT добавлен: {subdomain}.{domain} → {value[:60]}...: {result}") + + +def cmd_add_a(opener, domain, name, ip): + """Добавить A запись (субдомен).""" + result = api_call(opener, "editMain", { + "domain": domain, "action": "add", + "name": name, "type": "A", "value": ip, "prefix": "" + }) + print(f"A добавлен: {name}.{domain} → {ip}: {result}") + + +def cmd_del(opener, domain, record_type, index): + """Удалить запись по типу и индексу.""" + method_map = { + "A": "editMain", "AAAA": "editMain", "CNAME": "editMain", + "MX": "editMx", "TXT": "editTxt", "NS": "editNs", + "SRV": "editSrv", "CAA": "editCaa" + } + method = method_map.get(record_type.upper(), "editMain") + result = api_call(opener, method, { + "domain": domain, "action": "del", + "index": int(index), "type": record_type.upper() + }) + print(f"Удалено: {record_type} index={index}: {result}") + + +def main(): + if len(sys.argv) < 3: + print(__doc__) + sys.exit(1) + + cmd = sys.argv[1] + domain = sys.argv[2] + opener = create_session() + + if cmd == "info": + cmd_info(opener, domain) + elif cmd == "zone": + cmd_zone(opener, domain) + elif cmd == "add-mx" and len(sys.argv) >= 4: + pri = sys.argv[4] if len(sys.argv) > 4 else "10" + cmd_add_mx(opener, domain, sys.argv[3], pri) + elif cmd == "add-txt" and len(sys.argv) >= 5: + cmd_add_txt(opener, domain, sys.argv[3], sys.argv[4]) + elif cmd == "add-a" and len(sys.argv) >= 5: + cmd_add_a(opener, domain, sys.argv[3], sys.argv[4]) + elif cmd == "del" and len(sys.argv) >= 5: + cmd_del(opener, domain, sys.argv[3], sys.argv[4]) + else: + print(__doc__) + sys.exit(1) + + +if __name__ == "__main__": + main()