文章是本站的原创长文。每篇都保持无脚本、来源可溯,并与本站记录的系统现实一致:一个小型、静态、公网服务如何被构建、加固并保持诚实。

可复用的持有者访问令牌把约定访问能力交给持有它的人;发送者约束令牌还要求提供与发送者绑定的证明。把令牌放进地址,会让这项能力进入平时就会复制地址的系统,例如访问日志、监控记录、浏览器导航历史、书签和分享链接。具体暴露范围取决于请求及策略,但查询参数的名称看起来像秘密,并不会自动让它受到保护。传输位置本身就是凭据披露边界的一部分,应当结合它会经过哪些记录和分享环节判断。
对于 OAuth 持有者令牌请求,应采用 Authorization 请求头;RFC 9700 现已禁止客户端按照 RFC 6750 的查询方式,在 URI 查询参数中传递访问令牌。HTTPS 仍然不可缺少,因为把值从查询移动到请求头并不会加密它;TLS 保护的是到终止位置为止的传输数据。传输结束后,应用和中间组件仍然能够检查字段。请求头的直接收益,是让令牌离开请求地址,而不是保证它永远不会被日志或诊断工具捕获。不能把两种不同的保护作用混为一谈。
分清减少暴露与绝对保密 某些访问日志只记录方法、请求目标和状态。在这种格式里,请求头令牌不会出现在目标字段中,而查询令牌会出现。但其他系统可能采集请求头,因此授权字段仍需在日志、链路和错误报告配置中被明确处理。应验证真实管线究竟保存了什么,不能假定请求头可以绕过全部日志层。检查时使用无效的合成值即可,不应为了演示而把真正凭据再次写入更多记录。
引用来源策略能够减少源地址向后续目标披露,却不会删除已经发给目的地的查询内容,也不会抹去浏览器记录或改写已复制的链接。同样,禁止缓存响应并不是要求清除全部访问日志和导航历史的通用指令。把令牌移到片段,会改变 HTTP 请求发送哪些内容,但值仍可被页面脚本和浏览器地址状态访问,也不是适合所有场景的凭据存储方案。
坏掉的样子 团队可能把接口认证从查询参数改成请求头,却让旧的带令牌地址继续留在报告和共享书签中。传输方式改变并不会撤销此前暴露,需要调查受影响的副本,并在系统支持时使已经暴露的可复用凭据失效。另一种误判是把有效期很短的链接称为泄露也无所谓;在有效期间,任何取得这项能力的人仍然可能使用它。缩短时间窗口只能减少风险,不能取消披露的后果。
当实际客户端流程需要时,签名地址可以是有意设计的、范围很窄的资源访问方式,但允许访问的资源、操作、期限和处理规则都应明确审阅。到期失效与只能使用一次并不相同,要阻止到期之前重复使用,需要真正强制执行消费状态的机制。简单的回调接口或浏览器导航,也不会自动构成把无限制可复用接口凭据放进查询的理由,应根据真实客户端选择合适的认证流程。
请求头位置减少 URL 暴露,但请求头仍须保护。签名资源 URL 是独立限定的设计,不是 OAuth 例外。运行与技术检查仍受阻。 快速验证 保存代码为 token-placement.py,执行 python3 token-placement.py --serve,打开打印出的页面。原文报告使用 Python 3.14.4 和 Chrome 152,本轮源码复核没有复现。显式 Python 检查在优化模式下仍会执行;全部演示值都是明确无效的固定标记,不能替换为真实凭据。回环服务只是观察实验,不是认证服务;它记录请求目标以及示例授权头是否存在,不记录该请求头的值。Python 已禁用代理发现并拒绝重定向,浏览器代理策略仍须单独控制。服务模式默认保留 120 秒,--serve-seconds 接受 1–300;控制键加 C 也会请求清理。只有单独的清理结果确认服务线程停止且监听关闭;有时限的 join 未能确认结束就会报错。临时事件只保存在进程内存,前面的检查通过不证明清理已经完成。
...

