<?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>Infrastructure · XB Field Notes</title><link>https://hk.xbcatm.com/tags/infrastructure/</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/infrastructure/index.xml" rel="self" type="application/rss+xml"/><item><title>Zone transfer copies authoritative data under explicit authorization and serial rules</title><link>https://hk.xbcatm.com/posts/dns-zone-transfer-replicates-answers/</link><pubDate>Sun, 05 Dec 2027 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/dns-zone-transfer-replicates-answers/</guid><description>Run TSIG-authorized AXFR to a client and a real secondary, expose unchanged-serial divergence, request IXFR without history, and observe secondary expiry.</description><content:encoded>&lt;p&gt;AXFR transfers a complete DNS zone so another server or authorized client can obtain its data. A secondary can then answer authoritatively from its local copy. The protocol does not magically identify callers as legitimate authoritative servers: access control decides who may transfer. A zone is also a defined authority boundary, not every record in every delegated child zone or the organization&amp;rsquo;s entire DNS namespace.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-dns-zone-transfer-replicates-answers.detail.en.svg" alt="One authorized AXFR transfers one zone with opening and closing SOA framing. Primary and secondary diverge at serial 10 and converge at serial 11. TSIG authenticates without encrypting, an ordinary allow-list is not automatically an AND, IXFR can fall back to full transfer, and secondary expiry differs from downstream TTL." width="720" height="3780"&gt;&lt;figcaption&gt;Compare serial and actual data, not serial alone. The source’s 10-to-11 repair is a concrete case; general comparison uses 32-bit serial arithmetic. Complete-transfer framing, TSIG authorization and SOA expiry are separate checks. The zone-transfer exercise was not rerun and no shared secret was generated.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Editing primary data without updating the SOA serial can leave a secondary serving different content with the same version number. Reloading the primary does not force every secondary to replace an apparently unchanged zone. DNS serial comparison uses 32-bit serial arithmetic, including wraparound rules; a date-looking number is only an operator convention, not an independent guarantee that every numeric change is newer.&lt;/p&gt;
&lt;p&gt;An unrestricted transfer can reveal the zone&amp;rsquo;s full published contents to an arbitrary requesting client. Source restrictions and TSIG can control access, but their configuration semantics matter. In particular, listing an address and a key in an ordinary BIND allow-list is not automatically an AND requirement. Review the actual ACL behavior and test an unauthorized request from a relevant source, rather than assuming two listed conditions both apply.&lt;/p&gt;
&lt;p&gt;A secondary&amp;rsquo;s ability to answer during primary failure is bounded by SOA expiry. Record TTL controls caching by downstream consumers; it is not the same as the secondary&amp;rsquo;s expiry timer for its authoritative copy. After unsuccessful refresh for the allowed period, the secondary must stop treating that copy as current authoritative data. Existing successful answers do not establish unlimited independence from the primary.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save this as &lt;code&gt;check-zone-transfer.py&lt;/code&gt; and run &lt;code&gt;python3 check-zone-transfer.py&lt;/code&gt; with Docker. Two BIND 9.18.49 processes run inside an isolated container. A randomly generated disposable TSIG secret authorizes transfers, and is not printed. The fixture uses deliberately short refresh and expiry values to expose state changes promptly; these values are test settings, not a suggested production policy:&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;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&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;tempfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;uuid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;FROM python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends bind9=1:9.18.49-1~deb12u2 bind9-dnsutils=1:9.18.49-1~deb12u2 dnsmasq-base=2.90-4~deb12u2 python3-dnspython=2.3.0-1 &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;CMD [&amp;#34;/usr/bin/python3&amp;#34;]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;program&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;from contextlib import ExitStack,contextmanager
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;from pathlib import Path
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;from tempfile import TemporaryDirectory
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;import json,re,signal,subprocess,time
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;import dns.exception,dns.message,dns.query,dns.rcode,dns.rdatatype
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def run(*args,check=True):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; result=subprocess.run(args,capture_output=True,text=True,timeout=15)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; if check:assert result.returncode==0,(args,result.returncode,result.stdout,result.stderr)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return result
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def query(name,kind=&amp;#39;A&amp;#39;,port=1053):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return dns.query.udp(dns.message.make_query(name,kind),&amp;#39;127.0.0.1&amp;#39;,port=port,timeout=1)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def values(response,kind=&amp;#39;A&amp;#39;):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wanted=dns.rdatatype.from_text(kind)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return [item.to_text() for rrset in response.answer if rrset.rdtype==wanted for item in rrset]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def show(response):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def section(records):return [{&amp;#39;name&amp;#39;:str(rr.name),&amp;#39;type&amp;#39;:dns.rdatatype.to_text(rr.rdtype),&amp;#39;ttl&amp;#39;:rr.ttl,&amp;#39;data&amp;#39;:[item.to_text() for item in rr]} for rr in records]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return {&amp;#39;rcode&amp;#39;:dns.rcode.to_text(response.rcode()),&amp;#39;answer&amp;#39;:section(response.answer),&amp;#39;authority&amp;#39;:section(response.authority)}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def wait(predicate,timeout=15):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; deadline=time.monotonic()+timeout
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; while time.monotonic()&amp;lt;deadline:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; try:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; if predicate():return
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; except (OSError,dns.exception.Timeout):pass
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; time.sleep(.05)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; raise AssertionError(&amp;#39;DNS fixture condition did not become true&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def stop(process):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; if process.poll() is None:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; process.terminate()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; try:process.wait(timeout=5)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; except subprocess.TimeoutExpired:process.kill();process.wait(timeout=5)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;@contextmanager
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def lab(records,soa_ttl=8,minimum=4,extra=&amp;#39;&amp;#39;):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; with TemporaryDirectory(prefix=&amp;#39;xbcatm-dns-&amp;#39;) as directory,ExitStack() as stack:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; root=Path(directory);zone=root/&amp;#39;zone&amp;#39;;conf=root/&amp;#39;named.conf&amp;#39;;log=root/&amp;#39;named.log&amp;#39;;serial=1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def save(body):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; zone.write_text(f&amp;#39;$ORIGIN lab.test.\n$TTL 30\n@ &lt;/span&gt;&lt;span class="si"&gt;{soa_ttl}&lt;/span&gt;&lt;span class="s2"&gt; IN SOA ns.lab.test. hostmaster.lab.test. (&lt;/span&gt;&lt;span class="si"&gt;{serial}&lt;/span&gt;&lt;span class="s2"&gt; 60 60 3600 &lt;/span&gt;&lt;span class="si"&gt;{minimum}&lt;/span&gt;&lt;span class="s2"&gt;)\n@ IN NS ns.lab.test.\nns IN A 127.0.0.1\n&amp;#39;+body+&amp;#39;\n&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; run(&amp;#39;named-checkzone&amp;#39;,&amp;#39;lab.test&amp;#39;,str(zone))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; save(records)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; conf.write_text(f&amp;#39;&amp;#39;&amp;#39;options {{ directory &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{root}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;; listen-on port 1053 {{127.0.0.1;}}; listen-on-v6 {{none;}};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;recursion no; dnssec-validation no; notify no; querylog yes; max-cache-size 8m; pid-file &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{root}&lt;/span&gt;&lt;span class="s2"&gt;/named.pid&amp;#34;; session-keyfile none;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="si"&gt;{extra}&lt;/span&gt;&lt;span class="s2"&gt; }};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;controls {{}};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;zone &amp;#34;lab.test&amp;#34; {{ type primary; file &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{zone}&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="s2"&gt;&amp;#39;&amp;#39;&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; run(&amp;#39;named-checkconf&amp;#39;,str(conf))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; output=stack.enter_context(log.open(&amp;#39;w&amp;#39;))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; authority=subprocess.Popen([&amp;#39;named&amp;#39;,&amp;#39;-g&amp;#39;,&amp;#39;-c&amp;#39;,str(conf),&amp;#39;-n&amp;#39;,&amp;#39;1&amp;#39;,&amp;#39;-U&amp;#39;,&amp;#39;1&amp;#39;],stdout=output,stderr=subprocess.STDOUT)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; stack.callback(stop,authority)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def ready():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert authority.poll() is None,log.read_text()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; response=query(&amp;#39;lab.test.&amp;#39;,&amp;#39;SOA&amp;#39;);return response.rcode()==0 and response.answer[0][0].serial==serial
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wait(ready)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def update(body):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; nonlocal serial
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; serial+=1;save(body);authority.send_signal(signal.SIGHUP);wait(ready)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def cache(port=1054,upstream=1053,size=150,options=()):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; logfile=root/(&amp;#39;cache-&amp;#39;+str(port)+&amp;#39;.log&amp;#39;);stream=stack.enter_context(logfile.open(&amp;#39;w&amp;#39;))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; command=[&amp;#39;dnsmasq&amp;#39;,&amp;#39;--no-daemon&amp;#39;,&amp;#39;--conf-file=/dev/null&amp;#39;,&amp;#39;--no-resolv&amp;#39;,&amp;#39;--no-hosts&amp;#39;,&amp;#39;--bind-interfaces&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;--listen-address=127.0.0.1&amp;#39;,&amp;#39;--port=&amp;#39;+str(port),&amp;#39;--server=127.0.0.1#&amp;#39;+str(upstream),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;--cache-size=&amp;#39;+str(size),&amp;#39;--log-queries&amp;#39;,&amp;#39;--log-facility=-&amp;#39;,*options]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; process=subprocess.Popen(command,stdout=stream,stderr=subprocess.STDOUT);stack.callback(stop,process)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def cache_ready():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert process.poll() is None,logfile.read_text()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return query(&amp;#39;lab.test.&amp;#39;,&amp;#39;SOA&amp;#39;,port).rcode()==0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wait(cache_ready);return process,logfile
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def queries(name,kind=&amp;#39;A&amp;#39;):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return sum(bool(re.search(r&amp;#39;query: &amp;#39;+re.escape(name.rstrip(&amp;#39;.&amp;#39;))+r&amp;#39; IN &amp;#39;+kind+r&amp;#39;\b&amp;#39;,line)) for line in log.read_text().splitlines())
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; yield root,update,cache,queries
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;@contextmanager
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;def named_process(root,config):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; root.mkdir(exist_ok=True);conf=root/&amp;#39;named.conf&amp;#39;;log=root/&amp;#39;named.log&amp;#39;;conf.write_text(config)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; run(&amp;#39;named-checkconf&amp;#39;,str(conf))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; with log.open(&amp;#39;w&amp;#39;) as output:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; process=subprocess.Popen([&amp;#39;named&amp;#39;,&amp;#39;-g&amp;#39;,&amp;#39;-c&amp;#39;,str(conf),&amp;#39;-n&amp;#39;,&amp;#39;1&amp;#39;,&amp;#39;-U&amp;#39;,&amp;#39;1&amp;#39;],stdout=output,stderr=subprocess.STDOUT)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; try:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def ready():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert process.poll() is None,log.read_text()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return re.search(r&amp;#39;\brunning\s*$&amp;#39;,log.read_text(),re.M) is not None
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wait(ready);yield process,log
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; finally:stop(process)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;import base64,secrets
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;import dns.tsig,dns.tsigkeyring
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;with TemporaryDirectory(prefix=&amp;#39;xbcatm-xfr-&amp;#39;) as directory:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; root=Path(directory);primary=root/&amp;#39;primary&amp;#39;;secondary=root/&amp;#39;secondary&amp;#39;;primary.mkdir();secondary.mkdir()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; secret=base64.b64encode(secrets.token_bytes(32)).decode();keyring=dns.tsigkeyring.from_text({&amp;#39;xfr.lab.test.&amp;#39;:secret})
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; key_config=f&amp;#39;key &amp;#34;xfr.lab.test.&amp;#34; {{ algorithm hmac-sha256; secret &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{secret}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;; }};&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; zone=primary/&amp;#39;zone&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def save(serial,address):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; zone.write_text(f&amp;#39;$ORIGIN lab.test.\n$TTL 2\n@ IN SOA ns.lab.test. hostmaster.lab.test. (&lt;/span&gt;&lt;span class="si"&gt;{serial}&lt;/span&gt;&lt;span class="s2"&gt; 1 1 6 1)\n@ IN NS ns.lab.test.\nns IN A 127.0.0.1\nservice IN A &lt;/span&gt;&lt;span class="si"&gt;{address}&lt;/span&gt;&lt;span class="s2"&gt;\n&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; run(&amp;#39;named-checkzone&amp;#39;,&amp;#39;lab.test&amp;#39;,str(zone))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def options(path,port):return f&amp;#39;&amp;#39;&amp;#39;options {{ directory &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{path}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;; listen-on port &lt;/span&gt;&lt;span class="si"&gt;{port}&lt;/span&gt;&lt;span class="s2"&gt; {{127.0.0.1;}}; listen-on-v6 {{none;}};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;recursion no; dnssec-validation no; max-cache-size 8m; session-keyfile none; pid-file &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{path}&lt;/span&gt;&lt;span class="s2"&gt;/named.pid&amp;#34;; lock-file &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{path}&lt;/span&gt;&lt;span class="s2"&gt;/named.lock&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;min-refresh-time 1; max-refresh-time 2; min-retry-time 1; max-retry-time 2; }}; controls {{}};&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; save(10,&amp;#39;127.0.0.2&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; primary_config=key_config+options(primary,1053)+f&amp;#39;&amp;#39;&amp;#39;zone &amp;#34;lab.test&amp;#34; {{ type primary; file &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{zone}&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="s2"&gt;allow-transfer {{key &amp;#34;xfr.lab.test.&amp;#34;;}}; notify explicit; also-notify {{127.0.0.1 port 1054 key &amp;#34;xfr.lab.test.&amp;#34;;}}; }};&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; secondary_config=key_config+options(secondary,1054)+f&amp;#39;&amp;#39;&amp;#39;zone &amp;#34;lab.test&amp;#34; {{ type secondary; file &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{secondary}&lt;/span&gt;&lt;span class="s2"&gt;/copy&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;primaries {{127.0.0.1 port 1053 key &amp;#34;xfr.lab.test.&amp;#34;;}}; allow-transfer {{none;}}; }};&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; with named_process(primary,primary_config) as (master,master_log),named_process(secondary,secondary_config):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wait(lambda: values(query(&amp;#39;service.lab.test.&amp;#39;,port=1054))==[&amp;#39;127.0.0.2&amp;#39;])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def transfer(**kwargs):return [rr for message in dns.query.xfr(&amp;#39;127.0.0.1&amp;#39;,&amp;#39;lab.test.&amp;#39;,port=1053,timeout=2,lifetime=4,relativize=False,**kwargs) for rr in message.answer]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; copied=transfer(keyring=keyring,keyname=&amp;#39;xfr.lab.test.&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert copied[0].rdtype==copied[-1].rdtype==dns.rdatatype.SOA
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert copied[0][0].serial==copied[-1][0].serial==10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert any(str(rr.name)==&amp;#39;service.lab.test.&amp;#39; and any(str(item)==&amp;#39;127.0.0.2&amp;#39; for item in rr) for rr in copied)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; rejected=&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; for label,kwargs in [(&amp;#39;unsigned&amp;#39;,&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;),(&amp;#39;wrong_secret&amp;#39;,{&amp;#39;keyring&amp;#39;:dns.tsigkeyring.from_text({&amp;#39;xfr.lab.test.&amp;#39;:base64.b64encode(secrets.token_bytes(32)).decode()}),&amp;#39;keyname&amp;#39;:&amp;#39;xfr.lab.test.&amp;#39;})]:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; try:transfer(**kwargs);raise AssertionError(&amp;#39;unauthorized zone transfer succeeded&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; except (dns.query.TransferError,dns.tsig.PeerBadSignature) as error:rejected[label]=type(error).__name__
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert values(query(&amp;#39;service.lab.test.&amp;#39;))==[&amp;#39;127.0.0.2&amp;#39;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; save(10,&amp;#39;127.0.0.3&amp;#39;);master.send_signal(signal.SIGHUP);wait(lambda: values(query(&amp;#39;service.lab.test.&amp;#39;))==[&amp;#39;127.0.0.3&amp;#39;])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; time.sleep(2.2);unchanged=query(&amp;#39;service.lab.test.&amp;#39;,port=1054);assert values(unchanged)==[&amp;#39;127.0.0.2&amp;#39;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; save(11,&amp;#39;127.0.0.3&amp;#39;);master.send_signal(signal.SIGHUP)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wait(lambda: query(&amp;#39;lab.test.&amp;#39;,&amp;#39;SOA&amp;#39;).answer[0][0].serial==11)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wait(lambda: values(query(&amp;#39;service.lab.test.&amp;#39;,port=1054))==[&amp;#39;127.0.0.3&amp;#39;])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; secondary_soa=query(&amp;#39;lab.test.&amp;#39;,&amp;#39;SOA&amp;#39;,1054);assert secondary_soa.answer[0][0].serial==11
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; requested_ixfr=transfer(keyring=keyring,keyname=&amp;#39;xfr.lab.test.&amp;#39;,rdtype=&amp;#39;IXFR&amp;#39;,serial=10)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert requested_ixfr[0][0].serial==11 and requested_ixfr[1].rdtype!=dns.rdatatype.SOA
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; assert requested_ixfr[-1].rdtype==dns.rdatatype.SOA and requested_ixfr[-1][0].serial==11
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; stop(master)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; while_valid=query(&amp;#39;service.lab.test.&amp;#39;,port=1054);assert values(while_valid)==[&amp;#39;127.0.0.3&amp;#39;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; expired=[]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; def no_longer_served():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; answer=query(&amp;#39;service.lab.test.&amp;#39;,port=1054);expired.append(answer);return answer.rcode()==dns.rcode.SERVFAIL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; wait(no_longer_served,timeout=12)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; print(json.dumps({&amp;#39;AXFR_SOA_serials&amp;#39;:[copied[0][0].serial,copied[-1][0].serial],&amp;#39;AXFR_record_sets&amp;#39;:len(copied),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;unauthorized_transfer_errors&amp;#39;:rejected,&amp;#39;same_serial_secondary_address&amp;#39;:values(unchanged),&amp;#39;updated_secondary_serial&amp;#39;:11,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;IXFR_without_history_returned_AXFR&amp;#39;:True,&amp;#39;primary_stopped_secondary_initially_answers&amp;#39;:values(while_valid),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;secondary_after_SOA_expire&amp;#39;:dns.rcode.to_text(expired[-1].rcode())},sort_keys=True))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;xbcatm-dns-&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&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&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;docker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&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;try&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;with&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&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;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;);(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Dockerfile&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&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;built&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;build&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-t&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name&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;root&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;built&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;built&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;run&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--rm&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--network&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--memory&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;384m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--pids-limit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;128&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name&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="s1"&gt;&amp;#39;/usr/bin/python3&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;program&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&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;finally&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;docker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rm&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;image&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rm&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name&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;The primary starts at serial 10 with &lt;code&gt;service.lab.test&lt;/code&gt; pointing to 127.0.0.2. A real secondary transfers the zone and answers that address. An ordinary Python client using the authorized TSIG key also completes AXFR. Its five RRset entries include opening and closing SOA records with serial 10 and the expected service data, demonstrating both transfer completion and access by an authorized non-server client.&lt;/p&gt;
&lt;p&gt;An unsigned request and a request signed with a different secret fail, even though the requests originate inside the same loopback environment. Ordinary permitted address queries still succeed. The tested rule requires the configured key for transfer; it does not make every DNS query require TSIG. Authentication of the transfer operation is separate from publishing ordinary authoritative answers.&lt;/p&gt;
&lt;p&gt;The primary then reloads a changed service address, 127.0.0.3, while retaining serial 10. After the short refresh window, the secondary still answers 127.0.0.2. Raising the serial to 11 and reloading lets the secondary acquire the changed address. The example therefore verifies the divergence and its repair, rather than merely checking that both configuration files mention the same primary.&lt;/p&gt;
&lt;p&gt;An IXFR request for changes since serial 10 receives a full AXFR-style response because this primary retains no incremental history for the update. The script identifies the response&amp;rsquo;s SOA framing. Finally, it stops the primary. The secondary initially continues answering 127.0.0.3, then returns SERVFAIL after its short SOA expiry interval. No host clock or shared network setting is changed to produce that transition.&lt;/p&gt;
&lt;h2 id="validate-replication-authorization-and-confidentiality-separately"&gt;Validate replication, authorization, and confidentiality separately&lt;/h2&gt;
&lt;p&gt;NOTIFY can prompt an earlier check, while periodic refresh provides another opportunity to detect a newer serial. Neither makes replication instantaneous. Monitor each authoritative server&amp;rsquo;s serial and actual answer data, and investigate transfer failures before expiry removes service. When repairing a serial mistake, ensure the secondary receives the intended complete state rather than assuming that any subsequent incremental update will repair arbitrary divergence.&lt;/p&gt;
&lt;p&gt;IXFR can save work when a suitable change history exists, but the request type does not guarantee an incremental response. AXFR can span multiple DNS messages over TCP, so complete-transfer verification must respect its protocol boundaries. A partial stream or a single matching record is insufficient evidence that the entire zone arrived and can be served correctly.&lt;/p&gt;
&lt;p&gt;TSIG authenticates messages using shared key material and protects integrity; it does not encrypt the zone contents. Protect the key and apply transport confidentiality where the deployment requires it. Keep transfer authorization, successful replication, ordinary query availability, and replica expiry as distinct checks. Together they establish the practical boundary far more clearly than an assumption that transfers only happen between trusted servers.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc5936.html"&gt;RFC 5936 AXFR&lt;/a&gt;, full-zone transfer framing and completion, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc1995.html"&gt;RFC 1995 IXFR&lt;/a&gt;, incremental requests and full-transfer fallback, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc1982.html"&gt;RFC 1982 serial number arithmetic&lt;/a&gt;, 32-bit DNS serial comparisons and wraparound, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8945.html"&gt;RFC 8945 TSIG&lt;/a&gt;, shared-key message authentication and integrity, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bind9.readthedocs.io/en/v9.18.49/reference.html"&gt;BIND 9.18.49 reference&lt;/a&gt;, transfer ACLs, notification, and secondary refresh controls, checked 2026-09-12.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Infrastructure as code is a changelog, not magic</title><link>https://hk.xbcatm.com/posts/terraform-and-infrastructure-as-code/</link><pubDate>Fri, 13 Nov 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/terraform-and-infrastructure-as-code/</guid><description>Describing your platform in files makes changes reviewable and repeatable; the value is the plan and the diff it produces.</description><content:encoded>&lt;p&gt;Declarative infrastructure describes a desired result and lets a tool calculate changes. An imperative script can also inspect existing resources and behave idempotently; duplication is not inherent to scripting. Terraform&amp;rsquo;s advantage is a shared model of configuration, resource identities, dependencies, and proposed actions that reviewers can inspect together.&lt;/p&gt;
&lt;p&gt;Review configuration and proposed operations together: changing an address can alter identity, while unchanged files can still produce a plan after external changes.&lt;/p&gt;
&lt;h2 id="plan-is-a-proposal-not-a-transaction"&gt;Plan is a proposal, not a transaction&lt;/h2&gt;
&lt;p&gt;A normal plan reads managed objects through providers and compares the refreshed view with configuration. It proposes creation, updates, replacement, or deletion. Unknown values, provider behavior, permissions, and changes between planning and applying mean it cannot guarantee success or describe every eventual API call.&lt;/p&gt;
&lt;p&gt;Saving a plan lets you apply the reviewed proposal instead of automatically calculating another. It does not freeze the remote system. A stale state can invalidate the saved plan, and an apply can fail partway through. Protect saved plans and state as sensitive artifacts rather than posting their complete contents in public logs.&lt;/p&gt;
&lt;p&gt;After a successful apply, an unchanged, stable configuration should normally produce no further changes. Persistent differences deserve investigation: external controllers, computed attributes, provider bugs, and changing inputs can defeat that expectation. A clean plan is evidence about managed observations, not a complete inventory of the account.&lt;/p&gt;
&lt;h2 id="state-preserves-identity"&gt;State preserves identity&lt;/h2&gt;
&lt;p&gt;State binds configuration addresses to remote objects and retains other information Terraform needs. Losing that mapping does not immediately delete the objects. A later plan may propose new resources; whether applying creates duplicates or encounters name conflicts depends on the provider and service.&lt;/p&gt;
&lt;p&gt;First investigate backend versions and recoverable snapshots. When restoration is inappropriate or unavailable, import can associate supported existing objects with configuration addresses. Both the CLI workflow and configuration-driven import exist; this is not necessarily a sequence of manual commands. Reconcile the resource configuration and review the resulting plan before making changes.&lt;/p&gt;
&lt;p&gt;Shared state needs access control, recoverable history, and a backend with appropriate locking support. Not every backend locks. A lock acquisition failure stops the operation; waiting depends on command options such as the lock timeout. Avoid disabling locking to work around another legitimate run, and never interpret state version history alone as a complete infrastructure audit trail.&lt;/p&gt;
&lt;h2 id="drift-and-refactoring-are-different-decisions"&gt;Drift and refactoring are different decisions&lt;/h2&gt;
&lt;p&gt;For an already tracked resource changed outside Terraform, decide whether to keep the new value or restore the configured one. Keeping it generally means updating configuration; ordinary attribute drift does not require import. A refresh-only apply updates Terraform&amp;rsquo;s records without reconciling remote objects to configuration. It does not edit configuration, so a later normal plan can still propose reversal.&lt;/p&gt;
&lt;p&gt;Ignored attributes and provider schemas affect which differences produce actions. The console change may have its own audit logs; the operational failure is often that nobody linked the emergency action to the next deployment.&lt;/p&gt;
&lt;p&gt;Renaming an address without recording the relationship can produce a destroy/create proposal. A &lt;code&gt;moved&lt;/code&gt; block documents that relationship, including supported moves into modules, so a compatible refactor need not recreate the object. Other configuration changes can still require replacement. Removing a resource block generally proposes destruction unless you explicitly use an appropriate removal workflow.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;During an incident, somebody enlarges an instance and forgets to update configuration. An unrelated apply later proposes to shrink it. Require review of the relevant actions and record the intended lasting value. Exit code 2 means a nonempty plan, not specifically drift, danger, or failure.&lt;/p&gt;
&lt;p&gt;For an unexpected destroy, compare old and new addresses before approving anything. For apparently missing resources, verify the selected backend and workspace before assuming state was lost. Stop concurrent writers during recovery; a plausible-looking empty state is not permission to recreate the estate.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;
&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-terraform-and-infrastructure-as-code.detail.en.svg" alt="Configuration, observed objects and state yield a plan; destroy and create differ from an address move, while import, drift and backend locks remain separate." width="560" height="2236"&gt;
&lt;figcaption&gt;Terraform address identity: mechanism and evidence boundaries.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;With Terraform 1.4 or newer, use a fresh lab and reviewed CLI settings; inherited TF_CLI_ARGS, workspace and configuration can alter behavior. Run each command separately and stop on error: this shell block has no fail-fast guard. Its built-in &lt;code&gt;terraform_data&lt;/code&gt; changes local state, not cloud infrastructure.&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="nv"&gt;lab&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d&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;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&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;cat &amp;gt; main.tf &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;HCL&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;resource &amp;#34;terraform_data&amp;#34; &amp;#34;old&amp;#34; { input = &amp;#34;kept&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HCL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform init -input&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform validate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform apply -input&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; -auto-approve
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform state show terraform_data.old
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; main.tf &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;HCL&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;resource &amp;#34;terraform_data&amp;#34; &amp;#34;new&amp;#34; { input = &amp;#34;kept&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HCL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform plan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt;&amp;gt; main.tf &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;HCL&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;moved {
&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 = terraform_data.old
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; to = terraform_data.new
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HCL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform validate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform plan -out&lt;span class="o"&gt;=&lt;/span&gt;moved.tfplan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform apply -input&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; moved.tfplan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform state show terraform_data.new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform plan -detailed-exitcode
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The retained Terraform 1.13.3 review reports the unrecorded rename as one creation and one deletion; do not apply that intermediate plan. With &lt;code&gt;moved&lt;/code&gt;, it reports zero resource actions, the same ID, and final exit 0. This review did not rerun it. A move-only detailed plan can return 2 for an address change.&lt;/p&gt;
&lt;p&gt;This fixture exercises local Terraform planning and address migration. It does not test cloud-provider import, remote drift, or backend locking. In general, detailed exit codes mean 0 for no changes, 1 for error, and 2 for changes present; inspect the actions rather than treating 2 as failure.&lt;/p&gt;
&lt;p&gt;When finished, use &lt;code&gt;cd -&lt;/code&gt; to leave the lab, then remove only the generated directory named by &lt;code&gt;$lab&lt;/code&gt;; it contains disposable configuration, plans, and state.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state"&gt;Terraform: state&lt;/a&gt;, address bindings and state requirements, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/commands/plan"&gt;Terraform: plan command&lt;/a&gt;, planning modes, saved plans, lock options, and exit codes, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/remote"&gt;Terraform: remote state&lt;/a&gt;, shared storage and backend capabilities, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/locking"&gt;Terraform: state locking&lt;/a&gt;, backend support and lock failures, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/modules/develop/refactoring"&gt;Terraform: refactor modules&lt;/a&gt;, explicit moved mappings and compatible refactors, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/import"&gt;Terraform: import overview&lt;/a&gt;, configuration-driven adoption of existing resources, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/import"&gt;Terraform: CLI import&lt;/a&gt;, the alternative CLI import workflow, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/tutorials/state/resource-drift"&gt;Terraform: manage resource drift&lt;/a&gt;, refreshed observations and configuration reconciliation, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle"&gt;Terraform: lifecycle&lt;/a&gt;, ignored attributes and removal consequences, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/resources/terraform-data"&gt;Terraform: terraform_data&lt;/a&gt;, the built-in local lifecycle resource used by the exercise, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/commands/apply"&gt;Terraform: apply command&lt;/a&gt;, applying saved plans and partial failures, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/config/environment-variables"&gt;Terraform CLI environment variables&lt;/a&gt;, inherited CLI arguments, workspace and configuration, checked 2026-10-18.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>