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:
@@ -11,6 +11,7 @@ services:
|
||||
# edge frontend ↔ backend 전용
|
||||
# app-network backend ↔ db·redis·mail — 메일 엔진의 신뢰 대역이다. frontend 는 붙이지 않는다
|
||||
# (붙이면 frontend 가 mail:8080 관리 API 에 닿고, 그 주소가 자동 차단 예외·게이트웨이 로그인 대역에 든다)
|
||||
# mail-dns mail ↔ mail-resolver(메일 전용 DNS) 전용 — 바깥 길 없음. 리졸버의 바깥 조회는 mail-dns-egress 로만 (6번 참조)
|
||||
# ----------------------------------------
|
||||
frontend:
|
||||
container_name: ${PROJECT_NAME}-frontend
|
||||
@@ -255,14 +256,75 @@ services:
|
||||
- mail-config:/etc/stalwart
|
||||
- mail-data:/var/lib/stalwart
|
||||
restart: unless-stopped
|
||||
# 컨테이너의 DNS — 도커 DNS(127.0.0.11)가 도커 내부 이름(mailpit 등)은 직접 풀고 바깥 이름은 메일 전용 리졸버에 묻는다.
|
||||
# 엔진 자신의 DNS(x:DnsResolver)는 mail setup 이 맞춘다 — 운영은 이 리졸버에 바로(도커 DNS 를 거치지 않는다), 개발은 이 길(System)
|
||||
dns:
|
||||
- ${MAIL_RESOLVER_IP:-172.16.73.254}
|
||||
networks:
|
||||
- app-network
|
||||
- mail-dns
|
||||
# 리졸버가 답한 뒤에 뜬다 — 엔진은 기동할 때 리졸버가 DNSSEC 을 검증하는지 보고, 못 하면 DANE 을 끈다.
|
||||
# required: false — 평소에는 healthy 를 기다리고, 리졸버가 unhealthy 거나 떠 있지 못하면(설정 오류로 바로 끝남 · 재시작을 되풀이함)
|
||||
# compose 가 경고("optional dependency … failed to start")만 내고 mail 을 띄운다. 없으면 리졸버 오류 하나로 `up -d mail` 이 옛 mail 을 지운 뒤
|
||||
# 새 mail 을 띄우지 않고 끝나 메일 전체가 선다 — 바깥 DNS 만 안 되는 편이 낫다 (2026-10-30 개발 Compose 2.40 에서 넷 다 확인, mail/README.md "리졸버 장애 때")
|
||||
depends_on:
|
||||
mail-resolver:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1.0'
|
||||
memory: 1g
|
||||
|
||||
# ----------------------------------------
|
||||
# 6. Mail resolver (unbound) — 메일 엔진 전용 재귀 DNS · profile `mail`
|
||||
#
|
||||
# 엔진의 DNS 조회(스팸 차단 목록 DNSBL · SPF · DKIM · DMARC · MX · PTR · TLSA)를 공용 · ISP DNS 가 아니라 이 서버에서 직접 푼다.
|
||||
# Spamhaus 는 공용 · ISP DNS 를 거친 조회를 거부한다(127.255.255.254). DNSSEC 을 검증해 엔진의 DANE 이 켜진다.
|
||||
# 설정 mail/resolver/unbound.conf · mail setup 이 엔진의 x:DnsResolver 를 이 주소로 맞춘다 (mail/README.md "DNS 리졸버와 스팸 차단 목록").
|
||||
#
|
||||
# 네트워크 — 메일 엔진만 닿게:
|
||||
# mail-dns mail ↔ 리졸버 전용. internal(바깥으로 나가는 길 없음). 리졸버 주소는 MAIL_RESOLVER_IP 로 고정한다 —
|
||||
# 엔진 설정 · mail 의 dns 가 주소(IP)만 받는다. 서브넷의 끝 쪽에 둔다(도커는 mail 에 앞쪽부터 차례로 준다)
|
||||
# mail-dns-egress 리졸버의 바깥 조회 길 — 리졸버 혼자 붙는다
|
||||
# app-network(메일 엔진의 신뢰 대역)에는 붙이지 않는다 — mail setup 이 두 네트워크의 구성원을 확인한다 (운영은 그 밖이 있으면 중단)
|
||||
# 이미지는 판까지 고정한다 (docker 스킬 "이미지 버전 고정"). 1.26.2 는 NLnet Labs 의 보안 릴리스(2026-10-21)라 2주를 기다리지 않았다
|
||||
# ----------------------------------------
|
||||
mail-resolver:
|
||||
profiles: ["mail"]
|
||||
container_name: ${PROJECT_NAME}-mail-resolver
|
||||
# 공식이 아닌(커뮤니티) 이미지라 판에 digest 까지 묶는다 — 같은 태그를 누가 다시 올려도 바뀌지 않게 (2026-10-30 manifest list digest)
|
||||
image: klutchell/unbound:1.26.2@sha256:50cbeb32b5838a829a7226b19cb151f9535bd7f9f3190f950780f452fe807478
|
||||
# 이미지는 root 로 떠서 53 에 붙은 뒤 내려가는 꼴이다 — 처음부터 비-root(이미지의 unbound 사용자 101:102)로 띄운다.
|
||||
# 53 에 붙는 것은 도커가 컨테이너마다 net.ipv4.ip_unprivileged_port_start=0 으로 두기 때문이다 — 권한(capability)은 모두 뺀다
|
||||
user: "101:102"
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
# 이미지의 기본 설정을 통째로 바꾼다. DNSSEC 신뢰 앵커(/var/unbound/root.key)는 이미지의 것을 쓰고 컨테이너 안에서 고쳐 쓴다
|
||||
- ./mail/resolver/unbound.conf:/etc/unbound/unbound.conf:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
mail-dns:
|
||||
ipv4_address: ${MAIL_RESOLVER_IP:-172.16.73.254}
|
||||
mail-dns-egress: {}
|
||||
healthcheck:
|
||||
# 데몬이 답하는지만 본다(자기 안의 localhost) — 바깥 DNS 가 잠깐 막혀도 mail 이 뜨지 못하는 일이 없게.
|
||||
# 바깥 조회 · DNSSEC 은 mail setup 이 메일 컨테이너에서 확인한다. drill-hc 는 이미지에 든 drill 의 종료 코드를 0 · 1 로 맞춘 것
|
||||
test: ["CMD", "drill-hc", "localhost", "@127.0.0.1", "A"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.25'
|
||||
memory: 128m
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
# 이름·서브넷을 고정한다. 메일 엔진은 이 대역만 신뢰한다 — 게이트웨이 로그인·자동 차단 예외
|
||||
@@ -283,6 +345,20 @@ networks:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: ${EDGE_NETWORK_SUBNET:-172.16.72.0/24}
|
||||
mail-dns:
|
||||
# mail ↔ mail-resolver 전용. internal — 이 네트워크로는 바깥에 나가지 못한다(mail 의 바깥 길은 app-network 그대로).
|
||||
# 리졸버 주소(MAIL_RESOLVER_IP)를 고정하려고 서브넷을 고정한다. 겹치면 MAIL_RESOLVER_SUBNET · MAIL_RESOLVER_IP 를 함께 바꾼다
|
||||
name: ${PROJECT_NAME}_mail-dns
|
||||
driver: bridge
|
||||
internal: true
|
||||
ipam:
|
||||
config:
|
||||
- subnet: ${MAIL_RESOLVER_SUBNET:-172.16.73.0/24}
|
||||
mail-dns-egress:
|
||||
# 리졸버가 루트 · 각 도메인의 DNS 서버에 묻는 길 — 리졸버 혼자 붙는다. 믿는 대역이 아니라 서브넷을 고정하지 않는다 —
|
||||
# 도커가 기본 주소 풀(172.17~31.x · 192.168.x)에서 정한다. 서버의 사설망(VPC)이 그 대역이면 겹치지 않는지 본다 (mail/README.md)
|
||||
name: ${PROJECT_NAME}_mail-dns-egress
|
||||
driver: bridge
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user