名为 request_duration 的指标会让读者猜测,二百五十究竟表示秒、毫秒还是别的量。面向 Prometheus 的名称,例如 http_request_duration_seconds,能够把单位约定带入查询、记录规则与告警配置。但这个约定必须由正确实现支撑:给毫秒数据的名字加上秒后缀,不会自动改变数值大小,反而可能让监控产生一千倍的误差。名称提供线索,转换逻辑才决定数据是否可信。
图解依据原文的三条十进制定点输入与阈值。历史 promtool 解析及命名检查结果保持不变。名称正确不证明真实埋点正确,应沿采集、导出、查询与显示核对已知输入。 在明确的边界完成换算 每个指标族应只表示一种量和一种单位。Prometheus 建议使用秒、字节等基础单位,并在适用时把单位写为后缀。持续时间与绝对时间戳都可以用秒表示,却不是同一种量,因此不能因为名字都含秒,就把上次成功的时间戳与一次请求的耗时直接相加。帮助文本应写明测量对象、开始结束边界以及含义,让简短名称背后仍有明确的契约。
如果底层库输出毫秒,应在观测或导出边界转换一次,并保留足够精度,避免把很短的耗时截断为零。仪表盘可以按基础单位配置显示,让零点二五秒自动显示为二百五十毫秒,这只改变呈现方式,不改变存储样本。不能仅因为坐标轴显示毫秒,就在记录规则中再做一次换算,否则查询和图表会在不同环节重复缩放。
单位与类型回答不同问题 累计传输字节数通常使用字节加总量后缀,当前内存占用仪表则只使用字节后缀。两者单位相同,但只有前者表示不断累积的工作量,适合计算计数器速率。请求次数按惯例使用总量后缀,无须编造一个物理单位。直方图的桶、总和与计数,还会在耗时指标族名称后加入各自的组件后缀,因此阅读名称时也要识别指标族与导出组件之间的关系。
比例通常表示零到一之间的分数,而百分数使用另一种数值尺度,面板应明确自己期待哪种输入。计数器可以在进程重启时归零,Prometheus 的计数器速率函数会根据观测序列中的下降处理重置,并不要求额外存在创建时间序列。应先分别计算独立计数器的速率,再执行聚合,让各自的重置仍能被识别;不要先把多个实例的累计值混成一条看似连续的曲线。
坏掉的样子 名字正确的指标仍然可能承载错误数值。如果同一个标识下混入秒和毫秒,历史数据已经被污染,修改帮助文字或补一个说明标签不能修复旧样本。单位迁移应被视为契约变化,生产端、查询、告警和仪表盘需要一起核对,历史区间的比较也必须说明边界。旧序列含义不兼容时,可以采用新的指标标识,避免新旧数值被误当成同一种量连续使用。
名称后缀也不会强制执行计数器语义。一个普通运行过程中可以升降的仪表,即使名字带总量后缀,也不适合按累计计数器解释。反过来,已经得到每秒速率后再除以时间窗口,会制造另一种量。这些是维度和测量语义的问题,不能只靠显示格式解决。其他遥测体系可能把单位放在元数据中,应遵循已定义的导出映射,而不是机械重命名所有上游仪器。
快速验证 把代码保存为 units.py,使用 Python 3 执行。程序转换三条人为构造的延迟观测,并输出带帮助文字和类型声明的 Prometheus 文本格式。Decimal 十进制运算使毫秒到秒的换算清楚可见;阈值比较则展示,仅仅改名、却不转换数据的生产端,为什么不能和正确的生产端共用同一条延迟规则。
from decimal import Decimal latency_ms = [Decimal('120'), Decimal('250'), Decimal('630')] latency_seconds = [value / Decimal('1000') for value in latency_ms] threshold_seconds = Decimal('0.300') correct_good = sum(value <= threshold_seconds for value in latency_seconds) rename_only_good = sum(value <= threshold_seconds for value in latency_ms) transferred_bytes = 1024 * 1024 used_ratio = Decimal('25') / Decimal('100') assert correct_good == 2 and rename_only_good == 0 assert sum(latency_seconds) == Decimal('1.000') assert used_ratio == Decimal('0.25') print(f"# within 0.300s: converted={correct_good}/3, rename_only={rename_only_good}/3") print('# HELP demo_last_request_duration_seconds Duration of the most recent completed request.') print('# TYPE demo_last_request_duration_seconds gauge') print(f'demo_last_request_duration_seconds {latency_seconds[-1]:.3f}') print('# HELP demo_transferred_bytes_total Bytes transferred since this process started.') print('# TYPE demo_transferred_bytes_total counter') print(f'demo_transferred_bytes_total {transferred_bytes}') print('# HELP demo_capacity_used_ratio Fraction of configured capacity currently used.') print('# TYPE demo_capacity_used_ratio gauge') print(f'demo_capacity_used_ratio {used_ratio}') 首行注释说明,转换后有三分之二的观测不超过零点三秒,而把毫秒数值直接当成秒时,三条都无法满足阈值。导出的最后请求耗时为零点六三秒,累计字节数为一百零四万八千五百七十六,容量使用比例为零点二五,在百分比界面上对应百分之二十五。三个数分别对应持续时间、累计大小与无量纲比例,不能因为都能画成曲线就混用。
...

