# ========================================== # 메일 엔진 전용 재귀 DNS 리졸버 (unbound) — docker-compose.yml 의 mail-resolver 가 읽는다 # # 엔진(Stalwart)의 DNS 조회 — 스팸 차단 목록(DNSBL) · SPF · DKIM · DMARC · MX · PTR · TLSA(DANE) — 를 # 공용 · ISP DNS 가 아니라 이 서버에서 루트부터 직접 푼다 (mail/README.md "DNS 리졸버와 스팸 차단 목록"). # - Spamhaus 는 공용 · ISP DNS 를 거친 조회를 거부한다(127.255.255.254). 이 리졸버는 서버 자신의 공인 IP 로 묻는다 # - DNSSEC 을 검증한다 — 엔진의 DANE 이 켜진다 (기동 로그 "DNS resolver cannot validate DNSSEC" 가 사라진다) # # 이미지 klutchell/unbound(distroless — 셸 없음)의 기본 설정을 이 파일로 통째로 바꾼다. # compose 가 처음부터 비-root(101:102)로 띄우므로 이 파일은 그 사용자가 읽을 수 있어야 한다(644 — 저장소를 umask 077 로 클론하지 않는다). # 바꾼 뒤에는 docker compose … up -d --force-recreate mail-resolver (설정 파일만 바뀌면 compose 가 다시 만들지 않는다) # ========================================== server: # 컨테이너 — 앞에서 돌고, 사용자를 바꾸지 않고(처음부터 비-root), chroot 하지 않는다 do-daemonize: no username: "" chroot: "" directory: "/var/unbound" pidfile: "" logfile: "" use-syslog: no # 1 = 동작 정보와 오류만. 조회 이름은 남기지 않는다 — DNSBL 조회 이름에 Spamhaus DQS 키가 들어간다 # (log-queries · log-replies · log-servfail · val-log-level 은 기본값(끔) 그대로 둔다) verbosity: 1 # 받는 곳 — 컨테이너의 53 (UDP · TCP). 엔진은 TCP 도 쓸 수 있어야 DNSSEC 을 믿는다 (Stalwart 의 기동 확인) # 이 컨테이너는 메일 엔진과의 전용 네트워크(mail-dns — 바깥 길 없음)와 자기 바깥 조회 길(mail-dns-egress)에만 붙고 # 호스트 포트를 열지 않는다 — 닿는 것은 메일 엔진뿐이다. (비-root 가 53 에 붙는 것은 도커가 컨테이너마다 # net.ipv4.ip_unprivileged_port_start=0 으로 두기 때문이다 — 권한(capability)은 모두 뺀다) interface: 0.0.0.0 port: 53 do-ip4: yes do-ip6: no do-udp: yes do-tcp: yes # 묻는 쪽 — 사설 대역(도커 네트워크)과 자기 자신(healthcheck)만 받는다. 1차 막은 네트워크 구성(메일 엔진만 붙는다)이고, 이것은 그 뒤의 막이다 — # 누가 53 을 호스트에 공개해도 바깥(공인 주소)에서 쓰는 열린 리졸버가 되지 않게 access-control: 0.0.0.0/0 refuse access-control: 127.0.0.0/8 allow access-control: 10.0.0.0/8 allow access-control: 172.16.0.0/12 allow access-control: 192.168.0.0/16 allow # DNSSEC 검증 — 루트 신뢰 앵커는 이미지에 든 root.key(KSK-2017 · KSK-2024)다. RFC 5011 로 스스로 고쳐 쓰므로 # /var/unbound 는 쓸 수 있어야 한다(컨테이너 쓰기 계층 — read_only 로 두지 않는다). 이미지를 올리면 새 파일로 시작한다 module-config: "validator iterator" auto-trust-anchor-file: "/var/unbound/root.key" root-hints: "/var/unbound/root.hints" harden-dnssec-stripped: yes harden-glue: yes harden-below-nxdomain: yes # 조회 이름은 필요한 만큼만 위로 보낸다(QNAME minimisation — 기본값) — 루트 · .net 서버는 DQS 키가 든 이름 전체를 보지 못한다 qname-minimisation: yes hide-identity: yes hide-version: yes # 사설 · 내부 주소로 풀리는 바깥 이름은 버린다 — 바깥 도메인의 MX 가 이 서버의 사설망(VPC · 도커 네트워크)을 가리켜 # 엔진이 그쪽으로 메일을 보내는 일이 없게. # ⚠️ 127.0.0.0/8 은 넣지 않는다 — 스팸 차단 목록(DNSBL)은 127.0.0.x 로 답한다 private-address: 10.0.0.0/8 private-address: 172.16.0.0/12 private-address: 192.168.0.0/16 private-address: 169.254.0.0/16 private-address: 100.64.0.0/10 private-address: fc00::/7 private-address: fe80::/10 # 리졸버 자신도 사설 · 내부 주소의 DNS 서버에는 묻지 않는다(위 private-address 와 짝) — 바깥 도메인이 이름 서버(NS)의 주소(glue)를 # 사설 주소로 두어 이 리졸버가 VPC · 호스트 · 도커 네트워크(메일 엔진 포함)의 53 에 묻게 만드는 것을 막는다. # 127.0.0.0/8 · ::1 은 기본값(do-not-query-localhost: yes)이 이미 막는다. 이 리졸버는 루트부터 직접 풀어 사설 주소에 물을 일이 없다 do-not-query-address: 10.0.0.0/8 do-not-query-address: 172.16.0.0/12 do-not-query-address: 192.168.0.0/16 do-not-query-address: 169.254.0.0/16 do-not-query-address: 100.64.0.0/10 do-not-query-address: fc00::/7 do-not-query-address: fe80::/10 # 크기 — 메일 서버 하나 몫 (compose 메모리 한도 128m 안) num-threads: 1 msg-cache-size: 8m rrset-cache-size: 16m edns-buffer-size: 1232 so-rcvbuf: 0 so-sndbuf: 0 prefetch: yes prefetch-key: yes