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:
Claude Code
2026-03-03 22:34:52 +00:00
parent 41f46e3ac2
commit 066855ce28
5 changed files with 370 additions and 7 deletions

View 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 |

View File

@@ -2,7 +2,7 @@
> ⚠️ **КОНФИДЕНЦИАЛЬНО** — не распространять за пределы команды > ⚠️ **КОНФИДЕНЦИАЛЬНО** — не распространять за пределы команды
> >
> Последнее обновление: 2026-02-27 > Последнее обновление: 2026-03-03
--- ---
@@ -42,6 +42,34 @@
| Email | `it5870@yandex.ru` | | Email | `it5870@yandex.ru` |
| Пароль | `1qaz!QAZ` | | Пароль | `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-ключи и доступы ## SSH-ключи и доступы
| Хост | Порт | Метод | | Хост | Порт | Метод |

View File

@@ -15,7 +15,7 @@
## Сервисы ## Сервисы
- **Nextcloud AIO** — `https://new.niikn.com`, управление: `https://new.niikn.com:8080` - **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` - **SMB** — `//192.168.1.79/share`, смонтирован на VM108 как `/mnt/ncsmb`
## Ключевые файлы на VM108 ## Ключевые файлы на VM108
@@ -24,12 +24,41 @@
- `/etc/samba/smb-niikn.creds` — учётные данные SMB - `/etc/samba/smb-niikn.creds` — учётные данные SMB
- `/home/cloud/setup-firewall.sh` — скрипт UFW - `/home/cloud/setup-firewall.sh` — скрипт UFW
## Ключевые файлы на VM106 (Mailcow) ## Mailcow (VM106, 192.168.1.128)
- `/opt/mailcow-dockerized/mailcow.conf` — конфиг Mailcow - **Web UI:** `https://mail.niikn.com` (через NPM proxy host #17)
- DBPASS=8VcUSgpKEOoxNojIZBRJx0FzMxzm - **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 ```bash
cd /opt/mailcow-dockerized cd /opt/mailcow-dockerized

View File

@@ -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~~ - [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:** создать Groupfolders, назначить права, scan (см. groupfolders-migration.md)
- [ ] **После rsync:** удалить External Storage ID 4 и 5 (SMB пока не удалять) - [ ] **После rsync:** удалить External Storage ID 4 и 5 (SMB пока не удалять)
- [ ] Настроить AIO Backup на VM108 - [ ] Настроить 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 - [ ] Проверить работу пользователей на new.niikn.com
- [ ] Решить судьбу Linkwarden и FileBrowser (переносить или нет) - [ ] Решить судьбу Linkwarden и FileBrowser (переносить или нет)
- [ ] Зафиксировать статический IP для VM100 (сейчас DHCP 192.168.1.245) - [ ] Зафиксировать статический IP для VM100 (сейчас DHCP 192.168.1.245)

View 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()