<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ricco020</title>
    <description>The latest articles on DEV Community by ricco020 (@ricco020).</description>
    <link>https://dev.to/ricco020</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960216%2Fbc01eb94-de65-41c2-9767-966a157976ae.jpeg</url>
      <title>DEV Community: ricco020</title>
      <link>https://dev.to/ricco020</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ricco020"/>
    <language>en</language>
    <item>
      <title>Three of the eight sites that support /.well-known/change-password do not support it</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:31:06 +0000</pubDate>
      <link>https://dev.to/ricco020/three-of-the-eight-sites-that-support-well-knownchange-password-do-not-support-it-251p</link>
      <guid>https://dev.to/ricco020/three-of-the-eight-sites-that-support-well-knownchange-password-do-not-support-it-251p</guid>
      <description>&lt;p&gt;There is a small W3C standard called &lt;a href="https://w3c.github.io/webappsec-change-password-url/" rel="noopener noreferrer"&gt;A Well-Known URL for Changing Passwords&lt;/a&gt;. A site serves &lt;code&gt;/.well-known/change-password&lt;/code&gt;, redirects it to its real change-password page, and password managers can send a user straight there instead of making them hunt through account settings.&lt;/p&gt;

&lt;p&gt;I checked 24 well known sites for it tonight. The interesting part is not how many support it. It is that &lt;strong&gt;three of the eight apparent supporters do not support it at all&lt;/strong&gt;, and the specification predicted exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap is written into the spec
&lt;/h2&gt;

&lt;p&gt;A naive check is "does &lt;code&gt;/.well-known/change-password&lt;/code&gt; return 200". That check is wrong, because plenty of servers return 200 for &lt;em&gt;everything&lt;/em&gt;. A SPA with a catch-all route will happily serve its shell for any path you invent.&lt;/p&gt;

&lt;p&gt;So the spec includes a countermeasure. Section 5 tells clients to also request a URL that should never exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.well-known/resource-that-should-not-exist-whatever-forty-two
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns 200 too, the server is not answering your question. It is just saying 200 at everything. The support signal is void.&lt;/p&gt;

