mail.niikn.com: DNS настроен, сценарий задокументирован для dttb.ru
- README НИИКН: обновлён Mailcow (пароль, DNS, порты, ящик) - changelog: добавлена запись о настройке mail.niikn.com - credentials: добавлены Spaceweb, Mailcow НИИКН, NPM НИИКН - decisions: сценарий настройки почтового сервера (шаблон для dttb.ru) - snippets: скрипт spaceweb-dns-api.py для управления DNS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
97
decisions/2026-03-03-mailserver-setup-scenario.md
Normal file
97
decisions/2026-03-03-mailserver-setup-scenario.md
Normal file
@@ -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=<MAILCOW_IP> comment="SMTP"
|
||||
/ip firewall nat add chain=dstnat dst-port=465 protocol=tcp action=dst-nat to-addresses=<MAILCOW_IP> comment="SMTPS"
|
||||
/ip firewall nat add chain=dstnat dst-port=587 protocol=tcp action=dst-nat to-addresses=<MAILCOW_IP> comment="Submission"
|
||||
/ip firewall nat add chain=dstnat dst-port=993 protocol=tcp action=dst-nat to-addresses=<MAILCOW_IP> comment="IMAPS"
|
||||
/ip firewall nat add chain=dstnat dst-port=995 protocol=tcp action=dst-nat to-addresses=<MAILCOW_IP> comment="POP3S"
|
||||
/ip firewall nat add chain=dstnat dst-port=4190 protocol=tcp action=dst-nat to-addresses=<MAILCOW_IP> comment="Sieve"
|
||||
```
|
||||
> Порты 80/443 НЕ пробрасываем если NPM уже занимает их.
|
||||
|
||||
### 2. NPM — proxy host для mail.domain.com
|
||||
- Создать proxy host: `mail.<DOMAIN>` → `https://<MAILCOW_IP>: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 <DOMAIN>
|
||||
|
||||
# A запись для mail (если нет)
|
||||
python3 spaceweb-dns-api.py add-a <DOMAIN> mail <PUBLIC_IP>
|
||||
|
||||
# MX
|
||||
python3 spaceweb-dns-api.py add-mx <DOMAIN> mail.<DOMAIN> 10
|
||||
|
||||
# SPF
|
||||
python3 spaceweb-dns-api.py add-txt <DOMAIN> @ "v=spf1 mx a:mail.<DOMAIN> ~all"
|
||||
|
||||
# DMARC
|
||||
python3 spaceweb-dns-api.py add-txt <DOMAIN> _dmarc "v=DMARC1; p=none; rua=mailto:admin@<DOMAIN>"
|
||||
|
||||
# DKIM (получить ключ из Mailcow → Configuration → ARC/DKIM)
|
||||
python3 spaceweb-dns-api.py add-txt <DOMAIN> dkim._domainkey "<DKIM_KEY>"
|
||||
```
|
||||
|
||||
> ⚠️ Капча: после 2-3 запросов подряд нужна новая сессия (перезапустить скрипт).
|
||||
|
||||
### 4. Mailcow — домен и ящики
|
||||
Через API (X-API-Key) или веб-интерфейс:
|
||||
1. Добавить домен
|
||||
2. Получить DKIM ключ (Configuration → ARC/DKIM)
|
||||
3. Создать ящики
|
||||
|
||||
### 5. Пароль admin
|
||||
```bash
|
||||
# Через API:
|
||||
curl -X POST https://mail.<DOMAIN>/api/v1/edit/admin \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: <API_KEY>" \
|
||||
-d '{"items":["admin"],"attr":{"password":"<PASS>","password2":"<PASS>"}}'
|
||||
```
|
||||
|
||||
### 6. PTR (rDNS)
|
||||
Обратиться к провайдеру: `<PUBLIC_IP>` → `mail.<DOMAIN>`
|
||||
|
||||
### 7. Nextcloud SMTP (если есть)
|
||||
Администрирование → Основные параметры → Email:
|
||||
- SMTP, хост: `<MAILCOW_IP>`, порт: 587
|
||||
- STARTTLS, LOGIN auth
|
||||
- Логин: noreply@<DOMAIN>, пароль
|
||||
|
||||
### Проверка
|
||||
```bash
|
||||
dig mail.<DOMAIN> A +short # → <PUBLIC_IP>
|
||||
dig <DOMAIN> MX +short # → mail.<DOMAIN>
|
||||
dig <DOMAIN> TXT +short # → v=spf1 ...
|
||||
dig _dmarc.<DOMAIN> TXT +short # → v=DMARC1 ...
|
||||
dig dkim._domainkey.<DOMAIN> 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 |
|
||||
@@ -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-ключи и доступы
|
||||
|
||||
| Хост | Порт | Метод |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
167
snippets/spaceweb-dns-api.py
Normal file
167
snippets/spaceweb-dns-api.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user