<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>praneeth.me</title><description>Praneeth&apos;s lab notes on Entra ID, Intune and Conditional Access — what broke, what stuck, from the homelab.</description><link>https://praneeth.me/</link><item><title>This site isn&apos;t vibe coded — here&apos;s what I&apos;d call it instead</title><link>https://praneeth.me/log/vibe-coding-vs-ai-assisted-development/</link><guid isPermaLink="true">https://praneeth.me/log/vibe-coding-vs-ai-assisted-development/</guid><description>&quot;Vibe coding&quot; means shipping AI output nobody read. Why that&apos;s not what happened here, and what I&apos;d call it instead.</description><pubDate>Sun, 02 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Someone asked me recently whether this site was “vibe coded.” Fair question — AI helped write a lot of the code. But I don’t think that’s the right term, and the distinction is worth making.&lt;/p&gt;
&lt;h2 id=&quot;what-vibe-coding-actually-means&quot;&gt;What “vibe coding” actually means&lt;/h2&gt;
&lt;p&gt;When Andrej Karpathy introduced the phrase, he wasn’t describing every project built with AI. He was describing a particular way of working: prompt the model, accept whatever it generates, and keep moving without really reviewing or understanding the output. If something breaks, paste the error back in and let the AI have another go.&lt;/p&gt;
&lt;p&gt;The defining feature isn’t that AI wrote the code. It’s that nobody really checked it.&lt;/p&gt;
&lt;p&gt;That’s a perfectly reasonable approach for a quick prototype or a weekend experiment. But if you’re building something you’re going to publish for other people to use, the bar is higher. A public-facing website still needs someone to review the output, test it, question assumptions, and understand how it will behave once it’s out in the real world.&lt;/p&gt;
&lt;p&gt;Over time, “vibe coding” has been used more broadly to describe almost any AI-assisted coding workflow. I think that misses the important distinction. The interesting question isn’t whether AI was involved — it’s whether someone was still applying judgement throughout the process.&lt;/p&gt;
&lt;h2 id=&quot;what-building-this-site-actually-looked-like&quot;&gt;What building this site actually looked like&lt;/h2&gt;
&lt;p&gt;AI wrote a lot of the code. It also got plenty of things wrong.&lt;/p&gt;
&lt;p&gt;Some problems only showed up once I tested the site properly. Others meant reading documentation, comparing framework changes, or adding tooling so the same mistake couldn’t happen again. None of those were solved by blindly asking the AI to “fix it.” They were solved by understanding the problem first, deciding on the right approach, and then using AI as another tool in the process.&lt;/p&gt;
&lt;p&gt;That’s the difference I think gets lost.&lt;/p&gt;
&lt;p&gt;AI can generate code incredibly quickly, and it’s becoming increasingly capable. Regardless of whether the code came from me or from an AI, it’s still my responsibility to verify it’s the right solution before it ships.&lt;/p&gt;
&lt;h2 id=&quot;so-what-do-i-call-it&quot;&gt;So what do I call it?&lt;/h2&gt;
&lt;p&gt;There isn’t a universally accepted term yet.&lt;/p&gt;
&lt;p&gt;The one that makes the most sense to me is AI-assisted development.&lt;/p&gt;
&lt;p&gt;AI can write code, explain unfamiliar concepts, help investigate problems, and speed up repetitive work. But reviewing the output, testing it, deciding what to trust, and understanding why something should be built a certain way still matters.&lt;/p&gt;
&lt;p&gt;The label itself isn’t really the point, though.&lt;/p&gt;
&lt;p&gt;Whether you use AI a little or a lot doesn’t tell me much about how software was built. What matters is whether you’re willing to question the output, verify it, and take responsibility for the final result.&lt;/p&gt;
&lt;p&gt;That’s the difference between using AI as a shortcut and using it as a tool.&lt;/p&gt;</content:encoded></item><item><title>DMARC just had its first real rewrite since 2015 — here&apos;s what DMARCbis actually changes</title><link>https://praneeth.me/log/dmarc-rfc-9989-what-changed/</link><guid isPermaLink="true">https://praneeth.me/log/dmarc-rfc-9989-what-changed/</guid><description>RFC 9989 quietly replaced a decade of informational DMARC guidance with a real IETF standard. What changed, what got deprecated, and a DNSSEC interaction worth checking before you rely on the new np= tag.</description><pubDate>Thu, 23 Jul 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In May 2026 the IETF published &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9989.html&quot;&gt;RFC 9989&lt;/a&gt;, &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9990.html&quot;&gt;RFC 9990&lt;/a&gt; and &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9991.html&quot;&gt;RFC 9991&lt;/a&gt; — three documents that, between them, replace RFC 7489, the spec DMARC has run on since 2015. Unofficially this is being called “DMARCbis,” and if you manage DNS for a domain that sends mail — which, if you’re reading this, you probably do — it’s worth twenty minutes to understand what actually changed, because some of it changes how you should be writing your DMARC record today.&lt;/p&gt;
&lt;p&gt;I went looking into this while tightening up the DNS hygiene on this site’s own domain (praneeth.me runs on Cloudflare, DNSSEC on, the whole checklist), and ended up down a rabbit hole that’s more interesting than “here’s a new DNS tag.” So: what changed, why it changed, and one gotcha that most of the coverage I’ve read doesn’t mention.&lt;/p&gt;
&lt;h2 id=&quot;why-this-update-matters-more-than-it-sounds&quot;&gt;Why this update matters more than it sounds&lt;/h2&gt;
&lt;p&gt;The detail that actually matters isn’t in the technical changes — it’s in the paperwork. RFC 7489 was published via the IETF’s &lt;em&gt;Independent Submission Stream&lt;/em&gt;, tagged “Informational.” That’s a real distinction: it means DMARC, despite running a meaningful chunk of the world’s anti-phishing infrastructure for a decade, was never formally endorsed by the IETF. It was a widely-adopted spec that happened to live outside the standards process.&lt;/p&gt;
&lt;p&gt;RFC 9989 (the core protocol), RFC 9990 (aggregate reporting) and RFC 9991 (failure reporting) fix that. All three are Proposed Standards on the IETF Standards Track, the product of years of work by the IETF’s DMARC Working Group that picked up pace once the related ARC protocol was published (as Experimental RFC 8617) in 2019. It’s a bureaucratic upgrade more than a technical one — but it’s also the excuse the working group used to clean up a decade of accumulated cruft in the spec, which is where the actual changes come in.&lt;/p&gt;
&lt;h2 id=&quot;what-changed&quot;&gt;What changed&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Organisational domain discovery got a real algorithm.&lt;/strong&gt; The old spec leaned on the Public Suffix List — a community-maintained, not-quite-authoritative text file — to work out which domain “owns” a DMARC policy. DMARCbis replaces that with a &lt;strong&gt;DNS Tree Walk&lt;/strong&gt;: receivers query DNS hierarchically upward from the &lt;code&gt;From:&lt;/code&gt; address looking for a &lt;code&gt;_dmarc&lt;/code&gt; record, with built-in limits so a malicious domain can’t force a receiver into an unbounded chain of lookups — however many labels the &lt;code&gt;From:&lt;/code&gt; domain has, the walk never issues more than eight queries. Less reliance on an external list nobody formally governs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A handful of tags are gone, a few are new.&lt;/strong&gt;&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tag&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;th&gt;What happened to it&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;pct&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Removed&lt;/td&gt;&lt;td&gt;Percentage-based partial enforcement is gone entirely&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;rf&lt;/code&gt;, &lt;code&gt;ri&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Removed&lt;/td&gt;&lt;td&gt;Report format and interval are no longer sender-configurable&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;t&lt;/code&gt;&lt;/td&gt;&lt;td&gt;New&lt;/td&gt;&lt;td&gt;A boolean testing flag, replacing what &lt;code&gt;pct&lt;/code&gt; used to do&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;psd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;New&lt;/td&gt;&lt;td&gt;Lets a domain declare itself a &lt;em&gt;public suffix domain&lt;/em&gt; (RFC 9091’s experiment, standardised)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;np&lt;/code&gt;&lt;/td&gt;&lt;td&gt;New&lt;/td&gt;&lt;td&gt;Sets policy specifically for &lt;strong&gt;non-existent&lt;/strong&gt; subdomains&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;SPF’s role got clarified, not changed.&lt;/strong&gt; RFC 7489 already keyed DMARC’s SPF check to the &lt;code&gt;MAIL FROM:&lt;/code&gt; identity, but its wording left room for receivers to also count a standalone &lt;code&gt;HELO&lt;/code&gt;/&lt;code&gt;EHLO&lt;/code&gt; SPF pass toward DMARC. The new text closes that reading: DMARC “relies solely on SPF validation of the MAIL FROM identity.” Bounce handling is unaffected — an empty &lt;code&gt;MAIL FROM&lt;/code&gt; is still evaluated as &lt;code&gt;postmaster@&lt;/code&gt; the &lt;code&gt;HELO&lt;/code&gt; domain, exactly as RFC 7208 has always defined it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reporting got simpler and more private.&lt;/strong&gt; Aggregate reports were already XML in practice; RFC 9990 makes that a MUST. The removed &lt;code&gt;rf&lt;/code&gt; tag — which let senders request alternate &lt;em&gt;failure&lt;/em&gt;-report formats — is gone, and with &lt;code&gt;ri&lt;/code&gt; gone too, receivers now control their own reporting schedule rather than honouring a sender’s requested interval. RFC 9990 is also explicit that aggregate reports contain no identifying information about individual users — no personal mail addresses, no individual IPs, no message content — which matters given how many of them get forwarded to third-party analysis tools.&lt;/p&gt;
&lt;h2 id=&quot;the-tag-actually-worth-your-attention-np&quot;&gt;The tag actually worth your attention: &lt;code&gt;np=&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Of everything in DMARCbis, &lt;code&gt;np=&lt;/code&gt; is the one with real security value — though the gap it fills is subtler than most of the coverage suggests.&lt;/p&gt;
&lt;p&gt;Under the old spec, your &lt;code&gt;sp=&lt;/code&gt; tag (or &lt;code&gt;p=&lt;/code&gt;, if you never set one) applied to &lt;em&gt;every&lt;/em&gt; subdomain — ones you’d created and ones you hadn’t alike. So &lt;code&gt;billing.yourdomain.com&lt;/code&gt; and &lt;code&gt;payroll.yourdomain.com&lt;/code&gt; were never actually policy-free: mail forged from them fell back to your organisational domain’s record like anything else. The real gap was granularity. A domain mid-rollout at &lt;code&gt;p=none&lt;/code&gt; or &lt;code&gt;sp=quarantine&lt;/code&gt; — the standard cautious path — was forced to be exactly that lenient about subdomains that don’t exist and can never send legitimate mail. Phishing infrastructure lived happily in that leniency window.&lt;/p&gt;
&lt;p&gt;DMARCbis splits the two cases: &lt;code&gt;sp=&lt;/code&gt; now covers only subdomains that exist, and &lt;code&gt;np=&lt;/code&gt; covers the ones that don’t (if you don’t set it, non-existent subdomains fall back to &lt;code&gt;sp&lt;/code&gt;, then &lt;code&gt;p&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes min-light night-owl&quot; style=&quot;--shiki-light:#24292eff;--shiki-dark:#d6deeb;--shiki-light-bg:#ffffff;--shiki-dark-bg:#011627;overflow-x:auto;--file-name-offset:-0.75rem&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;_dmarc.example.com.  TXT  &quot;v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:dmarc@example.com&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the record where &lt;code&gt;np=&lt;/code&gt; earns its keep: your real subdomains stay at &lt;code&gt;quarantine&lt;/code&gt; while you finish rolling out, but mail claiming to come from a subdomain that doesn’t exist gets rejected outright, today. (Already at &lt;code&gt;sp=reject&lt;/code&gt;? Then &lt;code&gt;np=reject&lt;/code&gt; is harmless but redundant — the fallback gets you the same outcome.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;np=reject&lt;/code&gt; — the sensible setting for almost everyone: a non-existent subdomain sends zero legitimate mail &lt;em&gt;by definition&lt;/em&gt;, making this the rare enforcement tag where skipping the usual gradual ramp is actually defensible&lt;/li&gt;
&lt;li&gt;&lt;code&gt;np=quarantine&lt;/code&gt; — routed to spam instead&lt;/li&gt;
&lt;li&gt;&lt;code&gt;np=none&lt;/code&gt; — monitor only, if you’d rather watch a reporting cycle first&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-gotcha-np-and-dnssec-dont-always-agree-on-what-non-existent-means&quot;&gt;The gotcha: &lt;code&gt;np=&lt;/code&gt; and DNSSEC don’t always agree on what “non-existent” means&lt;/h2&gt;
&lt;p&gt;Here’s the part I haven’t seen covered much, and it’s directly relevant if you’re on Cloudflare, Route 53, Azure DNS or NS1 — which between them is a large share of everyone.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;np=&lt;/code&gt; is defined against a DNS receiver detecting an &lt;code&gt;NXDOMAIN&lt;/code&gt; response — the standard “this name doesn’t exist” answer. But a lot of DNSSEC-signed zones don’t actually return &lt;code&gt;NXDOMAIN&lt;/code&gt; anymore. &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9824.html&quot;&gt;RFC 9824&lt;/a&gt; defines &lt;strong&gt;“compact denial of existence,”&lt;/strong&gt; a technique where the authoritative nameserver instead returns &lt;code&gt;NOERROR&lt;/code&gt;, with a single &lt;code&gt;NSEC&lt;/code&gt; record carrying an &lt;code&gt;NXNAME&lt;/code&gt; bit to signal non-existence — cheaper to compute and smaller to transmit than the multi-record proof a traditional &lt;code&gt;NXDOMAIN&lt;/code&gt; response needs under DNSSEC.&lt;/p&gt;
&lt;p&gt;The problem: a DMARC implementation that strictly checks for &lt;code&gt;NXDOMAIN&lt;/code&gt; (as the spec assumes) will see &lt;code&gt;NOERROR&lt;/code&gt; from a compact-denial provider and conclude the subdomain &lt;strong&gt;exists&lt;/strong&gt; — silently skipping your &lt;code&gt;np=&lt;/code&gt; policy for every non-existent subdomain, on every domain hosted there.&lt;/p&gt;
&lt;p&gt;And this isn’t a niche configuration. A &lt;a href=&quot;https://dmarcwise.io/blog/dmarc-np-incompatibility-with-dnssec&quot;&gt;December 2024 measurement of the top million domains&lt;/a&gt; found compact denial in use on roughly &lt;strong&gt;44% of the ~73,000 domains with valid DNSSEC&lt;/strong&gt; — it’s the default at Cloudflare (since 2016), NS1 (2019), AWS Route 53 (2020) and Azure DNS (2025). If your DNS is DNSSEC-signed on any of those, &lt;code&gt;np=reject&lt;/code&gt; may not actually be doing anything until receivers catch up.&lt;/p&gt;
&lt;p&gt;The fix has to happen on the receiving side: implementations need to also recognise the &lt;code&gt;NXNAME&lt;/code&gt; bit on &lt;code&gt;NSEC&lt;/code&gt;/&lt;code&gt;NSEC3&lt;/code&gt; records as proof of non-existence, not just a literal &lt;code&gt;NXDOMAIN&lt;/code&gt; response code. Some already do. Plenty don’t yet. There’s no sender-side workaround — you can’t opt out of your DNS provider’s denial-of-existence method — so the honest answer right now is: &lt;strong&gt;treat &lt;code&gt;np=reject&lt;/code&gt; as “probably helping,” not “definitely enforced,”&lt;/strong&gt; until you’ve confirmed your receiving mail providers handle compact denial correctly. Worth re-checking every few months rather than setting and forgetting.&lt;/p&gt;
&lt;h2 id=&quot;what-id-actually-do-with-this&quot;&gt;What I’d actually do with this&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Drop &lt;code&gt;pct&lt;/code&gt; from any record that still has it&lt;/strong&gt; — DMARCbis receivers ignore it entirely, so a &lt;code&gt;pct=25&lt;/code&gt; you were relying on quietly becomes full enforcement as receivers update. If you still need a soft rollout, that’s what the new &lt;code&gt;t=y&lt;/code&gt; flag is for.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add &lt;code&gt;np=reject&lt;/code&gt; even if the rest of your record is still mid-rollout&lt;/strong&gt; — it only governs subdomains that don’t exist, which can’t send legitimate mail, so the usual none → quarantine → reject ramp buys you little here. If you’d rather be cautious anyway, one reporting cycle at &lt;code&gt;np=none&lt;/code&gt; costs nothing — just don’t let it linger the way &lt;code&gt;p=none&lt;/code&gt; tends to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t assume &lt;code&gt;np=reject&lt;/code&gt; is airtight&lt;/strong&gt; if your DNS runs on Cloudflare, Route 53, Azure DNS or NS1 with DNSSEC on — it’s a real layer of protection once receivers support &lt;code&gt;NXNAME&lt;/code&gt; detection, but it’s not the hard guarantee the tag name implies today.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ignore any coverage telling you to re-test your bounce handling&lt;/strong&gt; — the null-&lt;code&gt;MAIL FROM&lt;/code&gt; behaviour didn’t change. An empty &lt;code&gt;MAIL FROM&lt;/code&gt; is still evaluated as &lt;code&gt;postmaster@&lt;/code&gt; the &lt;code&gt;HELO&lt;/code&gt; domain, exactly as RFC 7208 always specified; what DMARCbis shut down was only the looser reading that a standalone &lt;code&gt;HELO&lt;/code&gt; SPF pass could satisfy DMARC on its own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this requires touching a record you’re happy with today — existing records keep parsing fine, since unknown tags have always been ignored by design and no surviving tag changed meaning. (The one caveat is &lt;code&gt;pct&lt;/code&gt;, above.) But if you’re setting up DMARC fresh, or tightening an existing policy, it’s worth writing the record against the new spec rather than a decade-old one that just got formally retired.&lt;/p&gt;</content:encoded></item></channel></rss>