&lt;p&gt;That is a rare thing to find in a standard: the authors anticipated the false positive and shipped the control with the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;Two requests per host, one for the standard path and one for the control path, real HTTP, browser User-Agent, 24 hosts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supports it for real   5 / 24     change-password 200, control 404
false positive         3 / 24     change-password 200, control 200
no support            16 / 24     404 (13), 403 (2), 400 (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github.com        200 / 404
reddit.com        200 / 404
wordpress.com     200 / 404
apple.com         200 / 404
1password.com     200 / 404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;False positives, which a 200-only check would have counted as wins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;twitter.com       200 / 200
netflix.com       200 / 200
linkedin.com      200 / 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the control request I would have reported 8 supporters instead of 5. That is a 60 % overcount, from a check that never threw an error and looked green all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I did not expect
&lt;/h2&gt;

&lt;p&gt;I included six password managers, since this standard exists mainly for their benefit. They are the consumers of the signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1password.com     supports it
bitwarden.com     404
dashlane.com      404
proton.me         404
lastpass.com      404
nordpass.com      404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One out of six serves the standard that password managers rely on. I am not going to pretend that is a scandal, these are marketing domains and the account systems often live elsewhere. But if you are building anything that touches &lt;a href="https://www.pwdfortress.com/en/blog/are-passkeys-safe" rel="noopener noreferrer"&gt;password and passkey hygiene&lt;/a&gt;, it is a useful reminder that ecosystem support is thinner than the existence of a spec suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to add it
&lt;/h2&gt;

&lt;p&gt;It is genuinely a two line change on most stacks. Redirect &lt;code&gt;/.well-known/change-password&lt;/code&gt; to your existing change-password page with a 302, and make sure your 404 handler actually returns 404 for unknown paths under &lt;code&gt;/.well-known/&lt;/code&gt;. That second half is the part people get wrong, and it is the half that makes the first half detectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;I have spent a lot of this week finding checks that returned a confident answer to a question slightly different from the one I meant to ask. This is the same shape, with one difference: here the spec authors saw it coming and gave you the control request for free.&lt;/p&gt;

&lt;p&gt;Most of the time nobody hands you the control. You have to think of it yourself, which usually means asking "what would this check return if the thing I am testing were completely absent?" If the answer is "the same thing", the check is decorative.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>standards</category>
      <category>testing</category>
    </item>
    <item>
      <title>Google said it had never heard of my URL. The URL was fine, my probe was not.</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:23:06 +0000</pubDate>
      <link>https://dev.to/ricco020/google-said-it-had-never-heard-of-my-url-the-url-was-fine-my-probe-was-not-53gg</link>
      <guid>https://dev.to/ricco020/google-said-it-had-never-heard-of-my-url-the-url-was-fine-my-probe-was-not-53gg</guid>
      <description>&lt;p&gt;Last night I ran Google's URL Inspection API against a site I run and got this back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;coverageState &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Google&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ne&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reconnait&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cette&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;URL'&lt;/span&gt;
&lt;span class="na"&gt;lastCrawl     &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JAMAIS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Google has never heard of this URL." On a page that had been in the sitemap for weeks.&lt;/p&gt;

&lt;p&gt;I was about to open a very large investigation. The page was fine. My probe was not. Here are the three ways that API lied to me in one sitting, and the one number that finally told me something true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: www and non-www are different URLs to this API
&lt;/h2&gt;

&lt;p&gt;I passed &lt;code&gt;https://save-my-disk.com/fr/blog/carte-sd-non-detectee&lt;/code&gt;. The sitemap declares &lt;code&gt;https://www.save-my-disk.com/...&lt;/code&gt;. Same site to a human. Two different URLs to the API.&lt;/p&gt;

&lt;p&gt;The result is not an error. It is not a 404. It is a confident, well-formed answer to a question I did not mean to ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;without www &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Google&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ne&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reconnait&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cette&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;URL'&lt;/span&gt;   &lt;span class="s"&gt;lastCrawl JAMAIS&lt;/span&gt;
&lt;span class="na"&gt;with www    &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Envoyee&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;et&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;indexee'&lt;/span&gt;                  &lt;span class="s"&gt;lastCrawl 2026-07-18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same page. Indexed the whole time.&lt;/p&gt;

&lt;p&gt;The rule I now follow: &lt;strong&gt;never compose the URL you inspect. Read it out of the sitemap you actually serve.&lt;/strong&gt; I fetch &lt;code&gt;sitemap.xml&lt;/code&gt;, grep the slug, and inspect that exact string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the &lt;code&gt;indexed&lt;/code&gt; field on sitemaps is zero for everyone
&lt;/h2&gt;

&lt;p&gt;The Sitemaps endpoint returns a &lt;code&gt;contents&lt;/code&gt; block that looks like it answers the only question you care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"submitted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3258&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"indexed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;indexed: 0&lt;/code&gt; is not a measurement. I have a site doing 1677 impressions a month reporting &lt;code&gt;indexed: 0&lt;/code&gt;. Every property I own reports &lt;code&gt;indexed: 0&lt;/code&gt;. If you build a dashboard on that field you will build an alarm that fires forever.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;errors&lt;/code&gt; and &lt;code&gt;warnings&lt;/code&gt; from the same block. Those are real. Ignore &lt;code&gt;indexed&lt;/code&gt; and inspect URLs individually if you want indexing state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: two states that read alike and mean opposite things
&lt;/h2&gt;

&lt;p&gt;This is the one that matters, and it is the reason the whole exercise was worth it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Discovered - currently not indexed"&lt;/strong&gt; means Googlebot knows the URL exists and &lt;strong&gt;has never fetched it&lt;/strong&gt;. &lt;code&gt;lastCrawl&lt;/code&gt; is empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Crawled - currently not indexed"&lt;/strong&gt; means Googlebot &lt;strong&gt;fetched the page, read it, and declined to index it&lt;/strong&gt;. &lt;code&gt;lastCrawl&lt;/code&gt; has a date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They sit next to each other in the same report. They call for opposite work. The first is a crawl budget and discovery problem: more internal links, sitemap pings, authority. The second is a quality or duplication judgment: the page was seen and found wanting, and adding more pages like it makes things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;I sampled 14 article URLs at random from a 590-page corpus on &lt;a href="https://www.save-my-disk.com/en/blog/sd-card-not-showing-up" rel="noopener noreferrer"&gt;a data recovery site I run&lt;/a&gt;, and inspected each one. I validated the probe on a known-healthy control page first, which came back "Submitted and indexed" as expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 4/14   Submitted and indexed                      29 %
 4/14   Google does not recognise this URL     |
 4/14   Discovered - currently not indexed     |   8/14 with lastCrawl = never
 2/14   Crawled - currently not indexed            fetched, then declined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight of fourteen pages had never been fetched at all. Only two had been read and rejected.&lt;/p&gt;

&lt;p&gt;I had spent the previous hour preparing to write another article for that site. The measurement says that would have produced a 657th URL that Googlebot has never visited. The bottleneck is not the writing. It is that two thirds of what already exists has never been looked at.&lt;/p&gt;

&lt;p&gt;That reframes the work entirely: internal linking, sitemap hygiene and off-site authority, instead of publishing volume. Not a conclusion I wanted, which is usually a sign it is the real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell past me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the URL out of the artifact you serve. Do not build it from a folder name or a slug you remember.&lt;/li&gt;
&lt;li&gt;Run any new probe against a page you already know is healthy, &lt;strong&gt;before&lt;/strong&gt; you trust it on the page you are worried about.&lt;/li&gt;
&lt;li&gt;When a check returns zero on everything it looks at, suspect the check first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The API is not wrong in any of these cases. It answers precisely the question you asked. The gap between that question and the one you meant is where the hour goes.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>googlecloud</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Eight checks lied to me in one day, and none of them threw an error</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:11:19 +0000</pubDate>
      <link>https://dev.to/ricco020/eight-checks-lied-to-me-in-one-day-and-none-of-them-threw-an-error-18m1</link>
      <guid>https://dev.to/ricco020/eight-checks-lied-to-me-in-one-day-and-none-of-them-threw-an-error-18m1</guid>
      <description>&lt;p&gt;In one working day I wrote or relied on eight checks that were wrong. &lt;strong&gt;Not one of them threw an&lt;br&gt;
error.&lt;/strong&gt; Every single one returned a plausible answer, which is exactly why each cost me time.&lt;/p&gt;

&lt;p&gt;Here they are, with what they actually returned.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four ways a check can lie
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. It never runs
&lt;/h3&gt;

&lt;p&gt;I wrote assertions that validate prices against the strings shown to users, then deliberately&lt;br&gt;
corrupted one to confirm the build would fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sabotaged price string
BUILD EXIT=0     &amp;lt;- the guard saw nothing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assertions run on import. &lt;strong&gt;Nothing imported the module yet.&lt;/strong&gt; After wiring it into a page that&lt;br&gt;
is actually built, the same sabotage gave &lt;code&gt;EXIT=1&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A check nothing calls protects nothing, and it is &lt;strong&gt;worse&lt;/strong&gt; than no check, because you believe you&lt;br&gt;
are covered.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  2. It caches a stale verdict
&lt;/h3&gt;

&lt;p&gt;A crawler wrote &lt;code&gt;sitemap_absent: true&lt;/code&gt; on the first pass. I fixed the fetch, the data came back&lt;br&gt;
correctly, and the report still printed &lt;strong&gt;"SITEMAP NOT SERVED"&lt;/strong&gt; — because the flag was never&lt;br&gt;
cleared.&lt;/p&gt;

&lt;p&gt;The JSON on disk said 445 URLs. The screen said absent. &lt;strong&gt;I only caught it by opening the artefact&lt;br&gt;
instead of reading the summary.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. It asks for the wrong shape
&lt;/h3&gt;

&lt;p&gt;Counting rendered questions on a page returned &lt;strong&gt;0&lt;/strong&gt;. I nearly reported that a paid feature shipped&lt;br&gt;
nothing.&lt;/p&gt;

&lt;p&gt;My selectors looked for DOM classes. The items were &lt;strong&gt;props of a client component&lt;/strong&gt;, serialised into&lt;br&gt;
the framework payload as &lt;code&gt;{"q": …}&lt;/code&gt;. The page was 16,908 bytes in both measurements. &lt;strong&gt;The content&lt;br&gt;
was always there; only my way of counting was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix was not a better guess. It was reading the type definition and counting what the code&lt;br&gt;
actually emits.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. It includes itself in its own sample
&lt;/h3&gt;

&lt;p&gt;Two on the same day, and this is my favourite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"next start"&lt;/span&gt;
&lt;span class="c"&gt;# returns only the bash -c wrapper that contains the pattern&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old server was still holding the port under a different process name. I found it with&lt;br&gt;
&lt;code&gt;ss -ltnp&lt;/code&gt; — &lt;strong&gt;searching for who holds the port always works; searching for a name you assume it has&lt;br&gt;
does not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then, checking whether a new slug collided with existing ones, I globbed the directory — including&lt;br&gt;
the file I had just written. It saw itself and answered &lt;strong&gt;"already taken."&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A probe that includes itself in its own sample always confirms what it is looking for.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The two failure modes, and why calibration is the whole job
&lt;/h2&gt;

&lt;p&gt;A check can fail in exactly two directions, and I hit both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;too weak   counting accented characters       -&amp;gt; a text with a few accents passes while being wrong
too strong requiring a fixed list of words    -&amp;gt; cries on correct text that had no reason to use them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The German page was correct. My check demanded words the article never needed, and shouted anyway.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A false alarm that repeats gets ignored, and an ignored check protects nothing.&lt;/strong&gt; That is the&lt;br&gt;
quietest way to lose a safeguard: it stays in the code, it goes green in the reader's head, and it&lt;br&gt;
stops saying anything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The calibrated version turned out to be: &lt;strong&gt;a non-zero count of language-specific characters AND a&lt;br&gt;
few witness words present — not all of them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now, and it costs three minutes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Test the guard by making it fail on purpose.&lt;/strong&gt; Not once written, but every time it is written.&lt;br&gt;
Sabotage the input, confirm it screams, restore. Two of my checks passed that test only on the second&lt;br&gt;
attempt, and I would never have known otherwise.&lt;/p&gt;

&lt;p&gt;Two other habits that came out of the same day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the artefact, not the summary.&lt;/strong&gt; The JSON, the manifest, the served HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove identity, not just presence.&lt;/strong&gt; "The server responds" is not "my version responds." A
deployment I thought I was testing had never started; the old process answered &lt;code&gt;200&lt;/code&gt; all along.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the same shape as a thing I ended up writing about elsewhere: a VPN kill switch&lt;br&gt;
toggle being on tells you what the app intends, not what the operating system will do when the tunnel&lt;br&gt;
dies at an awkward moment. I wrote up the four tests that actually settle it&lt;br&gt;
&lt;a href="https://www.vpnsmith.com/en/blog/vpn-kill-switch-test-2026" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eight wrong checks, zero errors raised.&lt;/strong&gt; A check that crashes gets fixed. A check that answers&lt;br&gt;
slightly the wrong question gets believed.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>debugging</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Two traps in the Bing Webmaster API that silently invert your site comparison</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:05:00 +0000</pubDate>
      <link>https://dev.to/ricco020/two-traps-in-the-bing-webmaster-api-that-silently-invert-your-site-comparison-57om</link>
      <guid>https://dev.to/ricco020/two-traps-in-the-bing-webmaster-api-that-silently-invert-your-site-comparison-57om</guid>
      <description>&lt;p&gt;I spent a night comparing two sites in the Bing Webmaster API and reached three different&lt;br&gt;
conclusions, each one demolishing the last. All three came from the same two traps. Neither raises&lt;br&gt;
an error. Neither shows up in the JSON. Both silently invert the comparison.&lt;/p&gt;

&lt;p&gt;Figures below are anonymised, because the ratios are the point, not whose sites they are.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 1: &lt;code&gt;GetCrawlStats&lt;/code&gt; history length is per site, and it is not the same
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site A   76 data points   2026-06-05 .. 2026-08-19
site B    7 data points   2026-08-13 .. 2026-08-19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I wrote the obvious thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CrawledPages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For site A that sums 30 days. &lt;strong&gt;For site B it sums 7&lt;/strong&gt;, because there are only 7. No error, no&lt;br&gt;
warning, just a number that is roughly a quarter of what I thought I was comparing.&lt;/p&gt;

&lt;p&gt;My conclusion was "site B is crawled nine times less". Normalised per day, &lt;strong&gt;site B is crawled&lt;br&gt;
more&lt;/strong&gt;: 168 pages/day against 121.&lt;/p&gt;

&lt;p&gt;The cause is that Bing does not backfill. A property verified last week has a week of history, not&lt;br&gt;
a week of low activity. &lt;strong&gt;A short history looks exactly like a quiet site if you only read the&lt;br&gt;
sum.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# what it should have been
&lt;/span&gt;&lt;span class="n"&gt;per_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CrawledPages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# and print it, always
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trap 2: &lt;code&gt;GetPageStats&lt;/code&gt; covers a different date range per site, and never tells you
&lt;/h2&gt;

&lt;p&gt;This one is worse, because there is no date parameter to get wrong. You call it, you get rows, each&lt;br&gt;
row has a &lt;code&gt;Date&lt;/code&gt;. I never looked at those dates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site A   GetPageStats covers  2026-06-05 .. 2026-06-19    (15 days, in JUNE)
site B   GetPageStats covers  2026-08-14 .. 2026-08-14    (ONE day, in AUGUST)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had been comparing &lt;strong&gt;June against August, fifteen days against one&lt;/strong&gt;, and calling it a finding&lt;br&gt;
about the sites. The whole analysis, and the follow-up work it justified, rested on that.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check that caught it, and why it was written first
&lt;/h2&gt;

&lt;p&gt;Before running any of this I had written down one rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If &lt;code&gt;GetPageStats&lt;/code&gt; total impressions differ from &lt;code&gt;GetRankAndTrafficStats&lt;/code&gt; over the same window, the&lt;br&gt;
endpoint is sampling and the page counts are worthless. Stop there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The measurement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site A   PageStats 12 impressions   RankAndTraffic 10   -&amp;gt;  120 % coverage
site B   PageStats 714 impressions  RankAndTraffic 325  -&amp;gt;  220 % coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two endpoints, same property, same window, and they do not agree with each other. So the check&lt;br&gt;
fired and I stopped, instead of building one more conclusion on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing that rule down before measuring is the only reason I caught it.&lt;/strong&gt; I had also written a&lt;br&gt;
"reserve" in the earlier analysis, hedging that the average was fragile on a small sample. That&lt;br&gt;
hedge was aimed at the right object for the wrong reason: the problem was never the average, it was&lt;br&gt;
the window. &lt;strong&gt;A smart-sounding caveat can cover a defect without naming it, and reassure you into&lt;br&gt;
keeping the wrong number.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone using this API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Print the date range and the row count of every response&lt;/strong&gt; before you compute anything. Not in
debug mode. Always.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never sum across sites without dividing by the number of points.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check two endpoints on the same window.&lt;/strong&gt; If they disagree, neither is a total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the invalidation condition before you measure&lt;/strong&gt;, not after you like the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is specific to Bing. GSC anonymises rare queries, so summing by query dimension covered&lt;br&gt;
only 19 % of impressions on one of my properties, and comparing two sites that way &lt;strong&gt;reversed their&lt;br&gt;
ranking&lt;/strong&gt;. Same shape of error: an API answered exactly what I asked, and I had asked something&lt;br&gt;
slightly different from what I meant.&lt;/p&gt;

&lt;p&gt;The sites in the table are mine; one of them is &lt;a href="https://www.vpnsmith.com/en/blog/cheapest-vps-wireguard-vpn-2026" rel="noopener noreferrer"&gt;VPNSmith&lt;/a&gt;,&lt;br&gt;
where the traffic data came from. I am not going to publish their raw numbers, which is why the&lt;br&gt;
ratios above are anonymised.&lt;/p&gt;

&lt;p&gt;If you have a comparison in a dashboard right now that spans two properties, check that both sides&lt;br&gt;
cover the same dates. Mine did not, for three hours, and it was confident the whole time.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>api</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Two thirds of VPN vendors ship no Content-Security-Policy. Including, sort of, us.</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Thu, 20 Aug 2026 21:38:33 +0000</pubDate>
      <link>https://dev.to/ricco020/two-thirds-of-vpn-vendors-ship-no-content-security-policy-including-sort-of-us-i8</link>
      <guid>https://dev.to/ricco020/two-thirds-of-vpn-vendors-ship-no-content-security-policy-including-sort-of-us-i8</guid>
      <description>&lt;p&gt;I scanned the public homepages of thirty commercial VPN vendors for the six HTTP response headers&lt;br&gt;
that browsers actually act on. One GET each, 2026-08-20, 29 of 30 hosts answering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mean score 3.66 / 6

Strict-Transport-Security   26/29   90%
X-Content-Type-Options      25/29   86%
X-Frame-Options             23/29   79%
Referrer-Policy             14/29   48%
Content-Security-Policy     10/29   34%   &amp;lt;-- the interesting one
Permissions-Policy           8/29   28%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two out of three companies selling security do not ship a Content-Security-Policy.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why that particular header
&lt;/h2&gt;

&lt;p&gt;HSTS is one line in a config file and every hosting guide mentions it. That is why it is at 90%.&lt;/p&gt;

&lt;p&gt;CSP is the one that costs real work, because writing a useful one means auditing every script the&lt;br&gt;
marketing department has added over the years. It is the header that separates "we followed a&lt;br&gt;
checklist" from "someone owns this".&lt;/p&gt;

&lt;p&gt;Full scores (6/6): cryptostorm.is, mullvad.net, nordvpn.com, torguard.net, windscribe.com.&lt;br&gt;
One vendor scored 0/6.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that makes this post worth writing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;We are in the 34%, and our CSP is close to worthless.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;script-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-inline'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-eval'&lt;/span&gt; &lt;span class="s"&gt;https:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inline scripts allowed, &lt;code&gt;eval&lt;/code&gt; allowed, script from any HTTPS host allowed. Against the injection&lt;br&gt;
attacks a CSP exists to stop, that policy blocks very nearly nothing. We ship the header. We do not&lt;br&gt;
ship the defence.&lt;/p&gt;

&lt;p&gt;Which is the actual finding, and the reason it is printed on the dataset itself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Presence is not correctness.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ten vendors have a CSP. I did not check how many of those ten have a CSP worth having, because that&lt;br&gt;
means reading each policy line by line. That work is not done, so the claim is not made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated up front
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One observation, one moment.&lt;/strong&gt; Headers change, sometimes weekly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One vantage point.&lt;/strong&gt; A CDN can inject or strip headers per edge node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homepage only.&lt;/strong&gt; Checkout and account pages matter more and are not in this scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presence, not correctness.&lt;/strong&gt; As above. It is the main way to misread the whole table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The vendor list, the six header names and the scoring rule are published as a dataset under&lt;br&gt;
&lt;a href="https://zenodo.org/records/22032840" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.22032840&lt;/a&gt;, so you can disagree with my&lt;br&gt;
reading without taking my word for the numbers. Any HTTP client reproduces it in a few minutes, and&lt;br&gt;
differences from this snapshot are expected and informative in themselves.&lt;/p&gt;

&lt;p&gt;The longer write-up, including why a header scan tells you nothing at all about whether a VPN&lt;br&gt;
service is trustworthy, is on &lt;a href="https://www.vpnsmith.com/en/blog/is-nordvpn-safe-header-scan" rel="noopener noreferrer"&gt;VPNSmith&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run any public site: check your own &lt;code&gt;script-src&lt;/code&gt; before you feel good about having a CSP. I&lt;br&gt;
did, and that is how this post got its most useful paragraph.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitHub says these three repos have no licence. They all do.</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Thu, 20 Aug 2026 01:52:17 +0000</pubDate>
      <link>https://dev.to/ricco020/github-says-these-three-repos-have-no-licence-they-all-do-1nl8</link>
      <guid>https://dev.to/ricco020/github-says-these-three-repos-have-no-licence-they-all-do-1nl8</guid>
      <description>&lt;p&gt;GitHub puts a licence label on every repository. It reads it from the &lt;code&gt;LICENSE&lt;/code&gt; file and&lt;br&gt;
normalises it to an SPDX identifier — &lt;code&gt;MIT&lt;/code&gt;, &lt;code&gt;BSD-3-Clause&lt;/code&gt;, &lt;code&gt;Apache-2.0&lt;/code&gt;. When it cannot,&lt;br&gt;
the API returns &lt;code&gt;NOASSERTION&lt;/code&gt; and the web UI quietly says &lt;strong&gt;"Other"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I checked twelve popular proxy, VPN and mesh-networking repositories through the GitHub API&lt;br&gt;
on 20 August 2026. Three came back &lt;code&gt;NOASSERTION&lt;/code&gt;. Here is what each of the three actually&lt;br&gt;
ships — because "Other" is not one situation, it is three very different ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;SPDX from the API&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SagerNet/sing-box&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NOASSERTION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;37,141&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XTLS/Xray-core&lt;/td&gt;
&lt;td&gt;MPL-2.0&lt;/td&gt;
&lt;td&gt;41,121&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;juanfont/headscale&lt;/td&gt;
&lt;td&gt;BSD-3-Clause&lt;/td&gt;
&lt;td&gt;43,009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tailscale/tailscale&lt;/td&gt;
&lt;td&gt;BSD-3-Clause&lt;/td&gt;
&lt;td&gt;35,368&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2fly/v2ray-core&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;34,491&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wireguard/wireguard-go&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;4,347&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;apernet/hysteria&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;22,355&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAimTY/tuic&lt;/td&gt;
&lt;td&gt;GPL-3.0&lt;/td&gt;
&lt;td&gt;3,261&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudflare/cloudflared&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;15,304&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;netbirdio/netbird&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NOASSERTION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;28,520&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zerotier/ZeroTierOne&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NOASSERTION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;17,029&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slackhq/nebula&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;17,630&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three out of twelve. Now the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. sing-box — GPLv3, plus a clause of its own
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;LICENSE&lt;/code&gt; file opens with the standard GPL version 3 or later grant. Then it adds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In addition, no derivative work may use the name or imply association with this&lt;br&gt;
application without prior consent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is why the label cannot be &lt;code&gt;GPL-3.0&lt;/code&gt;: the text is no longer the unmodified&lt;br&gt;
licence. The grant is still copyleft, with a naming restriction bolted on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. netbird — BSD-3-Clause, except where it is AGPLv3
&lt;/h2&gt;

&lt;p&gt;Its &lt;code&gt;LICENSE&lt;/code&gt; file says it plainly in the first two lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This BSD-3-Clause license applies to all parts of the repository except for the&lt;br&gt;
directories [...] Those directories are licensed under the GNU Affero General Public&lt;br&gt;
License version 3.0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A permissive licence for most of the tree, and the strongest network copyleft there is for&lt;br&gt;
part of it. One SPDX identifier cannot express that, and picking the friendlier of the two&lt;br&gt;
is exactly the mistake the label would encourage.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ZeroTier — MPL, and a &lt;code&gt;nonfree/&lt;/code&gt; directory
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LICENSE.txt&lt;/code&gt; is three lines, and the second one is the one that matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See nonfree/LICENSE.md for all non-free ("source available") portions of this repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source-available is not open source. A repository can be mostly MPL and still contain a&lt;br&gt;
directory you may read but not freely reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NOASSERTION&lt;/code&gt; does not mean "no licence" and it does not mean "unclear". It means &lt;strong&gt;GitHub&lt;br&gt;
declined to summarise this one in a single word&lt;/strong&gt;, and in all three cases it was right to&lt;br&gt;
decline. An extra clause, a per-directory split, and a non-free carve-out are all things&lt;br&gt;
that matter the moment you ship something built on top.&lt;/p&gt;

&lt;p&gt;The practical rule is boring and holds up: &lt;strong&gt;if you are redistributing, open the &lt;code&gt;LICENSE&lt;/code&gt;&lt;br&gt;
file.&lt;/strong&gt; The badge is a convenience, not a statement of terms. Two minutes of reading, and&lt;br&gt;
the three repositories above stop being surprises.&lt;/p&gt;

&lt;p&gt;I went down this road while comparing two of these cores in detail — the licence turned out&lt;br&gt;
to be the sharpest difference between them, well ahead of the protocol lists everyone else&lt;br&gt;
compares:&lt;br&gt;
&lt;a href="https://www.vpnsmith.com/en/blog/sing-box-vs-xray-2026" rel="noopener noreferrer"&gt;sing-box vs Xray: the differences that actually decide it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All figures read from the GitHub REST API on 20 August 2026; star counts move, the licence&lt;br&gt;
files are the stable part.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>go</category>
      <category>licensing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your sysctl says one thing, the kernel says another</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 19 Aug 2026 05:06:12 +0000</pubDate>
      <link>https://dev.to/ricco020/your-sysctl-says-one-thing-the-kernel-says-another-4gha</link>
      <guid>https://dev.to/ricco020/your-sysctl-says-one-thing-the-kernel-says-another-4gha</guid>
      <description>&lt;p&gt;You set &lt;code&gt;net.ipv4.ip_forward=1&lt;/code&gt; in &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;. You rebooted. The value is still &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nothing is broken, and you did not make a typo. On a host running UFW, &lt;strong&gt;there is a second sysctl file, and it wins&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two files, one kernel
&lt;/h2&gt;

&lt;p&gt;UFW ships &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt; and loads it when the firewall comes up. It takes precedence over &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;. Anything you set in the familiar file that UFW also sets is quietly overwritten on the next &lt;code&gt;ufw enable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Quietly is the operative word. There is no warning, no log line, no diff. The file you edited still contains exactly what you typed — which is why this costs hours rather than minutes: you keep re-reading a correct file.&lt;/p&gt;

&lt;p&gt;The symptom that most often brings people here: a WireGuard tunnel that comes up, a client that receives its address, and no traffic going anywhere. &lt;code&gt;ip_forward&lt;/code&gt; fell back to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the effective value, not the file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.ipv4.ip_forward          &lt;span class="c"&gt;# what the kernel applies&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_forward   &lt;span class="c"&gt;# same thing, one file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that disagrees with what you wrote, look in &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt;. Note the notation there uses slashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;net&lt;/span&gt;/&lt;span class="n"&gt;ipv4&lt;/span&gt;/&lt;span class="n"&gt;ip_forward&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;net.ipv4.ip_forward&lt;/code&gt; and &lt;code&gt;net/ipv4/ip_forward&lt;/code&gt; name the same knob. Grepping for only one of them is a good way to conclude the setting is absent when it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small script to stop guessing
&lt;/h2&gt;

&lt;p&gt;I wrote a read-only checker that compares &lt;strong&gt;effective&lt;/strong&gt; sysctl values against hardening recommendations and, more importantly, flags the ones that are &lt;em&gt;declared somewhere and not applied&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ECART  kernel.kptr_restrict            effectif=0      attendu=2
 ECART  fs.protected_hardlinks          effectif=0      attendu=1
         declare a 1 dans /etc/sysctl.conf, mais le noyau applique 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second line is the whole point. No dependencies, Python 3 only, changes nothing:&lt;br&gt;
&lt;a href="https://gitlab.com/ler.eric/sysctl-hardening-check" rel="noopener noreferrer"&gt;https://gitlab.com/ler.eric/sysctl-hardening-check&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  While we are here: what hardening does not cover
&lt;/h2&gt;

&lt;p&gt;Tightening sysctl narrows what a compromised process reaches &lt;em&gt;on that machine&lt;/em&gt;. It says nothing about the paths that never touch the machine — a password reused on a service that leaked, a session left open on a device you no longer carry, the one account that receives every recovery link.&lt;/p&gt;

&lt;p&gt;That distinction is worth keeping in view: &lt;a href="https://secure-os.org/articles/linux-hardening" rel="noopener noreferrer"&gt;Linux hardening, what it covers and what it does not&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One line to keep: &lt;strong&gt;the file you edited is not necessarily the file that wins.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>UFW and WireGuard: the tunnel is up and nothing goes through</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 19 Aug 2026 03:37:35 +0000</pubDate>
      <link>https://dev.to/ricco020/ufw-and-wireguard-the-tunnel-is-up-and-nothing-goes-through-4lpo</link>
      <guid>https://dev.to/ricco020/ufw-and-wireguard-the-tunnel-is-up-and-nothing-goes-through-4lpo</guid>
      <description>&lt;p&gt;The tunnel comes up. &lt;code&gt;wg show&lt;/code&gt; prints a recent handshake. The client has its address inside the tunnel. And not a single byte reaches the internet.&lt;/p&gt;

&lt;p&gt;Almost every guide answers this with "open UDP 51820 in the firewall". You already did that — it is why the handshake works at all. The problem is somewhere else, and UFW makes the distinction easy to miss:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entering a machine and traversing it are two different permissions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ufw allow 51820/udp&lt;/code&gt; lets packets &lt;em&gt;arrive&lt;/em&gt; at the server. Your clients' traffic does not stop there — it goes &lt;em&gt;through&lt;/em&gt; the box and out the public interface. That path lives in the FORWARD chain, which UFW denies by default and which no &lt;code&gt;allow&lt;/code&gt; rule touches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things to check, in order
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. IP forwarding — and the file that overwrites the other file
&lt;/h3&gt;

&lt;p&gt;This is the one that costs hours, because the setting &lt;em&gt;looks&lt;/em&gt; done.&lt;/p&gt;

&lt;p&gt;UFW loads &lt;strong&gt;its own sysctl file&lt;/strong&gt; at startup, and it takes precedence over the system one. A value you carefully set in &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; can be silently overwritten on the next &lt;code&gt;ufw enable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The right place is &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;net&lt;/span&gt;/&lt;span class="n"&gt;ipv4&lt;/span&gt;/&lt;span class="n"&gt;ip_forward&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;/&lt;span class="n"&gt;ipv6&lt;/span&gt;/&lt;span class="n"&gt;conf&lt;/span&gt;/&lt;span class="n"&gt;default&lt;/span&gt;/&lt;span class="n"&gt;forwarding&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;/&lt;span class="n"&gt;ipv6&lt;/span&gt;/&lt;span class="n"&gt;conf&lt;/span&gt;/&lt;span class="n"&gt;all&lt;/span&gt;/&lt;span class="n"&gt;forwarding&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the &lt;strong&gt;effective&lt;/strong&gt; value, not the file you just edited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.ipv4.ip_forward
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Forwarding, which is not the same as ingress
&lt;/h3&gt;

&lt;p&gt;Targeted, and the one to prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw route allow &lt;span class="k"&gt;in &lt;/span&gt;on wg0 out on eth0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or globally, in &lt;code&gt;/etc/default/ufw&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DEFAULT_FORWARD_POLICY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ACCEPT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second opens forwarding for &lt;em&gt;every&lt;/em&gt; interface. It is a good ten-second diagnostic and a poor permanent configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. NAT, which UFW never adds on its own
&lt;/h3&gt;

&lt;p&gt;Without it, packets leave carrying their tunnel address, which nothing on the internet knows how to answer. In &lt;code&gt;/etc/ufw/before.rules&lt;/code&gt;, &lt;strong&gt;at the very top&lt;/strong&gt;, before the &lt;code&gt;*filter&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two classic mistakes here: putting this block &lt;em&gt;after&lt;/em&gt; &lt;code&gt;*filter&lt;/code&gt; (it is then ignored), and copying &lt;code&gt;eth0&lt;/code&gt; without checking.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The real name of the egress interface
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;eth0&lt;/code&gt; is an article convention, not a fact. Depending on the host it will be &lt;code&gt;ens3&lt;/code&gt;, &lt;code&gt;enp1s0&lt;/code&gt;, something else. The command that answers without guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip route get 1.1.1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The test that separates the two families of causes
&lt;/h2&gt;

&lt;p&gt;From a connected client, try to reach the &lt;strong&gt;server's address inside the tunnel&lt;/strong&gt; (often &lt;code&gt;10.8.0.1&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you observe&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tunnel address answers, nothing external does&lt;/td&gt;
&lt;td&gt;tunnel is fine — forwarding or NAT missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tunnel address does not answer either&lt;/td&gt;
&lt;td&gt;upstream: keys, &lt;code&gt;AllowedIPs&lt;/code&gt;, client routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;everything answers but names do not resolve&lt;/td&gt;
&lt;td&gt;not UFW at all — it is the DNS pushed to the client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third row is worth knowing. "I have no internet" very often means "no name resolves". Pinging an IP directly tells you in one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  After applying
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;before.rules&lt;/code&gt; is only re-read on reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw disable &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status verbose
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-t&lt;/span&gt; nat &lt;span class="nt"&gt;-L&lt;/span&gt; POSTROUTING &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One last thing that produces duplicate configurations: if your &lt;code&gt;PostUp&lt;/code&gt;/&lt;code&gt;PostDown&lt;/code&gt; rules in the WireGuard config already add &lt;code&gt;MASQUERADE&lt;/code&gt;, you do not need the &lt;code&gt;*nat&lt;/code&gt; block above. Both together rarely break anything, but they make the &lt;em&gt;next&lt;/em&gt; outage much harder to read — and there will be a next one.&lt;/p&gt;




&lt;p&gt;If you are still choosing a port or wondering whether 443/UDP is worth it, I wrote a longer piece on &lt;a href="https://www.vpnsmith.com/en/blog/wireguard-port" rel="noopener noreferrer"&gt;WireGuard ports, changing them and opening them properly&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One line to keep: &lt;strong&gt;an open port means packets can enter, not that they can traverse.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>wireguard</category>
      <category>linux</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your drive says SMART PASSED and is already failing: the 5 attributes that actually predict it</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sat, 15 Aug 2026 15:52:24 +0000</pubDate>
      <link>https://dev.to/ricco020/your-drive-says-smart-passed-and-is-already-failing-the-5-attributes-that-actually-predict-it-6g</link>
      <guid>https://dev.to/ricco020/your-drive-says-smart-passed-and-is-already-failing-the-5-attributes-that-actually-predict-it-6g</guid>
      <description>&lt;p&gt;A drive that SMART calls &lt;strong&gt;PASSED&lt;/strong&gt; can already be losing your data. The overall&lt;br&gt;
self-assessment is a manufacturer threshold, not a statement about your files — and it stays&lt;br&gt;
green well past the point where sectors have started going unreadable.&lt;/p&gt;

&lt;p&gt;Here is the case that matters, from a real report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMART overall verdict : PASSED

Reallocated_Sector_Ct  = 24   sectors already remapped
Current_Pending_Sector = 8    sectors unreadable RIGHT NOW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That drive passes. It is also actively failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five attributes worth reading
&lt;/h2&gt;

&lt;p&gt;Forget the table of forty values. Five of them describe the two families that precede&lt;br&gt;
unreadable data — reallocation and pending sectors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;attribute&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Reallocated_Sector_Ct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sectors already remapped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;197&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Current_Pending_Sector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unreadable now, waiting to be remapped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Offline_Uncorrectable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the drive could not read them offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Reported_Uncorrect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;errors it could not correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Command_Timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;commands that timed out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A non-zero &lt;strong&gt;197&lt;/strong&gt; on a drive reporting PASSED is the exact situation to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read it in one command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smartctl &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; /dev/sda | python3 smart_triage.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is MIT, has no dependencies and no network access — it reads stdin and prints a&lt;br&gt;
verdict, with exit codes &lt;code&gt;0&lt;/code&gt; healthy, &lt;code&gt;1&lt;/code&gt; monitor, &lt;code&gt;2&lt;/code&gt; back up now, &lt;code&gt;3&lt;/code&gt; stop and image:&lt;br&gt;
&lt;a href="https://gitlab.com/ler.eric/smart-triage" rel="noopener noreferrer"&gt;https://gitlab.com/ler.eric/smart-triage&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The order of operations, which decides whether you keep the data
&lt;/h2&gt;

&lt;p&gt;If the verdict is &lt;strong&gt;stop and image&lt;/strong&gt;, the sequence is not negotiable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stop using the drive.&lt;/strong&gt; Every hour of use turns more pending sectors into lost ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image the whole device to a file&lt;/strong&gt; with &lt;code&gt;ddrescue&lt;/code&gt; — not a file copy, a block-level image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work on the copy.&lt;/strong&gt; Never on the original.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mistake that destroys recoverable data is running a filesystem repair tool first. &lt;code&gt;fsck&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;chkdsk&lt;/code&gt; &lt;strong&gt;write&lt;/strong&gt; to the disk. Writing to a drive with pending sectors is how a&lt;br&gt;
recoverable image becomes an unrecoverable one — and it is irreversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SMART does not tell you
&lt;/h2&gt;

&lt;p&gt;SMART predicts a meaningful share of failures, not all of them. Controller failures, firmware&lt;br&gt;
faults and head crashes can arrive with every attribute still green. A clean report is a reason&lt;br&gt;
to keep monitoring, never a reason to skip backups.&lt;/p&gt;

&lt;p&gt;If you want the detail of what is realistically recoverable per failure mode — mechanical,&lt;br&gt;
electronic, logical, firmware — this breakdown covers it:&lt;br&gt;
&lt;a href="https://www.save-my-disk.com/en/blog/hard-drive-failure" rel="noopener noreferrer"&gt;https://www.save-my-disk.com/en/blog/hard-drive-failure&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>sysadmin</category>
      <category>hardware</category>
      <category>devops</category>
    </item>
    <item>
      <title>A failed deployment is invisible from the web</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 14 Aug 2026 06:27:43 +0000</pubDate>
      <link>https://dev.to/ricco020/a-failed-deployment-is-invisible-from-the-web-3kc9</link>
      <guid>https://dev.to/ricco020/a-failed-deployment-is-invisible-from-the-web-3kc9</guid>
      <description>&lt;p&gt;Last night a deployment of mine failed for 21 hours and every monitoring check stayed green. The site answered &lt;code&gt;200&lt;/code&gt; the whole time. Here is why that happens, and the two-line check that catches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;When a build fails on Vercel (and on Railway, and on most platforms with atomic deploys), the platform does the sensible thing: it keeps serving the &lt;strong&gt;previous&lt;/strong&gt; successful build. Your users see a working site. Your uptime monitor sees &lt;code&gt;200 OK&lt;/code&gt;. Your health check passes.&lt;/p&gt;

&lt;p&gt;What nobody sees is that &lt;strong&gt;the commit you pushed is not the code being served&lt;/strong&gt;. Every article, fix and config change since the last green build is sitting in git, live nowhere.&lt;/p&gt;

&lt;p&gt;That is a good default — far better than serving a broken build. But it means "the site is up" and "my work is deployed" are two different questions, and most monitoring only answers the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that actually answers it
&lt;/h2&gt;

&lt;p&gt;Compare the timestamp of your last commit with the timestamp of the last &lt;strong&gt;successful&lt;/strong&gt; deployment. If the commit is newer by more than a build's duration, something is wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%ct                 &lt;span class="c"&gt;# last commit, unix time&lt;/span&gt;
npx vercel &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--prod&lt;/span&gt;                    &lt;span class="c"&gt;# last deployments + their state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the age of the newest &lt;code&gt;Ready&lt;/code&gt; deployment is older than your last commit, you are serving stale code. In my case the newest &lt;code&gt;Ready&lt;/code&gt; was 21 hours old, and two &lt;code&gt;Error&lt;/code&gt; deployments sat above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap that cost me an hour
&lt;/h2&gt;

&lt;p&gt;Here is the part worth the read. Outside a TTY, &lt;code&gt;vercel ls&lt;/code&gt; &lt;strong&gt;splits its output across two streams&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdout&lt;/strong&gt; gets the deployment URLs, one per line — no status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stderr&lt;/strong&gt; gets the formatted table, including the &lt;code&gt;● Ready&lt;/code&gt; / &lt;code&gt;● Error&lt;/code&gt; column&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this looks correct and silently returns nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vercel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--prod&lt;/span&gt;&lt;span class="sh"&gt;"&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="bp"&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="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# always empty
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your script reports zero errors on a project that is entirely broken. The fix is to read both streams:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The general lesson is worth more than the specific fix: &lt;strong&gt;an empty result and a clean result are not the same thing&lt;/strong&gt;. If a check can return "nothing found" both when everything is fine and when the parsing broke, it will eventually lie to you — and it will lie in the reassuring direction. Make the two cases distinguishable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOT VERIFIED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# not "0 errors"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What actually broke the build
&lt;/h2&gt;

&lt;p&gt;For the curious: a TypeScript error on a component prop. One call site out of 21 was missing a required &lt;code&gt;id&lt;/code&gt;. Nothing exotic — but because the previous build kept serving, nothing surfaced it either.&lt;/p&gt;

&lt;p&gt;I found the second occurrence only after the first fix failed, because I had listed the call sites with &lt;code&gt;grep ... | head -8&lt;/code&gt; on a file with 21 of them. &lt;strong&gt;A &lt;code&gt;head&lt;/code&gt; on a search meant to be exhaustive turns "I saw nothing" into "there is nothing".&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to take away
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Uptime monitoring does not detect failed deployments. Compare commit time to last successful deploy time.&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;state&lt;/strong&gt; of recent deployments, not just the age of the last good one.&lt;/li&gt;
&lt;li&gt;When a check returns nothing, make sure it can tell you &lt;em&gt;why&lt;/em&gt; — no data and no problem must not look identical.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are wiring up this kind of monitoring, the same reasoning applies to the telemetry you collect about it — &lt;a href="https://coldwa.st/e/blog/2026-08-14-what-is-opentelemetry.html" rel="noopener noreferrer"&gt;what OpenTelemetry actually is, and what it deliberately does not give you&lt;/a&gt; covers the difference between producing telemetry and having observability, which is the trap one layer up.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>You don't need to restart WireGuard to add a peer</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Thu, 13 Aug 2026 15:09:57 +0000</pubDate>
      <link>https://dev.to/ricco020/you-dont-need-to-restart-wireguard-to-add-a-peer-2b3l</link>
      <guid>https://dev.to/ricco020/you-dont-need-to-restart-wireguard-to-add-a-peer-2b3l</guid>
      <description>&lt;p&gt;Most guides answer "how do I add a WireGuard peer" with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg-quick down wg0 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wg-quick up wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works. It also tears down &lt;strong&gt;every&lt;/strong&gt; active tunnel on the box to add one line to a file. On a server anyone actually depends on, that is the reason people batch peer additions for weeks instead of doing them when asked.&lt;/p&gt;

&lt;p&gt;There is a command that does it live.&lt;/p&gt;

&lt;h2&gt;
  
  
  wg set
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg &lt;span class="nb"&gt;set &lt;/span&gt;wg0 peer &amp;lt;CLIENT_PUBLIC_KEY&amp;gt; allowed-ips 10.0.0.5/32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The peer exists from that instant. Existing sessions are untouched — &lt;code&gt;wg set&lt;/code&gt; edits the running interface through the kernel module rather than rebuilding it.&lt;/p&gt;

&lt;p&gt;Nothing is printed on success, which throws people. &lt;code&gt;wg show wg0&lt;/code&gt; is what confirms it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch nobody mentions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wg set&lt;/code&gt; changes the &lt;em&gt;running&lt;/em&gt; state, not &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt;. Reboot and your peer is gone.&lt;/p&gt;

&lt;p&gt;You can write the live state back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg showconf wg0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/wireguard/wg0.conf
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/wireguard/wg0.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But note what &lt;code&gt;showconf&lt;/code&gt; actually prints: the interface's &lt;strong&gt;private key&lt;/strong&gt;, along with everything else. If your umask leaves that file world-readable, you have just published it to every account on the machine. The &lt;code&gt;chmod&lt;/code&gt; is not optional politeness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reloading an edited file without dropping anyone
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg syncconf wg0 &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;wg-quick strip wg0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;syncconf&lt;/code&gt; applies the &lt;em&gt;difference&lt;/em&gt;. &lt;code&gt;wg-quick down&lt;/code&gt;/&lt;code&gt;up&lt;/code&gt; rebuilds from scratch — the thing you were trying to avoid in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that looks like magic
&lt;/h2&gt;

&lt;p&gt;Two peers sharing an &lt;code&gt;AllowedIPs&lt;/code&gt; address is the bug that wastes an afternoon, because the symptom is backwards: you add a new client, and the &lt;strong&gt;older&lt;/strong&gt; one stops working. &lt;code&gt;AllowedIPs&lt;/code&gt; is what decides which peer a packet belongs to, so a duplicate makes that decision ambiguous.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;/32&lt;/code&gt; per client, never overlapping.&lt;/p&gt;

&lt;p&gt;I wrote a small tool that reads a folder of client configs and flags exactly that — duplicate addresses, reused private keys, and &lt;code&gt;AllowedIPs&lt;/code&gt; ranges wide enough to swallow the local network: &lt;a href="https://gitlab.com/ler.eric/wg-clients-audit" rel="noopener noreferrer"&gt;wg-clients-audit&lt;/a&gt;. Deliberately silent on &lt;code&gt;0.0.0.0/0&lt;/code&gt;, since full-tunnel is a choice, not a mistake.&lt;/p&gt;

&lt;p&gt;The longer version, with the client-side config and the three reasons a new peer fails to connect, is here: &lt;a href="https://www.vpnsmith.com/en/blog/wireguard-add-peer" rel="noopener noreferrer"&gt;WireGuard add peer without restarting the tunnel&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wireguard</category>
      <category>linux</category>
      <category>networking</category>
      <category>vpn</category>
    </item>
    <item>
      <title>What is an API? A clear, practical explainer</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:25:36 +0000</pubDate>
      <link>https://dev.to/ricco020/what-is-an-api-a-clear-practical-explainer-4mkh</link>
      <guid>https://dev.to/ricco020/what-is-an-api-a-clear-practical-explainer-4mkh</guid>
      <description>&lt;p&gt;An API (Application Programming Interface) is a contract that lets one piece of software ask another for data or actions, without knowing how it works inside. What an API is, how a request and response work, the common styles like REST and GraphQL, and why developers build on them.&lt;/p&gt;

&lt;p&gt;An API — short for Application Programming Interface — is a set of rules that lets one program talk to another. It defines how you ask for data or an action, what you have to send, and what you get back, so two pieces of software can work together without either one knowing how the other is built inside. It is the contract between them.&lt;/p&gt;

&lt;p&gt;A common analogy is a restaurant menu. The menu lists what you can order and what each dish costs, you place an order, and the kitchen prepares it — but you never need to see the kitchen or know how the cooking is done. An API is that menu for software: it tells you what you can request and how, and hides everything behind it.&lt;/p&gt;

&lt;p&gt;More precisely, an API exposes a set of operations a service is willing to perform. A weather service might offer an operation that returns the forecast for a city; a payment service might offer one that charges a card. You call the operation with the inputs it expects, and the service does the work and hands back a result. The internals stay private and can change freely, as long as the contract holds.&lt;/p&gt;

&lt;p&gt;On the web, most APIs work over the same protocol as web pages: HTTP. Your program sends a request to a URL called an endpoint, usually with a method that signals intent — GET to read data, POST to create it, PUT or PATCH to update, DELETE to remove. The request can carry parameters and a body; the server processes it and sends back a response.&lt;/p&gt;

&lt;p&gt;That response has two important parts: a status code and a payload. The status code is a short number that says how it went — 200 means success, 404 means the thing was not found, 500 means the server hit an error. The payload is the data itself, today almost always formatted as JSON, a lightweight text format that is easy for programs to read and write.&lt;/p&gt;

&lt;p&gt;APIs come in a few common styles. REST is the most widespread: it organises everything around resources addressed by URLs and leans on the standard HTTP methods. GraphQL takes a different approach, letting the client ask for exactly the fields it needs in a single query, which avoids over-fetching. Older systems may use SOAP, and real-time apps often add WebSockets for a continuous two-way connection.&lt;/p&gt;

&lt;p&gt;Many APIs are protected, because they expose real data and actions. Authentication proves who is calling — often with an API key or a token such as OAuth — and rate limiting caps how many requests a caller can make in a window, so one client cannot overwhelm the service. Public APIs publish documentation describing every endpoint, its inputs, and its responses.&lt;/p&gt;

&lt;p&gt;Developers rely on APIs because they let you build on top of existing services instead of reinventing them. You can add maps, payments, email, login, or AI to an app by calling someone else’s API. They also let large systems split into smaller services that talk to each other, and they are how a mobile app or single-page front end fetches data from a backend.&lt;/p&gt;

&lt;p&gt;Developers rely on APIs because they let you build on top of existing services instead of reinventing them. You can add maps, payments, email, login, or AI to an app by calling someone else’s API. They also let large systems split into smaller services that talk to each other, and they are how a mobile app or single-page front end fetches data from a backend.&lt;/p&gt;

&lt;p&gt;Finally, an API has to run somewhere. The service behind it — the backend that receives requests and returns responses — needs a reliable, always-on server. For your own APIs, that means a host you control: a VPS or cloud server where the backend stays up and reachable. A solid host is the foundation the whole interface sits on.&lt;/p&gt;

&lt;p&gt;Self-hosting needs a reliable home with full runtime and network control. Infomaniak — a Swiss, privacy-respecting provider — offers VPS and cloud servers to host your CMS or app.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://versiondude.net/articles/what-is-an-api" rel="noopener noreferrer"&gt;versiondude.net&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
