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

  1. 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 region

    Fix

    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.

  2. 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 yours

    Fix

    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.

  3. 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 fixture

    Fix

    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

  1. 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 committed

    Fix

    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.

  2. 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 -f

    Fix

    git rm --cached .env. The existing rule then keeps it out.

  3. 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 pushed

    Fix

    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

  1. 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 it

    Fix

    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.

  2. 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 required

    Fix

    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

  1. 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 it

    Fix

    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.

  2. 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 output

    Fix

    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

  1. 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 values

    Fix

    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.

  2. 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 file

    Fix

    Rotate, remove and purge as above; never write state into the working tree.

  3. 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 variable

    Fix

    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

  1. 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 index

    Fix

    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.

  2. 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 ship

    Fix

    Rebuild and redeploy through the pipeline; scan the image you ship, not only the source.

  3. 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 parent

    Fix

    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

  1. 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 releases

    Fix

    Upgrade the parent to a version that allows the fixed child: pip-compile --upgrade-package <parent> --upgrade-package <package>. Run the tests.

  2. 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 fix

    Fix

    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

  1. 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 reachable

    Fix

    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.

  2. 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 services

    Fix

    Mitigate now: validate or block that input, or move to a maintained alternative. Keep the finding open until a fix ships.

  3. 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 range

    Fix

    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

  1. 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 too

    Fix

    Replace the package, or get a written legal decision before changing the policy.

  2. 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 reads

    Fix

    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

  1. The dependency graph is off for the repository, so there is nothing to compare.

    Check

    Settings > Advanced Security: Dependency graph   # disabled

    Fix

    Enable the dependency graph, and run the action on pull_request events: it compares a pull request's base and head.

  2. 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 disabled

    Fix

    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

  1. 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 name

    Fix

    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.

  2. 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: confusion

    Fix

    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

  1. 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 pin

    Fix

    Generate a lock: pip-compile --generate-hashes requirements.in (npm ci with package-lock.json, and so on), and install only from it.

  2. 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 pin

    Fix

    Regenerate the whole lock with pip-compile --generate-hashes on the Python version CI uses, instead of editing it.

  3. 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.txt

    Fix

    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

  1. Nobody owns triage, so security fixes wait behind routine bumps.

    Check

    gh pr list --author app/dependabot   # many open, some weeks old

    Fix

    Merge security fixes first; group routine updates and schedule them.

  2. 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 --auto

    Fix

    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

  1. 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: injectable

    Fix

    Pass values as parameters: cur.execute("... WHERE id = %s", (value,)). The placeholder depends on the driver: %s for psycopg, ? for sqlite3.

  2. 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 value

    Fix

    Choose it from an allowlist and quote it with psycopg.sql.Identifier.

  3. Only constants are formatted in, so it is a false positive.

    Check

    Trace each formatted value   # all are literals in the code

    Fix

    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

  1. 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: exploitable

    Fix

    Render with a template (Jinja escapes by default), or escape each value with markupsafe.escape.

  2. 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 string

    Fix

    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

  1. 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 disabled

    Fix

    Have an organisation owner or security manager turn on Code Security for the repository, then re-run the workflow.

  2. 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 up

    Fix

    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

  1. 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 why

    Fix

    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.

  2. 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.

  3. 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 it

    Fix

    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

  1. 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 user

    Fix

    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.

  2. 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 root

    Fix

    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

  1. The Dockerfile names the user instead of giving its number.

    Check

    docker inspect --format '{{.Config.User}}' <image>   # a name, such as app

    Fix

    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

  1. 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 base

    Fix

    Multi-stage build: build in a full image, ship from slim, alpine or distroless.

  2. 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 ago

    Fix

    Rebuild regularly on a current base, pinned by digest and updated by a bot.

  3. The findings are in your app's packages, not the OS.

    Check

    trivy image --pkg-types library <image>   # Python, npm or Java packages

    Fix

    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

  1. 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 supports

    Fix

    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.

  2. 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 directory

    Fix

    Install the build tools in a build stage only, build the wheels there, and copy them into the final image.

  3. 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 wheel

    Fix

    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

  1. 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 reached

    Fix

    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>.

  2. The product does reach it, so it is affected.

    Check

    Trace calls into the package   # the vulnerable function is used

    Fix

    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

  1. 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 setting

    Fix

    Remove the ACL and set object ownership to BucketOwnerEnforced; set all four public access block settings to true. Share single objects with presigned URLs.

  2. 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 evaluates

    Fix

    Narrow the policy to named principals or a CloudFront origin access control; BlockPublicPolicy then stops it coming back.

  3. 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 assets

    Fix

    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

  1. 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 22

    Fix

    Remove the rule and use SSM Session Manager, or allow only the VPN range. Moving SSH to another port is not a fix.

  2. 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

  1. 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 asked

    Fix

    Set encrypted = true / storage_encrypted = true, with a KMS key if you need your own.

  2. 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 replaced

    Fix

    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

  1. 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 fixing

    Fix

    Name the actions and resources the code needs. IAM Access Analyzer can generate a policy from the role's CloudTrail activity.

  2. 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 it

    Fix

    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

  1. 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 allowed

    Fix

    metadata_options { http_tokens = "required" } on instances and launch templates. Fix the SSRF itself too: allowlist the hosts the app may fetch.

  2. 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 v1

    Fix

    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

  1. 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.

  2. 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.sock

    Fix

    Build images in CI, or with a rootless builder (BuildKit rootless) that needs no socket.

  3. A node agent (network plugin, storage driver, monitoring) really needs it.

    Check

    kubectl get ds -A   # a vendor DaemonSet

    Fix

    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

  1. 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 fail

    Fix

    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.

  2. 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 message

    Fix

    The same fix in the Deployment's Pod template; the ReplicaSet then creates the Pods.

  3. 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 root

    Fix

    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

  1. 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 namespace

    Fix

    A default-deny-ingress policy (podSelector: {}, policyTypes: [Ingress]), then explicit allows for each flow: ingress controller, monitoring, app to database.

  2. 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 it

    Fix

    Restore it with git revert of that commit, or move it back to the folder conftest tests.

  3. 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 enforced

    Fix

    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

  1. 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 needed

    Fix

    A Role with the exact verbs and resources (and resourceNames), and a RoleBinding in that namespace; delete the cluster-admin binding.

  2. A Helm chart or operator asked for it at install.

    Check

    kubectl get clusterrolebinding -o wide | grep cluster-admin   # the binding and its subjects

    Fix

    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

  1. 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).

  2. 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: encrypted

    Fix

    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

  1. 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 to

    Fix

    uses: owner/action@<40-char sha> # vX.Y.Z. For an annotated tag, take the SHA on the ^{} line: that is the commit.

  2. 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

  1. 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 object

    Fix

    Use the SHA on the ^{} line, which is the commit.

  2. 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 repository

    Fix

    Copy the SHA again from git ls-remote of the right repository, or let Dependabot or Renovate write the pins.

  3. The repository was renamed, deleted or made private.

    Check

    gh api repos/<owner>/<action> --jq .full_name   # another name, or Not Found

    Fix

    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

  1. 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 expression

    Fix

    Pass it through the environment: env: TITLE: ${{ github.event.issue.title }}, then "$TITLE" in the script, quoted.

  2. 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

  1. 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 permissions

    Fix

    permissions: contents: read at the top; add packages: write or id-token: write only on the job that needs it.

  2. 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: write

    Fix

    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

  1. 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: read

    Fix

    Add permissions: packages: write to that job only.

  2. 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 role

    Fix

    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

  1. 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 had

    Fix

    Add the one permission the step needs to that job, for example permissions: pull-requests: write.

  2. 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 write

    Fix

    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.

  3. 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 job

    Fix

    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

  1. 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: write

    Fix

    Add id-token: write (with contents: read) to that job. Check the role's trust policy names this repository and branch.

  2. 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-token

    Fix

    Add id-token: write to the job's block.

  3. 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 permissions

    Fix

    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

  1. 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 one

    Fix

    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.

  2. 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 else

    Fix

    Set aud to sts.amazonaws.com and leave the action's audience at its default.

  3. 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.com

    Fix

    Create the IAM OIDC provider for https://token.actions.githubusercontent.com with audience sts.amazonaws.com.

  4. 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-role

    Fix

    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

  1. 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 it

    Fix

    Rotate the secret first and update it where CI reads it. Then remove the step and delete the affected runs: gh run delete <id>.

  2. 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 used

    Fix

    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

  1. 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.ref

    Fix

    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.

  2. Script injection: an untrusted ${{ github.event... }} is expanded inside run:.

    Check

    zizmor .github/workflows/   # template-injection

    Fix

    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

  1. 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 directory

    Fix

    Cache the database between runs.

  2. 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.io

    Fix

    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

  1. A fresh runner has an empty cache, and the job skips the download.

    Check

    ls ~/.cache/trivy/db   # No such file or directory: no database

    Fix

    Restore the cache before the scan, or download from a mirror on the first run.

  2. 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 differ

    Fix

    Restore the cache to the path Trivy reads, or pass that path with --cache-dir.

  3. The cache key missed, so nothing was restored.

    Check

    In the run log, the cache step   # Cache not found for input keys

    Fix

    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

  1. 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 pulls

    Fix

    Log in before pulling: docker/login-action with a Docker Hub access token kept as a secret (not the account password).

  2. 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 job

    Fix

    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

  1. 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 set

    Fix

    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.

  2. 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 job

    Fix

    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.

  3. 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 it

    Fix

    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

  1. The image was never signed: built on a laptop, or in a job without a signing step.

    Check

    cosign tree <image>   # no signatures listed

    Fix

    Sign in CI (keyless) and deploy what CI built.

  2. 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.

  3. 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 signing

    Fix

    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

  1. 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 ref

    Fix

    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.

  2. Another workflow or repository signed it, for example a test workflow or a fork.

    Check

    Read got subjects   # another workflow file or repository

    Fix

    Do not deploy it: rebuild and sign it in the release workflow. Never widen the identity to .* to make verification pass.

  3. 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 workflow

    Fix

    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

  1. 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 it

    Fix

    Deploy an image built and signed by CI; do not switch the policy to Audit.

  2. 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 access

    Fix

    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

  1. No SBOM is generated in the pipeline.

    Check

    grep -rn 'syft\|sbom' .github/workflows   # no SBOM step

    Fix

    syft <image> -o cyclonedx-json=sbom.cdx.json in the release job, then attest it (below).

  2. 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 type

    Fix

    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

  1. 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 request

    Fix

    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.

  2. 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 main

    Fix

    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

  1. 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 signed

    Fix

    Deploy by digest or by an immutable per-commit tag. Keep Kyverno's verifyDigest and mutateDigest on (both default to true).

  2. The registry allows tags to be overwritten.

    Check

    aws ecr describe-repositories --repository-names <repo> --query 'repositories[].imageTagMutability'   # MUTABLE

    Fix

    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

  1. 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 entry

    Fix

    Work in a fresh mirror clone: git clone --mirror <url>, run filter-repo there, and keep the old clone as a backup.

  2. It was a fresh clone, but something touched it afterwards (a fetch, a checkout of another branch).

    Check

    git reflog | head   # entries after the clone

    Fix

    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

  1. filter-repo ran in this clone and removed origin.

    Check

    git remote -v   # no origin listed

    Fix

    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.

  2. 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 run

    Fix

    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

  1. The app or proxy does not send the header at all.

    Check

    curl -sI https://<staging>/   # no Content-Security-Policy line

    Fix

    Set the headers in the app (after_request) or at the ingress, once, for every response.

  2. 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 only

    Fix

    Set them at the layer every response passes through, such as the ingress or proxy.

  3. 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 host

    Fix

    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

  1. 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 running

    Fix

    Upgrade, release through the pipeline, and confirm the running image's digest changed.

  2. 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 digest

    Fix

    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

  1. 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 region

    Fix

    Deactivate the key. Remove what it created, in every region. Delete the backdoor user: delete its keys, detach its policies, then delete-user.

  2. 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 key

    Fix

    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

  1. 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 policies

    Fix

    detach-user-policy for each managed policy, delete-user-policy for each inline one.

  2. 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.

  3. 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