Embedding Security Scanners in CI/CD: Safety and Bandit in GitLab Pipelines
The How To Embed Safety Into GitLab and How To Embed Bandit Into GitLab exercises teach how to integrate Software Component Analysis (SCA) and Static Application Security Testing (SAST) into GitLab CI/CD pipelines. These exercises appear across 3-5 courses. This article covers the most common failure modes and how to fix them.
Table of Contents
- Safety in CI/CD: Exit Code 64
- Safety Volume Mount Path
- Safety Policy File Required
- Bandit in CI/CD: User Permissions
- Bandit Output Format and Artifacts
allow_failurefor Scanner Jobs- Docker-in-Docker Requirement
- Common Questions
Safety in CI/CD: Exit Code 64
Safety Finds Vulnerabilities by Design
When you embed Safety (SCA/OAST scanner) into the pipeline, it will typically find vulnerable dependencies and exit with code 64:
Found 26 known vulnerability alerts affecting 15 packages
Exit code 64 means vulnerabilities were found. This causes the CI/CD job to fail (red status).
Fix: Add allow_failure: true to the job so the pipeline continues despite findings:
oast:
stage: test
script:
- docker run --rm -v $(pwd):/src hysnsec/safety check -r requirements.txt --json > oast-results.json
allow_failure: true
This is expected behavior at DevSecOps Maturity Levels 1-2, where security scanning is being introduced but shouldn't block deployments yet.
Safety Volume Mount Path
-v $(pwd):/src Must Match
The Safety Docker image expects the project files at /src inside the container. The volume mount must map the runner's current directory to /src:
script:
- docker run --rm -v $(pwd):/src hysnsec/safety check -r requirements.txt --json > oast-results.json
Common mistake: Using a different path like /app or /code:
# WRONG - Safety won't find requirements.txt:
docker run --rm -v $(pwd):/app hysnsec/safety check -r requirements.txt
# CORRECT:
docker run --rm -v $(pwd):/src hysnsec/safety check -r requirements.txt
The task verification checks for the exact pattern: docker run.*\$(pwd):/src.*hysnsec/safety check -r requirements.txt
Safety Policy File Required
.safety-policy.yml Must Exist
Without a Safety policy file, the scan may fail or produce unexpected results:
No such file or directory: .safety-policy.yml
Fix: Create the policy file before running the scan:
script:
- |
cat > .safety-policy.yml <<EOF
security:
ignore-vulnerabilities: {}
EOF
- docker run --rm -v $(pwd):/src hysnsec/safety check -r requirements.txt --json > oast-results.json
An empty ignore-vulnerabilities: {} means no vulnerabilities are suppressed — all findings are reported.
Artifacts Must Use when: always
Since the Safety job may fail (exit code 64), artifacts need when: always to be saved:
artifacts:
paths: [oast-results.json]
when: always
Without when: always, the JSON output is lost when the job fails.
Bandit in CI/CD: User Permissions
--user $(id -u):$(id -g) Is Required
The Bandit Docker image must run as the same user as the CI runner to have file permissions on the mounted volume:
sast:
stage: build
script:
- docker run --user $(id -u):$(id -g) -v $(pwd):/src --rm hysnsec/bandit -r /src -f json -o /src/bandit-output.json
Common mistake: Forgetting --user $(id -u):$(id -g):
# WRONG - permission denied on mounted volume:
docker run -v $(pwd):/src --rm hysnsec/bandit -r /src
# CORRECT:
docker run --user $(id -u):$(id -g) -v $(pwd):/src --rm hysnsec/bandit -r /src -f json -o /src/bandit-output.json
Without --user, the container runs as root, which may not have write access to the runner's mounted volume.
Bandit Output Format and Artifacts
JSON Output Must Be Written to the Mounted Volume
Bandit's output file (-o /src/bandit-output.json) must be written to the mounted volume path so the CI runner can save it as an artifact:
script:
- docker run --user $(id -u):$(id -g) -v $(pwd):/src --rm hysnsec/bandit -r /src -f json -o /src/bandit-output.json
artifacts:
paths: [bandit-output.json]
when: always
Key flags:
| Flag | Purpose |
|---|---|
-r /src |
Recursive scan of /src directory |
-f json |
JSON output format |
-o /src/bandit-output.json |
Write output to mounted volume |
allow_failure for Scanner Jobs
Both Safety and Bandit Need allow_failure
Security scanners in early DevSecOps maturity levels should not block the pipeline:
sast:
stage: build
script:
- docker run --user $(id -u):$(id -g) -v $(pwd):/src --rm hysnsec/bandit -r /src -f json -o /src/bandit-output.json
artifacts:
paths: [bandit-output.json]
when: always
allow_failure: true
With allow_failure: true, the job shows an exclamation mark (!) instead of a red X, and downstream jobs continue.
Docker-in-Docker Requirement
Scanner Docker Images Require docker:dind
Both Safety and Bandit exercises run Docker images inside the CI pipeline. Without the Docker-in-Docker service, the docker run command fails:
services:
- docker:dind
Fix: Add services: - docker:dind to the job or at the top level of .gitlab-ci.yml.
Common Questions
| Question | Answer |
|---|---|
| My Safety job fails with exit code 64. | This means vulnerabilities were found — expected behavior. Add allow_failure: true to keep the pipeline green. |
Safety can't find requirements.txt. |
Check that -v $(pwd):/src is used. Safety looks for files inside /src in the container. |
| Bandit output file is empty or missing. | Ensure -o /src/bandit-output.json writes to the mounted volume, and --user $(id -u):$(id -g) is set. |
docker run fails with "Cannot connect to Docker daemon." |
Add services: - docker:dind to your .gitlab-ci.yml. |
What does allow_failure: true do? |
The job can fail without blocking the pipeline. Shows ! instead of red X. |
| Where do I find the scan results? | Check the job's artifacts section in GitLab, or download the JSON file listed under artifacts: paths:. |
Wrap-Up
Embedding security scanners into CI/CD pipelines is straightforward but has a few gotchas:
- Safety and Bandit both use Docker images with
-v $(pwd):/srcvolume mounts. - Bandit needs
--user $(id -u):$(id -g)for file permissions. - Safety needs
.safety-policy.ymlbefore running the scan. - Both scanners will find issues and return non-zero exit codes — use
allow_failure: true. artifacts: when: alwayspreserves output from failing scan jobs.services: - docker:dindis required for Docker commands in CI.
Every machine resets after 2 hours. The DevSecOps Box is stateless and resets on any page refresh or tab close.
Reach out to support with your lab ID if you encounter errors not covered in this guide.