generated from ttipo/project-base
feat(docker): 메일 엔진 전용 DNS 리졸버(unbound)와 Spamhaus 조회 키(DQS)
엔진의 DNS 조회(DNSBL · SPF · DKIM · DMARC · MX · PTR · TLSA)를 공용 · ISP DNS 가 아니라 이 서버의 리졸버로 푼다. Spamhaus 는 공용 DNS 를 거친 조회를 거부하고, DNSSEC 을 검증해야 엔진의 DANE 이 켜지기 때문이다. - mail-resolver: unbound 1.26.2(digest 고정 · 비-root · cap_drop ALL). mail 과만 닿는 internal 네트워크 mail-dns 와 바깥 길 mail-dns-egress. 사설 주소의 답 · 질의를 막는다. - setup: 바꾸기 전에 리졸버 네트워크 · 주소 · DNSSEC 을 확인한다(UDP 는 루트 SOA, TCP 는 루트 DNSKEY — UDP DNSKEY 는 루트 키 교체 기간에 잘려 오탐이 난다). 엔진 DNS 는 운영 Custom, 개발 System. Spamhaus 키가 있으면 시험 항목을 확인한 뒤 DQS 로 켜고 없으면 두 항목을 끈다. 키는 모든 출력에서 가린다. - mail 은 리졸버를 기다리되(required: false) 리졸버 오류 하나로 메일 전체가 서지 않게 한다. - README: 리졸버와 스팸 차단 목록, 운영 배포 차례, 리졸버 장애 때(리로드 거절 · restart 비상 길), DNSSEC 이 깨진 도메인의 예외. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+244
-27
@@ -27,10 +27,12 @@
|
||||
|
||||
메일 엔진은 **app-network 대역 전체를 신뢰한다**(자동 차단 예외·게이트웨이 로그인). 그래서 app-network 에는 backend·db·redis·mail(개발은 mailpit 도)만
|
||||
붙이고, 인터넷을 향한 frontend 는 edge 로만 backend 에 닿는다. setup 이 앱 네트워크에 붙은 컨테이너를 확인한다.
|
||||
엔진의 DNS 는 메일 전용 리졸버(`mail-resolver`)가 맡는다 — mail 과 리졸버만 붙는 `mail-dns` 로 닿는다 ([DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl)).
|
||||
|
||||
| 구성 요소 | 위치 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| `mail` 서비스 | `docker-compose.yml` (profile `mail`) | named volume `<PROJECT_NAME>_mail-config`(config.json) · `<PROJECT_NAME>_mail-data`(RocksDB) |
|
||||
| `mail-resolver` | `docker-compose.yml` (profile `mail`) · 설정 `mail/resolver/unbound.conf` | 엔진 전용 재귀 DNS(unbound) — mail 만 닿는 `mail-dns`(internal)와 자기 바깥 길 `mail-dns-egress` 에만 붙는다. 상태 없음(볼륨 없음) — [DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl) |
|
||||
| dev 오버라이드 | `docker-compose.dev.yml` | 운영 포트 제거, `127.0.0.1:${MAIL_ADMIN_PORT}` → 8080 만 |
|
||||
| `mailpit` | `docker-compose.dev.yml` 전용 (profile `mail`) | 비-root(65534), UI `127.0.0.1:${MAILPIT_UI_PORT}` |
|
||||
| setup 스크립트 | `mail/scripts/setup.mjs` | 아래 "setup 스크립트가 하는 일" |
|
||||
@@ -58,7 +60,7 @@
|
||||
cp .env.development.example .env.development
|
||||
cp backend/.env.development.example backend/.env.development
|
||||
|
||||
# 1) 기동 — COMPOSE_PROFILES=mail 이 기본이라 backend·db·mail·mailpit 이 함께 뜬다
|
||||
# 1) 기동 — COMPOSE_PROFILES=mail 이 기본이라 backend·db·mail·mail-resolver(메일 전용 DNS)·mailpit 이 함께 뜬다
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d
|
||||
|
||||
# 2) 메일 엔진 설정 (처음 한 번 — 이후에도 env 를 바꾸면 다시 실행한다)
|
||||
@@ -88,6 +90,9 @@ node --env-file=.env.development mail/scripts/setup.mjs
|
||||
0. **사전 확인 (아무것도 바꾸기 전)** — app-network 의 실제 서브넷이 `APP_NETWORK_SUBNET` 인지, 앱 네트워크에 정해진 컨테이너
|
||||
(backend·db·redis·mail·mailpit)만 붙어 있는지, 백엔드 주소가 신뢰 대역 안인지. 어긋나면 **엔진 설정을 바꾸지 않고 중단**한다
|
||||
(신뢰 대역을 바꾼 뒤에 알면 전 직원의 메일이 멈추고 백엔드 주소가 자동 차단 예외에서 빠진다). 앱 네트워크에 다른 컨테이너가 있으면 운영은 중단, 개발은 경고.
|
||||
**메일 전용 리졸버**도 본다 — `mail-dns` 네트워크 · 리졸버 주소 · mail 의 `dns`, 그리고 메일 컨테이너에서 UDP · TCP 로 답하고 DNSSEC 을 검증하는지.
|
||||
닿지 않는 리졸버에 엔진의 DNS 를 맡기면 바깥 메일이 멈추므로 하나라도 어긋나면 중단한다(답 확인만은 개발에서 `[주의]` — 개발 메일은 Mailpit 이다). Spamhaus 키가 있으면 그 키로 시험 항목이 나오는지도 본다
|
||||
([DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl))
|
||||
1. **모드 감지** — bootstrap(첫 기동, 8080 만 열림) / bootstrap 완료·재시작 대기 / 정상
|
||||
2. **bootstrap** — 호스트명·기본 도메인·DKIM 키(ed25519·rsa)·로그(표준 출력) → 재시작.
|
||||
응답의 무작위 관리자 비밀번호는 한 번만 보이므로 `data/mail/bootstrap-admin.json`(git 무시)에 잠시 보관했다가 교체 후 지운다.
|
||||
@@ -112,13 +117,17 @@ node --env-file=.env.development mail/scripts/setup.mjs
|
||||
8. **엔진 한도** — 받는 사람 단위 받는 쪽 한도(같은 보낸 도메인 → 한 받는 사람, 기본 시간당 25)를 인증 없는 세션에만 건다 —
|
||||
로그인한 직원의 발송(웹메일)에는 걸지 않고 바깥에서 들어오는 메일에는 그대로. 설명·key 로 찾고, 없으면 만들지 않고 `[주의]` ([엔진 한도](#엔진-한도)).
|
||||
계정마다 임시 업로드 크기 합계(`x:Jmap` 의 `uploadQuota` — 1시간 동안 올린 양, 기본 50,000,000바이트)를 첨부 합계 한도의 20배(기본 524,288,000바이트 = 25MB × 20) 이상으로 올린다 —
|
||||
더 크게 둔 값은 내리지 않고, 개수 · 기간 · 한 번 크기는 그대로 ([엔진 한도](#엔진-한도) "임시 업로드 한도"). 둘 다 9 의 리로드로 반영된다
|
||||
더 크게 둔 값은 내리지 않고, 개수 · 기간 · 한 번 크기는 그대로 ([엔진 한도](#엔진-한도) "임시 업로드 한도").
|
||||
**엔진 DNS · Spamhaus** — 엔진의 DNS(`x:DnsResolver`)를 메일 전용 리졸버로(운영은 리졸버에 바로 `Custom`, 개발은 도커 DNS 를 거쳐 `System` — Mailpit 이름 때문),
|
||||
스팸 필터의 Spamhaus 두 항목은 키(`MAIL_SPAMHAUS_DQS_KEY`)가 있고 시험을 통과하면 DQS 로 켜고 아니면 끈다(공개 미러는 쓰지 않는다 — [DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl)).
|
||||
모두 9 의 리로드로 반영된다
|
||||
9. **설정 리로드 → 반영 확인** — 메일 컨테이너의 네트워크 주소로 `https://…:443/jmap/session` 이 403, 내부 8080 이 200 인지 본다.
|
||||
운영은 바깥에서 쓰는 주소 그대로(`MAIL_PUBLIC_URL/jmap/session`, 이 스크립트를 돌리는 호스트에서)도 403 인지 본다 —
|
||||
200 이면 실패(앞단 프록시가 8080 으로 넘기는 구성), 연결 자체가 안 되면 경고와 함께 바깥에서 확인할 `curl` 명령을 보여 준다.
|
||||
게이트웨이로 관리자를 대리 접근해 관리 API 가 열리는지도 데이터 없는 호출로 본다 — 열리면 매번 `[주의]` ([보안](#보안)).
|
||||
시스템 계정의 RCPT 도 메일 컨테이너 안에서 자기 네트워크 주소의 25 로 보내 550 인지 본다(RCPT 까지만 — 메일은 보내지 않는다).
|
||||
보낸 사람은 빈 봉투(`MAIL FROM:<>`)다 — 사내 도메인 밖이라 시범 공존의 MAIL FROM 위조 막기에 걸리지 않고, 어긋나 DATA 까지 가더라도 반송이 생기지 않는다
|
||||
보낸 사람은 빈 봉투(`MAIL FROM:<>`)다 — 사내 도메인 밖이라 시범 공존의 MAIL FROM 위조 막기에 걸리지 않고, 어긋나 DATA 까지 가더라도 반송이 생기지 않는다.
|
||||
엔진의 이번 기동 로그에 "DNS resolver cannot validate DNSSEC"(DANE 꺼짐) 경고가 없는지도 본다 — 리로드는 이 경고를 남기지 않아 기동 로그만 볼 수 있다
|
||||
10. **[개발] 외부 발송 경로** — Relay `mailpit:1025` + 발송 전략(사내 도메인은 로컬, 나머지는 mailpit) — 9 의 리로드로 반영된 것을 확인한 뒤에만 11
|
||||
(7 의 사내 목록 계정은 메일을 보내지도 받지도 못하므로 그보다 먼저 만들어도 된다)
|
||||
11. **[개발] 테스트 직원 계정** → 게이트웨이 대리 접근, 직원의 계정 목록 조회 차단(`Principal/get` → forbidden)까지 자체 확인
|
||||
@@ -221,12 +230,18 @@ node --env-file=.env.development mail/scripts/setup.mjs
|
||||
- `MAIL_HOSTNAME` 의 A 레코드 → 이 호스트 IP, 그 IP 의 PTR → `MAIL_HOSTNAME` ([PTR](#ptr))
|
||||
- 아웃바운드 25 허용 (클라우드는 기본 차단인 곳이 많다 — 막혀 있으면 [외부 릴레이](#외부-릴레이-ses-등))
|
||||
- 아웃바운드 HTTPS — Stalwart 는 기동 때 GitHub 에서 웹 포털을 내려받고, ACME(Let's Encrypt)로 인증서를 받는다
|
||||
- **메일 서버 전용 재귀 DNS** — 공용·ISP DNS(KT·Google·Cloudflare 등)로는 Spamhaus 조회가 막혀 스팸 판정이 틀어진다 ([DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl))
|
||||
- **스팸 차단 목록 이용 조건** — Spamhaus 는 회사용 무료 DQS 키를 쓰거나 해당 항목을 끈다. 나머지 목록도 조건 확인 (같은 절)
|
||||
- **메일 서버 전용 재귀 DNS** — compose 에 들어 있다(`mail-resolver` — 공용·ISP DNS(KT·Google·Cloudflare 등)로는 Spamhaus 조회가 막힌다).
|
||||
이 서버에서 **바깥 53(UDP · TCP)으로 나갈 수 있어야 한다** — 리졸버가 루트부터 각 도메인의 DNS 서버에 직접 묻는다. 막혀 있으면 setup 이 사전 확인에서 멈춘다
|
||||
([DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl))
|
||||
- **Spamhaus 이용 조건** — 공개 미러는 쓰지 않는다(소규모 · 비상업 용도). Spamhaus 의 "Blocklists via DNS Query" 키를 `.env.production` 의 `MAIL_SPAMHAUS_DQS_KEY` 에 넣으면
|
||||
두 항목을 그 키로 묻고, 비워 두면 두 항목이 꺼진다. **무료 계정은 Fair Use(소규모 · 비상업) 조건이다** — 회사가 대상인지는 Spamhaus 가 판단하고, 안 되면 끈 채로 두거나 유료(30일 체험).
|
||||
**체험 키는 30일이 지나면 답이 멈춘다** — 구독하거나 키를 비우고 setup (멈춘 줄은 다음 setup 이 종료 1 로 알린다 — 주기적으로 돌리거나 계정의 만료일을 본다).
|
||||
나머지 16개 목록도 이용 조건을 확인한다 (같은 절)
|
||||
- **엔진 한도** — 받는 쪽 한도 값, 직원마다 내보내는 양의 한도, 직원마다 1시간 업로드 한도(크기 · 개수)를 정한다 ([엔진 한도](#엔진-한도))
|
||||
- **포털(웹메일) 앞의 외부 리버스 프록시** — 요구사항은 [docker 스킬](../.claude/skills/docker/SKILL.md) "운영 환경" (같은 주소 배포 · `X-Forwarded-For` · IPv6 · 본문 한도·시간 제한).
|
||||
다른 서버의 Nginx Proxy Manager 라면 설정 · `TRUST_PROXY` 는 [리버스 프록시가 다른 서버에 있을 때](#리버스-프록시가-다른-서버에-있을-때-ncp-등--사설망)
|
||||
- **`APP_NETWORK_SUBNET`·`EDGE_NETWORK_SUBNET`** 이 그 호스트의 다른 도커 네트워크와 겹치지 않는다 (겹치면 `up` 이 "Pool overlaps" 로 실패한다)
|
||||
- **`APP_NETWORK_SUBNET`·`EDGE_NETWORK_SUBNET`·`MAIL_RESOLVER_SUBNET`** 이 그 호스트의 다른 도커 네트워크와 겹치지 않는다 (겹치면 `up` 이 "Pool overlaps" 로 실패한다)
|
||||
- 저장소는 기본 umask(022)로 클론한다 — `mail/resolver/unbound.conf` 를 리졸버 컨테이너 사용자(101:102)가 읽어야 한다(umask 077 이면 리졸버가 뜨지 않는다)
|
||||
- **rootful 도커 + iptables** (도커 기본값). 게시 포트(25·465·993·443)로 들어온 접속이 원래 주소 그대로 컨테이너에 보여야 한다 —
|
||||
rootless 도커(포트 전달이 주소를 바꾼다)나 `"iptables": false`(모든 접속이 docker-proxy 를 거친다)면 외부 접속이 도커 내부 주소로 보여
|
||||
자동 차단·게이트웨이 IP 제한이 무력화된다. 배포 뒤 [원래 접속 주소 확인](#원래-접속-주소-확인-배포-직후)으로 확인한다.
|
||||
@@ -272,6 +287,12 @@ node --env-file=.env.production mail/scripts/setup.mjs
|
||||
|
||||
- 3)의 `--recovery` 실행은 **사내 목록 계정**도 만든다 — 엔진 관리자는 그 권한을 내줄 수 없어 복구 관리자로만 된다 ([사내 목록 계정](#사내-목록-계정)).
|
||||
이 기능(8단계) 전부터 운영 중인 서버는 1)~5) 를 한 번 더 한다. 5)의 보통 실행은 그 계정이 있고 맞는지 읽어서 확인하고, 없으면 `[주의]` 만 낸다.
|
||||
- **DNS** — 2)의 `up` 이 메일 전용 리졸버(`mail-resolver`)를 mail 보다 먼저 띄운다. 3)에서 setup 이 리졸버를 확인한 뒤 엔진의 DNS 를 그쪽으로 바꾸고(`[변경] 엔진 DNS — … Custom`),
|
||||
4)에서 다시 만든 mail 은 그 설정으로 뜬다 — 5)의 출력에 `[확인] 메일 전용 리졸버 …` 와 `[확인] 엔진 기동 로그(…)에 DNSSEC 경고가 없다` 가 있어야 한다.
|
||||
Spamhaus 키가 있으면 `[확인] Spamhaus 키 … 시험 항목 …` 두 줄, 없으면 `[주의] MAIL_SPAMHAUS_DQS_KEY 가 없어 …` 다
|
||||
([DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl)).
|
||||
**리졸버 전에 띄운 서버**(이 기능 전 배포)는 `sudo docker compose --env-file .env.production up -d`(mail 이 다시 만들어진다 — 몇 초 끊긴다) 뒤 5)와 같은 형태로 setup 을 한 번 돌린다.
|
||||
`up` 이 `optional dependency "mail-resolver" failed to start` 경고를 내면 mail 은 떴지만 리졸버가 unhealthy · 멈춤이다 — [리졸버 장애 때](#리졸버-장애-때).
|
||||
- 운영 setup 은 끝에 **권장 DNS 레코드**(DKIM·SPF·MX·DMARC·SRV·MTA-STS·autoconfig)를 출력한다. 그대로 붙이지 말고 아래 전환 절차 순서를 따른다.
|
||||
- 공개 443 차단은 setup 이 컨테이너 안과 `MAIL_PUBLIC_URL`(호스트에서) 두 곳에서 확인한다. 호스트에서 자기 공인 주소로 되돌아오는 경로가 막힌 환경이면
|
||||
경고만 남기므로, 그때는 바깥에서 확인한다: `curl -s -o /dev/null -w '%{http_code}\n' https://<MAIL_HOSTNAME>/jmap/session` → `403`.
|
||||
@@ -350,29 +371,136 @@ Gmail·네이버 등이 거부하거나 스팸 처리하지 않는다.
|
||||
|
||||
스팸 필터는 기본으로 외부 차단 목록(DNSBL) 18개를 DNS 로 조회한다 — Spamhaus(`zen` IP · `dbl` 도메인), SURBL, URIBL, Barracuda, SpamCop,
|
||||
DNSWL, Mailspike, SenderScore 등 (목록: 관리 화면 Settings › Spam Filter › DNSBL, 또는 `x:SpamDnsblServer/query`).
|
||||
이 목록들은 Stalwart 와 **별개 서비스라 각자 이용 조건이 있다.**
|
||||
이 목록들은 Stalwart 와 **별개 서비스라 각자 이용 조건이 있다.** 그리고 공용 · ISP DNS(KT `168.126.63.1` · `8.8.8.8` · `1.1.1.1` 등)를 거친 조회는
|
||||
Spamhaus 가 거부한다 — 오류 코드 `127.255.255.254` (2026-10-26 개발 환경의 KT DNS 로 늘 등록된 시험 주소를 물어 확인).
|
||||
그래서 **메일 엔진 전용 재귀 리졸버**를 compose 에 두고 엔진의 DNS 를 그쪽으로 돌린다. DNSSEC 도 검증해 기동 로그의
|
||||
"DNS resolver cannot validate DNSSEC. DANE has been disabled" 경고가 사라진다.
|
||||
|
||||
1. **메일 서버 전용 재귀 DNS 를 둔다.**
|
||||
- 공용·ISP DNS(KT `168.126.63.1`, `8.8.8.8`, `1.1.1.1` 등)로 조회하면 Spamhaus 가 거부하고 오류 코드 `127.255.255.254` 를 돌려준다.
|
||||
2026-10-26 개발 환경(KT DNS)에서 항상 등록돼 있는 시험 주소를 조회해 이 오류 코드가 오는 것을 확인했다.
|
||||
- 전용 재귀 리졸버(unbound 등)를 메일 호스트나 같은 compose 네트워크에 두고, Stalwart 의 DNS 리졸버 설정을 그쪽으로 바꾼다
|
||||
(관리 화면 Settings › Network › DNS › DNS Resolver. 관리 API 로는 `x:DnsResolver` — 지금은 `@type: System`, 다른 유형의 필드 이름은 관리 화면에서 확인).
|
||||
- DNSSEC 검증도 되어 기동 로그의 "DNS resolver cannot validate DNSSEC. DANE has been disabled" 경고가 사라진다.
|
||||
2. **Spamhaus 이용 조건을 맞춘다.**
|
||||
- 공개 목록(public mirror)은 소규모·비상업용이다. 회사 메일 서버는 **무료 DQS 키**(사용량 한도 안)를 발급받아
|
||||
Spamhaus 두 항목(`STWT_RBL_SPAMHAUS_IP`, `STWT_DBL_SPAMHAUS_DOMAIN`)의 조회 주소를 DQS 주소로 바꾸거나,
|
||||
조건이 맞지 않으면 두 항목을 끈다 (`x:SpamDnsblServer/set` 으로 `enable: false` → `ReloadSettings`).
|
||||
- 나머지 목록도 운영 전에 각 서비스의 이용 조건(상업적 사용, 조회량 한도, 사전 등록 필요 여부)을 확인하고 맞지 않는 것은 끈다.
|
||||
3. **조회 거부 응답의 처리를 시험한다 (미검증).**
|
||||
`127.255.255.254` 같은 "조회 거부" 응답을 "목록에 있음"으로 처리하면 정상 메일까지 스팸 점수를 받는다는 사용자 보고가 있다(Stalwart 공식 확인 아님).
|
||||
운영 전에 외부에서 시험 메일을 받아 스팸 판정 기록에 Spamhaus 항목이 잘못 붙지 않는지 확인한다.
|
||||
#### 메일 전용 리졸버 (`mail-resolver`)
|
||||
|
||||
```
|
||||
mail ──(mail-dns — internal, MAIL_RESOLVER_SUBNET)──▶ mail-resolver MAIL_RESOLVER_IP:53 ──(mail-dns-egress)──▶ 루트 · 각 도메인의 DNS 서버
|
||||
└──(app-network)──▶ 바깥 25 · 443 (메일 · ACME) — 그대로 (서버 자신의 공인 IP 로)
|
||||
```
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| 서비스 | `docker-compose.yml` 의 `mail-resolver` (profile `mail` — mail 과 함께 뜬다, mail 은 리졸버가 healthy 가 된 뒤에 뜬다. 리졸버가 unhealthy 거나 떠 있지 못하면 compose 가 경고만 내고 mail 을 띄운다 — `required: false`, [리졸버 장애 때](#리졸버-장애-때)). 설정 [`resolver/unbound.conf`](./resolver/unbound.conf) |
|
||||
| 이미지 | `klutchell/unbound:1.26.2` — NLnet Labs 원본 소스를 체크섬으로 확인해 빌드한 distroless 이미지(셸 없음, `drill` 포함 · 비공식). digest `sha256:50cbeb32b583…`(2026-10-30 `latest` 와 같음). **1.26.2 는 보안 릴리스**(2026-10-21 — CVE-2026-96405 이름 압축 풀기로 서비스 저하: 바깥 DNS 서버의 응답으로 닿는다)라 2주를 기다리지 않았다 ([docker 스킬](../.claude/skills/docker/SKILL.md) "이미지 버전 고정") |
|
||||
| 네트워크 | `mail-dns` — mail ↔ 리졸버 전용, **internal**(이 네트워크로는 바깥에 못 나간다), 서브넷 `MAIL_RESOLVER_SUBNET`(기본 `172.16.73.0/24`) · 리졸버 주소 `MAIL_RESOLVER_IP`(기본 `172.16.73.254` — 서브넷의 끝 쪽. 도커는 mail 에 앞쪽부터 차례로 준다). `mail-dns-egress` — 리졸버의 바깥 조회 길, 리졸버 혼자(서브넷은 도커가 정한다). **app-network(신뢰 대역)에는 붙이지 않는다** |
|
||||
| 메일 엔진만 닿게 | 두 네트워크에 붙는 것은 mail · 리졸버뿐(setup 이 확인 — 운영은 그 밖이 있으면 중단). 호스트 포트를 열지 않는다. unbound 의 `access-control` 은 사설 대역 · 자기 자신만 받는다 — 누가 53 을 공개해도 바깥에서 쓰는 열린 리졸버가 되지 않게. **알려진 한계** — `mail-dns` 는 양쪽으로 열린 브리지라 리졸버 쪽에서도 mail 의 포트(관리 API 8080 포함)에 닿는다. 그 주소는 신뢰 대역(`APP_NETWORK_SUBNET`) 밖이라 자동 차단 예외 · 게이트웨이 로그인은 받지 못하고, 관리 API 는 관리자 자격 증명이 있어야 한다 — 리졸버가 뚫렸을 때의 일이라 HTTP 정책 식은 바꾸지 않았다 |
|
||||
| 실행 | 비-root `101:102`(이미지의 unbound 사용자 — 이미지는 root 로 떠서 내려가는 꼴이라 처음부터 이 사용자로 띄운다) · 권한(capability) 모두 뺌 · `no-new-privileges` · 메모리 128m. 비-root 가 53 에 붙는 것은 도커가 컨테이너마다 `net.ipv4.ip_unprivileged_port_start=0` 으로 두기 때문이다 |
|
||||
| DNSSEC | 검증한다 — 루트 신뢰 앵커는 이미지에 든 `root.key`(KSK-2017 · KSK-2024)이고 RFC 5011 로 스스로 고쳐 쓴다(그래서 `read_only` 로 두지 않는다). 2026-10-30 의 루트 DNSKEY 는 KSK-2024(key tag 38696)가 서명한다 |
|
||||
| 그 밖의 설정 | 사설 주소로 풀리는 바깥 이름은 버린다(`private-address` — 바깥 도메인의 MX 가 VPC · 도커 네트워크를 가리켜도 엔진이 그쪽으로 보내지 않게, **127/8 은 넣지 않는다** — DNSBL 은 127.0.0.x 로 답한다) · 리졸버 자신도 사설 주소의 DNS 서버에는 묻지 않는다(`do-not-query-address` — 같은 대역. 바깥 도메인이 이름 서버 주소(glue)를 사설 주소로 두어 리졸버가 VPC · 호스트 · 도커 네트워크의 53 에 묻게 만드는 것을 막는다, 127/8 은 기본값이 막는다) · 조회 이름은 필요한 만큼만 위로(QNAME minimisation — 루트 · .net 서버는 DQS 키가 든 이름을 보지 못한다) · 로그는 동작 정보만(`verbosity: 1` — 조회 이름을 남기지 않는다) |
|
||||
| healthcheck | `drill-hc localhost @127.0.0.1` — 데몬이 답하는지만 본다(바깥 DNS 가 잠깐 막혀도 mail 이 뜨지 못하는 일이 없게). 바깥 조회 · DNSSEC 은 setup 이 본다 |
|
||||
|
||||
**compose 서비스로 둔 까닭** (호스트에 unbound 를 까는 것과 견줌):
|
||||
|
||||
- **개발 · 운영이 같은 정의다** — 개발(Docker Desktop)에서 그대로 띄워 시험하고, 버전 고정 · CI 검사(`unbound-checkconf`) · 배포 절차(`up -d`)에 들어간다. 호스트 패키지는 저장소 밖의 상태라 서버를 다시 만들 때 빠뜨리기 쉽다.
|
||||
- **메일 엔진만 닿게 네트워크로 가를 수 있다** — 호스트에 깔면 모든 컨테이너(인터넷을 향한 frontend 포함)가 호스트 주소로 닿아, 막으려면 도커의 iptables 와 얽힌 호스트 방화벽 규칙이 필요하다. Ubuntu 의 systemd-resolved(127.0.0.53:53)와 자리도 겹친다.
|
||||
- 대가 — 이미지를 판까지 고정하고 우리가 올린다(호스트 apt 는 자동 보안 업데이트를 받는다). NLnet Labs 의 보안 릴리스 공지(<https://nlnetlabs.nl/projects/unbound/security-advisories/>)를 본다. 리졸버는 엔진 DNS 의 단일 지점이다 — 멈추면 바깥 메일의 DNS 가 모두 실패한다(보내는 메일은 대기열에서 다시 시도, 받는 메일은 검사가 약해진다). 그래서 공용 DNS 로 넘어가는 대체 길은 두지 않는다 — 넘어가면 Spamhaus 가 다시 막는다.
|
||||
|
||||
**엔진의 DNS** (`x:DnsResolver` 싱글톤 — 0.16.27 의 `@type` 은 `System` · `Custom` · `Cloudflare` · `Quad9` · `Google`, `Custom` 은 `servers { "<n>": { protocol udp|tcp|tls, address(IP), port } }`.
|
||||
`@type` 만 바꾸는 부분 갱신을 받고 공통 칸(`attempts` · `timeout` · `enableEdns` …)은 그대로 둔다 — 2026-10-30 개발 엔진에서 읽고 바꿔 확인):
|
||||
|
||||
| 환경 | setup 이 맞추는 값 | 길 | 까닭 |
|
||||
| --- | --- | --- | --- |
|
||||
| 운영 | `Custom` — `MAIL_RESOLVER_IP:53` 의 UDP + TCP | 엔진 → 리졸버 | 도커 DNS 를 거치지 않아 도커 내부 이름(backend · db …)이 엔진에 풀리지 않는다 — 바깥 도메인의 MX 가 내부 이름을 가리켜도 닿지 않는다. 엔진은 TCP 연결이 있어야 그 리졸버로 DNSSEC 을 믿는다(소스 `common/src/config/smtp/resolver.rs` 의 `ensure_dnssec`) |
|
||||
| 개발 | `System` | 엔진 → 도커 DNS(127.0.0.11) → 리졸버 | 개발의 외부 발송 경로가 도커 내부 이름 `mailpit` 이다 — `Custom` 이면 그 이름을 풀지 못해 바깥 주소 앞 메일이 `delivery.ip-lookup-failed` 로 **영구 실패(반송)** 했다(2026-10-30 개발). 도커 DNS 는 바깥 이름을 compose 의 `dns`(= 리졸버)로 넘기고 DNSSEC 자료(RRSIG)도 UDP · TCP 그대로 넘긴다 |
|
||||
|
||||
- mail 컨테이너의 `dns` 는 두 환경 모두 이 리졸버다 — 컨테이너 안의 이름 풀기(ACME · 웹 포털 내려받기 · setup 의 `getent` · `curl`)도 리졸버로 간다.
|
||||
- 바뀐 값은 setup 의 `ReloadSettings` 로 반영된다(리로드가 엔진 설정 전체를 다시 만든다). 다만 DNSSEC 확인의 경고는 **기동 때만** 로그에 남는다(리로드 응답 · 로그에는 없다) —
|
||||
setup 은 이번 기동의 로그에서 그 경고를 찾는다(`[확인] 엔진 기동 로그(…)에 DNSSEC 경고가 없다`). 엔진 DNS 를 그 실행에서 바꿨다면 다음 기동부터 새 설정의 확인이다.
|
||||
- **서버를 다시 켜면** 도커가 compose 의 차례(리졸버 먼저)를 지키지 않아 mail 이 먼저 뜰 수 있다 — 그러면 그 기동은 DNSSEC 을 믿지 못한 채(DANE 꺼짐) ·
|
||||
스팸 필터의 Pyzor 없이(주소를 풀지 못해 그 항목만 빼고 뜬다 — [리졸버 장애 때](#리졸버-장애-때) 3.) 뜨고,
|
||||
바깥 DNS 는 리졸버가 뜬 뒤부터 된다. 재부팅 뒤에는 `sudo docker compose --env-file .env.production restart mail` 뒤 setup 으로 `[확인]` 을 본다
|
||||
(개발에서 리졸버를 멈춘 채 mail 을 띄워 이 경고가 남고 setup 이 `[주의]` 로 알리는 것을 확인했다 — 2026-10-30).
|
||||
|
||||
**setup 이 하는 일** (DNS):
|
||||
|
||||
1. **사전 확인 — 바꾸기 전에** (어긋나면 아무것도 바꾸지 않고 종료 1): `mail-dns` 의 서브넷이 `MAIL_RESOLVER_SUBNET` 이고 internal 인지 · 구성원이 mail · 리졸버뿐인지 ·
|
||||
`mail-dns-egress` 에 리졸버만 붙었는지(구성원은 운영만 중단, 개발은 `[주의]`) · 리졸버가 떠 있고 주소가 `MAIL_RESOLVER_IP` 인지 · mail 이 `mail-dns` 에 있고 `dns` 가 그 주소인지,
|
||||
그리고 **메일 컨테이너 안에서** 그 주소로 DNSSEC 자료와 함께(DO) 물어 검증된 답(ad)과 서명(RRSIG)이 오는지 — 엔진과 같은 자리 · 같은 주소다.
|
||||
**UDP 는 루트 SOA**(389B 안팎), **TCP 는 루트 DNSKEY**(엔진의 기동 확인과 같은 질문 — 소스 `ensure_dnssec`)를 묻는다.
|
||||
UDP 로 루트 DNSKEY 를 묻지 않는 까닭: 지금 1,139B 인데 루트 키 교체(ZSK 미리 올리기 · KSK-2017 폐기) 동안에는 1,400B 를 넘어 EDNS 1232 의 UDP 로는
|
||||
잘린다(TC — 답 0 · 서명 0). 엔진은 그때 TCP 로 다시 묻으므로 멀쩡한 리졸버인데 setup 만 멈추게 된다(2026-10-30 리뷰 — EDNS 1100 으로 잘리는 조건을 만들어 확인).
|
||||
이 답 확인은 **운영만 멈추고 개발은 `[주의]` 뒤 이어 간다** — 바깥 53 이 막힌 개발 망에서도 개발 메일(Mailpit — 도커 DNS 가 푸는 이름)은 되기 때문이다.
|
||||
다만 리졸버가 바깥에 전혀 닿지 않으면 뒤의 설정 리로드를 엔진이 거절해(Pyzor 주소를 같은 리졸버로 푼다 — [리졸버 장애 때](#리졸버-장애-때) 3.) 개발 setup 도 거기서 종료 1 이다
|
||||
(2026-10-30 개발에서 리졸버의 바깥 길을 끊어 확인 — 저장한 설정은 남고 `restart mail` 때 적용된다). 개발 PC 도 리졸버가 바깥 53 에 닿아야 setup 이 끝난다.
|
||||
(이미지에 `dig` 이 없어 이미지의 perl 로 묻는다 — Debian 기본 `perl-base` 만 쓴다. 이미지에 perl 이 없으면(`docker exec` 종료 126 · 127) 리졸버 문제가 아니라 확인 도구 문제로 따로 알린다.)
|
||||
2. **엔진 DNS** — 위 표의 값으로(`[유지]` · `[변경] … · 전 System`). 견주는 것은 `@type` 과 서버마다의 protocol · address · port 뿐이다 —
|
||||
엔진은 `Custom` 의 servers 를 넣은 그대로 읽어 준다(2026-10-30 개발 엔진에 잠깐 저장해 확인), 엔진이 나중에 칸을 더 붙여 읽어 줘도 매번 `[변경]` 이 되지 않게.
|
||||
3. **Spamhaus 두 항목** — 아래.
|
||||
4. **반영 확인** — 리로드 뒤 이번 기동 로그에 DNSSEC 경고가 없는지. 그 기동의 로그가 남아 있지 않으면(기동 첫 줄 `server.startup` 이 없다 — 로그 회전 등)
|
||||
"경고 없음"이 아니라 `[주의] … 확인하지 못했습니다` 다(확인하려면 `restart mail` 뒤 setup — 몇 초 끊긴다).
|
||||
|
||||
`MAIL_RESOLVER_SUBNET` · `MAIL_RESOLVER_IP` 는 setup 도 검사한다 — 사설 대역 · 네트워크 주소 · `/24`~`/29` · 앱 네트워크와 겹치지 않음 · 주소는 그 안이고 네트워크 주소 · 첫 주소(도커 게이트웨이) · 끝 주소가 아님.
|
||||
바꿀 때는 둘을 함께 고치고 `docker compose … down` 후 `up -d`(네트워크를 다시 만든다) → setup.
|
||||
|
||||
#### Spamhaus — 조회 키(`MAIL_SPAMHAUS_DQS_KEY`)
|
||||
|
||||
- **공개 미러(`zen.spamhaus.org` · `dbl.spamhaus.org`)는 쓰지 않는다** — Spamhaus 의 Fair Use(소규모 · 비상업) 용도다.
|
||||
- 키는 Spamhaus 의 **"Blocklists via DNS Query"**(옛 Data Query Service — DQS) 계정에서 받는다. **무료 계정은 Spamhaus 의 Fair Use(소규모 · 비상업) 조건**이다 —
|
||||
회사가 대상인지는 Spamhaus 가 판단하고(신청 페이지: "If you are a non-commercial entity or individual with low query volumes, you may qualify"),
|
||||
안 되면 두 항목을 끈 채로 두거나 유료(상업용 30일 체험 뒤 구독)로 간다.
|
||||
[무료 계정 신청](https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account/) ·
|
||||
[공개 미러에서 옮기기](https://docs.spamhaus.com/datasets/docs/source/70-access-methods/data-query-service/015-migrating-to-dqs.html) ·
|
||||
[시험](https://docs.spamhaus.com/datasets/docs/source/70-access-methods/data-query-service/060-dqs-testing.html)
|
||||
- 키는 **서버의 `.env.production` 에만** 둔다(`MAIL_SPAMHAUS_DQS_KEY=` — 26자 영문 · 숫자). 파일에 없거나 파일과 다른 값이 셸에 있으면 setup 이 멈춘다(셸 값으로는 그 실행만 켜지고 다음 실행이 끈다). 개발은 비워 둔다.
|
||||
- **체험(30일) 키는 기간이 끝나면 답이 멈춘다** — 그 전에 구독하거나, 키를 비우고 setup 을 돌려 두 항목을 끈다. 계정이 끝나거나 키가 멈추면
|
||||
엔진의 Spamhaus 조회는 실패하고 표시(tag)가 붙지 않는다 — 메일은 그대로 받는다(닫히지 않고 열린 쪽 · fail-open). 조회마다 몇 초 늦어질 수 있다
|
||||
(틀린 키는 SERVFAIL 5초 남짓이거나 답이 없었다 — 아래 표). 엔진 로그에는 남지 않는다(조회 실패 `spam.dnsbl-error` 도 debug — 아래 "키가 남는 곳").
|
||||
**setup 은 다음에 돌 때에야** 시험 항목을 받지 못해 두 항목을 끄고 종료 1 로 알린다 — 그래서 운영은 setup 을 주기적으로 돌려 종료 0 인지 보거나 Spamhaus 계정의 만료일을 본다.
|
||||
|
||||
| 상태 | setup 이 하는 일 |
|
||||
| --- | --- |
|
||||
| 키 있음 · 시험 통과 | 그 항목을 **DQS 로 켠다** — 조회 이름 식 `ip_reverse + '.<키>.zen.dq.spamhaus.net'` · `value + '.<키>.dbl.dq.spamhaus.net'`. 응답 코드가 공개 미러와 같아 tag 식(점수)은 그대로 |
|
||||
| 키 있음 · 시험 실패 | 그 항목을 끄고 실패로 보고(종료 1) — 시험은 메일 컨테이너에서 리졸버로 늘 등록된 시험 항목을 묻는다: `2.0.0.127` → `127.0.0.2`(ZEN, `127.0.0.4` · `127.0.0.10` 도 온다) · `dbltest.com` → `127.0.1.2`(DBL). 형식만 맞는 가짜 키는 SERVFAIL(5초 남짓)이거나 12초 안에 답이 없었다(2026-10-30) |
|
||||
| 키 없음 | 두 항목을 끈다 — 운영 `[주의]`, 개발 `[참고]`(개발도 운영처럼 끈다: 공개 미러의 비상업 조건은 회사 개발 PC 에도 같고, 키는 운영 서버에만 둔다) |
|
||||
| 끌 때 | 조회 이름 식도 엔진 기본(공개 미러)으로 되돌린다 — 옛 키가 엔진에 남지 않게. 지금 식이 무엇이었는지는 키 없이 알린다(`전 켜짐 · 공개 미러 …` · `DQS …(다른 키)`) |
|
||||
|
||||
- **키가 남는 곳** — setup 은 키를 출력하지 않는다(식 · 옛 식도). 엔진의 오류 응답 · 거절 사유에 실려 와도 setup 의 출력(`[변경]` · `[주의]` · `[실패]` 줄 모두)은
|
||||
키를 `<MAIL_SPAMHAUS_DQS_KEY>` 로 바꿔 찍는다. 시험 조회 이름은 표준 입력으로만 넘긴다(`docker exec` 명령줄 · 도커 이벤트에 남지 않게).
|
||||
⚠️ 그러나 **엔진 설정(두 항목의 조회 이름 식)에 키가 그대로 저장된다** — 관리 화면 · 관리 API(`x:SpamDnsblServer/get`) · 메일 볼륨 백업으로 보인다.
|
||||
조회 자체에도 키가 실린다(평문 DNS — DQS 의 방식이다. 위 QNAME minimisation 으로 Spamhaus 앞 단계는 보지 못한다).
|
||||
엔진은 DNSBL 조회 이름을 debug 수준에서만 남긴다 — 답(`spam.dnsbl`)도, 실패(`spam.dnsbl-error` — 조회 이름 전체와 리졸버 오류를 싣는다)도 따로 정한 수준이 없어
|
||||
기본값 debug 다(0.16.27 소스 `crates/trc/src/event/enums_impl.rs` 의 `EventType::level`). 그래서 키가 멈춰 조회가 모두 실패해도 setup 이 둔 info 로그에는 쏟아지지 않는다.
|
||||
로그 수준을 debug 로 올리면 키가 로그에 남는다.
|
||||
- 바꾸거나 비우면 setup 을 다시 실행한다. 항목을 지웠거나 이름을 바꿨으면 setup 은 만들지 않고 `[주의]` 만 낸다.
|
||||
- **나머지 16개 목록** — 운영 전에 각 서비스의 이용 조건(상업적 사용 · 조회량 · 사전 등록)을 확인하고 맞지 않는 것은 관리 화면에서 끈다(setup 은 Spamhaus 두 항목만 맡는다).
|
||||
- **조회 거부 응답의 점수** — 0.16.27 기본값에서 Spamhaus 의 거부 · 오류 응답 tag(`RBL_SPAMHAUS_BLOCKED_OPENRESOLVER` · `RBL_SPAMHAUS_BLOCKED` · `DBL_BLOCKED_OPENRESOLVER` · `DBL_BLOCKED`, 그리고 `RECEIVED_…`)는
|
||||
**0점**이다(개발 엔진의 `x:SpamTag`, 2026-10-30) — 거부 응답이 정상 메일의 점수를 올리지는 않는다(점수가 붙는다는 사용자 보고는 이 판에서는 아니다). 이 리졸버로는 그 응답이 오지 않는다.
|
||||
|
||||
```bash
|
||||
# 리졸버가 Spamhaus 에 막혀 있는지 확인 — 127.0.0.2 는 항상 등록된 시험 주소다.
|
||||
# 정상이면 127.0.0.x 가 돌아오고, 127.255.255.254 면 그 리졸버는 막혀 있다.
|
||||
sudo docker exec "${PROJECT_NAME}-backend" nslookup 2.0.0.127.zen.spamhaus.org <리졸버 IP>
|
||||
# 리졸버가 Spamhaus 에 막혀 있지 않은지 — 127.0.0.2 는 늘 등록된 시험 주소다 (공개 미러 한 번 — 연결 확인용)
|
||||
sudo docker exec "${PROJECT_NAME}-mail-resolver" drill 2.0.0.127.zen.spamhaus.org @127.0.0.1 A
|
||||
# 127.0.0.x 가 오면 정상, 127.255.255.254 면 막혀 있다(공용 · 열린 리졸버로 보인다)
|
||||
# DNSSEC — dnssec.works 는 flags 에 ad, fail01.dnssec.works 는 SERVFAIL
|
||||
sudo docker exec "${PROJECT_NAME}-mail-resolver" drill -D dnssec.works @127.0.0.1 A
|
||||
sudo docker exec "${PROJECT_NAME}-mail-resolver" drill fail01.dnssec.works @127.0.0.1 A
|
||||
# 엔진 기동 로그의 DNSSEC 경고 — 나오지 않아야 한다
|
||||
sudo docker logs "${PROJECT_NAME}-mail" 2>&1 | grep 'cannot validate DNSSEC'
|
||||
# DQS 키는 명령줄에 적지 않는다 — setup 의 "[확인] Spamhaus 키 … 시험 항목 …" 줄로 본다
|
||||
```
|
||||
|
||||
확인 (2026-10-30, 개발 — Docker Desktop 28.5 · KT 망, 엔진 0.16.27):
|
||||
|
||||
- 리졸버로 `2.0.0.127.zen.spamhaus.org` → `127.0.0.10` · `127.0.0.4` · `127.0.0.2`(UDP · TCP), `dbltest.com.dbl.spamhaus.org` → `127.0.1.2` — 같은 PC 의 KT DNS 로는 `127.255.255.254` 였다.
|
||||
`dnssec.works` 는 ad, `fail01.dnssec.works` 는 SERVFAIL. 도커 DNS 를 거쳐도(`dns` 지정) 루트 DNSKEY 의 ad · RRSIG 가 UDP · TCP 그대로 왔다.
|
||||
- 엔진 기동 로그 — 리졸버 전(2026-10-26 기동, KT DNS)에는 `DnsResolver … cannot validate DNSSEC` 경고가 있었고, 리졸버 뒤 `System`(개발) · `Custom`(운영 꼴을 개발에서 잠시) 기동 모두 없었다.
|
||||
- 메일 — 465 인증 제출로 바깥 주소(Mailpit 에 도착) · 사내 주소(받은편지함), 25 인증 없는 수신(`someone@gmail.com` → doyun.lee)의 `spf=softfail`(google 의 SPF 를 실제로 따라감) · `dmarc=fail … policy.dmarc=none` 이 그대로 나왔다.
|
||||
`Custom` 으로 바꾼 동안은 Mailpit 경로가 위 표처럼 영구 실패했고, setup 으로 `System` 에 되돌린 뒤 다시 Mailpit 에 왔다.
|
||||
- setup — 처음 `[변경]` 둘(Spamhaus 두 항목 끔), 다시 돌리면 바뀐 줄 0 · 종료 0. 형식만 맞는 가짜 키 → 두 항목 꺼진 채 종료 1 · 출력에 키 없음. 리졸버를 멈추면 사전 확인에서 종료 1(바뀐 것 없음).
|
||||
`mail-dns` 에 다른 컨테이너를 붙이면 개발은 `[주의]` · 종료 0. **실제 Spamhaus 키로는 아직 시험하지 않았다** — 키를 받은 뒤 운영에서 `[확인] Spamhaus 키 …` 두 줄을 본다.
|
||||
- 리뷰 반영(같은 날) — 루트 DNSKEY(DO)는 1,139B 라 메일 컨테이너에서 EDNS 1100 으로 물으면 UDP 가 잘렸다(TC · 답 0 · 서명 0 — 예전 사전 확인이면 멈췄을 자리).
|
||||
같은 조건에서 지금 확인(UDP 루트 SOA · TCP 루트 DNSKEY)은 둘 다 ad · 서명 1 로 통과했다. compose 의 `required: false` — 리졸버 healthcheck 실패(unhealthy) ·
|
||||
설정 오류로 바로 끝남(`exited (1)`) · 운영 재시작 정책(`unless-stopped`)으로 재시작을 되풀이함 모두 `optional dependency "mail-resolver" failed to start` 경고 뒤
|
||||
mail 이 떴다(5~9초). 바꾸기 전 설정(`required` 없음)은 `dependency failed to start: container … is unhealthy` 로 끝나고 mail 이 `Created` 에 남았다(옛 mail 은 지워진 채).
|
||||
`up -d --no-deps mail` 은 리졸버를 기다리지 않는다. 엔진의 `x:DnsResolver` 를 잠깐 `Custom` 으로 저장해 읽으면 servers 가 넣은 그대로였다(되돌림).
|
||||
`domain-insecure` 한 줄은 그 도메인만 검증을 끈다(`fail01.dnssec.works` SERVFAIL → NOERROR, `dnssec.works` 는 그대로 ad, `fail02.dnssec.works` 는 그대로 SERVFAIL — 시험용 리졸버 컨테이너).
|
||||
리졸버의 바깥 길을 끊으면(캐시도 비움) 개발 setup 은 사전 확인을 `[주의]` 로 넘기고 리로드에서 엔진의 거절(Pyzor 주소)로 종료 1 이었다 — 바뀐 것 0.
|
||||
그 상태에서 엔진 DNS `Quad9` 저장 → 리로드 거절 → `restart mail` 은 DNSSEC 경고 없이 떴다(비상 길 — [리졸버 장애 때](#리졸버-장애-때)). 모두 되돌린 뒤 setup 은 `[확인]` · 바뀐 줄 0.
|
||||
|
||||
참고: [Spamhaus — 공용 DNS 로 무료 목록 쓰기](https://www.spamhaus.com/resource-center/successfully-accessing-spamhauss-free-block-lists-using-a-public-dns/),
|
||||
[Spamhaus — KT 경유 조회 안내](https://submit.spamhaus.org/resources/query-the-legacy-dnsbls-via-korea-telecom/),
|
||||
[Stalwart DNSBL 문서](https://www.stalw.art/docs/spamfilter/dnsbl)
|
||||
@@ -953,9 +1081,9 @@ PROXY protocol(2 — 리스너별 `overrideProxyTrustedNetworks`, 검증 필요)
|
||||
| 할 일 | 값 |
|
||||
| --- | --- |
|
||||
| 서버 B 만들기 | 서버 A 와 **같은 VPC** 의 Public 서브넷, 공인 IP 하나, 메모리 8GB (11단계의 ClamAV 까지 같은 서버에 올린다). 다른 VPC 면 Peering 이 있어야 한다 — 이 절은 같은 VPC 만 다룬다 |
|
||||
| VPC 대역 | `APP_NETWORK_SUBNET`(기본 `172.16.71.0/24`) · `EDGE_NETWORK_SUBNET`(기본 `172.16.72.0/24`)과 겹치지 않게 — 겹치면 서버 B 가 그 대역의 VPC 주소(서버 A 등)로 갈 길을 도커 브리지로 보낸다. 겹치면 두 값을 다른 사설 /24 로 바꾸고 mail setup 을 다시 돌린다 |
|
||||
| VPC 대역 | `APP_NETWORK_SUBNET`(기본 `172.16.71.0/24`) · `EDGE_NETWORK_SUBNET`(기본 `172.16.72.0/24`) · `MAIL_RESOLVER_SUBNET`(기본 `172.16.73.0/24`)과 겹치지 않게 — 겹치면 서버 B 가 그 대역의 VPC 주소(서버 A 등)로 갈 길을 도커 브리지로 보낸다. 겹치면 그 값을 다른 사설 /24 로 바꾸고(리졸버는 `MAIL_RESOLVER_IP` 도 함께) mail setup 을 다시 돌린다. 리졸버의 바깥 길 `mail-dns-egress` 는 도커가 기본 주소 풀(172.17~31.x · 192.168.x)에서 정한다 — VPC 가 그 대역이면 겹치지 않는지 본다 |
|
||||
| 서버 B ACG — 인바운드 | TCP 25 · 465 · 993 · 443 ← `0.0.0.0/0` (메일 수신 · 메일 프로그램 · ACME TLS-ALPN · MTA-STS) · TCP 8080 ← **서버 A 사설 IP `/32` 만** · TCP 22 ← 관리 IP 만. 그 밖(80 · 3000 등)은 열지 않는다 |
|
||||
| 서버 B ACG — 아웃바운드 | 좁힌다면 25(발송 — 열려 있음을 확인했다) · 443 · 80(ACME · 웹 포털 내려받기 · 외부 이미지 중계) · 53(DNS)은 남긴다 |
|
||||
| 서버 B ACG — 아웃바운드 | 좁힌다면 25(발송 — 열려 있음을 확인했다) · 443 · 80(ACME · 웹 포털 내려받기 · 외부 이미지 중계) · **53 UDP · TCP 를 모든 곳으로**(메일 전용 리졸버가 루트 · 각 도메인의 DNS 서버에 직접 묻는다 — NCP 의 DNS 서버만 열면 안 된다)은 남긴다 |
|
||||
| 서버 A ACG — 아웃바운드 | 좁혀 두었다면 서버 B 사설 IP 의 TCP 8080 을 더한다 |
|
||||
| Network ACL | 서브넷의 NACL 을 바꿔 두었다면 위 포트를 같게 연다 |
|
||||
| PTR | 서버 B 공인 IP → `mx1.arasystem.it.kr` — 가비아가 아니라 **NCP 고객지원**에 요청한다. 가비아의 `mx1` A 레코드를 먼저 넣는다(정방향 · 역방향이 맞아야 한다 — [PTR](#ptr)) |
|
||||
@@ -1162,4 +1290,93 @@ sudo docker logs --tail 3 "${PROJECT_NAME}-frontend"
|
||||
| `DKIM TXT … 의 공개 키가 엔진의 키와 다릅니다` · `DKIM TXT … 가 DNS 에 없습니다` | 키가 다르면 동료(mailcow) 앞 메일이 위조로 거절되고, 없으면 DMARC 가 SPF 정렬에만 기대게 되어 반송(빈 봉투)이나 SPF 가 빗나간 메일이 거절된다. `setup.mjs --print-dns` 가 보여 주는 selector TXT 로 DNS 를 고치거나 넣고, 바깥에서 `dig TXT <selector>._domainkey.<도메인>` 으로 보인 뒤 다시 실행한다. 처음 켜는 실행이면 relay 를 켜지 않고 멈췄고, 이미 켜진 시범이면 켠 채로 실패만 알렸다 |
|
||||
| `[주의] 관리자 역할("System Administrator")을 찾지 못했거나 메일을 받게(emailReceive) 바뀌어 …` | 관리자 역할의 이름을 바꿨거나 `emailReceive` 를 더했다 — 관리자 주소는 RCPT 거절에서 빠진다(메일을 받는 계정을 막지 않으려고). 관리자가 메일을 받지 않게 둔다면 역할을 되돌린다 |
|
||||
| `Password is too weak` | 더 길고 무작위인 값으로 (예: `change-me-in-local-env` 는 통과, `devpassword1` 은 거부) |
|
||||
| 외부로 보낸 메일이 Mailpit 에 없다 | setup 의 `[확인] 설정 리로드 완료` 가 나왔는지. 로그: `docker logs "${PROJECT_NAME}-mail"` 의 `delivery.*` |
|
||||
| `리졸버 네트워크 … 가 없습니다` · `… 가 리졸버 네트워크 … 에 없습니다` · `… 의 dns(…)가 MAIL_RESOLVER_IP(…) 하나가 아닙니다` · `… 컨테이너가 없습니다` | 메일 전용 리졸버가 든 compose 로 아직 다시 띄우지 않았다 — `docker compose … up -d`(mail-resolver 를 띄우고 mail 을 다시 만든다 — 몇 초 끊긴다) 뒤 setup. 아무것도 바뀌지 않았다 |
|
||||
| `… 가 떠 있지 않습니다`(mail-resolver) | `docker logs "${PROJECT_NAME}-mail-resolver"` — 설정 오류면 줄 번호가 나온다(`unbound-checkconf`). `could not open … unbound.conf`·`Permission denied` 면 저장소를 umask 077 로 클론했다 — `chmod 644 mail/resolver/unbound.conf`. 고친 뒤 `docker compose … up -d` |
|
||||
| mail-resolver 가 `Address already in use` 로 뜨지 않는다 | 다른 컨테이너가 `MAIL_RESOLVER_IP` 를 먼저 가져갔다(도커는 앞쪽부터 차례로 준다). `docker compose … up -d --force-recreate mail-resolver mail`, 되풀이되면 `MAIL_RESOLVER_IP` 를 서브넷의 끝 쪽으로 |
|
||||
| `… 의 … 주소(…)가 MAIL_RESOLVER_IP(…) 와 다릅니다` · `서브넷(…)이 MAIL_RESOLVER_SUBNET(…) 과 다릅니다` · `internal 이 아닙니다` | env 를 바꾼 뒤 컨테이너 · 네트워크를 다시 만들지 않았다 — `docker compose … down` 후 `up -d`(네트워크 · 컨테이너를 함께), 그다음 setup |
|
||||
| `리졸버 … 가 메일 컨테이너에서 DNSSEC 을 검증한 답을 주지 않습니다` | 리졸버가 바깥에 묻지 못하거나 DNSSEC 이 깨졌다 — 괄호 안이 답마다의 결과다(UDP 루트 SOA · TCP 루트 DNSKEY). 이 서버의 바깥 53(UDP · **TCP**) 아웃바운드(NCP ACG), `docker exec "${PROJECT_NAME}-mail-resolver" drill . @127.0.0.1 NS`, 서버 시계(DNSSEC 은 시각을 본다), `docker logs` 의 리졸버 오류. 운영은 아무것도 바꾸지 않고 멈췄다. 개발은 `[주의]` 뒤 이어 가지만(개발 메일은 Mailpit), 리졸버가 바깥에 전혀 닿지 않으면 뒤의 리로드를 엔진이 거절해 거기서 종료 1 이다(아래 줄) |
|
||||
| `[실패] x:Action/set 요청이 거부되었습니다` + `생성 거부 (reload): validationFailed — Invalid address: failed to lookup address information` | 엔진이 설정을 다시 만들며 스팸 필터의 Pyzor 주소(`public.pyzor.org`)를 mail 컨테이너의 DNS(= 메일 전용 리졸버)로 풀지 못해 리로드를 통째로 거절했다 — 리졸버가 바깥에 닿지 못한다. `docker exec "${PROJECT_NAME}-mail-resolver" drill public.pyzor.org @127.0.0.1 A` 로 보고 [리졸버 장애 때](#리졸버-장애-때). 저장한 설정은 남아 있다(다음 리로드 · `restart mail` 때 적용) |
|
||||
| `메일 컨테이너(…)에서 리졸버를 확인하지 못했습니다 — 메일 컨테이너에 perl 이 없음(docker exec 종료 코드 127)` | 리졸버 문제가 아니다 — setup 은 메일 이미지의 perl 로 묻는데 `MAIL_IMAGE` 를 바꾼 새 이미지에 perl 이 없다(`docker exec "${PROJECT_NAME}-mail" perl -v`). 운영은 여기서 멈춘다 — 이미지를 되돌리거나, setup 의 리졸버 확인(`DNS_PROBE_SCRIPT`)을 그 이미지에 맞춘다. 개발은 `[주의]` |
|
||||
| `[주의] … 의 이번 기동 … 로그가 남아 있지 않아 … DNSSEC 을 검증했는지 확인하지 못했습니다` | mail 이 오래전에 떠서 그 기동의 로그가 회전(`json-file` 의 `max-size` · `max-file` 등)으로 지워졌다 — 실패가 아니다(메일 · 리졸버는 사전 확인대로다). 확인하려면 `docker compose … restart mail`(몇 초 끊긴다) 뒤 setup |
|
||||
| compose `up` 이 `optional dependency "mail-resolver" failed to start` 경고를 낸다 | mail 은 떴지만 리졸버가 unhealthy · 멈춤이라 바깥 DNS(바깥 메일 · SPF · DKIM · DNSBL)가 안 된다 — [리졸버 장애 때](#리졸버-장애-때) |
|
||||
| 특정 회사(도메인)로만 메일이 안 간다 · 그 도메인에서 온 메일만 SPF · DKIM · DMARC 가 `temperror` | 그 도메인의 DNSSEC 이 깨졌을 수 있다(리졸버가 검증해 SERVFAIL 로 답한다) — [DNSSEC 이 깨진 도메인](#dnssec-이-깨진-도메인-특정-회사로만-메일이-안-간다) |
|
||||
| `[주의] 리졸버 네트워크 … 에 mail · mail-resolver 밖의 컨테이너가 붙어 있습니다` · `리졸버의 바깥 길 … 에 다른 컨테이너가` (운영은 실패) | 그 컨테이너도 리졸버에 닿는다(리졸버도 그 컨테이너에 닿는다) — `docker network disconnect <네트워크> <컨테이너>`, compose 의 `networks` 확인 |
|
||||
| `[주의] 엔진이 … 때 리졸버로 DNSSEC 을 검증하지 못했다고 남겼습니다` | mail 이 리졸버보다 먼저 떴거나(서버 재부팅 — 도커는 compose 의 차례를 지키지 않는다. `docker compose … restart mail` 뒤 setup 으로 다시 본다), 리졸버가 TCP 로 답하지 못한다. 메일은 오가고 DANE 만 꺼진다. 리로드도 같은 확인을 다시 하지만 결과가 로그에 남지 않아 기동 로그로만 본다 |
|
||||
| `[주의] MAIL_SPAMHAUS_DQS_KEY 가 없어 Spamhaus 두 항목 … 을 꺼 둔다` (운영) | 키가 없으면 늘 나온다 — 실패가 아니다. 키를 받으면 `.env.production` 에 넣고 setup ([DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl) "Spamhaus") |
|
||||
| `Spamhaus 키(…)로 … 시험 항목 … 을 받지 못해 그 항목을 꺼 둡니다` | 키가 틀렸거나 · 계정이 끝났거나(체험 30일이 지났다 — 구독하거나 키를 비우고 setup) · 그 서비스(IP · 도메인)가 계정에서 꺼져 있다(꺼진 서비스는 SERVFAIL). Spamhaus 계정 화면에서 키 · 서비스를 본 뒤 setup. 그 항목은 꺼진 채다 — 공개 미러로 묻지 않는다. 키가 멈춘 뒤 이 실행 전까지는 엔진의 조회가 실패해 표시가 붙지 않았다(fail-open — "Spamhaus" 절) |
|
||||
| `MAIL_SPAMHAUS_DQS_KEY 는 Spamhaus 가 준 26자 영문 · 숫자 키여야 합니다` · `운영의 MAIL_SPAMHAUS_DQS_KEY 는 셸이 아니라 …` | 키를 `.env.production` 에 따옴표 · 공백 없이 그대로 넣는다(셸에는 두지 않는다 — `unset MAIL_SPAMHAUS_DQS_KEY`) |
|
||||
| `[주의] 스팸 차단 목록 …(STWT_…)을 찾지 못해 맞추지 않았습니다` | 엔진 기본 항목을 지웠거나 이름을 바꿨다 — 바꿨다면 그 항목의 조회 이름 · 켜짐을 관리 화면 › Settings › Spam Filter › DNSBL 에서 직접 본다 |
|
||||
| 외부로 보낸 메일이 Mailpit 에 없다 | setup 의 `[확인] 설정 리로드 완료` 가 나왔는지. 로그: `docker logs "${PROJECT_NAME}-mail"` 의 `delivery.*`. `delivery.ip-lookup-failed … hostname = "mailpit"` 이면 개발 엔진의 DNS 가 `Custom` 이다 — setup 을 다시 돌리면 `System` 으로 돌아온다 |
|
||||
|
||||
### 리졸버 장애 때
|
||||
|
||||
메일 전용 리졸버(`mail-resolver`)가 멈추거나 바깥에 묻지 못하면 엔진의 바깥 DNS 가 모두 실패한다 — 바깥으로 보내는 메일은 MX 를 찾지 못해 대기열에서 다시 시도하고,
|
||||
받는 메일은 SPF · DKIM · DMARC · DNSBL 검사가 약해진다. 사내 메일 · 웹메일은 그대로 된다. 다만 **설정 리로드가 막힌다**(엔진이 Pyzor 주소를 풀지 못해 리로드를 통째로 거절 — 아래 3.).
|
||||
표시는 compose 의 `optional dependency "mail-resolver" failed to start` 경고, setup 사전 확인의 `… 가 떠 있지 않습니다` · `… DNSSEC 을 검증한 답을 주지 않습니다`,
|
||||
setup · 관리 화면 리로드의 `validationFailed — Invalid address: failed to lookup address information` 다. 자동으로 공용 DNS 로 넘어가는 길은 두지 않는다
|
||||
(넘어가면 Spamhaus 가 다시 막고, 장애를 모른 채 지나간다 — [메일 전용 리졸버](#메일-전용-리졸버-mail-resolver) "대가"). 아래는 사람이 고르는 길이다.
|
||||
|
||||
1. **리졸버부터 고친다** (운영 명령 — 개발은 `docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development`):
|
||||
```bash
|
||||
sudo docker ps -a --filter "name=${PROJECT_NAME}-mail-resolver" # Up (healthy) 여야 한다
|
||||
sudo docker logs --tail 20 "${PROJECT_NAME}-mail-resolver" # 설정 오류면 파일 · 줄 번호가 나온다
|
||||
sudo docker exec "${PROJECT_NAME}-mail-resolver" drill . @127.0.0.1 NS # 바깥(루트)에 묻는가
|
||||
# 설정을 고쳤으면 리졸버만 다시 만든다 (설정 파일만 바뀌면 compose 가 다시 만들지 않는다 — mail 은 그대로 둔다)
|
||||
sudo docker compose --env-file .env.production up -d --force-recreate mail-resolver
|
||||
```
|
||||
healthy 가 되면 setup 을 돌려 `[확인] 메일 전용 리졸버 …` 를 본다("운영 배포" 5)와 같은 형태). mail 이 리졸버 없이 떴다면 그 기동은 DANE 이 꺼지고 Pyzor 가 빠진 채다 —
|
||||
setup 의 리로드가 둘 다 다시 만들지만 리로드의 DNSSEC 확인은 로그에 남지 않으므로, 확인하려면 `sudo docker compose --env-file .env.production restart mail`(몇 초 끊긴다)
|
||||
뒤 setup 의 `[확인] 엔진 기동 로그(…)에 DNSSEC 경고가 없다` 를 본다.
|
||||
2. **mail 이 리졸버 때문에 뜨지 않을 때** — compose 는 리졸버가 unhealthy · 멈춤(설정 오류로 바로 끝남) · 재시작을 되풀이해도 경고만 내고 mail 을 띄운다
|
||||
(mail 의 `depends_on` 이 `required: false` — 2026-10-30 개발에서 셋 다 확인. 리졸버의 healthcheck 가 unhealthy 로 판정할 때까지는 기다린다 — 최대 2분쯤).
|
||||
그래도 mail 이 `Created` 에 남았다면(`required` 를 지운 compose 등) 리졸버를 기다리지 않고 띄운다:
|
||||
```bash
|
||||
sudo docker compose --env-file .env.production up -d --no-deps mail
|
||||
```
|
||||
3. **리졸버를 곧 고칠 수 없을 때 — 비상 길: 엔진 DNS 를 공용 DNS 로.** 엔진 DNS 를 `System` 으로 돌려도 소용없다 — mail 컨테이너의 `dns:` 가 같은 리졸버라
|
||||
도커 DNS 도 바깥 이름을 그 리졸버에 묻는다. 관리 API(관리 화면의 DNS Resolver 설정과 같다 — 운영은 SSH 터널, "운영 배포")로 `@type` 을 `Quad9` 이나 `Cloudflare` 로 저장한 뒤
|
||||
**리로드가 아니라 mail 을 다시 띄운다** ([외부 릴레이](#외부-릴레이-ses-등)와 같은 호출 형식):
|
||||
```jsonc
|
||||
[["x:DnsResolver/set", { "accountId": "<세션의 urn:stalwart:jmap 계정>", "update": { "singleton": { "@type": "Quad9" } } }, "0"]]
|
||||
```
|
||||
```bash
|
||||
sudo docker compose --env-file .env.production restart mail # 몇 초 끊긴다
|
||||
```
|
||||
- **리로드(`ReloadSettings` · 관리 화면의 Reload)는 이때 엔진이 거절한다** — 설정을 다시 만들며 스팸 필터의 Pyzor 주소(`public.pyzor.org`)를 mail 컨테이너의 DNS(= 죽은 리졸버)로 풀다
|
||||
실패하면 리로드 전체를 버린다(`validationFailed — Invalid address: failed to lookup address information`). 기동은 그 항목(Pyzor)만 빼고 뜬다(로그 `registry.build-error … SpamPyzor`)
|
||||
— 0.16.27 소스 `crates/common/src/config/mailstore/spamfilter.rs` · `crates/common/src/cache/reload.rs`.
|
||||
2026-10-30 개발에서 리졸버의 바깥 길을 끊고 확인했다: `System` 으로 다시 띄우면 DNSSEC 경고(엔진 DNS 가 안 된다), `Quad9` 저장 뒤 리로드는 위 거절,
|
||||
`restart mail` 뒤에는 DNSSEC 경고 없이 떴다(엔진 DNS 가 Quad9 로 된다 — Pyzor 만 빠진 채).
|
||||
- 엔진은 `@type` 만 바꾸는 부분 갱신을 받고, 두 값 모두 기본이 `"useTls": true`(DNS over TLS — 바깥 853/TCP)다.
|
||||
이 서버에서 바깥 853 이 막혀 있으면 `"useTls": false` 를 함께 준다(53 UDP · TCP).
|
||||
- 그동안은 공용 DNS 를 거친다 — 공용 DNS 조회를 거부하는 차단 목록은 답하지 않아 검사가 약해진다(메일은 오간다). mail 컨테이너 자체의 이름 풀기(Pyzor · ACME · `getent`)는 여전히 리졸버라 안 된다.
|
||||
- **리졸버를 고친 뒤 setup 을 다시 돌린다** — 사전 확인을 통과하면 `[변경] 엔진 DNS — … Custom … · 전 Quad9` 처럼 리졸버로 되돌리고 리로드한다(개발은 `System`) —
|
||||
그 리로드가 빠졌던 Pyzor 도 다시 만든다. 기동 로그로 DNSSEC 을 확인하려면 그 뒤 `restart mail` 한 번과 setup(`[확인] 엔진 기동 로그 …`).
|
||||
운영 setup 은 리졸버가 확인되기 전에는 사전 확인에서 멈춰 엔진 DNS 를 바꾸지 않는다 — 비상 길을 연 채로 돌려도 공용 DNS 가 그대로 남으니 리졸버부터 고친다
|
||||
(개발 setup 은 `[주의]` 뒤 이어 가며 `System` 으로 저장한다 — 개발은 Mailpit 이라 비상 길이 필요 없다).
|
||||
|
||||
### DNSSEC 이 깨진 도메인 (특정 회사로만 메일이 안 간다)
|
||||
|
||||
메일 전용 리졸버는 DNSSEC 을 검증한다 — 서명이 깨진 도메인(만료된 RRSIG · 키를 바꾸다 상위의 DS 를 고치지 못함 등)은 SERVFAIL 이 된다.
|
||||
그 도메인으로 보내는 메일은 MX 를 찾지 못해 대기열에서 다시 시도하다 끝내 반송되고, 그 도메인에서 오는 메일은 SPF · DKIM · DMARC 조회가 `temperror` 다.
|
||||
검증을 켜면서 새로 생길 수 있는 증상이다(검증하지 않는 DNS 로는 답이 왔다). 다른 회사로는 잘 간다면 이것부터 본다.
|
||||
|
||||
1. **그 도메인 쪽 문제인지 본다** — 검증을 끄고(CD 비트) 물으면 답이 오는데 검증하면 SERVFAIL 이면 그 도메인의 DNSSEC 이 깨졌다:
|
||||
```bash
|
||||
sudo docker exec "${PROJECT_NAME}-mail-resolver" drill <도메인> @127.0.0.1 MX # rcode: SERVFAIL
|
||||
sudo docker exec "${PROJECT_NAME}-mail-resolver" drill -o CD <도메인> @127.0.0.1 MX # rcode: NOERROR + 답 → DNSSEC 이 깨졌다
|
||||
```
|
||||
(예: 시험용으로 일부러 깨 둔 `fail01.dnssec.works` 는 앞 줄 SERVFAIL, 뒷줄 `5.45.109.212` — 2026-10-30.) CD 로 물어도 SERVFAIL 이면 DNSSEC 이 아니라
|
||||
그 도메인의 DNS 서버가 답하지 않는 것이다 — 아래 예외로 풀리지 않는다. 바깥 도구로도 한 번 더 본다: <https://dnsviz.net/> · <https://dnssec-analyzer.verisignlabs.com/>
|
||||
(빨간 표시 — 다른 검증 리졸버(Google · Cloudflare)도 같은 SERVFAIL 을 준다).
|
||||
2. **그 도메인의 관리자에게 알린다** — 그쪽이 고치면 저절로 풀린다(unbound 는 검증 실패를 1분만 기억한다 — `val-bogus-ttl` 기본값 60).
|
||||
3. **급하면 그 도메인만 검증 예외로** — `mail/resolver/unbound.conf` 의 `server:` 안에 한 줄 더하고 리졸버만 다시 만든다:
|
||||
```
|
||||
domain-insecure: "example.com"
|
||||
```
|
||||
```bash
|
||||
sudo docker compose --env-file .env.production up -d --force-recreate mail-resolver # 몇 초 동안 바깥 DNS 가 끊긴다(엔진이 다시 시도한다)
|
||||
```
|
||||
그 도메인과 하위 도메인 전체의 DNSSEC 검증을 끈다 — 그 도메인으로 가는 메일의 DANE 도 꺼진다. 다른 도메인은 그대로 검증한다
|
||||
(시험용 리졸버 컨테이너에서 `fail01.dnssec.works` 만 SERVFAIL → NOERROR, `dnssec.works` 는 그대로 ad · `fail02.dnssec.works` 는 그대로 SERVFAIL — 2026-10-30).
|
||||
예외는 저장소 파일이라 커밋 기록에 남는다 — 그쪽이 고친 것을 1. 로 확인하면 지운다.
|
||||
|
||||
Reference in New Issue
Block a user