<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>生命周期 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/index.xml" rel="self" type="application/rss+xml"/><item><title>S3 存储类用成本换访问方式</title><link>https://www.xbcatm.com/zh/posts/s3-storage-classes-trade-cost-for-access/</link><pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/s3-storage-classes-trade-cost-for-access/</guid><description>选择存储类之前，比较访问期限、故障韧性、对象尺寸和完整费用。</description><content:encoded>&lt;p&gt;存储类改变的不只是每月保存字节的价格，还包括取回方式、故障韧性和计费结构。首先应确定业务允许多久恢复数据，以及丢失后是否有其他可重建副本。Standard-IA 可以立即读取，但取回另行收费；Glacier Flexible Retrieval 和 Deep Archive 则需要先恢复，再执行普通读取。One Zone-IA 还有不同故障边界：它不能抵御所在可用区被物理摧毁。相同的宣传持久性数字，不能替代对实际故障域的判断。&lt;/p&gt;
&lt;h2 id="先拆清费用再比较总额"&gt;先拆清费用，再比较总额&lt;/h2&gt;
&lt;p&gt;使用桶所在区域的当前价目表，明确存储类、取回档次与请求种类。存储取决于计费容量和持续时间；取回可能同时按字节及请求计费。逐对象迁移请求和归档元数据则随对象数量增长。不能把所有费用都说成只由对象数量决定，也不能从某一条存储单价直接推出整个工作负载的节省比例。同样大小的数据拆成大量小对象，成本模型可能完全不同。&lt;/p&gt;
&lt;p&gt;不要把 Flexible Retrieval 和 Deep Archive 的标准取回统一当成免费。AWS 明确提供 Flexible Retrieval 批量取回及其请求免费，Intelligent-Tiering 则有另外的标准和批量取回免费规则。这些例外不能推导出其他存储类和档次也免费。归档恢复后，原归档对象仍在；临时可读副本还会按选定保留时间收取 Standard 存储费。只计算原归档的低价，会漏掉这个并存阶段。&lt;/p&gt;
&lt;p&gt;即使对象提前消失，最短存储期限仍然影响费用：Standard-IA 和 One Zone-IA 为三十天，Glacier 即时与灵活取回为九十天，Deep Archive 为一百八十天。提前删除、覆盖或迁出可能产生剩余期限费用。Standard-IA、One Zone-IA 和 Glacier Instant Retrieval 有 128 KB 的最小计费对象大小；灵活取回与深度归档则每对象增加四十 KB 元数据，分别按 Standard 与归档费率计费。&lt;/p&gt;
&lt;h2 id="生命周期规则必须对应工作负载"&gt;生命周期规则必须对应工作负载&lt;/h2&gt;
&lt;p&gt;对象年龄不是距离上次访问的时间。三十天后迁移的规则，也会命中一份已经保存一年、却仍然每周读取的报告。需要同时评估对象尺寸、取回字节数、请求频率、保留时间及可接受的恢复延迟。前缀和标签可以分开不同工作负载，但只有分类长期准确时才有效，不能把存储时间长直接解释为访问已经变少。&lt;/p&gt;
&lt;p&gt;二零二四年九月开始的新默认行为，阻止小于 128 KB 对象的生命周期迁移。没有修改的旧配置可能保留此前行为，显式大小过滤器也可以覆盖默认限制。因此，一个小对象仍在 Standard，不能独自证明规则已经坏了。应检查配置实际生效的过滤条件和变更历史，再判断它为何保留原存储类，而不是看到某次查询结果就猜测迁移失败。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;存储小计下降，可能掩盖取回和请求费用上升。反过来，已经恢复的临时副本在有效期内被多次读取，也不一定每次都重新发起归档恢复。应按真正的“恢复后读取”流程建模，把临时副本存储算进去，不能机械地将归档恢复费用乘上每一次 GET。访问频率与恢复请求频率有关，却不是同一个数。&lt;/p&gt;
&lt;p&gt;恢复耗时同样是运维依赖。灵活取回的标准请求通常需要三至五小时；深度归档的标准请求通常在十二小时内完成，批量通常在四十八小时内完成。这是文档描述的典型预期，不是无条件的截止保证。承诺恢复时间目标之前，应实际演练恢复并确认完成；请求已接受、恢复正在进行和内容已经可读，是不同阶段。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;用 Python 3 运行下面独立脚本；本轮以 Python 3.12.14 执行。价格是刻意虚构的教学输入，不是当前 AWS 报价，也不是完整账单。模型比较一个 GiB 保存十二个模型月份，假定每次读取都取回整个对象。把参数说清楚之后，才可以讨论为什么访问量变化会让选择反转。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Invented rates, not an AWS quote; one GiB held for twelve modeled months.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;warm_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.02&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cold_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.01&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;retrieval_per_gib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.01&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;transition_per_object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_reads&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;warm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;warm_storage&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cold_storage&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;full_reads&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;retrieval_per_gib&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;transition_per_object&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.24&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.121&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.24&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.641&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;small_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minimum_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minimum_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;small_bytes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;12.8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="c1"&gt;# Illustrative minimum-duration billing boundary.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;reads&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;full reads:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;annual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reads&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10 KiB under a 128 KiB floor: 12.8x billable size&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Excluded: GET requests, transfer, restored copies, tax, and negotiated rates&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;零次读取时，较冷模型的 0.121 低于 0.24；五十二次完整读取后，0.641 反而高于 0.24。在这组输入下，十一次仍便宜，十二次就不再便宜。尺寸计算展示小对象起计门槛为何需要独立处理。这些结果都不是通用的存储类切换阈值，也没有验证云服务上的实际迁移或计费。&lt;/p&gt;
&lt;p&gt;下面保留原有 AWS CLI 诊断命令。需要已授权桶的读取权限，并替换 B 与 K；本轮没有执行这些命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;aws s3api list-objects-v2 --bucket B --query &amp;#39;Contents[].StorageClass&amp;#39; --output text | tr &amp;#39;\t&amp;#39; &amp;#39;\n&amp;#39; | sort | uniq -c
aws s3api get-bucket-lifecycle-configuration --bucket B --query &amp;#39;Rules[].[ID,Status,Transitions[0].Days,Transitions[0].StorageClass]&amp;#39; --output table
aws s3api head-object --bucket B --key K --query &amp;#39;StorageClass&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;清单成功且非空时，第一条按存储类统计当前对象个数，不统计字节或非当前版本。空清单和 CLI 错误需要另外处理；管道退出成功，本身不证明 AWS 调用成功。第二条只显示每条规则的第一个迁移，遗漏过滤条件、后续迁移与非当前版本迁移，决策前必须检查完整响应。HeadObject 对 Standard 不返回 StorageClass，所以投影为 null 本身不是故障。这些查询都不能测量访问频率，也不证明尚待处理的迁移或恢复已经完成。应结合完整策略、清单、访问证据与账单之后再改变生命周期行为。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/s3/pricing/"&gt;AWS S3 pricing&lt;/a&gt;，计费维度、恢复副本与免费取回例外，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html"&gt;AWS storage class guide&lt;/a&gt;，访问、韧性、最短期限与最小计费尺寸，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-transition-general-considerations.html"&gt;AWS lifecycle transitions&lt;/a&gt;，小对象默认行为及旧配置差异，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects-retrieval-options.html"&gt;AWS archive retrieval options&lt;/a&gt;，典型恢复时长，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/head-object.html"&gt;AWS CLI HeadObject&lt;/a&gt;，Standard 不返回 StorageClass，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/list-objects-v2.html"&gt;AWS CLI ListObjectsV2&lt;/a&gt;，清单范围与对象字段，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/get-bucket-lifecycle-configuration.html"&gt;AWS CLI lifecycle configuration&lt;/a&gt;，过滤器及迁移数组，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>凭据到期必须由验证方执行</title><link>https://www.xbcatm.com/zh/posts/secrets-need-expiry-not-just-rotation/</link><pubDate>Sun, 27 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/secrets-need-expiry-not-just-rotation/</guid><description>PostgreSQL 密码截止时间会拒绝新的密码认证，却不会结束既有连接，也不控制 trust 认证路径。</description><content:encoded>&lt;p&gt;只有负责决定是否允许访问的系统检查了适用截止时间，凭据才真的会到期。在资产清单里写一个日期，不会自动改变数据库或接口接受什么。较短有效期可以压缩被盗凭据继续建立新访问的时间窗口，但具体效果取决于认证机制，以及此前已经建立的会话、签发的其他凭据如何处理。秘密不是因为自身附带一行日期，就能独立让所有访问失效。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;到期不会撤销已经发生的行为。复制出去的数据仍在外面，截止时间之前建立的会话也可能继续工作。攻击者还可能在原本有效的权限下建立另一条访问路径。因此，有效期只是限制后续使用的一种手段，还要结合权限范围、撤销机制、会话管理和对实际影响的调查。它并不能为一次泄漏造成的全部损失设定通用上限，也不能把过去的访问自动变成没有发生过。&lt;/p&gt;
&lt;p&gt;PostgreSQL 的 &lt;code&gt;VALID UNTIL&lt;/code&gt; 控制角色密码的有效时间，不控制角色是否存在，也不覆盖所有认证方式。使用非密码方式建立连接，就不会获得同样的截止检查。如果通过本地信任规则来测试密码到期，便可能误以为到期功能失效。判断结果之前，应先确认客户端实际命中了哪条认证路径，不能只看两次连接用了同一个用户名。&lt;/p&gt;
&lt;p&gt;轮换与续期是两个不同变化。在 PostgreSQL 中，仅修改 PASSWORD 会保留 VALID UNTIL，仅修改 VALID UNTIL 也会保留原密码。更新流程如果只修改其中一项，可能导致应用依然无法重新连接，也可能让原值继续有效。一个连接池里的旧连接查询成功，不足以证明新凭据能够完成新的认证，因为它可能根本没有重新交出密码接受检查。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;准备隔离的 Linux 实验环境，让 PostgreSQL 18 程序位于 &lt;code&gt;PATH&lt;/code&gt; 中，以非 root 用户运行，并确认回环端口 55477 未被使用。将下列内容保存为脚本，用 &lt;code&gt;sh&lt;/code&gt; 执行。它初始化一次性数据库集群，只使用明确虚构的密码，TCP 仅监听回环地址，退出时删除临时目录。本地信任认证只为展示另一条认证路径而启用，不应照搬到真实服务：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;lab_root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /tmp/credential-expiry.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/data/postmaster.pid&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pg_ctl -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; -m immediate -w stop &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rm -rf &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; cleanup EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initdb -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; --locale&lt;span class="o"&gt;=&lt;/span&gt;C --auth-local&lt;span class="o"&gt;=&lt;/span&gt;trust --auth-host&lt;span class="o"&gt;=&lt;/span&gt;scram-sha-256 &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_ctl -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; -l &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/server.log&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="s2"&gt;&amp;#34;-k &lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt; -p 55477 -c listen_addresses=127.0.0.1&amp;#34;&lt;/span&gt; -w start &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; psql -X -qAt -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; -h &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -p &lt;span class="m"&gt;55477&lt;/span&gt; -d postgres &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Both passwords below are invented, disposable lab fixtures.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SET password_encryption=&amp;#39;scram-sha-256&amp;#39;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CREATE ROLE article_expiry LOGIN PASSWORD &amp;#39;LAB-ONLY-OLD-PASSWORD&amp;#39;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkfifo &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/input&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LAB-ONLY-OLD-PASSWORD psql -X -qAt -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -h 127.0.0.1 -p &lt;span class="m"&gt;55477&lt;/span&gt; -U article_expiry -d postgres &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/input&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/held.out&amp;#34;&lt;/span&gt; 2&amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/held.err&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;held_pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; 3&amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/input&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;SELECT &amp;#39;ready&amp;#39;;\n&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;until&lt;/span&gt; grep -qx ready &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/held.out&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;attempt &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$attempt&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -lt &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sleep 0.05
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SELECT format(&amp;#39;ALTER ROLE article_expiry VALID UNTIL %L&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; clock_timestamp()+interval &amp;#39;1 second&amp;#39;) \gexec
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SELECT pg_sleep(1.2);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SELECT &amp;#39;deadline passed=&amp;#39;||(rolvaliduntil&amp;lt;clock_timestamp())
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;FROM pg_authid WHERE rolname=&amp;#39;article_expiry&amp;#39;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LAB-ONLY-OLD-PASSWORD psql -X -qAt -h 127.0.0.1 -p &lt;span class="m"&gt;55477&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -U article_expiry -d postgres -c &lt;span class="s1"&gt;&amp;#39;SELECT 1&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/new.out&amp;#34;&lt;/span&gt; 2&amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/new.err&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -q &lt;span class="s1"&gt;&amp;#39;password authentication failed&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/new.err&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;fresh SCRAM login rejected after expiry&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;SELECT &amp;#39;held after expiry=&amp;#39;||current_user;\n&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -X -qAt -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; -h &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -p &lt;span class="m"&gt;55477&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -U article_expiry -d postgres -c &lt;span class="s2"&gt;&amp;#34;SELECT &amp;#39;local trust login=&amp;#39;||current_user&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SET password_encryption=&amp;#39;scram-sha-256&amp;#39;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;ALTER ROLE article_expiry PASSWORD &amp;#39;LAB-ONLY-NEW-PASSWORD&amp;#39;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LAB-ONLY-NEW-PASSWORD psql -X -qAt -h 127.0.0.1 -p &lt;span class="m"&gt;55477&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -U article_expiry -d postgres -c &lt;span class="s1"&gt;&amp;#39;SELECT 1&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/rotated.out&amp;#34;&lt;/span&gt; 2&amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/rotated.err&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -q &lt;span class="s1"&gt;&amp;#39;password authentication failed&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/rotated.err&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rotating password alone preserves expired deadline&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SELECT format(&amp;#39;ALTER ROLE article_expiry VALID UNTIL %L&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; clock_timestamp()+interval &amp;#39;1 hour&amp;#39;) \gexec
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LAB-ONLY-OLD-PASSWORD psql -X -qAt -h 127.0.0.1 -p &lt;span class="m"&gt;55477&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -U article_expiry -d postgres -c &lt;span class="s1"&gt;&amp;#39;SELECT 1&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/old.out&amp;#34;&lt;/span&gt; 2&amp;gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/old.err&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -q &lt;span class="s1"&gt;&amp;#39;password authentication failed&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/old.err&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;old password rejected after rotation&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LAB-ONLY-NEW-PASSWORD psql -X -qAt -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -h 127.0.0.1 -p &lt;span class="m"&gt;55477&lt;/span&gt; -U article_expiry -d postgres &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -c &lt;span class="s2"&gt;&amp;#34;SELECT &amp;#39;new password login=&amp;#39;||current_user&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;SELECT &amp;#39;held after rotation=&amp;#39;||current_user;\n&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;\\q\n&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; 3&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;-
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$held_pid&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -qx &lt;span class="s1"&gt;&amp;#39;held after expiry=article_expiry&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/held.out&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -qx &lt;span class="s1"&gt;&amp;#39;held after rotation=article_expiry&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/held.out&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab_root&lt;/span&gt;&lt;span class="s2"&gt;/held.out&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此前 PostgreSQL 18.6 记录中的输出确认截止时间已经过去；本轮没有重跑认证实验。脚本把期限设为数据库当前时间的一秒之后，然后等待数据库时钟越过它，所以这里实际经历了到期，而不是只拿一个编造日期做字符串比较。随后，通过 SCRAM 密码认证建立的新连接被拒绝，验证了真正决定认证结果的服务端规则。&lt;/p&gt;
&lt;p&gt;到期之前打开的连接仍然可用。输入管道让同一个客户端和数据库会话跨过截止时间与密码替换过程，最后的输出确认它在两件事发生后都还能以该角色执行查询。如果需求是终止既有会话，就必须建立超出这个密码属性的明确策略；只观察新登录失败，并不能宣称此前的连接也已全部失去访问。&lt;/p&gt;
&lt;p&gt;新的本地信任登录依然成功，这符合不同认证方式适用不同规则的文档契约。单独替换密码后，新的密码登录还是失败，因为原截止时间没有被重置。脚本再提供一个未来期限，替换后的密码才可以连接，而原密码被拒绝。这样分别验证了凭据值、有效日期和会话存续，不会把三件事合并成一个模糊的“轮换成功”。&lt;/p&gt;
&lt;p&gt;实验最终明确了三项独立观察：期限影响新的密码认证；认证方式决定这条规则是否适用；已经建立的会话不会自动终止。示例没有实现生产凭据代理，也没有证明所有访问路径都能被统一撤销。实验完成后，专用集群与两条虚构密码一起被丢弃，不把这套仅为观察行为而设计的认证配置留在运行环境中。&lt;/p&gt;
&lt;h2 id="在消费者需要重新连接之前完成更新"&gt;在消费者需要重新连接之前完成更新&lt;/h2&gt;
&lt;p&gt;选择有效期时，应为获取替换值、分发、刷新消费进程，以及从有限中断中恢复预留时间。监控验证方剩余有效时间，也要观察每个消费者实际载入值的版本或年龄。管理清单显示已经产生新版本，并不证明运行中的应用已经使用新值，更不能代替一次真正的新连接测试。&lt;/p&gt;
&lt;p&gt;平台的切换模型允许重叠时，应先用替换凭据完成新的认证，再退休旧版本。之后确认原凭据无法建立新访问，并单独决定是否排空或终止既有会话。发生泄漏时，紧急撤销可能优先于无中断切换；不能因为期限将来会到，就推迟当下应实施的撤销动作。流程目标是控制实际接受行为，而不是维持一个看起来整齐的轮换日历。&lt;/p&gt;
&lt;p&gt;保持时钟同步，避免续期失败就悄悄延长有效期。记录被测主体、版本、验证方和期限，不记录秘密值。有效证据应包含预期边界前能接受、边界后在目标路径上被拒绝，以及对既有会话影响的清楚认识。管理界面显示了一个日期，只说明配置里存在这个字段，还没有证明系统执行了相应的访问限制。&lt;/p&gt;
&lt;h2 id="延伸阅读"&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/notes/hsts-should-be-rolled-out-with-a-recovery-plan/"&gt;HSTS 应带着恢复计划来灰度发布&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/notes/security-headers-should-describe-the-actual-page/"&gt;安全响应头应描述实际页面&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/sql-createrole.html"&gt;PostgreSQL 18 CREATE ROLE&lt;/a&gt;，密码专属有效期与非密码认证，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/sql-alterrole.html"&gt;PostgreSQL 18 ALTER ROLE&lt;/a&gt;，密码与有效期的独立修改，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/auth-pg-hba-conf.html"&gt;PostgreSQL 18 主机认证&lt;/a&gt;，认证规则匹配，以及信任认证和 SCRAM 的区别，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"&gt;OWASP 秘密管理&lt;/a&gt;，到期、轮换、撤销和生命周期控制，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>