限流键定义了哪些请求共用一份预算。账号、API 凭据、地址、租户和已认证会话是不同身份,不是“客户端”的几种可互换写法。选错归组方式,可能让无关用户相互挤占额度,也可能让同一份工作只改一个字段,就拿到新的使用机会。
图解使用原文已经完成认证及路由解析的测试请求,计数范围为一个测试窗口。稳定账号归组不证明按人公平,也不验证真实认证、可信代理、并发计数或分布式执行。历史有限范围执行仍为通过,本次没有重跑示例。 使用政策真正承诺的身份 如果规则是按账号限流,就应从认证结果中取得账号,而不是相信调用方提交的账号请求头或查询参数。仅仅看见一个凭据,并不能证明它有效;认证层必须先验证凭据、解析主体,再让账号级准入决定使用这个结果。限流键的可信程度不能高于它的来源。
按凭据限流也可以是合理的产品契约,但行为与按账号限流不同。轮换凭据或再创建一份凭据,可能得到新的凭据键,却仍属于同一个账号。应先决定这是否应当带来新额度,而不是等出现滥用或投诉后,才发现实现按的身份与产品描述并不一致。
一个账号不一定只对应一个人或一个应用,一个人也可以拥有多个账号。稳定账号键改善的是归组的一致性,并不证明公平,不阻止批量养号,也不限制整个服务的总需求。其他层级的限制应由明确需求支持,不能认为“已经按账号计数”就涵盖所有容量风险。
地址表示的是网络路径 企业 NAT、运营商网络或 VPN 后面的多个账号,可能共用一个公网地址;按 IP 的桶会把它们的流量合在一起。反过来,更换地址的调用方可能进入一个新桶。这两种结果并不说明地址解析错误,而是说明网络地址与业务主体本来就在表示不同对象。
没有已验证账号的公开接口或认证前入口,仍可以用地址级限制保护资源。但应按地址政策评估阈值,包括正常共享网络的流量,不能把它描述成精确的个人限制,也不能假设一个地址规则一定拦得住分散在许多网络上的抓取者。
地址在成为键之前需要语法规范化。同一个 IPv6 地址的不同合法写法,不应创建多份额度。是否按网段聚合,或把特定地址族形式视为同一个对象,是另外的政策选择,需要权衡误伤范围;规范化文本本身并不能替你回答这些业务问题。
转发身份必须有信任边界 随意提交的 Forwarded、X-Forwarded-For 或类似字段,不是客户端来源的证明。RFC 7239 明确指出,客户端和沿途节点都能改变转发信息。是否可信取决于直接连接的对端、配置的代理链,以及这条链如何移除或安全处理用户原先提交的字段。
NGINX 的真实 IP 模块需要明确配置可信来源地址和递归处理规则。“永远取转发字段里的第一个地址”不是适用于所有网络的正确方法。如果代理接收后原样转发了不可信声明,看似稳定的身份键就可能变成由调用者控制的新预算生成器。
...

EDNS 通过 OPT 伪记录扩展 DNS,其中一项作用是让请求发送方宣告自己准备接收的 UDP 载荷大小,从而允许回答超过传统五百一十二字节 DNS 限制。这是 DNS 交换中的容量宣告,不是网络路径 MTU 发现协议,也不承诺所有中间设备都会交付相同大小的数据报。理解这一区别,才能准确解释大回答为何仍可能失败。
图中数值来自文章明确的回环配置与记录的 DNS 消息长度,不是统一默认值或路径 MTU 测量。OPT 宣告容量,客户端仍须理解 TC 并保留 TCP 访问。 坏掉的样子 客户端宣告四千零九十六字节,不会强迫服务器发送同样大小的响应。服务器可以实施更小的发送上限;需要的回答放不下时,可以返回 TC,客户端仍需可工作的 TCP 路径。增加客户端数字,不能修复服务器有意采用较小预算的策略。应查看两端实际配置与返回内容,而不是把请求里出现的大数值当成双方已承诺一定使用的大小。
...

