<?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://www.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://www.xbcatm.com/tags/lifecycle/index.xml" rel="self" type="application/rss+xml"/><item><title>S3 storage classes trade cost for access</title><link>https://www.xbcatm.com/posts/s3-storage-classes-trade-cost-for-access/</link><pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/s3-storage-classes-trade-cost-for-access/</guid><description>Different data cools at different rates. Storage classes let a tier that rarely changes pay less per byte while hot data stays fast to read.</description><content:encoded>&lt;p&gt;Choosing a storage class is a bet on when you will read the bytes again. S3 Standard costs $0.023 per GB-month in us-east-1; Glacier Deep Archive costs $0.00099. That is a 23-fold difference for identical durability, and the whole decision is which number your access pattern deserves.&lt;/p&gt;
&lt;p&gt;The headline rate is the least informative number in the table. Retrieval charges, minimum storage durations, per-object metadata overhead and transition requests scale with object count rather than total bytes, so the cheap-looking class can be the expensive one for your workload.&lt;/p&gt;
&lt;h2 id="what-the-real-prices-say"&gt;What the real prices say&lt;/h2&gt;
&lt;p&gt;The published us-east-1 rates: Standard $0.023, Standard-IA $0.0125, One Zone-IA $0.010, Glacier Instant Retrieval $0.004, Glacier Flexible Retrieval $0.0036 and Deep Archive $0.00099 per GB-month. Retrieval is where the classes diverge more sharply: Standard-IA and One Zone-IA charge $0.010 per GB read, and Glacier Instant Retrieval charges $0.030.&lt;/p&gt;
&lt;p&gt;The non-obvious part: in us-east-1 the current price list charges nothing per GB for standard-tier retrieval from Glacier Flexible Retrieval and Deep Archive. You pay per restore request instead — $0.00005 — and you wait hours. So &amp;ldquo;archive classes have retrieval fees&amp;rdquo; is only reliably true for the instant-retrieval classes. Assume otherwise and the arithmetic inverts: if Deep Archive charged the often-quoted $0.02 per GB, reading a 1 GB object 52 times a year would cost $1.05 instead of $0.015 — a 70-fold swing from one regional detail.&lt;/p&gt;
&lt;h2 id="minimum-durations-and-minimum-billable-sizes"&gt;Minimum durations and minimum billable sizes&lt;/h2&gt;
&lt;p&gt;Minimum storage duration is 30 days for Standard-IA and One Zone-IA, 90 for Glacier Instant Retrieval, 90 for Glacier Flexible Retrieval, 180 for Deep Archive. Delete or transition out earlier and you are billed for the remainder — a 1 GB object deleted after 10 days from a 180-day class still costs $0.00586.&lt;/p&gt;
&lt;p&gt;Minimum billable object size is the other trap. Standard-IA, One Zone-IA and Glacier Instant Retrieval bill a floor of 128 KB, so a 10 KB object stored in Glacier Instant Retrieval is charged as 128 KB — a 12.8-fold inflation. Glacier Flexible Retrieval and Deep Archive have no floor but add 40 KB of metadata per archived object, 32 KB at the class rate, 8 KB at the Standard rate. And the transition itself costs a request per object: $0.00001 into Standard-IA, $0.00002 into Glacier Instant Retrieval, $0.00003 into Glacier Flexible Retrieval, $0.00005 into Deep Archive. For a 10 KB object in Glacier Instant Retrieval that single request is 65% of the object&amp;rsquo;s whole annual cost. Since September 2024 the default lifecycle behaviour refuses to transition objects under 128 KB at all, for precisely that reason.&lt;/p&gt;
&lt;h2 id="break-even-not-vibes"&gt;Break-even, not vibes&lt;/h2&gt;
&lt;p&gt;For a 1 GB object held a year, Standard-IA stops being cheaper than Standard after 13 reads, and Glacier Instant Retrieval after 8. Both land near one read a month, which is what the documented design points — &amp;ldquo;once a month&amp;rdquo;, &amp;ldquo;once a quarter&amp;rdquo; — describe. The break-even is computable, so compute it instead of assuming it.&lt;/p&gt;
&lt;p&gt;The weekly reader is where this goes wrong. A 1 GB object read 52 times a year and transitioned to Glacier Instant Retrieval costs $1.61 against $0.276 on Standard, 5.8 times more, with $1.56 of retrieval fees accounting for the entire difference. A 30-day transition rule is right for a log archive and wrong for a weekly report, and the rule cannot tell them apart unless you scope it by prefix or tag.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The first failure is a lifecycle rule cooling objects that are read every week. It presents as a storage line that went down and a total bill that went up, with the difference sitting in retrieval charges elsewhere on the invoice. Pull per-class retrieval spend rather than looking at the storage line, and check whether any rule&amp;rsquo;s transition age is shorter than the interval between reads.&lt;/p&gt;
&lt;p&gt;The second failure is objects stranded in an archive class with no tested restore path. Glacier Flexible Retrieval restores take minutes on the expedited tier and 5 to 12 hours in bulk; Deep Archive takes 12 to 48. A restore also produces a temporary copy with its own expiry, so a restore that worked last quarter may not work today. Worse, a 100 KB object in Standard-IA is already more expensive than Standard at zero reads, because the transition request exceeds the entire year&amp;rsquo;s storage saving — the object paid to become slower.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;See where the bytes actually are and what will happen to them:&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;The first is the distribution across classes — a large &lt;code&gt;GLACIER&lt;/code&gt; count on a bucket whose consumers read weekly is the whole problem. The second shows the transition ages; any age shorter than your read interval is a rule that will bill you for retrieval. The third confirms one object&amp;rsquo;s actual class, which is how you catch a lifecycle rule that silently skipped objects under 128 KB.&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/storage-classes/"&gt;AWS S3 storage classes&lt;/a&gt;, what each class is designed for, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html"&gt;AWS S3 storage class guide&lt;/a&gt;, minimum storage durations and minimum billable object sizes, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-transition-general-considerations.html"&gt;AWS S3 lifecycle transition rules&lt;/a&gt;, transition constraints and the 128 KB default, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/s3/pricing/"&gt;AWS S3 pricing&lt;/a&gt;, per-GB storage, retrieval and request charges, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Credential expiry needs an enforcing verifier</title><link>https://www.xbcatm.com/posts/secrets-need-expiry-not-just-rotation/</link><pubDate>Sun, 27 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.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;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&amp;rsquo;s &lt;code&gt;VALID UNTIL&lt;/code&gt; applies to a role&amp;rsquo;s password, not to the role&amp;rsquo;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. Replacing a password does not necessarily move its expiry date, and extending a deadline does not replace the value. 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 recorded PostgreSQL 18.6 run reports that the deadline has passed. 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&amp;rsquo;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-09-12.&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-09-12.&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-09-12.&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-09-12.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>