DevOps Nimbus · DevSecOps · Searchable version
DevSecOps problems and fixes
58 errors and scanner findings: the exact text, what it means, each cause with the check that proves it and the fix for it, and how to stop it coming back. Then learn it for good: break and fix each one yourself in the browser playground. Practice examples use invented IDs such as PRACTICE-2026-0001.
Secrets
gitleaks: AWS access key found in history
gitleaks
The error
RuleID: aws-access-token
...
WRN leaks found: 1
What it means
gitleaks matched its aws-access-token rule: a string shaped like an AWS access key ID (AKIA... for a long-term key) is in a commit. The key ID alone cannot sign requests, but the secret access key is usually committed next to it. Once pushed, anyone who cloned, forked or viewed the repository may have both.
Causes, most likely first
-
A live key from your own account was committed, usually pasted into a config file or script for a quick test. This is the common case.
Check
gitleaks git -v --redact # commit, file and line, without printing the key aws sts get-access-key-info --access-key-id <key> # Account is your account ID: the key is yours aws iam get-access-key-last-used --access-key-id <key> # its user and last use; a recent use you cannot explain means it may already be in use aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=<key> # what it did: management events, last 90 days, this region only; repeat per regionFix
Rotate first: give the app a new key (better, a role), deactivate the leaked one at once (aws iam update-access-key --status Inactive) and delete it once nothing breaks. Deleting it from git does not revoke it. If CloudTrail shows use you do not recognise, follow "Leaked key already used by an attacker". Then remove it from code, and purge history in a fresh clone: git filter-repo --replace-text replace.txt, re-add origin, push --force, and ask collaborators to re-clone. Forks, pull request refs and caches keep the old commits, which is why rotation comes first.
-
The key belongs to another AWS account (a vendor's, a customer's, a former employer's). The rule matches the shape, not the owner.
Check
aws sts get-access-key-info --access-key-id <key> # an Account that is not yoursFix
Tell that account's owner now, through a channel you trust; only they can revoke it. Then remove it from code and history as above.
-
It is a documented example key, not a real one. The keys in AWS's documentation end in EXAMPLE and have the same shape.
Check
gitleaks git -v --redact # then open that file and line: a key ending in EXAMPLE, in docs or a test fixtureFix
Suppress that one finding: add its Fingerprint to .gitleaksignore with a comment saying why. Not a rule-wide allowlist.
Prevent it
Pre-commit gitleaks, push protection, and roles or OIDC instead of keys.
Now learn it: break and fix it yourself in AWS key committed, Incident: leaked key used by an attacker, then take Level 02 · Secrets. Open in the site
.env file committed
git
The error
new file: .env
What it means
git status lists .env under "Changes to be committed" as a new file: git is about to add it. A .env file usually holds passwords and API keys. How far it got decides the fix: only staged, committed locally, or already pushed.
Causes, most likely first
-
No ignore rule matches .env, so git add . picked it up. /.env only matches the top folder, and .env.local or .env.production need their own pattern.
Check
git check-ignore -v .env # prints nothing: no rule ignores it git log --oneline --all -- .env # empty: only staged, never committedFix
git rm --cached .env (your file stays on disk), add .env and .env.* to .gitignore, and keep !.env.example if the template should stay tracked. Nothing left your machine, so nothing needs rotating.
-
It was forced in with git add -f, which overrides ignore rules.
Check
git check-ignore -v .env # names a rule, yet the file is staged: it was added with -fFix
git rm --cached .env. The existing rule then keeps it out.
-
It was already committed, and maybe pushed. Unstaging no longer removes it: it is in history.
Check
git log --oneline --all -- .env # lists commits: it is in history git branch -r --contains <commit> # a remote branch listed: it was pushedFix
Committed, not pushed: take it out of those commits (git commit --amend, or git reset --soft to before it) before you push. Pushed: rotate every value in it first, then git rm --cached .env, fix .gitignore and purge it in a fresh clone: git filter-repo --path .env --invert-paths; push --force.
Prevent it
A template file (.env.example) with no values; gitleaks in pre-commit.
Now learn it: break and fix it yourself in .env committed, then take Level 02 · Secrets. Open in the site
GH013: Repository rule violations found (push protection)
git push
The error
remote: error: GH013: Repository rule violations found for refs/heads/main.
...
! [remote rejected] main -> main (push declined due to repository rule violations)
What it means
GitHub rejected the whole push because a repository rule failed on at least one of the commits being pushed, not only the last one. The remote: lines come from GitHub; the ones after the error name the rule, and for push protection they show the secret type and each commit: <sha> and path: <file>:<line> where it was found. (Without rulesets, a push protection block is GH009: Secrets detected.)
Causes, most likely first
-
Push protection found a secret in one of the commits. Removing it in a newer commit does not help: the older commit is still part of the push.
Check
Read the remote: lines # GITHUB PUSH PROTECTION, a secret type, commit: and path: git log --oneline origin/main..HEAD # every commit this push sends git show <sha> -- <path> # the commit that holds itFix
In the last commit: remove it, git commit -a --amend, push. Older: git rebase -i to edit that commit, or git reset --soft origin/main and recommit. GitHub did not receive the secret, but rotate it anyway if those commits went anywhere else (another remote, a fork, a CI cache). Do not use the bypass URL for a real secret.
-
Another ruleset rule failed: signed commits required, restricted file paths, extensions or sizes, or a commit message pattern. GH013 is the code for every ruleset rule, not only secrets.
Check
Read the remote: lines # a rule such as Commits must have verified signatures git log --show-signature origin/main..HEAD # unsigned commits, if signatures are requiredFix
Change the commits to meet the rule (sign them, drop the file, reword the message) instead of asking for a bypass.
Prevent it
Keep secrets in the environment or a secret store from the first line of a script.
Now learn it: break and fix it yourself in Push blocked by push protection, then take Level 02 · Secrets. Open in the site
trivy: secret found in an image layer
trivy image
The error
CRITICAL: ... (secrets)
/etc/pip.conf:2 (added by 'COPY pip.conf /etc/pip.conf')
What it means
Trivy's secret scanner found a credential inside the image. (secrets) marks that scanner, and added by 'COPY pip.conf /etc/pip.conf' names the Dockerfile step whose layer holds the file. Every layer is kept in the image and can be extracted by anyone who can pull it.
Causes, most likely first
-
A file with a credential was copied in, and a later step deleted it. Deleting in a later RUN only hides the file; the earlier layer still contains it.
Check
trivy image --scanners secret <image> # the file and the step that added it docker history --no-trunc <image> # a COPY of the file, and a later RUN rm of itFix
Rotate the credential first: every pulled copy of the image has it, and deleting the tag does not reach them. Then mount it only for the step that needs it: RUN --mount=type=secret,id=<id> with docker build --secret id=<id>,env=VAR. Rebuild, then delete the old image versions from the registry.
-
The token was passed as ARG or ENV. Build args stay in the image history and ENV in the image config, so it leaks without any file, and a file-only scan may not report it.
Check
trivy image --scanners secret --image-config-scanners secret <image> # findings in the image config: ENV or history docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' <image> | cut -d= -f1 # names of baked-in variables, not values docker history --no-trunc <image> # build-arg values show in RUN lines: do not paste the outputFix
Rotate it, use a build secret (above) instead of ARG or ENV, and pass runtime secrets when the container starts, from the platform's secret store.
Prevent it
.dockerignore for credential files; image secret scanning in CI.
Now learn it: break and fix it yourself in Secret in a Docker layer, Secret passed as a build argument, then take Level 06 · Container images. Open in the site
Passwords in terraform.tfstate
gitleaks, Terraform
The error
RuleID: generic-api-key
File: terraform/terraform.tfstate
What it means
gitleaks found a credential in terraform.tfstate. Terraform state stores every attribute of every resource in plain JSON, passwords and keys included, even values hidden as sensitive in plan output.
Causes, most likely first
-
A local state file was committed, because there is no remote backend and no ignore rule.
Check
git ls-files '*.tfstate*' # state files tracked by git grep -c '"password"' terraform/terraform.tfstate # counts matches without printing valuesFix
Rotate the secrets it holds first. Move state to an encrypted, access-controlled backend (backend "s3" with encrypt = true and locking). git rm --cached the state file, add *.tfstate* to .gitignore, purge it from history.
-
State is remote, but a copy was pulled for debugging (terraform state pull) or a backup file was committed.
Check
git log --stat --all -- '*.tfstate*' # who added which fileFix
Rotate, remove and purge as above; never write state into the working tree.
-
The secret is in state by design: a password passed as a variable is stored there, whichever backend you use.
Check
grep -n password terraform/*.tf # a password argument set from a variableFix
Let the service manage it (manage_master_user_password for RDS), or read it at run time from a secret store, so state never holds it.
Prevent it
A remote backend from day one; managed passwords (manage_master_user_password) so state never holds them.
Now learn it: break and fix it yourself in Secrets in Terraform state, then take Level 07 · Infrastructure as code. Open in the site
Dependencies
Critical vulnerability with a fixed version
osv-scanner, trivy
The error
│ PRACTICE-2026-0001 │ 9.8 │ PyPI │ imgresize │ 2.1.0 │ 2.1.4 │ requirements.txt │
What it means
The scanner matched a package version against an advisory database. The row reads: advisory ID, CVSS score (9.8 is critical), ecosystem, package, installed version, fixed version, and the file it came from. A fixed version (2.1.4) means an upgrade clears it.
Causes, most likely first
-
The version pinned in your lock file has the advisory, and a release with the fix exists. The scanner reads the lock file, so the file in the row is where to change it.
Check
osv-scanner scan source -r . # the row and its fixed version pip index versions <package> # the fixed version is available from your indexFix
Change the constraint at its source (requirements.in, package.json) and regenerate the lock: pip-compile --upgrade-package <package>. Run the tests and the pipeline.
-
The lock file already has the fix, but the scan read an older image or another manifest, so it reports what shipped before.
Check
docker run --rm <image> pip show <package> # Version: the one installed in the image you shipFix
Rebuild and redeploy through the pipeline; scan the image you ship, not only the source.
-
The package is a transitive dependency and its parent's range stops the upgrade, so pip-compile cannot resolve the fixed version.
Check
grep -n -A3 <package> requirements.txt # a '# via' line names the parentFix
Upgrade the parent; see "Vulnerable transitive dependency".
Prevent it
Dependabot or Renovate pull requests, merged often.
Now learn it: break and fix it yourself in Vulnerable dependency with a fix, New critical CVE in production, then take Level 04 · Dependencies (SCA). Open in the site
Vulnerable transitive dependency
osv-scanner
The error
Could not find a version that matches xmlsafe<0.9.3,==0.9.5 (from reportgen==1.8.0, --upgrade-package)
What it means
pip-compile was asked to upgrade a package (--upgrade-package) to a fixed version, but another package's requirement forbids that version. xmlsafe<0.9.3 is reportgen 1.8.0's range, ==0.9.5 is the fix you asked for, and no version satisfies both.
Causes, most likely first
-
The parent caps the child below the fix, and a newer parent release allows it.
Check
grep -n -A3 <package> requirements.txt # the '# via' line names the parent pipdeptree -r -p <package> # every package that requires it, with its range pip index versions <parent> # newer parent releasesFix
Upgrade the parent to a version that allows the fixed child: pip-compile --upgrade-package <parent> --upgrade-package <package>. Run the tests.
-
No parent release allows the fix yet, so upgrading cannot meet both ranges.
Check
curl -s https://pypi.org/pypi/<parent>/json | python3 -c "import json,sys; print(json.load(sys.stdin)['info']['requires_dist'])" # the newest release still caps the child below the fixFix
Replace the parent, or override the child in a resolver that supports overrides (uv's override-dependencies) and test it. A pip constraints file cannot do this: it only narrows ranges, it never overrides a parent's requirement. Ask the parent's maintainers to widen the range.
Prevent it
Keep parents current; small, frequent upgrades.
Now learn it: break and fix it yourself in Transitive vulnerability, then take Level 04 · Dependencies (SCA). Open in the site
Vulnerability with no fix available
osv-scanner, trivy
The error
│ PRACTICE-2026-0012 │ 7.5 │ PyPI │ pdfstamp │ 4.2.0 │ -- │ requirements.txt │
What it means
The scanner found an advisory for the installed version, and -- in the fixed-version column means no patched release exists yet. Upgrading cannot clear it.
Causes, most likely first
-
Your use does not reach the vulnerable code, or you can block the input that does. Advisories name a function or an input, and many projects never use it.
Check
osv-scanner scan source -r . # the advisory ID Read the advisory, then grep your code for the affected function # no call, or only input you control: not reachableFix
Record a scoped exception: osv-scanner.toml [[IgnoredVulns]] with id, ignoreUntil and reason (or .trivyignore.yaml with id, statement and expired_at). Track it; when the exception expires the gate fails again. Not trivy --ignore-unfixed in the gate: it hides every unfixed finding, including the next one that matters.
-
Your code does reach it with outside input, so the risk is real today.
Check
grep -rn <affected function> app/ # called with data from requests, files or other servicesFix
Mitigate now: validate or block that input, or move to a maintained alternative. Keep the finding open until a fix ships.
-
A fix exists in a newer release that the advisory database does not list yet.
Check
pip index versions <package> # releases newer than the advisory's affected rangeFix
Read the release notes; if a later release fixes it, upgrade, and report the missing fixed version to the advisory's source.
Prevent it
Prefer maintained libraries; have an exception process with owners and expiry.
Now learn it: break and fix it yourself in No fix available: accept the risk, then take Level 11 · Release gates and exceptions. Open in the site
Forbidden licence in a dependency
trivy fs --scanners license
The error
│ chartforge │ AGPL-3.0-only │ forbidden │ CRITICAL │
What it means
Trivy's licence scanner classified each package's licence. The row reads package, licence, classification and severity. forbidden is the strictest category (Trivy reports it as CRITICAL; restricted is HIGH): your policy does not allow it in what you ship.
Causes, most likely first
-
The package really is under a licence on your deny list, such as AGPL-3.0, whose terms can require you to publish your own source.
Check
trivy fs --scanners license . # the package and its licence pip show -v <package> # License and classifiers as the package declares them; read its LICENSE file tooFix
Replace the package, or get a written legal decision before changing the policy.
-
The metadata is wrong or incomplete: an old classifier, no licence at all (UNKNOWN), or one side of a dual licence. Scanners read metadata, not the licence text.
Check
pip show -v <package> # differs from the project's LICENSE file, or offers a choice of licences syft <image> -o cyclonedx-json | grep -i license # what another tool readsFix
Record the real licence with the evidence as a reviewed exception in the scanner's configuration, and ask the project to fix its metadata.
Prevent it
Licence checks on pull requests (Dependency Review can deny licences).
Now learn it: break and fix it yourself in Licence violation, then take Level 04 · Dependencies (SCA). Open in the site
Dependency review is not supported on this repository
dependency-review-action
The error
Error: Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled along with GitHub Advanced Security on private repositories
What it means
dependency-review-action compares the dependency graph of a pull request's base and head. GitHub refused because the repository has no dependency graph the action can read.
Causes, most likely first
-
The dependency graph is off for the repository, so there is nothing to compare.
Check
Settings > Advanced Security: Dependency graph # disabledFix
Enable the dependency graph, and run the action on pull_request events: it compares a pull request's base and head.
-
The repository is private or internal without GitHub Code Security (part of Advanced Security), which the action needs there.
Check
gh api repos/<owner>/<repo> --jq '.visibility, .security_and_analysis' # private or internal, with Advanced Security or Code Security disabledFix
Have an organisation owner or security manager turn on Code Security for the repository.
Prevent it
Turn on the dependency graph for every repository through the organisation's security configuration.
Now learn it: break and fix it yourself in Licence violation, Vulnerable dependency with a fix, then take Level 04 · Dependencies (SCA). Open in the site
Malicious or typosquatted package
osv-scanner
The error
PRACTICE-2026-0031: Malicious code in reqeusts (PyPI): a typosquat of requests ...
What it means
OSV has an entry saying this package contains malicious code: it is malware, not a bug. Such packages usually act at install time (setup scripts) or on first import, so if it was installed, assume its code ran.
Causes, most likely first
-
Typosquat: the name is one letter off a popular package (reqeusts for requests), from a typo in a requirements file or a command.
Check
osv-scanner scan source -r . # MAL-... ids are known malicious packages git log -p -S <name> -- 'requirements*' # the commit that added the misspelled nameFix
Treat the machine or job that installed it as compromised: rotate every secret it could read, then remove the package and install the intended one.
-
Dependency confusion: the name is your internal package, but the installer also searched the public index and took a higher version someone published there.
Check
pip config debug # the index-url and extra-index-url in effect; they can hold a token, do not paste them pip index versions <internal-name> --index-url https://pypi.org/simple # found on the public index: confusionFix
Rotate what the installing job could read. Then use one index that serves internal names only from inside (a proxy that blocks public copies of them), with no --extra-index-url, and reserve your names on the public index.
Prevent it
Lock files with hashes; review new dependencies; scoped or reserved internal names.
Now learn it: break and fix it yourself in Typosquatted package, Dependency confusion, then take Level 05 · Supply chain and SBOM. Open in the site
Dependencies not pinned
osv-scanner, pip
The error
ERROR: In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
flask>=3.0 from -r requirements.txt (line 1)
What it means
pip is in hash-checking mode: a line in the file has --hash, or --require-hashes was given. In that mode every requirement must be pinned exactly with == and have a hash, so pip stops and lists the lines that do not.
Causes, most likely first
-
The file has ranges like flask>=3.0. They resolve to different versions over time, so scans and SBOMs no longer describe the build.
Check
grep -vE '==|^\s*(#|--hash)' requirements.txt # requirement lines without an exact pinFix
Generate a lock: pip-compile --generate-hashes requirements.in (npm ci with package-lock.json, and so on), and install only from it.
-
A transitive package is missing from the hashed file, so pip has to fetch it unpinned. The lock was edited by hand, or generated on another Python version or platform.
Check
Read the lines after "These do not:" # a package that is not in requirements.txt at all: a missing transitive pinFix
Regenerate the whole lock with pip-compile --generate-hashes on the Python version CI uses, instead of editing it.
-
Scorecard reports the same problem as pipCommand not pinned by hash: a pip install in a Dockerfile or workflow names packages directly.
Check
grep -rn 'pip install' Dockerfile .github/workflows # pip install <name> instead of -r requirements.txtFix
Install from the hashed lock file: pip install --require-hashes -r requirements.txt.
Prevent it
CI installs only from the lock file.
Now learn it: break and fix it yourself in Unpinned dependencies, then take Level 04 · Dependencies (SCA). Open in the site
Dependabot pull requests pile up, or merge blindly
Dependabot, gh
The error
#101 Bump imgresize from 2.1.0 to 2.1.4
#104 Bump plotkit from 1.6.0 to 2.0.0
What it means
Dependabot opens one pull request per update. Left alone they pile up and security fixes wait; merged automatically without limits, majors and breaking changes reach main with nobody looking.
Causes, most likely first
-
Nobody owns triage, so security fixes wait behind routine bumps.
Check
gh pr list --author app/dependabot # many open, some weeks oldFix
Merge security fixes first; group routine updates and schedule them.
-
An auto-merge workflow merges everything, majors included, sometimes before checks finish.
Check
cat .github/workflows/dependabot-automerge.yml # no update-type condition, or a direct merge without --autoFix
Auto-merge only dependabot[bot], only patch and minor (dependabot/fetch-metadata update-type), with gh pr merge --auto so required checks gate it. Leave majors for a person.
Prevent it
Group updates, schedule them, and protect main with required checks.
Now learn it: break and fix it yourself in Dependabot triage and safe auto-merge, then take Level 04 · Dependencies (SCA). Open in the site
Code
Semgrep: formatted SQL query (SQL injection)
semgrep
The error
❯❯❱ python.lang.security.audit.formatted-sql-query.formatted-sql-query
Detected possible formatted SQL query. Use parameterized queries instead.
What it means
Semgrep's formatted-sql-query rule found SQL text built with string formatting (an f-string, % or +) and passed to a database call. If any formatted part comes from a user, they can change the query itself: SQL injection.
Causes, most likely first
-
Outside input is placed into the SQL text, so the driver sends the attacker's text as part of the query.
Check
semgrep scan --config p/python app/ # file and line of the query Trace each formatted value back # one comes from a request, a file or another service: injectableFix
Pass values as parameters: cur.execute("... WHERE id = %s", (value,)). The placeholder depends on the driver: %s for psycopg, ? for sqlite3.
-
The formatted part is an identifier (a table, column or ORDER BY name), which parameters cannot carry.
Check
Look at what is formatted in # a table or column name, not a valueFix
Choose it from an allowlist and quote it with psycopg.sql.Identifier.
-
Only constants are formatted in, so it is a false positive.
Check
Trace each formatted value # all are literals in the codeFix
Prefer parameters anyway; otherwise suppress that one line with the reason: # nosemgrep: <rule-id>. See "A finding is a false positive".
Prevent it
An ORM or query builder; SAST on every pull request.
Now learn it: break and fix it yourself in SQL injection flagged by SAST, The scanner that could not fail, then take Level 03 · Static analysis (SAST). Open in the site
Semgrep: directly returned format string (XSS)
semgrep
The error
❯❯❱ python.flask.security.audit.directly-returned-format-string.directly-returned-format-string
What it means
Semgrep found a Flask route that returns a string built with formatting. Flask sends a returned string as HTML, so input inside it reaches the browser unescaped and can run as script: reflected cross-site scripting (XSS).
Causes, most likely first
-
Request input is formatted into the HTML without escaping.
Check
semgrep scan --config p/python app/ # the route curl -s '<url>/search?q=<script>alert(1)</script>' # the script tag comes back unchanged: exploitableFix
Render with a template (Jinja escapes by default), or escape each value with markupsafe.escape.
-
Only trusted values are formatted in, or the response is not HTML.
Check
curl -sI '<url>/search?q=x' # Content-Type is not text/html, or nothing user-controlled reaches the stringFix
Return the right type (jsonify for JSON); if it still matches, suppress that one line with the reason.
Prevent it
Templates, not f-strings, for HTML; a Content-Security-Policy as a second layer.
Now learn it: break and fix it yourself in Reflected XSS, then take Level 03 · Static analysis (SAST). Open in the site
Advanced Security must be enabled to use code scanning
CodeQL, upload-sarif
The error
Error: Advanced Security must be enabled for this repository to use code scanning.
What it means
CodeQL (or another tool through upload-sarif) finished its analysis, but GitHub refused to store the results: the repository does not accept code scanning results. The analysis itself worked.
Causes, most likely first
-
The repository is private or internal without GitHub Code Security (part of Advanced Security), which code scanning needs there.
Check
gh api repos/<owner>/<repo> --jq '.visibility, .security_and_analysis' # private or internal, with Advanced Security or Code Security disabledFix
Have an organisation owner or security manager turn on Code Security for the repository, then re-run the workflow.
-
Code scanning is switched off for the repository, which can happen on public ones too.
Check
Settings > Advanced Security (Code security on older pages) # code scanning not set upFix
Enable code scanning, keep security-events: write on the analysing job only, and re-run the workflow.
Prevent it
Enable code scanning through the organisation's security configuration, so new repositories have it from the start.
Now learn it: break and fix it yourself in SQL injection flagged by SAST, then take Level 03 · Static analysis (SAST). Open in the site
A finding is a false positive
semgrep, checkov, trivy
The error
(a finding you have proven cannot be exploited)
What it means
Scanner rules match patterns in code or config, not intent, so some findings cannot be exploited in your context. Proving that, and suppressing only that one finding, is part of the job.
Causes, most likely first
-
The flagged value comes from a fixed source: an allowlist, a constant, a validated enum. The rule cannot see that.
Check
Trace the input to the flagged line # it never carries outside data: write down whyFix
Suppress one rule on one line, with the reason above it: # nosemgrep: <rule-id>, #checkov:skip=<ID>:<reason>, a .trivyignore.yaml entry with statement. Ask a second reviewer.
-
It is test or example data: a fixture, a documented example key.
Check
Look at the path # tests/, docs/ or examples/Fix
Mark that line with the reason, or exclude that path for that rule only. Never a bare # nosemgrep, a global rule exclusion, or continue-on-error.
-
It is not a false positive after all: the safe-looking path has a way round.
Check
Ask a second reviewer to try to break it # they find an input that reaches itFix
Fix the code; see the entry for that rule.
Prevent it
Review suppressions like code (CODEOWNERS on the files and paths).
Now learn it: break and fix it yourself in False positive, suppressed responsibly, then take Level 11 · Release gates and exceptions. Open in the site
Containers
container has runAsNonRoot and image will run as root
kubelet
The error
Error: container has runAsNonRoot and image will run as root (pod: "api-...", container: api)
What it means
The Pod's securityContext says runAsNonRoot: true. Before starting the container, the kubelet checked which user the image runs as, found root (UID 0), and refused to start it. The Pod shows CreateContainerConfigError.
Causes, most likely first
-
The image has no USER instruction, so it runs as root, often inherited from the base image, and the Pod sets no runAsUser.
Check
docker inspect --format '{{.Config.User}}' <image> # empty: root by default kubectl get pod <pod> -o jsonpath='{.spec.securityContext.runAsUser} {.spec.containers[*].securityContext.runAsUser}' # empty: the Pod does not choose a userFix
Add USER 10001 (numeric) to the Dockerfile and make the files the app writes owned by that user; or set runAsUser: 10001 in the securityContext if the image works as that user. Keep runAsNonRoot: true.
-
The image sets USER 0 or USER root, or the base image does and yours does not override it.
Check
docker inspect --format '{{.Config.User}}' <image> # 0 or rootFix
A numeric non-root USER in your Dockerfile, after the steps that need root.
Prevent it
Hadolint and Trivy config (DS002) in CI.
Now learn it: break and fix it yourself in Image runs as root, then take Level 06 · Container images. Open in the site
image has non-numeric user, cannot verify user is non-root
kubelet
The error
Error: container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root
What it means
runAsNonRoot: true makes the kubelet prove the user is not root. The image says USER app, a name, and the kubelet does not read the image's /etc/passwd, so it cannot know app's UID and refuses to start the container.
Cause
-
The Dockerfile names the user instead of giving its number.
Check
docker inspect --format '{{.Config.User}}' <image> # a name, such as appFix
USER 10001 in the Dockerfile (create the user with that UID), or runAsUser: 10001 in the securityContext.
Prevent it
Always use numeric users in images.
Now learn it: break and fix it yourself in Image runs as root, then take Level 06 · Container images. Open in the site
Image scan: dozens of HIGH and CRITICAL in OS packages
trivy image, grype
The error
Total: 213 (UNKNOWN: 2, LOW: 118, MEDIUM: 61, HIGH: 24, CRITICAL: 5)
What it means
Trivy or Grype counted the known vulnerabilities in the image by severity. In a total this large, most come from operating system packages in the base image, not from your code or your dependencies.
Causes, most likely first
-
A full OS base image ships hundreds of packages the app never uses, each with its own advisories.
Check
trivy image --severity HIGH,CRITICAL --pkg-types os <image> # most findings are OS packages docker images <image> # a size near 1 GB points to a full baseFix
Multi-stage build: build in a full image, ship from slim, alpine or distroless.
-
The base image is old: the fixes exist, but this image was built before them.
Check
trivy image --severity HIGH,CRITICAL --ignore-unfixed <image> # what a rebuild on a current base would fix docker inspect --format '{{.Created}}' <image> # built weeks agoFix
Rebuild regularly on a current base, pinned by digest and updated by a bot.
-
The findings are in your app's packages, not the OS.
Check
trivy image --pkg-types library <image> # Python, npm or Java packagesFix
Upgrade those dependencies at their source; see "Critical vulnerability with a fixed version".
Prevent it
Pin base images by digest and let a bot propose updates.
Now learn it: break and fix it yourself in Base image full of CVEs, then take Level 06 · Container images. Open in the site
Failed to build installable wheels after a base image update
pip, docker build
The error
ERROR: Failed to build installable wheels for some pyproject.toml based projects (imgresize)
What it means
pip found no prebuilt wheel for this package on this Python version and platform, so it tried to build one from source, and that build failed. The real reason is earlier in the log, in the package's build output.
Causes, most likely first
-
A floating base tag (python:latest, python:3) moved to a newer Python that the package has no wheels for yet and does not build on.
Check
grep FROM Dockerfile # a tag without a fixed minor version docker run --rm <base> python -V # newer than the package supportsFix
Pin the version you tested, ideally with its digest: FROM python:3.12-slim@sha256:..., or upgrade the package to a release with wheels for it.
-
The base changed to slim or alpine, which has no compiler or headers, so the source build cannot run.
Check
Scroll up in the log # gcc: not found, or Python.h: No such file or directoryFix
Install the build tools in a build stage only, build the wheels there, and copy them into the final image.
-
The base is alpine, which uses musl: manylinux wheels do not apply, so pip builds from source unless the package publishes musllinux wheels.
Check
grep FROM Dockerfile # an alpine image pip download --only-binary=:all: --platform musllinux_1_2_x86_64 --no-deps <package> # fails: no musl wheelFix
Use a slim (Debian) base, or build the wheels in an alpine build stage with the tools installed.
Prevent it
No :latest in Dockerfiles; DS001 / CKV_DOCKER_7 in CI.
Now learn it: break and fix it yourself in Base image on :latest, then take Level 06 · Container images. Open in the site
A CVE that does not affect you blocks the release
trivy, OpenVEX
The error
│ libpractice-glyphs2 │ PRACTICE-2026-0140 │ HIGH │ affected │ 2.13.1-1 │ │
What it means
The scanner matched a vulnerable package version in the image. It cannot tell whether your product ever runs the vulnerable code, so it reports affected and the gate fails.
Causes, most likely first
-
The vulnerable code is in the package, but your product never runs it.
Check
trivy image --severity HIGH,CRITICAL <image> # the package and advisory Show that nothing in the product calls the vulnerable code # the advisory's function is never reachedFix
Write an OpenVEX statement: status not_affected, a justification such as vulnerable_code_not_in_execute_path, and an impact_statement. Pass it to the scanner: trivy image --vex vex.openvex.json <image>.
-
The product does reach it, so it is affected.
Check
Trace calls into the package # the vulnerable function is usedFix
Upgrade or remove the package. A not_affected statement here would be false.
Prevent it
Review VEX statements like code; revisit them when the product changes.
Now learn it: break and fix it yourself in VEX for a non-exploitable CVE, then take Level 11 · Release gates and exceptions. Open in the site
Infrastructure as code
CKV_AWS_20: S3 bucket allows public read
checkov
The error
Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
FAILED for resource: aws_s3_bucket_acl.invoices
What it means
Checkov found an S3 bucket ACL that grants READ to everyone (public-read, the AllUsers group). Anyone on the internet can then list the bucket's objects without signing in.
Causes, most likely first
-
The Terraform sets acl = "public-read" or a grant to AllUsers. It only takes effect if ACLs are enabled and the public access block is off, which the same code often changes to make it apply.
Check
checkov -d terraform # CKV_AWS_20 on the bucket's ACL resource aws s3api get-public-access-block --bucket <bucket> # BlockPublicAcls or IgnorePublicAcls false aws s3control get-public-access-block --account-id 111122223333 # the account-wide settingFix
Remove the ACL and set object ownership to BucketOwnerEnforced; set all four public access block settings to true. Share single objects with presigned URLs.
-
The bucket is also, or instead, public through a bucket policy with Principal "*". This check does not cover that, so passing CKV_AWS_20 is not proof it is private.
Check
aws s3api get-bucket-policy-status --bucket <bucket> # IsPublic: true is what AWS actually evaluatesFix
Narrow the policy to named principals or a CloudFront origin access control; BlockPublicPolicy then stops it coming back.
-
It is meant to be public, such as a static website.
Check
Read the bucket's purpose in the code or its tags # website hosting, public assetsFix
Serve it through CloudFront with origin access control and keep the bucket private; if it must be public, skip the check on that resource with the reason.
Prevent it
Account-level S3 Block Public Access.
Now learn it: break and fix it yourself in Public S3 bucket, then take Level 07 · Infrastructure as code. Open in the site
CKV_AWS_24: SSH open to 0.0.0.0/0
checkov
The error
Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
What it means
Checkov found a security group rule that allows TCP port 22 (SSH) from 0.0.0.0/0, every IPv4 address. Internet-wide scanners find open SSH within minutes and try passwords and known exploits.
Causes, most likely first
-
An ingress rule was opened to everyone for convenience, often while debugging.
Check
checkov -d terraform # the resource with CKV_AWS_24 aws ec2 describe-security-groups --filters Name=ip-permission.from-port,Values=22 Name=ip-permission.cidr,Values=0.0.0.0/0 --query 'SecurityGroups[].GroupId' # groups in the account open to everyone on 22Fix
Remove the rule and use SSM Session Manager, or allow only the VPN range. Moving SSH to another port is not a fix.
-
The rule opens a port range that includes 22, or all ports, which the check also flags.
Check
grep -n -B2 -A6 from_port terraform/*.tf # from_port 0 and to_port 65535, or protocol "-1"Fix
Open only the ports the service needs, from the sources that need them.
Prevent it
No inbound admin ports at all; moving SSH to another port is not a fix.
Now learn it: break and fix it yourself in SSH open to 0.0.0.0/0, then take Level 07 · Infrastructure as code. Open in the site
CKV_AWS_3 / CKV_AWS_16: unencrypted EBS or RDS
checkov
The error
Check: CKV_AWS_3: "Ensure all data stored in the EBS is securely encrypted"
What it means
Checkov found an EBS volume (CKV_AWS_3) or RDS instance (CKV_AWS_16) without encryption at rest. Anyone who gets a snapshot or the underlying storage can read the data, and most compliance rules require it.
Causes, most likely first
-
encrypted (EBS) or storage_encrypted (RDS) is false or missing, and missing means false.
Check
checkov -d terraform # the resource aws ec2 get-ebs-encryption-by-default # false: new volumes in this region are not encrypted unless askedFix
Set encrypted = true / storage_encrypted = true, with a KMS key if you need your own.
-
The resource already exists unencrypted. Changing the flag in Terraform forces a new resource, which for a database means a new, empty one if applied carelessly.
Check
terraform plan -lock=false # must be replacedFix
Plan first. Snapshot, copy the snapshot with encryption, restore from it and cut over; do not apply a replacement on a live database.
Prevent it
EBS encryption by default in each region.
Now learn it: break and fix it yourself in Unencrypted EBS and RDS, then take Level 07 · Infrastructure as code. Open in the site
CKV_AWS_62: IAM policy allows * on *
checkov, trivy config
The error
Check: CKV_AWS_62: "Ensure IAM policies that allow full "*-*" administrative privileges are not created"
What it means
Checkov found an IAM policy statement with Action "*" on Resource "*": full administrator access. Whoever holds it (a role, a user, a CI job) can do anything in the account, including granting themselves more.
Causes, most likely first
-
It was a quick fix for an AccessDenied error: widening to * made the error go away.
Check
checkov -d terraform # the policy git log -p -S 'Action' -- terraform/ # the commit that widened it, and the error it was fixingFix
Name the actions and resources the code needs. IAM Access Analyzer can generate a policy from the role's CloudTrail activity.
-
It is meant as an admin role, but it is attached to something that is not a person (CI, an app).
Check
aws iam list-entities-for-policy --policy-arn <arn> # roles and users that have itFix
Keep admin for break-glass human roles with MFA; give CI and apps their own narrow roles.
Prevent it
Review IAM changes like code; permission boundaries.
Now learn it: break and fix it yourself in IAM policy with * on *, then take Level 07 · Infrastructure as code. Open in the site
CKV_AWS_79: instance metadata service v1 enabled
checkov
The error
Check: CKV_AWS_79: "Ensure Instance Metadata Service Version 1 is not enabled"
What it means
Checkov found an instance or launch template that allows IMDSv1: the instance metadata service answers a plain GET without a session token. Any bug that makes the app fetch a URL (SSRF) can then read the instance role's credentials from 169.254.169.254.
Causes, most likely first
-
metadata_options sets http_tokens = "optional", or is missing and the AMI or account default allows v1.
Check
checkov -d terraform # CKV_AWS_79 on the resource aws ec2 describe-instances --query 'Reservations[].Instances[].[InstanceId,MetadataOptions.HttpTokens]' # optional: v1 allowedFix
metadata_options { http_tokens = "required" } on instances and launch templates. Fix the SSRF itself too: allowlist the hosts the app may fetch.
-
Old software on the instance still uses v1 (an old SDK or agent), so it was left on.
Check
aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name MetadataNoToken --dimensions Name=InstanceId,Value=<id> --start-time <start> --end-time <end> --period 3600 --statistics Sum # above 0: something still calls v1Fix
Update that software, then require tokens.
Prevent it
Set the account default to IMDSv2 only.
Now learn it: break and fix it yourself in IMDSv1 and an SSRF, then take Level 07 · Infrastructure as code. Open in the site
Kubernetes
kube-linter: privileged container or docker.sock mounted
kube-linter
The error
container "debug" is privileged (check: privileged-container, remediation: Do not run your container as privileged unless it is required.)
What it means
kube-linter found a container with securityContext.privileged: true, or a hostPath mount of the container runtime socket. Either gives the container root on the node: it can reach every other container and the node's files.
Causes, most likely first
-
A debug or tooling sidecar was left in the manifest with privileged: true.
Check
kube-linter lint k8s/ # privileged-container and the container's name grep -rn 'privileged: true' k8s/Fix
Remove the sidecar; use kubectl debug for live debugging.
-
A container mounts /var/run/docker.sock (or containerd's socket) to build or inspect images. Control of the runtime is control of the node.
Check
grep -rn -A3 hostPath k8s/ # docker.sock or containerd.sockFix
Build images in CI, or with a rootless builder (BuildKit rootless) that needs no socket.
-
A node agent (network plugin, storage driver, monitoring) really needs it.
Check
kubectl get ds -A # a vendor DaemonSetFix
Run it in its own namespace with privileged allowed only there, and keep restricted everywhere else.
Prevent it
Pod Security Admission enforce: restricted on the namespace.
Now learn it: break and fix it yourself in Privileged Pod with the Docker socket, then take Level 08 · Kubernetes security. Open in the site
violates PodSecurity "restricted:latest"
Pod Security Admission
The error
Error from server (Forbidden): error when creating "deploy.yaml": pods "api" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "api" must set securityContext.allowPrivilegeEscalation=false), runAsNonRoot != true (pod or container "api" must set securityContext.runAsNonRoot=true)
What it means
Pod Security Admission rejected the Pod. The namespace has the label pod-security.kubernetes.io/enforce=restricted, and the message lists each rule of the restricted profile the Pod breaks, with the field to set: here allowPrivilegeEscalation and runAsNonRoot.
Causes, most likely first
-
The Pod spec does not set the fields the restricted profile requires; a field left out counts as not set.
Check
kubectl get ns <namespace> --show-labels # pod-security.kubernetes.io/enforce=restricted kubectl label --dry-run=server --overwrite ns <namespace> pod-security.kubernetes.io/enforce=restricted # warns for each running Pod that would failFix
Fix the Pod, not the namespace: runAsNonRoot: true and a numeric runAsUser, allowPrivilegeEscalation: false, capabilities drop ALL, seccompProfile type RuntimeDefault. Do not lower the namespace label to make it pass.
-
It came through a Deployment: kubectl only warned (would violate PodSecurity), and the ReplicaSet then failed to create Pods, so the Deployment looks applied but has none.
Check
kubectl get events -n <namespace> --field-selector reason=FailedCreate # the same violates PodSecurity messageFix
The same fix in the Deployment's Pod template; the ReplicaSet then creates the Pods.
-
The image runs as root, so with the fields set it still fails at start (runAsNonRoot).
Check
docker inspect --format '{{.Config.User}}' <image> # empty, 0 or rootFix
Build the image with a numeric USER so it can run that way.
Prevent it
Check manifests against the restricted profile in CI (kube-linter, conftest) and use the warn label in staging first.
Now learn it: break and fix it yourself in Image runs as root, Privileged Pod with the Docker socket, then take Level 08 · Kubernetes security. Open in the site
conftest: namespace has no default-deny NetworkPolicy
conftest
The error
FAIL - k8s/deployment.yaml - main - namespace "shop" has no default-deny NetworkPolicy
What it means
A conftest (OPA) policy in this repository requires each namespace to have a NetworkPolicy that denies traffic by default. Kubernetes allows all Pod-to-Pod traffic, across namespaces, until a policy selects a Pod.
Causes, most likely first
-
No NetworkPolicy exists for the namespace, so every Pod can reach every other Pod, the database included.
Check
kubectl get networkpolicy -n shop # No resources found kubectl exec -n other deploy/x -- nc -zv -w 3 postgres.shop 5432 # succeeded: reachable from another namespaceFix
A default-deny-ingress policy (podSelector: {}, policyTypes: [Ingress]), then explicit allows for each flow: ingress controller, monitoring, app to database.
-
The policy existed and was deleted, or moved to a folder conftest does not read.
Check
git log --diff-filter=D --oneline -- k8s/networkpolicy.yaml # the commit that deleted itFix
Restore it with git revert of that commit, or move it back to the folder conftest tests.
-
Policies exist but do nothing: the cluster's network plugin does not enforce them (flannel alone, for example). The API accepts them anyway.
Check
kubectl get networkpolicy -n shop # a deny policy is listed kubectl exec -n other deploy/x -- nc -zv -w 3 postgres.shop 5432 # still succeeds: not enforcedFix
Use a network plugin that enforces NetworkPolicy (Calico, Cilium, or your cloud's option), and test a deny after setting it up.
Prevent it
A policy check in CI and an allow rule added with each new flow.
Now learn it: break and fix it yourself in Missing NetworkPolicy, then take Level 08 · Kubernetes security. Open in the site
ServiceAccount bound to cluster-admin
kube-linter
The error
cluster-admin role is bound to the service account api (check: cluster-admin-role-binding, ...)
What it means
kube-linter found a binding that gives a ServiceAccount the cluster-admin role: every Pod using that account can do anything in every namespace, including reading all Secrets.
Causes, most likely first
-
A forbidden error (cannot get resource ...) was fixed with the biggest role instead of the one permission that was missing.
Check
kubectl auth can-i --list --as=system:serviceaccount:<ns>:<sa> -n <ns> # *.* with every verb kubectl logs deploy/<app> --previous # the original forbidden message names the verb and resource it neededFix
A Role with the exact verbs and resources (and resourceNames), and a RoleBinding in that namespace; delete the cluster-admin binding.
-
A Helm chart or operator asked for it at install.
Check
kubectl get clusterrolebinding -o wide | grep cluster-admin # the binding and its subjectsFix
Use the chart's RBAC values to scope it, or ask the vendor what it needs; do not accept cluster-admin for an app.
Prevent it
Review RBAC changes; kubectl auth can-i in tests.
Now learn it: break and fix it yourself in ServiceAccount bound to cluster-admin, then take Level 08 · Kubernetes security. Open in the site
Kubernetes Secret committed to git
conftest, gitleaks
The error
FAIL - k8s/secret.yaml - main - Secret "orders-db" is committed to git; create it from the secret store
What it means
A conftest policy in this repository rejects any kind: Secret manifest in git. The values under data: are base64, an encoding anyone can reverse (base64 -d), not encryption, so everyone who can read the repository has them.
Causes, most likely first
-
A Secret with real values was committed, usually to make a deploy work.
Check
git log --stat -- k8s/secret.yaml # when it was added git branch -r --contains <commit> # pushed: treat the values as exposed Do not decode the real values to check them.Fix
Rotate every value first, then remove the file and purge it from history. Sync from a secret store (External Secrets) or commit it encrypted (Sealed Secrets, SOPS).
-
The values are placeholders or already encrypted (a SOPS file), and the policy only looks at kind.
Check
grep -n -E '^kind:|^sops:' k8s/secret.yaml # a sops: block: encryptedFix
Keep it, and let the policy allow SOPS-encrypted files and SealedSecret objects.
Prevent it
A policy that rejects kind: Secret in the repository.
Now learn it: break and fix it yourself in Kubernetes Secret committed to git, then take Level 08 · Kubernetes security. Open in the site
CI/CD and supply chain
zizmor: unpinned action reference
zizmor
The error
error[unpinned-uses]: unpinned action reference
What it means
zizmor found a uses: line that names an action by tag or branch (owner/action@v1). Whoever controls that repository can move the tag to new code, and your workflow then runs it, with your token and secrets.
Causes, most likely first
-
The action is referenced by a tag or branch, as most examples show.
Check
zizmor .github/workflows/ # each unpinned uses: line git ls-remote https://github.com/<owner>/<action> 'refs/tags/<tag>*' # the SHA to pin toFix
uses: owner/action@<40-char sha> # vX.Y.Z. For an annotated tag, take the SHA on the ^{} line: that is the commit.
-
A docker:// action or container image is referenced by tag.
Check
grep -n 'docker://' .github/workflows/*.yml # an image:tag without @sha256:Fix
Pin the image by digest: docker://image@sha256:...
Prevent it
Dependabot for github-actions; an org policy requiring SHA pinning.
Now learn it: break and fix it yourself in Unpinned GitHub Action, then take Level 09 · Securing the pipeline. Open in the site
Unable to resolve action, unable to find version
GitHub Actions
The error
Error: Unable to resolve action `actions/setup-python@1111...`, unable to find version `1111...`
What it means
The runner tried to download the action at the ref after the @, and GitHub found no commit with that SHA in that repository, so the job failed before any step ran.
Causes, most likely first
-
The SHA is a tag object, not a commit: annotated tags have their own SHA, which git ls-remote shows on the line without ^{}.
Check
git ls-remote https://github.com/actions/setup-python 'refs/tags/<tag>*' # your SHA is on the line without ^{}: a tag objectFix
Use the SHA on the ^{} line, which is the commit.
-
A typo, a shortened SHA, or a SHA copied from another repository.
Check
gh api repos/<owner>/<action>/commits/<sha> --jq .sha # Not Found or No commit found: not in that repositoryFix
Copy the SHA again from git ls-remote of the right repository, or let Dependabot or Renovate write the pins.
-
The repository was renamed, deleted or made private.
Check
gh api repos/<owner>/<action> --jq .full_name # another name, or Not FoundFix
Point at the current repository, or move to a maintained action, pinned by SHA.
Prevent it
Let a bot write pins.
Now learn it: break and fix it yourself in Unpinned GitHub Action, then take Level 09 · Securing the pipeline. Open in the site
zizmor: code injection via template expansion
zizmor
The error
error[template-injection]: code injection via template expansion
What it means
zizmor found ${{ ... }} with data an outsider controls (an issue title, a branch name, a pull request body) inside a run: script. GitHub pastes the text into the script before the shell starts, so a title like "; curl ... | sh" becomes a command, run with the job's token and secrets.
Causes, most likely first
-
An event field is expanded directly in run:. Quoting does not help: the substitution happens before the shell parses anything.
Check
zizmor .github/workflows/ # the line and the expressionFix
Pass it through the environment: env: TITLE: ${{ github.event.issue.title }}, then "$TITLE" in the script, quoted.
-
The same inside actions/github-script's script: input, which runs as JavaScript.
Check
grep -n -A6 github-script .github/workflows/*.yml # ${{ github.event... }} inside script:Fix
Read it from context.payload in the script, or pass it through env and process.env.
Prevent it
Never expand event fields in run:.
Now learn it: break and fix it yourself in Script injection in a workflow, then take Level 09 · Securing the pipeline. Open in the site
zizmor: overly broad permissions
zizmor, Scorecard
The error
warning[excessive-permissions]: overly broad permissions
What it means
zizmor found a workflow or job whose GITHUB_TOKEN can do more than it needs. If any step is compromised, that token can push code, change releases or approve pull requests.
Causes, most likely first
-
permissions: write-all at the top or on a job. zizmor reports this as error.
Check
zizmor .github/workflows/ # error[excessive-permissions], uses write-all permissionsFix
permissions: contents: read at the top; add packages: write or id-token: write only on the job that needs it.
-
No permissions block, so the token gets the repository default, which may be write for everything. zizmor reports this as warning: default permissions used due to no permissions: block.
Check
gh api repos/<owner>/<repo>/actions/permissions/workflow # default_workflow_permissions: writeFix
Add permissions: contents: read to the workflow, and set the repository default to read-only.
Prevent it
Set the repository default to read-only.
Now learn it: break and fix it yourself in Overly broad GITHUB_TOKEN, Low OpenSSF Scorecard, then take Level 09 · Securing the pipeline. Open in the site
denied: installation not allowed to Write organization package
docker push (GHCR)
The error
denied: installation not allowed to Write organization package
What it means
GitHub Container Registry refused the push. installation means the job's GITHUB_TOKEN (it belongs to the GitHub Actions app installation), and Write organization package means that token has no write access to this package.
Causes, most likely first
-
The job's GITHUB_TOKEN lacks packages: write, because the workflow sets permissions without it.
Check
grep -n -A4 permissions .github/workflows/ci.yml # no packages: write on the job that pushes In the run log, Set up job > GITHUB_TOKEN Permissions # Packages: readFix
Add permissions: packages: write to that job only.
-
The package already exists and this repository has no write access to it: it was first pushed from another repository or by a person.
Check
The package's settings > Manage Actions access # this repository missing, or without the Write roleFix
Add the repository with the Write role there, or connect the package to the repository. Not a personal access token.
Prevent it
Per-job permissions.
Now learn it: break and fix it yourself in Overly broad GITHUB_TOKEN, then take Level 09 · Securing the pipeline. Open in the site
Resource not accessible by integration (GITHUB_TOKEN)
GitHub Actions
The error
RequestError [HttpError]: Resource not accessible by integration
What it means
A step called the GitHub API with the job's GITHUB_TOKEN, and GitHub said that token may not do this. integration means the GitHub Actions app the token belongs to. It is a permission problem, not a missing resource.
Causes, most likely first
-
The job's permissions lack the one the call needs: issues: write or pull-requests: write to comment, security-events: write to upload SARIF, contents: write to push.
Check
grep -n -A6 permissions .github/workflows/<workflow>.yml # the permission is missing In the run log, Set up job > GITHUB_TOKEN Permissions # what the token actually hadFix
Add the one permission the step needs to that job, for example permissions: pull-requests: write.
-
The run comes from a pull request from a fork (or from Dependabot): GitHub makes the token read-only whatever the workflow asks for.
Check
In the run log, Set up job > GITHUB_TOKEN Permissions # all read although the workflow asks for writeFix
Do not switch to pull_request_target with a checkout of the pull request's code. Split the work: a read-only job on pull_request, and a separate trusted workflow that comments without running fork code.
-
A job-level permissions block replaced the workflow-level one, so a permission set at the top is missing in that job.
Check
grep -n permissions .github/workflows/<workflow>.yml # blocks both at the top and on the jobFix
List every permission the job needs on the job itself.
Prevent it
Default the workflow to permissions: contents: read and grant more per job.
Now learn it: break and fix it yourself in Overly broad GITHUB_TOKEN, then take Level 09 · Securing the pipeline. Open in the site
Credentials could not be loaded (OIDC id-token missing)
aws-actions/configure-aws-credentials
The error
It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission?
Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers
What it means
configure-aws-credentials was given role-to-assume but could not get a GitHub OIDC token to exchange, so it fell back to looking for keys and found none. The first line is the action's own hint: the job may not request an ID token.
Causes, most likely first
-
The job has no permissions: id-token: write, so the runner gets no way to request a token.
Check
grep -n -B2 -A6 configure-aws-credentials .github/workflows/*.yml # no id-token: write for that job In the run log, Set up job > GITHUB_TOKEN Permissions # no IdToken: writeFix
Add id-token: write (with contents: read) to that job. Check the role's trust policy names this repository and branch.
-
id-token: write is set for the workflow, but the job has its own permissions block, which replaces the workflow's.
Check
grep -n -A4 permissions .github/workflows/<workflow>.yml # a job-level block without id-tokenFix
Add id-token: write to the job's block.
-
The job is in a reusable workflow, and the calling job did not grant id-token: write; a called workflow cannot have more than its caller.
Check
grep -n -B4 -A4 'uses: ./.github/workflows' .github/workflows/*.yml # the calling job's permissionsFix
Grant id-token: write on the calling job too.
Prevent it
OIDC for every cloud, no stored keys.
Now learn it: break and fix it yourself in Long-lived cloud keys in CI, then take Level 09 · Securing the pipeline. Open in the site
Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
aws-actions/configure-aws-credentials
The error
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
What it means
The job got a GitHub OIDC token and asked AWS STS to exchange it for the role's credentials (AssumeRoleWithWebIdentity). AWS refused: the role's trust policy did not accept this token, or the role is not the one you think.
Causes, most likely first
-
The trust policy's sub condition names another repository, branch or environment. A job with environment: prod sends repo:<owner>/<repo>:environment:prod, not a ref, and a pull request sends repo:<owner>/<repo>:pull_request.
Check
aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument # the sub and aud conditions Compare token.actions.githubusercontent.com:sub with the job # repository, ref, or the environment if the job has oneFix
Make the condition match exactly: StringEquals token.actions.githubusercontent.com:sub = repo:<owner>/<repo>:ref:refs/heads/main (or :environment:<name>). Do not widen sub to repo:<owner>/* or * to make it pass.
-
The aud condition is not sts.amazonaws.com, or the action was given another audience.
Check
aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument # token.actions.githubusercontent.com:aud is something elseFix
Set aud to sts.amazonaws.com and leave the action's audience at its default.
-
The account has no IAM OIDC provider for GitHub, or the trust policy's Federated ARN names another account. The message then says No OpenIDConnect provider found.
Check
aws iam list-open-id-connect-providers # no token.actions.githubusercontent.comFix
Create the IAM OIDC provider for https://token.actions.githubusercontent.com with audience sts.amazonaws.com.
-
role-to-assume is the wrong ARN: another account, a typo, or a deleted role.
Check
grep -n role-to-assume .github/workflows/*.yml # compare account ID and name with aws iam get-roleFix
Fix the ARN; keep it in one variable rather than in many workflows.
Prevent it
Keep roles and trust policies in Terraform, one role per repository and environment.
Now learn it: break and fix it yourself in Long-lived cloud keys in CI, then take Level 09 · Securing the pipeline. Open in the site
A secret appears in CI logs (base64 or transformed)
GitHub Actions
The error
Run echo "$PIP_TOKEN" | base64
UFJBQ1RJQ0Ut...
What it means
GitHub masks a secret's exact value in logs as ***. Here the step printed the secret base64-encoded, which is a different string, so it appears in clear and anyone with read access to the logs can decode it.
Causes, most likely first
-
A debug step printed the secret transformed (base64, reversed, split, URL-encoded). Masking only matches the exact value.
Check
gh run view <id> --log | grep -n -A1 base64 # the step and the encoded line; it is the secret, do not paste itFix
Rotate the secret first and update it where CI reads it. Then remove the step and delete the affected runs: gh run delete <id>.
-
A value derived from a secret (a token fetched at run time, a field of a JSON secret) was never registered for masking.
Check
grep -n add-mask .github/workflows/*.yml # nothing before the value is usedFix
Rotate it, register it with ::add-mask:: as soon as it is created, and never print it.
Prevent it
Never print secrets; review debug steps.
Now learn it: break and fix it yourself in Secret printed in CI logs, then take Level 09 · Securing the pipeline. Open in the site
Scorecard: Dangerous-Workflow 0 (pull_request_target)
Scorecard, zizmor
The error
| 0 / 10 | Dangerous-Workflow | dangerous workflow patterns detected |
What it means
OpenSSF Scorecard scored Dangerous-Workflow 0 out of 10: at least one workflow runs untrusted code with access to secrets or a write token.
Causes, most likely first
-
A pull_request_target (or workflow_run) workflow checks out the pull request's code and runs it. That trigger runs with secrets and a write token, so fork code gets them.
Check
zizmor .github/workflows/ # dangerous-triggers grep -n -A12 pull_request_target .github/workflows/*.yml # a checkout with ref: github.event.pull_request.head.sha or head.refFix
Use on: pull_request for anything that runs pull request code; keep pull_request_target for metadata only (labels, comments), with no checkout of the pull request.
-
Script injection: an untrusted ${{ github.event... }} is expanded inside run:.
Check
zizmor .github/workflows/ # template-injectionFix
Pass it through env: and use "$VAR"; see "zizmor: code injection via template expansion".
Prevent it
Keep pull_request_target for metadata only.
Now learn it: break and fix it yourself in Low OpenSSF Scorecard, then take Level 10 · Signing and provenance. Open in the site
trivy: TOOMANYREQUESTS downloading the vulnerability DB
trivy
The error
FATAL Fatal error run error: init error: DB error: failed to download vulnerability DB ...
* GET https://ghcr.io/v2/aquasecurity/trivy-db/manifests/2: TOOMANYREQUESTS: retry-after: 663.26µs, allowed: 44000/minute
What it means
Before scanning, Trivy downloads its vulnerability database from a container registry. The registry answered TOOMANYREQUESTS: too many anonymous downloads from your address, so Trivy stopped.
Causes, most likely first
-
Shared CI runners share addresses, and every job downloads the database again.
Check
grep -rn -B2 -A6 'actions/cache' .github/workflows # no cache for Trivy's cache directoryFix
Cache the database between runs.
-
The scan pulls from ghcr.io only: an older Trivy, or --db-repository (TRIVY_DB_REPOSITORY) set to ghcr.io. Recent versions try mirror.gcr.io first and fall back to ghcr.io.
Check
trivy --version grep -rn 'db-repository\|TRIVY_DB_REPOSITORY' .github/workflows # pinned to ghcr.ioFix
Use mirrors, in order: --db-repository mirror.gcr.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2, or upgrade Trivy.
Prevent it
Mirror and cache every scanner database; never continue-on-error.
Now learn it: break and fix it yourself in Scanner database rate limited, then take Level 11 · Release gates and exceptions. Open in the site
--skip-db-update cannot be specified on the first run
trivy
The error
FATAL Fatal error flag error: --skip-db-update cannot be specified on the first run
What it means
--skip-db-update tells Trivy to use the database it already has, and Trivy found none in its cache directory, so it has nothing to scan with.
Causes, most likely first
-
A fresh runner has an empty cache, and the job skips the download.
Check
ls ~/.cache/trivy/db # No such file or directory: no databaseFix
Restore the cache before the scan, or download from a mirror on the first run.
-
A cache is restored, but to another path than Trivy reads (--cache-dir or TRIVY_CACHE_DIR; ~/.cache/trivy by default).
Check
grep -rn 'cache-dir\|TRIVY_CACHE_DIR\|path:' .github/workflows # the restored path and Trivy's path differFix
Restore the cache to the path Trivy reads, or pass that path with --cache-dir.
-
The cache key missed, so nothing was restored.
Check
In the run log, the cache step # Cache not found for input keysFix
Add restore-keys so yesterday's database is restored when today's is missing.
Prevent it
Cache keyed by date.
Now learn it: break and fix it yourself in Scanner database rate limited, then take Level 11 · Release gates and exceptions. Open in the site
toomanyrequests: You have reached your pull rate limit (Docker Hub)
docker pull, docker build
The error
toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit
What it means
Docker Hub refused the pull because the pull limit was reached. Anonymous pulls are counted per IP address; logged-in pulls are counted per account, with a higher limit.
Causes, most likely first
-
The job pulls without logging in, and shared CI runners share addresses, so other people's pulls count against the same limit.
Check
grep -rn 'FROM\|image:' Dockerfile .github/workflows # images from Docker Hub grep -rn docker/login-action .github/workflows # no login in the job that pullsFix
Log in before pulling: docker/login-action with a Docker Hub access token kept as a secret (not the account password).
-
The job logs in, but after the pull or in another job, or the account's own limit is used up.
Check
The order of steps in the run log # the login comes after the failing pull, or is missing in this jobFix
Log in first in every job that pulls. For heavy use, pull from a mirror (mirror.gcr.io/library/<image> or public.ecr.aws/docker/library/<image>, pinned by digest) and cache the build so unchanged layers are not pulled again.
Prevent it
Copy the base images you depend on into your own registry and pin them by digest.
Now learn it: break and fix it yourself in Scanner database rate limited, Base image on :latest, then take Level 11 · Release gates and exceptions. Open in the site
401 Client Error: Unauthorized from the package index
pip
The error
ERROR: 401 Client Error: Unauthorized for url: https://pypi.fernleaf.internal/simple/fernleaf-billing/
What it means
pip asked the private index for the package's page and got HTTP 401 Unauthorized: the request had no valid credentials. (A 403 would mean the credentials were valid but not allowed this package.)
Causes, most likely first
-
The CI secret is stale, usually right after the token was rotated, so pip sends a revoked token.
Check
gh secret list # when each secret was last updated aws secretsmanager describe-secret --secret-id <id> --query '[LastChangedDate,LastRotatedDate]' # changed after the CI secret was setFix
Update the CI secret with the current value: gh secret set NAME, which reads it from stdin, so it is not on the command line.
-
The token is empty because this run cannot read the secret: a pull request from a fork, a Dependabot run (which gets Dependabot secrets, not Actions secrets), or an environment secret in a job without that environment.
Check
In the job: test -n "$PIP_TOKEN" && echo set || echo empty # empty: the secret did not reach the jobFix
Give that kind of run the secret (add it as a Dependabot secret, or set the job's environment), or do not run the step there.
-
The credentials are right but sent wrongly: the wrong user name for that index, or another index URL than the one holding the token.
Check
pip config debug # the index-url in effect; it can hold a token, do not paste itFix
Use the user name the index documents for tokens, and set the URL in one place (PIP_INDEX_URL built from the secret).
Prevent it
Fetch the token at run time (OIDC to the secret store) instead of copying it.
Now learn it: break and fix it yourself in Secret passed as a build argument, Secret printed in CI logs, then take Level 09 · Securing the pipeline. Open in the site
cosign: no signatures found / no matching signatures
cosign, Kyverno
The error
Error: no signatures found
main.go:74: error during command execution: no signatures found
What it means
cosign looked up signatures for the image's digest and found none at all. Signatures belong to a digest, not a tag, so this is about the exact image bytes you asked about. (Signed by another identity gives no matching signatures instead: see the next entry.)
Causes, most likely first
-
The image was never signed: built on a laptop, or in a job without a signing step.
Check
cosign tree <image> # no signatures listedFix
Sign in CI (keyless) and deploy what CI built.
-
The tag now points to a rebuild that was not signed, or one digest was signed and another verified (a multi-arch index against one platform's image).
Check
docker buildx imagetools inspect <image> # the digest the tag resolves to now; compare with the digest CI signed (in the CI log)Fix
Deploy and verify by digest, and sign the digest your deployments use.
-
The signature was stored in another repository (COSIGN_REPOSITORY), so cosign does not look for it there.
Check
grep -rn COSIGN_REPOSITORY .github/workflows # set when signingFix
Set the same COSIGN_REPOSITORY when verifying, and give the verifier read access to it.
Prevent it
Admission control in enforce mode.
Now learn it: break and fix it yourself in Unsigned image deployed, then take Level 10 · Signing and provenance. Open in the site
cosign: none of the expected identities matched what was in the certificate
cosign, Kyverno
The error
Error: no matching signatures: none of the expected identities matched what was in the certificate, got subjects [https://github.com/app/api/.github/workflows/release.yml@refs/tags/v1.4.0] with issuer https://token.actions.githubusercontent.com
What it means
The image is signed, and cosign read the signing certificate, but its identity is not the one you asked for. got subjects [...] is the identity that actually signed: for GitHub keyless signing, the workflow file and the ref that ran it.
Causes, most likely first
-
The trusted workflow signed it, but from another ref: a tag build signs as @refs/tags/v1.4.0, a main build as @refs/heads/main, and you asked for the other.
Check
Read got subjects # the same workflow, a different refFix
Name it exactly: --certificate-identity https://github.com/<owner>/<repo>/.github/workflows/release.yml@refs/tags/<tag>, or --certificate-identity-regexp anchored to that workflow for all tags, with --certificate-oidc-issuer https://token.actions.githubusercontent.com.
-
Another workflow or repository signed it, for example a test workflow or a fork.
Check
Read got subjects # another workflow file or repositoryFix
Do not deploy it: rebuild and sign it in the release workflow. Never widen the identity to .* to make verification pass.
-
A reusable workflow did the signing, so the certificate names that workflow, not the one that called it.
Check
Read got subjects # a workflow path in another repository, or a shared workflowFix
Trust the reusable workflow's identity, pinned to its ref.
Prevent it
One release workflow signs; the admission policy names that workflow and its ref.
Now learn it: break and fix it yourself in Unsigned image deployed, then take Level 10 · Signing and provenance. Open in the site
admission webhook denied the request: failed to verify image
Kyverno
The error
admission webhook "mutate.kyverno.svc-fail" denied the request: ... failed to verify image ...: no signatures found
What it means
Kyverno's admission webhook checked the image's signature against the cluster policy before admitting the Pod, and refused it. The text after failed to verify image says why: here, no signatures found.
Causes, most likely first
-
The image has no signature from the identity the policy trusts: built outside CI, or a tag that moved to an unsigned rebuild.
Check
kubectl get clusterpolicy # the verifyImages policy cosign verify --certificate-identity <identity from the policy> --certificate-oidc-issuer https://token.actions.githubusercontent.com <image> # fails here too: not signed by itFix
Deploy an image built and signed by CI; do not switch the policy to Audit.
-
Kyverno could not read the registry (a private registry without credentials for Kyverno). The message then says UNAUTHORIZED or failed to fetch, and the image may be signed.
Check
cosign verify ... <image> # passes from your machine but not in the cluster: Kyverno's registry accessFix
Give Kyverno pull credentials for that registry (imageRegistryCredentials in the policy); not a weaker policy.
Prevent it
Deploy only from the pipeline.
Now learn it: break and fix it yourself in Unsigned image deployed, then take Level 10 · Signing and provenance. Open in the site
Release gate: SBOM required but missing
policy gate
The error
policy: sbom: required, but the image has no CycloneDX attestation
What it means
This repository's release gate requires a CycloneDX SBOM attached to the image as a signed attestation, and found none for the image's digest.
Causes, most likely first
-
No SBOM is generated in the pipeline.
Check
grep -rn 'syft\|sbom' .github/workflows # no SBOM stepFix
syft <image> -o cyclonedx-json=sbom.cdx.json in the release job, then attest it (below).
-
The SBOM was generated but not attached, or attached with another predicate type (SPDX) or to another digest.
Check
cosign tree <image> # no attestations on this digest cosign verify-attestation --type cyclonedx --certificate-identity <workflow-ref> --certificate-oidc-issuer https://token.actions.githubusercontent.com <image> # none of the attestations matched the predicate type: another typeFix
cosign attest --type cyclonedx --predicate sbom.cdx.json <image@digest>, in the job that pushes the image.
Prevent it
SBOM and attestation as required CI steps.
Now learn it: break and fix it yourself in Missing SBOM for an audit, then take Level 05 · Supply chain and SBOM. Open in the site
Commits reach main without review
GitHub, Scorecard
The error
| 0 / 10 | Branch-Protection | branch protection not enabled on development/release branches |
What it means
Scorecard could not find branch protection on the default or release branches, so commits can reach main without review or checks.
Causes, most likely first
-
Branch protection was turned off or relaxed: no required reviews, admins exempt (enforce_admins false), or force pushes allowed.
Check
gh api repos/<owner>/<repo>/branches/main/protection # Branch not protected, or the weak settings git log --oneline origin/main # commits that reached main without a pull requestFix
Revert unreviewed changes with git revert, keeping the record. Restore the policy: gh api -X PUT repos/<owner>/<repo>/branches/main/protection --input branch-protection.json, with enforce_admins true.
-
main is protected by a ruleset, not classic protection. The classic API then says Branch not protected, and Scorecard may score 0 when its token cannot read the rules.
Check
gh api repos/<owner>/<repo>/rules/branches/main # the ruleset rules that apply to mainFix
If the rules require reviews and block force pushes, nothing needs restoring; run Scorecard with a token that can read them.
Prevent it
Keep the policy in code and alert on changes to it (audit log, rulesets).
Now learn it: break and fix it yourself in Branch protection bypassed, then take Level 09 · Securing the pipeline. Open in the site
A tag was re-pushed and the cluster runs different code
Kyverno, registry
The error
verifyImages:
- imageReferences: ["ghcr.io/fernleaf/*"]
mutateDigest: false
verifyDigest: false
What it means
This Kyverno verifyImages rule checks the signature but neither pins the Pod to the verified digest (mutateDigest: false) nor requires a digest (verifyDigest: false). The tag is checked once, and a later restart can pull whatever the tag points to by then.
Causes, most likely first
-
A tag was re-pushed (a rebuild, or someone pushing over it), and Pods deployed by tag pulled the new image when they restarted.
Check
kubectl get pods -o jsonpath='{.items[*].status.containerStatuses[*].imageID}' # different digests for the same tag cosign triangulate <image> # where its signature lives; compare digests with the one CI signedFix
Deploy by digest or by an immutable per-commit tag. Keep Kyverno's verifyDigest and mutateDigest on (both default to true).
-
The registry allows tags to be overwritten.
Check
aws ecr describe-repositories --repository-names <repo> --query 'repositories[].imageTagMutability' # MUTABLEFix
Turn on tag immutability where the registry supports it (ECR IMMUTABLE).
Prevent it
Immutable tags where the registry supports them (ECR IMMUTABLE).
Now learn it: break and fix it yourself in Mutable tags and digest pinning, then take Level 10 · Signing and provenance. Open in the site
Git history
git filter-repo: Refusing to destructively overwrite repo history
git filter-repo
The error
Aborting: Refusing to destructively overwrite repo history since
this does not look like a fresh clone.
What it means
git filter-repo rewrites every commit and cannot be undone in place, so by default it only runs in a repository that looks freshly cloned. This one has local work or history of its own, so it stopped before changing anything.
Causes, most likely first
-
You are in your working clone, with local branches, changes or reflog history.
Check
git status # changes or untracked files git reflog | head # more than the single clone entryFix
Work in a fresh mirror clone: git clone --mirror <url>, run filter-repo there, and keep the old clone as a backup.
-
It was a fresh clone, but something touched it afterwards (a fetch, a checkout of another branch).
Check
git reflog | head # entries after the cloneFix
Clone again, or add --force once you have a backup and are sure.
Prevent it
Keep a backup clone before rewriting.
Now learn it: break and fix it yourself in AWS key committed, .env committed, Kubernetes Secret committed to git, then take Level 02 · Secrets. Open in the site
'origin' does not appear to be a git repository (after filter-repo)
git push
The error
fatal: 'origin' does not appear to be a git repository
What it means
git push was told to push to origin, and there is no remote with that name. git filter-repo removes origin on purpose after a rewrite, so you do not push rewritten history by accident.
Causes, most likely first
-
filter-repo ran in this clone and removed origin.
Check
git remote -v # no origin listedFix
Check the rewrite, then git remote add origin <url> and git push --force --all (and --tags). Tell collaborators to re-clone. If you removed a secret, rotate it first: forks, pull request refs and old clones still hold the commits.
-
The remote has another name, or this repository never had one.
Check
git remote -v # a remote under another name, or none, without a filter-repo runFix
Push to that name, or add the remote.
Prevent it
Coordinate rewrites; protect branches against force-push except for this.
Now learn it: break and fix it yourself in AWS key committed, .env committed, Kubernetes Secret committed to git, then take Level 02 · Secrets. Open in the site
Runtime and response
ZAP: CSP / HSTS / X-Content-Type-Options header not set
ZAP baseline
The error
FAIL-NEW: Content Security Policy (CSP) Header Not Set [10038] x 4
What it means
ZAP's baseline scan saw responses without a security header. FAIL-NEW means a rule set to FAIL in the rules file found new instances; [10038] is ZAP's rule ID (CSP header not set) and x 4 is the number of URLs.
Causes, most likely first
-
The app or proxy does not send the header at all.
Check
curl -sI https://<staging>/ # no Content-Security-Policy lineFix
Set the headers in the app (after_request) or at the ingress, once, for every response.
-
The headers are set, but only on some responses: not on redirects, error pages or static files served by another layer.
Check
curl -sI https://<staging>/<a URL from the ZAP report> # the header is missing on that URL onlyFix
Set them at the layer every response passes through, such as the ingress or proxy.
-
HSTS is only checked on HTTPS, and ZAP scanned plain HTTP or another host than you tested.
Check
Read the -t target in the ZAP command # http://, or another hostFix
Scan the HTTPS URL users reach.
Prevent it
Keep the ZAP rules file at FAIL for these IDs.
Now learn it: break and fix it yourself in DAST: missing security headers, then take Level 12 · DAST and runtime. Open in the site
New critical advisory for something already in production
grype, osv-scanner
The error
quickcache 3.4.0 3.4.1 python PRACTICE-2026-0058 Critical
What it means
Grype scanned the SBOM of something already in production and found an advisory published after the release. Columns: package, installed version, fixed-in version, type, advisory ID, severity.
Causes, most likely first
-
Advisories are published after release; a build-time scan could not have known.
Check
grype sbom:./sbom.cdx.json # the package and its fixed-in version cosign verify-attestation --type cyclonedx ... <running image> # the SBOM belongs to the image that is actually runningFix
Upgrade, release through the pipeline, and confirm the running image's digest changed.
-
The SBOM is from an older image than the one running, so the finding may already be fixed.
Check
kubectl get pods -o jsonpath='{.items[*].status.containerStatuses[*].imageID}' # compare with the SBOM's image digestFix
Scan the SBOM attached to the running digest.
Prevent it
Rescan SBOMs of running images daily.
Now learn it: break and fix it yourself in New critical CVE in production, then take Level 12 · DAST and runtime. Open in the site
Leaked key already used by an attacker
aws, CloudTrail
The error
"EventName": "CreateUser", ... "SourceIPAddress": "203.0.113.77"
What it means
CloudTrail shows the leaked access key making calls you did not make. CreateUser from an address you do not recognise is persistence: the attacker makes another way in before you rotate.
Causes, most likely first
-
The key was found and used, and the attacker created persistence: users, keys, roles, policies, instances, often in regions you do not watch.
Check
aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=<key> # what it did: management events, last 90 days, this region aws cloudtrail lookup-events --region <other-region> --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=<key> # repeat for every regionFix
Deactivate the key. Remove what it created, in every region. Delete the backdoor user: delete its keys, detach its policies, then delete-user.
-
The key was used to get temporary credentials (GetSessionToken, AssumeRole). Those stay valid until they expire, even after the key is deactivated.
Check
aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=<key> | grep -E 'GetSessionToken|AssumeRole|GetFederationToken' # sessions issued with the keyFix
On that user, deny actions to sessions issued before now (an aws:TokenIssueTime condition), and revoke active sessions on any role it assumed.
Prevent it
No long-lived keys; GuardDuty; multi-region CloudTrail.
Now learn it: break and fix it yourself in Incident: leaked key used by an attacker, then take Level 13 · Cloud posture and incident response. Open in the site
DeleteConflict: Cannot delete entity, must detach all policies first
aws iam
The error
An error occurred (DeleteConflict) when calling the DeleteUser operation: Cannot delete entity, must detach all policies first.
What it means
The DeleteUser API refuses to delete an IAM user that still has anything attached, and names the first thing it found (here, policies). The console removes these for you; the API does not.
Causes, most likely first
-
Managed or inline policies are still on the user.
Check
aws iam list-attached-user-policies --user-name <user> # managed policies aws iam list-user-policies --user-name <user> # inline policiesFix
detach-user-policy for each managed policy, delete-user-policy for each inline one.
-
Access keys or group memberships remain (the message then says must delete access keys first, or must remove users from group first).
Check
aws iam list-access-keys --user-name <user> aws iam list-groups-for-user --user-name <user>Fix
delete-access-key, remove-user-from-group.
-
Other credentials remain: a console password, MFA devices, signing certificates, SSH keys, service-specific credentials.
Check
aws iam get-login-profile --user-name <user> # NoSuchEntity: no console password aws iam list-mfa-devices --user-name <user>Fix
delete-login-profile, deactivate-mfa-device (and delete a virtual one), and so on, then delete-user.
Prevent it
Prefer roles to users.
Now learn it: break and fix it yourself in Incident: leaked key used by an attacker, then take Level 13 · Cloud posture and incident response. Open in the site