Ubuntu 16.04下搭建私有BIND DNS服务器实战指南

发布时间:2026/6/23 18:02:00
Ubuntu 16.04下搭建私有BIND DNS服务器实战指南 1. 项目概述为什么在私有网络里亲手搭一个BIND DNS服务器比用现成的路由器DNS强十倍你有没有遇到过这样的场景公司内网几十台开发机、测试设备、IoT终端全靠IP地址硬记或靠主机名靠/etc/hosts手工维护改个服务IP就得挨个SSH去改hosts文件新同事入职要花半天配环境Docker容器之间连个curl api-service:8080都报Name or service not known——这不是运维水平问题是基础命名解析体系没立住。我2017年在一家做工业边缘计算的团队接手时整个192.168.50.0/24网段就靠一台老路由器自带的DNS转发结果某天它固件升级后DNS缓存崩了所有自动化脚本集体失联产线数据采集停摆两小时。后来我们用Ubuntu 16.04BIND9搭了一套轻量但完全可控的私有DNS三年没重启过所有设备开机自动注册服务变更秒级生效。这不是炫技而是把“名字到地址”这个最底层的映射权从不可控的黑盒里拿回来。关键词BIND、Ubuntu 16.04、DNS Server、Private Network、Configure——这五个词串起来本质是在说用开源、稳定、可审计的方案给你的局域网装上自己的“电话簿总机”。它不对外提供服务不处理公网查询只专注解决内部设备“我是谁”“我在哪”“我要找谁”这三个问题。对中小团队、实验室、嵌入式开发环境、Kubernetes测试集群来说它比dnsmasq更严谨比CoreDNS更易调试比修改每台机器的hosts更可持续。你不需要懂DNS协议RFC文档但得明白一件事DNS不是魔法它是一套有状态、可配置、必须理解其工作流的基础设施。接下来我会带你从零开始把BIND变成你内网里最安静也最可靠的那台服务器。2. 整体设计思路与方案选型为什么选BIND9而不是其他方案以及Ubuntu 16.04这个“老版本”的真实价值2.1 为什么非得是BIND9不是dnsmasq不是CoreDNS更不是Windows AD DNS很多人第一反应是“用dnsmasq不更简单”——确实dnsmasq安装快、配置少、内存占用小。但它本质是个“DNS转发器DHCP服务器”没有真正的区域zone管理能力不能做主从同步ACL控制粒度粗日志几乎等于没有。而BIND9是DNS协议的事实标准实现由ISCInternet Systems Consortium维护全球90%以上权威DNS服务器都在跑它。它的优势不在“快”而在“稳”和“可控”精确的访问控制能按IP段、子网、甚至单个客户端限制哪些域名可查、哪些查询被拒绝完整的区域文件语法支持SOA、NS、A、CNAME、PTR、SRV等全部记录类型还能写注释、用宏定义简化重复配置成熟的日志审计体系可以单独开启query log、security log、client log排查问题时直接grep就能定位到哪台机器在疯狂刷请求主从同步机制未来要加高可用只要再起一台BIND从服务器几行配置就能自动同步区域数据不用手动rsync。至于CoreDNS它是云原生时代的产物用Go写插件化架构很酷但2017年时它的文档还不完善Ubuntu 16.04官方源里也没有稳定包调试时堆栈全是Go runtime对Linux系统管理员不友好。而BIND9在Ubuntu 16.04的apt源里是bind9包版本是9.10.3虽然不是最新但经过了数年生产环境锤炼bug极少且所有教程、排错指南、社区问答都围绕这个组合展开——省下的时间够你多写三版业务代码。2.2 Ubuntu 16.04不是怀旧是为稳定性主动选择的“慢节奏”现在主流发行版都到了22.04甚至24.04为什么标题还死守16.04这不是技术保守而是工程判断。Ubuntu 16.04是LTSLong Term Support版本官方支持到2021年4月但更重要的是它的软件生态极其成熟systemd版本是229足够稳定没有后期版本那些复杂的unit依赖陷阱iptables还是默认防火墙规则直观不像nftables需要重新学一套语法所有BIND9相关路径、用户、日志位置都固化配置在/etc/bind/区域文件在/var/lib/bind/日志默认走/var/log/syslog新手不会被/etc/systemd/resolved.conf和/run/systemd/resolve/stub-resolv.conf绕晕。我试过在20.04上部署结果发现systemd-resolved会偷偷监听53端口和BIND冲突得先disable它再改/etc/nsswitch.conf再重启network-manager——这些额外步骤对只想快速搭好DNS的人来说就是无谓的摩擦。而16.04里resolvconf还没接管DNS配置一切干净利落。当然如果你的环境强制要求新系统这套思路依然适用只是路径和命令微调核心逻辑毫发无伤。2.3 私有网络DNS的核心边界它只做三件事绝不越界很多初学者一上来就想让BIND既解析内网域名又转发公网查询还带缓存加速。这看似全能实则埋雷。我们的设计原则非常明确只响应来自私有网段的查询比如192.168.50.0/24、10.0.0.0/8其他来源一律拒绝只解析预定义的内部域名如dev.example.local、iot-sensor.example.local对google.com这种公网域名直接返回REFUSED不转发不提供递归查询服务BIND在这里是“权威服务器”Authoritative Server不是“递归解析器”Recursive Resolver。这意味着它不帮你去问根DNS、问.com顶级域它只回答“我知道的”。这个边界划清后安全性和可维护性直线上升。你不用担心有人用你的DNS做放大攻击也不用操心缓存污染、TTL策略、上游超时重试这些递归服务器才要面对的复杂问题。它就像一栋楼的门禁系统只认本楼住户的门禁卡不帮访客查隔壁楼怎么走。3. 核心细节解析与实操要点BIND配置文件结构、权限模型与安全基线3.1 BIND的“三件套”配置文件named.conf到底拆成几个文件为什么这么拆BIND9的配置不是写在一个大文件里而是分层加载这是它强大又易出错的地方。Ubuntu 16.04默认的/etc/bind/named.conf其实是个入口文件内容极简include /etc/bind/named.conf.options; include /etc/bind/named.conf.local; include /etc/bind/named.conf.default-zones;这三部分各司其职named.conf.options全局选项控制监听地址、允许查询的客户端、是否递归、日志路径等named.conf.local本地自定义区域zones的声明也就是你内网域名的“注册处”named.conf.default-zonesUbuntu自带的localhost和反向解析区域一般不动。为什么必须拆开因为named.conf.options会被所有区域共享如果把zone声明也塞进去一旦某个zone配置出错整个BIND启动失败你连最基本的localhost解析都没了。而拆开后named.conf.local出错BIND还能带着默认zone跑起来至少ping localhost是通的给你留出修复时间。这是运维的黄金法则故障隔离。3.2 权限模型为什么BIND进程必须以bind用户运行且区域文件权限必须是640BIND9的安全设计非常古老但有效它启动时以root身份读取配置、绑定53端口只有root能绑1024以下端口然后立刻setuid()降权以bind用户身份运行后续所有操作。这意味着配置文件/etc/bind/目录权限应为755属主root:bind区域文件如db.example.local必须放在/var/lib/bind/下权限640属主bind:bind日志文件如果单独配置目录权限也得是750属主bind:bind。我踩过的坑有次把区域文件放在/etc/bind/zones/下权限设成644结果BIND启动时报error: loading configuration: permission denied。查了半天才发现/etc/bind/目录本身是755但BIND降权后bind用户对/etc/bind/zones/只有读权限而它需要读取文件内容还得检查文件所属组——如果组不是bind就会失败。解决方案不是chmod 777绝对禁止而是chown root:bind /etc/bind/zones chmod 750 /etc/bind/zones再把文件chown bind:bind db.example.local。这个细节决定了你的DNS是坚如磐石还是随时崩溃。3.3 安全基线五条必须写进named.conf.options的铁律下面这段配置是我在线上环境跑了三年的最小安全集每一行都有明确目的绝非照搬模板options { directory /var/cache/bind; // 1. 只监听内网IP绝不监听0.0.0.0或127.0.0.1除非你真需要本机查 listen-on { 192.168.50.10; }; // 假设服务器IP是192.168.50.10 listen-on-v6 { none; }; // IPv6关掉除非你真用 // 2. 只允许内网客户端查询其他一律拒之门外 allow-query { 192.168.50.0/24; }; // 3. 绝不提供递归服务REFUSE所有非授权查询 recursion no; allow-recursion { none; }; // 4. 不向外界透露BIND版本防信息泄露 version not currently available; // 5. 关键日志查询日志单独开方便审计 logging { channel query_log { file /var/log/bind/query.log versions 3 size 5m; severity info; print-time yes; print-severity yes; print-category yes; }; category queries { query_log; }; }; // 其他默认项保持不变... };逐条解释listen-on指定具体IP而非any避免意外暴露在公网allow-query是白名单不是黑名单宁可严一点后面加IP比删IP安全recursion no是核心它让BIND只回答自己“权威”的域名对baidu.com这种直接返回REFUSED不转发、不缓存version隐藏版本号防止攻击者根据已知漏洞扫描查询日志单独配置5MB轮转3个历史版本避免日志撑爆磁盘。提示allow-query和allow-recursion是两回事。前者控制“谁能问”后者控制“问什么能答”。我们这里两者都设为内网段但语义不同allow-query放行所有查询请求包括对example.local的A记录查询而allow-recursion因为设为none所以即使有人问google.comBIND也直接拒绝不尝试递归。4. 实操过程与核心环节实现从安装到验证的完整流水线含每一步的意图说明4.1 环境准备四步确认避免90%的启动失败在敲apt install之前请务必执行这四个检查它们能省下你两小时debug时间确认系统时间准确date命令看时区和时间。BIND对时间敏感如果服务器时间比客户端快几分钟某些动态更新会失败。用sudo timedatectl set-ntp on启用NTP同步确认53端口空闲sudo ss -tuln | grep :53。如果看到127.0.0.1:53或*:53被占用大概率是systemd-resolved或dnsmasq在抢端口。sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved再删掉/etc/resolv.conf的软链接重建为普通文件确认防火墙放行sudo ufw status。如果ufw启用必须加规则sudo ufw allow from 192.168.50.0/24 to any port 53确认hostname规范hostname -f应返回类似dns-server.example.local的FQDN不能是ubuntu或localhost。编辑/etc/hostname和/etc/hosts确保127.0.1.1行指向你的FQDN。做完这四步再执行安装sudo apt update sudo apt install bind9 bind9utils bind9-doc安装完成后BIND默认是禁用状态别急着start先配置。4.2 配置named.conf.options一行一意图拒绝无脑复制打开/etc/bind/named.conf.options清空内容粘贴我们前面说的五条铁律配置。特别注意listen-on里的IP必须是你服务器在私有网段的真实IP不是127.0.0.1。假设你的DNS服务器IP是192.168.50.10那么这一行就是listen-on { 192.168.50.10; };。为什么不能写127.0.0.1因为其他内网机器要查DNS得通过192.168.50.10这个地址发请求BIND必须在这个地址上监听。保存后用BIND自带的语法检查器验证sudo named-checkconf如果没输出说明语法正确如果有错它会精确到第几行第几个字符比如/etc/bind/named.conf.options:12: unknown option listne-on拼错了立刻修正。4.3 创建正向解析区域example.local的完整区域文件详解现在创建你的第一个内网域名example.local。在/etc/bind/named.conf.local末尾添加zone example.local { type master; file /var/lib/bind/db.example.local; allow-update { none; }; // 禁止动态更新纯静态 };然后创建区域文件/var/lib/bind/db.example.local内容如下注意SOA记录里的邮箱地址用.代替这是DNS规范$TTL 300 IN SOA ns1.example.local. admin.example.local. ( 2024052001 ; Serial (YYYYMMDDNN) 3600 ; Refresh 1800 ; Retry 604800 ; Expire 86400 ) ; Minimum TTL ; Name servers IN NS ns1.example.local. ; A records for name servers ns1 IN A 192.168.50.10 ; Your services dev IN A 192.168.50.20 test IN A 192.168.50.21 jenkins IN A 192.168.50.22 gitlab IN A 192.168.50.23关键点解析$TTL 300默认生存时间5分钟客户端缓存这个记录最多300秒Serial版本号每次改区域文件必须递增主从同步靠它判断是否更新。我用YYYYMMDDNN格式比如今天是2024年5月20日第一次改就是2024052001第二次就是2024052002NS记录指向ns1.example.local.注意末尾的.表示绝对域名不补example.localA记录直接映射主机名到IPdev.example.local→192.168.50.20。保存后检查区域文件语法sudo named-checkzone example.local /var/lib/bind/db.example.local它会输出OK或详细错误。常见错误是忘了末尾的.或者Serial格式不对必须是纯数字。4.4 创建反向解析区域让192.168.50.20能查到dev.example.local正向解析名字→IP有了反向解析IP→名字也得配否则nslookup 192.168.50.20会失败。反向区域名是IP段倒过来加.in-addr.arpa192.168.50.0/24对应50.168.192.in-addr.arpa。在/etc/bind/named.conf.local再加一段zone 50.168.192.in-addr.arpa { type master; file /var/lib/bind/db.192.168.50; allow-update { none; }; };创建/var/lib/bind/db.192.168.50$TTL 300 IN SOA ns1.example.local. admin.example.local. ( 2024052001 3600 1800 604800 86400 ) IN NS ns1.example.local. 20 IN PTR dev.example.local. 21 IN PTR test.example.local. 22 IN PTR jenkins.example.local. 23 IN PTR gitlab.example.local.注意PTR记录的左边是IP的最后一位即192.168.50.20的20右边是完整的FQDN末尾必须有.。检查sudo named-checkzone 50.168.192.in-addr.arpa /var/lib/bind/db.192.168.504.5 启动与验证从服务启动到跨机器查询的全流程配置完启动服务sudo systemctl start bind9 sudo systemctl enable bind9 # 开机自启检查状态sudo systemctl status bind9 # 应看到 active (running)且没有failed字样查看日志确认无错sudo tail -20 /var/log/syslog | grep named # 正常应有类似 named[1234]: zone example.local/IN: loaded serial 2024052001 的日志现在在DNS服务器本机验证dig 127.0.0.1 dev.example.local A short # 应返回 192.168.50.20 dig 127.0.0.1 192.168.50.20 PTR short # 应返回 dev.example.local.再从另一台内网机器比如IP是192.168.50.100的开发机验证# 先临时改本机DNS echo nameserver 192.168.50.10 | sudo tee /etc/resolv.conf # 然后查 nslookup dev.example.local # 应显示 Server: 192.168.50.10, Address: 192.168.50.10#53, Name: dev.example.local, Address: 192.168.50.20注意nslookup和dig的区别。nslookup是老工具会读/etc/resolv.confdig更现代可以用IP指定服务器不依赖本机配置。生产环境推荐用dig因为它行为更确定。5. 常见问题与排查技巧实录真实踩坑记录与独家速查表5.1 “SERVFAIL”不是配置错是权限或SELinux惹的祸Ubuntu虽无SELinux但权限逻辑通用现象dig 192.168.50.10 dev.example.local返回status: SERVFAIL但named-checkzone明明是OK的。排查路径查BIND日志sudo journalctl -u bind9 -n 50 --no-pager重点看error和warning最常见原因是区域文件权限不对。ls -l /var/lib/bind/确认db.example.local属主是bind:bind权限是-rw-r-----640检查/var/lib/bind/目录权限drwxr-s---2750组是bind且设置了setgid位s这样新创建的文件自动继承组如果改了权限必须重启BINDsudo systemctl restart bind9因为BIND启动时读一次文件权限之后不重读。实操心得我写了个一键检查脚本check-bind-perms.sh放在/usr/local/bin/下#!/bin/bash echo Checking /var/lib/bind permissions ls -ld /var/lib/bind ls -l /var/lib/bind/ echo Checking named config sudo named-checkconf echo Checking zones sudo named-checkzone example.local /var/lib/bind/db.example.local sudo named-checkzone 50.168.192.in-addr.arpa /var/lib/bind/db.192.168.505.2 “REFUSED”满天飞到底是allow-query没配对还是recursion关错了现象dig 192.168.50.10 google.com返回status: REFUSED这是正常的但dig 192.168.50.10 dev.example.local也返回REFUSED这就错了。根本原因allow-query里没包含你的客户端IP。比如你的客户端是192.168.50.100但named.conf.options里写的是allow-query { 192.168.50.0/25; };只到127那100就不在范围内。速查方法在DNS服务器上抓包sudo tcpdump -i any port 53 -nn -A然后从客户端dig 192.168.50.10 dev.example.local看tcpdump是否捕获到请求。如果没捕获说明请求根本没到服务器客户端DNS配置错或防火墙拦了如果捕获了但BIND没回包那就是allow-query拒绝了临时放宽allow-query为{ any; };如果这时能通100%是IP段写错了。5.3 “NXDOMAIN”还是“NOERROR”教你一眼看懂DNS响应码背后的真相dig返回的status字段是诊断核心NOERROR查询成功但没找到记录。比如查nonexist.example.local区域里没这行就返回NOERRORANSWER: 0NXDOMAIN域名不存在即权威服务器明确告诉你“这个域名我根本没管”。比如查example.comBIND因为recursion no直接返回NXDOMAINSERVFAIL服务器内部错误配置、权限、路径问题REFUSED服务器拒绝为你服务通常是allow-query或allow-recursion策略拦截。速查表现象dig命令预期status最可能原因能解析内网域名dig 192.168.50.10 dev.example.localNOERROR正常不能解析内网域名dig 192.168.50.10 dev.example.localNXDOMAIN区域文件里没写dev这行或域名拼错不能解析任何域名dig 192.168.50.10 dev.example.localSERVFAIL权限错、路径错、named-checkzone没过内网能查公网查不了dig 192.168.50.10 google.comREFUSED正常recursion no生效内网也查不了dig 192.168.50.10 dev.example.localREFUSEDallow-query没包含客户端IP5.4 日志爆炸与磁盘占满如何优雅地轮转BIND查询日志默认logging配置下/var/log/bind/query.log会一直追加不出三天就上G。Ubuntu 16.04的logrotate没默认管BIND日志得手动加。创建/etc/logrotate.d/bind9/var/log/bind/*.log { daily missingok rotate 7 compress delaycompress notifempty create 640 bind bind sharedscripts postrotate if [ -f /var/run/named/named.pid ]; then kill -SIGUSR2 cat /var/run/named/named.pid fi endscript }解释daily每天轮转rotate 7保留7天create 640 bind bind轮转后新建日志文件权限640属主bind:bindpostrotate里发SIGUSR2信号给BIND让它重新打开日志文件BIND支持这个信号。测试sudo logrotate -d /etc/logrotate.d/bind9-d是debug模式看它会不会报错再sudo logrotate -f /etc/logrotate.d/bind9强制轮转一次。5.5 进阶技巧用rndc动态重载配置不用重启服务每次改完配置都要systemctl restart bind9太重了。BIND自带rndcRemote Name Daemon Control工具可以热重载# 首先生成rndc密钥只需一次 sudo rndc-confgen -a # 这会生成 /etc/bind/rndc.key并自动设置权限 # 然后改完named.conf或区域文件后 sudo rndc reconfig # 重载named.conf新增zone会生效 sudo rndc reload example.local # 重载单个zone改了db文件后用rndc reconfig比restart快10倍且不中断已有查询。我习惯把这两条alias进~/.bashrcalias bind-reloadsudo rndc reconfig alias bind-reload-zonesudo rndc reload example.local6. 后续演进与实用扩展从单机DNS到可维护的内网命名体系这套方案跑稳后你会自然遇到新需求。这里分享三个我实际落地的扩展方向都不复杂但极大提升体验6.1 自动化用Ansible一键部署整套BIND5分钟搞定新环境手敲配置终究会错。我用Ansible写了playbook核心任务就四步apt安装bind9包template模块渲染named.conf.optionsIP和网段作为变量传入copy模块推送预定义的db.example.local和db.192.168.50service模块启动并enable bind9。变量文件group_vars/all.yml里定义bind_server_ip: 192.168.50.10 bind_network: 192.168.50.0/24 bind_domain: example.local这样换一个网段改三行变量ansible-playbook deploy-bind.yml就全搞定了。比写shell脚本靠谱比手动操作快十倍。6.2 动态更新让Docker容器启动时自动注册DNS记录静态配置适合固定IP的服务但Docker容器IP是动态分配的。我们用nsupdate实现在named.conf.local里把allow-update从{ none; }改成{ key docker-key; };用ddns-confgen -k docker-key生成TSIG密钥容器启动脚本里加一行echo server 192.168.50.10 key docker-key 密钥内容 update add container1.example.local. 300 A 172.17.0.5 send | nsupdate这样容器就知道自己叫什么、在哪其他服务curl container1.example.local:8080就能通。TSIG密钥保证只有可信客户端能更新比开放allow-update { any; }安全得多。6.3 监控告警用PrometheusBlackbox Exporter盯住DNS健康BIND本身不暴露metrics但我们可以监控它的基础健康netstat -tuln | grep :53确认53端口在监听dig 192.168.50.10 dev.example.local short返回正确IPtail -1 /var/log/bind/query.log时间戳是最近1分钟内的。用Blackbox Exporter的dns模块配置一个probe- job_name: bind-dns metrics_path: /probe params: module: [dns] static_configs: - targets: - 192.168.50.10:53 relabel_configs: - source_labels: [__address__] target_label: __param_target replacement: dev.example.local - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: blackbox-exporter:9115配上Alertmanager规则当连续3次dig超时就发钉钉告警。DNS挂了整个内网服务链就断了必须一级告警。我在实际使用中发现BIND9在Ubuntu 16.04上的稳定性远超预期。三年来它只因一次硬盘故障宕机过而那次故障本身跟BIND无关。真正让我省心的是它的“可预测性”配置改了什么日志里清清楚楚哪里不通dig和tcpdump一组合5分钟内必定位。它不时髦但像一把老焊枪握在手里沉甸甸的知道它不会突然罢工。如果你也在为内网服务发现头疼不妨花一小时搭起它——这一个小时会为你未来半年省下几十个小时的排障时间。