<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged devops</title>
<link>https://coderlegion.com/tag/devops</link>
<description>Powered by Question2Answer</description>
<item>
<title>From Writing the Spec to Approving It: What AI-DLC Looks Like on a Real Build</title>
<link>https://coderlegion.com/33078/from-writing-the-spec-to-approving-it-what-ai-dlc-looks-like-on-a-real-build</link>
<description>&lt;h3 id=&quot;-from-writing-the-spec-to-approving-it-what-ai-dlc-looks-like-on-a-real-build&quot;&gt;🤖 From Writing the Spec to Approving It: What AI-DLC Looks Like on a Real Build&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI-DLC is public and open source: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/awslabs/aidlc-workflows&quot;&gt;awslabs/aidlc-workflows&lt;/a&gt;. The method comes from this &lt;a rel=&quot;nofollow&quot; href=&quot;https://aws.amazon.com/blogs/devops/ai-driven-development-life-cycle/&quot;&gt;AWS DevOps blog post&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;My only written input was one sentence. Everything else I did was pick options and approve gates.&lt;/li&gt;
&lt;li&gt;The run produced requirements, four architecture decisions, a unit plan, a traceability file, and 12 tests that passed when I ran them, in about 44 minutes from start to the code approval.&lt;/li&gt;
&lt;li&gt;The human role moves from author to decision-maker. The decisions are still yours, and the record shows where they changed the outcome.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Most AI-assisted development still follows the old split. A human writes the spec, a human writes the code, and the AI helps with autocomplete or generates tests at the end. AI-DLC (the AI-Driven Development Life Cycle) reverses that. The AI drafts the requirements, the design, and the code. The human answers short questions and approves each stage before the next one starts.&lt;/p&gt;
&lt;p&gt;To see what that looks like in practice, I built a small command-line tool with it and then read the record the workflow left behind. This post walks through that record, folder by folder.&lt;/p&gt;
&lt;h3 id=&quot;the-app&quot;&gt;The app&lt;/h3&gt;
&lt;p&gt;The tool is a Spooky Date Fact Generator. It picks one of 10 spooky dates (or takes &lt;code&gt;--date MM-DD&lt;/code&gt;) and prints a spooky event for that day from Wikipedia's &quot;On this day&quot; feed. It is small on purpose.&lt;/p&gt;
&lt;p&gt;The whole request was this sentence:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/aidlc build random date generator with fact around that number
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-flip-in-one-table&quot;&gt;The flip, in one table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Traditional with AI help &lt;/th&gt;&lt;th&gt;AI-DLC&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Requirements &lt;/td&gt;&lt;td&gt;Human writes them &lt;/td&gt;&lt;td&gt;AI drafts them from multiple-choice answers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Design &lt;/td&gt;&lt;td&gt;Human sketches it &lt;/td&gt;&lt;td&gt;AI proposes, human picks between options&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Code &lt;/td&gt;&lt;td&gt;Human writes, AI assists &lt;/td&gt;&lt;td&gt;AI writes against an approved plan&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Tests &lt;/td&gt;&lt;td&gt;Human or AI at the end &lt;/td&gt;&lt;td&gt;AI writes them to the stated requirements&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Human job &lt;/td&gt;&lt;td&gt;Author &lt;/td&gt;&lt;td&gt;Decide, validate, approve&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Evidence &lt;/td&gt;&lt;td&gt;Pull request diff &lt;/td&gt;&lt;td&gt;Audit log, decisions, traceability file&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;where-everything-was-written&quot;&gt;Where everything was written&lt;/h3&gt;
&lt;p&gt;Two locations matter. The generated app sits in its own folder, and the workflow record sits in an &lt;code&gt;aidlc/&lt;/code&gt; folder.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app/                                  &amp;lt;- the generated tool
  src/spooky-facts.ts      core: dates, fetch, keyword filter, nearest-date search
  src/cli.ts               command line: options, output, exit codes
  test/                    12 tests, offline, with a fake fetch
  package.json, biome.json, tsconfig.json, README.md

aidlc/
  codekb/                             &amp;lt;- 9 docs about the existing repo
  intents/&amp;lt;date&amp;gt;-date-fact-generator/ &amp;lt;- the record for this build
    aidlc-state.md                    stage checklist and settings
    audit/                            every event, decision, and approval
    inception/
      reverse-engineering/            scan of the existing repo
      requirements-analysis/          questions + requirements.md
      domain-design/                  questions + components.md + decisions.md
      units-generation/               unit-of-work files
    construction/u1-spooky-date-cli/code-generation/
      code-generation-plan.md, code-summary.md,
      unit-test-instructions.md, traceability.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each stage writes a &lt;code&gt;-questions.md&lt;/code&gt; file, and your answers sit in the same file as the AI's questions. That makes the decision trail readable without any tooling.&lt;/p&gt;
&lt;h3 id=&quot;stage-by-stage-what-it-did-and-why-it-helps&quot;&gt;Stage by stage: what it did and why it helps&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Initialization.&lt;/strong&gt; The workflow scaffolded the record folder, scanned the workspace, and classified it as brownfield TypeScript on Bun. This is automatic. It helps because every later stage starts from facts about the repo instead of guesses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reverse Engineering.&lt;/strong&gt; A developer agent scanned the existing repo and wrote nine knowledge-base documents (architecture, technology stack, code structure and others). This matters most in a brownfield repo. The later code followed the repo's Bun, TypeScript, and Biome conventions because the AI had read them first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requirements Analysis.&lt;/strong&gt; The AI asked six questions, such as where facts come from, what to do when a date has no spooky event, and whether this should be a web page or a command line. Each had lettered options. I answered, and one answer included a free-text note: generate dates that have spooky facts, from 10 such scenarios. The result is &lt;code&gt;requirements.md&lt;/code&gt; with 15 functional and 5 non-functional requirements, each with acceptance criteria and a tag pointing back to the answer that produced it, such as &lt;code&gt;[Q5]&lt;/code&gt;. This stage turns one vague sentence into testable statements before any code exists.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Domain Design.&lt;/strong&gt; Four more questions produced &lt;code&gt;components.md&lt;/code&gt; and four architecture decision records. The AI recommended splitting the tool into four parts. I chose two. The decision record states that the four-part split was rejected as more structure than needed. This is the clearest example of the validator role, because the human overrode the AI's recommendation and the reason is recorded.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Units Generation.&lt;/strong&gt; One question: build as one unit or two. I chose one, approved the plan, and got &lt;code&gt;unit-of-work.md&lt;/code&gt; with boundaries, responsibilities, and constraints. It fixes the blast radius. The unit is limited to &lt;code&gt;apps/date-fact-generator/&lt;/code&gt;, and nothing in the framework folders may change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code Generation.&lt;/strong&gt; The AI wrote a plan, then the code, then the tests. It asked one thing here: Bun was not installed, so should it install Bun or switch to Node. I chose Bun. The &lt;code&gt;code-summary.md&lt;/code&gt; lists files created, decisions made, and deviations from the plan. One deviation worth noting: the AI added word forms such as &quot;dies&quot; and &quot;sank&quot; to the keyword list, because whole-word matching would otherwise miss &quot;Houdini dies&quot;. The summary also states that some planned checks were grouped into fewer tests to match the 12-test count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build and Test.&lt;/strong&gt; In the record I read, this stage was still marked in progress, so its summary was not written yet. I ran the checks myself from the app folder: 12 tests passed with 93 assertions, and lint and typecheck were clean.&lt;/p&gt;
&lt;h3 id=&quot;what-the-human-actually-did&quot;&gt;What the human actually did&lt;/h3&gt;
&lt;p&gt;Reading the audit log, the human work was this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed one sentence to start.&lt;/li&gt;
&lt;li&gt;Approved a recomposed plan. The workflow's planner found the default express plan was missing inputs for later stages, and proposed adding Domain Design and Units Generation while dropping deployment and monitoring.&lt;/li&gt;
&lt;li&gt;Answered 11 multiple-choice questions across three stages, plus two more decisions: build inside the existing repo, and install Bun.&lt;/li&gt;
&lt;li&gt;Approved 5 stage gates: reverse engineering, requirements, domain design, units, and code generation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the flip. Nothing in that list is writing a spec or writing code. All of it is deciding and checking.&lt;/p&gt;
&lt;h3 id=&quot;why-this-holds-up-better-than-a-prompt&quot;&gt;Why this holds up better than a prompt&lt;/h3&gt;
&lt;p&gt;Two details make the validation real instead of a rubber stamp.&lt;/p&gt;
&lt;p&gt;First, traceability. &lt;code&gt;traceability.json&lt;/code&gt; maps every requirement ID (FR1.1 through NFR5, 20 in total) to the file that satisfies it. If a requirement has no target, it shows up.&lt;/p&gt;
&lt;p&gt;Second, the record is complete. The audit log holds the timestamp of each question, answer, and approval. A reviewer six months later can see why the design has two components and not four.&lt;/p&gt;
&lt;h3 id=&quot;what-this-run-does-not-show&quot;&gt;What this run does not show&lt;/h3&gt;
&lt;p&gt;This was an express-scope build of a 658-line app, with plan approval, sensors, and learnings switched off by the scope. It shows the shape of the method, not how it behaves on a large system with many units and teams. The Build and Test stage had not completed in the record, and the tests are offline, so only one manual live smoke run, recorded in the code summary, exercised the real Wikipedia feed.&lt;/p&gt;
&lt;p&gt;The questions are only as good as the options offered. A weak option list can steer a hurried approver, so reading the alternatives still matters.&lt;/p&gt;
&lt;h3 id=&quot;takeaway&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;The useful change is not that the AI writes more code. It is that the human's time moves to the points where judgment counts: what the tool should do, which design to accept, and whether the result meets the requirements. If you want to try it, start with a small brownfield repo and read the &lt;code&gt;-questions.md&lt;/code&gt; files first. They show the method faster than any description.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33078/from-writing-the-spec-to-approving-it-what-ai-dlc-looks-like-on-a-real-build</guid>
<pubDate>Fri, 30 Oct 2026 10:36:49 +0000</pubDate>
</item>
<item>
<title>Flyway Migration: From Legacy Database to CI/CD</title>
<link>https://coderlegion.com/32961/flyway-migration-from-legacy-database-to-ci-cd</link>
<description>&lt;p&gt;Most teams think of Flyway only when they need to manage ongoing database schema changes. But what if your project already has a complete, stable schema and you’re only now introducing version control? Or what if you want to incorporate your schema into a CI/CD pipeline for reliable, reproducible deployments?&lt;/p&gt;
&lt;p&gt;In this guide, I’ll walk you through a real-world scenario where we used Flyway to baseline an existing MySQL database and then extended it to support version-controlled schema migrations in CI/CD, without disrupting production.&lt;/p&gt;
&lt;h3 id=&quot;what-is-flyway-&quot;&gt;What is Flyway?&lt;/h3&gt;
&lt;p&gt;Flyway is a simple but powerful tool for managing database migrations. It applies versioned SQL scripts to your database and tracks them in a special table called &lt;code&gt;flyway_schema_history&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Typical script names:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;V1__initial_schema.sql
V2__add_orders_table.sql
V3__rename_column.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flyway ensures these scripts run only once and in the correct order. It also integrates well with CI/CD tools, making it ideal for automated environments.&lt;/p&gt;
&lt;h3 id=&quot;use-case-existing-schema-with-no-migrations-yet-&quot;&gt;Use Case: Existing Schema with No Migrations (Yet)&lt;/h3&gt;
&lt;p&gt;In our project, we had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A production MySQL database with dozens of tables&lt;/li&gt;
&lt;li&gt;No migration history&lt;/li&gt;
&lt;li&gt;No current need for structural changes&lt;/li&gt;
&lt;li&gt;A desire to version control the database and enable future migrations via CI/CD&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-1-baselining-an-existing-schema&quot;&gt;Step 1: Baselining an Existing Schema&lt;/h3&gt;
&lt;h4 id=&quot;dump-the-schema&quot;&gt;Dump the schema&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mysqldump -u root -p --no-data your_database &amp;gt; V1__baseline_schema.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clean the output: remove auto-increments, user references, and environment-specific SQL.&lt;/p&gt;
&lt;h4 id=&quot;create-directory-structure&quot;&gt;Create directory structure&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p db/sql
mv V1__baseline_schema.sql db/sql/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;configure-flyway-conf&quot;&gt;Configure flyway.conf&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;flyway.url=jdbc:mysql://your-db-host:3306/your_database
flyway.user=db_user
flyway.password=secret_password
flyway.locations=filesystem:db/sql
flyway.baselineOnMigrate=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;run-baseline&quot;&gt;Run baseline&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;flyway baseline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates the &lt;code&gt;flyway_schema_history&lt;/code&gt; table and marks version 1 as the current state without running any script.&lt;/p&gt;
&lt;h3 id=&quot;step-2-add-a-real-migration&quot;&gt;Step 2: Add a Real Migration&lt;/h3&gt;
&lt;p&gt;After baselining, we added a real change in a new script&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- db/sql/V2__add_status_to_orders.sql
ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we ran:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;flyway migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Flyway applied version 2, recorded it in history, and our database was now safely versioned.&lt;/p&gt;
&lt;h3 id=&quot;-step-3-integrate-flyway-into-ci-cd&quot;&gt;⚙️ Step 3: Integrate Flyway into CI/CD&lt;/h3&gt;
&lt;p&gt;You can integrate Flyway with GitHub Actions, GitLab CI, Bitbucket Pipelines, or Jenkins.&lt;/p&gt;
&lt;h3 id=&quot;-example-github-actions-workflow&quot;&gt;✅ Example GitHub Actions Workflow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Flyway Migration
on:
  push:
    branches: [ main ]

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v3
      - name: Set up Java (Flyway needs it)
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      - name: Install Flyway
        run: |
          curl -L https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/10.8.1/flyway-commandline-10.8.1-linux-x64.tar.gz | tar xvz
          sudo ln -s $PWD/flyway-10.8.1/flyway /usr/local/bin/flyway
      - name: Run Flyway Migrations
        env:
          FLYWAY_URL: jdbc:mysql://your-host:3306/your_database
          FLYWAY_USER: ${{ secrets.DB_USER }}
          FLYWAY_PASSWORD: ${{ secrets.DB_PASSWORD }}
        run: |
          flyway -url=$FLYWAY_URL \
                 -user=$FLYWAY_USER \
                 -password=$FLYWAY_PASSWORD \
                 -locations=filesystem:./db/sql \
                 migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;benefits-of-this-approach&quot;&gt;Benefits of This Approach&lt;/h3&gt;
&lt;p&gt;✅ You gain &lt;strong&gt;auditability&lt;/strong&gt; for all database changes&lt;/p&gt;
&lt;p&gt;✅ Developers can build &lt;strong&gt;local environments&lt;/strong&gt; using the same scripts&lt;/p&gt;
&lt;p&gt;✅ CI/CD ensures &lt;strong&gt;consistency&lt;/strong&gt; across all environments&lt;/p&gt;
&lt;p&gt;✅ Flyway’s &lt;code&gt;validate&lt;/code&gt; command catches missing or changed scripts&lt;/p&gt;
&lt;p&gt;✅ Schema management becomes part of your &lt;strong&gt;software delivery lifecycle&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;potential-challenges&quot;&gt;Potential Challenges&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Problem → Fix&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flyway re-applies the baseline script&lt;br&gt;
→ Use baselineOnMigrate=true and never modify a baseline&lt;/li&gt;
&lt;li&gt;Passwords in config files&lt;br&gt;
→ Use secrets and environment variables in CI/CD&lt;/li&gt;
&lt;li&gt;Manual schema drift&lt;br&gt;
→ Disable direct DB access in prod or run flyway validate periodically&lt;/li&gt;
&lt;li&gt;Need rollback support&lt;br&gt;
→ Write manual undo scripts or use point-in-time backups&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;alternatives-and-why-flyway-wins-&quot;&gt;Alternatives (and Why Flyway Wins)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt; Liquibase&lt;/strong&gt; &lt;br&gt;
✅ Pros: Advanced features, rollback support &lt;br&gt;
❌ Cons: More complex, steeper learning curve&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Django ORM / Alembic&lt;/strong&gt; &lt;br&gt;
✅ Pros: Built-in for app devs &lt;br&gt;
❌ Cons: Not cross-language or DB-agnostic&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Manual SQL or DB dumps&lt;/strong&gt; &lt;br&gt;
✅ Pros: Simple &lt;br&gt;
❌ Cons: No versioning, no tracking, no automation&lt;/p&gt;
&lt;p&gt;Flyway is a great middle-ground: powerful enough for teams, simple enough for solo devs, and perfect for CI/CD.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Flyway isn’t just for teams writing migrations every day. It’s also ideal for bringing an existing database under version control, giving your project a foundation for safe schema evolution.&lt;/p&gt;
&lt;p&gt;We started with a baseline, added our first real migration, and then built it into our CI/CD pipeline — all in a day’s work.&lt;/p&gt;
&lt;p&gt;If you’re managing an existing DB without schema tracking, now’s the time to fix that. Start with a baseline. Your future self will thank you.&lt;/p&gt;
&lt;h3 id=&quot;let-s-connect-&quot;&gt;Let’s Connect!&lt;/h3&gt;
&lt;p&gt;Have you tried Flyway for baseline-only use cases or integrated it into your CI/CD pipeline? I’d love to hear your experience in the comments.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If you found this helpful, follow me here on DEV for more DevOps and database engineering content.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32961/flyway-migration-from-legacy-database-to-ci-cd</guid>
<pubDate>Thu, 29 Oct 2026 12:05:30 +0000</pubDate>
</item>
<item>
<title>The log was deleted. Its 48M was still allocated.</title>
<link>https://coderlegion.com/30657/the-log-was-deleted-its-48m-was-still-allocated</link>
<description>&lt;p&gt;A 64M scratch tmpfs, a worker process holding its log open on file descriptor 3, and the usual reflex when space runs low: &lt;code&gt;rm&lt;/code&gt; the log. &lt;code&gt;du&lt;/code&gt; on the directory dropped from 48M to 0. &lt;code&gt;df&lt;/code&gt; stayed at 48M used. The next 32M write failed with &lt;code&gt;No space left on device&lt;/code&gt;, and &lt;code&gt;df&lt;/code&gt; read 100% while &lt;code&gt;du&lt;/code&gt; found only the 16M that had been written.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rm&lt;/code&gt; removes a name. The kernel frees the blocks only when no names are left and no process has the file open, and the worker still had it open. &lt;code&gt;lsof +L1&lt;/code&gt; lists exactly those files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ lsof -w +L1 /srv
COMMAND    PID USER FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
sleep   608657 root 3w   REG   0,78 50331648     0    3 /srv/app/worker.log (deleted)
exit=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NLINK 0&lt;/code&gt;, &lt;code&gt;(deleted)&lt;/code&gt;, 48M held for writing by PID 608657. Truncating through the process's own descriptor, &lt;code&gt;: &amp;gt; /proc/608657/fd/3&lt;/code&gt;, took &lt;code&gt;df&lt;/code&gt; back to 25% while the process kept running.&lt;/p&gt;
&lt;p&gt;Next time, empty a live log with &lt;code&gt;: &amp;gt; file&lt;/code&gt; instead of deleting it. With an append-mode writer (&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;) that is clean. A writer that opened with &lt;code&gt;&amp;gt;&lt;/code&gt; keeps its old offset: in the same test, its log came back as a 17M sparse file with 1.0M on disk.&lt;/p&gt;
&lt;p&gt;Finding which process holds a port or a busy mount, plus a script that groups deleted-but-open files and totals them, is on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/lsof-command-examples&quot;&gt;lsof command examples page&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30657/the-log-was-deleted-its-48m-was-still-allocated</guid>
<pubDate>Wed, 28 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>A Stopwatch Needs an Isolated Lane</title>
<link>https://coderlegion.com/32820/a-stopwatch-needs-an-isolated-lane</link>
<description>&lt;p&gt;Timing tests look wonderfully objective. Start a stopwatch, call the component, stop the watch, and compare the result with a budget.&lt;/p&gt;
&lt;p&gt;The number is precise. The experiment may not be.&lt;/p&gt;
&lt;p&gt;That distinction matters in continuous integration, where many tests share CPU time, thread pools, network stacks, containers, and database capacity. A wall-clock assertion can fail because the product became slower. It can also fail because the test runner became busier. If we cannot distinguish those two explanations, the test is producing urgency without producing useful evidence.&lt;/p&gt;
&lt;h3 id=&quot;the-contract-was-reasonable&quot;&gt;The contract was reasonable&lt;/h3&gt;
&lt;p&gt;Consider a service with separate liveness and readiness checks. Liveness says the process is still running. Readiness says the instance can safely accept work.&lt;/p&gt;
&lt;p&gt;One integration test deliberately points the service at an unreachable dependency. The expected behavior is not success. The service should return an unavailable response, and it should do so within a fixed upper bound. That is a meaningful reliability contract: an orchestrator needs a quick, honest answer rather than a request that hangs behind retry policies intended for normal user operations.&lt;/p&gt;
&lt;p&gt;The test boots the real host, makes the request, verifies the unavailable result, and checks elapsed time.&lt;/p&gt;
&lt;p&gt;So far, so good.&lt;/p&gt;
&lt;h3 id=&quot;the-failure-blamed-the-wrong-interval&quot;&gt;The failure blamed the wrong interval&lt;/h3&gt;
&lt;p&gt;The timing assertion began failing repeatedly in CI. Raising the limit would have made the build green. Adding an automatic retry might also have hidden the symptom.&lt;/p&gt;
&lt;p&gt;Neither action would have explained the result.&lt;/p&gt;
&lt;p&gt;The endpoint implementation had not changed during the relevant window. More importantly, the surrounding timing evidence showed that host startup in the same test took roughly as long as the delay attributed to the readiness request. The test also ran beside many parallel integration classes doing heavier work.&lt;/p&gt;
&lt;p&gt;The stopwatch was attached to one request, but that request still depended on threads scheduled in a shared process. Under contention, continuation work can wait. Startup can wait. Connection failure can be observed later than expected. The test reports wall-clock time, not pure execution time.&lt;/p&gt;
&lt;p&gt;This does not prove the service is fast. It proves the original experiment was unable to tell product latency from runner contention.&lt;/p&gt;
&lt;h3 id=&quot;keep-the-budget-before-changing-it&quot;&gt;Keep the budget before changing it&lt;/h3&gt;
&lt;p&gt;When a timing test turns red, I prefer to keep the bound intact while investigating. The bound represents a real operational expectation. Relaxing it before understanding the measurement destroys evidence that may still be valuable.&lt;/p&gt;
&lt;p&gt;A useful first pass is to separate three intervals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Setup&lt;/strong&gt; — host boot, fixture creation, migrations, container startup, and test data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contention&lt;/strong&gt; — time spent waiting for CPU, threads, sockets, database slots, or another shared resource.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Behavior&lt;/strong&gt; — the operation whose latency the assertion is meant to protect.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Instrumenting those boundaries does not require a production profiler. Even a few carefully placed timestamps can reveal that the supposedly slow operation accounts for only a small part of the elapsed test.&lt;/p&gt;
&lt;h3 id=&quot;isolate-the-claim-not-the-whole-suite&quot;&gt;Isolate the claim, not the whole suite&lt;/h3&gt;
&lt;p&gt;The narrow repair was to move only the timing proof into a non-parallel test collection.&lt;/p&gt;
&lt;p&gt;The response status did not change. The time budget did not change. The endpoint code did not change. Functional readiness tests stayed in the normal parallel suite. Only the test making a wall-clock claim received an isolated measurement lane.&lt;/p&gt;
&lt;p&gt;That scope matters. Serializing an entire integration project can turn a useful diagnosis into an expensive habit. Parallel tests are valuable because they shorten feedback cycles and expose accidental shared state. The aim is not to remove parallelism. It is to stop using an uncontrolled parallel environment as a precision instrument.&lt;/p&gt;
&lt;p&gt;I like to keep separate tests for separate questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the check return the correct status when the dependency is unavailable?&lt;/li&gt;
&lt;li&gt;Does the response avoid leaking infrastructure details?&lt;/li&gt;
&lt;li&gt;Does the host remain alive while readiness is false?&lt;/li&gt;
&lt;li&gt;Under controlled contention, does the readiness answer stay within its operational budget?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only the last question necessarily needs timing isolation.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off-is-real&quot;&gt;The trade-off is real&lt;/h3&gt;
&lt;p&gt;An isolated collection adds a serial tail to the build. It also creates another test boundary that future maintainers must understand. If many tests demand exclusive execution, the suite can become slow and the isolation can conceal real capacity problems.&lt;/p&gt;
&lt;p&gt;That is why this technique should be applied to the measurement contract, not used as a blanket cure for flakiness.&lt;/p&gt;
&lt;p&gt;In return, failures become more actionable. A red timing check is less likely to mean “the runner happened to be crowded” and more likely to mean “the protected latency contract changed.” The team spends less time rerunning the same experiment and less time normalizing noise by widening budgets.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-review-checklist&quot;&gt;A practical review checklist&lt;/h3&gt;
&lt;p&gt;Before changing a failed CI time limit, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Did the measured implementation change?&lt;/li&gt;
&lt;li&gt;Is setup included in the timed region?&lt;/li&gt;
&lt;li&gt;Which resources are shared with parallel tests?&lt;/li&gt;
&lt;li&gt;Does the test need fixtures it is currently competing for?&lt;/li&gt;
&lt;li&gt;Can functional correctness remain parallel while the timing proof runs alone?&lt;/li&gt;
&lt;li&gt;Would the same isolation exist in the production condition we care about?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last question prevents overfitting. An isolated test should remove irrelevant test-runner noise, not erase meaningful load. If the production contract is specifically about behavior under concurrency, create a controlled load test with explicit pressure instead of relying on incidental CI contention.&lt;/p&gt;
&lt;h3 id=&quot;precision-is-a-property-of-the-experiment&quot;&gt;Precision is a property of the experiment&lt;/h3&gt;
&lt;p&gt;A stopwatch does not make a test scientific by itself. The environment, boundaries, and competing work determine what the number means.&lt;/p&gt;
&lt;p&gt;Keep meaningful budgets. Investigate before retrying. Isolate the smallest timing claim that needs a controlled lane, and leave the rest of the suite parallel.&lt;/p&gt;
&lt;p&gt;The result is not merely a greener pipeline. It is a test whose failure tells you something you can act on.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32820/a-stopwatch-needs-an-isolated-lane</guid>
<pubDate>Wed, 28 Oct 2026 10:49:36 +0000</pubDate>
</item>
<item>
<title>Organization-Wide EC2 DNS Automation with EventBridge &amp; AWS Config Aggregator: One Rule Per Account</title>
<link>https://coderlegion.com/32736/organization-wide-ec2-dns-automation-with-eventbridge-aws-config-aggregator-rule-account</link>
<description>&lt;h3 id=&quot;-organization-wide-ec2-dns-automation-with-eventbridge-and-aws-config-aggregator-one-rule-per-account&quot;&gt;🌐 Organization-Wide EC2 DNS Automation with EventBridge and AWS Config Aggregator: One Rule Per Account&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Detecting EC2 launches and terminations across a whole AWS Organization for DNS is not the five-minute single-account job it looks like.&lt;/li&gt;
&lt;li&gt;The EC2 state-change event carries only an instance ID and a state, no IP, so it is useless for DNS on its own.&lt;/li&gt;
&lt;li&gt;A hub-and-spoke design keeps the per-account footprint to a single EventBridge rule; all logic lives in one hub account.&lt;/li&gt;
&lt;li&gt;The hub enriches each event by querying the AWS Config Aggregator you already run, which holds every instance's IP, tags, and state org-wide. No new data pipeline.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; resource policy on the hub's custom bus gives cross-account fan-in without per-account config.&lt;/li&gt;
&lt;li&gt;SQS between the rule and the Lambda absorbs bulk launches and gives you retries plus a dead-letter queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;How to detect every EC2 launch and termination across an entire AWS Organization, and turn it into automated DNS registration, without deploying repeated infrastructure in every account.&lt;/p&gt;
&lt;p&gt;I spent longer than I would like to admit staring at this problem before the answer got simple. So if you are wiring up DNS across an AWS Organization right now, maybe this saves you a weekend.&lt;/p&gt;
&lt;p&gt;Here is the short version of what I landed on. I needed near real-time DNS registration for every EC2 instance across a multi-account, multi-region org, feeding an on-prem enterprise DDI system (think Infoblox). The two obvious ways to do it both annoyed me: one puts infrastructure in every account, the other leans on a fragile cross-account API-scraping job.&lt;/p&gt;
&lt;p&gt;What I used instead is a hub-and-spoke event setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each spoke account gets exactly one EventBridge rule. That is the whole footprint.&lt;/li&gt;
&lt;li&gt;One hub account does everything else: a custom event bus, an SQS queue, one Lambda.&lt;/li&gt;
&lt;li&gt;The Lambda fills in the missing details by querying the AWS Config Aggregator, which, and this is the part I almost missed, already has every instance's IP, tags, and state for the entire org. No new data pipeline required.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Onboarding a new account came down to a one-line deploy. Below is the whole thing: the architecture, why I made the calls I made, and the code.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-why-the-single-account-version-does-not-scale-&quot;&gt;The problem (why the single-account version does not scale)&lt;/h3&gt;
&lt;p&gt;In one account this is a five-minute job. EC2 fires a state-change event, you point an EventBridge rule at a Lambda, done. I have written that Lambda a dozen times.&lt;/p&gt;
&lt;p&gt;Across an organization it falls apart for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instances come up in dozens of accounts and several regions.&lt;/li&gt;
&lt;li&gt;The people who run DNS are not the people who own the workload accounts, so &quot;just add it to the account&quot; is not really on the table.&lt;/li&gt;
&lt;li&gt;The EC2 state-change event does not include the IP address. You get an instance ID and a state. That is it. Useless for DNS on its own.&lt;/li&gt;
&lt;li&gt;Nobody wants to deploy, version, and babysit a Lambda plus queue plus IAM role in every account just to catch these events.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The usual reason you end up here is a move to an enterprise DDI platform. Every instance that boots needs a DNS record, every instance that dies needs its record cleaned up, org-wide, in near real-time, with as little junk in each account as you can manage.&lt;/p&gt;
&lt;p&gt;So two things really drove the design:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adding an account should be almost free.&lt;/li&gt;
&lt;li&gt;The IP is not in the event, so I have to look it up, and that lookup cannot be flaky.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;the-approaches-i-threw-out&quot;&gt;The approaches I threw out&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Option A: put the whole stack in every account.&lt;/strong&gt; Rule, queue, Lambda, IAM role, shipped everywhere with StackSets. It works. But now you own N copies of the same code, and every bug fix or IAM tweak is an N-account rollout. I have maintained fleets like that. No thanks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option B: a central poller that assumes a role into the source account&lt;/strong&gt; and calls DescribeInstances for the IP. Logic is centralized, which is nice, but you need an assume-role trust in every account and you are paying a cross-account API hop on every single event. More IAM surface, more things to break.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option C: hub-and-spoke, enriched from the Config Aggregator.&lt;/strong&gt; This is the one. All the logic sits in the hub. Spokes just forward. And instead of building anything to fetch the IP, the hub asks the Config Aggregator, which is already sitting there aggregating EC2 config (IP, tags, state) from every account and region.&lt;/p&gt;
&lt;p&gt;Option C won for a boring but decisive reason: the hard part, the enrichment data, already existed. I did not have to build it. I just had to notice it was there.&lt;/p&gt;
&lt;h3 id=&quot;how-it-fits-together&quot;&gt;How it fits together&lt;/h3&gt;
&lt;p&gt;Spokes forward EC2 state changes to the hub's custom bus; the hub enriches via the org Config Aggregator, then calls the DDI system. Spokes are dumb, the hub is smart, and the data already exists. That is the whole idea in one line.&lt;/p&gt;
&lt;p&gt;That second hub rule is easy to forget, by the way. If you only listen on the custom bus, the hub account's own instances never get DNS records, because they never leave home to reach the custom bus. Ask me how I know.&lt;/p&gt;
&lt;h3 id=&quot;filling-in-the-blanks&quot;&gt;Filling in the blanks&lt;/h3&gt;
&lt;p&gt;Most single-account tutorials skip this bit, which is exactly the bit that matters at scale. Here is what the event actually gives you:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;source&quot;: &quot;aws.ec2&quot;,
  &quot;detail-type&quot;: &quot;EC2 Instance State-change Notification&quot;,
  &quot;detail&quot;: {
    &quot;instance-id&quot;: &quot;i-1234567890&quot;,
    &quot;state&quot;: &quot;running&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An ID and a state. No IP, no tags, no hostname. So the Lambda goes and gets the rest, from the aggregator, not from the source account.&lt;/p&gt;
&lt;p&gt;The nice thing: the aggregator is org-level, so one query resolves an instance no matter which account or region it lives in. No assume-role, no per-account permissions to manage.&lt;/p&gt;
&lt;h3 id=&quot;the-code&quot;&gt;The code&lt;/h3&gt;
&lt;p&gt;Everything here is vendor-neutral and anonymized. Swap the Infoblox call for whatever you actually talk to: Route 53, a CMDB, an IPAM API.&lt;/p&gt;
&lt;h4 id=&quot;the-spoke-s-one-and-only-rule-terraform-&quot;&gt;The spoke's one and only rule (Terraform)&lt;/h4&gt;
&lt;p&gt;This is the entire per-account footprint. Ship it with StackSets or a Terraform module.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# --- Deployed in EVERY spoke account/region ---

resource &quot;aws_cloudwatch_event_rule&quot; &quot;forward_ec2_state&quot; {
  name           = &quot;forward-ec2-state-to-hub&quot;
  description    = &quot;Forward EC2 running/terminated events to the hub account&quot;
  event_bus_name = &quot;default&quot;

  event_pattern = jsonencode({
    source        = [&quot;aws.ec2&quot;]
    &quot;detail-type&quot; = [&quot;EC2 Instance State-change Notification&quot;]
    detail = {
      state = [&quot;running&quot;, &quot;terminated&quot;]
    }
  })
}

resource &quot;aws_cloudwatch_event_target&quot; &quot;to_hub_bus&quot; {
  rule           = aws_cloudwatch_event_rule.forward_ec2_state.name
  event_bus_name = &quot;default&quot;
  arn            = &quot;arn:aws:events:${var.region}:${var.hub_account_id}:event-bus/ec2-dns-hub-bus&quot;
  role_arn       = aws_iam_role.eventbridge_cross_account.arn
}

# Minimal role that lets EventBridge PutEvents to the hub bus
resource &quot;aws_iam_role&quot; &quot;eventbridge_cross_account&quot; {
  name = &quot;eventbridge-forward-to-hub&quot;
  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [{
      Effect    = &quot;Allow&quot;
      Principal = { Service = &quot;events.amazonaws.com&quot; }
      Action    = &quot;sts:AssumeRole&quot;
    }]
  })
}

resource &quot;aws_iam_role_policy&quot; &quot;put_events&quot; {
  role = aws_iam_role.eventbridge_cross_account.id
  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [{
      Effect   = &quot;Allow&quot;
      Action   = &quot;events:PutEvents&quot;
      Resource = &quot;arn:aws:events:${var.region}:${var.hub_account_id}:event-bus/ec2-dns-hub-bus&quot;
    }]
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-hub-s-custom-bus-with-an-org-scoped-resource-policy&quot;&gt;The hub's custom bus, with an org-scoped resource policy&lt;/h4&gt;
&lt;p&gt;This policy is the thing that makes &quot;one rule per spoke&quot; actually work: any account in the org can publish, and the hub needs zero per-account config.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# --- Deployed ONCE in the hub account ---

resource &quot;aws_cloudwatch_event_bus&quot; &quot;hub&quot; {
  name = &quot;ec2-dns-hub-bus&quot;
}

resource &quot;aws_cloudwatch_event_bus_policy&quot; &quot;allow_org&quot; {
  event_bus_name = aws_cloudwatch_event_bus.hub.name
  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [{
      Sid       = &quot;AllowOrgAccountsToPutEvents&quot;
      Effect    = &quot;Allow&quot;
      Principal = &quot;*&quot;
      Action    = &quot;events:PutEvents&quot;
      Resource  = aws_cloudwatch_event_bus.hub.arn
      Condition = {
        StringEquals = {
          &quot;aws:PrincipalOrgID&quot; = var.aws_org_id
        }
      }
    }]
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not skip the &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; condition. It scopes that &lt;code&gt;Principal = &quot;*&quot;&lt;/code&gt; down to your organization instead of, you know, the entire internet. It is the one line standing between &quot;elegant pattern&quot; and &quot;open event bus.&quot;&lt;/p&gt;
&lt;h4 id=&quot;rules-queue-and-lambda-wiring-hub-&quot;&gt;Rules, queue, and Lambda wiring (hub)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;aws_sqs_queue&quot; &quot;events&quot; {
  name                       = &quot;ec2-dns-events&quot;
  visibility_timeout_seconds = 120
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.dlq.arn
    maxReceiveCount     = 5
  })
}

resource &quot;aws_sqs_queue&quot; &quot;dlq&quot; {
  name = &quot;ec2-dns-events-dlq&quot;
}

# Rule on the custom bus (events from spokes)
resource &quot;aws_cloudwatch_event_rule&quot; &quot;hub_custom&quot; {
  name           = &quot;ec2-state-custom-bus&quot;
  event_bus_name = aws_cloudwatch_event_bus.hub.name
  event_pattern = jsonencode({
    source        = [&quot;aws.ec2&quot;]
    &quot;detail-type&quot; = [&quot;EC2 Instance State-change Notification&quot;]
    detail        = { state = [&quot;running&quot;, &quot;terminated&quot;] }
  })
}

resource &quot;aws_cloudwatch_event_target&quot; &quot;custom_to_sqs&quot; {
  rule           = aws_cloudwatch_event_rule.hub_custom.name
  event_bus_name = aws_cloudwatch_event_bus.hub.name
  arn            = aws_sqs_queue.events.arn
}

# Second rule on the default bus catches the hub account's OWN instances
resource &quot;aws_cloudwatch_event_rule&quot; &quot;hub_default&quot; {
  name           = &quot;ec2-state-default-bus&quot;
  event_bus_name = &quot;default&quot;
  event_pattern = jsonencode({
    source        = [&quot;aws.ec2&quot;]
    &quot;detail-type&quot; = [&quot;EC2 Instance State-change Notification&quot;]
    detail        = { state = [&quot;running&quot;, &quot;terminated&quot;] }
  })
}

resource &quot;aws_cloudwatch_event_target&quot; &quot;default_to_sqs&quot; {
  rule = aws_cloudwatch_event_rule.hub_default.name
  arn  = aws_sqs_queue.events.arn
}

resource &quot;aws_lambda_event_source_mapping&quot; &quot;sqs_to_lambda&quot; {
  event_source_arn = aws_sqs_queue.events.arn
  function_name    = aws_lambda_function.dns_registrar.arn
  batch_size       = 10
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-enrichment-lambda-python-&quot;&gt;The enrichment Lambda (Python)&lt;/h4&gt;
&lt;p&gt;This is the heart of it: resolve the instance against the Config Aggregator, then act on the state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
import boto3

config = boto3.client(&quot;config&quot;)

AGGREGATOR_NAME = &quot;OrganizationConfigAggregator&quot;

def get_instance_details(instance_id: str) -&amp;gt; dict | None:
    &quot;&quot;&quot;Resolve IP, tags, account, and region from the org Config Aggregator.&quot;&quot;&quot;
    expression = (
        &quot;SELECT resourceId, accountId, awsRegion, &quot;
        &quot;configuration.privateIpAddress, &quot;
        &quot;configuration.publicIpAddress, tags &quot;
        f&quot;WHERE resourceId = '{instance_id}' &quot;
        &quot;AND resourceType = 'AWS::EC2::Instance'&quot;
    )
    resp = config.select_aggregate_resource_config(
        Expression=expression,
        ConfigurationAggregatorName=AGGREGATOR_NAME,
        Limit=1,
    )
    results = resp.get(&quot;Results&quot;, [])
    return json.loads(results[0]) if results else None


def handler(event, _context):
    for record in event[&quot;Records&quot;]:
        body = json.loads(record[&quot;body&quot;])
        detail = body[&quot;detail&quot;]
        instance_id = detail[&quot;instance-id&quot;]
        state = detail[&quot;state&quot;]

        details = get_instance_details(instance_id)
        if not details:
            # Terminated instances may already be gone from the aggregator;
            # fall back to event data for deregistration.
            if state == &quot;terminated&quot;:
                deregister_dns(instance_id)
            continue

        ip = (
            details.get(&quot;configuration&quot;, {}).get(&quot;privateIpAddress&quot;)
            or details.get(&quot;configuration&quot;, {}).get(&quot;publicIpAddress&quot;)
        )

        if state == &quot;running&quot; and ip:
            register_dns(instance_id, ip, details.get(&quot;tags&quot;, []))
        elif state == &quot;terminated&quot;:
            deregister_dns(instance_id)


def register_dns(instance_id, ip, tags):
    # Replace with your Infoblox / Route 53 / IPAM call.
    # Infoblox example: POST to the WAPI record:a endpoint.
    print(f&quot;REGISTER {instance_id} -&amp;gt; {ip}&quot;)


def deregister_dns(instance_id):
    print(f&quot;DEREGISTER {instance_id}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing worth calling out: that &lt;code&gt;SELECT&lt;/code&gt; against the aggregator resolves a resource across every account and region in a single call. That is the capability that collapses the whole cross-account enrichment headache into a one-liner. I keep coming back to it for other problems now.&lt;/p&gt;
&lt;p&gt;And notice the fallback in the handler. A terminated instance has often already dropped out of the aggregator by the time you look, so you cannot rely on the lookup for deregistration. You fall back to the instance ID from the event. Small thing, but it will bite you in testing if you miss it.&lt;/p&gt;
&lt;h3 id=&quot;why-there-is-an-sqs-queue-in-the-middle&quot;&gt;Why there is an SQS queue in the middle&lt;/h3&gt;
&lt;p&gt;You can target the Lambda straight from the rule, and for a demo I would. In production I put SQS in between on purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A bulk launch, a scale-out, a StackSet rollout, can throw hundreds of events at you at once. SQS soaks that up and lets the Lambda drain at its own pace instead of getting hammered.&lt;/li&gt;
&lt;li&gt;When Infoblox or the aggregator has a bad minute, messages retry and eventually land in a DLQ you can actually look at. Nothing quietly disappears.&lt;/li&gt;
&lt;li&gt;Batching. Ten events per invocation, fewer cold starts, lower cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Decoupling ingest from processing is most of what separates &quot;worked in the demo&quot; from &quot;still working at 2am during a mass launch.&quot;&lt;/p&gt;
&lt;h3 id=&quot;what-actually-came-out-of-it&quot;&gt;What actually came out of it&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Detection and DNS updates happened within seconds, across the whole org.&lt;/li&gt;
&lt;li&gt;Adding an account or region was a single forwarding rule. No Lambda, no queue, no Config setup per account.&lt;/li&gt;
&lt;li&gt;Registration and cleanup were fully automated across the instance lifecycle, nobody touching DNS by hand.&lt;/li&gt;
&lt;li&gt;Cost tracked instance activity, not the clock, because there is no idle compute.&lt;/li&gt;
&lt;li&gt;The shape is reusable. Same hub-spoke plus aggregator-enrichment works for CMDB sync, security tagging, IPAM updates, anything org-wide and event-driven.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-part-worth-stealing&quot;&gt;The part worth stealing&lt;/h3&gt;
&lt;p&gt;Take Infoblox out of the picture and you are left with a pattern I now reach for often:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Fan org-wide events into one hub, then enrich them from a data source the organization already has, instead of building a new one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two moves do the work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A custom event bus with an &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; resource policy gives you cross-account fan-in and a one-rule spoke footprint.&lt;/li&gt;
&lt;li&gt;The Config Aggregator, used as an enrichment lookup, resolves any resource in any account or region without an assume-role fan-out.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So the next time you are about to copy the same Lambda into 40 accounts, stop and ask whether a fan-in plus an aggregator you already run could do it with one rule per account instead. Usually it can.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Honestly, none of the individual pieces here are clever. Cross-account EventBridge is documented. Config aggregators are documented. The win was in composing them so the per-account cost dropped to near zero and the enrichment data was something we already had lying around. That is where most of the good architecture calls seem to live, not in the shiny new component, but in the one you talked yourself out of building.&lt;/p&gt;
&lt;p&gt;If you end up adapting the enrichment step to Route 53, a CMDB, or your own IPAM, I am happy to talk through it in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32736/organization-wide-ec2-dns-automation-with-eventbridge-aws-config-aggregator-rule-account</guid>
<pubDate>Tue, 27 Oct 2026 19:19:28 +0000</pubDate>
</item>
<item>
<title>TruffleHog Secret Scanner Automation</title>
<link>https://coderlegion.com/32727/trufflehog-secret-scanner-automation</link>
<description>&lt;h4 id=&quot;context-and-problem&quot;&gt;Context and Problem&lt;/h4&gt;
&lt;p&gt;An AWS EKS Cluster was attacked, and its secrets were compromised. The Kubernetes Secrets were not encrypted, anyone who works with Kubernetes know they are only &lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/concepts/configuration/secret/#working-with-secrets&quot;&gt;base64-encoded by default&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;task&quot;&gt;Task&lt;/h4&gt;
&lt;p&gt;I had a mission to scan 115 repositories across GitHub and Azure DevOps for secrets exposed anywhere in the Git history.&lt;/p&gt;
&lt;p&gt;The goal was simple: identify exposed secrets and provide actionable reports for the engineering team to fix them.&lt;/p&gt;
&lt;p&gt;A colleague recommended a tool called &lt;a rel=&quot;nofollow&quot; href=&quot;https://trufflesecurity.com/trufflehog&quot;&gt;TruffleHog&lt;/a&gt;, which became the foundation of the workflow.&lt;/p&gt;
&lt;h4 id=&quot;trufflehog&quot;&gt;TruffleHog&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;TruffleHog is a secrets scanning tool that digs deep into your code repositories to find secrets, passwords, and sensitive keys.&quot;&lt;/p&gt;
&lt;p&gt;— TruffleHog official website&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;my-workflow&quot;&gt;My Workflow&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Read the TruffleHog documentation;&lt;/li&gt;
&lt;li&gt;Ran a test in a personal GitHub repository to see how the tool works;&lt;/li&gt;
&lt;li&gt;Check the total amount of repositories to scan, 115;&lt;/li&gt;
&lt;li&gt;Designed how I was going to complete the Task:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;- Decided to automate the process using Bash and jq.
- The script would receive the repository URL as a parameter.
- Used jq to filter and mask findings.
- Used jq to generate separate JSON files for active and inactive secrets.
- Generated standardized Notion reports to support remediation workflows.
- Used the generated JSON data with Gemini to create the reports.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;report-example&quot;&gt;Report Example&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7tdrh77o38r464g7mra5.png&quot; alt=&quot;Report Example&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;- Standardizing reports made remediation much faster.
- Active and inactive secrets should be separated to improve prioritization.
- Automation becomes essential when dealing with dozens of repositories.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-bash-and-jq-&quot;&gt;Why Bash and jq?&lt;/h4&gt;
&lt;p&gt;Keep the workflow simple and easy to run.&lt;/p&gt;
&lt;p&gt;jq made it easy to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;- filter findings
- mask sensitive values
- split active and inactive secrets
- generate structured outputs
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;check-the-code&quot;&gt;Check the code&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ja-lourenco/trufflehog-scan&quot;&gt;https://github.com/ja-lourenco/trufflehog-scan&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32727/trufflehog-secret-scanner-automation</guid>
<pubDate>Tue, 27 Oct 2026 16:34:53 +0000</pubDate>
</item>
<item>
<title>A week of green runs, and the pipeline was waiting for me</title>
<link>https://coderlegion.com/32698/a-week-of-green-runs-and-the-pipeline-was-waiting-for-me</link>
<description>&lt;p&gt;On 1 September at 10:57 UTC the monthly ingest ran. It opened issue number 3 of the magazine, picked the vertical from the source registry, wrote 19 candidate sources into Postgres with &lt;code&gt;stage = 'discovery'&lt;/code&gt;, and opened a GitHub issue telling me it was my turn.&lt;/p&gt;
&lt;p&gt;Every morning after that, the daily job ran and succeeded. Eight consecutive green runs, from the first of the month to the eighth. The article count for September stayed at zero the whole time.&lt;/p&gt;
&lt;h3 id=&quot;the-gate-was-doing-its-job&quot;&gt;The gate was doing its job&lt;/h3&gt;
&lt;p&gt;The daily job does not decide anything by itself. It asks the database which stage to run, and the answer depends on a view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, issue_id, source_url, tier, independent
FROM signals
WHERE stage = 'verify' AND (tier = 1 OR tier = 2 AND independent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing satisfies that view until I sit in Supabase Studio and tag the sources by hand. Tier 1 is a primary source, a regulator or the organisation that ran the thing. Tier 2 counts only when it is independent of whoever it describes, which rules out a consultancy writing up its own project as a case study.&lt;/p&gt;
&lt;p&gt;That judgement is mine on purpose. It is the editorial bar of the magazine, and I did not want a model deciding what counts as proof of anything.&lt;/p&gt;
&lt;p&gt;So the decision function found a draft issue with no verified signals, returned &lt;code&gt;niente&lt;/code&gt;, and printed its reason to stderr:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;advance: signal in attesa di verifica in Studio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it exited zero, which is correct. There was nothing for it to do.&lt;/p&gt;
&lt;h3 id=&quot;what-my-alarms-actually-watch&quot;&gt;What my alarms actually watch&lt;/h3&gt;
&lt;p&gt;I have three, and I was fairly pleased with them.&lt;/p&gt;
&lt;p&gt;A cron that fails opens a GitHub issue on its own. A cron that never starts is caught by a daily watchman that asks the GitHub API when each schedule last fired, because a schedule that GitHub has quietly disabled produces no failure to report. And because a watchman inside the same failure domain is not a watchman, that job also checks in with a Sentry monitor that alarms on silence from outside GitHub.&lt;/p&gt;
&lt;p&gt;Between them they cover a job that breaks and a job that never runs. Neither of those happened. What happened was a job that ran, succeeded, and did nothing, for seven days, because it was waiting for a person who had not noticed.&lt;/p&gt;
&lt;h3 id=&quot;the-information-was-already-there&quot;&gt;The information was already there&lt;/h3&gt;
&lt;p&gt;The part I find annoying is that nothing was hidden. The reason is written every single morning, in the run log, in Italian, on stderr. The job knows exactly why it is idle and says so out loud.&lt;/p&gt;
&lt;p&gt;What it does not know is how long it has been saying it. &lt;code&gt;niente&lt;/code&gt; on the second of the month and &lt;code&gt;niente&lt;/code&gt; on the eighth are the same string, and the only thing that separates them is a week of my attention going somewhere else. No alarm reads that log line, and no counter turns it into a duration.&lt;/p&gt;
&lt;p&gt;Green means the machine is healthy. I had been reading it as &quot;the work is moving&quot;, and those are different claims.&lt;/p&gt;
&lt;h3 id=&quot;how-it-ended&quot;&gt;How it ended&lt;/h3&gt;
&lt;p&gt;On the eighth I opened Studio and tagged 11 of the 19 sources: two from the regulator as tier 1, nine analyst reports as tier 2 and independent. I left out a vendor case study, which fails the independence test by construction, and seven security bulletins whose scraped body is only a title.&lt;/p&gt;
&lt;p&gt;After that the pipeline did the rest in a few minutes. Generate wrote the piece from eight sources, embed rebuilt the chunks, export produced the two markdown files, and the judge on the pull request failed me for weak attribution, which is a story for another post.&lt;/p&gt;
&lt;p&gt;The September issue went out a week late. Nothing had broken, and every run had been green.&lt;/p&gt;
&lt;p&gt;So I do not need a fourth alarm. I need the daily job to count the mornings it has spent answering &lt;code&gt;niente&lt;/code&gt;, and to say that number where I actually look.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32698/a-week-of-green-runs-and-the-pipeline-was-waiting-for-me</guid>
<pubDate>Tue, 27 Oct 2026 16:02:11 +0000</pubDate>
</item>
<item>
<title>Node 22.22 runs TypeScript. Ubuntu's Node 22.22 doesn't.</title>
<link>https://coderlegion.com/32697/node-22-22-runs-typescript-ubuntus-node-22-22-doesnt</link>
<description>&lt;p&gt;Four test files in my site's repo import TypeScript directly. &lt;code&gt;faq.test.mjs&lt;/code&gt; pulls in &lt;code&gt;src/lib/faq.ts&lt;/code&gt;, and three others do the same with their own modules. There is no build step and no loader in between: the script is &lt;code&gt;node --test &quot;test/*.test.mjs&quot;&lt;/code&gt;, and type stripping has been on by default since Node 22.18.0, so Node strips the types and runs what's left.&lt;/p&gt;
&lt;p&gt;In CI those tests pass. The workflow asks &lt;code&gt;actions/setup-node&lt;/code&gt; for &lt;code&gt;node-version: &quot;22&quot;&lt;/code&gt;, and the latest runs on &lt;code&gt;main&lt;/code&gt; are green.&lt;/p&gt;
&lt;p&gt;On my Ubuntu server, with Node 22.22.1, all four fail before a single assertion runs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension &quot;.ts&quot; for .../src/lib/faq.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;22.22.1 is newer than 22.18.0, so on paper the feature is there. &lt;strong&gt;The version number was right. The binary behind it was a different build.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;same-version-different-binary&quot;&gt;Same version, different binary&lt;/h3&gt;
&lt;p&gt;Node will tell you whether it strips types. The v22 docs describe &lt;code&gt;process.features.typescript&lt;/code&gt; as &lt;code&gt;&quot;strip&quot;&lt;/code&gt; by default, &lt;code&gt;&quot;transform&quot;&lt;/code&gt; with &lt;code&gt;--experimental-transform-types&lt;/code&gt;, and &lt;code&gt;false&lt;/code&gt; if Node is started with &lt;code&gt;--no-experimental-strip-types&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I did not pass that flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ /usr/bin/node --version
v22.22.1
$ /usr/bin/node -p 'process.features.typescript'
false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;node&lt;/code&gt; comes from Ubuntu's &lt;code&gt;nodejs&lt;/code&gt; package, version &lt;code&gt;22.22.1+dfsg+~cs22.19.15-1ubuntu1&lt;/code&gt;. Asking for the feature explicitly does not bring it back:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ /usr/bin/node --experimental-strip-types t.ts
node:internal/util:226
    throw new ERR_NO_TYPESCRIPT();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To rule out the version itself, I downloaded the official 22.22.1 build from nodejs.org, checked it against &lt;code&gt;SHASUMS256.txt&lt;/code&gt;, and asked the same question. It printed &lt;code&gt;strip&lt;/code&gt; and ran the same &lt;code&gt;.ts&lt;/code&gt; file without complaint.&lt;/p&gt;
&lt;h3 id=&quot;where-typescript-went&quot;&gt;Where TypeScript went&lt;/h3&gt;
&lt;p&gt;The package's own changelog explains most of it. The December 2024 entry, &lt;code&gt;22.12.0+dfsg-1&lt;/code&gt;, says:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;dfsg-exclude amaro, build without it - it requires swc. This disables the ability to execute TypeScript files using the --experimental-strip-types flag.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The November 2025 entry, &lt;code&gt;22.21.1+dfsg+~cs22.19.0-1&lt;/code&gt;, says: &quot;we build without-amaro for now, disable strip-types&quot;.&lt;/p&gt;
&lt;p&gt;The March 2026 Debian entry, merged into the version I have, says: &quot;Drop 'no amaro' patch, solved upstream&quot;. The build still answers &lt;code&gt;false&lt;/code&gt;, and it reports how it was compiled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ /usr/bin/node -p 'process.config.variables.node_use_amaro'
false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The official 22.22.1 build prints &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The changelog doesn't say what &quot;solved upstream&quot; changed. My tests only see what the build does.&lt;/p&gt;
&lt;h3 id=&quot;why-ci-never-saw-it&quot;&gt;Why CI never saw it&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;setup-node&lt;/code&gt; README says the action first checks the runner's tool cache, then pulls LTS versions from the &lt;code&gt;actions/node-versions&lt;/code&gt; releases and, on a miss, falls back to downloading from nodejs.org. None of those is Ubuntu's package. The four test files pass in CI, so the Node there strips types. The one on my server does not, and both report major version 22.&lt;/p&gt;
&lt;p&gt;That is how one repository can be green in CI and red on my server without a line of code changing.&lt;/p&gt;
&lt;h3 id=&quot;the-fix&quot;&gt;The fix&lt;/h3&gt;
&lt;p&gt;I extracted the official Node 22.23.2 tarball into &lt;code&gt;/usr/local&lt;/code&gt;. &lt;code&gt;/usr/local/bin&lt;/code&gt; comes before &lt;code&gt;/usr/bin&lt;/code&gt; in &lt;code&gt;PATH&lt;/code&gt;, so &lt;code&gt;node&lt;/code&gt; now resolves to the official build, with npm 10.9.8 bundled.&lt;/p&gt;
&lt;p&gt;I left the Ubuntu package installed. On this machine 15 installed packages depend on &lt;code&gt;nodejs&lt;/code&gt;: &lt;code&gt;handlebars&lt;/code&gt; and 14 &lt;code&gt;node-*&lt;/code&gt; libraries. Removing it to get the right &lt;code&gt;node&lt;/code&gt; would have meant fighting apt over things that have nothing to do with my site.&lt;/p&gt;
&lt;p&gt;Same four files, official binary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tap&quot;&gt;# tests 42
# pass 42
# fail 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ask-node-what-it-can-do&quot;&gt;Ask Node what it can do&lt;/h3&gt;
&lt;p&gt;If a project relies on type stripping, &lt;code&gt;node --version&lt;/code&gt; is not the check. This is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;node -p 'process.features.typescript'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the two builds above it prints &lt;code&gt;strip&lt;/code&gt; when the feature works and &lt;code&gt;false&lt;/code&gt; when it doesn't, whatever the version string says. It belongs in a setup script next to the version check, because on my server the version check was the one that passed.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32697/node-22-22-runs-typescript-ubuntus-node-22-22-doesnt</guid>
<pubDate>Tue, 27 Oct 2026 14:27:58 +0000</pubDate>
</item>
<item>
<title>Three ways a jq one-liner lies to your bash script</title>
<link>https://coderlegion.com/28027/three-ways-a-jq-one-liner-lies-to-your-bash-script</link>
<description>&lt;p&gt;Most &quot;the filter looks right but the script misbehaves&quot; bugs come down to three silent failure modes.&lt;/p&gt;
&lt;p&gt;First, a missing &lt;code&gt;-r&lt;/code&gt;. jq outputs JSON by default, so a string arrives with its quotes attached — your variable holds &lt;code&gt;&quot;web-01&quot;&lt;/code&gt;, quotes included, and a comparison against web-01 never matches. Any value headed into a shell variable, filename, or test wants &lt;code&gt;-r&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Second, missing keys don't error. jq emits &lt;code&gt;null&lt;/code&gt;, which lands in bash as a literal four-character string — non-empty, so &lt;code&gt;[ -n ]&lt;/code&gt; guards pass and backup-null.tar.gz gets written. The &lt;code&gt;//&lt;/code&gt; operator supplies a real fallback.&lt;/p&gt;
&lt;p&gt;Third, select() compares exactly. A value that is almost right matches nothing, and nothing is not an error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;jq -r '.items[] | select(.active == true) | .name'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the API sends &lt;code&gt;&quot;active&quot;: &quot;true&quot;&lt;/code&gt; as a string, that item is skipped. If you match &lt;code&gt;&quot;Web-01&quot;&lt;/code&gt; against &lt;code&gt;&quot;web-01&quot;&lt;/code&gt;, zero elements come back. Either way jq prints nothing and exits 0, so the silence reads as success.&lt;/p&gt;
&lt;p&gt;All three share the same fingerprint: exit code zero, plausible output, wrong result. The fastest way I've found to catch them before they ship is building the filter against the actual response: this &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/jq-filter-builder&quot;&gt;interactive jq filter builder with a live in-browser preview&lt;/a&gt; lets you click through your real JSON to build the path, handles the &lt;code&gt;-r&lt;/code&gt;, the &lt;code&gt;//&lt;/code&gt; fallback and select() quoting for you, and shows exactly what the filter emits before anything runs.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28027/three-ways-a-jq-one-liner-lies-to-your-bash-script</guid>
<pubDate>Tue, 27 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>How I Built a 24/7 AI DevOps Coding Agent for About CAD 200</title>
<link>https://coderlegion.com/32625/how-i-built-a-24-7-ai-devops-coding-agent-for-about-cad-200</link>
<description>&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Amiri83/hermis/main/docs/images/24_7_ai_devops_agent_setup.png&quot; alt=&quot;My 24/7 AI DevOps Coding Agent Setup&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Hermes + Claude Code + Telegram on a cheap always-on thin client.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;hi-i-m-amir&quot;&gt;Hi, I'm Amir&lt;/h3&gt;
&lt;p&gt;I'm a DevOps engineer, and a surprising amount of my time goes into work that is important but repetitive.&lt;/p&gt;
&lt;p&gt;A server needs patching. A CI/CD pipeline fails. A script needs a small change. A Terraform module needs an update. A tool I built needs one more feature. Tests need to be run, branches need to be checked, and somebody still has to make sure the generated code actually works.&lt;/p&gt;
&lt;p&gt;None of those jobs is necessarily difficult on its own. The annoying part is the &lt;strong&gt;constant context switching&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I kept thinking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What if I had a small AI worker that was always online, understood my repositories, could call a real coding agent, verify the result, manage Git, and report back to me on my phone?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is what this setup became.&lt;/p&gt;
&lt;p&gt;I use &lt;strong&gt;Hermes&lt;/strong&gt; as the agentic AI harness and orchestrator. Hermes is connected to &lt;strong&gt;OpenRouter&lt;/strong&gt;, where I use a low-cost &lt;strong&gt;DeepSeek Flash v4&lt;/strong&gt; model for the orchestration layer. When real coding work is needed, Hermes delegates that work to the &lt;strong&gt;Claude Code CLI&lt;/strong&gt;, which is my main coding agent.&lt;/p&gt;
&lt;p&gt;The split is intentional:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Claude helps me think through the problem. Claude Code does the main coding. Hermes runs the workflow, performs QA, manages Git, pushes the repo, and keeps me updated through Telegram.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is not meant to replace me as an engineer. It is meant to remove the repetitive overhead around the work.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-i-divide-the-work-between-claude-hermes-and-claude-code&quot;&gt;How I divide the work between Claude, Hermes and Claude Code&lt;/h3&gt;
&lt;p&gt;I deliberately &lt;strong&gt;do not ask one AI agent to do everything&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When I have a new idea, bug, or feature, I normally start by talking it through with &lt;strong&gt;Claude&lt;/strong&gt;. I explain what I want, what is currently broken, what constraints matter, and what I do &lt;em&gt;not&lt;/em&gt; want changed.&lt;/p&gt;
&lt;p&gt;Claude helps me turn that into a clear, compact prompt for Hermes.&lt;/p&gt;
&lt;p&gt;Then I hand the task over.&lt;/p&gt;
&lt;h4 id=&quot;claude-planning-and-shaping-the-task&quot;&gt;Claude: planning and shaping the task&lt;/h4&gt;
&lt;p&gt;Claude is where I usually work through questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What exactly should change?&lt;/li&gt;
&lt;li&gt;What should stay untouched?&lt;/li&gt;
&lt;li&gt;What edge case am I forgetting?&lt;/li&gt;
&lt;li&gt;What is the smallest useful test?&lt;/li&gt;
&lt;li&gt;What should count as PASS?&lt;/li&gt;
&lt;li&gt;What instructions does Hermes actually need?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don't want a giant five-page prompt. By the time I send the task to Hermes, the thinking should already be distilled into something actionable.&lt;/p&gt;
&lt;h4 id=&quot;hermes-orchestration-qa-and-git-management&quot;&gt;Hermes: orchestration, QA and Git management&lt;/h4&gt;
&lt;p&gt;Hermes is my &lt;strong&gt;manager layer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Its job is not to out-code Claude Code. Its job is to keep the job moving.&lt;/p&gt;
&lt;p&gt;In my workflow Hermes is responsible for things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;checking the current repo and branch&lt;/li&gt;
&lt;li&gt;launching the coding agent&lt;/li&gt;
&lt;li&gt;monitoring the task&lt;/li&gt;
&lt;li&gt;running the relevant tests itself&lt;/li&gt;
&lt;li&gt;verifying that the result actually matches the request&lt;/li&gt;
&lt;li&gt;checking Git status&lt;/li&gt;
&lt;li&gt;committing only after the task passes&lt;/li&gt;
&lt;li&gt;pushing the branch&lt;/li&gt;
&lt;li&gt;reporting PASS / FAIL, issues and next steps back to Telegram&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That separation matters to me:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The agent that writes the code is not the same layer that decides the job is finished.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;claude-code-the-main-coding-agent&quot;&gt;Claude Code: the main coding agent&lt;/h4&gt;
&lt;p&gt;When the work gets into the actual codebase, &lt;strong&gt;Claude Code CLI&lt;/strong&gt; is my primary coding agent.&lt;/p&gt;
&lt;p&gt;That is where I want the heavier coding intelligence spent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reading the codebase&lt;/li&gt;
&lt;li&gt;implementing features&lt;/li&gt;
&lt;li&gt;fixing bugs&lt;/li&gt;
&lt;li&gt;changing tests&lt;/li&gt;
&lt;li&gt;debugging failures&lt;/li&gt;
&lt;li&gt;reasoning about implementation details&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hermes stays relatively lightweight and cheap. Claude Code does the expensive coding work only when I actually need it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-real-prompt-from-my-workflow&quot;&gt;A real prompt from my workflow&lt;/h3&gt;
&lt;p&gt;Here is a real example.&lt;/p&gt;
&lt;p&gt;After discussing the issue with Claude, this was the kind of short prompt I sent to Hermes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Amiri83/hermis/main/docs/images/claude-to-hermes-prompt.png&quot; alt=&quot;Claude to Hermes implementation prompt&quot;&gt;&lt;/p&gt;
&lt;p&gt;In text form:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;On feature/patch-all: after app restart, an analysis left RUNNING is not
marked INTERRUPTED and Analyze/Patch stay disabled. Startup recovery from
faa2ae6 isn't working on a real DB (schema v7, existing runs). Fix + test
with a DB containing a RUNNING analysis before startup.
Full suite + ruff. Commit after PASS, push. Report PASS/FAIL | issues | next.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I like this kind of prompt because it is short but still gives Hermes everything it needs:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;where the problem is → what the failure looks like → what must be tested → when Git operations are allowed → what I want reported back.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hermes-does-not-just-trust-the-coding-agent&quot;&gt;Hermes does not just trust the coding agent&lt;/h3&gt;
&lt;p&gt;This is the part that made the setup useful for me rather than just interesting.&lt;/p&gt;
&lt;p&gt;After Claude Code finishes, Hermes still has work to do.&lt;/p&gt;
&lt;p&gt;Here is a real result from my Telegram workflow:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Amiri83/hermis/main/docs/images/hermes-qa-git-result.png&quot; alt=&quot;Hermes QA and Git management&quot;&gt;&lt;/p&gt;
&lt;p&gt;In this example Hermes exercised the fallback path, brought up the required containers, reran the integration test against the real environment, cleaned things up, and then handled the Git commit.&lt;/p&gt;
&lt;p&gt;That is exactly the kind of repetitive workflow I want the orchestrator doing for me.&lt;/p&gt;
&lt;p&gt;I want Claude Code focused on implementation.&lt;/p&gt;
&lt;p&gt;I want Hermes focused on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;implement
   ↓
verify
   ↓
test
   ↓
inspect
   ↓
commit
   ↓
push
   ↓
report
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;telegram-is-my-remote-control&quot;&gt;Telegram is my remote control&lt;/h3&gt;
&lt;p&gt;Telegram is what makes the whole setup feel different from simply running an AI coding tool on my laptop.&lt;/p&gt;
&lt;p&gt;I can be somewhere else and send something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Check the latest branch.

Have Claude fix the regression.

Run the targeted tests.

If everything passes, commit and push.

Report PASS/FAIL, issues, and next.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I can put my phone away.&lt;/p&gt;
&lt;p&gt;The actual work is happening on the thin client at home.&lt;/p&gt;
&lt;p&gt;I don't need a remote desktop session. I don't need VS Code open. I don't need my laptop to stay awake.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Telegram is just the control surface.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-hardware-cost-me-about-cad-200&quot;&gt;The hardware cost me about CAD 200&lt;/h3&gt;
&lt;p&gt;I deliberately did &lt;strong&gt;not&lt;/strong&gt; build an expensive home server for this.&lt;/p&gt;
&lt;p&gt;The machine only needs enough resources to run Linux, Hermes, Git, development tools, containers when needed, and the local Claude Code CLI. The heavy LLM computation happens remotely.&lt;/p&gt;
&lt;p&gt;I bought a used &lt;strong&gt;Dell Wyse 5070 thin client for about CAD 120&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Part &lt;/th&gt;&lt;th&gt;My setup &lt;/th&gt;&lt;th&gt;Approx. cost&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Thin client &lt;/td&gt;&lt;td&gt;Dell Wyse 5070 &lt;/td&gt;&lt;td&gt;CAD 120&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CPU &lt;/td&gt;&lt;td&gt;Intel Celeron J4105, 4 cores &lt;/td&gt;&lt;td&gt;included&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;RAM &lt;/td&gt;&lt;td&gt;4 GB physical RAM &lt;/td&gt;&lt;td&gt;included&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Internal storage &lt;/td&gt;&lt;td&gt;32 GB &lt;/td&gt;&lt;td&gt;included&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Wireless &lt;/td&gt;&lt;td&gt;Linux-compatible USB Wi-Fi adapter &lt;/td&gt;&lt;td&gt;CAD 20&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;External storage &lt;/td&gt;&lt;td&gt;256 GB external SSD &lt;/td&gt;&lt;td&gt;CAD 60&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Approx. total&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;CAD 200&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The original 32 GB storage was too small once Docker, repositories, package caches and development tools entered the picture.&lt;/p&gt;
&lt;p&gt;I mounted the external SSD at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and moved the storage-heavy workloads there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/data
├── docker
├── containerd
├── projects
└── swapfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My project path points to the SSD:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;~/projects -&amp;gt; /data/projects
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Wyse has &lt;strong&gt;4 GB of physical RAM&lt;/strong&gt;, so I also added a &lt;strong&gt;4 GB swap file&lt;/strong&gt; on the external SSD.&lt;/p&gt;
&lt;p&gt;That gives the OS 4 GB RAM + 4 GB swap to work with.&lt;/p&gt;
&lt;p&gt;Swap is obviously &lt;strong&gt;not the same as real RAM&lt;/strong&gt;, but for a cheap orchestration box it gives me useful headroom during temporary spikes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;installing-hermes-on-ubuntu-26-04&quot;&gt;Installing Hermes on Ubuntu 26.04&lt;/h3&gt;
&lt;p&gt;I run the box on &lt;strong&gt;Ubuntu 26.04&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For Hermes itself, I followed the official installation method from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://hermes-agent.nousresearch.com/&quot;&gt;Hermes Agent website&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After installation, the important pieces in my setup are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Hermes Agent&lt;/strong&gt; running on the thin client.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenRouter&lt;/strong&gt; as the model provider for Hermes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DeepSeek Flash v4&lt;/strong&gt; as my low-cost orchestration model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Telegram&lt;/strong&gt; connected so I can talk to Hermes remotely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code CLI&lt;/strong&gt; installed and authenticated as the main coding agent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git / GitHub access&lt;/strong&gt; configured so Hermes can manage the repository workflow.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The main idea is simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Spend the expensive model on the coding problem. Keep orchestration lightweight.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-happens-when-the-first-coding-attempt-fails-&quot;&gt;What happens when the first coding attempt fails?&lt;/h3&gt;
&lt;p&gt;One thing I did not want was this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Claude Code says &quot;done&quot;
        ↓
Hermes trusts it
        ↓
bad code gets pushed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That defeats the point of having an orchestration layer.&lt;/p&gt;
&lt;p&gt;My preferred flow looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/n373ihl75dwienho5oga.png&quot; alt=&quot;Hermes failure and retry flow&quot;&gt;&lt;/p&gt;
&lt;p&gt;If QA fails, Hermes has evidence it can hand back to the coding agent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;failing tests&lt;/li&gt;
&lt;li&gt;command output&lt;/li&gt;
&lt;li&gt;wrong behavior&lt;/li&gt;
&lt;li&gt;dirty Git state&lt;/li&gt;
&lt;li&gt;integration failure&lt;/li&gt;
&lt;li&gt;missing expected changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The coding agent gets another chance to fix the actual failure.&lt;/p&gt;
&lt;p&gt;Only after Hermes can reproduce a passing result does the workflow move to commit and push.&lt;/p&gt;
&lt;p&gt;I find that much more useful than simply asking the coding model:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Are you sure it works?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-i-keep-the-running-cost-under-control&quot;&gt;How I keep the running cost under control&lt;/h3&gt;
&lt;p&gt;The hardware cost is only one part of the story.&lt;/p&gt;
&lt;p&gt;What matters more over time is &lt;strong&gt;where I spend model tokens&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Hermes mainly needs to do things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inspect the current state&lt;/li&gt;
&lt;li&gt;decide which tool or agent should run next&lt;/li&gt;
&lt;li&gt;wait for a result&lt;/li&gt;
&lt;li&gt;run tests&lt;/li&gt;
&lt;li&gt;check Git status&lt;/li&gt;
&lt;li&gt;retry when something fails&lt;/li&gt;
&lt;li&gt;report back to me&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For that layer I use a cheaper model through OpenRouter.&lt;/p&gt;
&lt;p&gt;I save Claude Code for the part where the extra intelligence actually matters: &lt;strong&gt;understanding and changing the codebase&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cheap model
    ↓
orchestration / routing / checking

Expensive coding model
    ↓
used only when real coding is required
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also don't need to pay for a cloud VM just to keep the agent online because the Wyse is doing that job at home.&lt;/p&gt;
&lt;p&gt;My ongoing cost is therefore mostly whatever I spend on Claude/Claude Code and OpenRouter usage rather than another always-on server bill.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;guardrails-i-use-before-code-gets-pushed&quot;&gt;Guardrails I use before code gets pushed&lt;/h3&gt;
&lt;p&gt;I want this system to save time, not create a faster way to break things.&lt;/p&gt;
&lt;p&gt;Some rules I like are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No commit before QA passes.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No push before the working tree is in the expected state.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test the changed behavior, not just whether the code imports.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer a small targeted regression test first.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the existing full test suite when appropriate before finalizing.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Report failures instead of hiding or working around them.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep production credentials and private keys outside prompts and public repos.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not let the coding agent silently change unrelated parts of the project.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my personal projects, I also try not to turn every small change into a giant enterprise QA exercise.&lt;/p&gt;
&lt;p&gt;The goal is enough verification to catch the mistake &lt;strong&gt;without spending more time testing than the original task was worth&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-this-setup-works-well-and-where-it-doesn-t&quot;&gt;Where this setup works well — and where it doesn't&lt;/h3&gt;
&lt;p&gt;This setup is a good fit for work that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;repetitive&lt;/li&gt;
&lt;li&gt;repository-based&lt;/li&gt;
&lt;li&gt;testable&lt;/li&gt;
&lt;li&gt;scriptable&lt;/li&gt;
&lt;li&gt;safe to perform from a development machine&lt;/li&gt;
&lt;li&gt;easy to describe with a clear PASS/FAIL condition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;small internal tools&lt;/li&gt;
&lt;li&gt;bug fixes&lt;/li&gt;
&lt;li&gt;repetitive patching utilities&lt;/li&gt;
&lt;li&gt;CI/CD helper scripts&lt;/li&gt;
&lt;li&gt;Terraform changes in non-production environments&lt;/li&gt;
&lt;li&gt;report generators&lt;/li&gt;
&lt;li&gt;automation around Git and testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; something I would blindly point at production and tell:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Do whatever you think is best.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For high-risk production changes, security-sensitive operations, destructive database work, or anything with a large blast radius, I still want explicit human review and normal change controls.&lt;/p&gt;
&lt;p&gt;The agent is useful because it removes repetitive work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It does not remove engineering judgment.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-few-things-i-learned-while-building-it&quot;&gt;A few things I learned while building it&lt;/h3&gt;
&lt;h4 id=&quot;1-the-orchestration-machine-does-not-need-to-be-powerful&quot;&gt;1. The orchestration machine does not need to be powerful&lt;/h4&gt;
&lt;p&gt;At first it is easy to think &quot;AI agent&quot; means &quot;expensive AI hardware.&quot;&lt;/p&gt;
&lt;p&gt;In this design, it doesn't.&lt;/p&gt;
&lt;p&gt;The thin client mostly needs to stay online, run tools reliably, and have enough storage.&lt;/p&gt;
&lt;h4 id=&quot;2-separating-coding-from-verification-is-valuable&quot;&gt;2. Separating coding from verification is valuable&lt;/h4&gt;
&lt;p&gt;Claude Code is very good at coding.&lt;/p&gt;
&lt;p&gt;That does not mean I should automatically accept its own definition of &quot;finished.&quot;&lt;/p&gt;
&lt;p&gt;Having Hermes independently run the checks gives the workflow a much cleaner boundary.&lt;/p&gt;
&lt;h4 id=&quot;3-a-short-prompt-can-be-better-than-a-huge-prompt&quot;&gt;3. A short prompt can be better than a huge prompt&lt;/h4&gt;
&lt;p&gt;Because I discuss the idea first, the prompt I send to Hermes can stay focused.&lt;/p&gt;
&lt;p&gt;That reduces noise and makes failures easier to understand.&lt;/p&gt;
&lt;h4 id=&quot;4-reliability-matters-more-than-raw-speed&quot;&gt;4. Reliability matters more than raw speed&lt;/h4&gt;
&lt;p&gt;The Wyse is not fast.&lt;/p&gt;
&lt;p&gt;But it is always there.&lt;/p&gt;
&lt;p&gt;For this use case, &lt;strong&gt;always available + predictable&lt;/strong&gt; is more valuable to me than having a much faster machine that disappears when I close my laptop.&lt;/p&gt;
&lt;h4 id=&quot;5-the-best-automation-is-the-one-i-actually-use&quot;&gt;5. The best automation is the one I actually use&lt;/h4&gt;
&lt;p&gt;The biggest win is not that the architecture looks clever.&lt;/p&gt;
&lt;p&gt;It is that I can send a task from Telegram, walk away, and come back to a tested branch instead of spending another hour doing repetitive setup and Git work.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;I did not build this because I wanted an AI experiment sitting in a corner of my house.&lt;/p&gt;
&lt;p&gt;I built it because I'm a DevOps engineer and I have repetitive work.&lt;/p&gt;
&lt;p&gt;Sometimes the right answer to a repetitive task is a Bash script.&lt;/p&gt;
&lt;p&gt;Sometimes it is Terraform.&lt;/p&gt;
&lt;p&gt;Sometimes it is a small Python tool.&lt;/p&gt;
&lt;p&gt;And now, sometimes I can simply describe the problem, let my agent workflow build or fix the tool, verify the result, and give me the branch when it is ready.&lt;/p&gt;
&lt;p&gt;My setup has a very simple division of responsibility:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;I bring the problem. Claude helps shape the solution. Claude Code writes the code. Hermes manages the job, verifies it, handles Git, and reports back to me.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Wyse provides the always-on home.&lt;/p&gt;
&lt;p&gt;Telegram gives me access from anywhere.&lt;/p&gt;
&lt;p&gt;The cloud models provide the intelligence.&lt;/p&gt;
&lt;p&gt;A cheap little thin client became my &lt;strong&gt;24/7 AI DevOps coding worker&lt;/strong&gt;. &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Source / setup files:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Amiri83/hermis&quot;&gt;Amiri83/hermis on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Original Gist:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://gist.github.com/Amiri83/cc2d4f619da3096bfc299e2fec68d984&quot;&gt;My 24/7 AI DevOps Coding Agent&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32625/how-i-built-a-24-7-ai-devops-coding-agent-for-about-cad-200</guid>
<pubDate>Tue, 27 Oct 2026 05:18:01 +0000</pubDate>
</item>
<item>
<title>IBM Z Atlas Uses AI to Prove Mainframe Changes Are Safe Before They Hit Production</title>
<link>https://coderlegion.com/32375/ibm-z-atlas-uses-ai-to-prove-mainframe-changes-are-safe-before-they-hit-production</link>
<description>&lt;p&gt;Changing a mainframe is slow for a reason. One middleware upgrade can touch dozens of applications. And the knowledge of how everything connects often lives in the heads of a few senior engineers, many of whom are close to retirement.&lt;/p&gt;
&lt;p&gt;IBM is going after that problem. At its TechXchange conference today, the company announced IBM Z Atlas. It maps a client's entire z/OS stack, from infrastructure to applications to operations. Then it uses that map, plus AI, to help teams plan, test, and document changes before they reach production.&lt;/p&gt;
&lt;p&gt;Atlas enters public preview today. General availability is targeted for Q1 2027.&lt;/p&gt;
&lt;p&gt;I sent Andy Bradfield, VP of IBM Z Hybrid Cloud, questions about how Atlas works, where AI fits, and what developers need to know. I edited his answers for length and clarity.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;What is IBM Z Atlas, and what problem does it solve for developers and architects working on z/OS?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Andy Bradfield:&lt;/strong&gt; Most Z teams can see pieces of their environment today. But no end-to-end solution understands how the entire IBM Z stack fits together, so teams can't understand, change, modernize, or govern mission-critical mainframe environments.&lt;/p&gt;
&lt;p&gt;Z Atlas is an AI-powered system intelligence and change orchestration companion for IBM Z. It maps a client's entire z/OS stack across infrastructure, applications, and operations. It then uses that intelligence to reduce manual effort for tasks like patch management, staff onboarding, and regulatory response.&lt;/p&gt;
&lt;p&gt;The leading cause of unplanned z/OS outages is changes that aren't proven to be safe. Z Atlas addresses this by mapping how a change affects the stack, testing and validating it, and generating the evidence needed for approval. Experts stay in control, and everyone works from the same view of the system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How does a team figure out the impact of a change today, and how is that different with Atlas?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; It often takes weeks to prove a change manually. Understanding the full impact means tracing dependencies across multiple domains and roles in the Z stack. Teams collect information from multiple sources, identify what could be affected, build test plans, and document evidence before they can deploy a change safely.&lt;/p&gt;
&lt;p&gt;It's an arduous process. If it isn't done properly, it slows teams down or adds risk. Some clients cut corners, knowingly or by mistake, and end up testing in production.&lt;/p&gt;
&lt;p&gt;With Z Atlas, teams already have the visibility on screen. They can generate the plan and automatically prove the change and tests. A developer can also spin up an isolated environment to investigate further.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where do the time savings come from?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; Results from our early design program include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;60–80% less time spent gathering context before a change&lt;/li&gt;
&lt;li&gt;Detailed impact analysis and test planning reduced from hours to minutes&lt;/li&gt;
&lt;li&gt;Provisioning and test generation reduced from weeks to hours&lt;/li&gt;
&lt;li&gt;Change evidence that cuts hours of manual tracking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What data does Atlas pull from, and how does it stay current?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; Atlas pulls from existing Z software and ecosystem solutions across system configuration, operations, performance, modernization, and development workflows. It uses configuration-as-code and environment metadata to discover and map relationships across the environment.&lt;/p&gt;
&lt;p&gt;It combines that discovered context with AI-driven analysis and agentic capabilities. The result is shared system intelligence for every team. It doesn't replace the specialized capabilities those other tools provide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where does AI do the work, and where does Atlas rely on deterministic data? How do you keep a wrong AI answer from becoming a production problem?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; Atlas uses AI where reasoning and synthesis add value. That means interpreting what the user wants, connecting context, explaining potential impact, and generating recommended change or validation plans.&lt;/p&gt;
&lt;p&gt;The underlying facts come from deterministic sources. That includes discovered topology, configurations, software levels, dependencies, policies, and results from systems of record.&lt;/p&gt;
&lt;p&gt;The customer's existing deployment and change-management systems continue to govern production execution. Atlas exposes the supporting evidence and assumptions, keeps people and existing change controls in the decision path, and uses isolated testing and validation before anything reaches production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What does a real use case look like?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; A z/OS system programmer can use Atlas to evaluate how a middleware upgrade might affect dependent applications. They can identify which systems belong in a test plan, or coordinate validation across application, infrastructure, and operations teams.&lt;/p&gt;
&lt;p&gt;Atlas avoids the most time-consuming parts of enterprise change: manual analysis, planning, research, provisioning, testing, and documentation.&lt;/p&gt;
&lt;p&gt;For patch planning, Atlas can help assess PTF impact, plan and test changes, and capture readiness evidence. For onboarding, it can help new system programmers understand an environment when veteran knowledge isn't available.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The announcement talks about &quot;proving&quot; change. What does that proof look like, and who uses it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; Atlas analyzes potential impacts, identifies affected dependencies, generates the required validation and testing activities, and captures the results in a complete change record. That record can include impacted resources, recommended validation steps, test coverage, test results, approvals, and deployment readiness.&lt;/p&gt;
&lt;p&gt;It creates end-to-end traceability from the proposed change through validation. Change managers, approvers, compliance teams, and auditors get documented proof that the change was analyzed, the right systems were evaluated, testing was done, and the results supported deployment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where does Atlas run, and does any data leave the customer's environment?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; Atlas is on-prem. No data leaves the customer environment.&lt;/p&gt;
&lt;p&gt;The Atlas server is a containerized stack. It runs on IBM Z (s390x) or x86_64, deployed on Red Hat OpenShift through the Atlas Kubernetes operator, or on any CNCF-conformant Kubernetes cluster. The AI layer runs through IBM watsonx Assistant for Z, using Gemma 4 31B and IBM Granite 4.1 models.&lt;/p&gt;
&lt;p&gt;Discovery supports z14 and later. Provisioning is expected to support Linux on Z, Linux on x86_64, and z17 environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How does Atlas fit into IBM's broader plans for AI agents on Z?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; IBM's AI operating model rests on four systems working together: agents, data, automation, and hybrid infrastructure. Today's announcements, including Z Atlas, extend our portfolio to deliver on that. It starts with the trust and control every agent needs: a verified identity, only the access it needs, and one place to discover, secure, and evaluate agents.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What are early users saying?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; One client told us: &quot;Within minutes, Z Atlas gave me a complete map of my z/OS environment — infrastructure to application — the kind of picture that used to live only in the heads of my most experienced engineers.&quot;&lt;/p&gt;
&lt;p&gt;Another said: &quot;I don't just make changes anymore; I prove them. Z Atlas generates the plan and the tests automatically, and can spin up an isolated environment when I need to go further.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When is it available, and what do teams need?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bradfield:&lt;/strong&gt; Public preview opens October 27. General availability is targeted for Q1 2027. Atlas requires z/OS 3.1 or later. It uses configuration discovery from z/OS and supported middleware, including Db2, CICS, IMS, MQ, and z/OS Connect.&lt;/p&gt;
&lt;p&gt;Atlas is packaged as one integrated offering that brings together the AI, discovery, agent, skill, and provisioning pieces while keeping the deployment footprint small. We'll publish specific middleware levels, dependencies, and entitlements closer to GA.&lt;/p&gt;
&lt;p&gt;Developers who want to try it can &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ibm.com/forms/mkt-waitlist-4a680&quot;&gt;join the public preview waitlist&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-means-for-developers&quot;&gt;What This Means for Developers&lt;/h3&gt;
&lt;p&gt;Atlas tackles a real problem. Mainframe change management depends on tribal knowledge, and that knowledge is walking out the door. A shared, current map of the z/OS stack is useful on its own, before any AI gets involved.&lt;/p&gt;
&lt;p&gt;The most important design choice is the split between AI and facts. Atlas uses models to reason, explain, and draft plans. But the dependencies, configurations, and software levels come from discovered data and systems of record. And deployment still runs through your existing change controls. That's the right order for systems that move money and keep airlines running.&lt;/p&gt;
&lt;p&gt;A few things to keep in mind. The performance numbers come from IBM's early design program, not independent testing. GA is still months away, and pricing and entitlement details aren't public yet. The z/OS 3.1 requirement will also rule out some shops for now.&lt;/p&gt;
&lt;p&gt;If your team spends weeks proving changes, or you're about to lose the one engineer who knows how everything connects, the preview is worth a look.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32375/ibm-z-atlas-uses-ai-to-prove-mainframe-changes-are-safe-before-they-hit-production</guid>
<pubDate>Tue, 27 Oct 2026 05:00:00 +0000</pubDate>
</item>
<item>
<title>Limpando ~20 GB de caches de Terraform e Node.js com um único comando Bash</title>
<link>https://coderlegion.com/32582/limpando-20-gb-de-caches-de-terraform-e-node-js-com-um-unico-comando-bash</link>
<description>&lt;p&gt;Mantenho todos os meus repositórios em uma única pasta — cerca de 150, entre trabalho para clientes, templates e experimentos. Cada um deles é uma pequena armadilha: um &lt;code&gt;node_modules&lt;/code&gt; dentro de &lt;code&gt;app/&lt;/code&gt; e um &lt;code&gt;.terraform&lt;/code&gt; dentro de &lt;code&gt;infra/terraform/&lt;/code&gt;. Nenhum dos dois é versionado, ambos são reconstruídos em segundos (bem, minutos), e juntos tinham comido mais de 20 GB do meu disco sem que eu percebesse.&lt;/p&gt;
&lt;p&gt;O gatilho foi banal: &lt;code&gt;df -h&lt;/code&gt; mostrando 94% de uso numa partição de 233 GB, com 10 GB livres. A correção também deveria ser banal. Em vez de rodar um &lt;code&gt;find ... -exec rm -rf&lt;/code&gt; do qual eu me arrependeria, escrevi um comando pequeno em que eu pudesse confiar — e mantive.&lt;/p&gt;
&lt;p&gt;Este artigo traz o script, o raciocínio por trás de cada proteção e o que aprendi ao rodá-lo.&lt;/p&gt;
&lt;h3 id=&quot;o-objetivo&quot;&gt;O objetivo&lt;/h3&gt;
&lt;p&gt;Um comando, de qualquer lugar, que:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Varra &lt;code&gt;~/Documentos/github&lt;/code&gt; (em qualquer profundidade — incluindo monorepos e pastas agrupadas).&lt;/li&gt;
&lt;li&gt;Encontre &lt;strong&gt;apenas&lt;/strong&gt; dois tipos de diretório:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*/infra/terraform/.terraform&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*/app/node_modules&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Informe o tamanho de cada um e os totais.&lt;/li&gt;
&lt;li&gt;Delete esses diretórios — mas somente quando isso for pedido explicitamente.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;decis-es-de-projeto&quot;&gt;Decisões de projeto&lt;/h3&gt;
&lt;p&gt;1. Dry-run como padrão: apagar cache de build é reversível, mas executar a operação em 30 diretórios distribuídos por 150 repositórios exige cautela. O parâmetro &lt;code&gt;--delete&lt;/code&gt; é obrigatório para efetivar as exclusões.&lt;/p&gt;
&lt;p&gt;2. &lt;code&gt;find -prune&lt;/code&gt; em vez de &lt;code&gt;du&lt;/code&gt; recursivo: evita percorrer diretórios como &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt; e &lt;code&gt;.terraform&lt;/code&gt;, mantendo a varredura eficiente mesmo com milhares de arquivos.&lt;/p&gt;
&lt;p&gt;3. Whitelist de caminhos antes de apagar: o &lt;code&gt;rm -rf&lt;/code&gt; só é executado em caminhos que correspondem exatamente aos sufixos permitidos e estão dentro da raiz varrida. Qualquer caminho fora dessas regras é ignorado e reportado.&lt;/p&gt;
&lt;p&gt;4. Medição de tamanho em paralelo: executa o &lt;code&gt;du&lt;/code&gt; em paralelo usando &lt;code&gt;xargs -P &quot;$(nproc)&quot;&lt;/code&gt;, aproveitando os núcleos disponíveis e reduzindo o tempo de execução em árvores grandes.&lt;/p&gt;
&lt;p&gt;5. Saída legível, cálculos precisos: os tamanhos são somados em bytes e formatados com &lt;code&gt;numfmt&lt;/code&gt;, evitando divergências causadas pela soma de valores arredondados.&lt;/p&gt;
&lt;p&gt;6. Espaço livre antes e depois: compara o espaço disponível no disco antes e depois da limpeza para verificar o ganho real obtido.&lt;/p&gt;
&lt;p&gt;Um detalhe que vale destacar: &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; é um &lt;strong&gt;arquivo&lt;/strong&gt; e &lt;em&gt;não&lt;/em&gt; é tocado. Ele fixa as versões dos providers e deve ser versionado. Apenas o diretório &lt;code&gt;.terraform/&lt;/code&gt; (cache de providers e módulos) é removido.&lt;/p&gt;
&lt;h3 id=&quot;o-script&quot;&gt;O script&lt;/h3&gt;
&lt;p&gt;Salve como &lt;code&gt;limpar-cache&lt;/code&gt;, dê permissão de execução e coloque em &lt;code&gt;/usr/local/bin&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/usr/bin/env bash
#
# limpar-cache — varre um workspace e levanta/remove:
#   * &amp;lt;projeto&amp;gt;/infra/terraform/.terraform
#   * &amp;lt;projeto&amp;gt;/app/node_modules
#
# Uso:
#   limpar-cache                 # só relatório (dry-run, nada é apagado)
#   limpar-cache --delete        # apaga, pedindo confirmação
#   limpar-cache --delete --yes  # apaga sem confirmação
#   limpar-cache --only node     # apenas node_modules
#   limpar-cache --only terraform
#   limpar-cache --base ~/outro/dir
#

set -uo pipefail

BASE=&quot;${HOME}/Documentos/github&quot;
ONLY=&quot;all&quot;
DO_DELETE=0
ASSUME_YES=0
JOBS=&quot;$(nproc 2&amp;gt;/dev/null || echo 4)&quot;

usage() {
  cat &amp;lt;&amp;lt;'EOF'
limpar-cache — relatório e limpeza de caches em ~/Documentos/github

Alvos:
  */infra/terraform/.terraform
  */app/node_modules

Opções:
  -d, --delete         remove os diretórios encontrados (com confirmação)
  -y, --yes            não pede confirmação (implica --delete)
  -n, --dry-run        apenas relatório (padrão)
      --only TIPO      all (padrão) | terraform | node
      --base DIR       raiz a varrer (padrão: ~/Documentos/github)
  -h, --help           esta ajuda

Exemplos:
  limpar-cache
  limpar-cache --only terraform
  limpar-cache -d -y
EOF
}

while (($#)); do
  case &quot;$1&quot; in
    -d | --delete) DO_DELETE=1 ;;
    -y | --yes) ASSUME_YES=1 ;;
    -n | --dry-run) DO_DELETE=0 ;;
    --only)
      ONLY=&quot;${2:-}&quot;
      shift
      ;;
    --base)
      BASE=&quot;${2:-}&quot;
      shift
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    *)
      printf 'limpar-cache: opção inválida: %s\n\n' &quot;$1&quot; &amp;gt;&amp;amp;2
      usage &amp;gt;&amp;amp;2
      exit 2
      ;;
  esac
  shift
done

case &quot;$ONLY&quot; in
  all | terraform | node) ;;
  *)
    printf 'limpar-cache: --only deve ser all, terraform ou node\n' &amp;gt;&amp;amp;2
    exit 2
    ;;
esac

((ASSUME_YES)) &amp;amp;&amp;amp; DO_DELETE=1

if [[ ! -d &quot;$BASE&quot; ]]; then
  printf 'limpar-cache: diretório não encontrado: %s\n' &quot;$BASE&quot; &amp;gt;&amp;amp;2
  exit 1
fi

BASE=&quot;$(realpath -m -- &quot;$BASE&quot;)&quot;

human() { numfmt --to=iec-i --suffix=B --format='%.1f' &quot;$1&quot; 2&amp;gt;/dev/null || printf '%sB' &quot;$1&quot;; }

avail_bytes() { df -B1 --output=avail -- &quot;$BASE&quot; 2&amp;gt;/dev/null | tail -1 | tr -d ' '; }

# ---------------------------------------------------------------- coleta -----
# -prune evita descer em .git, node_modules e .terraform (rápido mesmo com 150 repos)
TF_DIRS=()
NODE_DIRS=()
while IFS= read -r -d '' d; do
  case &quot;$d&quot; in
    */infra/terraform/.terraform) TF_DIRS+=(&quot;$d&quot;) ;;
    */app/node_modules) NODE_DIRS+=(&quot;$d&quot;) ;;
  esac
done &amp;lt; &amp;lt;(find &quot;$BASE&quot; -mindepth 2 \( -name .git -o -name node_modules -o -name .terraform \) -prune -print0 2&amp;gt;/dev/null)

[[ &quot;$ONLY&quot; == &quot;node&quot; ]] &amp;amp;&amp;amp; TF_DIRS=()
[[ &quot;$ONLY&quot; == &quot;terraform&quot; ]] &amp;amp;&amp;amp; NODE_DIRS=()

if ((${#TF_DIRS[@]} == 0 &amp;amp;&amp;amp; ${#NODE_DIRS[@]} == 0)); then
  printf 'Nada encontrado em %s (filtro: %s).\n' &quot;$BASE&quot; &quot;$ONLY&quot;
  exit 0
fi

TMP=&quot;$(mktemp)&quot;
trap 'rm -f &quot;$TMP&quot;' EXIT

# tamanho aparente em paralelo: &quot;tipo&amp;lt;TAB&amp;gt;bytes&amp;lt;TAB&amp;gt;caminho&quot;
sizes_of() {
  local kind=&quot;$1&quot;
  shift
  (($#)) || return 0
  printf '%s\0' &quot;$@&quot; |
    xargs -0 -r -n1 -P &quot;$JOBS&quot; bash -c '
      p=&quot;$1&quot;
      s=&quot;$(du -sb -- &quot;$p&quot; 2&amp;gt;/dev/null | cut -f1)&quot;
      [[ -n &quot;$s&quot; ]] || s=0
      printf &quot;%s\t%s\t%s\n&quot; &quot;$0&quot; &quot;$s&quot; &quot;$p&quot;' &quot;$kind&quot;
}

{
  sizes_of terraform &quot;${TF_DIRS[@]+&quot;${TF_DIRS[@]}&quot;}&quot;
  sizes_of node &quot;${NODE_DIRS[@]+&quot;${NODE_DIRS[@]}&quot;}&quot;
} &amp;gt;&quot;$TMP&quot;

sort -t $'\t' -k2,2nr -o &quot;$TMP&quot; &quot;$TMP&quot;

total_tf=&quot;$(awk -F'\t' '$1==&quot;terraform&quot;{s+=$2} END{print s+0}' &quot;$TMP&quot;)&quot;
total_node=&quot;$(awk -F'\t' '$1==&quot;node&quot;{s+=$2} END{print s+0}' &quot;$TMP&quot;)&quot;
total_all=&quot;$((total_tf + total_node))&quot;
count_tf=&quot;${#TF_DIRS[@]}&quot;
count_node=&quot;${#NODE_DIRS[@]}&quot;

# ------------------------------------------------------------- relatório -----
printf '\nRaiz: %s\n' &quot;$BASE&quot;
printf '%-9s %12s  %s\n' &quot;TIPO&quot; &quot;TAMANHO&quot; &quot;CAMINHO&quot;
printf '%-9s %12s  %s\n' &quot;--------&quot; &quot;-----------&quot; &quot;----------------------------------------&quot;
while IFS=$'\t' read -r kind bytes path; do
  printf '%-9s %12s  %s\n' &quot;$kind&quot; &quot;$(human &quot;$bytes&quot;)&quot; &quot;${path#&quot;$BASE&quot;/}&quot;
done &amp;lt;&quot;$TMP&quot;

printf '\nResumo\n'
printf '  terraform/.terraform : %3d diretório(s)  %10s\n' &quot;$count_tf&quot; &quot;$(human &quot;$total_tf&quot;)&quot;
printf '  app/node_modules     : %3d diretório(s)  %10s\n' &quot;$count_node&quot; &quot;$(human &quot;$total_node&quot;)&quot;
printf '  TOTAL                : %3d diretório(s)  %10s\n' &quot;$((count_tf + count_node))&quot; &quot;$(human &quot;$total_all&quot;)&quot;
printf '  Livre em disco agora : %10s\n' &quot;$(human &quot;$(avail_bytes)&quot;)&quot;

if ((DO_DELETE == 0)); then
  printf '\nModo relatório (nada foi apagado). Para remover: %s --delete\n\n' &quot;$(basename &quot;$0&quot;)&quot;
  exit 0
fi

# --------------------------------------------------------------- remoção -----
if ((ASSUME_YES == 0)); then
  printf '\nRemover %d diretório(s), liberando ~%s? [s/N] ' &quot;$((count_tf + count_node))&quot; &quot;$(human &quot;$total_all&quot;)&quot;
  read -r reply
  case &quot;$reply&quot; in
    s | S | y | Y | sim | SIM) ;;
    *)
      printf 'Cancelado.\n\n'
      exit 1
      ;;
  esac
fi

is_safe() {
  local p=&quot;$1&quot; real
  [[ -n &quot;$p&quot; ]] || return 1
  [[ &quot;$p&quot; != &quot;/&quot; &amp;amp;&amp;amp; &quot;$p&quot; != &quot;$HOME&quot; ]] || return 1
  case &quot;$p&quot; in
    */infra/terraform/.terraform | */app/node_modules) ;;
    *) return 1 ;;
  esac
  real=&quot;$(realpath -m -- &quot;$p&quot; 2&amp;gt;/dev/null)&quot; || return 1
  [[ &quot;$real&quot; == &quot;$BASE&quot;/* ]] || return 1
  [[ &quot;$real&quot; != &quot;$BASE&quot; ]] || return 1
  return 0
}

avail_before=&quot;$(avail_bytes)&quot;
removed=0
failed=0

while IFS=$'\t' read -r kind bytes path; do
  if ! is_safe &quot;$path&quot;; then
    printf '  IGNORADO (fora do padrão/raiz): %s\n' &quot;$path&quot;
    ((failed++))
    continue
  fi
  if rm -rf -- &quot;$path&quot; 2&amp;gt;/dev/null &amp;amp;&amp;amp; [[ ! -e &quot;$path&quot; ]]; then
    printf '  removido  %10s  %s\n' &quot;$(human &quot;$bytes&quot;)&quot; &quot;${path#&quot;$BASE&quot;/}&quot;
    ((removed++))
  else
    printf '  FALHOU    %10s  %s\n' &quot;$(human &quot;$bytes&quot;)&quot; &quot;${path#&quot;$BASE&quot;/}&quot; &amp;gt;&amp;amp;2
    ((failed++))
  fi
done &amp;lt;&quot;$TMP&quot;

avail_after=&quot;$(avail_bytes)&quot;
freed=$((avail_after - avail_before))
((freed &amp;lt; 0)) &amp;amp;&amp;amp; freed=0

printf '\nConcluído: %d removido(s), %d falha(s).\n' &quot;$removed&quot; &quot;$failed&quot;
printf 'Espaço livre: %s -&amp;gt; %s (~%s liberados)\n\n' \
  &quot;$(human &quot;$avail_before&quot;)&quot; &quot;$(human &quot;$avail_after&quot;)&quot; &quot;$(human &quot;$freed&quot;)&quot;

((failed == 0)) || exit 1
exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instale e deixe disponível em todo shell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;install -m 755 limpar-cache /usr/local/bin/limpar-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;E no &lt;code&gt;~/.bashrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PATH=&quot;/usr/local/bin:$PATH&quot;
alias lcache='limpar-cache'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A linha explícita de &lt;code&gt;PATH&lt;/code&gt; é redundante na maioria das distribuições, mas não custa nada e deixa a configuração à prova de shells de login esquisitos. (Ela já me salvou uma vez — veja a última lição.)&lt;/p&gt;
&lt;h3 id=&quot;rodando&quot;&gt;Rodando&lt;/h3&gt;
&lt;p&gt;Relatório primeiro, sempre:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ lcache

Raiz: /home/user/Documentos/github
TIPO           TAMANHO  CAMINHO
--------   -----------  ----------------------------------------
node            1,4GiB  projeto-alfa/app/node_modules
node            1,1GiB  projeto-beta/app/node_modules
terraform     870,7MiB  projeto-alfa/infra/terraform/.terraform
terraform     865,2MiB  projeto-gama/infra/terraform/.terraform
node          771,2MiB  projeto-delta/app/node_modules
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Depois, quando a lista parecer certa:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;lcache --delete          # pergunta: Remover 30 diretório(s), liberando ~20,7GiB? [s/N]
lcache -d -y --only node # apenas node_modules, sem prompt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Uma execução na minha máquina:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Resumo
  terraform/.terraform :   9 diretório(s)      7,1GiB
  app/node_modules     :  21 diretório(s)     13,6GiB
  TOTAL                :  30 diretório(s)     20,7GiB
  Livre em disco agora :      9,9GiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;30 diretórios, ~20,7 GB. O disco saiu de 94% para cerca de 86% de uso — e cada um desses repositórios é reconstruível com &lt;code&gt;npm ci&lt;/code&gt; ou &lt;code&gt;terraform init&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;li-es-aprendidas&quot;&gt;Lições aprendidas&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;-prune&lt;/code&gt; é o truque inteiro.&lt;/strong&gt; Um &lt;code&gt;find ~/Documentos/github -name node_modules&lt;/code&gt; ingênuo desce em todo &lt;code&gt;node_modules&lt;/code&gt; procurando outros aninhados. Em 150 repositórios isso são minutos de I/O inútil. Podar no momento do match transforma tudo em uma única varredura rápida.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Tamanho aparente ≠ uso de disco.&lt;/strong&gt; &lt;code&gt;du -sb&lt;/code&gt; dá bytes aparentes (rápido, comparável), enquanto &lt;code&gt;du -s&lt;/code&gt; dá blocos alocados (o que o &lt;code&gt;df&lt;/code&gt; reporta). Para um relatório de limpeza, o tamanho aparente é o número honesto e previsível; para &quot;quanto eu vou ganhar de verdade&quot;, use &lt;code&gt;--block-size=1&lt;/code&gt; sem &lt;code&gt;-b&lt;/code&gt;. Eu mostro &lt;code&gt;df --output=avail&lt;/code&gt; antes/depois justamente porque os dois diferem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. O page cache frio domina o tempo de execução.&lt;/strong&gt; A primeira execução mediu ~2m26s; a segunda foi bem mais rápida. Nada de errado com o script — são 30 árvores de diretório sendo lidas do disco pela primeira vez. Paralelismo com &lt;code&gt;xargs -P&lt;/code&gt; ajuda, mas não vence a física.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Duas cópias de um script é um bug esperando para acontecer.&lt;/strong&gt; Eu tinha instalado em &lt;code&gt;~/.local/bin&lt;/code&gt; primeiro e depois &quot;movi&quot; para &lt;code&gt;/usr/local/bin&lt;/code&gt; — mas &lt;code&gt;~/.local/bin&lt;/code&gt; vem &lt;strong&gt;antes&lt;/strong&gt; no &lt;code&gt;PATH&lt;/code&gt;, então a cópia antiga sombreava a nova silenciosamente. Correção: &lt;em&gt;mover&lt;/em&gt; de verdade (&lt;code&gt;install&lt;/code&gt; + &lt;code&gt;rm&lt;/code&gt;), não copiar. E conferir com &lt;code&gt;command -v&lt;/code&gt;, não com &lt;code&gt;which&lt;/code&gt; (que é outro binário, com outra semântica).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Proteções são baratas; erros de &lt;code&gt;rm -rf&lt;/code&gt; não.&lt;/strong&gt; A função &lt;code&gt;is_safe()&lt;/code&gt; tem ~12 linhas e rejeita &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;$HOME&lt;/code&gt;, qualquer coisa fora da raiz varrida e qualquer caminho que não termine nos dois sufixos esperados. Ela roda em cada caminho, mesmo os vindos do meu próprio &lt;code&gt;find&lt;/code&gt;. Essa redundância é o ponto: o custo é microssegundos, o modo de falha é &quot;apaguei meu diretório home&quot;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Whitelist ganha de blacklist.&lt;/strong&gt; Eu deliberadamente &lt;em&gt;não&lt;/em&gt; apago &lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;.next/&lt;/code&gt;, &lt;code&gt;.venv/&lt;/code&gt; ou &lt;code&gt;coverage/&lt;/code&gt;. Esses têm custos de reconstrução muito diferentes e alguns são artefatos de deploy. Se eu precisar deles depois, cada um ganha seu sufixo explícito no &lt;code&gt;case&lt;/code&gt; — nunca uma regra de &quot;apague tudo que pareça cache&quot;.&lt;/p&gt;
&lt;h3 id=&quot;checklist-de-recupera-o&quot;&gt;Checklist de recuperação&lt;/h3&gt;
&lt;p&gt;Depois de uma limpeza dessas, cada repositório precisa de uma coisa:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Projetos Node
cd app &amp;amp;&amp;amp; npm ci          # ou pnpm install / yarn install

# Projetos Terraform
cd infra/terraform &amp;amp;&amp;amp; terraform init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;O &lt;code&gt;terraform init&lt;/code&gt; baixa os providers novamente; se você usa cache de plugins (&lt;code&gt;TF_PLUGIN_CACHE_DIR&lt;/code&gt;), será bem mais rápido da próxima vez — vale configurar se costuma limpar &lt;code&gt;.terraform&lt;/code&gt; com frequência.&lt;/p&gt;
&lt;h3 id=&quot;considera-es-finais&quot;&gt;Considerações finais&lt;/h3&gt;
&lt;p&gt;O script tem cerca de 200 linhas, a maioria delas &lt;code&gt;printf&lt;/code&gt;. O que o torna seguro não é sofisticação, e sim um punhado de escolhas entediantes: dry-run primeiro, podar cedo, whitelist de caminhos, confirmar antes de apagar e imprimir o que realmente aconteceu.&lt;/p&gt;
&lt;p&gt;Vinte gigabytes não é muito em termos absolutos, mas era 60% do espaço livre que eu tinha. Mais importante: o comando agora existe. Na próxima vez que o disco apertar, é uma linha em vez de uma tarde de &lt;code&gt;du&lt;/code&gt; e dúvidas.&lt;/p&gt;
&lt;p&gt;Se você mantém muitos repositórios num só lugar, o padrão se generaliza — troque os dois sufixos pelos seus próprios artefatos de build, mantenha o dry-run como padrão e nunca deixe o script adivinhar o que deve apagar.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32582/limpando-20-gb-de-caches-de-terraform-e-node-js-com-um-unico-comando-bash</guid>
<pubDate>Mon, 26 Oct 2026 18:03:24 +0000</pubDate>
</item>
<item>
<title>Drill the Safe Open: How IANA's 2020 Root Key Ceremony Ran Four Days Late</title>
<link>https://coderlegion.com/32572/drill-the-safe-open-how-ianas-2020-root-key-ceremony-ran-four-days-late</link>
<description>&lt;p&gt;&lt;em&gt;In February 2020 the safe holding the internet's root signing key would not open. IANA's published, hand-initialed ceremony scans show the custody behind the cryptography: a repair script written before anyone drilled, and a signing that took four minutes and fifteen seconds.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Page 6 of the script is a table with four columns: Step, Activity, Initials, Time. In step 5 a locksmith replaces a combination lock, and the first task on the list reads &quot;a) Drill the lock/safe open.&quot; Beside it, in blue pen, someone has initialed the box and written a time: 21:48+1.&lt;/p&gt;
&lt;p&gt;The safe is Equipment Safe #1 in the Key Management Facility that IANA runs in El Segundo, California. It holds the hardware security modules that keep the private half of the root zone's key-signing key, the trust anchor that validating resolvers chain every signed DNS answer back to. In February 2020 it would not open. IANA publishes the paperwork from its key ceremonies, scanned with the handwriting left in, so the story of how it got into its own safe can be read page by page, initials and all.&lt;/p&gt;
&lt;p&gt;Most writing about the root key ceremony is about cryptography. The paperwork is mostly about custody: who held what, who watched, and what got written down when something went wrong. That February the cryptography took four minutes and fifteen seconds. Getting into the safe took most of four days.&lt;/p&gt;
&lt;h3 id=&quot;how-a-ceremony-runs&quot;&gt;How a ceremony runs&lt;/h3&gt;
&lt;p&gt;The root zone is signed in quarters, and the key-signing key is brought out of its safe four times a year to sign the keys for the next one. The rule in force in 2020, in IANA's DNSSEC Practice Statement, was that each quarter's ceremony is held &quot;approximately 60 days but no later than 33 days, before the time cycle commences.&quot;&lt;/p&gt;
&lt;p&gt;The people are set out in roles. A Ceremony Administrator runs the script and an Internal Witness keeps the record. Safe Security Controllers hold the combinations, two people for each of the two safes: the Equipment Safe with the hardware, and the Credentials Safe with the safe deposit boxes. Crypto Officers hold the keys to those boxes. The Practice Statement describes &quot;successive barriers in three tiers, requiring at least seven trusted persons from four different roles,&quot; and activating a hardware module &quot;requires three out of seven Crypto Officers to extract activation material from above safe deposit boxes using their physical key.&quot; The Crypto Officers are Trusted Community Representatives, picked from statements of interest that IANA solicits from the internet community, and its roster page states the rule that keeps them independent: &quot;Persons considered directly affiliated with PTI, ICANN or VeriSign may not become a Trusted Community Representatives.&quot;&lt;/p&gt;
&lt;p&gt;The script's preamble says how the room behaves. &quot;CA reads each step aloud prior to its performance.&quot; &quot;Upon completion of each step, IW announces the time of completion, records the completion time, and initials their copy of the script.&quot; And then the paragraph the rest of this story runs on:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Ceremony participants who notice a problem or an error during the ceremony should interrupt the ceremony. Ceremony participants agree on a resolution before proceeding ... Unplanned events (exceptions) during the ceremony are evaluated, documented, and acted upon. It is the CA's sole responsibility to decide on proper actions after consulting with the IW. In either case, an exception is regarded as an incident, and incident handling procedures are enacted.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&quot;Documented&quot; has a form. The Root DNSSEC Script Exception is a one-page sheet. Its first row asks for the Act, the Step, the Page, and the date and time, and its second says &quot;IW describes the exception(s) and action(s) below,&quot; with a box for initials and a box for the time on each row. An exception is filed against a place in the script, the way a bug is filed against a line of code.&lt;/p&gt;
&lt;h3 id=&quot;the-day-the-dial-was-due-for-replacement&quot;&gt;The day the dial was due for replacement&lt;/h3&gt;
&lt;p&gt;The trouble surfaced during maintenance, and the maintenance was itself a ceremony with its own script. Administrative ceremonies are smaller affairs, with no signing, but the same rules apply: a Ceremony Administrator, an Internal Witness, cameras, and a participants table. In that week's tables the participants included a locksmith from an outside lock-and-security company.&lt;/p&gt;
&lt;p&gt;The work was to replace the combination dials on both safes, swapping each Kaba Mas X-09 for a new X-10. The day before, at a ceremony to program the safe deposit boxes' lock assemblies, one assembly had misbehaved. That got a sheet too:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The last KMF West Lock Assembly was malfunctioning, and we delayed the bagging of the unit to allow more time for the locksmith to troubleshoot. Once the lock assembly was functioning, we placed it in TEB BB91951419 and continued where we left off in the script.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TEB is a tamper-evident bag, and the bag's serial number goes in the record.&lt;/p&gt;
&lt;p&gt;The script for the Equipment Safe, dated Tuesday 11 February, has an Act 2 called &quot;Safe #1 Equipment Maintenance.&quot; Step 1, initialed at 21:34, has the administrator and the witness carry &quot;a flashlight, phillips screwdriver, change key tool&quot; into the safe room. Step 2 reads &quot;SSC1_A opens Safe #1 while shielding the combination from the camera,&quot; with a note that the controller &quot;will begin by rapidly spinning the dial counter-clockwise in order to charge it.&quot; The lock powers itself from the dial. Step 4 is the job they had come to do: &quot;a) Remove the current Kaba Mas X-09. b) Install a new Kaba Mas X-10.&quot;&lt;/p&gt;
&lt;p&gt;Step 2 has no initials. The exception sheet behind it is filed against Act 2, Step 2, Page 6, dated 2020/02/11 and initialed at 23:14:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;SSC1_A was unable to open the safe due to malfunctioning hardware. Escorted out by IW2 to verify combination with other SSC. Locksmith contacted manufacturer and they suggested the issue was a faulty logic board and the dial will require drilling. Ended ceremony to discuss steps going forward.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the dial that failed was the one they had come to replace, and it failed at the one step that needed it to work. A second ceremony a couple of hours later opened the Credentials Safe without trouble and swapped its X-09 for an X-10.&lt;/p&gt;
&lt;p&gt;The next day ICANN's blog announced that the 40th ceremony, &quot;originally scheduled for 12 February 2020 in El Segundo, California, has been postponed.&quot; &quot;One of the security mechanisms that protects the contents of the secure safes was found to be malfunctioning.&quot; &quot;The issue was identified yesterday, 11 February 2020, during routine maintenance of the Key Management Facility.&quot; The post said there would be &quot;no interruption to Domain Name System Security Extensions (DNSSEC) service,&quot; that &quot;We maintain a complete replica facility in Culpeper, Virginia,&quot; and that the repair was scheduled for Friday the 14th.&lt;/p&gt;
&lt;h3 id=&quot;the-branch-was-written-before-anyone-drilled&quot;&gt;The branch was written before anyone drilled&lt;/h3&gt;
&lt;p&gt;The 11 February script had no step for a safe that would not open. Step 2 simply assumed it would. When it failed, nobody improvised the repair in the room. They filed the sheet, ended the ceremony, and wrote a new script.&lt;/p&gt;
&lt;p&gt;The repair ceremony began on the 14th, and its Act 2 opens with a section titled &quot;Confirmation of Equipment Safe #1 Dial Malfunction.&quot; Before anyone was allowed to drill, the controller tried the safe again, under the cameras, at 17:26. The step carries a note that &quot;Locksmith may provide additional advice/techniques to attempt to operate the safe dial.&quot; Then the script splits:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If the safe opens: Complete the safe log by following the steps below ...&lt;/p&gt;
&lt;p&gt;If the lock malfunctions: Continue with the steps below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the safe had opened on that attempt, the script would have sent everyone to the safe log and the dial swap, and there would have been no drilling. It didn't open. Step 5 begins &quot;The locksmith performs the following tasks to replace the combination lock,&quot; and lists them: &quot;a) Drill the lock/safe open,&quot; &quot;b) Remove the current Kaba Mas X-09 Safe Dial,&quot; &quot;c) Return the safe door to a secure state by performing necessary repairs,&quot; &quot;d) Install a new Kaba Mas X-10 Safe Dial.&quot; Under the first task sits a note about breaks: &quot;A ceremony break will be performed as required. All personnel will be escorted out of the Tier 5 (Safe Room) and Tier 4 (Ceremony Room.)&quot; Each lettered task has its own box for initials and time.&lt;/p&gt;
&lt;p&gt;Drilling into the safe that guards the root key was written down in advance as a numbered, witnessed, timed step, with a branch for the possibility that it wasn't needed.&lt;/p&gt;
&lt;h3 id=&quot;what-the-sheets-caught&quot;&gt;What the sheets caught&lt;/h3&gt;
&lt;p&gt;The repair took two days, and the packet of exception sheets behind its script reads like a log of everything the plan didn't specify. I read four of them, all filed against Act 2, Step 5, Page 6. I didn't read every page of the packet, so four is a floor.&lt;/p&gt;
&lt;p&gt;The first, dated the 14th at 21:00, records that an external witness &quot;left the room on a break and stayed outside until the next break,&quot; returning at 22:19, and adds: &quot;Due to the length of the ceremony carrying into the next day, other non essential participants came and went as well.&quot; The second is one sentence: &quot;Ceremony was paused overnight and resumed in the morning.&quot;&lt;/p&gt;
&lt;p&gt;The third, at 19:26 on the 15th, is about the building. A security guard used the hand scanner outside Tier 3 to escort an external witness in, and that triggered the alarm. The witness was asked to step out and wait in the conference room, &quot;and the alarm was triggered again when he manually opened the door.&quot;&lt;/p&gt;
&lt;p&gt;The fourth is about the order of steps:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Steps e, f, g, and h were performed immediately after opening the safe for the first time which occurred between steps a and b.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Steps e to h are the safe log: the controller shows the old log to the camera, takes the pre-printed one from the witness, writes the time and signs where it says &quot;Open Safe,&quot; and the witness checks and initials the entry. The script had them after the new dial went on. In the room, they did them the moment the drilled door first opened, and the times in the script's own columns agree: a at 21:48, e to h at 21:54, the new dial at 23:56. Doing the log first was arguably the better order, since it put the opening on paper at once. Nobody rewrote the script to match. The deviation went on its own sheet, and the record shows the steps in the order they happened.&lt;/p&gt;
&lt;p&gt;The times in that column all carry a &quot;+1,&quot; which the script never defines. I read it as the day after the ceremony began, which puts the drilling at 21:48 UTC on the 15th. Three other records agree with that reading: the overnight sheet is dated the 15th at 00:38, IANA's announcement that the safe was open went out at 22:10 UTC on the 15th, and the participants signed the ceremony off at 00:44 on the 16th. The discovery sheet was initialed at 23:14 on the 11th, so the drilling step was initialed 3 days, 22 hours and 34 minutes after it.&lt;/p&gt;
&lt;p&gt;IANA's message to the root-dnssec-announce list read: &quot;We have successfully opened the safe with the failed mechanism, and are in the final stages of remediation, with the repair work and installation of a new lock underway. We are therefore arranging resources to start the KSK Ceremony 40 at 0000 UTC, or shortly thereafter.&quot;&lt;/p&gt;
&lt;h3 id=&quot;four-minutes-and-fifteen-seconds&quot;&gt;Four minutes and fifteen seconds&lt;/h3&gt;
&lt;p&gt;The signing went the way the script said it would. The signer's log for Ceremony 40 opens with &quot;Starting: ksrsigner /media/KSR/KSK40/ksr-root-2020-q2-0.xml (at Sun Feb 16 02:21:33 2020 UTC).&quot; Twenty-two seconds later the hardware module is activated. At 02:25:48 the log reads &quot;Generated new SKR,&quot; a signed key response whose first signature period begins on 1 April 2020, made with the key whose tag is 20326. The log prints the response's SHA-256 hash in hex, and then again as a run of English words: &quot;unwind leprosy fracture savagery tactics speculate Neptune miracle slingshot aftermath …&quot;&lt;/p&gt;
&lt;p&gt;From start to signed response is four minutes and fifteen seconds. Against the Practice Statement's rule, the latest date for the second-quarter ceremony was 28 February, so the signing finished with nearly twelve days to spare.&lt;/p&gt;
&lt;p&gt;The ceremony's closing act has the administrator read every exception out before anyone signs. Step 1 of the participants' signing reads &quot;CA reads all exceptions that occurred during the ceremony.&quot; In step 2 each attendee comes to the table and signs the witness's list, and &quot;All signatures declare that this script is a true and accurate record of the ceremony.&quot; The read-back comes first. You hear what went wrong before you put your name to the record.&lt;/p&gt;
&lt;h3 id=&quot;april-nobody-could-come&quot;&gt;April: nobody could come&lt;/h3&gt;
&lt;p&gt;Two months later the failure was the people. Ceremony 41 was set for 23 April 2020, in the first weeks of the pandemic. IANA's published questions-and-answers document says there were no representatives for the El Segundo facility &quot;located close enough to allow unrestricted and safe travel to the facility,&quot; and says where they were: &quot;Mauritius, Spain, Russia, Tanzania, Uruguay and east-coast United States.&quot;&lt;/p&gt;
&lt;p&gt;The plan IANA used kept the custody chain and moved the people. &quot;Four trusted community representatives (TCRs) who fulfil Cryptographic Officer roles in the ceremony have transmitted their safe deposit keys to separate ICANN and PTI staff. These staff will act as surrogates, but the TCRs will participate in the ceremony remotely and monitor their credential's use.&quot; The keys travelled sealed: &quot;The TCRs have wrapped their deposit keys with opaque material, and then transmitted them in tamper-evident bags. This bag will not be opened until within the ceremony so that each TCR can witness their key is in the same condition as when they released it.&quot; The ceremony would sign three quarters at once instead of one, and &quot;The additional generated signatures will be initially withheld.&quot;&lt;/p&gt;
&lt;p&gt;The plan they didn't use is in the same document. &quot;The first version of our contingency plan assumed the trusted community representatives would not transmit their safe deposit box keys for performance of the ceremony. In such a scenario, a trained locksmith would drill and replace the locks to three safe deposit boxes during the ceremony in order to retrieve the smart cards needed to activate the hardware security module.&quot; Two months after the safe, the first version of the fallback was to drill again.&lt;/p&gt;
&lt;p&gt;One part of the April plan broke the rules as they stood, and the document says which: &quot;The only element of our revised approach that was inconsistent with the DPS was signing additional quarters of KSRs during the ceremony. The Policy Management Authority, an internal committee that reviews changes to the DPS, convened on 6 April and approved revisions that allow for signing additional quarters of KSRs as a disaster recovery response.&quot; The rule was changed by its owner seventeen days before it was needed, and not by the people in the room on the day.&lt;/p&gt;
&lt;p&gt;The Ceremony 41 script also adds a step to its closing act. After reading the exceptions, the administrator &quot;asks any COs who are participating remotely if they have any concerns pertaining to the ceremony or exceptions which may have occurred.&quot;&lt;/p&gt;
&lt;p&gt;By November the bend was standing policy. The Practice Statement in force in February 2020, the fourth edition, has no variance clause. The sixth edition, dated 4 November 2020, carries section 4.5.4.1, and the eighth edition of 2025 still carries it word for word:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In the event of a declaration of a disaster recovery or business continuity response, the adverse event may necessitate variances to the procedural controls and technical security controls. These variances shall be limited in scope to those necessary for the successful conduct of key signing operations, and according to protocols that best ensures a proper record of the chain-of-custody and limiting sensitive operations to trusted persons.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The clause ends by requiring that variances are &quot;consented by the RZ KSK Operator's management, are documented and communicated.&quot; I didn't read the fifth edition, so I can't say which edition first carried it, only that it arrived between February and November 2020.&lt;/p&gt;
&lt;h3 id=&quot;the-record-since&quot;&gt;The record since&lt;/h3&gt;
&lt;p&gt;IANA's schedule lists 63 held ceremonies from June 2010 to August 2026, counting Ceremony 53's two parts separately. In 2020 and 2021 there were two each, the only full years with fewer than four. Ceremonies 41 and 42 are the only ones that signed more than one quarter, and the 294 days between them is the longest gap in the schedule.&lt;/p&gt;
&lt;p&gt;The public archive runs to 65 directories and 940 files, among them 315 video files from the ceremony cameras. Files from administrative ceremonies first appear in the directory for Ceremony 40, the safe repair, and turn up again in 15 later directories. I didn't read what those later administrative ceremonies were for. Everything here comes from IANA's and ICANN's own record. That is the point of reading it, and also its limit, because an outside auditor's view isn't quoted here.&lt;/p&gt;
&lt;h3 id=&quot;what-carries-over&quot;&gt;What carries over&lt;/h3&gt;
&lt;p&gt;None of this is exotic. It is a procedure that expects to be interrupted, and a record that treats the interruption as a first-class entry. Anyone running software that acts where actions can't be taken back, a deployment pipeline or an agent holding production credentials, can copy four habits from it.&lt;/p&gt;
&lt;p&gt;An exception there is a typed record: act, step, page, time, initials, and a description written by the witness rather than the actor. When the script doesn't cover a failure, the response in that room was to stop and write a new script, with a branch for each outcome, before taking the irreversible step. When steps happen out of order, the record says so instead of being tidied. And when a rule has to bend, its owner bends it in advance, in writing, and later turns the bend into a clause with limits.&lt;/p&gt;
&lt;p&gt;At the back of the Ceremony 40 script is a page for the Internal Witness alone. It is two sentences long: &quot;I hereby attest that the Key Ceremony was conducted in accordance to this script. Any exceptions that occurred were accurately and properly documented.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IANA, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.iana.org/dnssec/ceremonies&quot;&gt;Key Signing Ceremonies&lt;/a&gt;, the schedule of held and planned ceremonies (as fetched 7 October 2026).&lt;/li&gt;
&lt;li&gt;IANA, &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/&quot;&gt;ceremony archive&lt;/a&gt;, 65 directories of scripts, logs and video.&lt;/li&gt;
&lt;li&gt;IANA, Ceremony 40 administrative scripts, annotated scans: &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/40/20200210-AC40_1-Ceremony_Script_Annotated.pdf&quot;&gt;AC40_1, 10 February 2020&lt;/a&gt; (lock assembly exception, page 7); &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/40/20200211_1-AC40_2-Ceremony_Script_Annotated.pdf&quot;&gt;AC40_2, 11 February 2020&lt;/a&gt; (Act 2 and the discovery exception); &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/40/20200211_2-AC40_3-Ceremony_Script_Annotated.pdf&quot;&gt;AC40_3, 11 February 2020&lt;/a&gt; (the Credentials Safe); &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/40/20200214-AC40_4-Ceremony_Script_Annotated.pdf&quot;&gt;AC40_4, 14 to 16 February 2020&lt;/a&gt; (the drilling, script page 6, and the exception sheets).&lt;/li&gt;
&lt;li&gt;IANA, &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/40/20200212-KC40_Ceremony_Script.pdf&quot;&gt;Ceremony 40 script&lt;/a&gt; (preamble, closing act, Appendix C) and &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/40/ksrsigner-20200216-022133.log&quot;&gt;signer log&lt;/a&gt;, 16 February 2020.&lt;/li&gt;
&lt;li&gt;Kim Davies, &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.icann.org/en/blogs/details/root-key-signing-key-ceremony-postponed-12-2-2020-en&quot;&gt;Root Key Signing Key Ceremony Postponed&lt;/a&gt;,&quot; ICANN blog, 12 February 2020.&lt;/li&gt;
&lt;li&gt;Kim Davies, &quot;[Root KSK Ceremony 40 scheduled for 0000 UTC](&lt;a rel=&quot;nofollow&quot; href=&quot;https://lists.icann.org/hyperkitty/list/*Emails&quot;&gt;https://lists.icann.org/hyperkitty/list/*Emails&lt;/a&gt; are not allowed*/message/Z7KNDGDKD5PNZJ52XHP5Y4W3GIYM23H3/),&quot; root-dnssec-announce, 15 February 2020.&lt;/li&gt;
&lt;li&gt;IANA, &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/41/KC41_qa.pdf&quot;&gt;Questions and Answers on COVID-19 mitigations for KSK Ceremony 41&lt;/a&gt;, and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://data.iana.org/ksk-ceremony/41/KC41_Script.pdf&quot;&gt;Ceremony 41 script&lt;/a&gt;, April 2020.&lt;/li&gt;
&lt;li&gt;DNSSEC Practice Statement for the Root Zone KSK Operator: 4th edition, 1 October 2016, as published at iana.org/dnssec/icann-dps.txt in February 2020 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://web.archive.org/web/20200216072548/https://www.iana.org/dnssec/icann-dps.txt&quot;&gt;Wayback Machine capture, 16 February 2020&lt;/a&gt;), sections 4.2.2 and 6.6; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.iana.org/dnssec/procedures/ksk-operator/ksk-dps-20201104.html&quot;&gt;6th edition, 4 November 2020&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.iana.org/dnssec/procedures/ksk-operator/ksk-dps-20250414.html&quot;&gt;8th edition, 14 April 2025&lt;/a&gt;, sections 4.2.2 and 4.5.4.1.&lt;/li&gt;
&lt;li&gt;IANA, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.iana.org/dnssec/tcrs&quot;&gt;Trusted Community Representatives&lt;/a&gt; (as fetched 7 October 2026).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;How this was checked. Typed quotations were checked word for word against the saved text of the source named beside them. The handwritten ones were transcribed from the scanned pages of the annotated scripts and checked against the images; the scans have no text layer. Times are UTC, as the scripts record them, and the &quot;+1&quot; day is read as explained in the text. The intervals (3 days, 22 hours and 34 minutes; 4 minutes and 15 seconds; the margin before 28 February) and the counts from the schedule and the archive listings were computed by script from those sources. Four exception sheets is the number read in the repair ceremony's packet, not a total for the month.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Related on our blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/replit-ai-agent-deleted-production-database-during-code-freeze/&quot;&gt;Replit's AI Agent Deleted a Production Database During a Code Freeze&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/aviation-confessed-121128-times-last-year/&quot;&gt;Aviation Confessed 121,128 Times Last Year&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/iana-root-key-ceremony-2020-drill-the-safe-open/&quot;&gt;vibeagentmaking.com&lt;/a&gt;.&lt;/em&gt; If you want a tamper-evident, dated record of what an automated system did and what went off-script: &lt;code&gt;pip install chain-of-consciousness&lt;/code&gt; or &lt;code&gt;npm install chain-of-consciousness&lt;/code&gt;, or try &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/hosted-coc/&quot;&gt;Hosted Chain of Consciousness&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Security</category>
<guid isPermaLink="true">https://coderlegion.com/32572/drill-the-safe-open-how-ianas-2020-root-key-ceremony-ran-four-days-late</guid>
<pubDate>Mon, 26 Oct 2026 15:36:01 +0000</pubDate>
</item>
<item>
<title>You patched PHP. The old image is still in your cluster’s future.</title>
<link>https://coderlegion.com/32217/you-patched-php-the-old-image-is-still-in-your-clusters-future</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;You patched PHP. The old image is still in your cluster's future.&quot;&lt;br&gt;
published: false&lt;br&gt;
description: &quot;PHP2Shell (CVE-2026-109618, CVE-2026-109619) as seen from a Kubernetes cluster: how fast the scanners caught up, where they stay blind, and why rebuilding the image is only half of remediation.&quot;&lt;/p&gt;
&lt;h3 id=&quot;tags-kubernetes-security-php-devops&quot;&gt;tags: kubernetes, security, php, devops&lt;/h3&gt;
&lt;p&gt;On 21 October PHP shipped fixes for two critical bugs in the engine itself, CVE-2026-109618 and CVE-2026-109619, both CVSS 9.2, both reachable without authentication, both already being exploited by the time most people read the advisory. The community name is PHP2Shell. Fixed versions are 8.5.12, 8.4.27, 8.3.36 and 8.2.35. CISA and the UK NCSC both published &quot;patch now&quot; notices the same day.&lt;/p&gt;
&lt;p&gt;I am not going to explain the bugs; Wiz, watchTowr and Qualys have done that well. I want to write about what happened in Kubernetes teams in the forty-eight hours after, because I watched it happen and because the second half of it is the part nobody schedules.&lt;/p&gt;
&lt;h3 id=&quot;how-most-teams-found-out&quot;&gt;How most teams found out&lt;/h3&gt;
&lt;p&gt;From the news. Then someone opened a spreadsheet and typed &quot;PHP&quot; into the first column.&lt;/p&gt;
&lt;p&gt;This is not a criticism. It is what a platform team does when the only inventory of what runs in production is the memory of the people who deployed it. The questions that followed were always the same three:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which clusters run PHP at all?&lt;/li&gt;
&lt;li&gt;Which images, which versions?&lt;/li&gt;
&lt;li&gt;Who gets to say &quot;we are done&quot;?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The third question is the one that comes back in March, when an auditor asks what you knew on 22 October and what you did about it.&lt;/p&gt;
&lt;h3 id=&quot;how-fast-the-scanners-caught-up&quot;&gt;How fast the scanners caught up&lt;/h3&gt;
&lt;p&gt;I measured this on 23 October, two days after the advisory, with Trivy 0.72.0 and whatever vulnerability database it pulled that afternoon. You can repeat every step.&lt;/p&gt;
&lt;p&gt;The database build was stamped 13:13 UTC on 23 October. Both CVEs were in it, with the Debian and Alpine advisories attached:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ grep -a -o CVE-2026-109618 ~/.cache/trivy/db/trivy.db | wc -l
1247
$ grep -a -o CVE-2026-109619 ~/.cache/trivy/db/trivy.db | wc -l
1253
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Debian's security tracker already listed &lt;code&gt;php8.2&lt;/code&gt; as resolved in &lt;code&gt;8.2.35-1~deb12u1&lt;/code&gt; for bookworm and &lt;code&gt;php8.4&lt;/code&gt; in &lt;code&gt;8.4.27-1~deb13u1&lt;/code&gt; for trixie. Alpine's secdb for 3.22 carried both CVEs. A &lt;code&gt;debian:bookworm-slim&lt;/code&gt; image with &lt;code&gt;php8.2-cli&lt;/code&gt; installed from the archive that day came out as 8.2.35, already fixed; a &lt;code&gt;alpine:3.22&lt;/code&gt; image with &lt;code&gt;php84&lt;/code&gt; came out as 8.4.27, also fixed.&lt;/p&gt;
&lt;p&gt;So the honest answer to &quot;did the open-source scanners keep up&quot; is yes, within two days, for PHP installed as a distribution package. That is good news and it is the ordinary case for a lot of internal images.&lt;/p&gt;
&lt;h3 id=&quot;where-the-scanners-stay-blind&quot;&gt;Where the scanners stay blind&lt;/h3&gt;
&lt;p&gt;Now the uncomfortable part.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ trivy image --scanners vuln --severity CRITICAL,HIGH php:8.3-fpm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;126 CRITICAL and HIGH findings. Not one of them about PHP.&lt;/p&gt;
&lt;p&gt;The official &lt;code&gt;php:*&lt;/code&gt; images build PHP from source. There is no &lt;code&gt;php8.3&lt;/code&gt; package in the image for a package scanner to look at, so Trivy, and every scanner that works from the package database, has nothing to match the advisory against. The 126 findings are the Debian base. The thing you actually care about is invisible.&lt;/p&gt;
&lt;p&gt;This is not a Trivy bug. It is what package-based scanning is. But it means that if your PHP workloads run on the official image, or on any image that compiles PHP, then a quiet scanner is not a clean bill. It is silence.&lt;/p&gt;
&lt;p&gt;What you can do about it this week, in order of effort:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ask the binary. &lt;code&gt;docker run --rm &amp;lt;your-image&amp;gt; php -v&lt;/code&gt; tells you the truth in one line. Do it for every PHP image tag you run, not the ones you think you run.&lt;/li&gt;
&lt;li&gt;Rebuild on a base that was pushed after 21 October, pin the digest, and write the digest down somewhere an auditor can read.&lt;/li&gt;
&lt;li&gt;If you can, move to distribution packages for the runtime. It costs you a newer PHP, it buys you a scanner that can see it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;patching-is-half-the-job&quot;&gt;Patching is half the job&lt;/h3&gt;
&lt;p&gt;Here is the part that gets skipped. You rebuild the image, you roll the Deployment, the pods come up on 8.3.36, everyone exhales.&lt;/p&gt;
&lt;p&gt;The old image did not go anywhere. It is still in the registry. It is still in &lt;code&gt;helm history&lt;/code&gt;. It is the image someone will roll back to at 03:00 when the new build has a problem that has nothing to do with PHP. It is in the CI cache, in the staging cluster nobody looks at, in the Job that runs once a month.&lt;/p&gt;
&lt;p&gt;Remediation has two halves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run the fixed image.&lt;/li&gt;
&lt;li&gt;Make it impossible for the vulnerable image to run again, and keep a record that proves both.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most teams do the first half and call it done. The second half is what an admission controller is for.&lt;/p&gt;
&lt;h3 id=&quot;what-this-looks-like-with-an-admission-controller-in-the-path&quot;&gt;What this looks like with an admission controller in the path&lt;/h3&gt;
&lt;p&gt;This is where I talk about what we built, so take the next paragraphs for what they are. Attestkeep is an admission controller for Kubernetes that scans, verifies signatures, decides at admission, and keeps the record. Here is exactly what it did, and did not do, for PHP2Shell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The database refreshes itself.&lt;/strong&gt; The scanner pulls a fresh vulnerability database every six hours. Trivy publishes one every six hours too, so the lag between an advisory landing upstream and the cluster knowing about it is measured in hours, not in someone's calendar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every admitted image is rescanned.&lt;/strong&gt; Once a day by default (&lt;code&gt;scan.rescanInterval&lt;/code&gt; in the chart, if you want it shorter), every image the cluster has ever admitted is scanned again against the current database. An image that was clean on Monday and carries a CVE on Thursday shows up as not compliant on Thursday, with the CVE named. You do not open a spreadsheet; the inventory is the admission ledger.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It tells you.&lt;/strong&gt; A scan that finds critical vulnerabilities raises a &lt;code&gt;critical_finding&lt;/code&gt; event, which goes to whichever channel you configured: email, Slack, Teams, a webhook, Jira, GitLab. When a running image flips from &quot;would be admitted&quot; to &quot;would be denied&quot;, that is a second, separate message: &quot;Running image now fails policy&quot;. That is the message an operator actually needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New pods with the image are refused.&lt;/strong&gt; If your policy blocks critical findings, the vulnerable image cannot be admitted again. The rollback at 03:00 fails with a reason, the Job that runs monthly fails with a reason, the staging cluster with the same policy refuses it too. The old image is still in the registry. It is no longer in your cluster's future.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Running pods are not killed.&lt;/strong&gt; This is deliberate. Attestkeep does not evict a workload because a database update changed its verdict; that is an operator's decision, with the blast radius in front of them. What it does is make sure the next decision, any next decision, goes the right way, and that the running population is reconciled against the ledger on a schedule so drift is visible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If the exploit lands first, there is a second line.&lt;/strong&gt; PHP2Shell ends in a shell. If Falco is connected, a shell spawned inside a container is a runtime event, and a runtime policy can turn that event into a notification, an issue, or stopping the pod. That is not prevention, it is containment, and it is worth having when the window between advisory and rebuild is days.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every decision is signed.&lt;/strong&gt; The admission ledger records the image digest, the policy revision that ran, the verdict and the reason. When the auditor asks in March what you knew on 22 October, the answer is a record with a signature, not a recollection.&lt;/p&gt;
&lt;p&gt;And the blind spot above applies to us exactly as it applies to Trivy, because Trivy is the scanner inside. If you run the official &lt;code&gt;php:*&lt;/code&gt; image, Attestkeep's rescan will not name CVE-2026-109618 either. It will show you the digest you admitted, when, and under which policy. The &lt;code&gt;php -v&lt;/code&gt; step is still yours. I would rather tell you that here than have you find it out from a pentest report.&lt;/p&gt;
&lt;h3 id=&quot;the-checklist-tool-agnostic&quot;&gt;The checklist, tool-agnostic&lt;/h3&gt;
&lt;p&gt;For this week, whatever you run:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;List every image whose name or Dockerfile mentions PHP. Not from memory; from the cluster.&lt;/li&gt;
&lt;li&gt;For each, run &lt;code&gt;php -v&lt;/code&gt; inside the image. Trust that over the scanner.&lt;/li&gt;
&lt;li&gt;Rebuild on a post-21-October base, pin digests.&lt;/li&gt;
&lt;li&gt;Add a rule that refuses the old digests at admission, so a rollback cannot resurrect them.&lt;/li&gt;
&lt;li&gt;Write down what you found, what you changed, and when, in a form that survives until the audit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If step 4 is a policy in an admission controller and step 5 is produced automatically, this is a Tuesday. If step 4 is a Slack message and step 5 is a Confluence page, it is a project, and it will be a project again at the next CVE.&lt;/p&gt;
&lt;h3 id=&quot;closing&quot;&gt;Closing&lt;/h3&gt;
&lt;p&gt;The lesson of PHP2Shell for platform teams is not &quot;patch faster&quot;. Everyone patched. The lesson is that the news is a bad way to learn what is in your cluster, and that a patched Deployment with the old image still admissible is not a remediated cluster.&lt;/p&gt;
&lt;p&gt;Let the cluster do the checking. Keep the record. Make the old image a thing that cannot come back.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Sources: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching&quot;&gt;CISA alert, 21 October 2026&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ncsc.gov.uk/news/critical-security-issue-affecting-php&quot;&gt;NCSC advisory&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://watchtowr.com/intelligence/php-cve-2026-109618-faq/&quot;&gt;watchTowr FAQ&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.qualys.com/vulnerabilities-threat-research/2026/10/20/php-announces-critical-security-release-for-two-vulnerabilities&quot;&gt;Qualys&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://cloud.google.com/blog/products/identity-security/responding-to-core-php-vulnerabilities-cve-2026-109618-cve-2026-109619&quot;&gt;Google Cloud response&lt;/a&gt;. Scanner measurements are mine, 23 October 2026, Trivy 0.72.0; the commands are in the text.&lt;/p&gt;
&lt;p&gt;Attestkeep runs inside the cluster it protects; every security feature is in the free edition. &lt;a rel=&quot;nofollow&quot; href=&quot;https://attestkeep.com&quot;&gt;attestkeep.com&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.attestkeep.com&quot;&gt;docs.attestkeep.com&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32217/you-patched-php-the-old-image-is-still-in-your-clusters-future</guid>
<pubDate>Fri, 23 Oct 2026 17:56:15 +0000</pubDate>
</item>
<item>
<title>SOC 2, CRA, NIS2: they all ask your cluster the same five questions</title>
<link>https://coderlegion.com/32216/soc-2-cra-nis2-they-all-ask-your-cluster-the-same-five-questions</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;SOC 2, CRA, NIS2: they all ask your cluster the same five questions&quot;&lt;br&gt;
published: true&lt;br&gt;
description: &quot;A working engineer's read of SOC 2 and the EU instruments that landed alongside it, and why the hard part is not the controls but the evidence that they ran.&quot;&lt;/p&gt;
&lt;h3 id=&quot;tags-security-kubernetes-devops-compliance&quot;&gt;tags: security, kubernetes, devops, compliance&lt;/h3&gt;
&lt;p&gt;In eleven days, on 11 September 2026, the reporting obligations of the EU Cyber Resilience Act start applying to anyone who puts a product with digital elements on the European market. Not the full regulation. Just the part where, if you find out an actively exploited vulnerability is in your product, you have 24 hours to tell ENISA about it.&lt;/p&gt;
&lt;p&gt;I have watched a lot of engineering teams meet this class of deadline for the first time. It usually goes the same way. Somebody in sales gets a security questionnaire. Somebody in engineering gets forwarded the questionnaire. Three weeks later there is a shared folder called &lt;code&gt;evidence-final-v3&lt;/code&gt; with 200 screenshots in it, and nobody can tell you which screenshot answers which question.&lt;/p&gt;
&lt;p&gt;I have spent the last several months building a tool whose entire job is that folder, so I read the instruments properly. This is what I found out. It is written for engineers, not for a compliance team, and I try to be specific about what the text says rather than what a vendor blog says it says.&lt;/p&gt;
&lt;h3 id=&quot;where-soc-2-came-from-and-why-that-still-shapes-it&quot;&gt;Where SOC 2 came from, and why that still shapes it&lt;/h3&gt;
&lt;p&gt;SOC 2 exists because of a misuse.&lt;/p&gt;
&lt;p&gt;In 1992 the AICPA published SAS 70, an auditing standard for service organisations. Its purpose was narrow: if you outsourced your payroll, your auditor needed some assurance that your payroll provider's internal controls did not corrupt your financial statements. It was an accounting instrument, for accountants, about financial reporting.&lt;/p&gt;
&lt;p&gt;Then the industry outsourced everything else. By the mid-2000s companies were sending their customer data to service providers, and they wanted assurance about &lt;em&gt;that&lt;/em&gt;, not about financial reporting. There was nothing designed for it, so they asked for the thing that existed. Vendors started waving SAS 70 reports around as proof they were secure. They were not proof of that. SAS 70 had no defined control set at all: the service organisation wrote its own control objectives, and the auditor tested against whatever had been written. Two SAS 70 reports could have nothing in common.&lt;/p&gt;
&lt;p&gt;In 2010 the AICPA split the thing in half. Financial reporting controls stayed, renamed SOC 1 under SSAE 16 (SSAE 18 since 2017). The security question got its own report: SOC 2, with an actual defined set of criteria behind it.&lt;/p&gt;
&lt;p&gt;That history is worth carrying, because SOC 2 still has SAS 70's shape in one important respect. It defines &lt;em&gt;criteria&lt;/em&gt;, not controls. It tells you what has to be true. It does not tell you how. More on that in a moment, because it is the single most common misunderstanding.&lt;/p&gt;
&lt;h3 id=&quot;type-i-and-type-ii&quot;&gt;Type I and Type II&lt;/h3&gt;
&lt;p&gt;Two report types, and the difference is most of the value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Type I&lt;/strong&gt; is a point in time. An auditor looks at your controls on one date and gives an opinion on whether they are suitably designed. It answers: do you have the right things in place?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Type II&lt;/strong&gt; covers a period, typically 3 to 12 months, with 6 being the usual choice for a first report. The auditor gives an opinion on whether the controls were suitably designed &lt;em&gt;and operated effectively throughout that period&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;A Type I says you built the fence. A Type II says the fence was standing every day for six months.&lt;/p&gt;
&lt;p&gt;Almost every buyer who asks for SOC 2 means Type II, or will mean it at renewal. And it matters enormously to how you should think about evidence, because &quot;operated effectively throughout the period&quot; is not a thing you can produce at the end. The auditor samples across the whole window. If your quarterly access review was supposed to run four times and it ran once, in the week before fieldwork, that is an exception in your report and everybody reading it can see it.&lt;/p&gt;
&lt;p&gt;That property, that the evidence must have existed continuously rather than being assembled afterwards, is the one I would burn into a wall. It is what makes screenshot folders a bad answer.&lt;/p&gt;
&lt;h3 id=&quot;the-five-criteria-and-the-four-you-probably-do-not-need&quot;&gt;The five criteria, and the four you probably do not need&lt;/h3&gt;
&lt;p&gt;SOC 2 is built on the Trust Services Criteria (AICPA TSP section 100, 2017, revised 2022). Five categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt; — the Common Criteria, CC1 to CC9. Mandatory. Every SOC 2 includes it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Availability&lt;/strong&gt; — is the system up when you said it would be&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidentiality&lt;/strong&gt; — is information designated confidential protected&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Processing Integrity&lt;/strong&gt; — is processing complete, valid, accurate, timely, authorised&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt; — personal information handled per your notice&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only Security is required. You choose the rest, and you should choose as few as you can defend, because each one is more scope, more testing, more cost. Most infrastructure and SaaS companies do Security, or Security plus Availability. Add Confidentiality if your contracts talk about it. Processing Integrity is genuinely rare and mostly belongs to payments and payroll.&lt;/p&gt;
&lt;p&gt;Inside the Common Criteria, the ones an engineer actually touches:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Criterion &lt;/th&gt;&lt;th&gt;Roughly&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;CC1–CC3 &lt;/td&gt;&lt;td&gt;Governance, communication, risk assessment. Mostly organisational.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CC4 &lt;/td&gt;&lt;td&gt;Monitoring: do you check your own controls are present and functioning?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CC5 &lt;/td&gt;&lt;td&gt;Control activities.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CC6 &lt;/td&gt;&lt;td&gt;Logical and physical access. Who can reach what, and how you keep the outside out.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CC7 &lt;/td&gt;&lt;td&gt;System operations. Detecting vulnerabilities and configuration drift, spotting anomalies, handling incidents.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CC8 &lt;/td&gt;&lt;td&gt;Change management. Nothing untested or unauthorised reaches production.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CC9 &lt;/td&gt;&lt;td&gt;Risk mitigation, including vendor and third-party risk.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you run containers on Kubernetes, most of your technical evidence lands in CC6, CC7 and CC8. CC6.8 is the one people miss: &lt;em&gt;&quot;the entity implements controls to prevent or detect and act upon the introduction of unauthorized or malicious software.&quot;&lt;/em&gt; That is your image supply chain, stated in 2017 language.&lt;/p&gt;
&lt;h3 id=&quot;soc-2-does-not-tell-you-what-to-do&quot;&gt;SOC 2 does not tell you what to do&lt;/h3&gt;
&lt;p&gt;This is the part that catches teams out.&lt;/p&gt;
&lt;p&gt;There is no SOC 2 control list. There is no &quot;you must scan images&quot; clause. The criteria are outcome statements, and &lt;em&gt;you&lt;/em&gt; define the controls that meet them, and then the auditor tests the controls &lt;em&gt;you&lt;/em&gt; defined. You write a control saying &quot;all container images are scanned before deployment and images with critical vulnerabilities are blocked&quot;, and now you are on the hook to prove that happened every day of the period.&lt;/p&gt;
&lt;p&gt;This cuts both ways. The freedom is real: you are not forced into somebody else's architecture. The trap is also real, and it is that a weak control, written vaguely, passes the audit while protecting nothing. &quot;We use industry standard scanning tools&quot; is a sentence that can be true while your cluster runs whatever anybody pushes.&lt;/p&gt;
&lt;p&gt;The auditor is not there to tell you the control is weak. They are there to test whether it operated as described.&lt;/p&gt;
&lt;h3 id=&quot;the-other-instruments-and-their-clocks&quot;&gt;The other instruments, and their clocks&lt;/h3&gt;
&lt;p&gt;SOC 2 is voluntary and market-driven; you do it because a customer asked. Most of what arrived after it is not voluntary. Here is the landscape as it stands at the end of August 2026.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EU Cyber Resilience Act&lt;/strong&gt; — Regulation (EU) 2024/2847. Applies to products with digital elements sold in the EU, which for most of us means the software itself. Three dates: conformity assessment bodies from 11 June 2026 (passed), vulnerability and incident reporting from &lt;strong&gt;11 September 2026&lt;/strong&gt; (eleven days away), and the substantive obligations, including technical documentation and the SBOM requirement, from &lt;strong&gt;11 December 2027&lt;/strong&gt;. Annex I Part II point 1 is the SBOM clause: components documented in a commonly used machine-readable format, covering at least the top-level dependencies. You do not have to publish it. You do have to have it and hand it over when a market surveillance authority asks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NIS2&lt;/strong&gt; — Directive (EU) 2022/2555. Applies to entities in essential and important sectors, and reaches software vendors indirectly, forcefully, through Article 21(2), which names supply chain security and requires covered entities to consider the security practices of their direct suppliers. That is why NIS2-derived questionnaires arrive at companies that are not themselves in scope. Being a supplier to somebody in scope is enough.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DORA&lt;/strong&gt; — Regulation (EU) 2022/2554, in force since 17 January 2025 for EU financial entities and the ICT providers serving them. Chapter V Article 28 is the third-party risk chapter, and it has teeth in a way NIS2 does not, including audit rights written into contracts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ISO/IEC 27001:2022&lt;/strong&gt; — certification rather than a report. Buyers outside the US often prefer it. Annex A control 8.8 (technical vulnerabilities), 8.25 through 8.31 (secure development, separation of environments) are the ones your pipeline touches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NIST&lt;/strong&gt; — SP 800-190 for container security specifically, SP 800-218 (SSDF) for secure development, SP 800-53 Rev. 5 for the full federal control catalogue. Not law unless you sell to the US government, but the language is precise and much of the rest of the world quotes it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GDPR Article 32&lt;/strong&gt; — security of processing. Short, general, and still the clause that turns a container escape into a regulatory event if personal data was on the other side of it.&lt;/p&gt;
&lt;h3 id=&quot;they-are-all-asking-the-same-five-questions&quot;&gt;They are all asking the same five questions&lt;/h3&gt;
&lt;p&gt;Here is the thing I did not expect when I started reading these side by side. The instruments are written by different bodies, in different decades, for different legal systems, and underneath they converge hard.&lt;/p&gt;
&lt;p&gt;CRA Annex I Part I (3)(g), on availability and resilience of essential functions, and SOC 2 A1.2 both turn on outages and what the system does when a component fails. CRA (3)(c) on access control and CC6.1 both turn on hardening and federated access. CRA (3)(k) on recording and monitoring activity and CC7.2 both turn on the audit trail. DORA Article 9's protection and prevention requirements land in the same place as CC6.8.&lt;/p&gt;
&lt;p&gt;For a workload running in a cluster, strip away the citations and every one of them is asking:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What is running?&lt;/strong&gt; An inventory. Not &quot;what did we deploy&quot;, what is &lt;em&gt;actually admitted right now&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Where did it come from?&lt;/strong&gt; Provenance. A signature, an identifiable registry, a build you can point at.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What is wrong with it?&lt;/strong&gt; Known vulnerabilities, against a database that was current at the time of the check.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Who allowed the exceptions?&lt;/strong&gt; Because there are always exceptions. An unrecorded exception is indistinguishable from a control failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can you prove all of the above for a period you did not choose in advance?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Question 5 is the one tools get wrong.&lt;/p&gt;
&lt;h3 id=&quot;what-good-evidence-looks-like&quot;&gt;What good evidence looks like&lt;/h3&gt;
&lt;p&gt;Most compliance tooling is built to produce a dashboard. A dashboard tells you the state now. An auditor is not asking about now, they are asking about February.&lt;/p&gt;
&lt;p&gt;Working backwards from what a Type II sampling exercise actually needs, evidence has to carry five properties:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A period, stated on the document.&lt;/strong&gt; Not &quot;generated today&quot;, but &quot;covers 1 February to 31 July, and here is what happened in that window.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provenance of the evidence itself.&lt;/strong&gt; Who generated it, from which system, at what version. A compliance mapping is an interpretation, and interpretations change as you read a framework more carefully. Evidence that does not record which reading it used cannot be re-examined later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integrity.&lt;/strong&gt; A PDF is a suggestion. Anybody can edit a PDF. If the document that goes to the auditor is not cryptographically bound to the bytes that were generated, then the strongest statement you can make about it is &quot;someone in our company typed this.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Citation, not paraphrase.&lt;/strong&gt; Every claim should name the clause it answers, quoted close to the instrument's own words. Auditors read the criterion, not your marketing vocabulary. If your report says &quot;we are compliant with CC7.1&quot; without restating what CC7.1 asks, you have made the reader do a lookup, and made yourself impossible to check.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Honest gaps.&lt;/strong&gt; This is the one I care most about. A container security tool can evidence maybe a third of NIS2 Article 21. The rest is staff training, disclosure policies, reporting to a national CSIRT, business continuity. A report that quietly maps a feature list onto the other two thirds is worse than no report at all, because it tells an organisation a duty is discharged when nobody has done it. Anything that comes back all green is lying to you, and the lie is expensive precisely when it matters.&lt;/p&gt;
&lt;h3 id=&quot;why-the-admission-controller-is-the-right-place-to-stand&quot;&gt;Why the admission controller is the right place to stand&lt;/h3&gt;
&lt;p&gt;For the Kubernetes-specific part of this: scanning in CI is necessary and insufficient.&lt;/p&gt;
&lt;p&gt;CI scanning proves you scanned an image. It does not prove that only scanned images ran. Those are different claims and the second is the one CC6.8 asks for. Between the pipeline and the cluster there is a gap wide enough to drive a &lt;code&gt;kubectl apply&lt;/code&gt; through: a manually applied manifest, a Helm chart from a vendor, a sidecar injected by a mesh, a &lt;code&gt;latest&lt;/code&gt; tag that resolved to something different this morning than it did last week.&lt;/p&gt;
&lt;p&gt;The admission webhook is the last point where the answer to &quot;may this run&quot; is still no. It is also the only point where a record of the decision is a record of what actually entered the cluster, rather than a record of intent. Every admission review is a dated, attributable event: this digest, this policy, admitted or refused, at this moment. That is the raw material a Type II sample is made of, and it accumulates whether or not anybody remembers to collect it.&lt;/p&gt;
&lt;p&gt;That property, evidence that exists because the control ran and not because somebody prepared for an audit, is the whole point.&lt;/p&gt;
&lt;h3 id=&quot;how-we-built-this-into-attestkeep&quot;&gt;How we built this into Attestkeep&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://attestkeep.com&quot;&gt;Attestkeep&lt;/a&gt; is a Kubernetes admission controller that scans images, verifies signatures and attestations, and enforces policy at admission. That part is not novel and I would not write a post about it. The part I want to describe is what happens after six months of it running.&lt;/p&gt;
&lt;p&gt;You pick a period and a framework. It generates a report against eight of them: SOC 2, EU CRA + NIS2, DORA, NIST SP 800-190, SSDF, SP 800-53 Rev. 5, ISO 27001, and GDPR Article 32. Every requirement in the report carries its citation, the obligation restated close to the instrument's own words, a status, and a note written in the period's own numbers.&lt;/p&gt;
&lt;p&gt;There are four statuses, and the fourth is the reason I am writing this section:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;met&lt;/code&gt; — the control operates and the records show it operating&lt;/li&gt;
&lt;li&gt;&lt;code&gt;partial&lt;/code&gt; — it operates but something narrows what it proves: a permissive failure policy, audit-only mode, an exemption&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gap&lt;/code&gt; — squarely within what this thing can enforce, and the records say it is not being enforced&lt;/li&gt;
&lt;li&gt;&lt;code&gt;outside_scope&lt;/code&gt; — a duty of the organisation, not a property of a cluster. Named rather than omitted, so the reader can see where the document's coverage ends.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is an unedited fragment from a real 30-day report generated against a demo cluster:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: &quot;CC4.1&quot;,
  &quot;citation&quot;: &quot;CC4.1&quot;,
  &quot;title&quot;: &quot;Evaluations to ascertain that controls are present and functioning&quot;,
  &quot;asks&quot;: &quot;The entity selects, develops, and performs ongoing and/or separate evaluations to ascertain whether the components of internal control are present and functioning.&quot;,
  &quot;status&quot;: &quot;met&quot;,
  &quot;note&quot;: &quot;the control's operation is measured continuously rather than sampled: 293 reviews, 31 admitted, 262 refused, and 3 windows in which it was not operating&quot;,
  &quot;evidence&quot;: [&quot;admission&quot;, &quot;availability&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and, from the same report, one that did not pass:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: &quot;CC6.6&quot;,
  &quot;citation&quot;: &quot;CC6.6&quot;,
  &quot;status&quot;: &quot;gap&quot;,
  &quot;note&quot;: &quot;no registry allow-list is in force, so an image may be pulled from any registry the node can reach&quot;,
  &quot;evidence&quot;: [&quot;control_statement.allowed_registries&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The summary for that period was 3 met, 6 partial, 2 gap, 2 outside scope. That is a demo cluster with a loose policy, and the report says so. I would rather ship a tool that tells you that in August than one that tells you everything is fine and lets an auditor find it in February.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;evidence&lt;/code&gt; field is a pointer into the sections of the same document that carry the underlying numbers, so a reader can check the claim rather than accept the note.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integrity.&lt;/strong&gt; The report is signed with Ed25519 into a DSSE envelope carrying an in-toto Statement v1. The signature covers &lt;code&gt;report.json&lt;/code&gt; and nothing else. The subject is the report's SHA-256, so the signature binds to the exact bytes, and any copy of those bytes can be checked against it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What the auditor receives&lt;/strong&gt; is a zip:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;README.txt          what is signed, and three ways to check it
report.json         the evidence. these exact bytes are what is signed
attestation.json    DSSE envelope, in-toto Statement v1
certificate.jwt     the public half of the signing key, signed by us
&amp;lt;id&amp;gt;.pdf            a readable rendering. convenience only, carries no signature
&amp;lt;id&amp;gt;.attestkeep.json  all of the above in one file, for single-file verifiers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The README is always in English, whatever language the console is set to, because an auditor's file gets read in places you cannot predict. It states plainly that if the PDF and &lt;code&gt;report.json&lt;/code&gt; ever disagree, &lt;code&gt;report.json&lt;/code&gt; is the record and the PDF is wrong. Verification works three ways: a browser page that runs entirely client-side and uploads nothing, a Python script with no dependencies, or &lt;code&gt;cosign&lt;/code&gt; if you already have it. The archive is byte-for-byte reproducible: the zip entry timestamps come from the report's own generation time rather than the clock, so two people downloading the same package get the same file.&lt;/p&gt;
&lt;p&gt;And the scope statement, which is printed at the top of every SOC 2 report we produce:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;It is evidence for an audit, not an audit: only a service auditor issues an opinion, and only the entity defines its own system boundary.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I put that there because I have seen what the alternative does to people. A tool cannot make you compliant. It can make the day the auditor asks a boring one.&lt;/p&gt;
&lt;h3 id=&quot;what-this-does-not-do&quot;&gt;What this does not do&lt;/h3&gt;
&lt;p&gt;Being straight about the limits, since I just spent 400 words demanding honest gaps from everybody else.&lt;/p&gt;
&lt;p&gt;The compliance mappings have not been through legal or audit review. They are my careful reading of published texts, with citations included specifically so a professional can check my work rather than trust it. The NIST SP 800-190 citations were corrected once already, when I went back and compared them against the published guidance instead of my notes.&lt;/p&gt;
&lt;p&gt;Nothing here binds a report to a trusted clock. &lt;code&gt;generated_at&lt;/code&gt; is an assertion by the signer, not proof. If you need trusted timestamping, you need it from somewhere else, and the predicate says so in a comment rather than hiding it.&lt;/p&gt;
&lt;p&gt;And the honest scope line applies to me too: one cluster's admission control answers part of SOC 2 and a smaller part of NIS2. It does not answer your access reviews, your onboarding, your incident response drills, or your vendor register.&lt;/p&gt;
&lt;h3 id=&quot;if-you-take-one-thing&quot;&gt;If you take one thing&lt;/h3&gt;
&lt;p&gt;Whatever you use, and it does not have to be mine, make the evidence a byproduct of the control operating rather than a project you run before an audit. The frameworks converge on this even where their words differ. SOC 2 Type II wants the whole period. CRA wants technical documentation that was maintained, not assembled. DORA wants an audit right that is meaningful when exercised without notice.&lt;/p&gt;
&lt;p&gt;Update (September 2026). Vinh Nguyen pointed out in the comments that questions 1 and 5 come apart exactly where this article admits they might: the admission ledger records honestly when the control was not operating, but nothing in it says what crossed during those windows — or what predates the install, or what walked in under failurePolicy: Ignore. Whatever did is still running long after the window closed, and a ledger of boundary crossings has no row for it.&lt;/p&gt;
&lt;p&gt;He was right, so the control grew a second half. A periodic sweep now reads every running container's observed imageID — not the spec's image reference, which under a mutable tag still matches its original admission event while the bytes underneath have changed — and reconciles each digest against the admission ledger. A running digest with no allowed admission event behind it lands in the evidence package as its own population section, right next to the availability section it completes: the document that admitted the two questions come apart now stitches them back together. Policy decides what a finding does — recorded, announced, or the pod is stopped so its controller recreates it through the webhook, onto the ledger this time. The details, including the honest edges (out-of-scope namespaces, side-loaded images with no registry digest, truncated lists that say so), are in the comment thread below.&lt;/p&gt;
&lt;p&gt;Evidence you can only produce by preparing for the question is not evidence that the control was working. It is evidence that you can prepare.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://attestkeep.com&quot;&gt;Attestkeep&lt;/a&gt;, a Kubernetes admission controller with compliance evidence built in. It is early. If you have been through a Type II with containers in scope and something above is wrong, I would genuinely like to hear it.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32216/soc-2-cra-nis2-they-all-ask-your-cluster-the-same-five-questions</guid>
<pubDate>Fri, 23 Oct 2026 17:49:26 +0000</pubDate>
</item>
<item>
<title>Pub/Sub Isn't Event-Driven Architecture: Here's What Most Teams Miss</title>
<link>https://coderlegion.com/32197/pub-sub-isnt-event-driven-architecture-heres-what-most-teams-miss</link>
<description>&lt;p&gt;Pub/Sub is a messaging pattern. Event-Driven Architecture is an architectural discipline. Every EDA implementation may use Pub/Sub, but not every Pub/Sub implementation is truly Event-Driven Architecture.&lt;/p&gt;
&lt;p&gt;For many years, I would have agreed with the statement that Pub/Sub equals Event-Driven Architecture (EDA).&lt;/p&gt;
&lt;p&gt;Why? Because early discussions around EDA focused primarily on three characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time communication &lt;/li&gt;
&lt;li&gt;Publish/Subscribe messaging &lt;/li&gt;
&lt;li&gt;Asynchronous processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that time, implementing a Pub/Sub broker was often considered synonymous with implementing EDA.&lt;/p&gt;
&lt;p&gt;Today, however, the answer is no.&lt;/p&gt;
&lt;p&gt;As organizations have adopted EDA at scale, the discipline has evolved from a messaging concept into a comprehensive architectural practice. Modern EDA encompasses far more than simply publishing and subscribing to messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📡EDA Is More Than Messaging&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A mature Event-Driven Architecture consists of multiple foundational elements:&lt;br&gt;
&lt;strong&gt;📝Event Definition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Events must be clearly defined and categorized.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;br&gt;
Business Events: CustomerRegistered, OrderPlaced, PaymentCompleted&lt;br&gt;
System Events: FileUploaded, ServiceStarted, DatabaseUpdated, EOD&lt;/p&gt;
&lt;p&gt;Without a consistent event model, organizations quickly end up with thousands of loosely defined messages that are difficult to understand and maintain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;👤Event Ownership&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Just like data, events are business assets.&lt;/p&gt;
&lt;p&gt;Every event should have a clearly identified owner responsible for:&lt;br&gt;
Event definition&lt;br&gt;
Schema management&lt;br&gt;
Versioning&lt;br&gt;
Lifecycle governance&lt;/p&gt;
&lt;p&gt;When nobody owns an event, everyone assumes somebody else does.&lt;br&gt;
&lt;strong&gt;📏Event and Queue Standards&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Topic (event) and queue naming conventions form the foundation of a scalable EDA platform.&lt;br&gt;
A well-defined naming structure enables:&lt;br&gt;
Event discoverability&lt;br&gt;
Consistent routing&lt;br&gt;
Easier troubleshooting&lt;br&gt;
Better governance&lt;br&gt;
Without standards, the event landscape quickly becomes chaotic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🔀Event Flow Design&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EDA is not merely about sending messages.&lt;/p&gt;
&lt;p&gt;It is about how events collaborate to achieve business outcomes.&lt;/p&gt;
&lt;p&gt;Modern architectures require a clear understanding of:&lt;br&gt;
Event relationships&lt;br&gt;
Event choreography&lt;br&gt;
Event dependencies&lt;br&gt;
Business process visibility&lt;/p&gt;
&lt;p&gt;While services remain loosely coupled, the overall event flow must still be intentionally designed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🛡️Event Governance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Governance is the difference between a sustainable platform and an uncontrolled messaging ecosystem.&lt;/p&gt;
&lt;p&gt;Effective governance covers:&lt;br&gt;
Schema management&lt;br&gt;
Data classification&lt;br&gt;
Security controls&lt;br&gt;
Access management&lt;br&gt;
Event lifecycle management&lt;br&gt;
Compliance and auditing&lt;/p&gt;
&lt;p&gt;Governance should be embedded throughout the entire lifecycle, from design through implementation and operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📬Pub/Sub Is Only One Pattern Within EDA&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pub/Sub is an important communication pattern, but it is only one building block of a broader architecture.&lt;br&gt;
A Pub/Sub platform without governance, ownership, standards, and event design is simply a collection of disconnected messages.&lt;/p&gt;
&lt;p&gt;In such environments, organizations commonly encounter problems such as:&lt;br&gt;
&lt;strong&gt;⚠️Consumer Failures&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Consumers may crash when:&lt;br&gt;
Unexpected event types are received&lt;br&gt;
Event schemas change without coordination&lt;br&gt;
Required fields are removed or modified&lt;br&gt;
Data Exposure Risks&lt;/p&gt;
&lt;p&gt;Sensitive information, including Personally Identifiable Information (PII), may be unintentionally delivered to systems or teams that should not have access to it.&lt;br&gt;
Message Loss or Misrouting&lt;/p&gt;
&lt;p&gt;Poorly governed subscriptions can result in:&lt;br&gt;
Events being consumed by unintended systems&lt;br&gt;
Important messages being ignored&lt;br&gt;
Business processes becoming unreliable&lt;br&gt;
Rising Infrastructure Costs&lt;/p&gt;
&lt;p&gt;Unnecessary event distribution increases:&lt;br&gt;
Network traffic&lt;br&gt;
Broker workload&lt;br&gt;
Infrastructure consumption&lt;br&gt;
Operational costs&lt;br&gt;
What begins as architectural flexibility can quickly become architectural sprawl.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;💡 Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many organizations begin their event-driven journey by deploying a message broker and enabling Pub/Sub communication. While this is an important first step, it is only the foundation.&lt;br&gt;
The real challenge is not moving events between systems. The challenge is managing events as long-lived business assets.&lt;/p&gt;
&lt;p&gt;Without clear ownership, governance, standards, and lifecycle management, organizations often end up with an event ecosystem that is difficult to understand, secure, and evolve. Teams gain asynchronous communication, but lose visibility, consistency, and control.&lt;/p&gt;
&lt;p&gt;Successful Event-Driven Architecture is not measured by how many events are produced each day. It is measured by how effectively those events enable business capabilities while remaining discoverable, governed, and trusted across the enterprise.&lt;/p&gt;
&lt;p&gt;In other words:&lt;br&gt;
A message broker can make your systems event-enabled.&lt;br&gt;
Governance, ownership, and standards make your organization truly event-driven.&lt;/p&gt;
&lt;p&gt;❓ What do you think? Have you seen organizations mistake Pub/Sub implementation for Event-Driven Architecture? What governance challenges emerged as event adoption grew?&lt;/p&gt;
&lt;p&gt;This typically drives significantly more comments and discussion from architects, developers, and integration professionals.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32197/pub-sub-isnt-event-driven-architecture-heres-what-most-teams-miss</guid>
<pubDate>Fri, 23 Oct 2026 14:38:21 +0000</pubDate>
</item>
<item>
<title>Don’t Let the LLM Do Everything: Designing a Hybrid AI SRE With Deterministic Controls</title>
<link>https://coderlegion.com/32161/dont-let-the-llm-do-everything-designing-a-hybrid-ai-sre-with-deterministic-controls</link>
<description>&lt;p&gt;An alert fires because a checkout service has started returning HTTP 500 errors.&lt;/p&gt;
&lt;p&gt;An AI SRE begins investigating. It needs to determine which service is failing, which version is deployed, whether anything changed recently, what the logs and traces show, what probably caused the failure, and whether to restart the service or roll it back.&lt;/p&gt;
&lt;p&gt;It is tempting to give all of that work to an LLM.&lt;/p&gt;
&lt;p&gt;That is also where the architecture starts going wrong.&lt;/p&gt;
&lt;p&gt;Some parts of incident investigation genuinely require reasoning. An LLM can help correlate a deployment with a latency spike, interpret an unfamiliar exception, or compare two competing root-cause hypotheses.&lt;/p&gt;
&lt;p&gt;Other questions have exact answers.&lt;/p&gt;
&lt;p&gt;Which commit is deployed? Which team owns the service? Is the error budget exhausted? Does the current identity have permission to restart the workload? These facts should come from authoritative systems, not probabilistic inference.&lt;/p&gt;
&lt;p&gt;As &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.hyperprobe.co/resources/blog/what-is-an-ai-sre&quot;&gt;AI SRE&lt;/a&gt; agents become more capable of querying production systems and participating in incident response, the important architectural question is no longer simply whether an LLM can investigate an incident.&lt;/p&gt;
&lt;p&gt;It is &lt;strong&gt;which parts of the investigation should require an LLM at all&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A production AI SRE should use &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/1302/building-natural-language-command-interfaces-bridge-between-llms-deterministic-systems&quot;&gt;deterministic software&lt;/a&gt; where the answer can be established exactly and use LLM reasoning where uncertainty actually exists.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-an-llm-first-ai-sre&quot;&gt;The Problem With an LLM-First AI SRE&lt;/h3&gt;
&lt;p&gt;The simplest AI SRE architecture looks appealing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Alert
  |
  v
 LLM --------&amp;gt; Metrics
  | ----------&amp;gt; Logs
  | ----------&amp;gt; Traces
  | ----------&amp;gt; Kubernetes
  | ----------&amp;gt; Git
  | ----------&amp;gt; Incident history
  v
Diagnosis
  |
  v
Action
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model receives an alert, decides which tools to call, interprets the results, produces a diagnosis, and possibly chooses a remediation.&lt;/p&gt;
&lt;p&gt;This can work impressively in a controlled demo.&lt;/p&gt;
&lt;p&gt;Production incidents are less forgiving.&lt;/p&gt;
&lt;p&gt;Consider a model attempting to query a metric called:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkout_connection_pool_active
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The real metric is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkout_db_pool_active_connections
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query returns nothing.&lt;/p&gt;
&lt;p&gt;The LLM now has at least two possible interpretations. The metric does not exist, or the query was wrong.&lt;/p&gt;
&lt;p&gt;If there is no deterministic validation between the model and the observability system, the agent may confidently choose the first explanation and continue its investigation using a false assumption.&lt;/p&gt;
&lt;p&gt;This is not theoretical as an architectural failure mode. Recent analysis of multi-agent systems for infrastructure observability describes how an agent can issue an incorrect metric query, receive an empty response, and interpret that response as evidence that the metric itself does not exist.&lt;/p&gt;
&lt;p&gt;The larger problem is that an LLM can blur three very different things:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A fact retrieved from a system, an inference made from evidence, and an assumption created because evidence is missing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For incident response, those distinctions matter.&lt;/p&gt;
&lt;p&gt;An AI SRE should not infer a deployment SHA when the deployment platform already knows it. It should not decide whether a production restart is authorized when an access-control policy can answer the question exactly. It should not assume a query is valid when its schema can be checked before execution.&lt;/p&gt;
&lt;p&gt;Better prompting can reduce some mistakes. Architecture can eliminate entire classes of them.&lt;/p&gt;
&lt;h3 id=&quot;divide-incident-response-into-deterministic-and-probabilistic-work&quot;&gt;Divide Incident Response Into Deterministic and Probabilistic Work&lt;/h3&gt;
&lt;p&gt;A useful design exercise is to take every task performed during an incident and ask one question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Could this operation be reliably expressed as a lookup, schema, policy, threshold, state machine, or executable test?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the answer is yes, the LLM probably should not be the primary authority for that operation.&lt;/p&gt;
&lt;p&gt;Consider a typical production investigation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Incident task&lt;/strong&gt;                               &lt;/th&gt;&lt;th&gt;&lt;strong&gt;Best owner&lt;/strong&gt;               &lt;/th&gt;&lt;th&gt;&lt;strong&gt;Reason&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Identify service owner                          &lt;/td&gt;&lt;td&gt;Deterministic                &lt;/td&gt;&lt;td&gt;Service catalog already contains the answer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Retrieve deployed commit                        &lt;/td&gt;&lt;td&gt;Deterministic                &lt;/td&gt;&lt;td&gt;Deployment system contains the exact state&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Check SLO threshold                             &lt;/td&gt;&lt;td&gt;Deterministic                &lt;/td&gt;&lt;td&gt;Mathematical evaluation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Validate log or metric query                    &lt;/td&gt;&lt;td&gt;Deterministic                &lt;/td&gt;&lt;td&gt;Schema can be checked&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Retrieve logs and traces                        &lt;/td&gt;&lt;td&gt;Tooling                      &lt;/td&gt;&lt;td&gt;Execution does not require reasoning&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Correlate unusual signals                       &lt;/td&gt;&lt;td&gt;LLM                          &lt;/td&gt;&lt;td&gt;Requires contextual interpretation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Interpret an unfamiliar exception               &lt;/td&gt;&lt;td&gt;LLM                          &lt;/td&gt;&lt;td&gt;Semantic reasoning is useful&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Generate competing root-cause hypotheses        &lt;/td&gt;&lt;td&gt;LLM                          &lt;/td&gt;&lt;td&gt;Requires inference from incomplete evidence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Verify infrastructure permissions               &lt;/td&gt;&lt;td&gt;Deterministic                &lt;/td&gt;&lt;td&gt;Policy decision&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Check whether evidence contradicts a hypothesis &lt;/td&gt;&lt;td&gt;Hybrid                       &lt;/td&gt;&lt;td&gt;Reasoning plus explicit verification&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Execute production remediation                  &lt;/td&gt;&lt;td&gt;Deterministic or human-gated &lt;/td&gt;&lt;td&gt;High-consequence operation&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This division does not make the AI SRE less capable. It gives the model a smaller and better-defined job.&lt;/p&gt;
&lt;p&gt;The model should reason about what operational facts mean. It should not be responsible for inventing those facts.&lt;/p&gt;
&lt;h3 id=&quot;a-hybrid-architecture-for-ai-sre&quot;&gt;A Hybrid Architecture for AI SRE&lt;/h3&gt;
&lt;p&gt;A safer architecture puts deterministic controls both before and after LLM reasoning.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                 +-------------------+
                 |       Alert       |
                 +---------+---------+
                           |
                           v
            +-----------------------------+
            | Deterministic Enrichment   |
            | Service / Owner            |
            | Environment                |
            | Deployment SHA             |
            | Recent Changes             |
            | SLO State                  |
            +-------------+---------------+
                          |
                          v
            +-----------------------------+
            | Evidence Retrieval          |
            | Logs, Metrics, Traces       |
            | Events, Runtime State       |
            +-------------+---------------+
                          |
                          v
                 +-------------------+
                 |   LLM Reasoning   |
                 | Correlate         |
                 | Hypothesize       |
                 | Explain           |
                 +---------+---------+
                           |
                           v
            +-----------------------------+
            | Deterministic Verification  |
            | Query validation            |
            | Evidence checks             |
            | Policy evaluation           |
            | Permission boundaries       |
            +-------------+---------------+
                          |
                          v
                 Human / Action Gate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first deterministic layer enriches the incident with facts that should not require interpretation.&lt;/p&gt;
&lt;p&gt;Instead of prompting an LLM to work out which deployment caused the problem, the system can provide:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;service&quot;: &quot;checkout-api&quot;,
  &quot;environment&quot;: &quot;production&quot;,
  &quot;deployment_sha&quot;: &quot;a83f9c2&quot;,
  &quot;deployment_time&quot;: &quot;14:03:17Z&quot;,
  &quot;incident_start&quot;: &quot;14:07:42Z&quot;,
  &quot;owner&quot;: &quot;checkout-platform&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The evidence layer then retrieves the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dzone.com/articles/observability-pillars-exploring-logs-metrics-and-t&quot;&gt;logs, metrics, and traces&lt;/a&gt; relevant to that incident.&lt;/p&gt;
&lt;p&gt;Only then does the LLM perform the work it is actually good at: correlating signals, interpreting unstructured information, finding relationships, and generating hypotheses.&lt;/p&gt;
&lt;p&gt;The final layer is equally important.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The LLM produces a hypothesis. It should not be allowed to turn its own hypothesis into a verified fact.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That requires a verification step outside the reasoning loop.&lt;/p&gt;
&lt;h3 id=&quot;a-worked-example-the-database-is-slow-but-is-it-the-root-cause-&quot;&gt;A Worked Example: The Database Is Slow, but Is It the Root Cause?&lt;/h3&gt;
&lt;p&gt;Consider an API that begins returning intermittent 500 errors four minutes after a deployment.&lt;/p&gt;
&lt;p&gt;The initial telemetry looks suspicious:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;14:03 Deployment a83f9c2 completed
14:07 HTTP 500 rate begins increasing
14:08 Database latency increases
14:09 Checkout error rate reaches 7.8%
14:10 CPU remains normal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A model looking at this evidence might generate a reasonable hypothesis:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Increased database latency indicates connection pool exhaustion, causing checkout requests to fail.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The explanation sounds plausible. That does not make it true.&lt;/p&gt;
&lt;h4 id=&quot;start-with-facts&quot;&gt;Start With Facts&lt;/h4&gt;
&lt;p&gt;The deterministic enrichment layer establishes the production context:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Service: checkout-api
Environment: production
Version: a83f9c2
Deployment time: 14:03 UTC
Incident start: 14:07 UTC
Owner: Checkout Platform
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;None of these values needs to pass through probabilistic reasoning.&lt;/p&gt;
&lt;h4 id=&quot;retrieve-the-available-evidence&quot;&gt;Retrieve the Available Evidence&lt;/h4&gt;
&lt;p&gt;The investigation layer retrieves failed request traces, matching application errors, database pool metrics, the deployment diff, and recent infrastructure events.&lt;/p&gt;
&lt;p&gt;The LLM now sees three important facts.&lt;/p&gt;
&lt;p&gt;The failures started shortly after the application deployment. Almost every failed request executes the same validation path. Database latency increased at roughly the same time.&lt;/p&gt;
&lt;p&gt;That produces at least two viable hypotheses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;H1:&lt;/strong&gt; Database connection pool exhaustion is causing requests to fail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H2:&lt;/strong&gt; The new application code is allowing unexpected request state to reach the database layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important step is what happens next.&lt;/p&gt;
&lt;p&gt;The system should not ask the model to choose whichever explanation sounds more likely. It should determine what evidence would falsify or support each hypothesis.&lt;/p&gt;
&lt;p&gt;For H1, that might mean checking active and maximum connection counts.&lt;/p&gt;
&lt;p&gt;For H2, the missing piece might be the value of a request field at a specific line in the validation path.&lt;/p&gt;
&lt;h3 id=&quot;when-the-evidence-does-not-exist-yet&quot;&gt;When the Evidence Does Not Exist Yet&lt;/h3&gt;
&lt;p&gt;This is where incident investigation sometimes hits the limit of &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/16268/observability-vs-monitoring-whats-the-difference-and-why-it-matters-for-developers&quot;&gt;conventional observability&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Logs only contain values the application was instructed to record. Traces record instrumented operations and context. Metrics aggregate measurements chosen in advance.&lt;/p&gt;
&lt;p&gt;If the variable needed to confirm a hypothesis was never captured, the LLM cannot recover it by reasoning harder.&lt;/p&gt;
&lt;p&gt;The correct response to missing evidence is to obtain better evidence.&lt;/p&gt;
&lt;p&gt;One approach is bounded runtime instrumentation. For example, a production debugging system such as HyperProbe can place a temporary read-only probe at a specific line and capture runtime state when live traffic reaches it. The important architectural property here is not the product itself. It is that the investigation gains new evidence without allowing the reasoning model to modify application state.&lt;/p&gt;
&lt;p&gt;Suppose the capture returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;customer.region = null
connectionPool.active = 34
connectionPool.max = 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The investigation has changed materially.&lt;/p&gt;
&lt;p&gt;The database pool is nowhere near exhaustion. The first hypothesis can be rejected.&lt;/p&gt;
&lt;p&gt;The unexpected null value, combined with the deployment diff and failing code path, now provides evidence for the second hypothesis.&lt;/p&gt;
&lt;p&gt;This is a better incident-response pattern than asking an LLM to repeatedly reinterpret the same incomplete telemetry.&lt;/p&gt;
&lt;p&gt;It also highlights an important distinction between AI reasoning and observability: &lt;strong&gt;an agent can only reason from evidence that exists.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;put-deterministic-controls-around-tool-calls&quot;&gt;Put Deterministic Controls Around Tool Calls&lt;/h3&gt;
&lt;p&gt;The same principle should apply whenever an AI SRE interacts with production systems.&lt;/p&gt;
&lt;p&gt;An LLM can decide that querying a particular metric would help the investigation. It does not follow that the raw model output should be executed directly.&lt;/p&gt;
&lt;p&gt;A validation layer can first check whether the requested tool exists, required parameters are present, the metric or field is valid, the requested time range is permitted, and the caller has access to the target environment.&lt;/p&gt;
&lt;p&gt;This becomes more important as agents gain access to more systems. DZone's coverage of &lt;a rel=&quot;nofollow&quot; href=&quot;https://dzone.com/articles/tool-call-observability-reliable-secure-ai-agents&quot;&gt;tool-call observability&lt;/a&gt; makes the related point that teams need visibility into which tools an agent invoked, what parameters were provided, whether those calls succeeded, and whether access controls were respected.&lt;/p&gt;
&lt;p&gt;For an AI SRE, I would go one step further.&lt;/p&gt;
&lt;p&gt;Tool calls should not only be observable after execution. Where possible, they should be validated &lt;strong&gt;before&lt;/strong&gt; execution.&lt;/p&gt;
&lt;p&gt;Suppose the model generates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;tool&quot;: &quot;query_prometheus&quot;,
  &quot;metric&quot;: &quot;checkout_connection_pool_active&quot;,
  &quot;range&quot;: &quot;15m&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A deterministic metric registry can reject the nonexistent metric before the query reaches Prometheus.&lt;/p&gt;
&lt;p&gt;The LLM receives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;status&quot;: &quot;invalid_metric&quot;,
  &quot;suggested_matches&quot;: [
    &quot;checkout_db_pool_active_connections&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the model is reasoning from a known validation failure rather than an ambiguous empty result.&lt;/p&gt;
&lt;h3 id=&quot;separate-diagnosis-from-authority&quot;&gt;Separate Diagnosis From Authority&lt;/h3&gt;
&lt;p&gt;Production access is another place where prompting is a poor substitute for deterministic controls.&lt;/p&gt;
&lt;p&gt;Telling an agent, “Never restart production unless absolutely necessary,” is not an authorization system.&lt;/p&gt;
&lt;p&gt;A better design divides actions by consequence.&lt;/p&gt;
&lt;p&gt;Reading telemetry may be fully automatic. Capturing bounded diagnostic evidence may be permitted under policy. Restarting a staging workload might be automatic. Restarting a production service could require approval. Database mutations might always remain behind an explicit human gate.&lt;/p&gt;
&lt;p&gt;The actual thresholds will vary by organization. The architectural principle does not.&lt;/p&gt;
&lt;p&gt;The model can recommend an action.&lt;/p&gt;
&lt;p&gt;A separate system decides whether that action is authorized.&lt;/p&gt;
&lt;p&gt;This is where human-in-the-loop approval gates become more useful than placing a human behind every single AI operation. Human review should concentrate on consequential or ambiguous actions rather than routine evidence gathering.&lt;/p&gt;
&lt;p&gt;An investigation agent that requires approval every time it reads a log will create new toil. An agent that can restart a stateful database because the prompt described it as “safe” has the opposite problem.&lt;/p&gt;
&lt;h3 id=&quot;the-llm-still-has-an-important-job&quot;&gt;The LLM Still Has an Important Job&lt;/h3&gt;
&lt;p&gt;A hybrid architecture is not an argument against using LLMs in SRE.&lt;/p&gt;
&lt;p&gt;Quite the opposite.&lt;/p&gt;
&lt;p&gt;Incidents routinely produce large amounts of heterogeneous and partially structured evidence. A human might need to move between logs, dashboards, traces, deployment histories, runbooks, source code, and previous incidents before forming a plausible explanation.&lt;/p&gt;
&lt;p&gt;LLMs can be effective at synthesizing that context.&lt;/p&gt;
&lt;p&gt;They can recognize relationships across signals, interpret unfamiliar errors, search previous incident reports, generate competing hypotheses, identify what evidence is still missing, and explain a complex investigation to the on-call engineer.&lt;/p&gt;
&lt;p&gt;These are probabilistic tasks.&lt;/p&gt;
&lt;p&gt;The distinction can be summarized simply:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deterministic systems are good at answering what is true when the truth is encoded somewhere. LLMs are useful for reasoning about what those facts might mean.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An effective AI SRE needs both.&lt;/p&gt;
&lt;p&gt;Even agentic SRE architectures that allow systems to observe, reason, and act still depend on strong guardrails and observability around that reasoning.&lt;/p&gt;
&lt;p&gt;The goal should not be maximum autonomy. The goal should be useful autonomy inside explicit operational boundaries.&lt;/p&gt;
&lt;h3 id=&quot;verify-the-architecture-with-operational-metrics&quot;&gt;Verify the Architecture With Operational Metrics&lt;/h3&gt;
&lt;p&gt;A hybrid design should also be evaluated differently from a chatbot.&lt;/p&gt;
&lt;p&gt;Response quality alone is not sufficient.&lt;/p&gt;
&lt;p&gt;An SRE team should be able to measure whether an agent generated invalid tool calls, retrieved evidence relevant to the actual failure, made claims unsupported by telemetry, attempted actions beyond its permissions, or required unnecessary calls before reaching a useful hypothesis.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric                   &lt;/th&gt;&lt;th&gt;What it tells you&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Invalid tool-call rate       &lt;/td&gt;&lt;td&gt;Whether the agent understands its operational interfaces&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Evidence precision           &lt;/td&gt;&lt;td&gt;Whether retrieved signals actually support the investigation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unsupported-claim rate       &lt;/td&gt;&lt;td&gt;How often conclusions outrun available evidence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Time to confirmed hypothesis &lt;/td&gt;&lt;td&gt;Whether the system reduces investigation time&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Tool calls per incident      &lt;/td&gt;&lt;td&gt;How efficiently it investigates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unsafe action attempts       &lt;/td&gt;&lt;td&gt;Whether policy boundaries are working&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Root-cause accuracy          &lt;/td&gt;&lt;td&gt;Whether the final diagnosis matches known outcomes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is also why end-to-end observability for agents and workflows matters. If an AI SRE reaches the wrong conclusion, engineers need to reconstruct the investigation: which context was provided, which tools were called, what each tool returned, what hypothesis was generated, and why the workflow proceeded to the next step.&lt;/p&gt;
&lt;p&gt;Without that trace, debugging the AI SRE itself becomes another production incident.&lt;/p&gt;
&lt;h3 id=&quot;use-ai-for-uncertainty-and-software-for-certainty&quot;&gt;Use AI for Uncertainty and Software for Certainty&lt;/h3&gt;
&lt;p&gt;The interesting question in AI SRE is not how many operational tasks can be handed to an LLM.&lt;/p&gt;
&lt;p&gt;It is where probabilistic reasoning creates enough value to justify probabilistic behavior.&lt;/p&gt;
&lt;p&gt;Service ownership, deployed versions, access policies, thresholds, schemas, and permissions already have deterministic representations. Keep them deterministic.&lt;/p&gt;
&lt;p&gt;Use the LLM where SRE work genuinely requires interpretation: correlating signals, understanding unstructured evidence, generating hypotheses, comparing explanations, and deciding what evidence to gather next.&lt;/p&gt;
&lt;p&gt;Then verify its conclusions outside the model.&lt;/p&gt;
&lt;p&gt;And when the available telemetry cannot confirm a diagnosis, do not ask the LLM to become more confident. Gather the missing evidence.&lt;/p&gt;
&lt;p&gt;That architecture may look less autonomous than an LLM connected directly to every production tool.&lt;/p&gt;
&lt;p&gt;In practice, it is much closer to what reliability engineering has always been about: allowing flexible reasoning where it helps while making critical behavior predictable where it matters.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32161/dont-let-the-llm-do-everything-designing-a-hybrid-ai-sre-with-deterministic-controls</guid>
<pubDate>Fri, 23 Oct 2026 09:16:16 +0000</pubDate>
</item>
<item>
<title>Can AI SRE Really Reduce MTTR? What It Can and Cannot Automate</title>
<link>https://coderlegion.com/32156/can-ai-sre-really-reduce-mttr-what-it-can-and-cannot-automate</link>
<description>&lt;p&gt;Most SRE teams do not lose the bulk of an incident because nobody noticed something was broken. In mature environments, alerting is already fast enough to wake the right people, interrupt the right channel, and open the right incident record. The harder question starts after the page: what changed, which dependency is involved, how wide is the blast radius, which dashboard matters, what did we try last time, and what action is safe enough to take right now?&lt;/p&gt;
&lt;p&gt;The practical case for &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.hyperprobe.co/resources/blog/what-is-an-ai-sre&quot;&gt;AI SRE&lt;/a&gt; is narrower than “replace the on-call engineer,” and more useful: reduce the time between alert and evidence-backed hypothesis. Good incident response depends on actionable alerts, prepared on-call teams, up-to-date playbooks, clear coordination, and automation that removes repetitive work so responders can focus on judgment.&lt;/p&gt;
&lt;p&gt;So, can AI SRE reduce MTTR? Yes, mostly by compressing investigation, triage, handoffs, runbook selection, and documentation. In practice, the largest MTTR gains usually come from shortening two intervals: alert-to-hypothesis during investigation and mitigation-to-postmortem during recovery review. AI can reduce decision-making drag. It should not be treated as a blanket license for autonomous production changes.&lt;/p&gt;
&lt;h3 id=&quot;where-incident-response-time-actually-goes&quot;&gt;Where incident-response time actually goes&lt;/h3&gt;
&lt;p&gt;MTTR is often discussed as one number, but responders experience it as a sequence of smaller clocks. Time to detect, time to acknowledge, time to orient, time to investigate, time to mitigate, time to verify, time to communicate, time to resolve, and time to document. Monitoring and paging tools have already improved the first two. The middle of the incident is where time still disappears.&lt;/p&gt;
&lt;p&gt;That middle stage is messy because it is not one task. It is a search problem under pressure. Engineers move between dashboards, logs, traces, deploy history, feature flags, ownership maps, recent pull requests, dependency graphs, previous incidents, runbooks, customer reports, and tribal memory. Some of this work is mechanical. Some of it is judgment. The expensive part is that both happen at the same time.&lt;/p&gt;
&lt;p&gt;A useful operating metric is the time from alert to first useful hypothesis. Not the first guess. Not the loudest theory in the incident channel. A useful hypothesis is specific, falsifiable, and supported by evidence: “The checkout latency spike started three minutes after deployment &lt;code&gt;x&lt;/code&gt;; error volume is concentrated in service &lt;code&gt;y&lt;/code&gt;; downstream dependency &lt;code&gt;z&lt;/code&gt; is healthy; rollback is likely safe.”&lt;/p&gt;
&lt;p&gt;A second clock starts after mitigation. Once the service is stable, teams still have to reconstruct the incident from chat threads, dashboards, timestamps, and memory. If that work takes days, the team loses context and ships weaker corrective actions. AI can preserve the evidence while it is still fresh, making the postmortem less dependent on whoever happened to take notes under pressure.&lt;/p&gt;
&lt;p&gt;This is where AI SRE reduces MTTR. It gathers context that humans would otherwise collect manually, ranks likely explanations, and keeps a traceable record of what it found. During the first minutes of an incident, responders usually need the same pieces of context: the relevant runbook, recent changes, &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/pn_28428886923dfc665/logs-are-not-audit-artifacts-why-ai-generated-code-needs-a-signed-ai-bom-28mo&quot;&gt;logs from the failing path&lt;/a&gt;, ownership details, downstream impact, and whether the same pattern has appeared before. AI is useful when it surfaces that context before the incident channel fills with guesses.&lt;/p&gt;
&lt;h3 id=&quot;what-ai-sres-can-automate-today&quot;&gt;What AI SREs can automate today&lt;/h3&gt;
&lt;p&gt;The near-term value of AI incident response automation is strongest when the work is read-heavy, repetitive, and evidence-based. Claims weaken when vendors imply that any incident can be diagnosed and fixed without human review.&lt;/p&gt;
&lt;h4 id=&quot;alert-triage&quot;&gt;Alert triage&lt;/h4&gt;
&lt;p&gt;AI can help with alert triage by grouping related alerts, suppressing obvious duplicates, identifying impacted services, suggesting ownership, and connecting a new incident to past incidents. It can also summarize severity signals for the incident commander: which regions are affected, which customer-facing paths are degraded, whether error budgets are burning, and whether the pattern resembles a known failure mode.&lt;/p&gt;
&lt;p&gt;The caveat is simple: AI does not rescue bad alert hygiene. If alerts are noisy, cause-based rather than symptom-based, or detached from user impact, the AI has to reason from weak inputs. That usually produces faster confusion, not better triage.&lt;/p&gt;
&lt;h4 id=&quot;log-and-metric-investigation&quot;&gt;Log and metric investigation&lt;/h4&gt;
&lt;p&gt;AI is well suited to pulling together logs, metrics, traces, deploy events, and topology data. A human can ask a focused question, and the system can retrieve the relevant slices without requiring the engineer to remember every query language or dashboard path at 3 a.m.&lt;/p&gt;
&lt;p&gt;Summarization is not proof. Correlation is not causation. The output has to remain inspectable: links to raw logs, metric windows, traces, deploy diffs, and the reasoning path. If the AI cannot show why a signal matters, the responder still has to redo the investigation manually.&lt;/p&gt;
&lt;h4 id=&quot;root-cause-analysis&quot;&gt;Root-cause analysis&lt;/h4&gt;
&lt;p&gt;Root-cause analysis automation is most credible when it produces ranked hypotheses, rather than a single magical answer. A good AI SRE should say what it thinks happened, what evidence supports that view, what evidence argues against it, and what validation step should come next.&lt;/p&gt;
&lt;p&gt;This is the philosophy &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.hyperprobe.co/&quot;&gt;HyperProbe&lt;/a&gt; is built around. The goal is not to ask engineers to trust an opaque RCA summary. It is to capture diagnostic evidence through read-only probes, preserve the context around the failure, and help the engineer &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/sentry/how-to-find-root-causes-faster-2n4g&quot;&gt;confirm the root cause faster.&lt;/a&gt; That distinction matters. In production incidents, “the AI says so” is not an acceptable causal chain.&lt;/p&gt;
&lt;p&gt;RCA is also where many AI systems overstate their confidence. Distributed failures rarely announce themselves cleanly. A downstream timeout may be the visible symptom, while the real cause is a slow dependency, a bad config push, a connection-pool limit, a partial deploy, or an unexpected interaction between services. An AI SRE should make that uncertainty explicit and keep alternative hypotheses alive until the evidence eliminates them.&lt;/p&gt;
&lt;h4 id=&quot;incident-timelines-and-postmortems&quot;&gt;Incident timelines and postmortems&lt;/h4&gt;
&lt;p&gt;Incident postmortem automation is one of the least controversial uses of AI SRE. The system can capture actions, decisions, timestamps, communications, metric changes, deploy events, and remediation attempts while the incident is still active. &lt;/p&gt;
&lt;p&gt;Afterward, it can draft a timeline, summarize customer impact, propose action items, and turn recurring patterns into playbook candidates.&lt;/p&gt;
&lt;p&gt;This will not make postmortems automatically good. Humans still need to decide what was causal, which actions to prioritize, and how to write a blameless analysis. AI can remove a lot of reconstruction toil.&lt;/p&gt;
&lt;h4 id=&quot;runbook-execution&quot;&gt;Runbook execution&lt;/h4&gt;
&lt;p&gt;AI can make runbooks more usable. It can find the right runbook, map the current incident to the relevant branch, fill in service names or regions, check preconditions, and propose an action. &lt;/p&gt;
&lt;p&gt;In a mature setup, it can also verify postconditions after the action: alert cleared, error rate normalizing, affected pods healthy, latency returning to baseline.&lt;/p&gt;
&lt;p&gt;The key is to separate runbook recommendation from runbook execution. Suggesting “restart this worker pool” is not the same as restarting it. Parameterizing a rollback is not the same as executing it across production. AI should earn execution authority workflow by workflow, not inherit it globally.&lt;/p&gt;
&lt;h3 id=&quot;where-autonomous-remediation-becomes-risky&quot;&gt;Where autonomous remediation becomes risky&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.gartner.com/en/documents/8162429&quot;&gt;Autonomous remediation&lt;/a&gt; becomes risky when an AI system moves from reading production to changing production. That boundary is operational. A read-only investigation can waste time or lead to the wrong hypothesis. A state-changing action can expand the outage.&lt;/p&gt;
&lt;p&gt;The risk increases when an action has a broad blast radius, ambiguous rollback, hidden coupling, incomplete telemetry, customer or revenue implications, or interaction with another live change. &lt;/p&gt;
&lt;p&gt;Database failovers, schema changes, region-level traffic shifts, global cache flushes, broad restarts, security-policy changes, and feature-flag flips tied to billing or conversion are not good first candidates for full autonomy.&lt;/p&gt;
&lt;p&gt;The right mental model is copilot before autopilot. A safer workflow uses deterministic tools, scoped permissions, policy checks, human authorization, and a durable audit trail before mutating production. &lt;/p&gt;
&lt;p&gt;The AI can propose the action, explain the expected effect, list the rollback path, and verify the result. That does not mean it should always be allowed to execute the action by itself.&lt;/p&gt;
&lt;p&gt;The hard part is not generating a plausible command. The hard part is knowing whether the command is safe under the current production state, whether it conflicts with another action, whether it can be reversed, and whether the system is healthier afterward. &lt;/p&gt;
&lt;p&gt;This is why autonomous remediation needs more than model confidence. It needs bounded actions, rollback semantics, post-action verification, and clear rules for giving control back to humans.&lt;/p&gt;
&lt;h3 id=&quot;human-in-the-loop-vs-fully-autonomous-workflows&quot;&gt;Human-in-the-loop vs. fully autonomous workflows&lt;/h3&gt;
&lt;p&gt;The practical question is which workflow deserves which level of autonomy.&lt;/p&gt;
&lt;p&gt;A read-only AI investigation workflow is the easiest place to start. The AI gathers evidence, summarizes context, ranks hypotheses, and drafts updates, but it cannot mutate production.&lt;/p&gt;
&lt;p&gt;HyperProbe fits naturally here because its read-only probe architecture, default PII redaction, and immutable audit logs support evidence capture without turning every investigation into a write-capable automation path.&lt;/p&gt;
&lt;p&gt;The next level is human-approved execution. The AI proposes a runbook-backed plan with preconditions, expected impact, rollback steps, and verification checks. A human approves the action, and the system executes through scoped tools.&lt;/p&gt;
&lt;p&gt;Fully autonomous remediation should be narrower. It belongs to pre-approved, low-blast-radius, reversible actions with clear success criteria: restart a known stateless worker, scale a saturated deployment within policy, roll back a recent canary, or rotate traffic away from a single unhealthy instance. Even then, autonomy needs scoped identity, RBAC, dry runs, policy gates, rollback, audit logs, and post-action verification.&lt;/p&gt;
&lt;p&gt;Teams should not flip a global switch from manual operations to autonomous operations. They should graduate individual workflows. A stateless restart might move to approval-based execution after several successful recommendations. A canary rollback might become automatic only after the team has clear checks for customer impact, deploy state, and rollback success. A database failover may remain human-approved indefinitely. That is not a failure of AI adoption; it is good production engineering.&lt;/p&gt;
&lt;h3 id=&quot;what-teams-should-realistically-expect-from-ai-sre&quot;&gt;What teams should realistically expect from AI SRE&lt;/h3&gt;
&lt;p&gt;Teams should expect AI SRE to reduce investigation drag before it eliminates on-call work. The realistic gains are fewer blind pivots, faster context gathering, more consistent triage, cleaner handoffs, better incident summaries, faster postmortems, and more disciplined runbook use.&lt;/p&gt;
&lt;p&gt;They should not expect perfect RCA, universal zero-touch remediation, or a system that can compensate for missing telemetry and stale documentation. AI SRE depends on operational inputs: service ownership, deploy metadata, logs, metrics, traces, runbooks, dependency maps, past incidents, and clear access boundaries. If those inputs are incomplete, the AI will either ask better questions or make weaker guesses.&lt;/p&gt;
&lt;p&gt;The best implementations start with a measurement plan. Track time from alert to first useful hypothesis. Track time spent gathering logs and change context. Track how often AI-suggested hypotheses were confirmed, rejected, or partially useful. Track whether postmortems became faster and more complete. MTTR reduction is the outcome, but diagnostic compression is the leading indicator.&lt;/p&gt;
&lt;p&gt;Trust matters as much as speed. A fast suggestion that engineers ignore does not reduce MTTR; a cited, reviewable investigation has a better chance of changing responder behavior.&lt;/p&gt;
&lt;h3 id=&quot;the-maturity-model-ai-assisted-ai-investigated-ai-recommended-ai-remediated&quot;&gt;The maturity model: AI-assisted → AI-investigated → AI-recommended → AI-remediated&lt;/h3&gt;
&lt;p&gt;A useful way to evaluate AI SRE adoption is to treat autonomy as a maturity model, not a binary switch.&lt;/p&gt;
&lt;h4 id=&quot;ai-assisted&quot;&gt;AI-assisted&lt;/h4&gt;
&lt;p&gt;At this stage, AI helps responders read and write faster. It summarizes alerts, explains dashboards, searches documentation, drafts status updates, and answers questions about known systems. It does not perform independent investigation, and it does not take production action. This is low risk and useful, but it will only modestly reduce MTTR if engineers still have to gather all evidence themselves.&lt;/p&gt;
&lt;h4 id=&quot;ai-investigated&quot;&gt;AI-investigated&lt;/h4&gt;
&lt;p&gt;Here, the AI actively collects incident evidence. It pulls relevant telemetry, correlates recent changes, builds an incident timeline, finds similar historical incidents, and produces ranked hypotheses. Engineers still verify the facts, but they start from a prepared diagnostic brief rather than a blank page. This is where many teams will see the first meaningful MTTR reduction.&lt;/p&gt;
&lt;h4 id=&quot;ai-recommended&quot;&gt;AI-recommended&lt;/h4&gt;
&lt;p&gt;At this stage, the AI proposes a remediation plan. The recommendation includes the suspected cause, evidence, runbook reference, expected impact, rollback path, and verification criteria. Humans approve execution. This is the right default for high-value production services because it accelerates action without removing accountability.&lt;/p&gt;
&lt;h4 id=&quot;ai-remediated&quot;&gt;AI-remediated&lt;/h4&gt;
&lt;p&gt;The final stage is limited autonomy. The AI executes only pre-approved, reversible actions inside strict policy boundaries. It verifies the result, rolls back when conditions fail, and records the full trail for review. This can work for narrow classes of incidents, but it should be earned through repeated evidence, not assumed because the model sounds confident.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-reduce-mttr-by-reducing-uncertainty&quot;&gt;Conclusion: reduce MTTR by reducing uncertainty&lt;/h3&gt;
&lt;p&gt;The strongest case for AI SRE is not that it can take production away from engineers. It is that it can remove the investigative drag that keeps engineers from making a confident decision sooner.&lt;/p&gt;
&lt;p&gt;MTTR stays high when responders have to rebuild context from scratch during every incident. They need to know what changed, what is affected, which signal matters, which hypothesis is most likely, which action is safe, and how to prove the system recovered. AI can help with that. It can gather evidence, preserve timelines, connect symptoms to recent changes, surface similar incidents, draft postmortems, and recommend the next step with enough context for a human to challenge it.&lt;/p&gt;
&lt;p&gt;That is a meaningful reduction in operational load. It gives engineers back minutes during the incident and hours after it. It also improves response quality because the team spends less time searching and more time deciding.&lt;/p&gt;
&lt;p&gt;Autonomous remediation should come later, and only for workflows that have earned it. The safer path is to move from AI-assisted to AI-investigated to AI-recommended before allowing AI-remediated actions. Each step should be backed by evidence, scoped access, rollback, verification, and auditability.&lt;/p&gt;
&lt;p&gt;For teams evaluating AI SRE, the question should not be “Can this system fix production by itself?” A better question is: “Can this system help our engineers reach the right conclusion faster, with evidence they trust?” That is where MTTR reduction starts. &lt;/p&gt;
&lt;p&gt;HyperProbe is built for that evidence-first path: faster investigation, verified root cause, and human-controlled production decisions.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;can-ai-sre-really-reduce-mttr-&quot;&gt;Can AI SRE really reduce MTTR?&lt;/h4&gt;
&lt;p&gt;Yes, but usually by reducing investigation time rather than by fully automating production fixes. AI SRE is most useful when it helps engineers move faster from alert to evidence-backed hypothesis, then preserves the context needed for verification and postmortems.&lt;/p&gt;
&lt;h4 id=&quot;what-part-of-incident-response-can-ai-sre-automate-today-&quot;&gt;What part of incident response can AI SRE automate today?&lt;/h4&gt;
&lt;p&gt;AI SRE can automate or assist with alert triage, log and metric investigation, change correlation, root-cause hypothesis generation, incident timelines, postmortem drafts, and runbook recommendations. These workflows are valuable because they are evidence-heavy and often repetitive.&lt;/p&gt;
&lt;h4 id=&quot;can-ai-sre-automate-root-cause-analysis-&quot;&gt;Can AI SRE automate root-cause analysis?&lt;/h4&gt;
&lt;p&gt;AI SRE can assist root-cause analysis by producing ranked hypotheses, supporting evidence, counter-evidence, and next validation steps. It should not be treated as a final authority because production incidents often involve incomplete telemetry, hidden dependencies, and multiple plausible causes.&lt;/p&gt;
&lt;h4 id=&quot;when-does-autonomous-remediation-become-risky-&quot;&gt;When does autonomous remediation become risky?&lt;/h4&gt;
&lt;p&gt;Autonomous remediation becomes risky when the action changes production state, has broad blast radius, is hard to roll back, depends on incomplete data, or affects customers, revenue, security, or compliance. These actions should usually require human approval.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-safest-way-to-adopt-ai-sre-&quot;&gt;What is the safest way to adopt AI SRE?&lt;/h4&gt;
&lt;p&gt;The safest path is a staged maturity model: AI-assisted, AI-investigated, AI-recommended, and only then AI-remediated for narrow, reversible workflows. Each stage should include evidence, scoped permissions, rollback, verification, and audit logs.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32156/can-ai-sre-really-reduce-mttr-what-it-can-and-cannot-automate</guid>
<pubDate>Fri, 23 Oct 2026 07:58:29 +0000</pubDate>
</item>
<item>
<title>Tolerant Decoding, Strict Release Gates</title>
<link>https://coderlegion.com/32154/tolerant-decoding-strict-release-gates</link>
<description>&lt;p&gt;A release verifier has two jobs that are easy to blur together. It must understand the representation produced by the build chain, and it must enforce the policy that decides whether an artifact may leave that chain.&lt;/p&gt;
&lt;p&gt;Those jobs need different attitudes.&lt;/p&gt;
&lt;p&gt;Decoding can be narrowly tolerant of documented, meaning-preserving variation. Release policy should remain exact and fail closed.&lt;/p&gt;
&lt;p&gt;That distinction surfaced in a recent mobile build repair. A generated JSON settings file could begin with the optional UTF-8 byte-order marker. The JSON content was valid, but the verifier decoded it as plain UTF-8 and failed before it could evaluate the values that actually governed release safety.&lt;/p&gt;
&lt;p&gt;The fix was not to “clean up” the input generally. It was to accept one specific encoding variation, then run the same strict semantic checks as before.&lt;/p&gt;
&lt;h3 id=&quot;representation-is-not-policy&quot;&gt;Representation is not policy&lt;/h3&gt;
&lt;p&gt;A byte-order marker sits at the representation boundary. In this case, accepting UTF-8 with or without that marker did not change the JSON values.&lt;/p&gt;
&lt;p&gt;An endpoint or feature-handoff setting is different. It changes what the package will do after installation. That is policy-bearing data.&lt;/p&gt;
&lt;p&gt;Treating both categories as raw bytes creates a brittle gate: a harmless encoding detail can block a valid package. Treating both categories as broadly normalisable creates a permissive gate: a meaningful difference may be trimmed, defaulted, coerced, or discarded before verification sees it.&lt;/p&gt;
&lt;p&gt;The useful middle ground is narrow tolerance:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Decode only the representation variants the producer is documented to emit.&lt;/li&gt;
&lt;li&gt;Parse into an explicit data shape.&lt;/li&gt;
&lt;li&gt;Compare policy-bearing values exactly.&lt;/li&gt;
&lt;li&gt;Publish nothing until every check has passed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a small design choice, but it keeps compatibility and authority in their proper layers.&lt;/p&gt;
&lt;h3 id=&quot;why-broad-normalisation-is-risky&quot;&gt;Why broad normalisation is risky&lt;/h3&gt;
&lt;p&gt;“Normalise before comparing” sounds sensible. Sometimes it is. The danger is that normalisation is often written without naming which differences are harmless.&lt;/p&gt;
&lt;p&gt;Should surrounding whitespace be trimmed? Should an empty string become null? Should a missing value receive a default? Should a hostname lose its trailing separator? Should key casing be ignored?&lt;/p&gt;
&lt;p&gt;Each transformation might be correct for one contract and wrong for another. Once a release gate performs all-purpose cleanup, reviewers can no longer see which deviations the gate intentionally accepts.&lt;/p&gt;
&lt;p&gt;A better question is: who owns this variation?&lt;/p&gt;
&lt;p&gt;The decoder owns permitted encoding markers. The JSON parser owns JSON syntax. The release policy owns the approved endpoint and capability settings. Package signing owns artifact identity. Device acceptance owns behaviour on the target platform.&lt;/p&gt;
&lt;p&gt;Let each boundary make only the decision it has enough information to make.&lt;/p&gt;
&lt;h3 id=&quot;test-tolerance-and-strictness-together&quot;&gt;Test tolerance and strictness together&lt;/h3&gt;
&lt;p&gt;A positive test for the newly accepted variant is necessary, but it is not enough. It proves compatibility; it does not prove that policy remained intact.&lt;/p&gt;
&lt;p&gt;The stronger test set combines the representation variant with both approved and disallowed semantics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a marker-bearing file with approved values passes;&lt;/li&gt;
&lt;li&gt;the verifier emits a clean, parseable receipt;&lt;/li&gt;
&lt;li&gt;a marker-bearing file with an unapproved endpoint fails;&lt;/li&gt;
&lt;li&gt;a marker-bearing file with an unapproved handoff configuration fails;&lt;/li&gt;
&lt;li&gt;either failure produces no distributable package and no success receipt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those negative cases are the important counterweight. They show that the change expanded the accepted representation, not the accepted deployment policy.&lt;/p&gt;
&lt;p&gt;The no-output assertions also matter. A verifier that reports failure after copying the package has already lost the race. Publication must happen after verification, not alongside it. The absence of a receipt is useful too: downstream automation must not mistake a partially completed run for approval.&lt;/p&gt;
&lt;h3 id=&quot;make-the-artifact-boundary-transactional&quot;&gt;Make the artifact boundary transactional&lt;/h3&gt;
&lt;p&gt;File-based pipelines rarely have a database transaction around the whole release. We can still design them with transactional thinking.&lt;/p&gt;
&lt;p&gt;Build into a private staging location. Verify the package, its embedded settings, its signature, and any required metadata there. Only after the full decision succeeds should the pipeline copy or promote the artifact and write the corresponding receipt.&lt;/p&gt;
&lt;p&gt;On failure, the public output location should remain unchanged.&lt;/p&gt;
&lt;p&gt;This ordering turns the output directory into a simple commit boundary. Before promotion, the candidate is merely work in progress. After promotion, the package and its evidence agree.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-review-checklist-1&quot;&gt;A practical review checklist&lt;/h3&gt;
&lt;p&gt;When one tool generates a file and another tool gates a release from it, review four things:&lt;/p&gt;
&lt;h4 id=&quot;1-enumerate-representation-variants&quot;&gt;1. Enumerate representation variants&lt;/h4&gt;
&lt;p&gt;Record the encodings, markers, line endings, ordering guarantees, and optional fields the producer may legitimately emit. Avoid accepting variants merely because a parser can.&lt;/p&gt;
&lt;h4 id=&quot;2-identify-policy-bearing-fields&quot;&gt;2. Identify policy-bearing fields&lt;/h4&gt;
&lt;p&gt;List the values that change destination, authority, enabled capabilities, signing identity, or runtime behaviour. Compare them deliberately rather than through a generic object cleanup step.&lt;/p&gt;
&lt;h4 id=&quot;3-add-crossed-tests&quot;&gt;3. Add crossed tests&lt;/h4&gt;
&lt;p&gt;Test each permitted representation with approved values and with at least one disallowed value. This catches the accidental move from narrow compatibility to broad permissiveness.&lt;/p&gt;
&lt;h4 id=&quot;4-prove-fail-closed-publication&quot;&gt;4. Prove fail-closed publication&lt;/h4&gt;
&lt;p&gt;Assert not only that verification returns failure, but also that no package, receipt, or other success signal appears in the publish location.&lt;/p&gt;
&lt;h3 id=&quot;the-leadership-lesson&quot;&gt;The leadership lesson&lt;/h3&gt;
&lt;p&gt;Strictness is not the same as rejecting every variation. Good gates are precise about what they tolerate and equally precise about what they refuse.&lt;/p&gt;
&lt;p&gt;That precision makes a pipeline easier to operate. Engineers can fix harmless toolchain differences without weakening release policy. Reviewers can see the exact compatibility allowance. Operators can trust that a failed gate did not leave an ambiguous artifact behind.&lt;/p&gt;
&lt;p&gt;The goal is not a gate that never bends. It is a gate that bends only where meaning is unchanged—and stays immovable where deployment meaning begins.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32154/tolerant-decoding-strict-release-gates</guid>
<pubDate>Fri, 23 Oct 2026 06:38:59 +0000</pubDate>
</item>
<item>
<title>A Clean Scan Is a Data Point, Not a Verdict: Lessons from PHP2Shell Week</title>
<link>https://coderlegion.com/32029/a-clean-scan-is-a-data-point-not-a-verdict-lessons-from-php2shell-week</link>
<description>&lt;p&gt;PHP's two critical flaws, CVE-2026-109618 and CVE-2026-109619 (PHP2Shell), were patched in 8.5.12, 8.4.27, 8.3.36 and 8.2.35. The advice everywhere was to update. I want to write about something less covered: how much you can honestly claim afterwards.&lt;/p&gt;
&lt;h4 id=&quot;the-scan-that-found-nothing&quot;&gt;The scan that found nothing&lt;/h4&gt;
&lt;p&gt;I ran &lt;a rel=&quot;nofollow&quot; href=&quot;http://github.com/hryagstn/laravel-scalpel&quot;&gt;Laravel Scalpel&lt;/a&gt;, an open source filesystem intrusion scanner, against three Laravel applications after updating. No backdoor files. My first instinct was relief. My second was to ask what that result actually rules out.&lt;/p&gt;
&lt;h4 id=&quot;why-a-negative-result-is-weak-evidence&quot;&gt;Why a negative result is weak evidence&lt;/h4&gt;
&lt;p&gt;A scan can only report on what it can see, and it can only recognise what it knows how to recognise. Scalpel looks at five things: PHP files in places they should not be, known obfuscation patterns, risky directives in &lt;code&gt;.htaccess&lt;/code&gt; and &lt;code&gt;.user.ini&lt;/code&gt;, the integrity of &lt;code&gt;.env&lt;/code&gt;, and differences from a saved baseline. Each is a question it answers well. Anything outside them is a question it never asks. Coverage and sensitivity multiply, and if either is low, 'found nothing' tells you little.&lt;/p&gt;
&lt;p&gt;There are at least two explanations for my result. My applications are small, which lowers the odds that anything needed to hide. Or something sits outside the questions above. A scanner inside the application also shares the web server's user, so a flaw that grants code execution in that process gives an attacker the means to tamper with the tool. I ran Scalpel from inside the application and through an external runner for that reason, which narrows the gap without closing it. My strongest evidence is not the scan but a routine that predates it: since April, a scheduled external diff against my baseline has reported to a Telegram bot. I refreshed the baseline myself after the update, so any unexpected change before that should already have alerted me, and none did. That evidence has conditions too: the runs must actually have happened in the window, and the alert level must cover what an attacker would change. Scalpel's documentation states the first limit plainly, and I would rather state all of them than have a reader discover them. From the output alone I cannot separate these cases.&lt;/p&gt;
&lt;h4 id=&quot;a-ladder-of-claims&quot;&gt;A ladder of claims&lt;/h4&gt;
&lt;p&gt;It helped me to treat each check as supporting a different, specific statement:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Version verified:&lt;/strong&gt; the PHP that serves web traffic is a fixed version, containers included. Supports: 'this flaw can no longer be used against us.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs reviewed from 20 October:&lt;/strong&gt; no repeated 500s or memory limit errors that look like probing. Supports: 'we saw no sign of attempts in these logs.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filesystem scan clean:&lt;/strong&gt; no known backdoor patterns in the application files. Supports: 'we found no evidence of intrusion in these files.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets rotated:&lt;/strong&gt; exposed credentials no longer work. Supports: 'even if memory was read, those secrets are dead.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rebuilt from a known good image:&lt;/strong&gt; supports the only claim close to 'clean.'&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most of us stop at rungs one to three and announce we are fine. Only the last rung justifies that word.&lt;/p&gt;
&lt;h4 id=&quot;why-the-timing-makes-this-sharper&quot;&gt;Why the timing makes this sharper&lt;/h4&gt;
&lt;p&gt;The identifiers leaked about a day before the fix, and a security firm reported exploitation attempts shortly after release. For teams that patched hours or days later, the unprotected window was real, and a patch does nothing about what happened inside it.&lt;/p&gt;
&lt;h4 id=&quot;what-i-would-write-in-a-post-patch-note&quot;&gt;What I would write in a post-patch note&lt;/h4&gt;
&lt;p&gt;Something like: 'Updated PHP on this date. Reviewed logs from 20 October. Scanned the application filesystem. Found no evidence of intrusion in those places. Rotated these secrets.' Each sentence is a claim you can defend, and the places you did not check are visible by omission.&lt;/p&gt;
&lt;p&gt;I would be glad to hear what steps other teams include in their own post-patch verification.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32029/a-clean-scan-is-a-data-point-not-a-verdict-lessons-from-php2shell-week</guid>
<pubDate>Thu, 22 Oct 2026 05:13:41 +0000</pubDate>
</item>
<item>
<title>Kubernetes Security Is Three Problems, Not One: Admission, Identity, Runtime</title>
<link>https://coderlegion.com/32018/kubernetes-security-is-three-problems-not-one-admission-identity-runtime</link>
<description>&lt;h3 id=&quot;-kubernetes-security-is-three-problems-not-one-admission-identity-runtime&quot;&gt;🛡️ Kubernetes Security Is Three Problems, Not One: Admission, Identity, Runtime&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Most Kubernetes compromises start inside the perimeter, where the firewall verifies nothing.&lt;/li&gt;
&lt;li&gt;Treat security as three independent problems: admission (what deploys), identity (what a pod can reach), runtime (what a pod actually does).&lt;/li&gt;
&lt;li&gt;Admission: Pod Security Admission as a free floor, Kyverno for per-resource policy, Gatekeeper where you need Rego. Run in audit mode first, then enforce.&lt;/li&gt;
&lt;li&gt;Identity: IRSA kills static AWS keys, ExternalSecrets kills hardcoded app secrets. Scope roles per workload.&lt;/li&gt;
&lt;li&gt;Runtime: Falco watches syscalls via eBPF and catches what the manifest never declared. Budget time to tune per-workload rules.&lt;/li&gt;
&lt;li&gt;Supply chain is the layer most teams skip: Cosign signing plus Kyverno verifyImages closes the mutable-tag gap, and SLSA provenance plus SBOMs answer &quot;which running images have this CVE?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kubernetes security failures usually do not look like network intrusions. An attacker who gets code execution inside a container, through an application bug, a poisoned dependency, or a policy gap that let a privileged pod schedule, is already inside the perimeter the firewall was watching. From there the firewall verified nothing: which secrets the process can reach, which syscalls it can make, whether the running image is even the one you signed.&lt;/p&gt;
&lt;p&gt;That is the useful reframe. Kubernetes security is not one problem at the boundary. It is three problems inside it, and each one needs its own control that keeps working when the others fail:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Admission&lt;/strong&gt; decides what is allowed to deploy, and with what configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identity&lt;/strong&gt; decides what each running pod is allowed to reach.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime&lt;/strong&gt; watches what a pod actually does, which is often not what its manifest claimed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of the individual tools below are new, and none of this is a new technique. What is worth sharing is how the layers fit together, so a failure in any one of them does not open an exploitable gap. The model maps cleanly onto NIST SP 800-207 (zero trust) and NIST SP 800-190 (container security), and you can hold it to the CIS Kubernetes Benchmark as a measurable floor.&lt;/p&gt;
&lt;h3 id=&quot;layer-1-admission-decide-what-gets-in&quot;&gt;Layer 1: admission, decide what gets in&lt;/h3&gt;
&lt;p&gt;Start with what Kubernetes gives you for free. Pod Security Admission (PSA) enforces security profiles per namespace with no external tooling. The Restricted profile blocks host namespace sharing, privilege escalation, root contexts, and unsafe seccomp settings. A practical split is Restricted for externally facing namespaces and Baseline for internal platform namespaces with documented exceptions.&lt;/p&gt;
&lt;p&gt;PSA is a floor, not a scalpel. It enforces at the namespace level, so it cannot say &quot;this specific Deployment must run as UID 1000 with this seccomp profile.&quot; That is where &lt;strong&gt;Kyverno&lt;/strong&gt; comes in. Its policies are plain YAML, they live in the same GitOps repo as your manifests, and they go through the same pull request review as any other change. Four rule types cover most needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;validate&lt;/code&gt; rejects non-compliant resources (for example, require every image to come from an approved ECR registry).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutate&lt;/code&gt; corrects omissions automatically, which turns &quot;you forgot a security context&quot; from an admission failure into a silent fix.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate&lt;/code&gt; creates dependent resources on creation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verifyImages&lt;/code&gt; enforces signature checks at admission time (more on that below).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For constraint logic that depends on the state of &lt;em&gt;other&lt;/em&gt; cluster resources, or cross-namespace rules, &lt;strong&gt;OPA Gatekeeper&lt;/strong&gt; and Rego express things Kyverno's YAML cannot. Running both is a reasonable choice: Kyverno for the common case, Gatekeeper where you need Rego's relational power.&lt;/p&gt;
&lt;p&gt;One rollout lesson worth stating plainly: both tools have an audit mode, and you should use it first. Measure violations with enforcement off, remediate systematically, then switch to enforce. Flipping straight to enforcement is how a security rollout turns into a production incident.&lt;/p&gt;
&lt;h3 id=&quot;layer-2-identity-kill-the-static-credential&quot;&gt;Layer 2: identity, kill the static credential&lt;/h3&gt;
&lt;p&gt;A static AWS key in a ConfigMap or an environment variable is a persistent risk with a wide blast radius. &lt;strong&gt;IRSA&lt;/strong&gt; (IAM Roles for Service Accounts) removes the category entirely. The AWS SDK in a pod annotated with an IRSA service account exchanges a short-lived, audience-scoped JWT for temporary IAM credentials through STS. Nothing static is created, stored, or rotated. The IAM trust policy validates the token's subject claim against a specific service account and namespace, so a compromised pod in one namespace cannot assume another namespace's role.&lt;/p&gt;
&lt;p&gt;The payoff is blast-radius reduction you can describe concretely. Without IRSA, a compromised pod inherits whatever the node's instance profile can reach. With IRSA, it is limited to that one pod's scoped role.&lt;/p&gt;
&lt;p&gt;Application secrets need a parallel mechanism, and that is the &lt;strong&gt;ExternalSecrets Operator&lt;/strong&gt;. It syncs values from AWS Secrets Manager or Parameter Store into Kubernetes Secrets and refreshes them on the source's rotation schedule. When Secrets Manager rotates a database password, the new value propagates without a deployment. IRSA plus ExternalSecrets together removes every static credential: cloud access through IRSA, application secrets through ESO, nothing hardcoded in a Helm values file.&lt;/p&gt;
&lt;p&gt;Scaling per-workload IAM roles across hundreds of workloads is the part teams tend to defer. Attribute-based access control with IAM permission boundaries makes it tractable: a role that expresses &quot;this service account, in this namespace, for this environment&quot; can be generated from workload metadata instead of hand-authored each time.&lt;/p&gt;
&lt;h3 id=&quot;layer-3-runtime-watch-what-actually-happens&quot;&gt;Layer 3: runtime, watch what actually happens&lt;/h3&gt;
&lt;p&gt;Admission decides what can deploy. Identity decides what it can reach. Neither sees what a pod does after it is running. &lt;strong&gt;Falco&lt;/strong&gt; does, using eBPF probes at the kernel level to observe every syscall from every container on the node. A shell spawned in a container whose image has no shell in its approved process list raises an alert, regardless of how the compromise happened or whether the attacker edited the deployment manifest. Because eBPF sits below the container runtime, it keeps working even if the runtime is tampered with, and it needs no change to the image or the application.&lt;/p&gt;
&lt;p&gt;Two practical notes. First, uniform rules across every workload generate false positives that erode trust in the alerts. A data-processing pod that legitimately reads many files will trip rules tuned for an application server. Per-workload rule sets, stored in Git next to the manifests, let you tighten baselines for payment or secret-handling services and loosen them for CI runners. Second, route Falco alerts into a SIEM so individual detections become correlated events. A shell-spawn alert followed within seconds by an outbound connection to an unknown IP is a pattern a correlation engine can act on, including automated network-policy quarantine of the affected pod.&lt;/p&gt;
&lt;p&gt;Plan for a calibration period. Default Falco rules produce false-positive volume that takes iterative tuning, with both security and application knowledge, before the layer runs at production-grade sensitivity.&lt;/p&gt;
&lt;h3 id=&quot;the-layer-most-teams-skip-supply-chain&quot;&gt;The layer most teams skip: supply chain&lt;/h3&gt;
&lt;p&gt;An image that passes admission is trusted to be what its manifest says, but only if the reference is cryptographically verified. Tags are mutable. Someone can overwrite the content behind a trusted tag and the cluster will deploy it under the same name, which bypasses every admission policy that checks the image's &lt;em&gt;properties&lt;/em&gt; rather than its &lt;em&gt;identity&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cosign&lt;/strong&gt; signs images at the registry, and Kyverno &lt;code&gt;verifyImages&lt;/code&gt; rules reject anything at admission that is not signed by an approved identity. That closes the tag-mutability gap. &lt;strong&gt;SLSA&lt;/strong&gt; provenance is the next step up: signing proves an image came from your build system, provenance proves what went into that build (source commit, build platform, resolved dependencies). A Software Bill of Materials attached as an attestation then lets you answer &quot;which running images contain this newly disclosed CVE?&quot; by querying the SBOM store instead of inspecting containers one by one.&lt;/p&gt;
&lt;h3 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h3&gt;
&lt;p&gt;The point of three independent layers is that no single failure is fatal. A policy gap that lets a bad pod through is still caught by scoped identity and runtime monitoring. A runtime compromise still cannot reach credentials the pod's IAM role never granted. A swapped image is rejected before it schedules.&lt;/p&gt;
&lt;p&gt;If you are starting from scratch, a reasonable order is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Turn on Pod Security Admission at Baseline or Restricted. Free, immediate.&lt;/li&gt;
&lt;li&gt;Add Kyverno in audit mode, learn your violations, then enforce.&lt;/li&gt;
&lt;li&gt;Move workloads to IRSA and ExternalSecrets to delete static credentials.&lt;/li&gt;
&lt;li&gt;Deploy Falco, then budget real time to tune per-workload rules.&lt;/li&gt;
&lt;li&gt;Add Cosign signing and verifyImages, then SLSA provenance and SBOMs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The honest tradeoff is maintenance. Policy-as-code needs upkeep as Kubernetes APIs and workloads change, and Falco rule tuning is ongoing work. The reason to pay that cost is coverage no single control gives you: admission, identity, and runtime failures are different problems, and defending only one of them leaves the other two open.&lt;/p&gt;
&lt;h4 id=&quot;further-reading&quot;&gt;Further reading&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;NIST SP 800-207, Zero Trust Architecture.&lt;/li&gt;
&lt;li&gt;NIST SP 800-190, Application Container Security Guide.&lt;/li&gt;
&lt;li&gt;CIS Kubernetes Benchmark.&lt;/li&gt;
&lt;li&gt;Kyverno, OPA Gatekeeper, Falco, and Sigstore/Cosign project docs.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;If you run this in production, I would be interested in which layer gave you the most trouble to operationalize. For me it is runtime rule tuning. Drop a comment.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32018/kubernetes-security-is-three-problems-not-one-admission-identity-runtime</guid>
<pubDate>Thu, 22 Oct 2026 03:19:32 +0000</pubDate>
</item>
<item>
<title>php2shell: how are you confirming which PHP your web traffic actually runs on?</title>
<link>https://coderlegion.com/32010/php2shell-how-are-you-confirming-which-php-your-web-traffic-actually-runs-on</link>
<description>&lt;p&gt;PHP's 21 October fixes (8.5.12 / 8.4.27 / 8.3.36 / 8.2.35) cover two critical runtime flaws. Because they sit below the application, updating a CMS or framework doesn't help — the runtime has to change and workers have to restart.&lt;/p&gt;
&lt;p&gt;The step I'd expect to trip teams up is inventory. &lt;code&gt;php -v&lt;/code&gt; on a shell tells you about the CLI, not necessarily what PHP-FPM, &lt;code&gt;mod_php&lt;/code&gt; or a bundled appliance is serving.&lt;/p&gt;
&lt;p&gt;What are you using to answer &quot;where does PHP run and is it patched?&quot; — package queries, &lt;code&gt;phpinfo()&lt;/code&gt; behind auth, image scanning, something else? And has anyone found a clean way to confirm old FPM workers are gone after the upgrade?&lt;/p&gt;
&lt;p&gt;Write-up with versions, interim settings and detection signals: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/_firelinks/php2shell-a-request-that-fails-halfway-still-leaves-state-behind-5877&quot;&gt;https://dev.to/_firelinks/php2shell-a-request-that-fails-halfway-still-leaves-state-behind-5877&lt;/a&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32010/php2shell-how-are-you-confirming-which-php-your-web-traffic-actually-runs-on</guid>
<pubDate>Thu, 22 Oct 2026 01:34:44 +0000</pubDate>
</item>
<item>
<title>Use service_healthy gating in Compose — prerequisites, decision, and a limitation</title>
<link>https://coderlegion.com/32008/use-servicehealthy-gating-in-compose-prerequisites-decision-and-a-limitation</link>
<description>&lt;p&gt;Prerequisites&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You control the service image (or can add a lightweight wrapper) and can add a healthcheck that exercises the real readiness condition.&lt;/li&gt;
&lt;li&gt;You use Docker Compose v2-style depends_on conditions (support varies by Compose implementation/version).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Implementation decision&lt;/p&gt;
&lt;p&gt;Put the readiness check on the dependency, not on the dependent. Define a healthcheck that verifies the real work boundary (for example, a small query or a completed bootstrap signal). Then set the dependent’s depends_on to use condition: service_healthy. This makes Compose wait for the backend’s own claim of readiness instead of relying on timed waits.&lt;/p&gt;
&lt;p&gt;Expected result&lt;/p&gt;
&lt;p&gt;Compose starts services in right order and then blocks the dependent until the backend reports healthy. This eliminates the startup race where the dependent is running and fails because its backend is still booting. If the backend lacks a healthcheck, Compose can’t honor service_healthy — which surfaces the missing readiness contract rather than silently permitting a race.&lt;/p&gt;
&lt;p&gt;One limitation&lt;/p&gt;
&lt;p&gt;A healthcheck must be honest and relevant. If it only checks that a TCP port is open, it may still return healthy before the backend can serve the app’s real needs. When the healthcheck is superficial, the gating gives you a false sense of correctness. Keep the check aligned to the actual dependency boundary.&lt;/p&gt;
&lt;h3 id=&quot;working-example&quot;&gt;Working example&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export function evaluateStartability(services, targetServiceName) {
  if (!services || typeof services !== 'object' || Array.isArray(services)) {
    throw new TypeError('services must be an object')
  }

  const target = services[targetServiceName]
  if (!target) throw new Error(`service not found: ${targetServiceName}`)

  const deps = target.depends_on ?? {}
  const normalizedDeps = {}

  if (Array.isArray(deps)) {
    for (const name of deps) normalizedDeps[name] = { condition: 'service_started' }
  } else {
    for (const [name, val] of Object.entries(deps)) {
      if (val == null || val === 'short') {
        normalizedDeps[name] = { condition: 'service_started' }
      } else if (typeof val === 'string') {
        normalizedDeps[name] = { condition: val }
      } else {
        normalizedDeps[name] = { condition: val.condition ?? 'service_started' }
      }
    }
  }

  const reasons = []
  for (const [depName, opts] of Object.entries(normalizedDeps)) {
    const dep = services[depName]
    if (!dep) {
      reasons.push({ dep: depName, ok: false, why: 'missing' })
      continue
    }

    if (dep.state !== 'running') {
      reasons.push({ dep: depName, ok: false, why: `not running (${dep.state})` })
      continue
    }

    if (opts.condition === 'service_healthy') {
      if (!Object.prototype.hasOwnProperty.call(dep, 'health') || dep.health == null) {
        reasons.push({ dep: depName, ok: false, why: 'no healthcheck defined' })
        continue
      }
      if (dep.health !== 'healthy') {
        reasons.push({ dep: depName, ok: false, why: `health=${String(dep.health)}` })
        continue
      }
    }

    reasons.push({ dep: depName, ok: true, why: 'satisfied' })
  }

  return { ok: reasons.every((r) =&amp;gt; r.ok), reasons }
}

export function makeService(state = 'running', health = null, depends_on = undefined) {
  return { state, health, depends_on }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the full article: &lt;a rel=&quot;nofollow&quot; href=&quot;https://chriseugenerodriguez.com/blog/compose-wait-for-ready-service&quot;&gt;https://chriseugenerodriguez.com/blog/compose-wait-for-ready-service&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32008/use-servicehealthy-gating-in-compose-prerequisites-decision-and-a-limitation</guid>
<pubDate>Wed, 21 Oct 2026 23:23:24 +0000</pubDate>
</item>
<item>
<title>Mapping 32k+ MCP Servers: How we are handling tool discovery without schema drift</title>
<link>https://coderlegion.com/31988/mapping-32k-mcp-servers-how-we-are-handling-tool-discovery-without-schema-drift</link>
<description>&lt;p&gt;Hey devs! 👋&lt;/p&gt;
&lt;p&gt;If you are currently running automated code generation or orchestrating local agent loops inside Claude Code, Cursor, or Windsurf, you've probably noticed how fast legacy REST integrations break down under heavy production prompts.&lt;/p&gt;
&lt;p&gt;The ecosystem is rapidly moving toward the Model Context Protocol (MCP) using JSON-RPC 2.0 transport layers. To map out where this tech is heading, we've indexed and tracked the data footprints of active servers across three main directories:&lt;/p&gt;
&lt;p&gt;• Glama Layer: 22.7k–32.7k live environments tracked via active usage signal telemetry.&lt;br&gt;
• mcp.so Index: 20k+ community schema setups.&lt;br&gt;
• Smithery Layer: 6k–7.3k configurations supporting single-command automated setups.&lt;/p&gt;
&lt;p&gt;We wrote a comprehensive engineering guide breaking down how to securely connect trendy components like GitHub repository hooks, PostgreSQL data layers, and Playwright automation routines without blowing up context limits or leaking environmental tokens.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16557954759598184536&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Check out our structural layout mapping and read the full technical walkthrough on our main blog:&lt;br&gt;
🔗 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.seosiri.com/2026/10/mcp-server-registries-guide.html&quot;&gt;https://www.seosiri.com/2026/10/mcp-server-registries-guide.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Let's discuss in the comments below: What custom MCP servers are you currently spinning up in your workspace? 🛠️👇&lt;/p&gt;
&lt;h2 id=&quot;softwareengineering-aiagents-modelcontextprotocol-webdev-buildinpublic&quot;&gt;SoftwareEngineering #AIAgents #ModelContextProtocol #WebDev #BuildInPublic&lt;/h2&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31988/mapping-32k-mcp-servers-how-we-are-handling-tool-discovery-without-schema-drift</guid>
<pubDate>Wed, 21 Oct 2026 14:28:26 +0000</pubDate>
</item>
<item>
<title>bash ran a path that no longer existed, and exited 127</title>
<link>https://coderlegion.com/30653/bash-ran-a-path-that-no-longer-existed-and-exited-127</link>
<description>&lt;p&gt;A demo script called &lt;code&gt;report&lt;/code&gt;, two directories on &lt;code&gt;PATH&lt;/code&gt;, and one &lt;code&gt;mv&lt;/code&gt; between them, all on a scratch tmpfs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ report
report 2.1: 14 jobs ok
exit=0
$ mv /opt/jobs/bin/report /opt/jobs/newbin/report
$ ls -l /opt/jobs/newbin/report
-rwxrwxr-x 1 root root 42 Oct  3 18:26 /opt/jobs/newbin/report
$ report
session.sh: line 5: /opt/jobs/bin/report: No such file or directory
exit=127
$ type -a report
report is /opt/jobs/newbin/report
$ hash -t report
/opt/jobs/bin/report
$ command -v report
/opt/jobs/bin/report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file is there, on &lt;code&gt;PATH&lt;/code&gt;, and bash ran the old path anyway. The first time it finds a command through a &lt;code&gt;PATH&lt;/code&gt; search, it stores the full path in a hash table and skips the search after that. When the file moves, it executes the remembered path and reports &lt;code&gt;No such file or directory&lt;/code&gt; with exit 127, the same code a typo gets.&lt;/p&gt;
&lt;p&gt;The confusing part is the end of that output. &lt;code&gt;type -a&lt;/code&gt; searches &lt;code&gt;PATH&lt;/code&gt; fresh and finds the new location. &lt;code&gt;command -v&lt;/code&gt; reports what bash would run, which is the stale one. &lt;code&gt;hash -r&lt;/code&gt; clears the table, a new terminal starts empty, and &lt;code&gt;shopt -s checkhash&lt;/code&gt; makes bash re-search when a hashed path has gone: with it set, the same sequence printed the tool's output and exit 0.&lt;/p&gt;
&lt;p&gt;The stale hash is one of six causes behind &lt;code&gt;command not found&lt;/code&gt;. The others, from a cron &lt;code&gt;PATH&lt;/code&gt; to a CRLF shebang, each reproduced with a script that names the cause, are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-command-not-found&quot;&gt;command not found in bash, six causes&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30653/bash-ran-a-path-that-no-longer-existed-and-exited-127</guid>
<pubDate>Wed, 21 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>TSB Went Live on a Letter Regulators Called &quot;Statements of Good Intention&quot;; £330M Aftermath</title>
<link>https://coderlegion.com/31971/tsb-went-live-on-a-letter-regulators-called-statements-of-good-intention-330m-aftermath</link>
<description>&lt;p&gt;&lt;em&gt;TSB's 2018 move off the Lloyds platform could not be rolled back. The FCA and PRA notices show the go decision rested on a supplier letter, a test run on one of two data centres, and a memo that left the letter's caveats behind.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;On 10 April 2018, TSB's board met to decide whether to serve notice on Lloyds Banking Group. Serving it would commit the bank to moving its customers off the Lloyds IT platform it had run on since it was carved out of Lloyds in 2014, and onto Proteo4UK, a platform built for it by its new owner, Sabadell. It would also give up the fallback, a copy of the Lloyds platform that TSB had the option to carve out and keep. That same day, the last outstanding confirmation from the platform's supplier chain arrived.&lt;/p&gt;
&lt;p&gt;The board went ahead. On the weekend of 20 to 22 April, TSB moved its customers' accounts across, predominantly in a single event. When the new platform opened, its online and mobile channels failed, and they kept failing. TSB &quot;did not return to a BAU position until 10 December 2018,&quot; the FCA found, and between April 2018 and April 2019 it received 225,492 complaints.&lt;/p&gt;
&lt;p&gt;There was no way back. The FCA's Final Notice puts it plainly: &quot;after MME it was, technically speaking, essentially impossible to roll back from the changes made and revert to using the LBG IT Platform.&quot; MME is the Main Migration Event, that April weekend.&lt;/p&gt;
&lt;p&gt;So the go decision was the whole decision, and what it rested on is the story. The record is unusually complete: two Final Notices issued on 20 December 2022, one from the Financial Conduct Authority and one from the Prudential Regulation Authority; a third from the PRA in April 2023, against the bank's chief information officer; and the bank's own 2018 accounts. Read together, they show a cutover with no way back approved on three pieces of evidence: a letter, a test and a memo. None of the three was what it looked like.&lt;/p&gt;
&lt;h3 id=&quot;why-the-bank-was-moving-at-all&quot;&gt;Why the bank was moving at all&lt;/h3&gt;
&lt;p&gt;Sabadell bought TSB in July 2015, and the migration was the point of the purchase. Its offer document promised &quot;efficiency cost savings in IT amounting to approximately £160 million per annum on a pre-tax basis, in the third full year after completion of the Offer,&quot; and said those savings would &quot;derive from a full migration&quot; onto Sabadell's Proteo platform. The FCA records that &quot;The strategic rationale for the takeover included the expected financial returns from a full migration,&quot; and that &quot;The aim was to achieve migration by the end of 2017.&quot;&lt;/p&gt;
&lt;p&gt;Sabadell knew how to do this. It &quot;had previously migrated seven banks onto&quot; Proteo, the PRA notes, and its &quot;existing methodology for IT migrations was to use a 'big bang' or single operation data migration approach.&quot; This one was different in size, as the FCA notes: &quot;The scale of the migration project was unprecedented,&quot; and the Proteo4UK platform &quot;was not yet proven.&quot;&lt;/p&gt;
&lt;p&gt;TSB's board saw the shape of the risk early. In December 2015 the minutes recorded that &quot;the Board could not be put in a position where they gave up the carve-out option, migration &quot;stalled&quot; or was found to be unworkable leaving the bank in a position without an IT infrastructure.&quot; That is, in so many words, the risk the go decision would later take.&lt;/p&gt;
&lt;p&gt;The schedule slipped once. In September 2017 the board was told the bank could not migrate in November as planned. It acknowledged that the November date, set two and a half years earlier, had been &quot;deliberately very ambitious&quot;, had acted as a &quot;forcing mechanism&quot;, and had been &quot;based on very little information&quot;. The new &quot;Defender Plan&quot; was approved on 24 October 2017. Then, according to the PRA, &quot;The Migration Programme quickly fell behind the Defender Plan schedule, with the result that critical testing plans and principles had to be deviated from to keep on track for MME.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-letter&quot;&gt;The letter&lt;/h3&gt;
&lt;p&gt;The supplier was SABIS, Sabadell's IT subsidiary, which built and ran the platform. On 5 April 2018 it wrote to TSB expressing &quot;confidence as to the migration readiness of the platform.&quot; The letter gave &quot;an early report on certain test results (noting that some tests were still to be completed)&quot; and referred to confirmations from SABIS's own suppliers, the &quot;Critical Fourth Parties.&quot; Those confirmations &quot;were referenced in (but not appended to) the letter.&quot;&lt;/p&gt;
&lt;p&gt;Both regulators later read those confirmations and reached the same verdict, in the same words. They were &quot;to some extent forward looking statements of good intention or expectation rather than statements of fact about the completeness of readiness activities undertaken.&quot; And &quot;All but one was caveated with a number of outstanding tasks or tests which had not yet been completed.&quot;&lt;/p&gt;
&lt;p&gt;The letter then travelled up the bank inside a summary. The memo that recommended going ahead carried the chief information officer's attestation, which &quot;asserted that SABIS was ready for MME and, without any further explanation, that Mr Abarca was satisfied that the SABIS Confirmation could be relied upon.&quot; The memo &quot;did not annex the letter itself. Nor was the letter included in any of the papers for the Board.&quot; The caveats were not in the papers of the board that committed the bank.&lt;/p&gt;
&lt;p&gt;The dates make it starker. The last supplier confirmation &quot;was received on 10 April 2018, the same day that the TSB Board was meeting to determine whether to serve the definitive notice of migration on LBG.&quot; The tests the letter called outstanding &quot;were completed on 17 April 2018.&quot; The board's decisions came on 10, 18 and 19 April, and the migration began on the 20th.&lt;/p&gt;
&lt;p&gt;One more change sat underneath. On 9 February 2018, confirming that non-functional testing was complete became the IT function's sole responsibility. The FCA found that this change &quot;does not appear to have been explicitly drawn to the attention of the TSB Board&quot; when it made those April decisions.&lt;/p&gt;
&lt;h3 id=&quot;the-test-that-ran-on-half-the-system&quot;&gt;The test that ran on half the system&lt;/h3&gt;
&lt;p&gt;The failure itself was not in the data. The PRA is explicit that &quot;the data migration itself was successful.&quot; What failed was the way customers reached the new platform.&lt;/p&gt;
&lt;p&gt;Proteo4UK ran in two data centres in an &quot;Active-Active&quot; configuration, with a Global Load Balancer in front deciding which centre served each session. Each centre &quot;had the capacity to run the entirety of the customer sessions on its own.&quot; And there was nowhere safe to rehearse: &quot;TSB did not have a pre-production environment on which to conduct testing which would stress the platform without affecting the live production environment.&quot;&lt;/p&gt;
&lt;p&gt;TSB first decided to performance-test both data centres together, &quot;to ensure that the conditions were the same as expected at MME.&quot; After a testing forum on 28 February 2018, it accepted a &quot;counter-proposal&quot; from SABIS to test only one, &quot;due to concerns that the live services would otherwise be unavailable (for example ATMs would not have been available for a period of approximately three to four hours).&quot;&lt;/p&gt;
&lt;p&gt;The notice records the reasoning. The configuration of the two centres was assumed to be &quot;symmetric following assurances received,&quot; and &quot;if it could pass with half the capacity, it would be able to easily pass using data centres.&quot; So the test, too, stood on an assurance. The NFT Final Report and the memo that went with it did not refer &quot;to any risks associated with conducting performance testing in only one data centre.&quot;&lt;/p&gt;
&lt;p&gt;Then the platform went live, and &quot;following MME it became apparent that both the Global Load Balancer and the Security Session (OpenAM) component of the data centres had not in fact been correctly configured, leading to the issues experienced by customers in trying to access the digital channels.&quot;&lt;/p&gt;
&lt;p&gt;The notice states those facts and stops. The inference is ours, and it is the operator's lesson of the whole case. A capacity test on one node proves capacity. It cannot prove that two nodes are the same, and it never exercises the component that routes between them. The fault sat in exactly the parts the one-centre test left out: the load balancer, whose job is to divide traffic between the two centres, and the session component, whose configuration was assumed to match. The test was real and it passed. It answered a different question from the one the go decision needed answered.&lt;/p&gt;
&lt;p&gt;TSB's incident plan had made a similar assumption about failure itself. The bank &quot;was prepared to deal with &quot;bumps in the road&quot;&quot; but not &quot;a massive failure in the configuration leading to an erratic digital service.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-bill&quot;&gt;The bill&lt;/h3&gt;
&lt;p&gt;TSB's 2018 annual report puts the post-migration charges at £330.2 million. The two largest pieces were customer redress and associated costs, £107.3 million, and additional resource and advisory costs, £122.4 million. Against that the bank booked &quot;the estimated recovery of £153.0 million of post migration charges&quot; from SABIS under its two service contracts, &quot;up to the level of liability caps in each agreement.&quot; Net of the recovery, the charges came to £177.2 million.&lt;/p&gt;
&lt;p&gt;That recovery deserves a sentence of context. SABIS is &quot;a subsidiary of the Banco Sabadell Group,&quot; and so is TSB. The £153.0 million moved money from one Sabadell subsidiary to another. The accounts read here do not net the group's position, so they cannot say what the group as a whole lost.&lt;/p&gt;
&lt;p&gt;Then there were the costs of getting to the cutover. The same report shows £417.3 million of costs for preparing the migration, including &quot;£249.0 million payable to Sabadell under the Migration Services Agreement.&quot; Preparation and aftermath together come to £747.5 million in one year's accounts. That is 4.7 times the annual IT saving the takeover promised. The comparison is a sense of scale, not a payback calculation: the promise was for the third full year after the deal, and nothing read here says whether it was ever met. (Our list of &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/most-expensive-software-failures-in-history/&quot;&gt;the most expensive software failures in history&lt;/a&gt; does not include TSB; on these figures it would not look out of place there.)&lt;/p&gt;
&lt;p&gt;The regulators priced the failure more than four years later. The FCA fined TSB £29.75 million and the PRA £18.9 million, £48.65 million in all. That is 14.7% of the gross post-migration charges, or 27.5% of the net. Both fines ran the standard discount chain: the FCA's started at £50 million and came down by 15% for remediation and 30% for settling. The FCA found the breaches &quot;committed negligently&quot;, and the PRA found them &quot;neither deliberate nor reckless.&quot;&lt;/p&gt;
&lt;p&gt;The customers' redress is in the record too. To 7 April 2019, TSB paid £32,705,762 in redress against 225,492 complaints, about £145 per complaint. The complaints equalled about 4.3% of a customer base the notices put at &quot;approximately 5.2 million,&quot; roughly one for every twenty-three customers.&lt;/p&gt;
&lt;h3 id=&quot;the-one-person-in-the-record&quot;&gt;The one person in the record&lt;/h3&gt;
&lt;p&gt;One individual penalty appears in the published record. In April 2023 the PRA fined Carlos Abarca, TSB's chief information officer through the migration, £81,620. That was 15% of his relevant income of £777,356.86, cut by 30% for settling. He owned, by his senior-manager responsibilities, the risk that &quot;migration causes operational instability or a degradation in resilience and poor customer outcomes.&quot;&lt;/p&gt;
&lt;p&gt;The PRA's notice to him adds a detail. Before TSB, &quot;Mr Abarca was Chief Process and Information Officer at Banco de Sabadell, S.A. ('Sabadell'), and at the same time, the Chief Executive Officer (&quot;CEO&quot;) of Sabadell Information Systems, S.A. ('SABIS Spain').&quot; At TSB he was responsible for the bank's &quot;key outsourcing relationship with Sabadell Information Systems Limited ('SABIS UK').&quot; The man whose job was to judge the supplier's word had run its Spanish counterpart.&lt;/p&gt;
&lt;p&gt;The PRA drew the general rule from it: &quot;The fact that a firm and its service provider are within the same group does not do away with the need for a careful assessment.&quot; Sam Woods, then the PRA's chief executive, put it in one line when the fine was announced: &quot;the PRA has fined Mr Abarca because his management of a key outsourcing relationship fell below the standard we expect.&quot;&lt;/p&gt;
&lt;p&gt;His fine was 0.17% of the firms' fines and 0.025% of the post-migration charges. The failure the PRA made personal was the management of an outsourcing relationship, down to an attestation that a supplier's letter could be relied upon.&lt;/p&gt;
&lt;h3 id=&quot;what-a-readiness-claim-has-to-say&quot;&gt;What a readiness claim has to say&lt;/h3&gt;
&lt;p&gt;Strip the bank out of it and the case is about the grammar of &quot;ready.&quot; SABIS's letter was written in the future tense: confidence, expectation, tests still to be completed. The board received it as a conclusion: the memo that carried it asserted readiness and left the letter, and its caveats, behind. Regulators later separated the two tenses for them: statements of good intention on one side, statements of fact about completed activities on the other.&lt;/p&gt;
&lt;p&gt;The test made the same move in a different register. That the second data centre matched the first was a claim of fact. It rested on assurances, nobody had tested it, and the test plan treated it as settled.&lt;/p&gt;
&lt;p&gt;For a team running any cutover, the lessons are plain. A readiness claim counts as evidence only when it says what was completed, not what is expected. An equivalence assumption is a test that has not been run, and it belongs on the list of open items rather than in the reasoning that closes them. And the less reversible the step, the more the go decision has to rest on evidence of the first kind. TSB's migration could not be rolled back, so its go decision was the last point at which the difference mattered.&lt;/p&gt;
&lt;p&gt;The same grammar now turns up in software that reports on itself. An AI agent that says a task is &quot;done&quot; is writing SABIS's letter: a confident summary of activities, some of them still outstanding, written by the party that did the work. The fix is the one the regulators applied after the fact: ask for the statements of fact, attached, and read them before the step you cannot undo.&lt;/p&gt;
&lt;p&gt;TSB's board met again on 18 and 19 April. The outstanding tests had finished on the 17th, and the memo in its papers still carried the attestation, not the letter. On 20 April the weekend began.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Financial Conduct Authority, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.fca.org.uk/publication/final-notices/tsb-bank-plc-2022.pdf&quot;&gt;Final Notice to TSB Bank plc&lt;/a&gt;, 20 December 2022 (115 pages), paragraphs 2.1-2.8, 2.24-2.25, 4.48-4.52, 4.179-4.205, 4.257-4.258, 4.267 and section 6.&lt;/li&gt;
&lt;li&gt;Prudential Regulation Authority, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.bankofengland.co.uk/-/media/boe/files/prudential-regulation/regulatory-action/final-notice-from-pra-to-tsb-bank.pdf&quot;&gt;Final Notice to TSB Bank plc&lt;/a&gt;, 20 December 2022 (108 pages), including paragraph 1.9 (Sabadell's offer document) and the penalty annex.&lt;/li&gt;
&lt;li&gt;Prudential Regulation Authority, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.bankofengland.co.uk/-/media/boe/files/prudential-regulation/regulatory-action/final-notice-from-pra-to-former-tsb-bank-plc-cio.pdf&quot;&gt;Final Notice to Carlos Abarca&lt;/a&gt;, 13 April 2023 (35 pages).&lt;/li&gt;
&lt;li&gt;TSB Banking Group plc, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.tsb.co.uk/content/dam/tsb-public/documents/investors/financial-results-and-reports/2018/tsb-banking-group-ara-2018.pdf&quot;&gt;Annual Report and Accounts 2018&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Financial Conduct Authority, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.fca.org.uk/news/press-releases/tsb-fined-48m-operational-resilience-failings&quot;&gt;press release, 20 December 2022&lt;/a&gt;; Bank of England, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.bankofengland.co.uk/news/2022/december/tsb-fined-for-operational-resilience-failings&quot;&gt;press release, 20 December 2022&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.bankofengland.co.uk/news/2023/april/pra-fines-former-cio-of-tsb-bank-plc-for-breach-of-pra-senior-manager-conduct-rules&quot;&gt;press release, 13 April 2023&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;How this was computed. Every ratio above (fines against charges, the £747.5 million, the multiple of the promised saving, the redress per complaint and the CIO's fine against his income) is arithmetic on figures quoted in this piece from the notices and the 2018 accounts, so each can be recomputed from the text. Page and paragraph references are to the PDFs as published. Every quotation was checked against the saved text of its source.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Related on our blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/citibank-900-million-approval-screen-hid-the-amount/&quot;&gt;Citibank's $900 Million Mistake: Six Eyes on an Approval Screen That Never Showed the Amount&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/done-is-not-a-state/&quot;&gt;“Done” Is Not a State&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/tsb-went-live-on-a-letter-regulators-called-good-intention/&quot;&gt;vibeagentmaking.com&lt;/a&gt;.&lt;/em&gt; If you want a tamper-evident, dated record of what a system actually completed, and when: &lt;code&gt;pip install chain-of-consciousness&lt;/code&gt; or &lt;code&gt;npm install chain-of-consciousness&lt;/code&gt;, or try &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/hosted-coc/&quot;&gt;Hosted Chain of Consciousness&lt;/a&gt;.&lt;/p&gt;
</description>
<category>DevOps &amp; Infra</category>
<guid isPermaLink="true">https://coderlegion.com/31971/tsb-went-live-on-a-letter-regulators-called-statements-of-good-intention-330m-aftermath</guid>
<pubDate>Wed, 21 Oct 2026 12:29:53 +0000</pubDate>
</item>
<item>
<title>Software Maintenance Cost: What to Budget Each Year</title>
<link>https://coderlegion.com/31896/software-maintenance-cost-what-to-budget-each-year</link>
<description>&lt;p&gt;Most people budget carefully for the build and then forget the bill that never stops: &lt;strong&gt;software maintenance cost&lt;/strong&gt;. A web app, SaaS product or internal system is not a one-off purchase. It needs hosting, security patches, dependency upgrades, backups, monitoring and fixes for the problems real users find. If nobody pays for that work, the system quietly decays until it becomes the legacy application you were trying to avoid.&lt;/p&gt;
&lt;p&gt;I look after production systems as part of my &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/services/cloud-devops-deployment&quot;&gt;cloud, DevOps and deployment work&lt;/a&gt;, so this guide covers what maintenance actually includes, what drives the cost, and how to budget for it without guessing.&lt;/p&gt;
&lt;h3 id=&quot;what-software-maintenance-actually-covers&quot;&gt;What software maintenance actually covers&lt;/h3&gt;
&lt;p&gt;&quot;Maintenance&quot; is a bucket for four different kinds of work. Knowing which is which makes the budget conversation much easier.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Corrective:&lt;/strong&gt; fixing bugs found in production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preventive:&lt;/strong&gt; security patches, framework and library upgrades, and clean-up that stops problems before they appear.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adaptive:&lt;/strong&gt; changing the system because the world changed, such as a new PHP version, a payment provider update or a browser change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Perfective:&lt;/strong&gt; small improvements and requests that make the product better but are not a new project.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;New features are usually a separate budget. The trap is letting small feature requests quietly consume the maintenance hours that keep the system safe.&lt;/p&gt;
&lt;h3 id=&quot;the-recurring-costs-people-forget&quot;&gt;The recurring costs people forget&lt;/h3&gt;
&lt;p&gt;Beyond developer time, a live system has running costs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hosting and infrastructure:&lt;/strong&gt; servers, databases, storage and bandwidth. My comparison of &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-hosting-options-cost&quot;&gt;Laravel hosting options and cost&lt;/a&gt; shows how widely this varies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third-party services:&lt;/strong&gt; email, SMS, payments, search, error tracking and AI APIs, often billed by usage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Domain, SSL and licences&lt;/strong&gt; for paid plugins or tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups and disaster recovery,&lt;/strong&gt; including actually testing a restore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitoring and alerting,&lt;/strong&gt; so you hear about problems before customers do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security work:&lt;/strong&gt; patching, access reviews and responding to vulnerability notices.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These costs are easy to estimate once you have a list. The surprise usually comes from usage-based services that grow with your traffic.&lt;/p&gt;
&lt;h3 id=&quot;what-drives-maintenance-cost-up-or-down&quot;&gt;What drives maintenance cost up or down&lt;/h3&gt;
&lt;p&gt;I will not quote a percentage that pretends to fit every system. These factors decide where yours lands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Age and technology.&lt;/strong&gt; Systems on unsupported frameworks cost more to keep safe. This is the main link to &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/rewrite-vs-refactor-legacy-application&quot;&gt;rewriting or refactoring a legacy application&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test coverage.&lt;/strong&gt; Tested code is cheaper to upgrade because you can see what broke.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number of integrations.&lt;/strong&gt; Every third-party API is something that can change or fail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traffic and data volume.&lt;/strong&gt; More load means more tuning and more infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality of documentation.&lt;/strong&gt; Missing knowledge makes every task slower.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment process.&lt;/strong&gt; Manual releases cost time and cause errors. A clean pipeline, as in my guide to &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-zero-downtime-deployments&quot;&gt;zero-downtime deployments&lt;/a&gt;, lowers the cost of every future change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Required uptime.&lt;/strong&gt; A system that must always be available needs monitoring and on-call, which costs more.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;three-ways-to-pay-for-maintenance&quot;&gt;Three ways to pay for maintenance&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Retainer.&lt;/strong&gt; A fixed monthly amount that covers a set number of hours or a defined scope. It suits systems with steady needs and gives you predictable budgeting and fast response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Time and materials.&lt;/strong&gt; You pay for hours as they are used. It is flexible for quiet systems, but costs are unpredictable and urgent issues compete with other clients' work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Fixed-scope packages.&lt;/strong&gt; Specific tasks, such as a framework upgrade or security audit, for a fixed price. These are good for planned one-off work on top of a baseline.&lt;/p&gt;
&lt;p&gt;Many businesses use a small retainer for monitoring and urgent fixes, plus fixed-scope projects for bigger upgrades.&lt;/p&gt;
&lt;h3 id=&quot;how-to-estimate-your-own-maintenance-budget&quot;&gt;How to estimate your own maintenance budget&lt;/h3&gt;
&lt;p&gt;Use this simple worksheet:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;List every running cost:&lt;/strong&gt; hosting, services, domains, licences. Check last month's invoices for each.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estimate routine hours:&lt;/strong&gt; security patches, dependency updates, backup checks and monitoring review, per month.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add a bug-fix allowance&lt;/strong&gt; based on how often production issues have appeared so far.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add planned upgrade work&lt;/strong&gt; for the year, such as a major framework or PHP version.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add a contingency&lt;/strong&gt; for the unexpected, and treat it as real money.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate new features&lt;/strong&gt; into their own budget so they do not eat maintenance time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the system is brand new and has no history, expect the early months to need more attention while real usage reveals problems. A short audit gives you a better starting point than a guess.&lt;/p&gt;
&lt;h3 id=&quot;why-support-dates-decide-your-upgrade-calendar&quot;&gt;Why support dates decide your upgrade calendar&lt;/h3&gt;
&lt;p&gt;Two clocks set most of your planned maintenance: the language and the framework.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Language versions&lt;/strong&gt; each get a limited support window, after which they stop receiving security fixes. PHP publishes these dates on its &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.php.net/supported-versions.php&quot;&gt;supported versions page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework versions&lt;/strong&gt; follow their own policy. Laravel documents which releases still receive bug and security fixes in its &lt;a rel=&quot;nofollow&quot; href=&quot;https://laravel.com/docs/releases&quot;&gt;release notes and support policy&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Put both sets of dates in a calendar and plan an upgrade before a version runs out of support, not after. Doing one planned upgrade a year is far cheaper and less risky than jumping across several versions at once, which is the situation I describe in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-upgrade-checklist&quot;&gt;Laravel upgrade checklist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A good maintenance plan therefore includes a yearly &quot;version review&quot;: check what you run, check the support dates, and decide what gets upgraded next. It takes a few hours and avoids an emergency later.&lt;/p&gt;
&lt;h3 id=&quot;what-happens-when-you-skip-maintenance&quot;&gt;What happens when you skip maintenance&lt;/h3&gt;
&lt;p&gt;Skipping maintenance does not save money. It moves the cost to later and makes it larger:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Known security holes stay open&lt;/li&gt;
&lt;li&gt;Upgrades pile up until they become a risky big-bang project&lt;/li&gt;
&lt;li&gt;Small bugs turn into outages&lt;/li&gt;
&lt;li&gt;Only one person understands the system&lt;/li&gt;
&lt;li&gt;Hosting and dependencies freeze because nobody dares touch them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the Henceforward WordPress project, the work combined website development with migrating infrastructure to Scaleway with Laravel Forge, strengthening security and optimising the production environment. That kind of foundation is what makes ongoing care cheaper. You can read the &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/work/henceforward-wordpress-migration-security-optimization&quot;&gt;Henceforward case study&lt;/a&gt; for the details.&lt;/p&gt;
&lt;h3 id=&quot;a-simple-way-to-report-maintenance-value&quot;&gt;A simple way to report maintenance value&lt;/h3&gt;
&lt;p&gt;Maintenance feels invisible when nothing breaks, which makes it easy to cut. Ask for a short monthly note covering four things: what was patched or upgraded, which incidents occurred and how fast they were resolved, how backups and monitoring performed, and what is planned next. A one-page summary turns &quot;nothing happened&quot; into evidence that the system is being looked after, and it gives you the data to adjust the plan when needs change.&lt;/p&gt;
&lt;h3 id=&quot;questions-to-ask-before-you-agree-a-maintenance-plan&quot;&gt;Questions to ask before you agree a maintenance plan&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;What exactly is included, and what counts as a new feature?&lt;/li&gt;
&lt;li&gt;What is the response time for an urgent production issue?&lt;/li&gt;
&lt;li&gt;Who monitors the system, and how will I be alerted?&lt;/li&gt;
&lt;li&gt;How are backups tested?&lt;/li&gt;
&lt;li&gt;What happens to the plan if the system grows?&lt;/li&gt;
&lt;li&gt;Can I see a report of what was done each month?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A clear answer to these is a better signal than the lowest price. My thoughts on &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/why-experience-beats-low-cost-outsourcing&quot;&gt;why experience beats low-cost outsourcing&lt;/a&gt; apply here too.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Maintenance is four kinds of work: fixing, preventing, adapting and improving.&lt;/li&gt;
&lt;li&gt;Running costs go beyond developer time: hosting, services, backups, monitoring and security.&lt;/li&gt;
&lt;li&gt;Age, tests, integrations and deployment process drive the price more than size alone.&lt;/li&gt;
&lt;li&gt;Choose a retainer for steady needs, fixed-scope packages for planned upgrades.&lt;/li&gt;
&lt;li&gt;Skipping maintenance makes problems larger and more expensive later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want a clear maintenance budget for a system you already run, &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/book&quot;&gt;book a call&lt;/a&gt; and I will review it and tell you what it really needs.&lt;/p&gt;
&lt;p&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/software-maintenance-cost-what-to-budget&quot;&gt;aqibjavaid.site&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31896/software-maintenance-cost-what-to-budget-each-year</guid>
<pubDate>Wed, 21 Oct 2026 00:02:58 +0000</pubDate>
</item>
<item>
<title>One Bash Script to Audit a Polyglot Codebase (Python, TS, Java, Go, C, HTML)</title>
<link>https://coderlegion.com/31711/one-bash-script-to-audit-a-polyglot-codebase-python-ts-java-go-c-html</link>
<description>&lt;p&gt;If your project mixes languages, one terminal command can run each language's own tests and security checks and fail the build when anything is red.&lt;/p&gt;
&lt;p&gt;The script (1) counts files per language, (2) runs build and type checks, (3) runs tests and SAST, (4) scans dependencies and secrets, (5) runs a quick concurrency check, and (6) writes a checksummed report.&lt;/p&gt;
&lt;p&gt;I tested it on a small demo project: python compile passed, missing scanners showed as SKIP, an invalid HTML file produced a real FAIL, and the run exited with code 1.&lt;/p&gt;
&lt;p&gt;Full guide with per-language settings, a troubleshooting table and a CI example: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.seosiri.com/2026/10/polyglot-code-audit-bash-testing-pipeline.html#conclusion&quot;&gt;https://www.seosiri.com/2026/10/polyglot-code-audit-bash-testing-pipeline.html#conclusion&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Which scanner would you add first?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31711/one-bash-script-to-audit-a-polyglot-codebase-python-ts-java-go-c-html</guid>
<pubDate>Mon, 19 Oct 2026 11:43:05 +0000</pubDate>
</item>
<item>
<title>Database Migrations: Can Your Previous Release Still Run?</title>
<link>https://coderlegion.com/31693/database-migrations-can-your-previous-release-still-run</link>
<description>&lt;p&gt;A deployment can roll back cleanly while the database stays incompatible with the old application. Before shipping a schema change, check whether the previous release can still read and write the data that the new release creates.&lt;/p&gt;
&lt;p&gt;Consider a fictional account service. Version 1 reads a column called &lt;code&gt;display_name&lt;/code&gt;. Version 2 expects &lt;code&gt;public_name&lt;/code&gt;. Renaming the column and deploying the new code together looks tidy, until an old worker, a rolling-deployment instance, or an emergency rollback still asks for &lt;code&gt;display_name&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The migration succeeded. The service can still fail.&lt;/p&gt;
&lt;h3 id=&quot;separate-compatibility-from-cleanup&quot;&gt;Separate compatibility from cleanup&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/bliki/ParallelChange.html&quot;&gt;Danilo Sato’s explanation of parallel change&lt;/a&gt; describes three stages: expand, migrate, and contract. The useful idea is to introduce the new interface while the old one remains available, move consumers, then remove the old interface.&lt;/p&gt;
&lt;p&gt;For a database rollout, turn that idea into explicit release gates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expand:&lt;/strong&gt; introduce the new storage shape without removing what the currently deployed application needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate:&lt;/strong&gt; move reads and writes deliberately, checking compatibility during the transition.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contract:&lt;/strong&gt; remove the old shape only after its consumers and the intended rollback window are accounted for.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These stages are a planning model, not a guarantee of zero downtime. The exact migration still depends on the database, data volume, application behavior, and operational limits.&lt;/p&gt;
&lt;h3 id=&quot;work-through-the-awkward-middle&quot;&gt;Work through the awkward middle&lt;/h3&gt;
&lt;p&gt;In the account-service example, adding &lt;code&gt;public_name&lt;/code&gt; is only the beginning. Before copying values, decide which field is authoritative at each stage.&lt;/p&gt;
&lt;p&gt;A possible transition is to deploy compatibility code that continues serving old readers while maintaining both values. If both columns live in the same row, update them in the same database transaction where practical. That still leaves an important question: can any old application instance or background job write only the original column?&lt;/p&gt;
&lt;p&gt;Do not switch reads to the new column while those writers can silently make it stale. Either finish upgrading writers first or design a deliberate synchronization mechanism. A fallback read helps with missing values; it does not detect an outdated non-empty value.&lt;/p&gt;
&lt;p&gt;Backfills need the same care. A naive copy can overwrite a newer value if it races with a live update. Define an update condition, concurrency strategy, and restart behavior before running the job. Record progress so an interrupted backfill can resume without blindly replaying the entire table.&lt;/p&gt;
&lt;h3 id=&quot;test-the-rollback-with-new-data&quot;&gt;Test the rollback with new data&lt;/h3&gt;
&lt;p&gt;A useful rehearsal includes more than deploying version 2 successfully:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with representative data, including empty and unusual values.&lt;/li&gt;
&lt;li&gt;Apply the expansion and deploy the compatibility release.&lt;/li&gt;
&lt;li&gt;Run the backfill alongside representative writes.&lt;/li&gt;
&lt;li&gt;Create and edit records using the new release.&lt;/li&gt;
&lt;li&gt;Restore the application version you actually intend to roll back to.&lt;/li&gt;
&lt;li&gt;Verify that it can read and update those new records correctly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Include background workers, scheduled jobs, exports, and direct database consumers in that exercise. A healthy web endpoint does not prove that every writer is compatible.&lt;/p&gt;
&lt;p&gt;Use synthetic or appropriately protected test data. This checklist proposes evidence to collect; it does not claim that a production migration has been tested.&lt;/p&gt;
&lt;h3 id=&quot;a-compatible-change-can-still-block-traffic&quot;&gt;A compatible change can still block traffic&lt;/h3&gt;
&lt;p&gt;In PostgreSQL, &lt;code&gt;ALTER TABLE&lt;/code&gt; generally acquires an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock unless the documentation specifies otherwise. Compatibility at the application level does not eliminate lock waits.&lt;/p&gt;
&lt;p&gt;PostgreSQL also supports separating certain constraint additions from validation with &lt;code&gt;NOT VALID&lt;/code&gt; and &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt;. The permitted constraint types and locking details matter. Check the documentation for your deployed version rather than applying the technique to every schema operation. See the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/sql-altertable.html&quot;&gt;PostgreSQL ALTER TABLE reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Before execution, define acceptable lock wait, query latency, replication lag, and backfill load. Decide who can stop the rollout and what the safe next step is if a threshold is crossed.&lt;/p&gt;
&lt;h3 id=&quot;make-cleanup-its-own-decision&quot;&gt;Make cleanup its own decision&lt;/h3&gt;
&lt;p&gt;Keep a short release note that answers four questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which application versions can run against this schema?&lt;/li&gt;
&lt;li&gt;Which data changes can the previous version understand?&lt;/li&gt;
&lt;li&gt;What evidence allows the old column or interface to be removed?&lt;/li&gt;
&lt;li&gt;After removal, does recovery mean application rollback, a forward fix, or data restoration?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Give the cleanup an owner. Leaving parallel paths indefinitely creates its own maintenance cost, but removing them too early destroys the compatibility you built.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The practical takeaway:&lt;/strong&gt; rehearse rollback against data written by the new release before treating it as a recovery plan.&lt;/p&gt;
&lt;p&gt;What is the most easily missed database consumer in your deployment process?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Waqas Ahmad | Software Engineer | Content Creator&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31693/database-migrations-can-your-previous-release-still-run</guid>
<pubDate>Mon, 19 Oct 2026 07:45:55 +0000</pubDate>
</item>
<item>
<title>Building the MyZubster Metaverse: From Public Characters to Secure World Actions</title>
<link>https://coderlegion.com/31679/building-the-myzubster-metaverse-from-public-characters-to-secure-world-actions</link>
<description>&lt;p&gt;Building the MyZubster Metaverse: From Public Characters to Secure World Actions&lt;br&gt;
MyZubster is building an open-source metaverse that connects public characters, verified knowledge, contributor identities, realtime interactions, and GitHub-based development.&lt;br&gt;
The project is not limited to a graphical virtual world. Its technical foundation is based on authenticated actions, explicit user consent, verifiable evidence, secure automation, and independently inspectable deployments.&lt;br&gt;
This is a technical update on what we have implemented, what we recently stabilized, and what remains before the platform can be considered production-ready.&lt;br&gt;
The Architecture&lt;br&gt;
The current architecture follows this general flow:&lt;br&gt;
MyZubster account&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authenticated session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Time-limited session lease&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zorgax or metaverse character&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authorized world action&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Realtime event distribution&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persistent and verifiable evidence&lt;br&gt;
Each state-changing request should answer four questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which account initiated the request?&lt;/li&gt;
&lt;li&gt;Is its session still valid?&lt;/li&gt;
&lt;li&gt;Which character is associated with that session?&lt;/li&gt;
&lt;li&gt;Is the requested action permitted?&lt;br&gt;
This is important because a client-provided character ID cannot be considered proof of ownership.&lt;br&gt;
The Public Metaverse&lt;br&gt;
The public interface is available at:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Our latest production verification confirmed that the page loads correctly and exposes:&lt;ul&gt;
&lt;li&gt;25 public characters&lt;/li&gt;
&lt;li&gt;7 verified explorers&lt;/li&gt;
&lt;li&gt;A deployed Vercel production build&lt;/li&gt;
&lt;li&gt;No blocking browser-console errors&lt;br&gt;
This public page represents the visible part of a larger system. Most of our recent work has focused on the security and infrastructure beneath it.&lt;br&gt;
Session-Bound World Actions&lt;br&gt;
World actions are being migrated to a session-bound authorization model.&lt;br&gt;
The server must derive the acting identity from an authenticated session instead of trusting identifiers supplied by the client.&lt;br&gt;
const session = await resolveAuthenticatedSession(request);&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;if (!session) {&lt;br&gt;
  throw new UnauthorizedError();&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;const lease = await resolveActiveLease(session.id);&lt;/p&gt;
&lt;p&gt;if (!lease || lease.expiresAt &amp;lt;= new Date()) {&lt;br&gt;
  throw new SessionExpiredError();&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;const character = await resolveCharacterForAccount(session.accountId);&lt;/p&gt;
&lt;p&gt;await authorizeWorldAction({&lt;br&gt;
  session,&lt;br&gt;
  lease,&lt;br&gt;
  character,&lt;br&gt;
  action: request.body.action,&lt;br&gt;
});&lt;br&gt;
This approach reduces the risk of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Character impersonation&lt;/li&gt;
&lt;li&gt;Cross-account actions&lt;/li&gt;
&lt;li&gt;Replayed requests&lt;/li&gt;
&lt;li&gt;Actions performed after logout&lt;/li&gt;
&lt;li&gt;Use of expired sessions&lt;/li&gt;
&lt;li&gt;Client-side manipulation of ownership fields&lt;br&gt;
Authentication identifies the account. The session lease determines whether its authorization is still active. The policy layer determines which world actions are allowed.&lt;br&gt;
Session Lease Lifecycle&lt;br&gt;
An authenticated session should not provide unlimited access.&lt;br&gt;
We introduced a lease lifecycle that can represent the following states:&lt;br&gt;
CREATED&lt;br&gt;
 ↓&lt;br&gt;
ACTIVE&lt;br&gt;
 ↓&lt;br&gt;
RENEWED&lt;br&gt;
 ↓&lt;br&gt;
EXPIRED&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ACTIVE&lt;br&gt;
   ↓&lt;br&gt;
REVOKED&lt;br&gt;
A lease can be invalidated when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The expiration time is reached&lt;/li&gt;
&lt;li&gt;The user signs out&lt;/li&gt;
&lt;li&gt;The account is disabled&lt;/li&gt;
&lt;li&gt;Security credentials are rotated&lt;/li&gt;
&lt;li&gt;An administrator revokes the session&lt;/li&gt;
&lt;li&gt;A security policy invalidates older leases&lt;br&gt;
Metaverse endpoints must verify the lease immediately before performing a protected action.&lt;br&gt;
Zorgax and Contributor Consent&lt;br&gt;
Zorgax connects several areas of MyZubster:&lt;/li&gt;
&lt;li&gt;GitHub contributors&lt;/li&gt;
&lt;li&gt;Public professional profiles&lt;/li&gt;
&lt;li&gt;Knowledge Cards&lt;/li&gt;
&lt;li&gt;Verified Knowledge&lt;/li&gt;
&lt;li&gt;Metaverse characters&lt;/li&gt;
&lt;li&gt;Development Requests&lt;br&gt;
A public GitHub identity can generate an optional character proposal, but registration must require explicit consent.&lt;br&gt;
GitHub contribution&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Character draft&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Contributor reviews proposal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Explicit acceptance&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Public character registration&lt;br&gt;
A character proposal does not imply:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Legal identity verification&lt;/li&gt;
&lt;li&gt;Employment&lt;/li&gt;
&lt;li&gt;Partnership&lt;/li&gt;
&lt;li&gt;Ownership&lt;/li&gt;
&lt;li&gt;Payment eligibility&lt;/li&gt;
&lt;li&gt;Institutional endorsement&lt;br&gt;
Keeping these distinctions explicit is essential for an open contributor ecosystem.&lt;br&gt;
Realtime Communication&lt;br&gt;
The realtime layer still needs to be completed, but its event structure must support reliable synchronization.&lt;br&gt;
A typical event could look like this:&lt;br&gt;
{&lt;br&gt;
&quot;eventId&quot;: &quot;evt&lt;em&gt;01...&quot;,&lt;br&gt;
&quot;worldId&quot;: &quot;myzubster-main&quot;,&lt;br&gt;
&quot;characterId&quot;: &quot;character&lt;/em&gt;...&quot;,&lt;br&gt;
&quot;sessionId&quot;: &quot;session_...&quot;,&lt;br&gt;
&quot;sequence&quot;: 1842,&lt;br&gt;
&quot;type&quot;: &quot;character.position.updated&quot;,&lt;br&gt;
&quot;occurredAt&quot;: &quot;2026-10-19T04:50:00.000Z&quot;,&lt;br&gt;
&quot;payload&quot;: {&lt;br&gt;
  &quot;x&quot;: 12.4,&lt;br&gt;
  &quot;y&quot;: 0,&lt;br&gt;
  &quot;z&quot;: -8.1&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
The event identifier and sequence number allow clients to detect:&lt;/li&gt;
&lt;li&gt;Duplicate messages&lt;/li&gt;
&lt;li&gt;Out-of-order updates&lt;/li&gt;
&lt;li&gt;Missing events&lt;/li&gt;
&lt;li&gt;Stale world state&lt;br&gt;
After reconnecting, a client should send its last accepted sequence number and recover any missing state.&lt;br&gt;
Distributed Rate Limiting&lt;br&gt;
In-memory rate limiting works only for a single application instance. It is insufficient when requests are distributed across several workers or serverless deployments.&lt;br&gt;
Production enforcement therefore requires shared state:&lt;br&gt;
Client&lt;br&gt;
 ↓&lt;br&gt;
Application instance A ─┐&lt;br&gt;
Application instance B ─┼→ Shared atomic rate limiter&lt;br&gt;
Application instance C ─┘&lt;br&gt;
Different policies are required for:&lt;/li&gt;
&lt;li&gt;Login attempts&lt;/li&gt;
&lt;li&gt;Session creation&lt;/li&gt;
&lt;li&gt;Lease renewal&lt;/li&gt;
&lt;li&gt;Character movement&lt;/li&gt;
&lt;li&gt;World interactions&lt;/li&gt;
&lt;li&gt;Realtime messages&lt;/li&gt;
&lt;li&gt;Search endpoints&lt;/li&gt;
&lt;li&gt;Profile updates&lt;br&gt;
The system must also define what happens when the rate-limit service becomes unavailable: fail open, fail closed, or apply a limited fallback policy.&lt;br&gt;
CI Runtime Modernization&lt;br&gt;
We recently completed an important GitHub Actions upgrade.&lt;br&gt;
GitHub warned that older JavaScript actions were using the deprecated Node.js 20 runtime. The affected references were updated from:&lt;/li&gt;
&lt;li&gt;uses: actions/checkout@v4&lt;/li&gt;
&lt;li&gt;uses: actions/setup-node@v4&lt;br&gt;
to:&lt;/li&gt;
&lt;li&gt;uses: actions/checkout@v5&lt;/li&gt;
&lt;li&gt;uses: actions/setup-node@v5&lt;br&gt;
The migration covered:&lt;/li&gt;
&lt;li&gt;12 workflow files&lt;/li&gt;
&lt;li&gt;26 action references&lt;/li&gt;
&lt;li&gt;CI and lint&lt;/li&gt;
&lt;li&gt;Security auditing&lt;/li&gt;
&lt;li&gt;SBOM generation&lt;/li&gt;
&lt;li&gt;Vercel deployment&lt;/li&gt;
&lt;li&gt;Evidence validation&lt;/li&gt;
&lt;li&gt;Seller policy regression testing&lt;/li&gt;
&lt;li&gt;MYZ ledger verification&lt;/li&gt;
&lt;li&gt;Pilot API testing&lt;/li&gt;
&lt;li&gt;Decentraland SDK7 proof workflows&lt;br&gt;
We validated all 16 workflow YAML files before publishing the change and confirmed that no affected v4 references remained.&lt;br&gt;
The update was integrated through Pull Request #38.&lt;br&gt;
The final commit on main is:&lt;br&gt;
69ef67716df50482e8fbf9960ccab7dfc37d3000&lt;br&gt;
Post-Merge Results&lt;br&gt;
The merge triggered seven workflows on the production branch.&lt;br&gt;
All completed successfully:&lt;/li&gt;
&lt;li&gt;CI — Test and Lint&lt;/li&gt;
&lt;li&gt;Security Audit&lt;/li&gt;
&lt;li&gt;SBOM and Vulnerability Scan&lt;/li&gt;
&lt;li&gt;Continuous Evidence Gate&lt;/li&gt;
&lt;li&gt;MYZ Canonical Ledger Contract&lt;/li&gt;
&lt;li&gt;MYZ-164 Seller Free Policy&lt;/li&gt;
&lt;li&gt;Deploy to Vercel&lt;br&gt;
The deployment workflow also completed successfully, confirming that the runtime migration did not break the production delivery process.&lt;br&gt;
What Is Currently Working&lt;br&gt;
The current baseline includes:&lt;/li&gt;
&lt;li&gt;Public metaverse interface&lt;/li&gt;
&lt;li&gt;Public characters&lt;/li&gt;
&lt;li&gt;Verified explorers&lt;/li&gt;
&lt;li&gt;GitHub-connected contributor identities&lt;/li&gt;
&lt;li&gt;Optional Zorgax character onboarding&lt;/li&gt;
&lt;li&gt;Session-bound authorization development&lt;/li&gt;
&lt;li&gt;Session lease lifecycle enforcement&lt;/li&gt;
&lt;li&gt;Knowledge and profile integration&lt;/li&gt;
&lt;li&gt;Automated security auditing&lt;/li&gt;
&lt;li&gt;SBOM generation&lt;/li&gt;
&lt;li&gt;Continuous evidence validation&lt;/li&gt;
&lt;li&gt;Node.js 24-compatible GitHub Actions&lt;/li&gt;
&lt;li&gt;Automated Vercel deployments&lt;br&gt;
What Still Needs to Be Built&lt;br&gt;
Complete Authentication Enforcement&lt;br&gt;
Every state-changing endpoint must use the same canonical authentication and authorization process.&lt;br&gt;
Required scenarios include:&lt;/li&gt;
&lt;li&gt;Expired sessions&lt;/li&gt;
&lt;li&gt;Revoked leases&lt;/li&gt;
&lt;li&gt;Logout&lt;/li&gt;
&lt;li&gt;Credential rotation&lt;/li&gt;
&lt;li&gt;Concurrent sessions&lt;/li&gt;
&lt;li&gt;Disabled accounts&lt;/li&gt;
&lt;li&gt;Replay attempts&lt;/li&gt;
&lt;li&gt;Cross-account character access&lt;br&gt;
Finish Distributed Rate Limiting&lt;br&gt;
We still need to validate:&lt;/li&gt;
&lt;li&gt;Atomic counters&lt;/li&gt;
&lt;li&gt;Multiple application instances&lt;/li&gt;
&lt;li&gt;Concurrent requests&lt;/li&gt;
&lt;li&gt;Counter expiration&lt;/li&gt;
&lt;li&gt;Boundary conditions&lt;/li&gt;
&lt;li&gt;Backend failure behavior&lt;/li&gt;
&lt;li&gt;Recovery after temporary outages&lt;br&gt;
Complete Realtime Synchronization&lt;br&gt;
The realtime layer must support:&lt;/li&gt;
&lt;li&gt;Presence&lt;/li&gt;
&lt;li&gt;Character movement&lt;/li&gt;
&lt;li&gt;Interactions&lt;/li&gt;
&lt;li&gt;Reconnection&lt;/li&gt;
&lt;li&gt;Missed-event recovery&lt;/li&gt;
&lt;li&gt;Duplicate suppression&lt;/li&gt;
&lt;li&gt;Event ordering&lt;/li&gt;
&lt;li&gt;Backpressure management&lt;br&gt;
Add Privacy-Safe Observability&lt;br&gt;
Operational monitoring should measure:&lt;/li&gt;
&lt;li&gt;Active sessions&lt;/li&gt;
&lt;li&gt;Rejected world actions&lt;/li&gt;
&lt;li&gt;Lease-renewal failures&lt;/li&gt;
&lt;li&gt;Realtime disconnections&lt;/li&gt;
&lt;li&gt;Rate-limit activations&lt;/li&gt;
&lt;li&gt;API latency&lt;/li&gt;
&lt;li&gt;Backend error rates&lt;br&gt;
The system should minimize personal data and avoid storing unnecessary raw identifiers.&lt;br&gt;
Reliability and Load Testing&lt;br&gt;
Before expanding public access, the platform needs tests for:&lt;/li&gt;
&lt;li&gt;Concurrent users&lt;/li&gt;
&lt;li&gt;Movement bursts&lt;/li&gt;
&lt;li&gt;Realtime fan-out&lt;/li&gt;
&lt;li&gt;Session-store latency&lt;/li&gt;
&lt;li&gt;Infrastructure restarts&lt;/li&gt;
&lt;li&gt;Partial failures&lt;/li&gt;
&lt;li&gt;Deployment rollback&lt;/li&gt;
&lt;li&gt;Rate-limit contention&lt;br&gt;
End-to-End Acceptance&lt;br&gt;
The complete flow must be tested as one system:&lt;br&gt;
Sign in&lt;br&gt;
→ select a character&lt;br&gt;
→ create a session lease&lt;br&gt;
→ enter the metaverse&lt;br&gt;
→ perform an authorized action&lt;br&gt;
→ receive a realtime event&lt;br&gt;
→ disconnect&lt;br&gt;
→ reconnect and recover state&lt;br&gt;
→ sign out&lt;br&gt;
→ verify that further actions are rejected&lt;br&gt;
WebXR&lt;br&gt;
WebXR remains part of the roadmap, but it should follow security, realtime communication, and reliability work.&lt;br&gt;
Our planned order is:&lt;ol&gt;
&lt;li&gt;Authentication and session enforcement&lt;/li&gt;
&lt;li&gt;Distributed rate limiting&lt;/li&gt;
&lt;li&gt;Realtime synchronization&lt;/li&gt;
&lt;li&gt;Privacy-safe analytics&lt;/li&gt;
&lt;li&gt;Reliability and load testing&lt;/li&gt;
&lt;li&gt;End-to-end acceptance&lt;/li&gt;
&lt;li&gt;WebXR experience&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse now has a concrete technical foundation: public characters, contributor identity integration, authenticated session work, automated security checks, reproducible deployments, and a modernized CI runtime.&lt;br&gt;
It is not finished, and we do not want to describe experimental components as complete systems.&lt;br&gt;
Every future milestone should be accompanied by:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Public source code&lt;/li&gt;
&lt;li&gt;Automated tests&lt;/li&gt;
&lt;li&gt;Deployment evidence&lt;/li&gt;
&lt;li&gt;Clear limitations&lt;/li&gt;
&lt;li&gt;Reproducible verification steps&lt;br&gt;
Our goal is to build the metaverse transparently, incrementally, and with evidence that contributors can inspect independently.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31679/building-the-myzubster-metaverse-from-public-characters-to-secure-world-actions</guid>
<pubDate>Mon, 19 Oct 2026 04:58:09 +0000</pubDate>
</item>
<item>
<title>How We Secured the MyZubster Metaverse: Session Ownership, Verified Explorers and the Road to Realti</title>
<link>https://coderlegion.com/31491/how-secured-the-myzubster-metaverse-session-ownership-verified-explorers-and-road-realti</link>
<description>&lt;p&gt;A technical update on the architecture, security controls, testing and remaining roadmap behind the open-source MyZubster Metaverse.&lt;br&gt;
The MyZubster Metaverse is moving from an experimental interface toward a secure, testable multiplayer environment.&lt;br&gt;
Our goal is not simply to display avatars in a browser. We want to create an open-source world connecting public profiles, verified contributors, Zorgax characters, community knowledge and software-development workflows.&lt;br&gt;
The public Metaverse is available at:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
This article describes what we have implemented, the security problem we recently solved and what remains before the platform can support realtime interaction at scale.&lt;br&gt;
The current experience&lt;br&gt;
A user can enter the MyZubster world as a guest without providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a cryptocurrency wallet;&lt;/li&gt;
&lt;li&gt;identity documents;&lt;/li&gt;
&lt;li&gt;a GitHub account;&lt;/li&gt;
&lt;li&gt;an installed application.&lt;br&gt;
The current interface supports guest onboarding through a public display name and also presents verified explorers associated with authorized public accounts.&lt;br&gt;
This creates two identity levels:&lt;ol&gt;
&lt;li&gt;Guest explorers use temporary world sessions.&lt;/li&gt;
&lt;li&gt;Verified explorers have characters linked to a MyZubster account or confirmed GitHub identity.&lt;br&gt;
“Verified” does not mean KYC or legal identity verification. It only confirms control of the connected public account.&lt;br&gt;
Building the presence layer&lt;br&gt;
The backend maintains a presence record for every active participant.&lt;br&gt;
A simplified record may contain:&lt;br&gt;
{&lt;br&gt;
sessionId,&lt;br&gt;
displayName,&lt;br&gt;
characterId,&lt;br&gt;
accountId,&lt;br&gt;
githubLogin,&lt;br&gt;
position,&lt;br&gt;
status,&lt;br&gt;
lastSeenAt&lt;br&gt;
}&lt;br&gt;
This model supports the main world operations:&lt;br&gt;
Join&lt;br&gt;
Synchronize&lt;br&gt;
Move&lt;br&gt;
Chat&lt;br&gt;
Emote&lt;br&gt;
Leave&lt;br&gt;
Retrieve events&lt;br&gt;
Public players must be discoverable by other clients, so each presence has a public session identifier.&lt;br&gt;
However, a public identifier must never be treated as proof that a request belongs to that player.&lt;br&gt;
That distinction became the focus of our latest security milestone.&lt;br&gt;
The original session-ownership problem&lt;br&gt;
A multiplayer client needs to identify which session it wants to update.&lt;br&gt;
For example:&lt;br&gt;
{&lt;br&gt;
&quot;sessionId&quot;: &quot;public-session-id&quot;,&lt;br&gt;
&quot;x&quot;: 120,&lt;br&gt;
&quot;y&quot;: 80&lt;br&gt;
}&lt;br&gt;
If the backend trusts only sessionId, a malicious user may be able to submit another player’s identifier and perform actions on their behalf.&lt;br&gt;
Depending on the exposed routes, this could allow an attacker to:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;move another character;&lt;/li&gt;
&lt;li&gt;send chat messages under another identity;&lt;/li&gt;
&lt;li&gt;publish unauthorized emotes;&lt;/li&gt;
&lt;li&gt;retrieve session-specific events;&lt;/li&gt;
&lt;li&gt;terminate another player’s session.&lt;br&gt;
A public session ID is an identifier, not an authorization credential.&lt;br&gt;
Implementing possession-bound sessions&lt;br&gt;
We implemented a new security boundary in:&lt;br&gt;
Pull Request #34 — Bind world actions to session credentials&lt;br&gt;
The change was merged into main as commit:&lt;br&gt;
148a798&lt;br&gt;
Every new Metaverse session now receives two different values:&lt;br&gt;
Public session ID&lt;br&gt;
Private session token&lt;br&gt;
The public ID can appear in world-state responses. The private token proves that the client controls the session.&lt;br&gt;
Cryptographically random credentials&lt;br&gt;
The server generates a token using 32 random bytes:&lt;br&gt;
const sessionToken = crypto&lt;br&gt;
.randomBytes(32)&lt;br&gt;
.toString(&quot;base64url&quot;);&lt;br&gt;
This provides 256 bits of entropy.&lt;br&gt;
The raw token is returned only during session creation and is never included in public presence data.&lt;br&gt;
Storing only a digest&lt;br&gt;
The server does not persist the raw credential.&lt;br&gt;
Instead, it calculates a SHA-256 digest:&lt;br&gt;
const sessionTokenHash = crypto&lt;br&gt;
.createHash(&quot;sha256&quot;)&lt;br&gt;
.update(sessionToken)&lt;br&gt;
.digest(&quot;hex&quot;);&lt;br&gt;
Only sessionTokenHash is stored with the presence record.&lt;br&gt;
This prevents a database reader from immediately obtaining active bearer credentials.&lt;br&gt;
Constant-time comparison&lt;br&gt;
When a protected request arrives, the server hashes the submitted credential and compares it with the stored digest.&lt;br&gt;
The verification uses a constant-time comparison:&lt;br&gt;
crypto.timingSafeEqual(expected, received);&lt;br&gt;
A shared ownership middleware verifies that:&lt;/li&gt;
&lt;li&gt;the session exists;&lt;/li&gt;
&lt;li&gt;a credential was supplied;&lt;/li&gt;
&lt;li&gt;the credential has the expected format;&lt;/li&gt;
&lt;li&gt;its digest matches the stored value;&lt;/li&gt;
&lt;li&gt;the caller owns the requested session.&lt;br&gt;
Invalid or missing credentials return:&lt;br&gt;
HTTP 401 Unauthorized&lt;br&gt;
Protected world routes&lt;br&gt;
Session ownership is now required for mutable and session-specific operations such as:&lt;br&gt;
/sync&lt;br&gt;
/move&lt;br&gt;
/chat&lt;br&gt;
/emote&lt;br&gt;
/leave&lt;br&gt;
/events&lt;br&gt;
Knowing another participant’s sessionId is no longer sufficient to control their character.&lt;br&gt;
The authorization rule is now:&lt;br&gt;
Public session ID&lt;pre&gt;&lt;code&gt;  +
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Private possession token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authorized world action&lt;br&gt;
Keeping credentials out of public payloads&lt;br&gt;
The frontend sends the private token through a dedicated HTTP header:&lt;br&gt;
X-Metaverse-Session-Token: &amp;lt;private-token&amp;gt;&lt;br&gt;
Example:&lt;br&gt;
const createSessionHeaders = (sessionToken) =&amp;gt; ({&lt;br&gt;
&quot;X-Metaverse-Session-Token&quot;: sessionToken,&lt;br&gt;
});&lt;br&gt;
The world-state body remains limited to operational data:&lt;br&gt;
{&lt;br&gt;
&quot;sessionId&quot;: &quot;public-session-id&quot;,&lt;br&gt;
&quot;x&quot;: 120,&lt;br&gt;
&quot;y&quot;: 80&lt;br&gt;
}&lt;br&gt;
This separation reduces the probability that credentials are accidentally copied into:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;world events;&lt;/li&gt;
&lt;li&gt;analytics;&lt;/li&gt;
&lt;li&gt;public API responses;&lt;/li&gt;
&lt;li&gt;application logs;&lt;/li&gt;
&lt;li&gt;debugging output;&lt;/li&gt;
&lt;li&gt;cached payloads.&lt;br&gt;
The credential is also kept in React runtime memory instead of being written to localStorage or public profile objects.&lt;br&gt;
An in-memory token is not a complete defense against browser compromise, but it reduces persistent credential exposure.&lt;br&gt;
Identity boundaries&lt;br&gt;
The Metaverse now distinguishes between several layers:&lt;br&gt;
Account identity&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Public profile&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Character identity&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Temporary world session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Private session credential&lt;br&gt;
These layers have different meanings.&lt;br&gt;
A verified GitHub account does not automatically prove legal identity, employment, payment eligibility or institutional affiliation.&lt;br&gt;
Similarly, a public session ID identifies a player’s presence but does not authorize requests for that player.&lt;br&gt;
Documenting these boundaries is essential for avoiding security and governance problems later.&lt;br&gt;
Testing the implementation&lt;br&gt;
The session-ownership change includes backend, frontend and architecture-level tests.&lt;br&gt;
Backend tests&lt;br&gt;
The targeted backend suites verify:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;secure token generation;&lt;/li&gt;
&lt;li&gt;SHA-256 digest storage;&lt;/li&gt;
&lt;li&gt;valid credential acceptance;&lt;/li&gt;
&lt;li&gt;missing credential rejection;&lt;/li&gt;
&lt;li&gt;invalid credential rejection;&lt;/li&gt;
&lt;li&gt;cross-session mutation prevention;&lt;/li&gt;
&lt;li&gt;protected movement and chat;&lt;/li&gt;
&lt;li&gt;protected emotes and leave operations;&lt;/li&gt;
&lt;li&gt;exclusion of secrets from public responses.&lt;br&gt;
Results:&lt;br&gt;
3 backend suites&lt;br&gt;
22 tests passed&lt;br&gt;
Frontend tests&lt;br&gt;
Frontend coverage verifies:&lt;/li&gt;
&lt;li&gt;token retention in application memory;&lt;/li&gt;
&lt;li&gt;use of the session-token header;&lt;/li&gt;
&lt;li&gt;authenticated world operations;&lt;/li&gt;
&lt;li&gt;removal of tokens from normal JSON bodies;&lt;/li&gt;
&lt;li&gt;correct handling of protected session requests.&lt;br&gt;
Results:&lt;br&gt;
1 frontend suite&lt;br&gt;
5 tests passed&lt;br&gt;
Integration and deployment&lt;br&gt;
The completed verification included:&lt;br&gt;
Production frontend build: successful&lt;br&gt;
git diff --check: clean&lt;br&gt;
Marketplace regression suite: passed&lt;br&gt;
Vercel preview deployments: successful&lt;br&gt;
Main CI workflow: successful&lt;br&gt;
Production deployment: successful&lt;br&gt;
The public Metaverse route was also loaded after deployment.&lt;br&gt;
Current architecture&lt;br&gt;
The current system can be summarized as:&lt;br&gt;
React Metaverse Client&lt;pre&gt;&lt;code&gt;  |
  | HTTPS
  | X-Metaverse-Session-Token
  v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node.js / Express API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  |
  | Session ownership middleware
  | Hash verification
  v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Metaverse Presence Storage&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  |
  v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shared world synchronization&lt;br&gt;
This is a practical foundation, but it is not yet the final realtime architecture.&lt;br&gt;
What remains to be built&lt;br&gt;
Realtime communication&lt;br&gt;
The current synchronization model still depends on polling in important areas.&lt;br&gt;
We need a versioned realtime protocol supporting:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;authenticated WebSocket connections;&lt;/li&gt;
&lt;li&gt;session-to-connection binding;&lt;/li&gt;
&lt;li&gt;reconnect handling;&lt;/li&gt;
&lt;li&gt;ordered events;&lt;/li&gt;
&lt;li&gt;duplicate-event protection;&lt;/li&gt;
&lt;li&gt;presence expiration;&lt;/li&gt;
&lt;li&gt;backpressure;&lt;/li&gt;
&lt;li&gt;slow-client management.&lt;br&gt;
The protocol should remain usable by multiple clients, including future community-built and WebXR clients.&lt;br&gt;
Distributed rate limiting&lt;br&gt;
Per-process rate limiting is not enough when the backend runs across multiple instances.&lt;br&gt;
We need a distributed limiter for:&lt;br&gt;
Session creation&lt;br&gt;
Authentication attempts&lt;br&gt;
Movement&lt;br&gt;
Chat&lt;br&gt;
Emotes&lt;br&gt;
Reconnects&lt;br&gt;
Limits should be calculated using combinations of:&lt;/li&gt;
&lt;li&gt;IP address;&lt;/li&gt;
&lt;li&gt;account ID;&lt;/li&gt;
&lt;li&gt;session ID;&lt;/li&gt;
&lt;li&gt;route;&lt;/li&gt;
&lt;li&gt;world ID;&lt;/li&gt;
&lt;li&gt;time window.&lt;br&gt;
Chat and authentication need stricter limits than normal movement updates.&lt;br&gt;
Session expiration and revocation&lt;br&gt;
The possession credential is now implemented, but lifecycle controls still need improvement:&lt;/li&gt;
&lt;li&gt;explicit expiration;&lt;/li&gt;
&lt;li&gt;server-side revocation;&lt;/li&gt;
&lt;li&gt;abandoned-session cleanup;&lt;/li&gt;
&lt;li&gt;token rotation;&lt;/li&gt;
&lt;li&gt;reconnect-safe renewal;&lt;/li&gt;
&lt;li&gt;active-session limits;&lt;/li&gt;
&lt;li&gt;multi-device logout;&lt;/li&gt;
&lt;li&gt;suspicious-session invalidation.&lt;br&gt;
Canonical authentication&lt;br&gt;
Verified characters require a stable account-linking process.&lt;br&gt;
The remaining work includes:&lt;/li&gt;
&lt;li&gt;canonical GitHub OAuth;&lt;/li&gt;
&lt;li&gt;verified account-to-character relationships;&lt;/li&gt;
&lt;li&gt;account-link substitution protection;&lt;/li&gt;
&lt;li&gt;reauthorization;&lt;/li&gt;
&lt;li&gt;identity-change audit trails;&lt;/li&gt;
&lt;li&gt;account recovery procedures.&lt;br&gt;
The backend must never trust a GitHub username supplied directly by a browser.&lt;br&gt;
Privacy-safe analytics&lt;br&gt;
We need enough telemetry to maintain the platform without introducing unnecessary tracking.&lt;br&gt;
Useful operational metrics include:&lt;/li&gt;
&lt;li&gt;concurrent sessions;&lt;/li&gt;
&lt;li&gt;API latency;&lt;/li&gt;
&lt;li&gt;reconnect frequency;&lt;/li&gt;
&lt;li&gt;server errors;&lt;/li&gt;
&lt;li&gt;world capacity;&lt;/li&gt;
&lt;li&gt;rate-limit events;&lt;/li&gt;
&lt;li&gt;event throughput.&lt;br&gt;
Retention, aggregation and access rules must be documented before expanding analytics.&lt;br&gt;
Reliability and load testing&lt;br&gt;
Future tests must simulate:&lt;/li&gt;
&lt;li&gt;simultaneous joins;&lt;/li&gt;
&lt;li&gt;movement bursts;&lt;/li&gt;
&lt;li&gt;reconnect storms;&lt;/li&gt;
&lt;li&gt;malformed messages;&lt;/li&gt;
&lt;li&gt;database latency;&lt;/li&gt;
&lt;li&gt;backend restarts;&lt;/li&gt;
&lt;li&gt;multiple server instances;&lt;/li&gt;
&lt;li&gt;slow or disconnected clients.&lt;br&gt;
We also need measurable targets for latency, error rate, recovery time and maximum concurrent participants.&lt;br&gt;
Complete E2E acceptance testing&lt;br&gt;
The complete guest flow should be automated:&lt;br&gt;
Join&lt;br&gt;
→ receive credential&lt;br&gt;
→ become visible&lt;br&gt;
→ move&lt;br&gt;
→ chat&lt;br&gt;
→ publish an emote&lt;br&gt;
→ reject impersonation attempts&lt;br&gt;
→ reconnect safely&lt;br&gt;
→ leave or expire&lt;br&gt;
The same acceptance flow is required for verified characters.&lt;br&gt;
WebXR&lt;br&gt;
WebXR remains part of the roadmap, but it should follow security and realtime stabilization.&lt;br&gt;
The eventual world protocol should support:&lt;br&gt;
2D web client&lt;br&gt;
3D browser client&lt;br&gt;
WebXR client&lt;br&gt;
Mobile client&lt;br&gt;
Independent community clients&lt;br&gt;
All clients should use the same documented API and event protocol.&lt;br&gt;
Dependency-security debt&lt;br&gt;
The repository currently reports vulnerabilities during npm audit.&lt;br&gt;
These findings were already present and were not introduced by the Metaverse session-ownership pull request, which did not change package manifests or lockfiles.&lt;br&gt;
However, they remain technical debt.&lt;br&gt;
A separate dependency-hardening milestone should include:&lt;ol&gt;
&lt;li&gt;classifying production and development exposure;&lt;/li&gt;
&lt;li&gt;updating direct dependencies;&lt;/li&gt;
&lt;li&gt;reviewing transitive dependencies;&lt;/li&gt;
&lt;li&gt;replacing abandoned packages;&lt;/li&gt;
&lt;li&gt;regenerating lockfiles;&lt;/li&gt;
&lt;li&gt;running complete regression tests;&lt;/li&gt;
&lt;li&gt;updating SBOM and audit evidence.&lt;br&gt;
Disabling the audit to produce a green badge would hide the problem instead of solving it.&lt;br&gt;
Recommended roadmap&lt;br&gt;
The recommended implementation sequence is:&lt;/li&gt;
&lt;li&gt;Session expiration and revocation&lt;/li&gt;
&lt;li&gt;Distributed rate limiting&lt;/li&gt;
&lt;li&gt;Canonical account authentication&lt;/li&gt;
&lt;li&gt;Versioned realtime protocol&lt;/li&gt;
&lt;li&gt;Authenticated WebSocket transport&lt;/li&gt;
&lt;li&gt;Privacy-safe monitoring&lt;/li&gt;
&lt;li&gt;Reliability and load testing&lt;/li&gt;
&lt;li&gt;Complete E2E acceptance tests&lt;/li&gt;
&lt;li&gt;Dependency-security remediation&lt;/li&gt;
&lt;li&gt;WebXR client development&lt;br&gt;
Security, reliability and protocol stability must come before graphical complexity.&lt;br&gt;
Open-source participation&lt;br&gt;
We are looking for contributors interested in:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;React;&lt;/li&gt;
&lt;li&gt;Node.js and Express;&lt;/li&gt;
&lt;li&gt;MongoDB;&lt;/li&gt;
&lt;li&gt;Redis;&lt;/li&gt;
&lt;li&gt;WebSockets;&lt;/li&gt;
&lt;li&gt;application security;&lt;/li&gt;
&lt;li&gt;privacy engineering;&lt;/li&gt;
&lt;li&gt;abuse prevention;&lt;/li&gt;
&lt;li&gt;load testing;&lt;/li&gt;
&lt;li&gt;WebXR;&lt;/li&gt;
&lt;li&gt;technical documentation.&lt;br&gt;
Repository:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster&lt;/a&gt;&lt;br&gt;
Public Metaverse:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Security implementation:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/34&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/34&lt;/a&gt;&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse now has a stronger session-security foundation.&lt;br&gt;
We moved from public identifier-based operations to possession-bound authorization using:&lt;/li&gt;
&lt;li&gt;256-bit random credentials;&lt;/li&gt;
&lt;li&gt;SHA-256 token digests;&lt;/li&gt;
&lt;li&gt;constant-time verification;&lt;/li&gt;
&lt;li&gt;dedicated authorization headers;&lt;/li&gt;
&lt;li&gt;memory-only client credentials;&lt;/li&gt;
&lt;li&gt;protected world routes;&lt;/li&gt;
&lt;li&gt;backend and frontend security tests.&lt;br&gt;
The next milestone is transforming this foundation into a distributed realtime system with strong authentication, rate limiting, privacy-safe observability and measurable reliability.&lt;br&gt;
The Metaverse is online, but the deeper work is still ahead: building an open world protocol that other developers can inspect, test and extend.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31491/how-secured-the-myzubster-metaverse-session-ownership-verified-explorers-and-road-realti</guid>
<pubDate>Sat, 17 Oct 2026 05:20:55 +0000</pubDate>
</item>
<item>
<title>Dynamic GitHub Actions Dropdowns — A Clever Workaround Made Reusable!</title>
<link>https://coderlegion.com/31452/dynamic-github-actions-dropdowns-a-clever-workaround-made-reusable</link>
<description>&lt;p&gt;GitHub Actions doesn't natively support dynamically populated dropdown options for &lt;code&gt;workflow_dispatch&lt;/code&gt; inputs. This can become a maintenance headache, especially when managing multiple packages in an npm monorepo.&lt;/p&gt;
&lt;p&gt;I came across a clever workaround shared by &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jeevan-vj&quot;&gt;@jeevan-vj&lt;/a&gt;, demonstrating how to dynamically maintain dropdown options by automatically updating GitHub Actions workflow YAML files.&lt;/p&gt;
&lt;p&gt;Inspired by that approach, I built a &lt;strong&gt;reusable GitHub Actions workflow&lt;/strong&gt; that uses a dedicated &lt;code&gt;WORKFLOW_TOKEN&lt;/code&gt; to update workflow files and automatically discover publishable packages in an npm monorepo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here's what my implementation adds:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Automatic package discovery&lt;/strong&gt; — Detects publishable npm workspace packages without maintaining a hardcoded list.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Multi-workflow synchronization&lt;/strong&gt; — Keeps package dropdown options consistent across multiple GitHub Actions workflows.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;YAML formatting preservation&lt;/strong&gt; — Uses Python and &lt;code&gt;ruamel.yaml&lt;/code&gt; to update workflow configurations while preserving their structure and formatting.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Automatic and manual execution&lt;/strong&gt; — Supports synchronization on push as well as manual triggering when needed.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Configurable token authentication&lt;/strong&gt; — Accepts a &lt;code&gt;workflow-token&lt;/code&gt; secret, allowing each consuming repository to choose its own secret name.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;No unnecessary commits&lt;/strong&gt; — Updates workflow files only when package options actually change.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;The end result?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;My &lt;strong&gt;release, publish, restore, reset, and release-draft workflows&lt;/strong&gt; now automatically stay synchronized with the publishable packages in my npm monorepo.&lt;/p&gt;
&lt;p&gt;No more manually updating package dropdowns every time a package is added, renamed, or removed.&lt;/p&gt;
&lt;p&gt;Even better, the solution is reusable across repositories, so I have adopted the same automation in other repos without duplicating the synchronization logic.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Original inspiration — @jeevan-vj:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jeevan-vj/iamjeevan/blob/master/data/blog/dynamically-populating-parameter-options-in-manual-github-action.md&quot;&gt;https://github.com/jeevan-vj/iamjeevan/blob/master/data/blog/dynamically-populating-parameter-options-in-manual-github-action.md&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;My reusable workflow implementation:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/moyarich/reusable-workflows/blob/main/.github/workflows/reusable_sync-release-package-choices.yml&quot;&gt;https://github.com/moyarich/reusable-workflows/blob/main/.github/workflows/reusable_sync-release-package-choices.yml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Real-world implementation — Workspace Tools:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/moyarich/workspace-tools/blob/main/.github/workflows/sync-release-package-choices.yml&quot;&gt;https://github.com/moyarich/workspace-tools/blob/main/.github/workflows/sync-release-package-choices.yml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Huge credit to @jeevan-vj&lt;/strong&gt; for sharing the original workaround. It's a great example of how an open-source idea can evolve into a reusable solution that benefits other developers.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;How are you managing dynamic inputs in your GitHub Actions workflows?&lt;/strong&gt; Have you found another approach?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31452/dynamic-github-actions-dropdowns-a-clever-workaround-made-reusable</guid>
<pubDate>Fri, 16 Oct 2026 19:57:00 +0000</pubDate>
</item>
<item>
<title>OAuth Is Not “Login.” I Found That Out the Hard Way in Production</title>
<link>https://coderlegion.com/31422/oauth-is-not-login-i-found-that-out-the-hard-way-in-production</link>
<description>&lt;p&gt;Most people, including a lot of engineers, think OAuth means “the button that says Sign in with Google.” That’s not wrong exactly, but it’s like saying a car is “the thing with a steering wheel.” Technically true, misses basically everything that matters.&lt;/p&gt;
&lt;p&gt;I want to actually walk through what OAuth is, why it exists, what it costs you, and the specific way it bit me in production, because the bug taught me more about OAuth than any tutorial did.&lt;/p&gt;
&lt;p&gt;If I use a term that sounds like engineer-speak, I’ll flag it like this:&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: here’s the jargon, here’s what it actually means in plain English.&lt;/p&gt;
&lt;p&gt;Let’s go.&lt;/p&gt;
&lt;p&gt;What OAuth actually is&lt;br&gt;
OAuth is not a login system. OAuth is a permission system. It answers one question: “can this app do this specific thing on my behalf, without me handing it my password?”&lt;/p&gt;
&lt;p&gt;That’s it. That’s the whole idea.&lt;/p&gt;
&lt;p&gt;Before OAuth, if you wanted some third party app to, say, post to your Twitter or read your Gmail contacts, you had exactly one option: give that app your actual Twitter or Gmail password. The app would log in as you, directly, using your real credentials. Which means that random app now has full access to your account forever, until you go change your password, and even then you’re trusting a third party company stored your password responsibly in the first place.&lt;/p&gt;
&lt;p&gt;OAuth replaces that with a system where you never hand your password to the third party app at all. Instead, you briefly go to Google (or whoever), log in there, and Google hands the app a token, a kind of limited, revocable permission slip, instead of your actual credentials.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: token. Think of it as a hotel key card instead of your house key. It opens specific doors, for a specific amount of time, and the hotel can deactivate it instantly without you having to change your actual house locks.&lt;/p&gt;
&lt;p&gt;Could the internet work without OAuth&lt;br&gt;
Technically, yes. Plenty of early 2000s apps worked exactly how I described above, you just gave them your password. It’s not that the internet collapses without OAuth.&lt;/p&gt;
&lt;p&gt;What actually happens without it is worse in a slower, quieter way. Every app you connect to anything ends up holding a copy of your real password. A breach at any one of those apps means your actual account password leaks, not just some limited token. You can’t selectively revoke access (“let this app see my calendar but not send emails as me”) because the app either has your full password or it has nothing. And you can’t tell your bank, your email provider, or your social accounts “trust these five apps, not that one,” because from their point of view every connected app looks identical: someone logged in with your password.&lt;/p&gt;
&lt;p&gt;So yes, the world could work without OAuth. It would just be a world where a breach anywhere is a breach everywhere, and “revoke access” isn’t really a button that exists.&lt;/p&gt;
&lt;p&gt;A quick word on OAuth 1.0, the version before this one&lt;br&gt;
Before the OAuth everyone actually uses today, there was OAuth 1.0, back around 2007. Same basic idea, token instead of password, but the mechanics were painful. Every single request had to be cryptographically signed by the app using a shared secret, a process involving HMAC-SHA1 and exact timestamps, before the server would trust it.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: cryptographic. Just means “based on math that’s specifically designed to be really, really hard to fake or reverse without the right secret ingredient.” When something is described as cryptographically secure, breaking it isn’t impossible, it’s just so expensive and slow that nobody sane bothers trying.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: signature (signing). Not a handwritten signature. A short code generated from the actual contents of a message plus a secret value, proving two things at once: this message wasn’t altered in transit, and it genuinely came from whoever holds that secret. Change one character of the message and the signature stops matching, which is exactly how tampering gets caught.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: secret. A private value, basically a password, known only to the two parties who are supposed to trust each other. Whoever holds the secret can prove they are who they claim to be. Whoever doesn’t, can’t fake it convincingly. The whole system falls apart the moment that “secret” stops actually being secret.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: HMAC-SHA1 signing. A way of mathematically stamping each request so the server can verify nothing was tampered with in transit, instead of just trusting an encrypted connection to do that job. You don’t need the math, just know it meant extra, fiddly cryptography homework on every single request.&lt;/p&gt;
&lt;p&gt;In practice this meant developers spent real time fighting signature mismatches and clock sync issues instead of building their actual app. It worked, but it was miserable, and it still assumed every app had somewhere safe to keep a permanent secret. OAuth 2.0 showed up in 2012 and basically said, let’s just lean on HTTPS to protect the data in transit like everyone already does for everything else, and drop the per-request signing entirely.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: HTTPS. The padlock in your browser’s address bar. It encrypts everything traveling between your browser and the server, so anyone snooping on the connection, a nosy coffee shop WiFi for instance, sees scrambled nonsense instead of your actual data. OAuth 2.0 leans on this instead of signing every request itself, which is most of why it’s so much simpler than OAuth 1.0.&lt;/p&gt;
&lt;p&gt;Simpler for developers, which is exactly why OAuth 2.0 is the one that actually won and is what the rest of this article has been describing the whole time.&lt;/p&gt;
&lt;p&gt;But that “every app has somewhere safe to keep a secret” assumption didn’t go away, it just got quietly inherited by OAuth 2.0. And a few years later, that assumption stopped being true for a huge chunk of apps. More on that in a minute.&lt;/p&gt;
&lt;p&gt;How it actually works, the short version&lt;br&gt;
Here’s the flow in plain English, using “Sign in with Google” as the example:&lt;/p&gt;
&lt;p&gt;You click the button. The app sends you over to Google, not the other way around, you never type your Google password into the third party app’s own form. At Google, you log in (or you’re already logged in) and Google shows you a screen saying “This app wants to see your email and profile picture, allow it?” You say yes. Google then redirects you back to the app with a short-lived code. The app takes that code and, behind the scenes, trades it with Google for an actual token. From then on, the app uses that token to ask Google for the specific things you approved, nothing more.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: scope. A scope is the specific permission being requested, like “read your email address” or “post on your behalf.” Apps are supposed to ask for the narrowest scope that does the job, though in practice a lot of apps ask for way more than they need, because broader scopes are easier to code against and nobody’s really checking.&lt;/p&gt;
&lt;p&gt;Download the Medium app&lt;br&gt;
🚨 Jaggy jaggy alert: redirect URI. This is the exact web address Google is allowed to send you back to after login. It’s locked in advance when the app registers with Google, specifically so an attacker can’t trick Google into sending your freshly issued code to some address they control instead.&lt;/p&gt;
&lt;p&gt;That whole flow above quietly assumes one more thing: that the app trading the code for a token is a proper backend server, one that can hold onto a permanent, secret password of its own (a “client secret”) to prove to Google “yes, this request really is from the real app.” Which brings us to a problem nobody really had to think about back in 2012.&lt;/p&gt;
&lt;p&gt;The problem nobody saw coming, apps that can’t keep a secret&lt;br&gt;
Fast forward a few years and suddenly half the apps in the world are single-page apps running entirely in the browser, or mobile apps sitting on someone’s phone. A SPA’s entire codebase gets shipped straight to the user, anyone can pop open dev tools and read it line by line. A mobile app’s installer can be decompiled by anyone with a weekend and mild curiosity. There is nowhere to actually hide a secret in either case. Whatever you bake in, someone can pull back out, which is a uniquely bad punchline for something literally called a “client secret.”&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: client secret. A password-like value, known only to the real backend and Google, used to prove a token request is genuinely coming from the real app and not an imposter. Fine when there’s a server to keep it locked up. Useless the moment the “app” is just JavaScript running on a stranger’s laptop.&lt;/p&gt;
&lt;p&gt;The fix is called PKCE, Proof Key for Code Exchange, and it’s pronounced “pixie,” which might be the single most delightful name in all of security engineering.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: PKCE. Instead of a long-lived secret baked into the app, the app invents a random, one-time value right before login starts, the code verifier, and sends over a hashed version of it, the code challenge, when kicking off the flow. At the very end, when trading the code for a token, it has to produce that original, un-hashed value again. If someone intercepts the authorization code mid-flow, it’s useless to them, they don’t have the random value that generated it, and they can’t fake having it either.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: hashing. A one-way math function that turns any input into a fixed-length scrambled output, and crucially, you can’t run it backwards to get the original input out again. Same input always produces the same output, which is exactly what makes it useful here, the app can prove “I know the original value” without ever saying the original value out loud until the very last step.&lt;/p&gt;
&lt;p&gt;No permanent secret to leak, because there was never a permanent secret to begin with, just a fresh, disposable value that self-destructs after one use. Genuinely a cleaner idea than the thing it replaced, and these days it’s recommended for basically every app, server backed or not.&lt;/p&gt;
&lt;p&gt;The trade-off nobody puts on the slide&lt;br&gt;
Here’s what the “just add OAuth” tutorials gloss over: you’re trading simplicity for control, and that trade costs real engineering effort.&lt;/p&gt;
&lt;p&gt;With a plain username and password system, you own the entire flow. One table, one hashing function, done. With OAuth, you now depend on a third party’s uptime, their API changing underneath you, tokens that expire and need refreshing, and an entire second system, your own backend, that has to track who’s logged in even though the actual login happened somewhere else entirely.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: refresh token. Access tokens are deliberately short-lived, often an hour or less, so if one leaks, the damage window is small. A refresh token is a longer-lived, more sensitive token your backend holds onto quietly, so it can go get a fresh access token without making the user log in again every hour. Lose control of a refresh token and you’ve basically lost control of the session, so these need to be stored far more carefully than access tokens.&lt;/p&gt;
&lt;p&gt;That’s the actual trade-off. You get: no passwords for you to store or leak, users can revoke access with one click from Google’s side, scoped and limited permissions, and single sign-on convenience. You pay for it with: more moving parts, a dependency on an external provider, token lifecycle management, and a genuinely larger surface area for things to quietly go wrong.&lt;/p&gt;
&lt;p&gt;How I actually use this in production&lt;br&gt;
On one platform I worked on, we had Google OAuth as a social login option sitting alongside our own email and password system, with our own JWT issued after either path succeeded.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: JWT. Short for JSON Web Token. This is different from an OAuth access token, even though people use the words interchangeably all the time. A JWT is a signed, self-contained token your own backend creates, that your own backend can verify without a database lookup, basically a sealed, tamper-proof note saying “this is user 482, and I, the server, vouch for that.” OAuth tokens come from Google. JWTs, in this setup, come from us.&lt;/p&gt;
&lt;p&gt;That distinction, two different kinds of token doing two different jobs, is exactly where things went wrong for me once.&lt;/p&gt;
&lt;p&gt;What actually goes wrong: my real production bug&lt;br&gt;
We had a production authentication bug where users were randomly getting logged out, or logged in as the wrong state, with no clear pattern. Intermittent, hard to reproduce locally, the worst kind of bug.&lt;/p&gt;
&lt;p&gt;The root cause, once I actually traced it through production logs, was almost embarrassingly simple: the OAuth token coming back from the social login flow and our own JWT were both being stored under the same cookie key. Whichever one got written last silently overwrote the other. So depending on timing, a user’s session cookie might end up holding Google’s OAuth token instead of our own JWT, or vice versa, and the backend would try to verify it as the wrong type of token entirely, fail, and drop the session.&lt;/p&gt;
&lt;p&gt;Nobody designed it that way on purpose. It happened because OAuth tokens and JWTs look similar enough, both are just strings you shove in a cookie, that it’s easy to treat them as interchangeable “auth stuff” instead of two distinct tokens with two distinct lifecycles and two distinct purposes. The fix was straightforward once I actually understood the distinction: give each token its own, clearly named cookie key, so they could never collide again.&lt;/p&gt;
&lt;p&gt;The real lesson wasn’t the one line fix. It was that OAuth tokens and your own session tokens are not the same thing wearing a different hat, and treating them as the same thing is exactly the kind of assumption that only breaks in production, under real timing and real concurrent requests, never in your local dev environment where everything happens conveniently one at a time.&lt;/p&gt;
&lt;p&gt;Other pitfalls worth knowing before you hit them&lt;br&gt;
A few more ways this goes sideways, things I’ve seen or would actively watch for:&lt;/p&gt;
&lt;p&gt;Storing tokens in localStorage instead of an httpOnly cookie, which means any cross-site scripting vulnerability on your site can just read the token straight out and walk away with the user's session.&lt;/p&gt;
&lt;p&gt;🚨 Jaggy jaggy alert: httpOnly cookie. A cookie flagged so that JavaScript running in the browser can’t read it at all, only the browser itself can send it along with requests. It’s a deliberate wall against exactly the kind of theft I just described.&lt;/p&gt;
&lt;p&gt;Mismatched redirect URIs between what’s registered and what the app actually sends, which just breaks login outright and is a surprisingly common first deploy bug when you move from a local dev URL to a real production domain and forget to register the new one.&lt;/p&gt;
&lt;p&gt;Asking for way more scopes than you need, which makes users nervous at the consent screen and gives you a bigger blast radius if your token ever does leak.&lt;/p&gt;
&lt;p&gt;Treating “the user has an OAuth token” as “the user is logged in” without actually verifying who they are. OAuth by itself is about authorization, what you’re allowed to do, not identity. Actual login on top of OAuth is a separate layer called OpenID Connect, and conflating the two is a genuinely common source of security bugs.&lt;/p&gt;
&lt;p&gt;So, do you actually need it&lt;br&gt;
If you’re building something small and internal, OAuth is probably overkill, a simple username and password system with properly hashed passwords is less to maintain and less to get wrong. OAuth earns its complexity the moment you want users to log in with an identity they already have elsewhere, or you want to act on a third party service on their behalf without ever touching their password. At that point, yes, deal with the token juggling, because the alternative, a world where “give the app your password” is normal, is worse in ways that only show up after something’s already gone wrong.&lt;/p&gt;
&lt;p&gt;That’s the real shape of it. Not a login button. A permission system, with real trade-offs, that will absolutely find the one gap in your assumptions if you let two different tokens share a cookie key and call it a day.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31422/oauth-is-not-login-i-found-that-out-the-hard-way-in-production</guid>
<pubDate>Fri, 16 Oct 2026 17:09:26 +0000</pubDate>
</item>
<item>
<title>Software Maintenance Cost: What to Budget Each Year</title>
<link>https://coderlegion.com/31363/software-maintenance-cost-what-to-budget-each-year</link>
<description>&lt;p&gt;Most people budget carefully for the build and then forget the bill that never stops: &lt;strong&gt;software maintenance cost&lt;/strong&gt;. A web app, SaaS product or internal system is not a one-off purchase. It needs hosting, security patches, dependency upgrades, backups, monitoring and fixes for the problems real users find. If nobody pays for that work, the system quietly decays until it becomes the legacy application you were trying to avoid.&lt;/p&gt;
&lt;p&gt;I look after production systems as part of my &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/services/cloud-devops-deployment&quot;&gt;cloud, DevOps and deployment work&lt;/a&gt;, so this guide covers what maintenance actually includes, what drives the cost, and how to budget for it without guessing.&lt;/p&gt;
&lt;h3 id=&quot;what-software-maintenance-actually-covers-1&quot;&gt;What software maintenance actually covers&lt;/h3&gt;
&lt;p&gt;&quot;Maintenance&quot; is a bucket for four different kinds of work. Knowing which is which makes the budget conversation much easier.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Corrective:&lt;/strong&gt; fixing bugs found in production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preventive:&lt;/strong&gt; security patches, framework and library upgrades, and clean-up that stops problems before they appear.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adaptive:&lt;/strong&gt; changing the system because the world changed, such as a new PHP version, a payment provider update or a browser change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Perfective:&lt;/strong&gt; small improvements and requests that make the product better but are not a new project.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;New features are usually a separate budget. The trap is letting small feature requests quietly consume the maintenance hours that keep the system safe.&lt;/p&gt;
&lt;h3 id=&quot;the-recurring-costs-people-forget-1&quot;&gt;The recurring costs people forget&lt;/h3&gt;
&lt;p&gt;Beyond developer time, a live system has running costs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hosting and infrastructure:&lt;/strong&gt; servers, databases, storage and bandwidth. My comparison of &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-hosting-options-cost&quot;&gt;Laravel hosting options and cost&lt;/a&gt; shows how widely this varies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third-party services:&lt;/strong&gt; email, SMS, payments, search, error tracking and AI APIs, often billed by usage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Domain, SSL and licences&lt;/strong&gt; for paid plugins or tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups and disaster recovery,&lt;/strong&gt; including actually testing a restore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitoring and alerting,&lt;/strong&gt; so you hear about problems before customers do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security work:&lt;/strong&gt; patching, access reviews and responding to vulnerability notices.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These costs are easy to estimate once you have a list. The surprise usually comes from usage-based services that grow with your traffic.&lt;/p&gt;
&lt;h3 id=&quot;what-drives-maintenance-cost-up-or-down-1&quot;&gt;What drives maintenance cost up or down&lt;/h3&gt;
&lt;p&gt;I will not quote a percentage that pretends to fit every system. These factors decide where yours lands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Age and technology.&lt;/strong&gt; Systems on unsupported frameworks cost more to keep safe. This is the main link to &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/rewrite-vs-refactor-legacy-application&quot;&gt;rewriting or refactoring a legacy application&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test coverage.&lt;/strong&gt; Tested code is cheaper to upgrade because you can see what broke.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number of integrations.&lt;/strong&gt; Every third-party API is something that can change or fail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traffic and data volume.&lt;/strong&gt; More load means more tuning and more infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality of documentation.&lt;/strong&gt; Missing knowledge makes every task slower.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment process.&lt;/strong&gt; Manual releases cost time and cause errors. A clean pipeline, as in my guide to &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-zero-downtime-deployments&quot;&gt;zero-downtime deployments&lt;/a&gt;, lowers the cost of every future change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Required uptime.&lt;/strong&gt; A system that must always be available needs monitoring and on-call, which costs more.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;three-ways-to-pay-for-maintenance-1&quot;&gt;Three ways to pay for maintenance&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Retainer.&lt;/strong&gt; A fixed monthly amount that covers a set number of hours or a defined scope. It suits systems with steady needs and gives you predictable budgeting and fast response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Time and materials.&lt;/strong&gt; You pay for hours as they are used. It is flexible for quiet systems, but costs are unpredictable and urgent issues compete with other clients' work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Fixed-scope packages.&lt;/strong&gt; Specific tasks, such as a framework upgrade or security audit, for a fixed price. These are good for planned one-off work on top of a baseline.&lt;/p&gt;
&lt;p&gt;Many businesses use a small retainer for monitoring and urgent fixes, plus fixed-scope projects for bigger upgrades.&lt;/p&gt;
&lt;h3 id=&quot;how-to-estimate-your-own-maintenance-budget-1&quot;&gt;How to estimate your own maintenance budget&lt;/h3&gt;
&lt;p&gt;Use this simple worksheet:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;List every running cost:&lt;/strong&gt; hosting, services, domains, licences. Check last month's invoices for each.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estimate routine hours:&lt;/strong&gt; security patches, dependency updates, backup checks and monitoring review, per month.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add a bug-fix allowance&lt;/strong&gt; based on how often production issues have appeared so far.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add planned upgrade work&lt;/strong&gt; for the year, such as a major framework or PHP version.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add a contingency&lt;/strong&gt; for the unexpected, and treat it as real money.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate new features&lt;/strong&gt; into their own budget so they do not eat maintenance time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the system is brand new and has no history, expect the early months to need more attention while real usage reveals problems. A short audit gives you a better starting point than a guess.&lt;/p&gt;
&lt;h3 id=&quot;why-support-dates-decide-your-upgrade-calendar-1&quot;&gt;Why support dates decide your upgrade calendar&lt;/h3&gt;
&lt;p&gt;Two clocks set most of your planned maintenance: the language and the framework.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Language versions&lt;/strong&gt; each get a limited support window, after which they stop receiving security fixes. PHP publishes these dates on its &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.php.net/supported-versions.php&quot;&gt;supported versions page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework versions&lt;/strong&gt; follow their own policy. Laravel documents which releases still receive bug and security fixes in its &lt;a rel=&quot;nofollow&quot; href=&quot;https://laravel.com/docs/releases&quot;&gt;release notes and support policy&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Put both sets of dates in a calendar and plan an upgrade before a version runs out of support, not after. Doing one planned upgrade a year is far cheaper and less risky than jumping across several versions at once, which is the situation I describe in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-upgrade-checklist&quot;&gt;Laravel upgrade checklist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A good maintenance plan therefore includes a yearly &quot;version review&quot;: check what you run, check the support dates, and decide what gets upgraded next. It takes a few hours and avoids an emergency later.&lt;/p&gt;
&lt;h3 id=&quot;what-happens-when-you-skip-maintenance-1&quot;&gt;What happens when you skip maintenance&lt;/h3&gt;
&lt;p&gt;Skipping maintenance does not save money. It moves the cost to later and makes it larger:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Known security holes stay open&lt;/li&gt;
&lt;li&gt;Upgrades pile up until they become a risky big-bang project&lt;/li&gt;
&lt;li&gt;Small bugs turn into outages&lt;/li&gt;
&lt;li&gt;Only one person understands the system&lt;/li&gt;
&lt;li&gt;Hosting and dependencies freeze because nobody dares touch them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the Henceforward WordPress project, the work combined website development with migrating infrastructure to Scaleway with Laravel Forge, strengthening security and optimising the production environment. That kind of foundation is what makes ongoing care cheaper. You can read the &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/work/henceforward-wordpress-migration-security-optimization&quot;&gt;Henceforward case study&lt;/a&gt; for the details.&lt;/p&gt;
&lt;h3 id=&quot;a-simple-way-to-report-maintenance-value-1&quot;&gt;A simple way to report maintenance value&lt;/h3&gt;
&lt;p&gt;Maintenance feels invisible when nothing breaks, which makes it easy to cut. Ask for a short monthly note covering four things: what was patched or upgraded, which incidents occurred and how fast they were resolved, how backups and monitoring performed, and what is planned next. A one-page summary turns &quot;nothing happened&quot; into evidence that the system is being looked after, and it gives you the data to adjust the plan when needs change.&lt;/p&gt;
&lt;h3 id=&quot;questions-to-ask-before-you-agree-a-maintenance-plan-1&quot;&gt;Questions to ask before you agree a maintenance plan&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;What exactly is included, and what counts as a new feature?&lt;/li&gt;
&lt;li&gt;What is the response time for an urgent production issue?&lt;/li&gt;
&lt;li&gt;Who monitors the system, and how will I be alerted?&lt;/li&gt;
&lt;li&gt;How are backups tested?&lt;/li&gt;
&lt;li&gt;What happens to the plan if the system grows?&lt;/li&gt;
&lt;li&gt;Can I see a report of what was done each month?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A clear answer to these is a better signal than the lowest price. My thoughts on &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/why-experience-beats-low-cost-outsourcing&quot;&gt;why experience beats low-cost outsourcing&lt;/a&gt; apply here too.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaways-1&quot;&gt;Key takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Maintenance is four kinds of work: fixing, preventing, adapting and improving.&lt;/li&gt;
&lt;li&gt;Running costs go beyond developer time: hosting, services, backups, monitoring and security.&lt;/li&gt;
&lt;li&gt;Age, tests, integrations and deployment process drive the price more than size alone.&lt;/li&gt;
&lt;li&gt;Choose a retainer for steady needs, fixed-scope packages for planned upgrades.&lt;/li&gt;
&lt;li&gt;Skipping maintenance makes problems larger and more expensive later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want a clear maintenance budget for a system you already run, &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/book&quot;&gt;book a call&lt;/a&gt; and I will review it and tell you what it really needs.&lt;/p&gt;
&lt;p&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/software-maintenance-cost-what-to-budget&quot;&gt;aqibjavaid.site&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31363/software-maintenance-cost-what-to-budget-each-year</guid>
<pubDate>Fri, 16 Oct 2026 14:55:47 +0000</pubDate>
</item>
<item>
<title>Reverse Proxy or Transparent WAF? A Checklist for Web Teams</title>
<link>https://coderlegion.com/31302/reverse-proxy-or-transparent-waf-a-checklist-for-web-teams</link>
<description>&lt;h2 id=&quot;reverse-proxy-or-transparent-waf-a-checklist-for-web-teams&quot;&gt;Reverse Proxy or Transparent WAF? A Checklist for Web Teams&lt;/h2&gt;
&lt;p&gt;Ask five questions before deployment: Can the origin be restricted? Who terminates TLS? Where are logs collected? How will bypasses be detected? What is the rollback path?&lt;/p&gt;
&lt;p&gt;A reverse-proxy WAF makes these decisions explicit in the HTTP path. A transparent design may reduce topology changes but can require more network-level coordination. SafeLine’s documented deployment model is a self-hosted reverse proxy, so use the official guide to validate fit.&lt;/p&gt;
&lt;h3 id=&quot;faq-1&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;does-safeline-support-both-rule-based-and-semantic-protection-&quot;&gt;Does SafeLine support both rule-based and semantic protection?&lt;/h4&gt;
&lt;p&gt;SafeLine documents layered application protection and dynamic protection features. Check the current product documentation for the exact behavior of each release.&lt;/p&gt;
&lt;h4 id=&quot;should-i-test-changes-before-production-&quot;&gt;Should I test changes before production?&lt;/h4&gt;
&lt;p&gt;Yes. Use a staged rollout, review logs, and verify legitimate requests before tightening policies.&lt;/p&gt;
&lt;h4 id=&quot;where-can-i-check-current-pro-pricing-&quot;&gt;Where can I check current Pro pricing?&lt;/h4&gt;
&lt;p&gt;Use the official &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;700K+ Installs Sale page&lt;/a&gt; for current campaign terms.&lt;/p&gt;
&lt;h4 id=&quot;is-the-sale-renewable-at-the-same-price-&quot;&gt;Is the sale renewable at the same price?&lt;/h4&gt;
&lt;p&gt;The campaign page says promotional pricing ends with the campaign; renewals return to regular pricing.&lt;/p&gt;
&lt;p&gt;What evidence would you use to compare protection modes in your own traffic?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Read the full guide on dev.to:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;📖 &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/&quot;&gt;Reverse Proxy or Transparent WAF? A Checklist for Web Teams&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;SafeLine Pro 700K+ Installs Sale&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31302/reverse-proxy-or-transparent-waf-a-checklist-for-web-teams</guid>
<pubDate>Fri, 16 Oct 2026 07:56:12 +0000</pubDate>
</item>
<item>
<title>Reverse Proxy or Transparent WAF? A Checklist for Web Teams</title>
<link>https://coderlegion.com/31287/reverse-proxy-or-transparent-waf-a-checklist-for-web-teams</link>
<description>&lt;h2 id=&quot;reverse-proxy-or-transparent-waf-a-checklist-for-web-teams-1&quot;&gt;Reverse Proxy or Transparent WAF? A Checklist for Web Teams&lt;/h2&gt;
&lt;p&gt;Ask five questions before deployment: Can the origin be restricted? Who terminates TLS? Where are logs collected? How will bypasses be detected? What is the rollback path?&lt;/p&gt;
&lt;p&gt;A reverse-proxy WAF makes these decisions explicit in the HTTP path. A transparent design may reduce topology changes but can require more network-level coordination. SafeLine’s documented deployment model is a self-hosted reverse proxy, so use the official guide to validate fit.&lt;/p&gt;
&lt;h3 id=&quot;faq-2&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;does-safeline-support-both-rule-based-and-semantic-protection--1&quot;&gt;Does SafeLine support both rule-based and semantic protection?&lt;/h4&gt;
&lt;p&gt;SafeLine documents layered application protection and dynamic protection features. Check the current product documentation for the exact behavior of each release.&lt;/p&gt;
&lt;h4 id=&quot;should-i-test-changes-before-production--1&quot;&gt;Should I test changes before production?&lt;/h4&gt;
&lt;p&gt;Yes. Use a staged rollout, review logs, and verify legitimate requests before tightening policies.&lt;/p&gt;
&lt;h4 id=&quot;where-can-i-check-current-pro-pricing--1&quot;&gt;Where can I check current Pro pricing?&lt;/h4&gt;
&lt;p&gt;Use the official &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;700K+ Installs Sale page&lt;/a&gt; for current campaign terms.&lt;/p&gt;
&lt;h4 id=&quot;is-the-sale-renewable-at-the-same-price--1&quot;&gt;Is the sale renewable at the same price?&lt;/h4&gt;
&lt;p&gt;The campaign page says promotional pricing ends with the campaign; renewals return to regular pricing.&lt;/p&gt;
&lt;p&gt;What evidence would you use to compare protection modes in your own traffic?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Read the full guide on dev.to:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;📖 &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/&quot;&gt;Reverse Proxy or Transparent WAF? A Checklist for Web Teams&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;SafeLine Pro 700K+ Installs Sale&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31287/reverse-proxy-or-transparent-waf-a-checklist-for-web-teams</guid>
<pubDate>Fri, 16 Oct 2026 06:23:12 +0000</pubDate>
</item>
<item>
<title>Reverse Proxy WAF vs Transparent Bridge: A Deployment Decision</title>
<link>https://coderlegion.com/31286/reverse-proxy-waf-vs-transparent-bridge-a-deployment-decision</link>
<description>&lt;h2 id=&quot;reverse-proxy-waf-vs-transparent-bridge-a-deployment-decision&quot;&gt;Reverse Proxy WAF vs Transparent Bridge: A Deployment Decision&lt;/h2&gt;
&lt;p&gt;The main difference is how clearly the WAF participates in the application traffic path. A reverse proxy is an explicit HTTP gateway. A transparent bridge is designed to minimize visible topology changes.&lt;/p&gt;
&lt;h3 id=&quot;reverse-proxy-trade-offs&quot;&gt;Reverse proxy trade-offs&lt;/h3&gt;
&lt;p&gt;The explicit hop can make routing and policy ownership easier to reason about. It also means the team must plan certificates, upstream connectivity, health checks, and origin restrictions.&lt;/p&gt;
&lt;h3 id=&quot;transparent-designs&quot;&gt;Transparent designs&lt;/h3&gt;
&lt;p&gt;A transparent design may fit networks where changing endpoints is difficult, but it can make troubleshooting and application context more dependent on the surrounding network.&lt;/p&gt;
&lt;p&gt;SafeLine documents the reverse-proxy model, application onboarding, and web attack protections. Compare that documented model with your existing topology before choosing a deployment.&lt;/p&gt;
&lt;h3 id=&quot;faq-3&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;does-safeline-support-both-rule-based-and-semantic-protection--2&quot;&gt;Does SafeLine support both rule-based and semantic protection?&lt;/h4&gt;
&lt;p&gt;SafeLine documents layered application protection and dynamic protection features. Check the current product documentation for the exact behavior of each release.&lt;/p&gt;
&lt;h4 id=&quot;should-i-test-changes-before-production--2&quot;&gt;Should I test changes before production?&lt;/h4&gt;
&lt;p&gt;Yes. Use a staged rollout, review logs, and verify legitimate requests before tightening policies.&lt;/p&gt;
&lt;h4 id=&quot;where-can-i-check-current-pro-pricing--2&quot;&gt;Where can I check current Pro pricing?&lt;/h4&gt;
&lt;p&gt;Use the official &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;700K+ Installs Sale page&lt;/a&gt; for current campaign terms.&lt;/p&gt;
&lt;h4 id=&quot;is-the-sale-renewable-at-the-same-price--2&quot;&gt;Is the sale renewable at the same price?&lt;/h4&gt;
&lt;p&gt;The campaign page says promotional pricing ends with the campaign; renewals return to regular pricing.&lt;/p&gt;
&lt;p&gt;What evidence would you use to compare protection modes in your own traffic?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Read the full guide on dev.to:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;📖 &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/&quot;&gt;Reverse Proxy WAF vs Transparent Bridge: A Deployment Decision&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;SafeLine Pro 700K+ Installs Sale&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31286/reverse-proxy-waf-vs-transparent-bridge-a-deployment-decision</guid>
<pubDate>Fri, 16 Oct 2026 06:22:39 +0000</pubDate>
</item>
<item>
<title>Gating Cloudflare Worker Deployments With CI</title>
<link>https://coderlegion.com/31231/gating-cloudflare-worker-deployments-with-ci</link>
<description>&lt;p&gt;Automated build systems often deploy code immediately on merge without waiting for external quality gates to finish. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick solution:&lt;/strong&gt;&lt;br&gt;
Configure GitHub branch protection rules to require your test and lint status checks before merging, and ensure your Workers Builds configuration targets that protected production branch as detailed in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/&quot;&gt;Build branches - Cloudflare Workers docs&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: CI Validation
on:
  pull_request:
    branches: [main]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Tests
        run: npm test
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;understanding-the-deployment-automation-boundary&quot;&gt;Understanding the Deployment Automation Boundary&lt;/h3&gt;
&lt;p&gt;When a pull request passes all status checks and merges to the main branch, teams often assume that deployment waits for every upstream validation. In reality, automated continuous integration and platform builds run in decoupled systems. A green test suite or &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/android-static-analysis-lint-detekt-ci/&quot;&gt;static analysis&lt;/a&gt; badge on a pull request does not guarantee that Cloudflare Workers waited for that specific result before deploying the latest script.&lt;/p&gt;
&lt;p&gt;Understanding where validation occurs is essential for preventing regressions from reaching users. Pull request checks evaluate proposed changes in an isolated environment. However, platforms like Cloudflare Workers Builds can trigger automatically upon receiving a push to a configured branch, independently of whether external quality gates have finished or passed on that exact ref.&lt;/p&gt;
&lt;p&gt;GitHub allows you to enforce requirements via protected branches. According to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches&quot;&gt;About protected branches - GitHub Docs&lt;/a&gt;, repository owners can require specific status checks to pass before merging any code. If a required check fails or is missing from the protection rules, a developer can merge code that has not been fully verified.&lt;/p&gt;
&lt;h3 id=&quot;integrating-sonarqube-cloud-quality-gates&quot;&gt;Integrating SonarQube Cloud Quality Gates&lt;/h3&gt;
&lt;p&gt;Static analysis tools provide deep insights into code smells, vulnerabilities, and duplications. As detailed in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.sonarsource.com/sonarqube-cloud/standards/managing-quality-gates/introduction-to-quality-gates&quot;&gt;Introduction to quality gates - SonarQube Cloud&lt;/a&gt;, a &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/make-sonarcloud-quality-gate-fail-github-actions/&quot;&gt;quality gate&lt;/a&gt; defines the pass or fail criteria for a project based on clean code metrics.&lt;/p&gt;
&lt;p&gt;A common failure mode occurs when Sonar analysis runs post-merge rather than as a blocking pull request check. If the analysis executes only after code lands on the main branch, it reports the state of the codebase after deployment has already begun. To act as a true quality gate, the Sonar analysis status must be reported back to the pull request and registered as a required status check in your version control platform.&lt;/p&gt;
&lt;h3 id=&quot;configuring-workers-builds-and-branch-triggers&quot;&gt;Configuring Workers Builds and Branch Triggers&lt;/h3&gt;
&lt;p&gt;Cloudflare Workers can build and deduplicate automatically from connected Git repositories. Reviewing the &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.cloudflare.com/workers/ci-cd/&quot;&gt;CI/CD - Cloudflare Workers docs&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.cloudflare.com/workers/ci-cd/builds/configuration/&quot;&gt;Configuration - Cloudflare Workers docs&lt;/a&gt; helps clarify how build triggers map to repository branches.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Integration Option &lt;/th&gt;&lt;th&gt;When to Use It &lt;/th&gt;&lt;th&gt;Trade-off or Failure Mode &lt;/th&gt;&lt;th&gt;Recommendation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Automatic Branch Builds &lt;/td&gt;&lt;td&gt;Fast iterations and continuous delivery for single-service projects. &lt;/td&gt;&lt;td&gt;Triggers immediately on push; external analysis might still be running. &lt;/td&gt;&lt;td&gt;Use with strict branch protection rules.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Manual or API Deployment &lt;/td&gt;&lt;td&gt;Strict regulatory requirements or complex multi-service release sequencing. &lt;/td&gt;&lt;td&gt;Requires maintaining external CI pipeline tokens and credentials. &lt;/td&gt;&lt;td&gt;Use when deployment must wait for external sign-offs.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;tracing-release-evidence-across-systems&quot;&gt;Tracing Release Evidence Across Systems&lt;/h3&gt;
&lt;p&gt;Proper release engineering requires tracing a single release across multiple disconnected platforms. You should inspect the commit SHA of the pull request, verify the exact merge commit hash, confirm the corresponding Cloudflare Workers Build version, check the Sonar branch analysis report, and perform a live route check. If deployment starts immediately on merge, a later main-branch scan is not a pre-deployment gate unless another system explicitly waits for it.&lt;/p&gt;
&lt;p&gt;Do not infer correct ordering from two separate green badges. Inspect actual branch-protection rules, Workers Builds settings, and the deployed version directly. Record the exact commit SHA for each event and report any missing gate as a configuration gap in your infrastructure.&lt;/p&gt;
&lt;h3 id=&quot;release-verification-and-execution-checklist&quot;&gt;Release Verification and Execution Checklist&lt;/h3&gt;
&lt;p&gt;Use this execution checklist to confirm that your validation steps actually gate your production releases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Verify branch protection rules: Confirm that pull requests cannot be merged without passing unit test and lint checks.&lt;/li&gt;
&lt;li&gt;Inspect status check mapping: Ensure that the exact check names in GitHub match the jobs defined in your CI pipeline configuration.&lt;/li&gt;
&lt;li&gt;Check analysis timing: Verify that SonarQube Cloud evaluates the pull request commit rather than only scanning the main branch post-merge.&lt;/li&gt;
&lt;li&gt;Audit build triggers: Inspect your Cloudflare Workers Builds settings to ensure production deployments listen only to the protected main branch.&lt;/li&gt;
&lt;li&gt;Perform live route testing: Execute a targeted curl command against your production worker route to verify that the newly deployed version hash matches the expected release commit.&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31231/gating-cloudflare-worker-deployments-with-ci</guid>
<pubDate>Thu, 15 Oct 2026 17:16:41 +0000</pubDate>
</item>
<item>
<title>From “Why Is This Pod Pending?” to `HTTP/1.1 200 OK`: My NGINX Gateway Fabric Lab</title>
<link>https://coderlegion.com/31221/from-why-is-this-pod-pending-to-http-1-1-200-ok-my-nginx-gateway-fabric-lab</link>
<description>&lt;blockquote&gt;&lt;p&gt;A beginner-friendly Kubernetes networking story about Gateway API, Services, HTTPRoutes, DNS, a few misleading errors, and the joy of finally seeing a successful response.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-mission&quot;&gt;The mission&lt;/h3&gt;
&lt;p&gt;The goal of this exercise was simple to say and slightly less simple to do: deploy &lt;strong&gt;NGINX Gateway Fabric&lt;/strong&gt; in my Kubernetes cluster, create a small NGINX web application, and make the application accessible through a Kubernetes &lt;strong&gt;Gateway&lt;/strong&gt; and &lt;strong&gt;HTTPRoute&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The intended request path was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;My Kali terminal
      |
      | HTTP request to NodePort :30080
      v
NGINX Gateway
      |
      | HTTPRoute matches path /
      v
Kubernetes Service: frontend-svc:80
      |
      v
Pod: frontend-app (NGINX listens on port 80)
      |
      v
HTTP/1.1 200 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Think of it like sending a parcel. The Gateway is the front desk, the HTTPRoute is the instruction that says where the parcel should go, the Service is the stable address for the destination, and the Pod is the actual person receiving it.&lt;/p&gt;
&lt;p&gt;This post walks through what I did, the problems I hit, and how I worked through them. The useful lesson was not just getting the page to load. It was learning to test one layer at a time instead of randomly changing things until Kubernetes takes pity on me.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-installing-the-gateway-api-building-blocks&quot;&gt;1. Installing the Gateway API building blocks&lt;/h3&gt;
&lt;p&gt;I started in my &lt;code&gt;nginx-gateway-fabric&lt;/code&gt; working directory and applied the standard Gateway API CRDs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply -k config/crd/gateway-api/standard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CRD&lt;/strong&gt; means &lt;em&gt;CustomResourceDefinition&lt;/em&gt;. Kubernetes comes with built-in resources such as Pods, Deployments, and Services. CRDs teach Kubernetes about additional resource types. The Gateway API CRDs add resources such as &lt;code&gt;GatewayClass&lt;/code&gt;, &lt;code&gt;Gateway&lt;/code&gt;, and &lt;code&gt;HTTPRoute&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I then installed the NGINX Gateway Fabric-specific CRDs and the NodePort deployment for version &lt;code&gt;1.6.1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply -f https://raw.githubusercontent.com/nginx/nginx-gateway-fabric/v1.6.1/deploy/crds.yaml

kubectl apply -f https://raw.githubusercontent.com/nginx/nginx-gateway-fabric/v1.6.1/deploy/nodeport/deploy.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The NodePort deployment gave me a way to reach the Gateway using a port on a Kubernetes node. In this lab, HTTP was exposed on port &lt;strong&gt;30080&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;problem-1-kustomize-timed-out-while-fetching-the-crds&quot;&gt;Problem 1: Kustomize timed out while fetching the CRDs&lt;/h4&gt;
&lt;p&gt;My first attempt to fetch the Gateway API CRDs directly from GitHub using &lt;code&gt;kubectl kustomize&lt;/code&gt; failed with a Git fetch timeout. Because the command failed to produce the manifests, the following &lt;code&gt;kubectl apply&lt;/code&gt; command reported that no objects had been passed to it.&lt;/p&gt;
&lt;p&gt;That second message was a consequence of the first failure, not a separate mystery.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I did:&lt;/strong&gt; I worked with the repository locally and applied the Kustomize directory from the checked-out source:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply -k config/crd/gateway-api/standard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; When a command downloads configuration and then applies it, check the download/rendering step first. If it produces nothing, &lt;code&gt;kubectl apply&lt;/code&gt; cannot magically apply it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-gateway-pod-was-pending&quot;&gt;2. The Gateway Pod was Pending&lt;/h3&gt;
&lt;p&gt;After applying the NGINX Gateway Fabric deployment, I checked the Pod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get pods -n nginx-gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first, it showed &lt;code&gt;Pending&lt;/code&gt;. My first attempt to describe it failed too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl describe pod nginx-gateway-55f96886f7-j4kzq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Pod was not found because I had left out its namespace. It lived in &lt;code&gt;nginx-gateway&lt;/code&gt;, not the &lt;code&gt;default&lt;/code&gt; namespace.&lt;/p&gt;
&lt;p&gt;The corrected command was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl describe pod nginx-gateway-55f96886f7-j4kzq -n nginx-gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important event was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0/2 nodes are available: 2 node(s) had untolerated taint(s).
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-does-that-mean-&quot;&gt;What does that mean?&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;taint&lt;/strong&gt; is a rule on a Kubernetes node that says, in effect, “Don't schedule Pods here unless they tolerate this condition.” A &lt;strong&gt;toleration&lt;/strong&gt; is a matching permission in a Pod's specification.&lt;/p&gt;
&lt;p&gt;The Gateway Pod was not necessarily broken. Kubernetes was saying it could not find a node where the Pod was allowed to run. I also saw that &lt;code&gt;k8s-worker1&lt;/code&gt; briefly became &lt;code&gt;NotReady&lt;/code&gt; before returning to &lt;code&gt;Ready&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Check the node status and the Pod's scheduling events before changing the deployment. Taints may be protecting the cluster for a reason, so don't remove them blindly just to make a Pod start.&lt;/p&gt;
&lt;p&gt;Useful commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get nodes -o wide
kubectl describe pod nginx-gateway-55f96886f7-j4kzq -n nginx-gateway
kubectl describe node k8s-worker1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventually, the Gateway Pod was scheduled to &lt;code&gt;k8s-worker1&lt;/code&gt; and progressed toward running.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-creating-the-frontend-application&quot;&gt;3. Creating the frontend application&lt;/h3&gt;
&lt;p&gt;Next, I created a simple NGINX Pod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl run frontend-app --image=nginx --port=8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One subtle detail caused a later problem: &lt;strong&gt;&lt;code&gt;--port=8080&lt;/code&gt; does not reconfigure NGINX to listen on port 8080.&lt;/strong&gt; It records container-port metadata in the Pod specification. The NGINX image still listens on port &lt;strong&gt;80&lt;/strong&gt; by default.&lt;/p&gt;
&lt;p&gt;I exposed the Pod as a Service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl expose pod frontend-app \
  --name=frontend-svc \
  --port=80 \
  --target-port=8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;strong&gt;Service&lt;/strong&gt; gives a stable network address to a group of Pods. This is useful because Pods can be replaced, and their IP addresses can change. Clients can keep talking to the Service instead of tracking individual Pod IPs.&lt;/p&gt;
&lt;p&gt;I checked the Service and its endpoints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl describe svc frontend-svc -n default
kubectl get endpoints frontend-svc -n default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initially, the endpoint showed port &lt;code&gt;8080&lt;/code&gt;. That looked consistent with the Service configuration, but it was not consistent with the port NGINX actually used.&lt;/p&gt;
&lt;h4 id=&quot;problem-2-the-service-pointed-to-the-wrong-port&quot;&gt;Problem 2: The Service pointed to the wrong port&lt;/h4&gt;
&lt;p&gt;I tested NGINX from inside its own Pod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl exec frontend-app -n default -- \
  curl -I --max-time 5 http://127.0.0.1:8080/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It failed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;curl: (7) Failed to connect to 127.0.0.1 port 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of guessing, I tested the default NGINX port:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl exec frontend-app -n default -- \
  curl -I --max-time 5 http://127.0.0.1:80/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTTP/1.1 200 OK
Server: nginx/1.31.7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There was the clue. NGINX was alive; I had simply sent it to the wrong port.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; I changed the Service's &lt;code&gt;targetPort&lt;/code&gt; to &lt;code&gt;80&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl patch svc frontend-svc -n default \
  -p '{&quot;spec&quot;:{&quot;ports&quot;:[{&quot;port&quot;:80,&quot;targetPort&quot;:80}]}}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I checked it again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl describe svc frontend-svc -n default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The endpoint now pointed to the Pod on port &lt;code&gt;80&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; The Service's &lt;code&gt;port&lt;/code&gt; is the port clients use to reach the Service. The &lt;code&gt;targetPort&lt;/code&gt; is the port on the backend Pod that receives the traffic. Those values can be different, but &lt;code&gt;targetPort&lt;/code&gt; must match the application that is actually listening.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-building-the-httproute&quot;&gt;4. Building the HTTPRoute&lt;/h3&gt;
&lt;p&gt;The next step was to tell the Gateway where to send incoming HTTP requests. I created an &lt;code&gt;HTTPRoute&lt;/code&gt; called &lt;code&gt;frontend-route&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The important parts of the route were:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: frontend-route
  namespace: default
spec:
  parentRefs:
    - name: nginx-gateway
      namespace: nginx-gateway
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: frontend-svc
          port: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In plain English:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attach this route to the &lt;code&gt;nginx-gateway&lt;/code&gt; Gateway in the &lt;code&gt;nginx-gateway&lt;/code&gt; namespace.&lt;/li&gt;
&lt;li&gt;Match requests whose path starts with &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forward matching requests to the Service named &lt;code&gt;frontend-svc&lt;/code&gt; on port &lt;code&gt;80&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I checked the result with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl describe httproute frontend-route -n default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The good news was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Type:    Accepted
Status:  True

Type:    ResolvedRefs
Status:  True
Message: All references are resolved
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These conditions mean the Gateway accepted the route and Kubernetes could resolve its references. They are useful signals, but I still needed to test actual traffic.&lt;/p&gt;
&lt;h4 id=&quot;problem-3-the-httproute-could-not-find-the-backend-service&quot;&gt;Problem 3: The HTTPRoute could not find the backend Service&lt;/h4&gt;
&lt;p&gt;My first route was in the &lt;code&gt;nginx-gateway&lt;/code&gt; namespace and referred to &lt;code&gt;frontend-svc&lt;/code&gt;. Kubernetes returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BackendNotFound
spec.rules[0].backendRefs[0].name: Not found: &quot;frontend-svc&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had created &lt;code&gt;frontend-svc&lt;/code&gt; in the &lt;code&gt;default&lt;/code&gt; namespace. A Service reference in an HTTPRoute is normally resolved in the route's own namespace. Giving the Service the correct name was not enough; the namespace mattered too.&lt;/p&gt;
&lt;p&gt;I edited the route YAML and placed the working &lt;code&gt;frontend-route&lt;/code&gt; in &lt;code&gt;default&lt;/code&gt;, while keeping its &lt;code&gt;parentRefs&lt;/code&gt; pointing to the Gateway in &lt;code&gt;nginx-gateway&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That fixed the reference. The route then reported both &lt;code&gt;Accepted=True&lt;/code&gt; and &lt;code&gt;ResolvedRefs=True&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; In Kubernetes, always check both the resource's &lt;strong&gt;name and namespace&lt;/strong&gt;. Two resources can share a name in different namespaces, and a route can be accepted while still having an unresolved backend reference.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-testing-the-service-before-testing-the-gateway&quot;&gt;5. Testing the Service before testing the Gateway&lt;/h3&gt;
&lt;p&gt;Before troubleshooting the Gateway, I tested the Service directly. First, I checked that CoreDNS was running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get pods -n kube-system
kubectl get svc -n kube-system kube-dns
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I created a short-lived diagnostic Pod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl run dns-test --image=busybox:1.36 \
  --restart=Never --command -- sleep 300

kubectl wait --for=condition=Ready pod/dns-test \
  -n default --timeout=60s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I inspected its DNS settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl exec dns-test -- cat /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file showed the cluster DNS Service IP, &lt;code&gt;10.96.0.10&lt;/code&gt;. I then tested DNS resolution:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl exec dns-test -- \
  nslookup kubernetes.default.svc.cluster.local

kubectl exec dns-test -- \
  nslookup frontend-svc.default.svc.cluster.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both names resolved. The frontend Service resolved to &lt;code&gt;10.104.158.51&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;problem-4-one-curl-test-could-not-resolve-the-service-name&quot;&gt;Problem 4: One curl test could not resolve the Service name&lt;/h4&gt;
&lt;p&gt;An earlier temporary curl Pod returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;curl: (6) Could not resolve host: frontend-svc.default.svc.cluster.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That was confusing because the DNS Service and CoreDNS Pods looked healthy.&lt;/p&gt;
&lt;p&gt;Rather than restarting DNS immediately, I used a separate diagnostic Pod to test the basics. Its &lt;code&gt;nslookup&lt;/code&gt; commands successfully resolved both the Kubernetes API Service and &lt;code&gt;frontend-svc&lt;/code&gt;. Then I tested the Service itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl exec dns-test -- wget -S -O- \
  http://frontend-svc.default.svc.cluster.local/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This returned the default NGINX welcome page and an &lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt; response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I can conclude:&lt;/strong&gt; DNS resolution and Service connectivity worked from the diagnostic Pod. The earlier curl test failed, but the evidence did not establish its exact cause. The right move was to reproduce the test and inspect the layers separately rather than assume CoreDNS was broken.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-the-gateway-was-programmed-but-my-first-request-returned-a-500&quot;&gt;6. The Gateway was programmed, but my first request returned a 500&lt;/h3&gt;
&lt;p&gt;I checked the Gateway and its Service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get svc -n nginx-gateway
kubectl describe gateway nginx-gateway -n nginx-gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Service showed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TYPE       NodePort
PORT(S)    80:30080/TCP,443:30081/TCP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Gateway status showed &lt;code&gt;Accepted=True&lt;/code&gt; and &lt;code&gt;Programmed=True&lt;/code&gt;. That meant the Gateway configuration had been accepted and programmed, but an end-to-end test was still needed.&lt;/p&gt;
&lt;p&gt;I checked the route in &lt;code&gt;nginx-gateway&lt;/code&gt; and discovered that a &lt;strong&gt;second route with the same name&lt;/strong&gt; existed there. Unlike the working route in &lt;code&gt;default&lt;/code&gt;, this duplicate route still referenced a backend Service that was not in its namespace. Its status included &lt;code&gt;BackendNotFound&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I deleted the broken duplicate route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl delete httproute frontend-route -n nginx-gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I kept the working route in &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My first request to the NodePort returned &lt;code&gt;500 Internal Server Error&lt;/code&gt;. After removing the broken duplicate route, I tried again. The request succeeded.&lt;/p&gt;
&lt;p&gt;The logs show that the bad duplicate route existed and that the response changed after I removed it. That makes the duplicate route a strong suspect in the earlier failure, although the response alone does not prove it was the only cause.&lt;/p&gt;
&lt;h4 id=&quot;problem-5-two-routes-one-name-different-namespaces&quot;&gt;Problem 5: Two routes, one name, different namespaces&lt;/h4&gt;
&lt;p&gt;Kubernetes permits resources with the same name in different namespaces. That is not automatically an error, but it made this troubleshooting session confusing: one &lt;code&gt;frontend-route&lt;/code&gt; was correctly configured, while the other had an unresolved backend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; When a route behaves unexpectedly, check all routes across namespaces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get httproute -A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don't check only the namespace where you expect the route to live.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-the-final-test-success-through-the-nodeport&quot;&gt;7. The final test: success through the NodePort&lt;/h3&gt;
&lt;p&gt;I checked the node addresses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get nodes -o wide
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The worker node's internal IP was &lt;code&gt;10.0.2.4&lt;/code&gt;. The Gateway's HTTP NodePort was &lt;code&gt;30080&lt;/code&gt;, so I tested:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -i --max-time 10 http://10.0.2.4:30080/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first, I mistakenly typed the placeholder literally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -i --max-time 10 http://WORKER_IP:30080/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The terminal returned &lt;code&gt;Could not resolve host: WORKER_IP&lt;/code&gt;. The shell did exactly what I asked: it treated &lt;code&gt;WORKER_IP&lt;/code&gt; as a hostname. Placeholders must be replaced with actual values before running a command.&lt;/p&gt;
&lt;p&gt;The final request returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTTP/1.1 200 OK
Server: nginx
Content-Length: 896
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, below the headers, the familiar &lt;strong&gt;“Welcome to nginx!”&lt;/strong&gt; HTML page appeared.&lt;/p&gt;
&lt;p&gt;That was the end-to-end confirmation I wanted: a request from my Kali terminal entered through the node's NodePort, reached the NGINX Gateway, matched the HTTPRoute, and was forwarded to the frontend Service and Pod.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31221/from-why-is-this-pod-pending-to-http-1-1-200-ok-my-nginx-gateway-fabric-lab</guid>
<pubDate>Thu, 15 Oct 2026 15:15:48 +0000</pubDate>
</item>
<item>
<title>Every disk-alert script I had piped into a command this machine does not have</title>
<link>https://coderlegion.com/28024/every-disk-alert-script-i-had-piped-into-a-command-this-machine-does-not-have</link>
<description>&lt;p&gt;The standard bash alerting pattern ends the same way everywhere you read it: pipe the finding into &lt;code&gt;mail&lt;/code&gt;. So I checked what my own box would actually do with that line.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ type -a mail mailx sendmail
mail not found
mailx not found
sendmail not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing. No MTA, no &lt;code&gt;mailutils&lt;/code&gt;, no fallback. Any alert script written the usual way would have run its check correctly, found the problem correctly, piped the message into a command that does not exist, and told me nothing.&lt;/p&gt;
&lt;p&gt;This fails quieter than it sounds. Under &lt;code&gt;set -euo pipefail&lt;/code&gt; you at least get a non-zero exit somewhere a cron wrapper might notice. Without it, the pipeline's exit status is the &lt;em&gt;last&lt;/em&gt; command's, cron mails you nothing because cron's mail also needs an MTA, and the script looks like it ran fine.&lt;/p&gt;
&lt;p&gt;Check for the binary before you rely on it, not after an incident. And on a box that &lt;em&gt;does&lt;/em&gt; have &lt;code&gt;mail&lt;/code&gt;, exit 0 still only means the message reached a local MTA — on a host with no outbound relay it lands in a spool file under &lt;code&gt;/var/mail&lt;/code&gt; that nobody reads. Confirm real delivery once by tailing &lt;code&gt;/var/log/mail.log&lt;/code&gt;. Cron's minimal &lt;code&gt;PATH&lt;/code&gt; is a second reason to test from inside cron rather than your login shell.&lt;/p&gt;
&lt;p&gt;The msmtp relay config and the rate-limiting guard are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-send-email-alert&quot;&gt;bash email alert walkthrough&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28024/every-disk-alert-script-i-had-piped-into-a-command-this-machine-does-not-have</guid>
<pubDate>Thu, 15 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>CloudPloy: Deploy anywhere from your AI</title>
<link>https://coderlegion.com/31122/cloudploy-deploy-anywhere-from-your-ai</link>
<description>&lt;p&gt;&lt;strong&gt;CloudPloy: Deploy anywhere from your AI tool.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add an API key. Your agent deploys from Claude Code, Cursor, or any MCP client. Bring your own Ubuntu/AWS server or provision Hetzner/DigitalOcean/AWS at cost. Flat plan for the control plane; compute at the provider's rate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Free forever (1 small server, 1 app). Starter $9.99 / Pro $19 / Scale $39.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cloudploy.com&quot;&gt;https://cloudploy.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cloudploy.com/docs&quot;&gt;https://cloudploy.com/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MCP: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cloudploy.com/mcp&quot;&gt;https://cloudploy.com/mcp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/31122/cloudploy-deploy-anywhere-from-your-ai</guid>
<pubDate>Wed, 14 Oct 2026 23:17:09 +0000</pubDate>
</item>
<item>
<title>How to get alerted when someone changes your DNS records</title>
<link>https://coderlegion.com/31079/how-to-get-alerted-when-someone-changes-your-dns-records</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Most DNS changes do not take your site down. A new MX record or an edited SPF record usually still resolves, so your uptime checks stay green. To catch these changes, list every record a name should have. Then get an alert when the live answer is different in any way. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/monitor-dns-record-changes?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;original post&lt;/a&gt; has the same text with a short FAQ at the end.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;a-dns-change-does-not-look-like-an-outage&quot;&gt;A DNS change does not look like an outage&lt;/h3&gt;
&lt;p&gt;Uptime monitors wait for something to break. A DNS change often breaks nothing you can see.&lt;/p&gt;
&lt;p&gt;Here is an example. Someone adds a second MX record to your domain. MX records tell other servers where to deliver your email. Maybe it was a test of a new email provider, and nobody removed it. Maybe someone stole the password to your DNS account.&lt;/p&gt;
&lt;p&gt;Each MX record has a priority number, and senders try the lowest one first. If the new record has a lower number than yours, your email now goes to another server first. Your website works, your checks are green, and nobody notices for weeks.&lt;/p&gt;
&lt;p&gt;Other changes work the same way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Someone changes your nameservers at the registrar. Now a different server answers for your whole domain.&lt;/li&gt;
&lt;li&gt;Someone edits your SPF record during a migration. SPF lists the servers that may send email as you, and your newsletter tool is no longer on it.&lt;/li&gt;
&lt;li&gt;Someone deletes your &lt;code&gt;_dmarc&lt;/code&gt; record while cleaning up old records.&lt;/li&gt;
&lt;li&gt;Someone changes your CAA record. Now a different certificate authority can issue certificates for your domain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These changes can leave your DNS lookups and website checks passing.&lt;/p&gt;
&lt;h3 id=&quot;this-happens-in-real-attacks&quot;&gt;This happens in real attacks&lt;/h3&gt;
&lt;p&gt;In January 2019, the US Cybersecurity and Infrastructure Security Agency (CISA) published &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cisa.gov/news-events/directives/ed-19-01-mitigate-dns-infrastructure-tampering-closed&quot;&gt;Emergency Directive 19-01&lt;/a&gt;. Attackers had changed DNS records on several US government domains.&lt;/p&gt;
&lt;p&gt;They obtained credentials for a DNS account. They changed A, MX and NS records to point to their own servers. Then they could read web and email traffic as it passed through.&lt;/p&gt;
&lt;p&gt;CISA's first order to the agencies was simple: check your public DNS records, and make sure each one points to the right place. Start with NS records.&lt;/p&gt;
&lt;p&gt;A check like that tells you the records were right on one day. A monitor does the same check every few minutes, so you find out on the day of the change.&lt;/p&gt;
&lt;h3 id=&quot;why-does-it-resolve-is-not-enough&quot;&gt;Why &quot;does it resolve?&quot; is not enough&lt;/h3&gt;
&lt;p&gt;Most DNS checks ask one of two questions.&lt;/p&gt;
&lt;p&gt;The first is &quot;Does the name resolve?&quot; This catches a deleted record or an &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/domain-expired-but-site-still-up?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;expired domain&lt;/a&gt;. It does not catch a changed record, because a changed record still resolves.&lt;/p&gt;
&lt;p&gt;The second is &quot;Does the answer contain this value?&quot; For example, you enter your mail server, and the check fails if it disappears. This is better. But the check passes as long as your value is somewhere in the answer. So it misses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an extra MX or NS record next to yours;&lt;/li&gt;
&lt;li&gt;an SPF record changed around the part you check;&lt;/li&gt;
&lt;li&gt;one A record replaced, when you have several.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To catch these, the check must know every record that should be there. Then it can fail on anything else.&lt;/p&gt;
&lt;h3 id=&quot;watch-the-exact-record-set&quot;&gt;Watch the exact record set&lt;/h3&gt;
&lt;p&gt;An exact record set is a list of every record that one name should have, for one record type. The check reads the live answer and compares it with your list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A record in your list but not in the answer is &lt;strong&gt;missing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A record in the answer but not in your list is &lt;strong&gt;unexpected&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A changed record shows up as both. The old value is missing and the new value is unexpected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any difference fails the check. The order of the records does not matter, and neither does the TTL.&lt;/p&gt;
&lt;p&gt;To write the list, start from the records your domain has now. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/dns-lookup?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;DNS lookup tool&lt;/a&gt; shows them from Cloudflare and Google side by side. Check that every record is one you expect. If the two resolvers disagree, a change is still spreading, so wait until they match.&lt;/p&gt;
&lt;p&gt;In Uptimepage, create a DNS monitor and choose &lt;strong&gt;exact set&lt;/strong&gt; as the answer match. Write one record per line, the way a DNS answer shows it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;10 mail.example.com.
20 alt.example.com.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Say someone adds a third MX record. The next check fails, and the result shows what changed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;domain&quot;: &quot;example.com&quot;,
  &quot;record_type&quot;: &quot;MX&quot;,
  &quot;missing&quot;: [],
  &quot;unexpected&quot;: [&quot;5 mx.attacker.example.&quot;],
  &quot;answers&quot;: [&quot;10 mail.example.com.&quot;, &quot;20 alt.example.com.&quot;, &quot;5 mx.attacker.example.&quot;],
  &quot;expected_contains&quot;: null,
  &quot;matched&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new MX record has priority 5, lower than yours, so senders try it first.&lt;/p&gt;
&lt;p&gt;The check ignores small differences that would only cause false alarms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upper and lower case in names do not matter, so &lt;code&gt;10 Mail.Example.com&lt;/code&gt; matches &lt;code&gt;10 mail.example.com.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The dot at the end of a name is optional.&lt;/li&gt;
&lt;li&gt;Numbers and IP addresses are compared by value, so &lt;code&gt;2001:0db8::1&lt;/code&gt; matches &lt;code&gt;2001:db8::1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TXT values must match exactly, including case. In SPF and DKIM, every character counts.&lt;/p&gt;
&lt;p&gt;Enter only the record value, without the name, TTL or type. For TXT records, you can paste quoted strings from your zone file, with all parts on one line.&lt;/p&gt;
&lt;h3 id=&quot;which-records-to-watch&quot;&gt;Which records to watch&lt;/h3&gt;
&lt;p&gt;You do not need this for every record. Start with the ones where a quiet change hurts the most.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Record &lt;/th&gt;&lt;th&gt;Name &lt;/th&gt;&lt;th&gt;What it controls&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;NS &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Which servers answer for your whole domain&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MX &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Where your email goes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TXT &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your SPF record: who may send email as you&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TXT &lt;/td&gt;&lt;td&gt;&lt;code&gt;_dmarc.example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your DMARC policy: what receivers do with fake email from your domain&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TXT &lt;/td&gt;&lt;td&gt;&lt;code&gt;selector._domainkey.example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your DKIM public key, which receivers use to check your email signatures&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CAA &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Which certificate authorities may issue certificates for you&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A, AAAA &lt;/td&gt;&lt;td&gt;your server hostnames &lt;/td&gt;&lt;td&gt;Only if your servers have fixed IP addresses&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Your main domain often has other TXT records too, such as verification records from other services. Put all of them in the list. A new one means someone connected a new service to your domain, and you want to know about that.&lt;/p&gt;
&lt;p&gt;One monitor watches one name and one record type. Start with six monitors, one for each of the first six rows. Then add one for each other DKIM selector you use, and one for each fixed A or AAAA record set. A hostname with both an A and an AAAA record needs two monitors.&lt;/p&gt;
&lt;p&gt;If you set up only one, make it NS. CISA put NS first too. Whoever changes your NS records controls all your other records.&lt;/p&gt;
&lt;h3 id=&quot;query-your-own-nameserver&quot;&gt;Query your own nameserver&lt;/h3&gt;
&lt;p&gt;Normally a DNS lookup goes through a resolver, such as &lt;code&gt;1.1.1.1&lt;/code&gt; or the one from your internet provider. The resolver keeps each answer for a while. This time is called the TTL. If your MX record has a TTL of one hour, the resolver can show the old answer for about an hour after a change. Sometimes it is longer: a resolver that cannot reach your nameserver may &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc8767.html#section-1&quot;&gt;keep serving the old answer&lt;/a&gt; until it can.&lt;/p&gt;
&lt;p&gt;You can skip this delay. Set the monitor's resolver to the IP address of your own nameserver, the one that holds your real records (your authoritative nameserver). Then every check sees your records as they are right now.&lt;/p&gt;
&lt;p&gt;There is one exception: NS records. If someone changes your nameservers at the registrar, your own nameserver does not know. It still answers with the old, correct list. So watch NS through a public resolver, such as &lt;code&gt;1.1.1.1&lt;/code&gt; or &lt;code&gt;8.8.8.8&lt;/code&gt;. A public resolver follows the registrar, like your visitors do. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/dns-lookup?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;check what a public resolver sees&lt;/a&gt; for any name before you create the monitor.&lt;/p&gt;
&lt;h3 id=&quot;when-not-to-use-an-exact-set&quot;&gt;When not to use an exact set&lt;/h3&gt;
&lt;p&gt;Some records change on purpose. An exact set on them only gives you false alarms.&lt;/p&gt;
&lt;p&gt;Names behind a CDN or a load balancer get their IP addresses from a pool. The answer can change over time and depending on where you query from. I wrote about this in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/why-dns-returns-different-ip-addresses?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;why DNS returns different IP addresses&lt;/a&gt;. An exact A set on such a name would fail all the time. Watch the CNAME record that points to the CDN instead.&lt;/p&gt;
&lt;p&gt;SOA records change every time you edit your zone, because their serial number goes up. &lt;code&gt;_acme-challenge&lt;/code&gt; TXT records come and go with each certificate renewal. Do not use an exact set for these either.&lt;/p&gt;
&lt;h3 id=&quot;keep-the-list-next-to-your-dns&quot;&gt;Keep the list next to your DNS&lt;/h3&gt;
&lt;p&gt;If you manage DNS with Terraform, put the monitor in the same repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;uptimepage_target&quot; &quot;mx&quot; {
  name     = &quot;mx example.com&quot;
  interval = 300

  check = {
    type = &quot;dns&quot;
    dns = {
      domain           = &quot;example.com&quot;
      record_type      = &quot;MX&quot;
      expected_records = [&quot;10 mail.example.com.&quot;, &quot;20 alt.example.com.&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now a pull request that changes an MX record must change the monitor too. An MX change that nobody reviewed fails the check. You need Terraform provider 0.15.0 or later. Read more in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/monitoring-as-code?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;monitoring as code&lt;/a&gt;. All DNS options are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/docs/monitor-types?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline#dns&quot;&gt;DNS monitor docs&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;where-uptimepage-fits&quot;&gt;Where Uptimepage fits&lt;/h3&gt;
&lt;p&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=cta&quot;&gt;Uptimepage&lt;/a&gt;, an uptime monitor that checks your services from several regions and alerts you when one fails. The exact set match above is part of its &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/docs/monitor-types?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=cta#dns&quot;&gt;DNS monitor&lt;/a&gt;. A DNS change alerts you once the failure is confirmed: by default two failed checks in a row, from more than half of the regions. The result names the missing and the unexpected records, so you see the change without running &lt;code&gt;dig&lt;/code&gt; yourself. It is open source under AGPL, so it can also run on your own server.&lt;/p&gt;
&lt;p&gt;Whatever you use, look up your NS records today and check that every name in the answer is one you expect.&lt;/p&gt;
&lt;p&gt;Now the question for the comments: has a DNS record on one of your domains ever changed without anyone noticing? I would like to hear how you found out, and how long it took.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31079/how-to-get-alerted-when-someone-changes-your-dns-records</guid>
<pubDate>Wed, 14 Oct 2026 14:40:51 +0000</pubDate>
</item>
<item>
<title>Why AI Coding Agents Need a Feature Flag Firewall: Inside Spotify Confidence MCP + ThumbGate</title>
<link>https://coderlegion.com/31077/why-ai-coding-agents-need-feature-flag-firewall-inside-spotify-confidence-mcp-thumbgate</link>
<description>&lt;h2 id=&quot;why-ai-coding-agents-need-a-feature-flag-firewall-inside-spotify-confidence-mcp-thumbgate&quot;&gt;Why AI Coding Agents Need a Feature Flag Firewall: Inside Spotify Confidence MCP + ThumbGate&lt;/h2&gt;
&lt;p&gt;Spotify's experimentation team just announced &lt;strong&gt;&quot;A New Chapter for Confidence&quot;&lt;/strong&gt; (October 14, 2026), launching &lt;strong&gt;Confidence Agent&lt;/strong&gt;, &lt;strong&gt;Session Recordings&lt;/strong&gt;, and official &lt;strong&gt;MCP servers for Claude Code, Cursor, and Codex&lt;/strong&gt; (&lt;code&gt;https://mcp.confidence.dev/mcp/flags&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This is a massive leap forward for agentic software development: instead of a human engineer manually creating flags, setting rollout percentages, and monitoring A/B test sample sizes in a web dashboard, an AI coding agent can do it directly through natural language in their editor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But giving autonomous AI agents direct write access to production feature flags introduces an entirely new catastrophic blast radius.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-three-failure-modes-of-autonomous-flag-mutation&quot;&gt;The Three Failure Modes of Autonomous Flag Mutation&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Premature 100% Rollouts:&lt;/strong&gt;&lt;br&gt;
An autonomous agent refactors a checkout component or backend service. To &quot;verify&quot; its work, it calls &lt;code&gt;set_rollout&lt;/code&gt; to 100% in production before automated integration tests pass or without running a canary slice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A/B Experiment Pollution:&lt;/strong&gt;&lt;br&gt;
An agent modifies user variant allocations, sample sizes, or targeting criteria mid-experiment, destroying statistical validity and costing the business tens of thousands in unrecoverable experimentation bias.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Destructive Flag Purges:&lt;/strong&gt;&lt;br&gt;
During automated dead-code cleanup, an agent purges a legacy feature flag because it cannot find references in one repository, unaware that a mobile client or upstream microservice still relies on the flag schema.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-architecture-pre-action-feature-flag-diodes&quot;&gt;The Architecture: Pre-Action Feature Flag Diodes&lt;/h4&gt;
&lt;p&gt;At &lt;a rel=&quot;nofollow&quot; href=&quot;https://thumbgate.ai&quot;&gt;ThumbGate&lt;/a&gt;, we built the &lt;strong&gt;Pre-Action Infrastructure Firewall for Spotify Confidence MCP&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Before an agent executes any mutation via &lt;code&gt;https://mcp.confidence.dev/mcp/flags&lt;/code&gt;, ThumbGate intercepts the tool call in sub-0.5ms and enforces deterministic governance diodes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ AI Coding Agent: Claude / Cursor / Codex ]
                  â
                  â¼
       [ Proposed MCP Tool Call ]
       (e.g., set_rollout: 100%, flag: &quot;new-checkout&quot;)
                  â
                  â¼
   [ ThumbGate Infrastructure Firewall (&amp;lt;0.5ms) ]
   âââ gate:confidence:rollout_ceiling
   â   ââ Is rollout &amp;gt; 10%? âââº [BLOCK: Require Human Sign-Off]
   âââ gate:confidence:test_proof_required
   â   ââ Tests failing? ââââââº [BLOCK: Require Same-Turn Test Proof]
   âââ gate:confidence:protect_active_experiment
       ââ Active A/B test? ââââº [BLOCK: Experiment Allocation Locked]
                  â
                  â¼ (If Verified &amp;amp; Bounded)
    [ Allowed -&amp;gt; Passthrough to Spotify Confidence API ]
                  â
                  â¼
         [ mcp.confidence.dev ]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;key-pre-action-diodes-implemented&quot;&gt;Key Pre-Action Diodes Implemented&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gate:confidence:rollout_ceiling&lt;/code&gt; (Max 10% Autonomous Canary):&lt;/strong&gt;&lt;br&gt;
Autonomous agents are hard-capped at 10% canary traffic. Expanding beyond 10% requires an explicit operator confirmation token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gate:confidence:test_proof_required&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
Blocks creating or updating feature flags while the repository's CI or local test suite is failing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gate:confidence:protect_active_experiment&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
Protects live A/B tests against autonomous parameter mutation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gate:confidence:prevent_flag_deletion&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
Prevents deleting flags in production environments. Flags must follow an explicit deprecation and archive lifecycle.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;how-to-protect-your-agentic-tooling&quot;&gt;How to Protect Your Agentic Tooling&lt;/h4&gt;
&lt;p&gt;If your engineering organization is adopting agentic workflows, MCP servers, and autonomous coding tools like Cursor, Claude Code, or Codex, &lt;strong&gt;never grant unconstrained write access to production infrastructure without a pre-action firewall&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;ð &lt;strong&gt;Install the ThumbGate Pre-Action Firewall for your team:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://thumbgate.ai/go/pro&quot;&gt;https://thumbgate.ai/go/pro&lt;/a&gt; ($19/mo)&lt;br&gt;
ð &lt;strong&gt;Deploy a verified feature-flag safety diode on your production stack this week:&lt;/strong&gt; Book a &lt;a rel=&quot;nofollow&quot; href=&quot;https://thumbgate.ai/go/diagnostic&quot;&gt;Workflow Hardening Diagnostic ($499)&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31077/why-ai-coding-agents-need-feature-flag-firewall-inside-spotify-confidence-mcp-thumbgate</guid>
<pubDate>Wed, 14 Oct 2026 14:18:03 +0000</pubDate>
</item>
<item>
<title>Alternatives to the DevLabs Pull Request Merge Conflict Extension (2026)</title>
<link>https://coderlegion.com/31072/alternatives-to-the-devlabs-pull-request-merge-conflict-extension-2026</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;Alternatives to the DevLabs Pull Request Merge Conflict Extension (2026)&quot;&lt;br&gt;
published: true&lt;br&gt;
description: &quot;Microsoft's DevLabs merge conflict extension for Azure DevOps is no longer maintained. An honest comparison of the alternatives: resolving locally, your IDE, and a maintained extension.&quot;&lt;br&gt;
tags: azuredevops, git, devops, productivity&lt;br&gt;
canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://jr-devtools.com/guides/devlabs-merge-conflict-extension-alternatives/&quot;&gt;https://jr-devtools.com/guides/devlabs-merge-conflict-extension-alternatives/&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;cover-image-https-jr-devtools-com-merge-conflict-resolver-screenshot-editor-png&quot;&gt;cover_image: &lt;a rel=&quot;nofollow&quot; href=&quot;https://jr-devtools.com/merge-conflict-resolver/screenshot-editor.png&quot;&gt;https://jr-devtools.com/merge-conflict-resolver/screenshot-editor.png&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://jr-devtools.com/guides/devlabs-merge-conflict-extension-alternatives/&quot;&gt;jr-devtools.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For years, the free &lt;em&gt;Pull Request Merge Conflict Extension&lt;/em&gt; from Microsoft DevLabs (&lt;code&gt;ms-devlabs.conflicts-tab&lt;/code&gt;) was how teams resolved conflicts without leaving Azure DevOps. It is no longer maintained. Here's what's going on and what you can use instead.&lt;/p&gt;
&lt;p&gt;Disclosure: we make Merge Conflict Resolver, one of the options below. We've tried to keep this comparison fair; corrections are welcome at &lt;a rel=&quot;nofollow&quot; href=&quot;mailto:support@jr-devtools.com&quot;&gt;support@jr-devtools.com&lt;/a&gt;. Last checked: October 2026.&lt;/p&gt;
&lt;h3 id=&quot;is-the-devlabs-extension-still-supported-&quot;&gt;Is the DevLabs extension still supported?&lt;/h3&gt;
&lt;p&gt;No. Its last update was in November 2023, and Microsoft wrote in the extension's Q&amp;amp;A that &lt;em&gt;“there are no current development resources for this extension”&lt;/em&gt;. Questions like “Is this even supported anymore?” go unanswered. Since 2024 its reviews average about 1.6 out of 5. The most reported problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the tab freezes or keeps “taking longer than expected to load”, and work is lost when it crashes;&lt;/li&gt;
&lt;li&gt;non-ASCII characters (ä, é, č, Hebrew, Chinese …) are changed, especially in UTF-8 and ANSI files;&lt;/li&gt;
&lt;li&gt;you can only take a whole file from one side, not choose per conflict or keep both;&lt;/li&gt;
&lt;li&gt;the resolution lives only in the pull request's merge commit, so reviewers and builds never see it;&lt;/li&gt;
&lt;li&gt;large files such as &lt;code&gt;package-lock.json&lt;/code&gt;, or many conflicts, make it hang.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It still works for some teams on simple conflicts, so there's no need to remove it in a hurry. But if you run into the problems above, there won't be a fix.&lt;/p&gt;
&lt;h3 id=&quot;the-alternatives&quot;&gt;The alternatives&lt;/h3&gt;
&lt;h4 id=&quot;1-resolve-locally-with-git-free-&quot;&gt;1. Resolve locally with Git (free)&lt;/h4&gt;
&lt;p&gt;Merge the target branch into your source branch, resolve in your editor, and push. See our &lt;a rel=&quot;nofollow&quot; href=&quot;https://jr-devtools.com/guides/resolve-merge-conflicts-azure-devops/&quot;&gt;step-by-step guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; free, always works, you can build and test before pushing. &lt;strong&gt;Less good:&lt;/strong&gt; you need the repository cloned and a working Git setup; slow for a two-line fix or for someone else's pull request.&lt;/p&gt;
&lt;h4 id=&quot;2-your-ide-s-git-integration-free-&quot;&gt;2. Your IDE's Git integration (free)&lt;/h4&gt;
&lt;p&gt;Visual Studio, VS Code and JetBrains IDEs all have 3-way merge editors. In Visual Studio, check out the source branch, merge the target branch from &lt;em&gt;Git → Manage Branches&lt;/em&gt;, and resolve in the merge editor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; comfortable editor with IntelliSense. &lt;strong&gt;Less good:&lt;/strong&gt; same as locally: you need a clone, and it's per developer, not something a reviewer can do from the pull request.&lt;/p&gt;
&lt;h4 id=&quot;3-merge-conflict-resolver-free-plan-pro-from-12-per-organization-per-month-&quot;&gt;3. Merge Conflict Resolver (free plan, Pro from $12 per organization per month)&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://jr-devtools.com/merge-conflict-resolver/&quot;&gt;Merge Conflict Resolver&lt;/a&gt; is an independent, actively maintained extension that adds a &lt;strong&gt;Resolve conflicts&lt;/strong&gt; tab to pull requests, built to fix the problems above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3-way editor (Monaco, the editor of VS Code) with &lt;em&gt;Accept target / source / both&lt;/em&gt; per conflict;&lt;/li&gt;
&lt;li&gt;drafts are saved in the browser, so a reload or crash never loses work;&lt;/li&gt;
&lt;li&gt;UTF-8, UTF-8 with BOM, UTF-16 and Windows-1252 files and their line endings are written back byte for byte;&lt;/li&gt;
&lt;li&gt;edit/delete, renames, binary files, Git LFS and submodules each get a clear choice;&lt;/li&gt;
&lt;li&gt;stays responsive with hundreds of conflicts and multi-megabyte lockfiles;&lt;/li&gt;
&lt;li&gt;Pro: commit the resolution to the source branch, a summary comment on the pull request, bulk and whitespace auto-resolve.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; no clone needed, anyone on the pull request can resolve. &lt;strong&gt;Less good:&lt;/strong&gt; Azure DevOps Services only for now (not Azure DevOps Server); Pro is paid. The free plan covers 10 pull requests per organization per month, and every organization gets a 14-day Pro trial.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://jr-devtools.com/merge-conflict-resolver/screenshot-editor.png&quot; alt=&quot;Merge Conflict Resolver's 3-way editor in an Azure DevOps pull request&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;DevLabs extension&lt;/th&gt;&lt;th&gt;Locally / IDE&lt;/th&gt;&lt;th&gt;Merge Conflict Resolver&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Resolve in the pull request, no clone| ✔| –| ✔&lt;br&gt;
Choose per conflict| Whole file only| ✔| ✔&lt;br&gt;
Work survives a reload or crash| –| ✔| ✔&lt;br&gt;
Encodings kept intact| Reported to break| Depends on editor| ✔&lt;br&gt;
Reviewers and builds see the resolution| –| ✔| ✔ (Pro)&lt;br&gt;
Maintained| No (last update 2023)| ✔| ✔&lt;br&gt;
Price| Free| Free| Free plan; Pro $12/month per organization&lt;/p&gt;
&lt;h3 id=&quot;switching-from-the-devlabs-extension&quot;&gt;Switching from the DevLabs extension&lt;/h3&gt;
&lt;p&gt;Both extensions can be installed side by side, so you can try before you remove anything. An organization admin installs Merge Conflict Resolver from the Visual Studio Marketplace; there's nothing to configure, and the 14-day Pro trial starts the first time someone opens the tab.&lt;/p&gt;
&lt;h3 id=&quot;related-guides&quot;&gt;Related guides&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://jr-devtools.com/guides/resolve-merge-conflicts-azure-devops/&quot;&gt;How to resolve merge conflicts in an Azure DevOps pull request&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://jr-devtools.com/merge-conflict-resolver/&quot;&gt;Merge Conflict Resolver: features, pricing and free plan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Not affiliated with Microsoft. “Azure DevOps”, “Excel” and “Office” are trademarks of Microsoft.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31072/alternatives-to-the-devlabs-pull-request-merge-conflict-extension-2026</guid>
<pubDate>Wed, 14 Oct 2026 13:44:34 +0000</pubDate>
</item>
<item>
<title>One empty variable, one error, one wrong answer, exit 0</title>
<link>https://coderlegion.com/29670/one-empty-variable-one-error-one-wrong-answer-exit-0</link>
<description>&lt;p&gt;I wrote four lines to capture a bash error for a guide. One empty variable, tested twice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ cat queue-check.sh
#!/bin/bash
count=&quot;&quot;
if [ $count -eq 0 ]; then echo &quot;queue empty&quot;; fi
if [[ $count -eq 0 ]]; then echo &quot;queue empty ([[ ]])&quot;; fi

$ bash queue-check.sh
queue-check.sh: line 3: [: -eq: unary operator expected
queue empty ([[ ]])
exit=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The message blames &lt;code&gt;-eq&lt;/code&gt;. The bug is the unquoted &lt;code&gt;$count&lt;/code&gt; before it: word splitting removed the empty value, so &lt;code&gt;[&lt;/code&gt; received &lt;code&gt;-eq 0&lt;/code&gt; and failed with status 2, which an &lt;code&gt;if&lt;/code&gt; treats as &quot;not true&quot;. The &lt;code&gt;[[ ]]&lt;/code&gt; line does no splitting, read the empty string as 0 in an arithmetic test, and printed a confident wrong answer. Then the script exited 0.&lt;/p&gt;
&lt;p&gt;Strict mode does not help here. &lt;code&gt;count=&quot;&quot;&lt;/code&gt; is set, so &lt;code&gt;set -u&lt;/code&gt; stays quiet, and &lt;code&gt;set -e&lt;/code&gt; ignores failures inside an &lt;code&gt;if&lt;/code&gt; condition. What stopped it was validating the value first: &lt;code&gt;[[ $count =~ ^[0-9]+$ ]] || exit 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Most bash errors work this way. The message names the check that failed, not the cause: a syntax error reported on line 4 for a missing semicolon on line 2, &lt;code&gt;Permission denied&lt;/code&gt; after &lt;code&gt;chmod +x&lt;/code&gt; on a &lt;code&gt;noexec&lt;/code&gt; mount. Nine of them, each reproduced with the check that names the cause, are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-error-messages&quot;&gt;Bash Error Messages, Decoded&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29670/one-empty-variable-one-error-one-wrong-answer-exit-0</guid>
<pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
</item>
</channel>
</rss>