nsupdate.info 0.13.0 配置参数详解

nsupdate.info 0.13.0 配置体系详解:settings 三层覆盖机制、SECRET_KEY/ALLOWED_HOSTS/数据库/邮件/BAD_* 防护参数逐项说明、生产配置模板与 BIND 密钥/zone/update-policy 三点对齐,参数均在实测环境验证。

nsupdate.infoDNSDjangoPython2026/10/27

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 部署实战》。

二、前置条件

阅读与动手验证本篇前,确认以下前提成立:

  1. 已有一套可运行的 nsupdate.info(快速验证:systemctl is-active nsupdate 返回 active,或部署篇的栈已启动);
  2. 能定位你的 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 保证;
  3. 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+ 位随机串,每实例唯一
DEBUGprod.py 覆盖为 False调试模式必须 False,否则泄露堆栈
ALLOWED_HOSTSprod.py 默认官方三域名允许的 Host 头必配自有域名,错配直接 400
BASEDOMAINprod.py nsupdate.info提供动态主机的基准域换成你的域名
WWW_HOSTprod.py www.<BASEDOMAIN>服务主站域名跟随 BASEDOMAIN
WWW_IPV4_HOSTprod.py ipv4.<BASEDOMAIN>强制 IPv4 的检测域名跟随 BASEDOMAIN
WWW_IPV6_HOSTprod.py ipv6.<BASEDOMAIN>强制 IPv6 的检测域名跟随 BASEDOMAIN
WE_HAVE_TLSprod.py True是否有 HTTPS 站点(联动 Cookie 安全项)有证书 True,纯内网 False
DATABASESbase.py SQLite应用数据库生产换 PostgreSQL
NAMESERVERSbase.py ['8.8.8.8','1.1.1.1']应用做 MX/解析校验用的解析器换成自有/可信解析器
SERVICE_CONTACTbase.py 占位邮箱About 页展示的联络邮箱换成真实邮箱
DEFAULT_FROM_EMAILbase.py 占位邮箱系统发件地址换成真实发件域
ADMINSbase.py 空元组收异常 traceback 邮件的管理员生产建议配置
ACCOUNT_ACTIVATION_DAYSbase.py 7注册激活链接有效天数默认即可
EMAIL_BLACKLISTbase.py 三个示例正则注册邮箱黑名单(逐行正则)按运营需要加
BAD_AGENTSbase.py 空集合/nic/update 拒绝的 User-Agent可拉黑恶意客户端
BAD_IPS_HOSTbase.py 空集合/nic/update 拒绝的来源 IP(netaddr IPSet)拉黑已知滥用网段
BAD_HOSTSbase.py 空集合提前静默拒绝的主机名(省数据库查询)被攻击时临时加
TIME_ZONEbase.py Europe/Berlin时区按需,如 Asia/Shanghai
LANGUAGE_CODEbase.py en-us默认语言默认即可
SESSION_COOKIE_AGEbase.py 10 小时会话时长默认即可
SESSION_EXPIRE_AT_BROWSER_CLOSEbase.py True关浏览器即过期(remember_me 为 False 时)默认即可
STATIC_ROOTbase.py 注释掉collectstatic 收集目标目录必配(DEBUG=False 时)
REGISTRATION_OPENDjango 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 逐项对照:nginx server_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 的收集目标,nginx location /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;
}

八、参考链接