传输层均衡与 HTTP 均衡的实用区别,在于什么位置可以作出路由决定。TCP 转发可以为一条连接选择后端,然后不解释 HTTP 请求,直接转发字节流。HTTP 代理则解析请求,并能根据路径或请求头选择上游。因此,同一个客户端连接里的多个请求,可能获得不同路由结果。选择策略前,应先明确需要按连接还是按请求决定目标。
坏掉的样子 如果期待 TCP 连接选择还能分别路由内部每个 URL,请求就可能到达错误应用。字节流已经转发到一个后端后,后续 HTTP 路径不会自动触发重新选节点。反过来,如果把 HTTP 路由当成永久连接亲和,也可能让应用误以为同一客户端连接的每个请求都会到达同一份本地状态。两种假设都需要通过实际连接复用案例验证。
层级名称也不能完整说明 TLS 行为。TLS 终止与 HTTP 解释是不同能力。stream 代理可以终止 TLS,却不成为 HTTP 路径路由器;也可以不解密应用数据,只查看部分 ClientHello 元数据。按 SNI 选择目标,并不等于能看到加密 URL、请求头或响应正文。应描述实际配置行为,而不是认为产品标签已经证明所有这些性质。
性能结论同样需要谨慎。用户态 TCP 转发与内核包转发具有不同资源路径,即使两者都被称为传输层均衡。HTTP 代理会执行解析和策略工作,但相对吞吐取决于实现与负载。仅选择 L4 或 L7 这两个术语,不能推出通用的线速能力或固定额外开销;需要以真实路径和代表性流量测量,而不是用层级名称代替基准。
按连接或按请求选择后端。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 本轮复核没有重新运行这些 Docker 监听器;文中记录的路由结果仍属于此前观察。
...

进程使用文件描述符访问的不只是普通文件。网络套接字、管道、事件接口和复制出来的引用,都要占用描述符表中的入口。RLIMIT_NOFILE 约束新分配可以使用的编号,其软值比允许的最大编号大一。因此,即使机器仍有充足磁盘空间和内存,服务也可能因为自己的表项额度用尽而无法继续接受工作。排查时必须先确认耗尽的是哪一种资源。
检查真正失败的进程。EMFILE 属于进程边界,ENFILE 属于系统边界;预先打开的控制通道可在新分配失败时继续工作。 坏掉的样子 EMFILE 表示调用进程触及适用的描述符限制,ENFILE 则表示系统打开文件资源耗尽。这两种错误需要沿不同方向调查。提高机器级文件设置,不会自动改变失败进程的软值;扩大某个应用的限制,也不能修复系统级资源短缺。把日志里的错误名和实际限制对应起来,比看到“文件太多”就随意调整参数更有用。
耗尽还会破坏诊断。打开日志文件、枚举目录本身,也可能需要新的描述符。如果应用直到额度用光后才尝试读取 /proc/self/fd,检查动作就可能一起失败。已经存在的描述符通常仍可使用,因此预先建立的控制管道有价值:它让进程无需再分配资源,也能够报告当前错误,而不会把诊断失败误认为路径消失。
描述符数量持续增长,需要结合上下文解释。它可能反映正常并发增加、连接池逐渐填满,也可能是某些所有者忘记关闭引用。曲线本身不能证明泄漏。应同时比较描述符类型、工作负载,以及工作排空后的数量,再沿超出预期寿命的引用追踪所有权。只有把数量变化与资源生命周期关联起来,才能选择正确修复。
快速验证 把下面脚本保存为 exhaustion.py,在一次性 Linux 环境中运行 python3 exhaustion.py。此前记录的实测使用 Linux 6.12.76 和 Python 3.11.2。这里的描述符版本改用管道代替套接字,并为等待就绪设置截止时间。控制进程启动独立子进程,后者只把自身软上限降到三十二;继承的软硬值原本都必须允许这个设置:
...

