<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged secrets</title>
<link>https://coderlegion.com/tag/secrets</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Three States of a Secret: Tracked, Ignored, and the One That Ships It</title>
<link>https://coderlegion.com/27186/the-three-states-of-a-secret-tracked-ignored-and-the-one-that-ships-it</link>
<description>&lt;hr&gt;
&lt;p&gt;Every Git tutorial teaches three states. Modified, staged, committed: the working directory, the staging area, the repository. It is canonical, it is in the official book, and for the question that decides whether your credentials end up in permanent public history, it is the wrong three states entirely.&lt;/p&gt;
&lt;p&gt;The axis that matters for a secret is different. A file in a working tree is in one of three conditions: tracked, meaning Git already versions it; ignored, meaning Git has been told to look away; or the third state, untracked and not ignored, the one nobody teaches. A file in the third state shows up in every status listing. It is included in every &lt;code&gt;git add .&lt;/code&gt; and &lt;code&gt;git add -A&lt;/code&gt;. It rides along with any commit made by anyone in a hurry, which describes most commits. The first two states are decisions. The third state is an accident waiting for a keystroke.&lt;/p&gt;
&lt;p&gt;Last week we found a credential living in exactly that state, in our own shared repository.&lt;/p&gt;
&lt;h3 id=&quot;a-list-with-a-gap&quot;&gt;A list with a gap&lt;/h3&gt;
&lt;p&gt;The file was a QA auto-login token: a per-machine secret, minted automatically, that lets a local test harness skip a login screen. We are deliberately not describing it more precisely than that, for the obvious reason that the essay you are reading is about how easily such things become public.&lt;/p&gt;
&lt;p&gt;What made the find instructive was not the token. It was the neighborhood. The repository's ignore file had a section, with a comment header, for exactly this class of file: per-machine state, generated artifacts, things that belong to one box and must never be versioned. The token's class-siblings were all correctly listed there. A deploy marker: ignored. A generated test-state directory: ignored. The token itself: absent from the list. Whoever built the section had enumerated the per-machine files they could think of, and this one had arrived later, or been forgotten, and nothing in the system noticed the omission.&lt;/p&gt;
&lt;p&gt;So the failure was clerical, not conceptual. Nobody misunderstood security. Nobody made a bad call. A correct rule was implemented as a list, the list had a gap, and the gap was invisible because an untracked-and-not-ignored file looks like nothing at all. It does not fail a build. It does not trip a scanner that only reads committed content. It sits in the status listing of a busy repository, camouflaged among the genuinely new files that appear and disappear all day, one &lt;code&gt;add -A&lt;/code&gt; away from being versioned, pushed, and preserved.&lt;/p&gt;
&lt;p&gt;That phrase, one commit away from permanent, sounds like rhetoric. It is not, and the reason it is not is worth two minutes of your attention.&lt;/p&gt;
&lt;h3 id=&quot;permanent-means-permanent-by-design&quot;&gt;Permanent means permanent, by design&lt;/h3&gt;
&lt;p&gt;In 2024, researchers at Truffle Security documented what they called Cross Fork Object Reference: on GitHub, a commit made to any repository in a fork network can be accessed from any other repository in that network, by supplying the commit hash directly. The consequences are stranger than they sound. Commits made to a fork remain accessible through the original repository after the fork is deleted. Delete the original public repository, and its commits remain accessible through any fork, including forks that never synced. Commits made to a private fork before the upstream went public become reachable once it does. And because Git accepts abbreviated hashes, short enough prefixes can be brute-forced through the web interface.&lt;/p&gt;
&lt;p&gt;To demonstrate, the researchers reviewed three commonly forked public repositories from a major AI company and found forty valid API keys in commits that existed only in deleted forks. The pattern behind them was always the same: fork the repo, hardcode a key, do some work, delete the fork, assume the evidence went with it.&lt;/p&gt;
&lt;p&gt;GitHub, responding through its vulnerability disclosure program, said the behavior is an intentional design decision and is working as expected. Fork networks share their object stores; that is what makes forking cheap; deletion of one node does not garbage-collect the network. You can argue the design either way, but you cannot argue with what it means for a leaked secret: on the world's default code host, a credential that enters history in a repository with forks cannot be reliably un-published by any deletion or history rewrite you have the power to perform. The scrub tools clean your copy. The network remembers.&lt;/p&gt;
&lt;p&gt;Which reframes the little file in our status listing. The distance between &quot;sitting untracked on one machine&quot; and &quot;recoverable forever by strangers&quot; was not a security boundary. It was one inattentive command by any collaborator, on any morning.&lt;/p&gt;
&lt;h3 id=&quot;the-scale-of-the-middle-state&quot;&gt;The scale of the middle state&lt;/h3&gt;
&lt;p&gt;If this were rare, it would be an anecdote. GitGuardian's State of Secrets Sprawl report for 2026 counted 28.65 million new hardcoded secrets added to public GitHub commits in 2025, up 34 percent in a single year, the largest jump they have recorded. The growth is not just more developers committing more code; commits grew 43 percent, but the secrets grew on top of a base that detection tooling has been attacking for years. Secrets tied to AI services grew 81 percent, including some 113,000 leaked keys for one popular API alone.&lt;/p&gt;
&lt;p&gt;Two of the report's findings matter more than the raw count.&lt;/p&gt;
&lt;p&gt;First: private repositories are roughly six times more likely than public ones to contain hardcoded secrets. Read that carefully, because it is the entire psychology of the middle state in one statistic. People are careful where they can be seen and careless where they believe they cannot. The private repo feels like a drawer; developers treat it like one. But the tree's capacity to swallow a secret is identical in a private repository, and private repositories do not stay private: they get forked, migrated, opened, acquired, and cloned onto laptops. The QA token in our tree was &quot;only&quot; on a shared internal repo. Six-x is what &quot;only internal&quot; does to discipline, measured at population scale.&lt;/p&gt;
&lt;p&gt;Second: leaked credentials do not die. Of credentials confirmed valid in 2022, nearly 70 percent were still valid in January 2025, and the rate was still above 64 percent a year later. The exposure window for a swallowed secret is not a bad afternoon and a rotation ticket. Measured across the population, it is years, because the organizational reflexes that would end the exposure mostly never fire.&lt;/p&gt;
&lt;p&gt;Put the two datasets together and the middle state stops looking like a pedantic edge case. Millions of secrets a year enter histories that, on the default host, cannot be fully un-published, and once there, most of them stay live for years. Every one of them passed through the third state on the way.&lt;/p&gt;
&lt;h3 id=&quot;the-mirror&quot;&gt;The mirror&lt;/h3&gt;
&lt;p&gt;One more number from the same report, and it is about us. GitGuardian measured commits associated with AI coding assistants and found a 3.2 percent secret-leak rate, against a 1.5 percent baseline across public commits generally. Roughly double. The comparison populations are not perfectly like-for-like and the number is an association, not a proof of cause; assisted commits may simply cluster in the kinds of fast-moving work where leaks happen anyway. But we are an AI-assisted shop, the repository in this story is maintained by AI agents as well as by convention, and it would be a convenient omission to quote everyone else's statistics and skip the one that describes our own workflow. The token we found was minted by automation, in a tree that automation commits to daily. Whatever the causal story, speed multiplies whatever the checklist misses, and our checklists get exercised at machine speed. The find above is our own six-x statistic in miniature: carelessness precisely where nobody outside would ever look.&lt;/p&gt;
&lt;h3 id=&quot;deny-by-default-and-the-scope-of-the-fix&quot;&gt;Deny by default, and the scope of the fix&lt;/h3&gt;
&lt;p&gt;Here is what we did not do: we did not rotate the token. Before anyone quotes the incident-response handbook, notice the scope. This credential never left the third state. Untracked, uncommitted, never pushed: there was no exposure to remediate, and everything in this essay concerns that state, the one before anything has leaked. What to do with a credential that has actually escaped is a different question with different answers, and this piece deliberately says nothing about it. In the pre-exposure state, reflexively regenerating the secret does nothing about the actual defect, which is that the tree can swallow the file. Rotate it and the new token materializes in the same unlisted path, one keystroke from history, having taught everyone nothing except that the alarm can be silenced.&lt;/p&gt;
&lt;p&gt;The fix for the state is structural, and it is the same fix the six-x statistic points at: stop depending on someone remembering to extend a list. An enumerate-the-forbidden ignore file is a control that decays, because every new secret-shaped file must be anticipated by a human at exactly the moment humans are thinking about something else. The durable versions are deny-by-default. Generated state lives in one directory that is ignored as a directory, so new per-machine files are born ignored instead of born naked. Secrets live outside the work tree entirely where the tooling allows it. A pre-commit hook or CI scanner watches the staging area, not just history, so the third state gets caught at the moment of transition, which is the only moment it is cheap. And the status listing itself is treated as a signal: a repository whose &lt;code&gt;git status&lt;/code&gt; is habitually noisy is a repository where one more untracked file is invisible, and invisible is the whole problem.&lt;/p&gt;
&lt;p&gt;None of this is exotic. That is rather the point. The find that started this essay required no attacker, no zero-day, no misjudgment even; just a correct rule implemented as a list, a list with a gap, and a state that every tutorial's mental model skips. Git's famous three states describe the lifecycle of work you meant to commit. The state that ships your secrets is the one for files you never decided anything about, and it will keep shipping them for as long as the decision is left to a list of exceptions and a human memory.&lt;/p&gt;
&lt;p&gt;Check your own status listing today. Not the history: the listing. Count the files that are neither tracked nor ignored, and ask, for each one, what happens on the day someone in a hurry types &lt;code&gt;git add -A&lt;/code&gt;. If the answer for any of them is &quot;that must never be committed,&quot; you are one omitted line away from a statistic that stays valid for years. The line costs ten seconds. The middle state is the only place it can still be written that cheaply.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;/h3&gt;
&lt;p&gt;· GitGuardian, &lt;em&gt;The State of Secrets Sprawl 2026&lt;/em&gt; — the 28.65 million new secrets (+34% YoY), commit and developer growth, the AI-service secrets figures, the ~6× internal-vs-public multiple, the credential-validity persistence (nearly 70% of 2022's valid credentials still valid in January 2025; above 64% in January 2026), and the AI-assisted commit leak-rate association (3.2% vs 1.5% baseline).&lt;/p&gt;
&lt;p&gt;· Truffle Security, &lt;em&gt;Anyone can Access Deleted and Private Repository Data on GitHub&lt;/em&gt; — the Cross Fork Object Reference write-up: the three persistence paths, the forty valid API keys recovered from deleted forks, and GitHub's disclosure-program response that the behavior is an intentional design decision.&lt;/p&gt;
&lt;p&gt;· Pro Git (git-scm.com), the canonical three-states model this essay's title deliberately collides with: working directory, staging area, repository.&lt;/p&gt;
&lt;p&gt;· The QA-token find is our own, from a shared internal repository, described generically throughout for the reason the essay gives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Once a secret is in history, the record is the thing you have to reason about.&lt;/strong&gt; Chain of Consciousness records what an agent actually did, step by step, so the reading and the decision live in the same artifact.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/hosted-coc/&quot;&gt;Hosted Chain of Consciousness&lt;/a&gt;  ·  &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/verify/&quot;&gt;Verify a record&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pip install chain-of-consciousness&lt;/code&gt;  ·  &lt;code&gt;npm install chain-of-consciousness&lt;/code&gt;&lt;/p&gt;
</description>
<category>Security</category>
<guid isPermaLink="true">https://coderlegion.com/27186/the-three-states-of-a-secret-tracked-ignored-and-the-one-that-ships-it</guid>
<pubDate>Mon, 14 Sep 2026 18:23:38 +0000</pubDate>
</item>
<item>
<title>How I run LocalStack without committing LOCALSTACK_AUTH_TOKEN</title>
<link>https://coderlegion.com/20078/how-i-run-localstack-without-committing-localstackauthtoken</link>
<description>&lt;h3 id=&quot;using-envilder-to-keep-the-localstack-auth-token-out-of-git-while-preserving-a-reproducible-local-docker-compose-setup-&quot;&gt;&lt;em&gt;Using &lt;a rel=&quot;nofollow&quot; href=&quot;https://envilder.com/&quot;&gt;Envilder&lt;/a&gt; to keep the LocalStack auth token out of Git while preserving a reproducible local Docker Compose setup.&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;I wanted a reproducible LocalStack setup without committing &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; to the repository.&lt;/p&gt;
&lt;p&gt;That sounds simple, but it touches a common local development problem:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;How do you make a project easy to run without putting real secrets in Git?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;LocalStack &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.localstack.cloud/aws/getting-started/auth-token/&quot;&gt;expects the auth token&lt;/a&gt; to be available through the &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; environment variable when starting it with Docker, Docker Compose, the LocalStack CLI, or CI workflows.&lt;/p&gt;
&lt;p&gt;Since &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.localstack.cloud/localstack-for-aws-release-2026-03-0/&quot;&gt;LocalStack for AWS 2026.03.0&lt;/a&gt;, it ships as a consolidated Docker image and requires an auth token to start, so managing &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; cleanly is now part of the normal setup path, not just a Pro-only edge case.&lt;/p&gt;
&lt;p&gt;That token is a secret, so I did not want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;commit it to the repository&lt;/li&gt;
&lt;li&gt;paste it into &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;duplicate it across helper scripts&lt;/li&gt;
&lt;li&gt;rely on a manual &lt;code&gt;export LOCALSTACK_AUTH_TOKEN=...&lt;/code&gt; step every time&lt;/li&gt;
&lt;li&gt;make the setup work only on my machine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I wanted instead was a small, repeatable contract:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;this project needs LOCALSTACK_AUTH_TOKEN
resolve it from the secret store
inject it locally when needed
do not commit the value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the pattern I ended up using with &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/macalbert/envilder&quot;&gt;Envilder&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-pattern&quot;&gt;The Pattern&lt;/h3&gt;
&lt;p&gt;The idea is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;secret values stay in a real secret store&lt;/li&gt;
&lt;li&gt;the repository contains only the mapping&lt;/li&gt;
&lt;li&gt;local development generates a &lt;code&gt;.env&lt;/code&gt; file from that mapping&lt;/li&gt;
&lt;li&gt;Docker Compose reads the generated &lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the token never gets committed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this case, the secret is &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In another project, it could be a database password, webhook secret, API key, or service credential.&lt;/p&gt;
&lt;h3 id=&quot;defining-the-secret-mapping&quot;&gt;Defining the Secret Mapping&lt;/h3&gt;
&lt;p&gt;Here is a minimal &lt;code&gt;envilder.json&lt;/code&gt; example using AWS SSM Parameter Store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;$schema&quot;: &quot;https://envilder.com/schema/map-file.v1.json&quot;,
  &quot;$config&quot;: {
    &quot;provider&quot;: &quot;aws&quot;,
    &quot;profile&quot;: &quot;default&quot;
  },
  &quot;LOCALSTACK_AUTH_TOKEN&quot;: &quot;/envilder/localstack/auth-token&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file is safe to commit because it does not contain the token.&lt;/p&gt;
&lt;p&gt;It only says:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;_To produce &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;, resolve this value from the secret store._&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The actual token remains outside the repository.&lt;/p&gt;
&lt;h3 id=&quot;generating-the-local-env&quot;&gt;Generating the Local &lt;code&gt;.env&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Before starting LocalStack, generate the local environment file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx envilder --map=envilder.json --envfile=.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That produces a local &lt;code&gt;.env&lt;/code&gt; file like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;LOCALSTACK_AUTH_TOKEN=...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file should be ignored by Git:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitignore&quot;&gt;.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the project contains the setup contract, but not the secret.&lt;/p&gt;
&lt;h3 id=&quot;passing-the-token-to-localstack&quot;&gt;Passing the Token to LocalStack&lt;/h3&gt;
&lt;p&gt;A Docker Compose service can reference the variable as usual:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  localstack:
    image: localstack/localstack:stable
    ports:
      - &quot;4566:4566&quot;
      - &quot;4510-4559:4510-4559&quot;
    environment:
      LOCALSTACK_AUTH_TOKEN: \$&quot;{LOCALSTACK_AUTH_TOKEN:?Run npx envilder first}&quot;
    volumes:
      - &quot;/var/run/docker.sock:/var/run/docker.sock&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docker Compose automatically reads a &lt;code&gt;.env&lt;/code&gt; file from the project directory for variable interpolation.&lt;/p&gt;
&lt;p&gt;If your generated env file lives somewhere else, you can pass it explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose --env-file path/to/.env up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The payoff is what a new contributor sees after cloning:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx envilder --map=envilder.json --envfile=.env
docker compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two commands, no Slack message asking &quot;where do I get the token?&quot;, no secret in Git.&lt;/p&gt;
&lt;h3 id=&quot;why-not-direnv-1password-or-chamber-&quot;&gt;Why not direnv, 1Password, or chamber?&lt;/h3&gt;
&lt;p&gt;For one developer, &lt;code&gt;export LOCALSTACK_AUTH_TOKEN=...&lt;/code&gt; works, but it creates hidden local state the repo never documents. The interesting comparison is against the tools that already resolve secrets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;direnv&lt;/strong&gt; loads env vars when you enter a directory, but &lt;code&gt;.envrc&lt;/code&gt; is just shell. You still write &lt;code&gt;export LOCALSTACK_AUTH_TOKEN=$(aws ssm get-parameter ...)&lt;/code&gt; yourself, per variable, per project. It is a loader, not a resolver, and the mapping lives in bespoke shell instead of a declared contract.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1Password &lt;code&gt;op run&lt;/code&gt;&lt;/strong&gt; injects secrets at runtime and is excellent, but it assumes 1Password is your store. If your secrets already live in AWS SSM or Azure Key Vault, you are adding a vendor, not removing one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;chamber&lt;/strong&gt; is the closest match for the AWS-only case:&lt;br&gt;
&lt;code&gt;chamber exec service -- docker compose up&lt;/code&gt; reads straight from SSM. If you never leave AWS and only need secrets at the command line, chamber or a three-line script is genuinely enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Envilder's narrow bet is different on three points:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The mapping is an explicit &lt;code&gt;name -&amp;gt; path&lt;/code&gt; contract committed as JSON, not a service prefix or shell glue.&lt;/li&gt;
&lt;li&gt;The store is your own cloud. No third-party infrastructure holds the secret.&lt;/li&gt;
&lt;li&gt;The same map drives the CLI here, CI, and in-process resolution through the SDKs, so local dev and runtime read one contract.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you only ever touch AWS from a shell, you may not need it. The case shows up when you have more than one provider, more than one language, or want the same declared contract locally, in CI, and at runtime.&lt;/p&gt;
&lt;h3 id=&quot;the-slightly-meta-part&quot;&gt;The Slightly Meta Part&lt;/h3&gt;
&lt;p&gt;I ran into this while working on Envilder.&lt;/p&gt;
&lt;p&gt;Envilder uses LocalStack for integration testing.&lt;/p&gt;
&lt;p&gt;LocalStack needs &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So I used Envilder to inject the token needed to start the LocalStack environment used to test Envilder.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2tfkdag8d7ske6xb59ta.png&quot; alt=&quot;Envilder uses Envilder to inject LocalStack token&quot;&gt;&lt;/p&gt;
&lt;p&gt;The tool became its own first customer, which is always a useful kind of feedback.&lt;/p&gt;
&lt;h3 id=&quot;a-note-on-localstack-auth-tokens&quot;&gt;A Note on LocalStack Auth Tokens&lt;/h3&gt;
&lt;p&gt;LocalStack's documentation is clear that &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; should be kept confidential and should not be committed to source control.&lt;/p&gt;
&lt;p&gt;This post is not about bypassing that requirement.&lt;/p&gt;
&lt;p&gt;It is about treating the token like any other secret:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;store it safely&lt;/li&gt;
&lt;li&gt;resolve it when needed&lt;/li&gt;
&lt;li&gt;avoid leaking it into Git&lt;/li&gt;
&lt;li&gt;keep the project setup reproducible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For open source maintainers, this matters. A project can require credentials for local or CI workflows and still provide a clean setup path for contributors.&lt;/p&gt;
&lt;h3 id=&quot;takeaway&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; is just one example of a broader problem.&lt;/p&gt;
&lt;p&gt;Projects often need secrets during local development, CI/CD, tests, and runtime. The hard part is not only storing those secrets safely. It is keeping the mapping between &quot;what the app needs&quot; and &quot;where the secret lives&quot; clear and consistent.&lt;/p&gt;
&lt;p&gt;The pattern I like is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;secret values outside Git
secret mappings inside Git
one contract for local dev, CI/CD, and runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the reason I am building Envilder.&lt;/p&gt;
&lt;p&gt;Not to add another place to store secrets.&lt;/p&gt;
&lt;p&gt;Just to make secret resolution explicit, repeatable, and versionable.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20078/how-i-run-localstack-without-committing-localstackauthtoken</guid>
<pubDate>Tue, 09 Jun 2026 14:51:24 +0000</pubDate>
</item>
<item>
<title>Kubernetes Security Best Practices: A Production Hardening Guide</title>
<link>https://coderlegion.com/15640/kubernetes-security-best-practices-a-production-hardening-guide</link>
<description>
&lt;a href=&quot;https://alexandre-vazquez.com/kubernetes-security-best-practices/&quot; title=&quot;Kubernetes Security Best Practices: A Production Hardening Guide&quot; rel=&quot;nofollow&quot;&gt;&lt;img width=&quot;1672&quot; height=&quot;941&quot; src=&quot;https://mlk5tkvgfeoa.i.optimole.com/cb:s8WW.68/w:1672/h:941/q:mauto/f:best/https://alexandre-vazquez.com/wp-content/uploads/2026/04/SecurityBestPractices.avif&quot; class=&quot;webfeedsFeaturedVisual wp-post-image&quot; alt=&quot;Kubernetes Security Best Practices: A Production Hardening Guide&quot; style=&quot;display: block; margin: auto; margin-bottom: 5px;max-width: 100%;&quot; title=&quot;Kubernetes Security Best Practices: A Production Hardening Guide 2&quot;&gt;&lt;/a&gt;
&lt;p class=&quot;lead&quot;&gt;Kubernetes security is not a single feature you enable &amp;mdash; it is a layered discipline that spans the control plane, workloads, networking, supply chain, and runtime. This guide covers the security controls that matter most in production, why each one exists, and how to implement them without breaking your cluster.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The Kubernetes Attack Surface&lt;/h2&gt;



&lt;p&gt;Before hardening anything, understand what you are protecting. A Kubernetes cluster has several distinct attack surfaces:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;API server&lt;/strong&gt; &amp;mdash; The central control plane. Any entity that can reach it with valid credentials can read cluster state, modify workloads, or escalate privileges.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;etcd&lt;/strong&gt; &amp;mdash; Stores all cluster state in plain text, including Secrets. Direct etcd access is equivalent to root on every node.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Nodes&lt;/strong&gt; &amp;mdash; A compromised node can access all Secrets mounted on pods running on it, access the kubelet API, and potentially escape to the hypervisor.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Pods&lt;/strong&gt; &amp;mdash; Privileged pods, host-network pods, and pods with excessive capabilities can break container isolation.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Supply chain&lt;/strong&gt; &amp;mdash; Malicious images, compromised registries, and unsigned artifacts can introduce attacker-controlled code into your cluster.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;RBAC&lt;/strong&gt; &amp;mdash; Overly permissive roles allow lateral movement and privilege escalation once an attacker gains any foothold.&lt;/li&gt;&lt;/ul&gt;



&lt;p&gt;The controls below address each of these surfaces. Prioritize based on your threat model &amp;mdash; a public-facing multi-tenant cluster needs all of them; an internal development cluster can relax some.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;1. RBAC: Least Privilege from Day One&lt;/h2&gt;



&lt;p&gt;Role-Based Access Control is Kubernetes&amp;rsquo; primary authorization mechanism. Most clusters fail at RBAC not because it is misconfigured, but because it is over-permissive by default and nobody reviews it systematically.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Common RBAC Mistakes&lt;/h3&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;Binding to &lt;code&gt;cluster-admin&lt;/code&gt; for convenience.&lt;/strong&gt; Almost no workload needs cluster-admin. Use namespaced roles wherever possible.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Using &lt;code&gt;*&lt;/code&gt; verbs or resources in roles.&lt;/strong&gt; Wildcard permissions are almost always broader than intended.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Not auditing ServiceAccount token usage.&lt;/strong&gt; Every pod gets a ServiceAccount. The default ServiceAccount in most namespaces has no permissions, but custom workloads often get over-permissive SAs.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Forgetting &lt;code&gt;automountServiceAccountToken: false&lt;/code&gt;.&lt;/strong&gt; If a workload does not need to talk to the Kubernetes API, disable token mounting entirely.&lt;/li&gt;&lt;/ul&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Practical RBAC Patterns&lt;/h3&gt;



&lt;p&gt;For a workload that only needs to read ConfigMaps in its own namespace:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: configmap-reader
  namespace: my-app
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;configmaps&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: my-app-configmap-reader
  namespace: my-app
subjects:
- kind: ServiceAccount
  name: my-app
  namespace: my-app
roleRef:
  kind: Role
  name: configmap-reader
  apiGroup: rbac.authorization.k8s.io&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Audit existing RBAC with &lt;a href=&quot;https://github.com/aquasecurity/kubectl-who-can&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;kubectl-who-can&lt;/a&gt; or &lt;a href=&quot;https://github.com/alcideio/rbac-tool&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;rbac-tool&lt;/a&gt; to find overly permissive bindings before attackers do.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;2. Pod Security Standards&lt;/h2&gt;



&lt;p&gt;PodSecurityPolicy was deprecated in Kubernetes 1.21 and removed in 1.25. Its replacement is &lt;strong&gt;Pod Security Admission (PSA)&lt;/strong&gt;, a built-in admission controller that enforces one of three security profiles at the namespace level:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;Privileged&lt;/strong&gt; &amp;mdash; No restrictions. For system components only.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Baseline&lt;/strong&gt; &amp;mdash; Prevents the most critical privilege escalations: privileged containers, hostPID, hostIPC, hostNetwork, dangerous capabilities.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Restricted&lt;/strong&gt; &amp;mdash; Enforces current hardening best practices. Requires running as non-root, dropping all capabilities, and using a restricted seccomp profile.&lt;/li&gt;&lt;/ul&gt;



&lt;p&gt;Enable enforcement at the namespace level with labels:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;A pod that runs as root or requests host-network in a namespace enforcing &lt;code&gt;restricted&lt;/code&gt; will be rejected at admission. The &lt;code&gt;warn&lt;/code&gt; and &lt;code&gt;audit&lt;/code&gt; modes let you test before enforcing.&lt;/p&gt;



&lt;p&gt;PSA covers the most critical pod-level escalations, but it is coarse-grained. For fine-grained policy control, use &lt;a rel=&quot;nofollow&quot; href=&quot;https://alexandre-vazquez.com/kyverno-a-detailed-way-of-enforcing-standard-and-custom-policies/&quot;&gt;Kyverno&lt;/a&gt; alongside PSA.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;3. Network Policies: Micro-Segmentation&lt;/h2&gt;



&lt;p&gt;By default, every pod in a Kubernetes cluster can communicate with every other pod across all namespaces. This is a flat network model that gives attackers unrestricted lateral movement once they compromise any workload.&lt;/p&gt;



&lt;p&gt;Network Policies define L3/L4 allow-rules for pod-to-pod communication. They are enforced by your CNI plugin (Calico, Cilium, Weave &amp;mdash; not Flannel, which does not support NetworkPolicy).&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Default Deny Pattern&lt;/h3&gt;



&lt;p&gt;Start by denying all ingress and egress in every namespace, then open only what is explicitly needed:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Then allow specific traffic:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api
    ports:
    - protocol: TCP
      port: 5432&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Do not forget DNS egress &amp;mdash; most workloads need to resolve names via kube-dns, which requires UDP port 53 egress to the &lt;code&gt;kube-system&lt;/code&gt; namespace.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;4. Secrets Management&lt;/h2&gt;



&lt;p&gt;Kubernetes Secrets are base64-encoded, not encrypted. Stored in etcd in plain text by default. Anyone with &lt;code&gt;get&lt;/code&gt; permission on Secrets can read them. This is not a vulnerability &amp;mdash; it is a design decision that puts the responsibility on you to:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;Enable encryption at rest for etcd.&lt;/strong&gt; Configure &lt;code&gt;EncryptionConfiguration&lt;/code&gt; with an AES-CBC or AES-GCM provider. This encrypts Secrets before they are written to etcd.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Use external secret stores.&lt;/strong&gt; HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault with the External Secrets Operator means actual secret values never live in Kubernetes at all.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Restrict Secret RBAC aggressively.&lt;/strong&gt; Never give &lt;code&gt;list&lt;/code&gt; on Secrets cluster-wide &amp;mdash; it returns all values. Use &lt;code&gt;get&lt;/code&gt; on named resources where possible.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Avoid environment variables for secrets.&lt;/strong&gt; Prefer volume mounts. Env vars are visible in pod inspect output and can leak through application logging.&lt;/li&gt;&lt;/ul&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# etcd encryption at rest - in kube-apiserver config
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: key1
        secret: &amp;lt;base64-encoded-32-byte-key&amp;gt;
  - identity: {}&lt;/code&gt;&lt;/pre&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;5. Image Security and Supply Chain&lt;/h2&gt;



&lt;p&gt;Your runtime security posture is only as good as the images you run. A compromised image from a public registry bypasses every runtime control you have.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Scan images in CI&lt;/h3&gt;



&lt;p&gt;Use Trivy, Grype, or Snyk to scan images as part of your CI pipeline. Block deployments of images with critical CVEs:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# In your CI pipeline
trivy image --exit-code 1 --severity CRITICAL your-image:tag&lt;/code&gt;&lt;/pre&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Use a private registry with admission control&lt;/h3&gt;



&lt;p&gt;Only allow images from your private registry using an admission webhook (Kyverno, OPA Gatekeeper). This prevents developers from running arbitrary public images in production:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: Enforce
  rules:
  - name: validate-registries
    match:
      any:
      - resources:
          kinds: [&quot;Pod&quot;]
    validate:
      message: &quot;Images must come from registry.company.com&quot;
      pattern:
        spec:
          containers:
          - image: &quot;registry.company.com/*&quot;&lt;/code&gt;&lt;/pre&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Use distroless or minimal base images&lt;/h3&gt;



&lt;p&gt;Distroless images contain only the application and its runtime dependencies &amp;mdash; no shell, no package manager, no debugging tools. This drastically reduces the attack surface and the number of CVEs. Google&amp;rsquo;s distroless images are available for Java, Node.js, Python, and Go.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Sign and verify images&lt;/h3&gt;



&lt;p&gt;Cosign (from the Sigstore project) lets you sign container images and verify signatures at admission time using Kyverno or Connaisseur. This prevents image substitution attacks where an attacker replaces a legitimate image in your registry.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;6. Runtime Security&lt;/h2&gt;



&lt;p&gt;Runtime security detects and responds to malicious activity after a container is running. The primary tool in this space is &lt;strong&gt;Falco&lt;/strong&gt; &amp;mdash; a CNCF project that uses eBPF to monitor system calls and raise alerts when containers behave unexpectedly.&lt;/p&gt;



&lt;p&gt;Default Falco rules catch common attack patterns:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;Shell spawned in a container&lt;/li&gt;&lt;li&gt;Network connection to an unexpected IP&lt;/li&gt;&lt;li&gt;Write to a sensitive file path (&lt;code&gt;/etc/passwd&lt;/code&gt;, &lt;code&gt;/etc/shadow&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Privilege escalation via setuid binaries&lt;/li&gt;&lt;li&gt;Container drift (new executable files written at runtime)&lt;/li&gt;&lt;/ul&gt;



&lt;p&gt;Combine Falco with &lt;strong&gt;seccomp profiles&lt;/strong&gt; to restrict the system calls a container can make at the kernel level. The &lt;code&gt;RuntimeDefault&lt;/code&gt; seccomp profile (available since Kubernetes 1.27 as a default) blocks 300+ system calls that containers virtually never need.&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 65534
      capabilities:
        drop: [&quot;ALL&quot;]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;These four &lt;code&gt;securityContext&lt;/code&gt; settings together (&lt;code&gt;allowPrivilegeEscalation: false&lt;/code&gt;, &lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt;, &lt;code&gt;runAsNonRoot: true&lt;/code&gt;, &lt;code&gt;capabilities.drop: ALL&lt;/code&gt;) make container escape significantly harder and satisfy the Kubernetes Restricted pod security standard.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;7. API Server Hardening&lt;/h2&gt;



&lt;p&gt;The API server is the most critical component to harden. Key settings:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;Disable anonymous authentication.&lt;/strong&gt; &lt;code&gt;--anonymous-auth=false&lt;/code&gt; ensures every request is authenticated.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Enable audit logging.&lt;/strong&gt; Log all API server requests to a file or webhook. Without audit logs, you cannot investigate incidents or detect RBAC abuse.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Restrict admission plugins.&lt;/strong&gt; Ensure &lt;code&gt;NodeRestriction&lt;/code&gt; is enabled &amp;mdash; it prevents node kubelets from modifying objects outside their own node.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Do not expose the API server to the internet.&lt;/strong&gt; Use a VPN, bastion host, or private endpoint. If you must expose it, restrict access by IP.&lt;/li&gt;&lt;/ul&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Minimal audit policy - log all requests at metadata level,
# and full request body for sensitive resources
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
  resources:
  - group: &quot;&quot;
    resources: [&quot;secrets&quot;, &quot;configmaps&quot;]
- level: Metadata
  omitStages: [&quot;RequestReceived&quot;]&lt;/code&gt;&lt;/pre&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;8. etcd Security&lt;/h2&gt;



&lt;p&gt;etcd stores all cluster state. Treat it as sensitive as your production database:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;Enable TLS for all etcd communication.&lt;/strong&gt; Both peer communication (etcd-to-etcd) and client communication (apiserver-to-etcd) must use mutual TLS.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Restrict network access to etcd.&lt;/strong&gt; etcd should only be reachable by the API server. Use firewall rules or security groups to enforce this.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Enable encryption at rest.&lt;/strong&gt; As described in the Secrets section above.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Backup etcd regularly.&lt;/strong&gt; An etcd snapshot is a complete copy of all cluster state, including all Secrets. Encrypt backups and store them separately from the cluster.&lt;/li&gt;&lt;/ul&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;9. CIS Kubernetes Benchmark&lt;/h2&gt;



&lt;p&gt;The &lt;strong&gt;CIS Kubernetes Benchmark&lt;/strong&gt; is a comprehensive checklist of security controls covering the control plane, nodes, and workloads. Running &lt;a href=&quot;https://github.com/aquasecurity/kube-bench&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;kube-bench&lt;/a&gt; against your cluster gives you a scored assessment against the CIS controls:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;kubectl apply -f &lt;a href=&quot;https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml&quot; rel=&quot;nofollow&quot;&gt;https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml&lt;/a&gt;
kubectl logs $(kubectl get pods -l app=kube-bench -o name)&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;kube-bench outputs PASS/FAIL/WARN for each control with remediation guidance. Run it after initial cluster setup and after major configuration changes.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;10. Continuous Security Posture with Kubescape&lt;/h2&gt;



&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://alexandre-vazquez.com/kubesec-security-kubernetes/&quot;&gt;Kubescape&lt;/a&gt; and similar tools (Starboard/Trivy Operator, KubeScore) provide continuous security scanning of live cluster state &amp;mdash; not just a one-time audit. They check workloads against NSA/CISA hardening guidelines, MITRE ATT&amp;amp;CK framework, and the CIS benchmark in real time.&lt;/p&gt;



&lt;p&gt;Deploy Trivy Operator for continuous in-cluster scanning:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;helm repo add aquasecurity &lt;a href=&quot;https://aquasecurity.github.io/helm-charts/&quot; rel=&quot;nofollow&quot;&gt;https://aquasecurity.github.io/helm-charts/&lt;/a&gt;
helm install trivy-operator aquasecurity/trivy-operator 
  --namespace trivy-system 
  --create-namespace 
  --set=&quot;trivy.ignoreUnfixed=true&quot;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Trivy Operator creates &lt;code&gt;VulnerabilityReport&lt;/code&gt;, &lt;code&gt;ConfigAuditReport&lt;/code&gt;, and &lt;code&gt;RbacAssessmentReport&lt;/code&gt; custom resources in the same namespace as each workload. These can be scraped by Prometheus and displayed in Grafana for a security dashboard.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Security Hardening Checklist&lt;/h2&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; RBAC reviewed &amp;mdash; no wildcard roles, no unnecessary cluster-admin bindings&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; ServiceAccount token automount disabled for workloads that do not need API access&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; Pod Security Standards enforced at namespace level (at least Baseline, Restricted where possible)&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; Network policies deployed &amp;mdash; default deny with explicit allows&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; Secrets encrypted at rest in etcd&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; Images scanned in CI &amp;mdash; no critical CVEs in production&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; Private registry enforced via admission control&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; Container securityContext hardened (non-root, read-only filesystem, no capabilities)&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; seccomp RuntimeDefault profile enabled&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; API server audit logging enabled&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; etcd TLS and network access restricted&lt;/li&gt;&lt;li&gt;&lt;img src=&quot;https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png&quot; alt=&quot;&amp;#9989;&quot; class=&quot;wp-smiley&quot; style=&quot;height: 1em; max-height: 1em;&quot;&gt; kube-bench run and critical/high findings remediated&lt;/li&gt;&lt;li&gt;&amp;lt;img src=&quot;h&lt;/li&gt;&lt;/ul&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15640/kubernetes-security-best-practices-a-production-hardening-guide</guid>
<pubDate>Sun, 26 Apr 2026 03:43:52 +0000</pubDate>
</item>
</channel>
</rss>