Files
tamabox/conf/app.sample.ini
T
tamakyi d300167a30 fix(conf): 配置键名容错 + 未知键告警 + 蜜罐保留期缺省值修正
现场故障:配置写 `honeypot_retention_days <= 30`,日志却报
「蜜罐自动清理未启用:honeypot_retention_days <= 0」。

根因(实测 ini v1.67.0 行为确认):
1. 键名多一个 t(retention vs retain)→ ini 对未知键**静默忽略**,保留期成 0;
2. `<= 30` 这种写法让 ini 把键名解析成 "honeypot_retention_days <"
   (带尾随比较符),更加匹配不上;
3. 警告文案里写的也是错键名,等于在误导运维往错的键上配。

修复(三层):
- 键名归一化(internal/conf/keynorm.go):修剪首尾空白、剥掉尾随 `<` `>`
  `=` 等比较符残留,并按别名表纠正常见误拼。归一化结果必须命中该段
  合法键名白名单才会被采用 —— 不认识的键不猜,交给未知键告警。
- 未知键告警(internal/conf/unknown.go):启动时逐段比对键名,无法识别的
  键 WARN 出来并给出最接近的合法键名建议。合法键名由反射从结构体 ini tag
  得出(单一事实来源,新增配置项自动纳入,不会脱节)。
- 缺省值语义修正(internal/conf/honeypot_defaults.go):
  * 保留期缺省 30 天(原先零值 0 = 永不清理);显式 <= 0 也回退 30。
    要关闭自动清理请用 honeypot_auto_clean = false。
  * honeypot_auto_clean 缺省 true。原先靠 bool 零值,ini 的 bool 解析是
    「有键才赋值」,未配置与显式关闭无法区分 → 老配置升级上来自动清理
    实际是关着的。现改用 HasKey 判定。
  * honeypot_max_rows 缺省 200000;显式 0 = 不限(会告警提醒只增不减)。
  * honeypot_threshold 缺省 1。

实测 ini 行为(探针程序,已删):
- 键缺失或值非法时 MapTo **不覆盖**预置默认值 → 可安全预置缺省值;
- 键名大小写敏感(Honeypot_Auto_Clean 不生效);
- HasKey 能区分「缺失」与「显式 0」;
- 段不存在时 MapTo 返回 nil 且不修改目标。

顺带清理 app.sample.ini 里的两条死配置(app.title / server.xsrf_expire):
代码中从未读取(站点标题走 i18n),留着会让新检测对官方模板误报。

验证:
- 用现场原始配置端到端复现 → 归一化后保留期 30 天、自动清理开启、上限 20 万,
  日志明确说明「配置键名已自动纠正」;
- 误拼键(honeypot_autoclean / honeypot_max_row)自动纠正;未知键(totally_bogus)
  WARN 告警;
- 显式 honeypot_auto_clean = false 被尊重为关闭;完全未配置时缺省开启;
- 新增 TestSampleIniHasNoUnknownKeys 回归检测:官方样例配置零误报;
- go build ./... + go test -timeout 600s ./... 全绿,go vet 无输出。
2026-10-21 13:41:19 +08:00

