Docker Image Declaration, Seccomp Profiles, and Handling Failures in GitHub Actions
Learn why certain Docker image declarations don’t work in CI/CD pipelines, how seccomp filters affect chown/chmod, and the best ways to allow failures in GitHub Actions.
Introduction
When you start building DevSecOps pipelines, you quickly discover that small configuration details can have a big impact on security and reliability. This article explains three common stumbling blocks:
- Why you can’t declare a Docker image up‑front for certain
hysnsecimages – the difference between static image pulls and dynamic builds in GitLab CI. - How Seccomp profiles influence system calls such as
chownandchmod, and why those calls sometimes succeed and sometimes fail. - How to let a step or job fail gracefully in GitHub Actions using
continue-on-errorand conditional expressions.
By the end of the guide you’ll be able to write more robust CI/CD definitions, troubleshoot Seccomp‑related errors, and keep your pipelines running even when non‑critical steps break.
1. Declaring Docker Images in GitLab CI – Why “Up‑Front” Doesn’t Always Work
1.1 What “declare the image up‑front” means
In a .gitlab-ci.yml file you can specify an image in two ways:
# 1️⃣ Static declaration (up‑front)
image: hysnsec/bandit:latest
# 2️⃣ Dynamic declaration (inside a job)
job_name:
script:
- docker run --rm -v $(pwd):/src hysnsec/bandit -r /src -f json -o /src/bandit-output.json
The first approach tells GitLab Runner to pull the image once before any job starts. The second runs the image inside the job’s script.
1.2 Why the static approach fails for some hysnsec images
| Reason | Explanation |
|---|---|
| Image requires runtime arguments | Many hysnsec images expect volume mounts (-v $(pwd):/src) or environment variables that are only known at job execution time. Declaring the image alone provides none of these, causing the container to exit immediately. |
| Custom entrypoint logic | Some images replace the default entrypoint with a wrapper script that expects parameters (e.g., a path to scan). Without those parameters the container cannot start correctly. |
| Security restrictions | GitLab’s shared runners may block privileged operations required by the image when it is pulled as the default environment. Running the image explicitly inside script lets you add --privileged or other flags if needed. |
Bottom line: Use a static image: declaration only when the container can run without additional runtime configuration. For security‑focused images like those from hysnsec, it’s safer to invoke them inside the job’s script.
1.3 Quick reference – When to use each style
- Static
image:– Simple unit tests, language runtimes, linting tools that need no extra mounts. - Dynamic
docker run– Scanners, build tools, or any image that requires volumes, environment variables, or special flags.
2. Seccomp Profiles – Why chown and chmod May Appear to Work or Fail
Seccomp (Secure Computing Mode) filters the system calls a container can execute. A typical DevSecOps lab will provide a custom Seccomp JSON that blocks risky calls while allowing the rest.
2.1 Understanding the behavior
| Scenario | Seccomp rule | Observed result | Why it happens |
|---|---|---|---|
mkdir blocked |
mkdir syscall denied |
adduser fails at directory creation |
The user‑creation flow tries to create /home/abc. The blocked mkdir aborts the process, producing “Operation not permitted”. |
chown blocked |
chown syscall denied |
adduser fails after directory is created |
The home directory is created, but the subsequent chown 1000:1000 /home/abc is blocked, causing the same error message. |
chmod blocked |
chmod syscall denied |
adduser fails during permission change |
After mkdir, the tool attempts chmod 488 /home/abc. The denied syscall stops the flow. |
When the Seccomp profile does not block chown or chmod, those calls succeed because the container’s process has the required capabilities (usually root inside the container).
2.2 Practical example
# Inside a container with a restrictive seccomp profile
root@container:/# adduser abc
Adding user `abc' ...
Creating home directory `/home/abc' ...
Stopped: chown 1000:1000 /home/abc: Operation not permitted
If you remove the chown rule from the profile:
{
"syscalls": [
{ "name": "chmod", "action": "SCMP_ACT_ALLOW" },
{ "name": "mkdir", "action": "SCMP_ACT_ALLOW" }
// Note: chown is omitted → default allow
]
}
the same command succeeds because the kernel no longer blocks the chown syscall.
2.3 Tips for working with Seccomp
- Start with a permissive profile (
SCMP_ACT_ALLOWfor everything) and iteratively add blocks. - Log denied syscalls – run the container with
--security-opt seccomp=unconfinedand inspectdmesgto see which calls are being filtered. - Remember ownership matters –
chmodworks for the file owner. If you run as root, you can always change permissions; non‑root users will be blocked by the profile. - Test with real user‑creation commands (
useradd,adduser) to verify that the profile doesn’t unintentionally break provisioning steps.
3. Allowing Failures in GitHub Actions – continue-on-error and Conditional Execution
In CI pipelines, some steps are “nice‑to‑have” (e.g., security scans) and should not break the whole workflow. GitHub Actions provides two mechanisms:
3.1 continue-on-error – Mark a step or job as successful even if it fails
sast:
runs-on: ubuntu-20.04
needs: test
steps:
- uses: actions/checkout@v2
- name: Run Bandit scan
run: |
docker run --rm -v $(pwd):/src hysnsec/bandit -r /src -f json -o /src/bandit-output.json
continue-on-error: true # <-- step never fails the job
- name: Upload results
uses: actions/upload-artifact@v2
with:
name: Bandit
path: bandit-output.json
if: always() # ensures artifact upload runs even if previous step failed
Result: The job finishes with a green checkmark, but the scan’s exit code is still recorded in the logs.
3.2 if: always() – Run a step regardless of previous outcome
Useful for cleanup or artifact collection:
- name: Cleanup temporary files
run: rm -rf /tmp/tmpfile
if: always()
3.3 Combining both for a “soft‑fail” job
sast:
runs-on: ubuntu-20.04
needs: test
continue-on-error: true # entire job never fails the workflow
steps:
- uses: actions/checkout@v2
- run: docker run --rm -v $(pwd):/src hysnsec/bandit -r /src -f json -o /src/bandit-output.json
- uses: actions/upload-artifact@v2
with:
name: Bandit
path: bandit-output.json
if: always()
3.4 When to use each approach
| Situation | Recommended setting |
|---|---|
| Optional security scan – you want a report but don’t want to block the pipeline | continue-on-error: true on the scan step |
| Mandatory build step – failure must stop the pipeline | Omit continue-on-error; use default behavior |
| Always‑run cleanup – regardless of success/failure | if: always() on the cleanup step |
Common Questions & Quick Tips
-
Q: Can I override a Seccomp profile at runtime?
A: Yes. Use--security-opt seccomp=/path/to/profile.jsonwithdocker run. For GitHub Actions, add it to theruncommand. -
Q: Why does my GitLab job still fail even with
continue-on-error?
A:continue-on-erroris a GitHub Actions feature. In GitLab you needallow_failure: trueon the job definition. -
Tip: When testing Seccomp changes, run the container with
--security-opt seccomp=unconfinedfirst to confirm that the issue is truly a blocked syscall. -
Tip: Keep your GitHub Actions YAML tidy by extracting reusable steps into reusable workflows or composite actions – especially when you repeatedly apply
continue-on-error.
Conclusion
Understanding the nuances of Docker image declaration, Seccomp filtering, and failure handling makes your DevSecOps pipelines both secure and resilient. Declare images dynamically when they need runtime parameters, fine‑tune Seccomp profiles to block only the truly dangerous syscalls, and use continue-on-error together with conditional if: always() to keep non‑critical steps from derailing the entire workflow. Apply these best practices, and you’ll spend less time debugging and more time delivering secure software.