<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged lazysre</title>
<link>https://coderlegion.com/tag/lazysre</link>
<description>Powered by Question2Answer</description>
<item>
<title>Lazy SRE's guide to secure systems, part 2: the actions you didn't pin</title>
<link>https://coderlegion.com/19914/lazy-sres-guide-to-secure-systems-part-2-the-actions-you-didnt-pin</link>
<description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://harshit.cloud/blog/lazy-security-part-2-github-actions&quot;&gt;harshit.cloud&lt;/a&gt; on 2026-04-12.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Last March, someone with write access to the &lt;code&gt;trivy-action&lt;/code&gt; repo rewrote 76 of its 77 version tags in place. The tags still resolved to &lt;code&gt;aquasecurity/trivy-action&lt;/code&gt; — they just resolved to different commits than they did the week before. Every pipeline that ran &lt;code&gt;aquasecurity/trivy-action@0.20.0&lt;/code&gt; (and every other tagged version) ran the attacker's commit instead. Secrets exfiltrated. The stolen credentials chained into PyPI and took down LiteLLM. Nobody noticed for hours, because the workflow file diff was still clean.&lt;/p&gt;
&lt;p&gt;This is part 2. &lt;a rel=&quot;nofollow&quot; href=&quot;/blog/lazy-security-part-1-supply-chain&quot;&gt;Part 1&lt;/a&gt; covered npm: the dependencies you didn't read. Part 2 is the same problem one level up: the workflows you didn't pin. Part 3 is the unsexy list — Tailscale, PrivateLink, IAP, the PAT you forgot.&lt;/p&gt;
&lt;p&gt;The thesis from Part 1 stands. The best security work for a small team is the work &lt;em&gt;Future You at 3am&lt;/em&gt; will actually execute. The configuration that makes the wrong thing impossible beats the runbook that only discourages it. With GitHub Actions, &quot;the wrong thing&quot; has gotten very specific over the last twelve months, and the configs to block each variety have gotten correspondingly precise.&lt;/p&gt;
&lt;h3 id=&quot;pinning-is-necessary-but-not-sufficient&quot;&gt;pinning is necessary but not sufficient&lt;/h3&gt;
&lt;p&gt;The first thing the trivy-action incident proves: hash-pinning to &lt;code&gt;@0.20.0&lt;/code&gt; is not pinning. It's a name lookup. The owner of the repo is allowed to rewrite that name. The pin you actually wanted was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- uses: aquasecurity/trivy-action@9b9a3f5c8a5c7e1b6e4d2f1c9b8a7e6d5c4b3a2f # v0.20.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full forty-character SHA. Immutable. The version comment is so the next reader knows what they're looking at; the SHA is so the workflow runs the code you reviewed.&lt;/p&gt;
&lt;p&gt;Two GitHub features shipped in 2025 that change the math:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SHA pinning enforcement&lt;/strong&gt; (Aug 2025). An org-level policy that &lt;em&gt;fails&lt;/em&gt; workflow runs using unpinned actions, instead of warning about them. Settings → Actions → General → Action pinning. Turn it on. There is no &quot;we'll get to it&quot; version of this toggle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Immutable Releases&lt;/strong&gt; (Oct 2025, GA). Action authors opt in to making release tags non-rewritable after publication. If you publish actions, turn this on for downstream consumers. If you consume actions, prefer ones that have.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The lazy stance: enforcement at the org level. The workflow that doesn't have a forty-character SHA fails the run. The PR can't merge. The work of remembering to pin moves from every engineer's head to one setting.&lt;/p&gt;
&lt;p&gt;What this doesn't catch: an attacker who compromises the maintainer account and ships a new tag at a new SHA. The SHA is real. Pinning by SHA doesn't help, because the workflow author &lt;em&gt;will&lt;/em&gt; rev to the new version when they read the maintainer's release notes. Which is the next config.&lt;/p&gt;
&lt;h3 id=&quot;cooldown-is-the-same-trick-that-worked-for-npm&quot;&gt;cooldown is the same trick that worked for npm&lt;/h3&gt;
&lt;p&gt;Part 1's load-bearing config was &lt;code&gt;SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=48&lt;/code&gt;. The principle: most published malware is detected and pulled within hours. If you can wait, the wait does the work for you.&lt;/p&gt;
&lt;p&gt;The action ecosystem has the same property, with a longer window. &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns&quot;&gt;yossarian's analysis&lt;/a&gt; puts the cooldown that catches most supply-chain attacks at 7-14 days. So:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pinact --min-age 7 .github/workflows/*.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Refuses to write a pin younger than seven days. Add to pre-commit, your CI lint, or whatever your dependabot equivalent runs before opening the bump PR.&lt;/p&gt;
&lt;p&gt;For Renovate users, the equivalent lives in the action manager:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;packageRules&quot;: [
    { &quot;matchManagers&quot;: [&quot;github-actions&quot;], &quot;minimumReleaseAge&quot;: &quot;7 days&quot; }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it. Same trick, different ecosystem.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://harshit.cloud/images/lazy-security-part-2-github-actions/cooldown-window.gif&quot; alt=&quot;An animated horizontal bar chart in a dark editorial palette showing the share of recent supply-chain action compromises caught by a cooldown of 0, 3, 7, 14, or 21 days. The 0-day bar lands at 3% and the 3-day bar at 38%. The 7-day bar reaches 76% and the 14-day bar reaches 89%, both accented with a brighter cyan and a coral tip. The 21-day bar lands at 94%. A bottom strip notes that the trivy-action force-push was detected at about nine days.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fig. 2 — the wait is doing the work. Seven days closes most of the door; fourteen closes most of the rest.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The empirical question is whether seven days is enough. The trivy-action force-push was detected at about nine — seven would have caught most consumers, not all of them. The cost of fourteen is &quot;your action versions lag upstream by two weeks.&quot; If your action surface is small (most teams are running &lt;code&gt;actions/checkout&lt;/code&gt;, &lt;code&gt;actions/setup-node&lt;/code&gt;, one cloud-login action, maybe a deploy action), set fourteen and forget.&lt;/p&gt;
&lt;h3 id=&quot;pull-request-target-is-the-new-postinstall&quot;&gt;pull_request_target is the new postinstall&lt;/h3&gt;
&lt;p&gt;Part 1 named &lt;code&gt;postinstall&lt;/code&gt; as the single trigger that does the most damage and the single switch (&lt;code&gt;ignore-scripts=true&lt;/code&gt;) that closes the most doors. Actions has the same shape and the same fix.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pull_request_target&lt;/code&gt; runs in the context of the base repository, with access to repository secrets, but is triggered by a PR from a fork. The legitimate use case is small: comment on PRs, label them, run lightweight metadata jobs. The illegitimate use case is enormous: check out the fork's code and execute it. The attack writes itself. Open a fork, modify a script the trusted workflow runs, watch the runner exfiltrate every secret in the env.&lt;/p&gt;
&lt;p&gt;Astral, who maintain &lt;code&gt;uv&lt;/code&gt; and &lt;code&gt;ruff&lt;/code&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://astral.sh/blog/open-source-security-at-astral&quot;&gt;wrote it cleanly&lt;/a&gt;: &quot;these triggers are almost impossible to use securely.&quot; GitHub partially mitigated this in November 2025 by forcing &lt;code&gt;pull_request_target&lt;/code&gt; to always use the default branch's version of the workflow, so an attacker can't push a vulnerable workflow on a feature branch and trigger it. But the foot-cannon still ships loaded if your default-branch workflow checks out PR-head code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://harshit.cloud/images/lazy-security-part-2-github-actions/pull-request-target-contrast.png&quot; alt=&quot;A hand-drawn two-panel napkin. Left panel labeled 'pull_request_target' shows a fork PR boundary as a dashed line, a modified script.sh inside the fork, and a runner on the base side reaching across the boundary while holding a red keyring labeled NPM_TOKEN, AWS_KEY, GH_PAT. Right panel labeled 'pull_request' shows the same setup, but the keyring is replaced by a greyed-out 'secrets.* not in scope' bag. The two panels are structurally identical except for the presence or absence of secrets in the runner.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fig. 3 — same workflow, different trigger, opposite blast radius.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The lazy stance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Don't use &lt;code&gt;pull_request_target&lt;/code&gt; unless you've named the specific reason and one other person has signed off.&lt;/li&gt;
&lt;li&gt;If you do, never &lt;code&gt;actions/checkout&lt;/code&gt; the PR head from inside it. Check out the base SHA, do the metadata thing, exit.&lt;/li&gt;
&lt;li&gt;For everything else, use &lt;code&gt;pull_request&lt;/code&gt;. It runs without secrets. Attacker-controlled code stays attacker-jailed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Same shape as &lt;code&gt;ignore-scripts=true&lt;/code&gt;. The setting that closes the class.&lt;/p&gt;
&lt;h3 id=&quot;the-safe-defaults-that-go-in-every-workflow&quot;&gt;the safe defaults that go in every workflow&lt;/h3&gt;
&lt;p&gt;The four-line workflow header that does the most work per character:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;permissions:
  contents: read

defaults:
  run:
    shell: bash -euo pipefail {0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;contents: read&lt;/code&gt; overrides the org-level default. If a step needs to push a tag or open a PR, that job opts back up to &lt;code&gt;contents: write&lt;/code&gt; explicitly. The default is the safe one.&lt;/p&gt;
&lt;p&gt;At the checkout step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- uses: actions/checkout@&amp;lt;sha&amp;gt; # v4.2.0
  with:
    persist-credentials: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default behavior of &lt;code&gt;actions/checkout&lt;/code&gt; is to leave a credential sitting in &lt;code&gt;.git/config&lt;/code&gt; for the rest of the workflow. Later steps have shipped this credential into uploaded artifacts more than once. Opt out unless a later step in the same job needs to push.&lt;/p&gt;
&lt;p&gt;Three secret-access rules with the same flavor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Step-scoped &lt;code&gt;env:&lt;/code&gt;, never workflow-scoped, for any secret.&lt;/li&gt;
&lt;li&gt;Never &lt;code&gt;${{ toJson(secrets) }}&lt;/code&gt;. Exposes every secret in the project to the runner. There is no use case.&lt;/li&gt;
&lt;li&gt;Never &lt;code&gt;secrets: inherit&lt;/code&gt; on reusable workflows. Pass each secret by name. The reusable workflow gets exactly what it asked for.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trivy-action exfiltration worked partly because secrets were workflow-scoped. The malicious step inherited every credential in the env, not just the one the legitimate scan needed. Step-scoping wouldn't have prevented the credential theft — but it would have bounded the blast radius to one secret instead of all of them.&lt;/p&gt;
&lt;h3 id=&quot;oidc-the-promise-from-part-1&quot;&gt;OIDC, the promise from part 1&lt;/h3&gt;
&lt;p&gt;Part 1 ended on &quot;the next-tier defenses are real, Part 3 names them.&quot; OIDC is the part of that conversation that lives here.&lt;/p&gt;
&lt;p&gt;The trade: instead of storing an &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; in repo secrets and praying nobody exfiltrates it, you configure AWS to trust GitHub's OIDC issuer for a specific repo, branch, and workflow. GitHub mints a short-lived (five-minute) OIDC identity token for the workflow run. The workflow trades that for STS credentials whose lifetime you set (default one hour). Nothing long-lived ever sits in the env.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@&amp;lt;sha&amp;gt; # v4.0.2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-deploy
          aws-region: us-east-1
      - run: aws s3 sync ./dist s3://my-bucket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The role's trust policy restricts the OIDC subject to your exact repo and (ideally) branch. An attacker who compromises a fork PR can't assume the role, because they don't match the trust condition. The OIDC JWT itself lasts five minutes and the STS credential is scoped to whatever you configure (default one hour). Even an exfiltrated credential gets the attacker a bounded window of scoped access, not a permanent IAM user.&lt;/p&gt;
&lt;p&gt;For Google Cloud, the equivalent is Workload Identity Federation. For HashiCorp Vault, the JWT auth backend. Same shape across providers.&lt;/p&gt;
&lt;p&gt;The labor here is genuinely one-time. Configure the trust relationship once per repo, delete the long-lived key, forget about rotation forever. The rotation runbook you're not maintaining is one of the better quiet wins in this post.&lt;/p&gt;
&lt;h3 id=&quot;zizmor-is-the-local-proxy-for-workflows&quot;&gt;zizmor is the local proxy for workflows&lt;/h3&gt;
&lt;p&gt;Part 1's &lt;code&gt;safe-chain&lt;/code&gt; sat in front of every package install and refused malware before bytes hit disk. The action ecosystem's equivalent is &lt;code&gt;zizmor&lt;/code&gt; — a workflow linter that reads your YAML and catches the patterns this post is about, before they merge.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install zizmor
zizmor .github/workflows/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It catches unpinned actions, &lt;code&gt;pull_request_target&lt;/code&gt; with PR-head checkouts, template-injection patterns where attacker-controlled input lands in a &lt;code&gt;run:&lt;/code&gt; string, jobs with excessive permissions. Add it to pre-commit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .pre-commit-config.yaml
- repo: https://github.com/woodruffw/zizmor-pre-commit
  rev: v1.x  # pin the rev, obviously
  hooks:
    - id: zizmor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The principle is identical to safe-chain. Move the security check from &quot;after the incident, in the postmortem&quot; to &quot;before the PR can merge, on the dev machine.&quot; The CI run is the second line of defense. The pre-commit is the first.&lt;/p&gt;
&lt;h3 id=&quot;the-receipts&quot;&gt;the receipts&lt;/h3&gt;
&lt;p&gt;The above stack is approximately one afternoon: org-level SHA pinning enforcement, &lt;code&gt;pinact --min-age 7&lt;/code&gt; or Renovate &lt;code&gt;minimumReleaseAge: 7 days&lt;/code&gt;, the four-line workflow header, &lt;code&gt;persist-credentials: false&lt;/code&gt;, no &lt;code&gt;pull_request_target&lt;/code&gt; with PR-head checkouts, OIDC for every cloud credential, &lt;code&gt;zizmor&lt;/code&gt; in pre-commit.&lt;/p&gt;
&lt;p&gt;It will not catch a maintainer-account compromise that ships clean-looking code which activates weeks later. It will not catch a determined attacker who studies your build and writes a payload that survives every linter and looks innocent at PR review. Nothing in this post will. Part 3 will name the controls that buy partial mitigation against that class: sigstore, npm provenance, reproducible builds, attested deployments. And the ones that exist to make the postmortem readable, not to prevent the incident.&lt;/p&gt;
&lt;p&gt;For a small team, the delta from this post is moving from &quot;we're one tag-rewrite away from a credential theft cascade&quot; to &quot;an attacker would need a credentialed insider, or a fifteen-minute window of luck against a scoped IAM role.&quot; That's the only delta that matters at this scale.&lt;/p&gt;
&lt;p&gt;If you do one thing this week, turn on SHA pinning enforcement at the org level. Everything else gates off that.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19914/lazy-sres-guide-to-secure-systems-part-2-the-actions-you-didnt-pin</guid>
<pubDate>Sun, 07 Jun 2026 21:55:44 +0000</pubDate>
</item>
<item>
<title>Lazy SRE's guide to secure systems, part 1: the dependencies you didn't read</title>
<link>https://coderlegion.com/19913/lazy-sres-guide-to-secure-systems-part-1-the-dependencies-you-didnt-read</link>
<description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://harshit.cloud/blog/lazy-security-part-1-supply-chain&quot;&gt;harshit.cloud&lt;/a&gt; on 2026-04-05.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A few months ago a friend's CI pipeline tried to install a package none of us had heard of. The build failed. The error wasn't a missing dep. The error was a local proxy saying &lt;em&gt;this is malware, I'm not letting it touch disk&lt;/em&gt;. The package was a transitive dependency, six levels deep, that had been published to npm 38 minutes earlier. Nobody on the team had asked for it.&lt;/p&gt;
&lt;p&gt;I do platform work at a startup. The job is: keep production up, keep the bill down, keep the kind of person who reads HN comments from getting a free shell on the cluster. The thesis for the series is short. The best security work for a small team is the work &lt;em&gt;Future You at 3am&lt;/em&gt; will actually execute. The lazy answer is also, almost always, the right one: the configuration that makes the wrong thing impossible, rather than only discouraged. This is part 1. Part 2 is GitHub (&lt;code&gt;tj-actions&lt;/code&gt;, OIDC, fork PRs). Part 3 is the unsexy list (IAP, Tailscale, PrivateLink, Okta, default helm creds, the PAT you forgot).&lt;/p&gt;
&lt;h3 id=&quot;the-picture&quot;&gt;the picture&lt;/h3&gt;
&lt;p&gt;You opened your editor, ran &lt;code&gt;npm install&lt;/code&gt;, and onboarded somewhere between 800 and 2,000 packages maintained by people you have never audited. Your team reviewed five direct dependencies. The other 1,995 came in for the ride.&lt;/p&gt;
&lt;p&gt;The job is not to read all 1,995. The job is to make sure that when one of them is the problem, the blast radius is small and the alarm goes off.&lt;/p&gt;
&lt;h3 id=&quot;chainjacking&quot;&gt;chainjacking&lt;/h3&gt;
&lt;p&gt;Chainjacking is the umbrella term for &quot;someone got control of a package you depend on and pushed a bad version.&quot; The attacker doesn't break npm. They get the credentials of the human who publishes the package, ship a patch version, and semver puts it on your machine the next time anyone runs &lt;code&gt;npm install&lt;/code&gt;. &lt;code&gt;event-stream&lt;/code&gt; (2018), &lt;code&gt;ua-parser-js&lt;/code&gt; / &lt;code&gt;coa&lt;/code&gt; / &lt;code&gt;rc&lt;/code&gt; (2021), &lt;code&gt;lottie-player&lt;/code&gt; (2024), and the shai-hulud worm (Sept 2025, with a 2.0 wave in Nov 2025) that self-replicated by stealing tokens from compromised maintainer machines via TruffleHog-style secret scans and then republishing every other package that maintainer owned. The economics still work for the attacker. It is going to keep happening.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://harshit.cloud/images/lazy-security-part-1-supply-chain/chainjacking-timeline.png&quot; alt=&quot;A magazine-infographic-style timeline on a dark navy background. Six stages from left to right: T-7d maintainer account targeted, T-0 malicious version published, T+12m first CI installs it, T+12m02s secrets exfiltrated, T+1h backdoor in artifacts, T+24h credentials for sale. Stages 1-2 highlighted in coral as 'compromise' stages; stages 3-6 in cyan as 'victim' stages.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fig. 2 — twenty-four hours from maintainer phish to credential resale. Nobody noticed the version bump.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;What matters in that timeline is that the &lt;em&gt;human&lt;/em&gt; steps are slow and the &lt;em&gt;automated&lt;/em&gt; steps are fast. The window between &quot;malicious version published&quot; and &quot;your CI runs &lt;code&gt;npm install&lt;/code&gt;&quot; is whatever your dependabot cron is. If you auto-merge minor and patch bumps, that window is ninety seconds.&lt;/p&gt;
&lt;h3 id=&quot;dependency-confusion&quot;&gt;dependency confusion&lt;/h3&gt;
&lt;p&gt;You have a private package called &lt;code&gt;internal-utils&lt;/code&gt;. Your CI is configured with both your private registry and the public npm registry. Somebody publishes &lt;code&gt;internal-utils&lt;/code&gt; to public npm at a higher version. CI installs the public one. Birsan did this against Apple, Microsoft, Tesla, and PayPal in a weekend in 2021 for bug bounties.&lt;/p&gt;
&lt;p&gt;Fix, in &lt;code&gt;.npmrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;@yourorg:registry=https://npm.yourorg.internal
registry=https://registry.npmjs.org/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scope everything internal. Register your scope on public npm as a parked placeholder. It costs nothing.&lt;/p&gt;
&lt;h3 id=&quot;postinstall&quot;&gt;postinstall&lt;/h3&gt;
&lt;p&gt;Most install-time npm supply-chain incidents I have read the postmortem on shipped their malicious code in a &lt;code&gt;postinstall&lt;/code&gt; script — not in runtime code. (Some recent ones, like the chalk/debug compromise of Sept 2025, activate at runtime in the browser or on first import; the switch below doesn't help against those. It does help against the install-time class, which is still the majority.) The install hook runs before your tests, before your linter, as part of the install. Default is enabled. The one-line change with the highest blast-radius reduction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# .npmrc
ignore-scripts=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You'll need to allowlist two to five packages that genuinely need it (typically &lt;code&gt;bcrypt&lt;/code&gt;-shaped things). That number is small. The alternative is letting every package run code on install. Pick.&lt;/p&gt;
&lt;h3 id=&quot;the-install-that-buys-the-most&quot;&gt;the install that buys the most&lt;/h3&gt;
&lt;p&gt;Aikido's &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/AikidoSec/safe-chain&quot;&gt;&lt;code&gt;safe-chain&lt;/code&gt;&lt;/a&gt; is an open-source local proxy that sits in front of &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;npx&lt;/code&gt;, &lt;code&gt;yarn&lt;/code&gt;, &lt;code&gt;pnpm&lt;/code&gt;, &lt;code&gt;pnpx&lt;/code&gt;, &lt;code&gt;bun&lt;/code&gt;, &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, &lt;code&gt;poetry&lt;/code&gt;, &lt;code&gt;pipx&lt;/code&gt;. Every package download is intercepted and checked against Aikido Intel, an open malware feed. Malware is blocked before bytes hit disk. Which is before &lt;code&gt;postinstall&lt;/code&gt; runs. Free. No account.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://harshit.cloud/images/lazy-security-part-1-supply-chain/safe-chain-flow.png&quot; alt=&quot;A clean dark-editorial flow diagram. Five columns from left to right: developer terminal running 'npm install lodash-utils', a shell alias intercepting the command, a local proxy that all package downloads route through, the Aikido Intel cloud queried for malware reputation, and an outcome column with a green 'allowed → installed' branch and a red 'blocked → install aborted' branch.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fig. 3 — safe-chain in one picture. A local proxy in front of every package manager, checked against an open threat-intel feed.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;On a dev machine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh
# restart your shell
npm safe-chain-verify
# expected: OK: Safe-chain works!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In CI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Install safe-chain
  run: |
    curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh \
      | sh -s -- --install-dir /usr/local/.safe-chain
    echo &quot;$HOME/.safe-chain/bin&quot; &amp;gt;&amp;gt; &quot;$GITHUB_PATH&quot;
- run: npm ci
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, the part that quietly does the most work — refuse to install anything younger than 48 hours, because that's the window in which most npm malware is caught and removed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=48
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-receipts-1&quot;&gt;the receipts&lt;/h3&gt;
&lt;p&gt;The above stack is approximately one afternoon of work: &lt;code&gt;npm ci&lt;/code&gt; from a committed lockfile, &lt;code&gt;ignore-scripts=true&lt;/code&gt; with a tiny allowlist, scoped private packages with locked registry resolution, safe-chain in front of every install, minimum package age of 48 hours. It will catch most known-bad packages, kill dependency-confusion at the registry level, and reduce postinstall blast radius to zero for the long tail. It will not catch a maintainer-account compromise that ships clean-looking malware that only activates in production weeks later. Nothing in this post will. The next-tier defenses (sigstore signing, npm provenance, reproducible builds) are real, and Part 3 will name them.&lt;/p&gt;
&lt;p&gt;For a startup the delta from this post is moving from &quot;one of the next ten incidents has a non-trivial chance of being yours&quot; to &quot;you would have to be very unlucky.&quot; That's the only delta that matters.&lt;/p&gt;
&lt;p&gt;If you do one thing this week, go register your npm scope.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;diagrams-what-i-tried&quot;&gt;diagrams: what i tried&lt;/h3&gt;
&lt;p&gt;Three diagrams, three different tools, one brief: &quot;explain a supply-chain attack to a tired SRE in one image.&quot; Prompts kept short. Results below.&lt;/p&gt;
&lt;h4 id=&quot;1-the-napkin-contrast-coleam00-excalidraw-diagram-skill-&quot;&gt;1 — the napkin contrast (coleam00 excalidraw-diagram skill)&lt;/h4&gt;
&lt;p&gt;Brief: &lt;em&gt;&quot;two-panel hand-drawn napkin. Left panel 'what i installed': five direct deps off a 'your app' box. Right panel 'what npm install pulled in': same five direct deps, transitive sprawl under each, one of them is a red &lt;code&gt;evil-helper@1.2.3&lt;/code&gt;, callout reads 'shipping crypto miners.'&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Result: &lt;a rel=&quot;nofollow&quot; href=&quot;/images/lazy-security-part-1-supply-chain/dependency-tree-contrast.png&quot;&gt;Fig. 1&lt;/a&gt;. Built with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/coleam00/excalidraw-diagram-skill&quot;&gt;&lt;code&gt;excalidraw-diagram&lt;/code&gt;&lt;/a&gt; skill, a Claude Code skill that enforces a design methodology (depth assessment → pattern mapping → evidence artifacts → mandatory render-and-validate loop) and ships a Playwright-based renderer. The &lt;code&gt;.excalidraw&lt;/code&gt; source is &lt;a rel=&quot;nofollow&quot; href=&quot;/images/lazy-security-part-1-supply-chain/dependency-tree-contrast.excalidraw&quot;&gt;downloadable&lt;/a&gt;; open it on excalidraw.com to edit.&lt;/p&gt;
&lt;p&gt;Two things the skill produced that I wouldn't have prompted for: a semantic color palette (Start/Trigger orange for &quot;your app&quot;, Error red for the malicious package, Inactive blue-dashed for the &quot;+N more&quot; bags), and a summary-flow strip at the bottom (&lt;code&gt;5 direct → 1,200 transitive → 1 malicious → full keychain&lt;/code&gt;) that compresses the post's thesis into nine words. The methodology turns a drawing into an argument.&lt;/p&gt;
&lt;p&gt;One install gotcha worth knowing: the skill loads Excalidraw via ESM and the default CDN (&lt;code&gt;esm.sh&lt;/code&gt;) was unreachable from my environment. One-line patch in &lt;code&gt;render_template.html&lt;/code&gt; to use &lt;code&gt;cdn.jsdelivr.net&lt;/code&gt; and it worked.&lt;/p&gt;
&lt;p&gt;Verdict: &lt;strong&gt;won for the hero.&lt;/strong&gt; The Excalidraw aesthetic earns a place when a post needs a punchline; the skill's methodology adds the second zoom level that elevates a punchline into something that teaches.&lt;/p&gt;
&lt;h4 id=&quot;2-the-chainjacking-timeline-diagram-design-polished-editorial-&quot;&gt;2 — the chainjacking timeline (diagram-design, polished editorial)&lt;/h4&gt;
&lt;p&gt;Brief: &lt;em&gt;&quot;Horizontal six-stage timeline. Dark navy &lt;code&gt;#11141c&lt;/code&gt; ground, coral &lt;code&gt;#ff6b5a&lt;/code&gt; for the two 'compromise' stages, muted cyan &lt;code&gt;#5bc0d9&lt;/code&gt; for the four 'victim' stages. Each stage: small timestamp label (T-7d, T-0, T+12m, T+12m02s, T+1h, T+24h), a node on the line, a short title, one-line caption. Title 'a chainjacking attack, in six steps' (lowercase). Italic figcaption. Magazine-infographic feel, no neon, no scanlines.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Result: &lt;a rel=&quot;nofollow&quot; href=&quot;/images/lazy-security-part-1-supply-chain/chainjacking-timeline.png&quot;&gt;Fig. 2&lt;/a&gt;. Built by a diagram-design subagent. Came back with a ghosted &quot;COMPROMISE / VICTIM&quot; phase label in the background that I hadn't asked for and now wouldn't part with.&lt;/p&gt;
&lt;p&gt;Verdict: &lt;strong&gt;won for explaining attacker workflow.&lt;/strong&gt; Editorial polish without being a dashboard. The agent's improvisation (the phase label) was the part I would not have prompted my way to.&lt;/p&gt;
&lt;h4 id=&quot;3-the-safe-chain-flow-diagram-design-five-column-system-diagram-&quot;&gt;3 — the safe-chain flow (diagram-design, five-column system diagram)&lt;/h4&gt;
&lt;p&gt;Brief: &lt;em&gt;&quot;Five columns left to right: developer terminal running &lt;code&gt;npm install lodash-utils&lt;/code&gt;, shell alias intercepting, local proxy as the focal column, Aikido Intel cloud with a lookup arrow, outcome column with a green 'installed' branch and a red 'install aborted' branch. Dark editorial background &lt;code&gt;#0d1117&lt;/code&gt;. Cyan normal flow, red blocked, green allowed. Legend strip at the bottom.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Result: &lt;a rel=&quot;nofollow&quot; href=&quot;/images/lazy-security-part-1-supply-chain/safe-chain-flow.png&quot;&gt;Fig. 3&lt;/a&gt;. Same skill as #2. Came back with &lt;code&gt;harshit.cloud · lazy security&lt;/code&gt; baked into the footer (also unprompted, also welcome) and a clean two-outcome fork that makes the block/allow decision the visual sink.&lt;/p&gt;
&lt;p&gt;Verdict: &lt;strong&gt;won for explaining a system.&lt;/strong&gt; When the diagram has to show &lt;em&gt;what a tool does&lt;/em&gt;, this format beats the napkin every time. The napkin is a punchline. This one is a reference.&lt;/p&gt;
&lt;p&gt;I did not ship an animated &lt;code&gt;.gif&lt;/code&gt; for Part 1. The &lt;code&gt;infographic-gif&lt;/code&gt; skill is the right tool for &lt;em&gt;quantitative motion&lt;/em&gt; — a funnel decaying, a bar chart counting up. Nothing in Part 1 needed motion to make the point. If Part 3 ends up wanting a &quot;blast radius over 24 hours&quot; visualisation, that's where the GIF goes.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19913/lazy-sres-guide-to-secure-systems-part-1-the-dependencies-you-didnt-read</guid>
<pubDate>Sun, 07 Jun 2026 21:55:28 +0000</pubDate>
</item>
</channel>
</rss>