231 lines
17 KiB
INI
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; ── 关于配置键名 ──────────────────────────────────────────────────────────
; ⚠️ 键名写错时,程序启动会 WARN 提示「存在无法识别的键」并给出最接近的合法键名。
; 对于少量已知的常见误拼(如 honeypot_retention_days → honeypot_retain_days),
; 程序会自动纠正并在日志里说明纠正了什么。
; 但请优先写对键名 —— 自动纠正只覆盖已知的误拼,不认识的键一律不猜。
[app]
production = true
external_url = "https://box.shiroko.one" ; 【必填】站点对外访问地址。用于生成 og:url 与邮件中的全部链接
; (找回密码、私信通知等),必须以 http(s):// 开头、结尾不带斜杠
; (程序会自动 TrimRight "/")。域名迁移时只改这一处即可全局生效。
icp = ""
default_lang = "" ; 站点默认语言:zh-CN / zh-TW / en / ja,留空默认简体中文(访客未设置语言时的兜底)
uptrace_dsn = ""
qiniu_access_key = ""
qiniu_access_secret = ""
aliyun_access_key = ""
aliyun_access_key_secret = ""
share_background = "https://your-default-background.jpg" ; 默认分享图背景图链接(1280x720)
font_path = "static/fonts/NotoSansSC-Regular.otf" ; 中文字体文件路径(相对项目根目录)
[security]
enable_text_censor = true
text_censor_source = auto ; 文本审核服务通道:qiniu(七牛)/ aliyun(阿里云)/ auto(七牛优先、阿里云兜底)。
; 留空等同于 auto,即引入该配置前的历史行为。
; 显式选 qiniu / aliyun 时必须在 [app] 配好对应凭据,否则启动即报错
; —— 避免运行期审核必然失败(审核失败是 fail-closed,拒绝提交,
; 会让发布/私信整站不可用,且现象隐蔽、只在日志可见)。
; 凭据位置:[app] 的 qiniu_access_key / qiniu_access_secret、
; aliyun_access_key / aliyun_access_key_secret。
; ⚠️ 配好凭据 ≠ 可用:阿里云通道走的是「内容安全 2.0 / 文本审核增强版」
; (接口 TextModeration,接入点 green-cip.cn-shanghai.aliyuncs.com),
; 开通前提只要求【实名认证,个人账号即可】,不需要企业认证。
; 开通入口:内容安全 2.0 控制台 → 开通「内容审核增强版」按量付费
; (开通本身不收费,按实际调用量出账;文本审核标准 7.5 元/万次)。
; 用 RAM 子账号调用需授予 AliyunYundunGreenWebFullAccess;欠费停服同理。
; 审核场景固定为 comment_detection(公聊评论检测,覆盖涉政/涉暴/涉黄等底线维度)。
; 单次 content 上限 600 字符,超长文本由代码分段审核(段间带重叠,避免跨段漏判)。
; ⚠️ 不要再去开通「内容安全 1.0」:它要求企业实名认证,个人账号无法开通,
; 调用会永久返回 code=596 "You have not opened Yundun Content Moderation Service"。
; 审核失败时日志会打印阿里云返回的 code / msg / requestId 便于定位;
; 首次出现账号级失败(未开通 / 未授权 / 密钥错误)还会额外打印一条一次性告警。
ip_blacklist = ; 逗号分隔的IP黑名单,支持精确IP(如1.2.3.4)和CIDR(如1.2.3.0/24),留空不启用
ip_blacklist_redirect_url = ; 黑名单IP访问时强制跳转到该URL
trust_proxy = false ; 是否信任反向代理设置的 CF-Connecting-IP / X-Real-IP / X-Forwarded-For 头。
; 仅当站点部署在可信反向代理(如 nginx/Cloudflare)之后且代理已覆盖这些头时设为 true;
; 否则保持 false,防止客户端伪造 header 绕过速率限制与 IP 黑名单。
; ⚠️ 若站点在 Cloudflare 后且需使用下面的自动封禁,必须设为 true,
; 否则解析到的将是 Cloudflare 边缘 IP,自动封禁会误伤所有访客。
; ⚠️ 开启 trust_proxy 时,必须配合「Cloudflare Tunnel / Authenticated Origin Pull /
; 防火墙仅放行 Cloudflare / EdgeOne 回源网段」锁定源站,否则攻击者可直连源站并伪造
; CF-Connecting-IP / EO-Client-IP 头。无需维护 CDN 庞大 CIDR 清单。
;
; —— 双 CDN(境内腾讯 EdgeOne + 境外 Cloudflare)场景 ——
; EdgeOne 回源会用 EO-Client-IP 携带客户端真实 IP(等效 CF-Connecting-IP),
; 本程序在 trust_proxy=true 时已同时信任这两个权威头。
; 两家 CDN 并存时必须:① 防火墙仅放行两家回源网段;② 在各家 CDN 的规则引擎中
; 删除回源请求里「对方 CDN」的权威头(经 EdgeOne 回源删 CF-Connecting-IP、
; 经 Cloudflare 回源删 EO-Client-IP),否则攻击者可借道另一家伪造客户端身份,
; 绕过限流 / 自动封禁 / 单 IP 投票限制。详细步骤见 outputs/edgeone-cloudflare-dual-cdn-guide.md。
trusted_proxies =
; 逗号分隔的「你自己的」反向代理/负载均衡 IP 或 CIDR(如 127.0.0.1、
; 10.0.0.0/8)。仅当 TCP 对端落在该清单内时才解析 X-Forwarded-For / X-Real-IP。
; 留空(默认)则这两个头永不被信任,仅采用 CDN 权威头 CF-Connecting-IP /
; EO-Client-IP(需源站已锁定)。
; 注意:此清单用于你自己的基础设施,不是 CDN 的全量 CIDR 清单。
enable_auto_ban = true ; 自动封禁(fail2ban 风格):检测到批量扫描/探测后临时封禁来源 IP
ban_track_notfound = false ; 是否把「未注册路径的 404」计入封禁统计(默认 false)。正常业务
; (域名/题目不存在等均走 302 业务跳转,不产生 404 计数)一律放行,
; 仅命中后台扫描特征路径(/admin、/.env 等)才参与统计封禁,避免
; 共享出口(CDN 边缘 / CGNAT)下正常用户被 404 探测连坐误伤。
; 如需恢复对纯 404 字典扫描的封禁再置 true。
ban_threshold_sensitive = 10 ; 统计窗口内命中敏感路径(/admin、/.env、/wp-admin 等)的次数阈值
ban_threshold_404 = 20 ; 统计窗口内返回 404 的次数阈值(仅 ban_track_notfound = true 时生效)
ban_window_seconds = 60 ; 统计窗口时长(秒)
ban_duration_minutes = 15 ; 临时封禁时长(分钟),到期自动解除,重启即清零
security_alert_email = ; 触发封禁时发送告警邮件的收件人(复用下方 SMTP 发信配置),留空不发送
; ── 蜜罐(虚假后台登录界面)────────────────────────────────────────────
enable_honeypot = false ; 开启后,扫描器探测后台路径(/wp-admin、/.ssh、/.env 等)
; 时不再返回 302/404,而是返回一个仿 WordPress 的假登录页,
; 并把其提交的用户名/密码记录入库供取证。
; 只在「没有任何路由匹配」的路径上生效,真实业务路由零交集;
; 假页面只含 canary 标记与假凭据,绝不含任何真实信息。
honeypot_paths = ; 追加自定义诱饵路径(逗号分隔),与内置清单取并集。
honeypot_retain_days = 30 ; 蜜罐命中记录的保留天数(取证用途),到期自动清理。
; ⚠️ 键名是 honeypot_retain_days(没有 tion)。写错会被静默忽略,
; 程序会尝试自动纠正并在日志里提示,但别指望它每次都猜对。
; 缺省 30 天;写 0 或负数会回退 30(不表示「永不清理」)。
; 要关闭自动清理请用 honeypot_auto_clean = false。
; 可用 TamaBox honeypot size 查看行数与磁盘占用。
honeypot_auto_clean = true ; 是否启用蜜罐记录的后台自动保留期清理(每小时一轮)。
; honeypot_hits 是只追加的审计表,写入完全由外部扫描流量
; 驱动,不清理的话表体积只增不减。关闭后仍可手动 prune。
; 缺省为 true(开启)。
honeypot_max_rows = 200000 ; 蜜罐命中记录的行数上限(第二道闸)。缺省 200000。
; 显式写 0 = 不限(会告警提醒表只增不减)。
; 保留期是时间维度上界,但攻击者可用海量源 IP 在保留期内
; 把表撑爆。实测单行约 281 字节(含索引,常规 UA),20 万行约 53 MB。
; 实测数据:连续删 12000 插 12000,行数恒定 5 万,文件稳定在 15 MB 不再增长
; (VACUUM 让空间可复用,不保证文件缩小)。
honeypot_threshold = 1 ; 窗口内蜜罐命中多少次即封禁。默认 1(一次即封):蜜罐路径
; 与真实业务路由零交集,正常访客不可能命中,一次即足以判定扫描器。
honeypot_ban_minutes = ; 蜜罐封禁时长(分钟)。留空则复用 ban_duration_minutes(15)。
; ⚠️ 蜜罐封禁与 enable_auto_ban 相互独立:即使关掉自动封禁,
; 只要 enable_honeypot = true,蜜罐命中仍会封禁并记录。
default_moderation_strictness = ; 站点默认「内容审核严格度」:lax(宽松)/ standard(标准)/ strict(严格)。
; 仅对未单独设置档位的账号生效(账号在设置页选择后以账号设置为准)。
; 留空回退 strict(最严档):保证升级不会放宽任何拦截。
; 严格档是三个档位中最严的(拦截面不小于升级前),因此部署后默认拦截面可能略有扩大;
; 若希望默认更少误伤,改为 standard 即可,无需改代码。
; 三档的拦截范围见 internal/security/censor/policy.go,
; 适用于提问、投票、私聊三类内容。
[server]
port = 80
; 绑定监听地址(含端口)。Cloudflare Tunnel 部署时强烈建议设为 127.0.0.1:<port>,
; 使源站端口仅对本地 cloudflared 可见,即使防火墙误配也无法被外部直连伪造 CF-Connecting-IP。
; 留空(默认)则监听所有网卡 ":port"。
listen =
salt = ""
# ⚠️ 必填:密码哈希盐,留空会导致启动失败(已在 conf.Init 强制校验)
xsrf_key = ""
[database]
type = "postgres"
user = "nekobox"
password = ""
host = "127.0.0.1"
port = 5432
name = "nekobox"
schema = "public"
# 仅在已确认需要自动清理重复 email/domain 时开启;默认关闭,避免启动时改写账号数据。
dedupe_unique_fields = false
[redis]
addr = "127.0.0.1:6379"
password = ""
[recaptcha]
captcha_type = recaptcha ; recaptcha(默认), turnstile, math(本地算数验证码,国内访问友好)
domain = "https://www.recaptcha.net"
site_key = ""
server_key = ""
turnstile_style = false
[upload]
default_avatar = ""
default_background = ""
# User avatar and background.
aliyun_endpoint = ""
aliyun_access_id = ""
aliyun_access_secret = ""
aliyun_bucket = ""
aliyun_bucket_cdn_host = ""
image_endpoint = ""
image_access_id = ""
image_access_secret =""
image_bucket = ""
image_bucket_cdn_host = ""
# User uploaded images.
image_endpoint = ""
image_access_id = ""
image_access_secret = ""
image_bucket = ""
image_bucket_cdn_host = ""
[mail]
account = ""
password = ""
port = 465
smtp = ""
[limits]
; ── 全站内容 / 数量上限(运行时配置,修改后重启生效)──────────────────────
; 缺失本段或个别键时按内置默认值运行。长度统一按字符(rune)计数。
; 键留空或填 0/负数时自动回退内置默认值,不会导致启动失败,但也不会按你的意图生效,
; 请按需填写正整数。本文件启用 IgnoreInlineComment,行内分号注释会被并入配置值,
; 因此下面注释一律独占整行、勿写行内注释。
; ── 标签与屏蔽词(账号设置页)──────────────────────────────────────────────
; 最多标签个数
max_tags = 5
; 单个标签最大字符数
max_tag_len = 5
; 最多屏蔽词个数
max_block_words = 10
; 单个屏蔽词最大字符数
max_block_word_len = 10
; ── 投票 ────────────────────────────────────────────────────────────────────
; 参与者填写的「自定义补充选项」(其他/自定义)最大字符数
custom_answer_len = 200
; 站长创建投票时单个「选项」文本最大字符数(预设选项)
vote_option_len = 300
; ── 提问 / 回答 / 追问 ─────────────────────────────────────────────────────
; 提问正文最大字符数(访客提交的问题内容)
question_content_len = 1000
; 回答正文最大字符数(箱主对提问的回答,及对追问的回复,共用)
answer_content_len = 2000
; 追问正文最大字符数(提问者被回答后继续追问的内容)
followup_content_len = 2000
; ── 私聊 ────────────────────────────────────────────────────────────────────
; 单条消息(发送/回复)最大字符数
chat_message_len = 2000
; 每个私聊会话的消息条数上限(系统硬顶)。用户只能在 1..该值 之间设置自己的
; 会话条数上限;新注册用户的默认值即该硬顶。
chat_max_messages = 200
; 单用户每日私信条数上限(用户可设范围 0..该值;0 表示不限制每日条数)
daily_message_limit_max = 999
[theme]
; 主题系统:只改配色 / 图案 / 整页背景,不涉及布局。
; 作用范围:提问箱页面(主页 / 单题 / 密码 / 分享卡)、箱主本人的投票页与 /user 后台;
; 首页、探索等站方页面保持站点默认外观。
; default :站点默认主题(箱主未选择时使用)。填 default 或留空 = 现有外观。
; available :启用哪些主题,逗号分隔,同时是箱主设置页的白名单;
; 列了但 static/css/themes/<slug>.css 不存在的会被忽略并告警。
; 新增主题:把 static/css/themes/_scaffold.css 复制成 <slug>.css,
; 改选择器与令牌值,再把 <slug> 加进 available —— 不需要改模板。
; 需要图片素材的,放 static/images/themes/<slug>/(static/fs.go 已嵌 images/themes 目录)。
default = default
available = default,sakura,shiroko
; ⚠️ 没登记在 available 里的主题,即使 css 已随二进制发布也不会出现在设置页;
; 启动日志会打出「[theme] available 未登记以下主题」提醒。,shiroko