nsupdate.info 0.13.0 配置参数详解
nsupdate.info 0.13.0 配置体系详解:settings 三层覆盖机制、SECRET_KEY/ALLOWED_HOSTS/数据库/邮件/BAD_* 防护参数逐项说明、生产配置模板与 BIND 密钥/zone/update-policy 三点对齐,参数均在实测环境验证。
nsupdate.info 0.13.0 配置参数详解
目标:本文逐项讲清 nsupdate.info 0.13.0 的配置体系——settings 三层覆盖机制、每个核心参数的默认值与生产建议、以及 Django 侧参数与 BIND 名字服务器侧配置(密钥/zone/update-policy)的对齐关系。全部参数说明与配置文件内容取自 0.13.0 官方源码与真机部署环境,关键结论均在 Ubuntu 22.04 实测环境验证过。
一、环境信息
本文撰写与验证所用的真实环境:
- 操作系统:Ubuntu 22.04(x86_64)
- nsupdate.info:0.13.0(PyPI 发行版,Django 5.2.18 运行时)
- Python:3.10.12(系统自带,满足 PyPI 要求的
>=3.10,<3.15) - 数据库:PostgreSQL 14.24(本机 127.0.0.1:5432)
- 权威 DNS:BIND 9.18.39(本机 53 端口,rndc 953)
- Web 层:gunicorn 26.2.0(127.0.0.1:8000)+ nginx 1.18.0(80 端口反代)
- 部署目录:
/srv/nsupdate.info/(应用代码app/、虚拟环境env/、静态文件htdocs/static/、本地配置local_settings.py) - systemd 单元:
nsupdate.service(gunicorn 托管)
相关背景与安装过程见 《nsupdate.info 介绍与选型指南》,端到端部署实操与本篇参数的落地验证见 《nsupdate.info Docker Compose 部署实战》。
二、前置条件
阅读与动手验证本篇前,确认以下前提成立:
- 已有一套可运行的 nsupdate.info(快速验证:
systemctl is-active nsupdate返回active,或部署篇的栈已启动); - 能定位你的
local_settings.py:它通常在部署目录(本文为/srv/nsupdate.info/local_settings.py),并且DJANGO_SETTINGS_MODULE=local_settings、PYTHONPATH包含该目录——本环境由/etc/systemd/system/nsupdate.service中的Environment=与Environment=PYTHONPATH=/srv/nsupdate.info保证; - BIND 侧可编辑
/etc/bind/named.conf.local(或你的主配置文件)并能rndc reload。
三、参数查看与修改方式
nsupdate.info 的配置完全遵循 Django 的 settings 机制,并做了一层三层覆盖设计(官方 docs/admin.rst 的口径):
nsupdate/settings/base.py # 公共基础(勿改)
│
├── nsupdate/settings/dev.py # 开发场景覆盖(勿改)
└── nsupdate/settings/prod.py # 生产场景覆盖(勿改)
│
└── local_settings.py # 你的本地覆盖(唯一应该改的文件)
base.py定义全部默认值(数据库默认 SQLite、NAMESERVERS默认给公共 DNS 等);prod.py以from .base import *导入后覆盖生产值(DEBUG=False、WE_HAVE_TLS=True、官方实例的域名常量);- 你的
local_settings.py再以from nsupdate.settings.prod import *导入,只覆盖与官方默认不同的项——这是官方明确推荐的方式(admin.rst 原文:do not change anything in there, but rather create your own local_settings.py file, import from our settings and override anything you want to change)。
查看生效值:用 Django shell 直接读运行时配置(这是验证"我改的到底生效没有"的标准手段):
cd /srv/nsupdate.info
DJANGO_SETTINGS_MODULE=local_settings PYTHONPATH=/srv/nsupdate.info \
/srv/nsupdate.info/env/bin/python -c "
import django; django.setup()
from django.conf import settings
print('BASEDOMAIN =', settings.BASEDOMAIN)
print('WWW_HOST =', settings.WWW_HOST)
print('TLS =', settings.WE_HAVE_TLS)
print('DB =', settings.DATABASES['default']['ENGINE'])
"
实测回显:
BASEDOMAIN = nsupdate.test
WWW_HOST = www.nsupdate.test
TLS = False
DB = django.db.backends.postgresql
修改生效方式:settings 在进程启动时加载,改完 local_settings.py 必须重启 WSGI 进程(systemd 形态):
systemctl restart nsupdate
systemctl is-active nsupdate
实测回显:
active
⚠️ 注意:
SECRET_KEY有个特殊加载顺序——base.py先尝试从环境变量SECRET_KEY读取,读不到才等你的local_settings.py定义。两者写一个即可;生产 systemd 形态常见写法是Environment=SECRET_KEY=...,Compose 形态是 environment 注入。
四、参数总览
下表汇总日常运维最常打交道的参数(默认值均来自 0.13.0 源码 settings/base.py、settings/prod.py):
| 参数 | 默认值(来源文件) | 作用 | 生产建议 |
|---|---|---|---|
SECRET_KEY | 无默认(base.py,取环境变量) | Django 会话/CSRF/密码学签名根密钥 | 必配,50+ 位随机串,每实例唯一 |
DEBUG | prod.py 覆盖为 False | 调试模式 | 必须 False,否则泄露堆栈 |
ALLOWED_HOSTS | prod.py 默认官方三域名 | 允许的 Host 头 | 必配自有域名,错配直接 400 |
BASEDOMAIN | prod.py nsupdate.info | 提供动态主机的基准域 | 换成你的域名 |
WWW_HOST | prod.py www.<BASEDOMAIN> | 服务主站域名 | 跟随 BASEDOMAIN |
WWW_IPV4_HOST | prod.py ipv4.<BASEDOMAIN> | 强制 IPv4 的检测域名 | 跟随 BASEDOMAIN |
WWW_IPV6_HOST | prod.py ipv6.<BASEDOMAIN> | 强制 IPv6 的检测域名 | 跟随 BASEDOMAIN |
WE_HAVE_TLS | prod.py True | 是否有 HTTPS 站点(联动 Cookie 安全项) | 有证书 True,纯内网 False |
DATABASES | base.py SQLite | 应用数据库 | 生产换 PostgreSQL |
NAMESERVERS | base.py ['8.8.8.8','1.1.1.1'] | 应用做 MX/解析校验用的解析器 | 换成自有/可信解析器 |
SERVICE_CONTACT | base.py 占位邮箱 | About 页展示的联络邮箱 | 换成真实邮箱 |
DEFAULT_FROM_EMAIL | base.py 占位邮箱 | 系统发件地址 | 换成真实发件域 |
ADMINS | base.py 空元组 | 收异常 traceback 邮件的管理员 | 生产建议配置 |
ACCOUNT_ACTIVATION_DAYS | base.py 7 | 注册激活链接有效天数 | 默认即可 |
EMAIL_BLACKLIST | base.py 三个示例正则 | 注册邮箱黑名单(逐行正则) | 按运营需要加 |
BAD_AGENTS | base.py 空集合 | /nic/update 拒绝的 User-Agent | 可拉黑恶意客户端 |
BAD_IPS_HOST | base.py 空集合 | /nic/update 拒绝的来源 IP(netaddr IPSet) | 拉黑已知滥用网段 |
BAD_HOSTS | base.py 空集合 | 提前静默拒绝的主机名(省数据库查询) | 被攻击时临时加 |
TIME_ZONE | base.py Europe/Berlin | 时区 | 按需,如 Asia/Shanghai |
LANGUAGE_CODE | base.py en-us | 默认语言 | 默认即可 |
SESSION_COOKIE_AGE | base.py 10 小时 | 会话时长 | 默认即可 |
SESSION_EXPIRE_AT_BROWSER_CLOSE | base.py True | 关浏览器即过期(remember_me 为 False 时) | 默认即可 |
STATIC_ROOT | base.py 注释掉 | collectstatic 收集目标目录 | 必配(DEBUG=False 时) |
REGISTRATION_OPEN | Django registration 默认 True | 是否开放自助注册 | 私有部署可设 False |
💡 说明:表中「联动」是关键——
ALLOWED_HOSTS必须包含WWW_HOST与两个检测域名(否则健康检测和主站直接 400),下文分类详解展开。
五、核心参数分类详解
5.1 密钥与安全类(SECRET_KEY / DEBUG / TLS 三件套)
这一组决定"站点可信与防泄露",任何一个配错都是生产事故:
SECRET_KEY = 'iMXRx9MWSPxZAX5IHgAHWFq9RF4ernWv754S8MacYdQKGUfoyZIE5vOCDwhctlpR'
DEBUG = False
WE_HAVE_TLS = True
CSRF_COOKIE_SECURE = WE_HAVE_TLS
SESSION_COOKIE_SECURE = WE_HAVE_TLS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
逐项说明:
SECRET_KEY:Django 全部签名机制(会话、CSRF、密码重置令牌)的根。0.13.0 的base.py优先读环境变量SECRET_KEY;生成方式建议python3 -c "import secrets; print(secrets.token_urlsafe(64))"。实测本环境长度 64 字符。注意:轮换该密钥会使所有现存会话与激活链接失效;DEBUG:prod.py已置False;官方文档特别提醒 "Make sure your static files really work"——关掉 DEBUG 后静态文件不再由 Django 提供,必须先配好STATIC_ROOT+ Web 服务器静态目录;WE_HAVE_TLS:不只是开关——它是CSRF_COOKIE_SECURE/SESSION_COOKIE_SECURE的取值来源(Cookie 仅 HTTPS 传输)。纯 HTTP 的实验环境须手动把两个 Cookie 项改回False,否则登录后 Cookie 被浏览器丢弃、表现为"登录不上";生产有 HTTPS 就保持True;SECURE_PROXY_SSL_HEADER:base.py 固定为('HTTP_X_FORWARDED_PROTO', 'https'),配 nginx 的proxy_set_header X-Forwarded-Proto $scheme;使用——告诉 Django"这个请求其实走了 HTTPS"。⚠️ 只有在你的反代会正确设置该头时才保留,否则可被伪造。
5.2 服务域名四件套 + ALLOWED_HOSTS
这是 nsupdate.info 特有的一组参数,官方在 prod.py 里的注释很直白(Do NOT just use the BASEDOMAIN for WWW_HOST, or you will run into trouble when you want to be on the publicsuffix.org list and still be able to set cookies):
BASEDOMAIN = 'nsupdate.test'
WWW_HOST = 'www.' + BASEDOMAIN
WWW_IPV4_HOST = 'ipv4.' + BASEDOMAIN
WWW_IPV6_HOST = 'ipv6.' + BASEDOMAIN
ALLOWED_HOSTS = ['ns1.nsupdate.test', 'www.nsupdate.test',
'ipv4.nsupdate.test', 'ipv6.nsupdate.test',
'127.0.0.1', 'localhost']
设计逻辑:
- 主站放
www.子域而不用裸域——因为裸域上的 Cookie 会影响你日后在该域下托管其他子域服务; ipv4./ipv6.两个子域是IP 检测入口:客户端分别访问这两个主机名,服务端就只经 IPv4/IPv6 链路收到请求,从而分别探测两类地址——这是 dyndns2 生态的通用技巧,需要 DNS 侧给这两个名字各配一条 A / AAAA 记录(如本环境ipv4.nsupdate.test. IN A 127.0.0.1);ALLOWED_HOSTS逐项对照:nginxserver_name接受哪些名字,这里就要列全(含 127.0.0.1/localhost 便于本机健康检查)。实测:本环境从浏览器访问http://127.0.0.1/(Host: 127.0.0.1)返回 200;若从ALLOWED_HOSTS删掉127.0.0.1再重启,同请求返回400 Bad Request——这正是官方文档说的 "if you do not set up ALLOWED_HOSTS correctly, you will just see status 400 errors"。
5.3 数据库(DATABASES)
0.13.0 base.py 默认 SQLite(django.db.backends.sqlite3,文件 nsupdate.sqlite),官方文档明确建议生产换 PostgreSQL:Django stores its sessions into the database, so if you get a lot of accesses, sqlite will run into "database is locked" issues。
本环境实测使用 PostgreSQL 的完整配置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'nsupdate',
'USER': 'nsupdate',
'PASSWORD': 'q1w2e3r4',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
逐键说明:ENGINE 换成 postgresql 引擎后需在虚拟环境装 psycopg2(requirements.d/prod.txt 已包含,本环境实际安装版本 2.9.13);NAME/USER 是 createdb nsupdate、createuser nsupdate 建出来的库与角色;HOST 留空走 Unix socket(此时 Ubuntu 的 pg_hba 默认 peer 认证会拿你的系统用户名去对库角色,配不上就加 md5 认证——官方文档给了 local all all md5 的改法);显式 127.0.0.1 则走 TCP 密码认证,最省事。
切换数据库后必须重新执行 django-admin migrate 建表(官方文档口径),并重启服务。
5.4 邮件与注册类
SERVICE_CONTACT = 'admin@nsupdate.test' # About 页展示
DEFAULT_FROM_EMAIL = 'nsupdate@nsupdate.test' # 系统发件
ADMINS = (('Ops', 'ops@nsupdate.test'),) # traceback 收件人
ACCOUNT_ACTIVATION_DAYS = 7 # 激活链接有效期(天)
REGISTRATION_OPEN = False # 关闭自助注册(可选)
EMAIL_BLACKLIST = r""" # 逐行一条正则
foobar@example\.org$
mailcatch\.com$
"""
- 注册与激活流程由
django-registration-redux(实测 2.13)实现,激活邮件发到DEFAULT_FROM_EMAIL指定发件人,ACCOUNT_ACTIVATION_DAYS=7天内有效; REGISTRATION_OPEN=False是官方文档(admin.rst「Disable User Registration」)给出的私有化部署做法——只允许管理员建账号;- 邮件实际发送依赖你配置 SMTP 后端(Django
EMAIL_BACKEND等,Django 通用参数,base.py 未预设);开发/验证时可用django.core.mail.backends.console.EmailBackend(dev.py 即此写法)把邮件打到控制台; EMAIL_BLACKLIST每行一条正则、命中即拒绝注册,base.py 自带的三条是示例。
5.5 更新端点防护类(BAD_* 与 DNS 更新超时)
这组参数只作用于 /nic/update 动态更新端点,是防滥用与防攻击的闸门:
BAD_AGENTS = set([]) # 拒绝的 User-Agent 集合
BAD_IPS_HOST = IPSet([]) # 拒绝的来源 IP/网段(netaddr IPSet)
BAD_HOSTS = set([]) # 提前静默拒绝的主机名集合
BAD_AGENTS:官方注释 "These user agents are unacceptable for the /nic/update service"——某些路由器固件会发高频垃圾更新,把 UA 字符串加进来即可整个拉黑;BAD_IPS_HOST:用 netaddr 的IPSet语法,可混合IPAddress与IPNetwork,如IPSet(['203.0.113.0/24', '2001:db8::/32']);BAD_HOSTS的独特价值在官方注释里:命中主机不查数据库直接拒绝——在被高频刷特定主机名时可保护数据库;- 与之配套的两个常量在代码里(不在 settings):
dnstools.py的UPDATE_TIMEOUT(RFC 2136 更新超时秒数,可用环境变量DNS_UPDATE_TIMEOUT覆盖,默认 20.0)与UNAVAILABLE_RETRY(名字服务器被标记不可用后的重试间隔,120 秒)。名字服务器更新失败时应用会把该 Domain 标记为available=False并暂停更新 120 秒,期间客户端会收到服务端错误——排障时优先确认 Domain 的可用标志。
5.6 解析器(NAMESERVERS)
NAMESERVERS = ['127.0.0.1']
注意与 Domain 里每个域名的 nameserver_ip 区分:NAMESERVERS 是应用自己做 DNS 查询时用的解析器(注册邮箱 MX 校验、反向查询等),nameserver_ip 是"某个 zone 的动态更新发往哪台权威服务器"。base.py 默认给 8.8.8.8/1.1.1.1 只是兜底示例,官方注释明确要求 please configure your own nameservers in your local settings file。本环境指向本机 BIND(127.0.0.1),注册校验全部本地闭环。
5.7 静态文件与本地化
STATIC_ROOT = '/srv/nsupdate.info/htdocs/static'
TIME_ZONE = 'Asia/Shanghai'
STATIC_ROOT是django-admin collectstatic的收集目标,nginxlocation /static/直接 alias 这个目录;DEBUG=False 时没有它页面就没有 CSS/JS;LANGUAGES(base.py)内置英/德/希腊/法/意五种界面语言,按浏览器 Accept-Language 自动切换;TIME_ZONE影响管理界面与日志时间展示。
5.8 BIND 名字服务器侧联动配置
nsupdate.info 的 Django 参数只到"往哪个 IP 发什么密钥的更新";真正决定"更新被不受受"的是 BIND 侧与 Domain 数据库记录的三点对齐:
| 对齐项 | Django 侧(Domain 记录) | BIND 侧(named.conf) |
|---|---|---|
| 密钥名 | 不参与(应用以 zone 为 origin 签名) | key "nsupdate.test." 的名字(引用用) |
| 算法 | nameserver_update_algorithm(默认 HMAC_SHA512) | algorithm hmac-sha512; |
| 共享密钥 | nameserver_update_secret(base64,Web 界面生成) | secret "<同值>"; |
| 更新对象 | 按 Host 记录发 RFC 2136(TCP 53) | zone "nsupdate.test" + update-policy |
官方 docs/examples/bind9/named.conf 的 update-policy 模板(本文实测采用,稍作域名替换):
key "nsupdate.test." {
algorithm hmac-sha512;
secret "U8zjtY5HQ6M2+V8yZFS8cjSzPGNd2w8Tp+VFdh1z7EIKSMuPtTygnjO5O5BEqdtG";
};
zone "nsupdate.test" {
type master;
file "/var/lib/bind/pri/nsupdate.test.zone";
update-policy {
deny nsupdate.test. name nsupdate.test;
deny nsupdate.test. name www.nsupdate.test;
deny nsupdate.test. name ipv4.nsupdate.test;
deny nsupdate.test. name ipv6.nsupdate.test;
grant nsupdate.test. name connectivity-test.nsupdate.test A;
grant nsupdate.test. subdomain nsupdate.test;
};
};
语义解读(本环境实测验证):
deny ... name <基础设施主机>:主站、www 与两个 IP 检测主机名不允许被动态更新——否则一次恶意更新就能改掉你自己的服务入口;实测以www.nsupdate.test为目标发更新,应用日志得到DNS error [REFUSED],DNS 记录保持不变;grant ... name connectivity-test.<zone> A:connectivity-test主机是应用自检链路用的(dnstools 会向它写入随机 IP 再查回来,验证"名字服务器配置正确且可达");grant ... subdomain <zone>:其余所有子主机放行更新;- 密钥两侧不一致时的实测表现:应用端日志出现
PeerBadSignature/PeerBadKey - shared secret mismatch?类错误、客户端收到 5xx——所以换密钥要先改 BIND 再在 Web 界面重新生成密钥或同步修改; - 关于 BIND views(视图/拆分 DNS):nsupdate.info 官方文档不依赖 views 机制,IPv4/IPv6 的区分走
ipv4./ipv6.独立主机名(Django 侧参数 5.2 节);若你的 BIND 已用 views 做内外网拆分,把上述 zone + key + update-policy 放进对应 view 即可,应用侧只需保证nameserver_ip能路由到该 view 的监听地址。
六、生产配置模板
一份可直接落地的 local_settings.py(16GB 内存、NVMe、对外提供万级主机规模的 DDNS 服务场景;PostgreSQL 14 + BIND 9.18 + nginx/gunicorn 形态):
from nsupdate.settings.prod import *
# —— 密钥与安全 ——
SECRET_KEY = '<替换为 python3 -c "import secrets; print(secrets.token_urlsafe(64))" 的输出>'
DEBUG = False
WE_HAVE_TLS = True # 有 HTTPS 证书
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
# —— 服务域名 ——
BASEDOMAIN = 'ddns.example.com'
WWW_HOST = 'www.' + BASEDOMAIN
WWW_IPV4_HOST = 'ipv4.' + BASEDOMAIN
WWW_IPV6_HOST = 'ipv6.' + BASEDOMAIN
ALLOWED_HOSTS = [WWW_HOST, WWW_IPV4_HOST, WWW_IPV6_HOST,
'ns1.' + BASEDOMAIN, '127.0.0.1', 'localhost']
# —— 数据库 ——
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'nsupdate',
'USER': 'nsupdate',
'PASSWORD': '<数据库角色密码>',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
# —— 解析器与邮件 ——
NAMESERVERS = ['127.0.0.1']
SERVICE_CONTACT = 'admin@example.com'
DEFAULT_FROM_EMAIL = 'ddns@example.com'
ADMINS = (('Ops', 'ops@example.com'),)
# —— 本地化与静态文件 ——
TIME_ZONE = 'Asia/Shanghai'
STATIC_ROOT = '/srv/nsupdate.info/htdocs/static'
⚠️ 注意:模板中两处
<...>占位是仅限系统级真实密钥的占位写法(SECRET_KEY 与数据库密码),落地时用第 5.1/5.3 节给出的方式生成与注入;示例域名example.com、示例邮箱按你的实际值替换。演示环境(如本文实测环境)不需要占位,直接落实际值。
七、常见问题
Q1:访问站点直接 400 Bad Request?
Host 头不在 ALLOWED_HOSTS 里。用 curl -I http://<域名>/ 看 HTTP 头确认 Host,把对应名字加进 ALLOWED_HOSTS 并 systemctl restart nsupdate。官方文档把这条列为生产部署第一坑。
Q2:登录后一访问就掉线/登录不上?
WE_HAVE_TLS=True(Cookie 加了 Secure 标记)但站点是纯 HTTP,浏览器丢弃 Cookie。纯 HTTP 环境把 CSRF_COOKIE_SECURE/SESSION_COOKIE_SECURE 改 False;或尽快上 HTTPS。
Q3:页面没有样式、/static/ 404?
DEBUG=False 后 Django 不再伺服静态文件。确认 STATIC_ROOT 已配置、django-admin collectstatic 已执行、nginx 的 location /static/ alias 指向该目录。
Q4:更新请求返回 5xx,日志出现 PeerBadSignature/shared secret mismatch?
BIND 侧 key 块的 secret/algorithm 与 Web 界面 Domain 记录的 nameserver_update_secret/nameserver_update_algorithm 不一致。两侧对齐后 rndc reload。
Q5:更新返回 REFUSED?
update-policy 的 deny/grand 规则拒绝了目标主机:基础设施主机(zone 本身、www、ipv4、ipv6)被官方模板显式 deny,属预期行为;普通主机被拒则检查 grant ... subdomain 行的 zone 名拼写与密钥名引用。
Q6:SQLite 报 database is locked?
会话表高频写入导致。按官方文档迁移 PostgreSQL(5.3 节),或对高流量入口(如 /myip)按官方文档在 nginx 直接应答、不进 Python:
location /myip {
add_header Content-Type text/plain;
return 200 $remote_addr;
}
八、参考链接
- 官方管理文档(settings 覆盖机制、PostgreSQL、cron):
https://nsupdateinfo.readthedocs.io/en/stable/admin.html - 官方 BIND 示例配置:仓库
docs/examples/bind9/named.conf - Django settings 参考:
https://docs.djangoproject.com/en/5.2/ref/settings/ - Django 部署检查清单:
https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ - RFC 2136:
https://datatracker.ietf.org/doc/html/rfc2136 - 本专栏相关篇目:《nsupdate.info 介绍与选型指南》、《nsupdate.info Docker Compose 部署实战》、《nsupdate.info 0.13.0 常用命令与运维实战》