当一次分配在相关限制范围内无法满足,适当的回收尝试也不能解决问题时,Linux 可能调用内存不足终止机制。它是最后阶段的恢复手段,不是理解业务重要性的调度器。有些分配会直接失败而不杀进程,配置也可能使系统进入恐慌处理。因此,排查应先识别分配范围与实际事件,不能一开始就认定整台机器的每一字节内存都已经用完。
先看范围,再看评分 全局内存短缺与内存控制组短缺是两回事。某个控制组可以在主机仍有可用内存时达到自身上限;它的 OOM 处理不能任意挑选组外进程来解决组内问题。允许使用的内存节点等其他限制也可能影响分配。因此,主机剩余内存图表不足以证明或否定某次失败,更不能在没有检查事件范围时,把所有容器异常都归类为全局容量不足。
在适用的选择规则内,内存占用与调整值会影响牺牲者选择。内核并不知道哪个进程商业价值高、哪个空闲服务可以丢弃,也不知道哪一个业务必须持续可用。一个体积很大的数据库可能被选中,即使压力也来自许多小工作者。procfs 中的 oom_score 是可观察数值,不是承诺把所有可见进程排序以后,就能准确预测任何内存域下一次会杀谁。
理解调整值,不许诺必然存活 oom_score_adj 的范围为负一千到正一千。正值增加被选中的倾向,负值降低它;负一千会把该任务排除在这项内核 OOM 选择之外。一般负值并不保证存活,这个设置也不能阻止显式信号、服务管理器或独立用户态内存压力策略终止任务。调整分值不会增加容量,也不会修复泄漏。把它解释成“重要进程已经免疫所有内存故障”,会造成危险的运维误判。
保护所有任务不是可行的容量方案。如果没有合适的牺牲者,不能假定内核会越过控制组边界,去杀一个无关的监管进程。整组终止与恐慌设置需要分别分析。调整值可以被子进程继承,而持久服务设置应归入监管器配置;修改之前需要明确授权并验证适用范围。下面实验不会改动这些值,也不制造内存压力,避免把接口学习变成真实故障。
坏掉的样子 反复重启可能掩盖持续泄漏。如果同一个工作负载不断增长直至失败,增加内存也许只是延长两次失败之间的间隔。应关联分配速度、常驻和记账内存、负载大小、回收及交换活动。单个进程的常驻大小不能完整表示控制组全部费用或共享内存关系。降低上限可以约束增长范围,却也可能让应用更早失败;不能只观察邻居未被终止,就认为业务恢复问题已经解决。
退出状态一百三十七与 SIGKILL 相符,但并不是 OOM 专属证据。应查看运行时的终止原因、控制组内存事件计数和操作员可见的内核记录。分配失败、进程被杀、内核 OOM 和用户态压力管理器终止,是需要分开的情况。看不到内核日志,也可能只是当前观察范围受限,不能直接证明没有事件发生。诊断必须说明证据来自哪里、有哪些缺口。
评分会随内存和范围变化。读取 procfs 的过程中进程可能退出,容器内的进程视图也可能隐藏主机的大部分任务。一次成功读取并不是原子的整机快照。分析真实失败时,应保留事件时间与受影响的控制组,然后比较相同范围内的观察,而不是拿事后另一个进程的分数去解释之前的牺牲者选择。
OOM 范围及事件上下文先于评分排名;此处 PID 排序是虚构展示夹具。 快速验证 保存为 oom_observe.py,使用 Python 3.9 或更新版本执行 python3 oom_observe.py。程序只读取自身进程的两个评分文件,不创建压力负载,不修改任何限制或策略。两条确定性断言仅对明确虚构的记录测试展示顺序,实时读取和模型检查会分别报告结果,避免把一种成功误当成另一种验证。
...

Cookie 的主机与路径作用域,决定哪些目的地有资格收到它的名称和值。缩小这个集合有助于减少暴露,但作用域不是授权,而且 Cookie 并不采用浏览器完整的源边界。本文重点讨论 Domain 与 Path;传输方式、请求语境和浏览器策略还会增加其他限制,不能只凭一个属性就推断最终发送结果。
这里只展示发送资格,不代表授权或最终一定发送。公共后缀及其他 Cookie 规则仍适用;Path 和端口不隔离不可信应用。图示不宣称浏览器一致性验证。 Domain 选择主机,不隔离端口 省略 Domain 会建立 host-only Cookie。从 app.example.com 设置的 Cookie 只匹配这个主机,不匹配 shop.example.com。加入 Domain=example.com,则允许父域与匹配的子域收到它,但仍受其他 Cookie 条件约束。属性处理会忽略一个前导点,它并不是另一种通配符功能。只有那些更宽的接收方确实需要这个值,才应扩大作用域。
...