<?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>Lifecycle · XB Field Notes</title><link>https://hk.xbcatm.com/tags/lifecycle/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/lifecycle/index.xml" rel="self" type="application/rss+xml"/><item><title>S3 storage classes trade cost for access</title><link>https://hk.xbcatm.com/posts/s3-storage-classes-trade-cost-for-access/</link><pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/s3-storage-classes-trade-cost-for-access/</guid><description>Compare access deadlines, resilience, object sizes, and the complete bill before choosing a storage class.</description><content:encoded>&lt;p&gt;A storage class changes retrieval behavior, resilience, and billing, not just the monthly price of bytes. Start with the time allowed to recover data and whether another copy can be recreated. Standard-IA offers immediate access with retrieval charges; Glacier Flexible Retrieval and Deep Archive require restoration before ordinary reads. One Zone-IA also has a different failure boundary: its data is not resilient to physical loss of its Availability Zone.&lt;/p&gt;
&lt;h2 id="separate-the-charges-before-comparing-totals"&gt;Separate the charges before comparing totals&lt;/h2&gt;
&lt;p&gt;Use the current price sheet for the bucket’s Region and the exact class, retrieval tier, and request type. Storage is charged by billable capacity and duration; retrieval can be charged by bytes, requests, or both. Per-object transitions and archive metadata scale with object count. It is incorrect to describe all these costs as driven only by object count, or to infer total savings from one storage rate.&lt;/p&gt;
&lt;p&gt;Do not treat standard retrieval from Flexible Retrieval and Deep Archive as universally free. The price sheet checked on 2026-10-18 lists Flexible Retrieval bulk retrievals and requests as free; Intelligent-Tiering has separate free standard and bulk retrieval rules. Those exceptions do not establish the same price for another class or tier. A restored archive also retains the archived object while its temporary readable copy incurs Standard storage charges for the chosen lifetime.&lt;/p&gt;
&lt;p&gt;Minimum durations matter even when an object disappears sooner: Standard-IA and One Zone-IA use thirty days, Glacier Instant and Flexible Retrieval ninety, and Deep Archive 180. Early deletion, replacement, or transition can incur the remaining minimum charge. Standard-IA, One Zone-IA, and Glacier Instant Retrieval have a 128 KB billable-size floor. Flexible Retrieval and Deep Archive add forty KB of metadata per object, split between Standard and archive rates.&lt;/p&gt;
&lt;h2 id="match-lifecycle-rules-to-the-workload"&gt;Match lifecycle rules to the workload&lt;/h2&gt;
&lt;p&gt;An object’s age is not its time since last access. A thirty-day transition can affect a year-old report still read weekly. Evaluate object size, bytes retrieved, request frequency, retention, and acceptable restoration delay together. Prefixes and tags can distinguish workloads, but only if their classification stays accurate.&lt;/p&gt;
&lt;p&gt;The September 2024 default prevents lifecycle transitions of objects smaller than 128 KB. Older unmodified configurations can retain the previous behavior, and explicit size filters can override defaults. Therefore, a small object remaining in Standard does not alone prove a broken rule. Inspect the configuration’s effective filters and history before attributing the outcome.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A lower storage subtotal can conceal higher retrieval and request charges. Conversely, a frequently read restored copy need not trigger a fresh archive restoration for every read while that copy remains available. Model the actual restore-and-read workflow, including temporary-copy storage, instead of multiplying archive restore charges by every GET.&lt;/p&gt;
&lt;p&gt;A restore duration is also an operational dependency. Flexible Retrieval standard requests typically take three to five hours; Deep Archive standard requests typically finish within twelve hours, with bulk typically within forty-eight. These are documented expectations, not an unconditional deadline guarantee. Exercise restoration and verify completion before promising that a recovery-time objective is met.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;
&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-s3-storage-classes-trade-cost-for-access.detail.en.svg" alt="S3 decisions compare immediate versus restored access, one-zone failure risk, full billing dimensions and lifecycle workload filters." width="560" height="2354"&gt;
&lt;figcaption&gt;S3 access before cost — mechanism diagram.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this standalone Python 3 script with assertions enabled. The earlier article reports Python 3.12.14 execution; no matching raw run record was recovered here, and this review did not rerun it. Its deliberately invented prices demonstrate sensitivity, not a current AWS quote or a complete bill. It compares one GiB retained for twelve modeled months and assumes every read retrieves the entire object.&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;Zero reads favor the colder model at 0.121 versus 0.24; fifty-two full reads reverse the choice to 0.641 versus 0.24. With these inputs, eleven reads remain cheaper and twelve do not. The size calculation shows why small-object floors need explicit treatment. Neither result establishes a universal class-selection threshold.&lt;/p&gt;
&lt;p&gt;For an authorized bucket, the following unchanged AWS CLI diagnostics require configured read access and replacement of B and K. They were not executed during this review:&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;For a successful nonempty listing, the first command counts current objects by class, not bytes or noncurrent versions. Handle empty listings and CLI errors separately; pipeline success alone does not validate the AWS call. The second shows only each rule’s first transition, omitting filters and later or noncurrent transitions; inspect the full response before deciding. HeadObject omits StorageClass for Standard, so a null projection is not itself a failure. None of these observations measures access frequency or proves that a pending transition or restore completed. Combine inventory, complete policy, access evidence, and billing before changing lifecycle behavior.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, billing dimensions, restore copies, and free-tier distinctions, checked 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;, access, resilience, minimum duration, and billable size, checked 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;, small-object defaults and existing configurations, checked 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;, typical restore durations, checked 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 omits StorageClass, checked 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;, listing scope and object fields, checked 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;, filters and transition arrays, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Credential expiry needs an enforcing verifier</title><link>https://hk.xbcatm.com/posts/secrets-need-expiry-not-just-rotation/</link><pubDate>Sun, 27 Sep 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/secrets-need-expiry-not-just-rotation/</guid><description>A PostgreSQL password deadline blocks new password authentication, but leaves established sessions alive and does not govern trust authentication.</description><content:encoded>&lt;p&gt;A credential expires only when the system deciding access checks an applicable deadline. Writing an expiry date in an inventory does not change what a database or API accepts. A short lifetime can reduce the period during which a stolen credential starts new access, but its effect depends on the authentication mechanism and on what happens to sessions or other credentials already created with it.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-secrets-need-expiry-not-just-rotation.detail.en.svg" alt="After PostgreSQL VALID UNTIL, the historical fixture rejects fresh SCRAM but allows a held session and local trust login. PASSWORD and VALID UNTIL updates are independent. A separate generic overlap model, only where supported, validates the replacement before retiring the old version. Verify new acceptance, old rejection and session effects separately; do not wait for expiry after exposure." width="720" height="2370"&gt;&lt;figcaption&gt;Expiry changes acceptance only where the verifier enforces it. The historical PostgreSQL fixture separates fresh password login, held sessions and trust authentication. A separate generic overlap model applies only where supported; rotation, renewal and revocation require distinct evidence.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Expiry does not undo earlier actions. Data already copied remains copied, and a session established before the deadline may continue working. An attacker may also have created another access path while authorized. Treat lifetime as one limit on future use, alongside scope, revocation, session management, and investigation of actual effects. It does not put a universal ceiling on the damage from exposure.&lt;/p&gt;
&lt;p&gt;PostgreSQL’s &lt;code&gt;VALID UNTIL&lt;/code&gt; applies to a role’s password, not to the role’s existence or all authentication methods. A connection using a non-password method does not gain that deadline check. Testing expiry through a local trust rule can therefore misleadingly suggest that the deadline is broken. Verify the authentication path actually used by the client.&lt;/p&gt;
&lt;p&gt;Rotation and renewal are separate changes. In PostgreSQL, changing only PASSWORD preserves VALID UNTIL, while changing only VALID UNTIL preserves the password. A refresh design that updates only one can leave applications unable to reconnect or leave the old value usable. Successful access through an existing pool connection does not prove that replacement credentials can establish fresh connections.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Use an isolated Linux environment with PostgreSQL 18 programs on &lt;code&gt;PATH&lt;/code&gt;, running as a non-root user, and an unused loopback port 55477. Save this as a shell script and run it with &lt;code&gt;sh&lt;/code&gt;. It initializes a disposable cluster, uses explicitly invented passwords, binds TCP only to loopback, and removes its temporary directory on exit. Local trust is included solely to demonstrate a different authentication path:&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;The historical PostgreSQL 18.6 run reports that the deadline has passed; this review did not rerun the authentication fixture. It waits on the database clock after setting a deadline one second ahead, so this is an actual expiry crossing rather than merely a string comparison against a fabricated date. A fresh SCRAM password login is then rejected.&lt;/p&gt;
&lt;p&gt;The connection opened before expiry remains usable. Its input pipe keeps the same client and database session alive across the deadline and password replacement. The final output confirms queries under that role after both events. Closing or revoking existing sessions requires an explicit policy beyond this password attribute.&lt;/p&gt;
&lt;p&gt;A new local trust login succeeds despite the expired password, as the documented authentication distinction predicts. Replacing the password alone still leaves a fresh password login rejected because the old deadline remains. Once the script supplies a new future deadline, the replacement password connects and the original password is rejected.&lt;/p&gt;
&lt;p&gt;These checks isolate three independent observations: the deadline affects new password authentication, the chosen authentication method determines whether that rule applies, and existing sessions are not automatically terminated. The example does not implement a production credential broker or demonstrate revocation of every access path. The private cluster and both fixture passwords are discarded afterward.&lt;/p&gt;
&lt;h2 id="refresh-before-consumers-need-another-connection"&gt;Refresh before consumers need another connection&lt;/h2&gt;
&lt;p&gt;Choose lifetimes with enough allowance for obtaining a replacement, distributing it, refreshing consumers, and recovering from a bounded interruption. Monitor remaining validity at the verifier and the age of the value actually loaded by each consumer. An inventory showing a new version is insufficient if a running process still holds the old one.&lt;/p&gt;
&lt;p&gt;Validate replacement credentials through fresh authentication before retiring an old version where the platform’s transition model permits overlap. Confirm that retirement denies new use of the old credential, and separately decide whether sessions must be drained or terminated. During a compromise, emergency revocation may take priority over an uninterrupted transition; expiry should not become a reason to wait for the clock.&lt;/p&gt;
&lt;p&gt;Keep clocks synchronized and avoid silently extending lifetimes whenever renewal fails. Record which principal, version, verifier, and deadline were tested without recording secret values. The useful evidence is acceptance before the intended boundary, rejection afterward on the intended path, and an understood effect on existing sessions, not just a date displayed in a management interface.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, password-only validity and non-password authentication, checked 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;, independent password and validity changes, checked 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 host authentication&lt;/a&gt;, matching authentication rules and trust versus SCRAM, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"&gt;OWASP secrets management&lt;/a&gt;, expiry, rotation, revocation, and lifecycle control, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>