Ubuntu 서버에서 DNS 서버를 바꾸려고 /etc/resolv.conf를 열면 nameserver 127.0.0.53 한 줄만 보이고, 직접 고쳐도 재부팅하면 원래대로 돌아간다. 18.04부터 이름 해석을 systemd-resolved가 맡고 있기 때문이다. 실제 업스트림 DNS는 Netplan(DHCP 포함)이나 resolved.conf에서 정한다. 이 글에서는 Ubuntu 24.04 서버에서 현재 DNS 확인, 임시 변경, Netplan으로 영구 설정, 전역 DNS와 DNS over TLS, /etc/hosts와 캐시 동작을 실제 실행 결과로 정리한다.
현재 DNS 구성 확인
/etc/resolv.conf는 resolved가 만든 stub 파일을 가리키는 심볼릭 링크이고, 애플리케이션은 로컬 stub 리졸버 127.0.0.53에만 질의한다.
$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Apr 23 2024 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
$ grep -v '^#' /etc/resolv.conf | grep -v '^$'
nameserver 127.0.0.53
options edns0 trust-ad
search .
실제 업스트림 서버는 resolvectl로 본다. 이 호스트는 DHCP로 받은 KT DNS를 쓰고 있다.
$ resolvectl status enp0s3
Link 2 (enp0s3)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 168.126.63.1
DNS Servers: 168.126.63.1 168.126.63.2
$ cat /run/systemd/resolve/resolv.conf | grep -v '^#' | grep -v '^$'
nameserver 168.126.63.1
nameserver 168.126.63.2
search .
/run/systemd/resolve/resolv.conf는 stub을 거치지 않고 업스트림을 직접 적은 파일이다. 링크 대상에 따라 동작 모드가 달라진다.
| /etc/resolv.conf 링크 대상 | resolv.conf mode | 동작 |
|---|---|---|
| ../run/systemd/resolve/stub-resolv.conf | stub (기본값) | 127.0.0.53으로 질의, resolved 캐시·라우팅 사용 |
| ../run/systemd/resolve/resolv.conf | uplink | 업스트림 서버로 직접 질의, resolved 캐시 우회 |
| 일반 파일 | foreign | resolved가 관리하지 않음. 직접 작성한 내용 그대로 사용 |
조회는 resolvectl query로 하면 어느 링크와 프로토콜로 답을 받았는지까지 나온다. dig는 기본적으로 stub(127.0.0.53)에 질의한다.
$ resolvectl query ubuntu.com
ubuntu.com: 2001:67c:1562::1f -- link: enp0s3
2620:2d:4000:1::27 -- link: enp0s3
...
185.125.190.20 -- link: enp0s3
91.189.91.44 -- link: enp0s3
-- Information acquired via protocol DNS in 319.6ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network
$ dig ubuntu.com | grep -E 'SERVER|Query time'
;; Query time: 0 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
resolvectl로 임시 변경
링크별 DNS 서버와 검색 도메인을 런타임에만 바꾼다. /etc/resolv.conf의 search도 함께 바뀐다.
$ sudo resolvectl dns enp0s3 1.1.1.1 8.8.8.8
$ sudo resolvectl domain enp0s3 lab.example
$ resolvectl status enp0s3
Link 2 (enp0s3)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 1.1.1.1 8.8.8.8
DNS Domain: lab.example
$ grep -v '^#' /etc/resolv.conf | grep -v '^$'
nameserver 127.0.0.53
options edns0 trust-ad
search lab.example
revert로 되돌리면 DHCP 값으로 복귀한다. 재부팅이나 netplan apply 때도 사라지므로 장애 대응용으로만 쓴다.
$ sudo resolvectl revert enp0s3
$ resolvectl status enp0s3 | grep -E 'DNS Servers|DNS Domain'
DNS Servers: 168.126.63.1 168.126.63.2
Netplan으로 영구 설정
cloud-init이 만든 50-cloud-init.yaml은 건드리지 않고 뒤 번호 파일을 추가한다. Netplan은 파일을 이름순으로 병합한다.
network:
version: 2
ethernets:
enp0s3:
dhcp4-overrides:
use-dns: false
use-domains: false
nameservers:
addresses: [1.1.1.1, 9.9.9.9]
search: [lab.example]$ sudo chmod 600 /etc/netplan/60-dns.yaml
$ sudo netplan get ethernets.enp0s3
nameservers:
addresses:
- 1.1.1.1
- 9.9.9.9
search:
- lab.example
dhcp4: true
dhcp4-overrides:
use-dns: false
use-domains: false
$ sudo netplan apply
$ resolvectl status enp0s3
Link 2 (enp0s3)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 9.9.9.9
DNS Servers: 1.1.1.1 9.9.9.9
DNS Domain: lab.example
netplan get은 두 파일이 병합된 결과다. 실제로 networkd에 넘어간 설정은 /run/systemd/network/에서 확인한다.
$ sudo cat /run/systemd/network/10-netplan-enp0s3.network
[Match]
Name=enp0s3
[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6
DNS=1.1.1.1
DNS=9.9.9.9
Domains=lab.example
[DHCP]
RouteMetric=100
UseMTU=true
UseDNS=false
UseDomains=false
dhcp4-overrides를 빼면 DHCP 서버가 사라지지 않고 뒤에 합쳐진다. DHCP DNS를 완전히 대체하려면 use-dns: false가 필요하다.
network:
version: 2
ethernets:
enp0s3:
nameservers:
addresses: [1.1.1.1, 9.9.9.9]$ sudo netplan apply
$ resolvectl status enp0s3 | grep 'DNS Servers'
DNS Servers: 1.1.1.1 9.9.9.9 168.126.63.1 168.126.63.2
고정 IP 서버라면 dhcp4: false와 addresses/routes 옆에 같은 nameservers 블록을 둔다. Netplan 전반은 Ubuntu Netplan으로 네트워크 설정 관리하기에 정리했다.
전역 DNS와 DNS over TLS
인터페이스와 무관한 전역 서버는 resolved.conf drop-in으로 지정한다. # 뒤 이름은 TLS 인증서 검증용 서버 이름이다.
[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com
DNSOverTLS=yes
Domains=~.$ sudo systemctl restart systemd-resolved
$ resolvectl status
Global
Protocols: -LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
DNS Servers: 1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com
DNS Domain: ~.
Link 2 (enp0s3)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 168.126.63.1 168.126.63.2
$ resolvectl query ubuntu.com | tail -3
-- Information acquired via protocol DNS in 269.8ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: yes
-- Data from: network
$ sudo ss -tnp state established '( dport = :853 )'
Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
0 0 192.168.0.46:60002 1.1.1.1:853 users:(("systemd-resolve",pid=26306,fd=21))
“encrypted transport: yes”와 853 포트 연결로 DoT가 확인된다. 실제로 어느 서버에 질의했는지는 디버그 로그로 본다.
$ sudo resolvectl log-level debug
$ resolvectl query example.org | tail -2
-- Data is authenticated: no; Data was acquired via local or encrypted transport: yes
-- Data from: network
$ sudo journalctl -u systemd-resolved --since -20s | grep 'Using DNS server' | sort -u
... systemd-resolved[26306]: Using DNS server 1.1.1.1#cloudflare-dns.com for transaction 32025.
... systemd-resolved[26306]: Using DNS server 1.1.1.1#cloudflare-dns.com for transaction 31591.
$ sudo resolvectl log-level info
같은 설정에서 Domains=~.만 지우면 링크의 DHCP 서버에도 질의가 나간다. 링크도 +DefaultRoute라 둘이 동등한 후보가 되기 때문이다.
$ sudo journalctl -u systemd-resolved --since -10s | grep 'Using DNS server' | sort -u
... systemd-resolved[26586]: Using DNS server 1.1.1.1#cloudflare-dns.com for transaction 28129.
... systemd-resolved[26586]: Using DNS server 1.1.1.1#cloudflare-dns.com for transaction 60138.
... systemd-resolved[26586]: Using DNS server 168.126.63.1 for transaction 10283.
... systemd-resolved[26586]: Using DNS server 168.126.63.1 for transaction 6472.
자주 쓰는 [Resolve] 옵션은 다음과 같다.
| 옵션 | 예시 | 의미 |
|---|---|---|
| DNS= | 1.1.1.1#cloudflare-dns.com | 전역 DNS 서버(#뒤는 TLS 서버 이름) |
| FallbackDNS= | 9.9.9.9 | 어떤 링크에도 DNS가 없을 때만 사용 |
| Domains= | ~. / corp.example / ~corp.example | ~.는 모든 질의를 전역 서버로, 일반 값은 검색 도메인, ~값은 해당 도메인만 라우팅(split DNS) |
| DNSOverTLS= | yes / opportunistic / no | yes는 TLS 실패 시 질의 실패, opportunistic은 평문으로 후퇴 |
| DNSSEC= | allow-downgrade / yes / no | 서명 검증. 지원 안 하는 업스트림에서 yes는 해석 실패를 부른다 |
| Cache= | yes / no-negative / no | 응답 캐시 |
| ReadEtcHosts= | yes | /etc/hosts 항목을 stub 응답에 포함 |
| DNSStubListener= | yes / no | 127.0.0.53:53 리스너. 다른 DNS 서버를 53번에 띄우려면 no |
/etc/hosts와 stub의 관계
nsswitch.conf 순서대로 files(/etc/hosts)가 DNS보다 먼저다. resolved stub도 /etc/hosts를 읽기 때문에 dig 결과에도 나온다.
$ grep ^hosts /etc/nsswitch.conf
hosts: files mdns4_minimal [NOTFOUND=return] dns
$ echo '10.10.0.5 db.lab.example db' | sudo tee -a /etc/hosts
10.10.0.5 db.lab.example db
$ getent hosts db.lab.example
10.10.0.5 db.lab.example db
$ resolvectl query db.lab.example
db.lab.example: 10.10.0.5
-- Information acquired via protocol DNS in 1.7ms.
-- Data is authenticated: yes; Data was acquired via local or encrypted transport: yes
-- Data from: synthetic
$ dig +short db.lab.example
10.10.0.5
$ dig +short @1.1.1.1 db.lab.example
$ dig db.lab.example @1.1.1.1 | grep status
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 32367
“Data from: synthetic”은 네트워크가 아니라 hosts에서 만든 답이라는 뜻이다. 업스트림 서버 자체를 시험하려면 dig @서버로 stub을 우회한다.
캐시 확인과 비우기
DNS 레코드를 바꿨는데 예전 IP가 나오면 캐시부터 비운다. statistics는 root 권한이 필요하다.
$ sudo resolvectl statistics | sed -n '/^Cache/,/Misses/p'
Cache
Current Cache Size: 4
Cache Hits: 6
Cache Misses: 10
$ sudo resolvectl flush-caches
$ sudo resolvectl statistics | sed -n '/^Cache/,/Misses/p'
Cache
Current Cache Size: 0
Cache Hits: 6
Cache Misses: 10
주의사항
| 증상 | 원인 | 조치 |
|---|---|---|
| /etc/resolv.conf 수정이 사라짐 | stub 파일로의 심볼릭 링크라 resolved가 다시 씀 | Netplan nameservers 또는 resolved.conf.d drop-in으로 설정 |
| 설정한 DNS 뒤에 DHCP DNS가 붙음 | DHCP 서버가 병합됨 | Netplan dhcp4-overrides에 use-dns: false |
| 전역 DNS를 넣었는데 DHCP DNS로도 질의 | 전역과 링크가 모두 기본 라우트 | resolved.conf에 Domains=~. |
| DoT 설정 후 해석이 전부 실패 | 업스트림이 853을 지원하지 않거나 #서버이름 불일치 | 지원 서버 사용, 임시로 DNSOverTLS=opportunistic |
| 53번 포트를 쓰는 DNS 서버(dnsmasq, bind)가 안 뜸 | 127.0.0.53:53을 stub 리스너가 점유 | DNSStubListener=no 후 resolv.conf 링크를 uplink 파일로 교체 |
| netplan apply 후 경고 | yaml 파일 권한이 600이 아님 | sudo chmod 600 /etc/netplan/*.yaml |
| resolvectl statistics: Permission denied | 모니터링 소켓은 root 전용 | sudo로 실행 |
원격 서버에서 netplan apply는 링크를 다시 구성하므로 콘솔 접근이 가능한 상태에서 시도한다.
마무리
Ubuntu 서버의 DNS는 “/etc/resolv.conf는 stub, 실제 서버는 Netplan 또는 resolved.conf“로 기억하면 된다. 링크별 서버는 Netplan, 전역 서버나 DoT는 drop-in으로 두고, 결과는 항상 resolvectl status와 resolvectl query로 확인한다.