Skip to content

Commit 0b66f47

Browse files
mtsfoniconstruct agent
andauthored
Security/rootless container (#1035)
* security: document and partially implement rootless container execution - Dockerfile: add CycloneDX.E2ETests.csproj to build stage (pre-existing bug fix) - Dockerfile: create /tmp/dotnet-home and /tmp/nuget-packages with chmod 1777 so any runtime UID (e.g. via --user) can write .NET CLI state there - release.yml: add --user $(id -u):$(id -g) to docker run smoke-test - README: document --user recommendation for rootless Docker usage - docs/adr-001-rootless-container.md: record the decision, options considered, and rationale for deferring default non-root USER to next major version * docs: trim ADR to only options actually considered * docs: make --user the default in Docker usage example --------- Co-authored-by: construct agent <agent@construct.local>
1 parent 79132fb commit 0b66f47

4 files changed

Lines changed: 79 additions & 3 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373

7474
# Generate the JSON with the docker container as additional smoke test
7575
- name: Generate JSON SBOM
76-
run: docker run --rm -v ${GITHUB_WORKSPACE}:/usr/src/project cyclonedx/cyclonedx-dotnet:${{ steps.package_release.outputs.version }} /usr/src/project/CycloneDX.sln --output-format json -o /usr/src/project
76+
run: docker run --rm --user $(id -u):$(id -g) -v ${GITHUB_WORKSPACE}:/usr/src/project cyclonedx/cyclonedx-dotnet:${{ steps.package_release.outputs.version }} /usr/src/project/CycloneDX.sln --output-format json -o /usr/src/project
7777

7878
- name: Publish package to NuGet
7979
env:

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ WORKDIR /src
66
COPY ["CycloneDX.sln", "nuget.config", "Directory.Build.props", "Directory.Build.targets", "Directory.Packages.props", "./"]
77
COPY ["CycloneDX/CycloneDX.csproj", "CycloneDX/"]
88
COPY ["CycloneDX.Tests/CycloneDX.Tests.csproj", "CycloneDX.Tests/"]
9+
COPY ["CycloneDX.E2ETests/CycloneDX.E2ETests.csproj", "CycloneDX.E2ETests/"]
910

1011
RUN dotnet restore
1112

@@ -26,7 +27,7 @@ ENV DOTNET_CLI_HOME=/tmp/dotnet-home \
2627
DOTNET_CLI_TELEMETRY_OPTOUT=1
2728

2829
RUN mkdir -p /tmp/dotnet-home /tmp/nuget-packages \
29-
&& chmod -R 0755 /tmp/dotnet-home /tmp/nuget-packages
30+
&& chmod -R 1777 /tmp/dotnet-home /tmp/nuget-packages
3031

3132
COPY --from=build /app/publish /app
3233

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,16 @@ dotnet-CycloneDX <path> -o <OUTPUT_DIRECTORY>
6464
#### Execution via Docker
6565

6666
```bash
67-
docker run --rm cyclonedx/cyclonedx-dotnet [OPTIONS] <path>
67+
docker run --rm --user $(id -u):$(id -g) \
68+
-v $(pwd):/work \
69+
cyclonedx/cyclonedx-dotnet [OPTIONS] /work/<path>
6870
```
6971

72+
> **Note:** The `--user $(id -u):$(id -g)` flag runs the container as your host user,
73+
> ensuring `dotnet restore` can write to the mounted volume and output files are owned by
74+
> you rather than root. A future major release will make non-root the default. See
75+
> [docs/adr-001-rootless-container.md](docs/adr-001-rootless-container.md) for background.
76+
7077
#### Options
7178

7279
```text

docs/adr-001-rootless-container.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ADR-001: Rootless Container Execution
2+
3+
**Date:** 2026-03-01
4+
**Status:** Partially implemented — deferred to next major version
5+
6+
## Context
7+
8+
Running Docker containers as root is a security weakness. If the container process is
9+
compromised, the attacker has root inside the container, which increases the blast radius
10+
of an escape or a volume-mounted file system attack.
11+
12+
The goal was to make the `cyclonedx/cyclonedx-dotnet` Docker image run as a non-root user
13+
by default.
14+
15+
## Problem
16+
17+
The tool calls `dotnet restore` against the project files passed in via a volume mount. The
18+
`dotnet` CLI writes intermediate build artefacts (the `obj/` directory) directly into the
19+
project tree. This means the container user must have write access to the mounted volume.
20+
21+
When a caller runs:
22+
23+
```bash
24+
docker run --rm -v $(pwd):/work cyclonedx/cyclonedx-dotnet /work/MyProject.sln -o /work
25+
```
26+
27+
the volume is owned by the host UID. If the container runs as a different UID, `dotnet restore`
28+
fails with `Permission denied` on the `obj/` directory.
29+
30+
## Options considered
31+
32+
### Option 1: Dedicated non-root user baked into the image (rejected — breaking)
33+
34+
Create a non-root user in the image and set `USER` to it. Callers who do not pass `--user`
35+
would run as that UID, which does not match the host volume owner, causing `dotnet restore`
36+
to fail with `Permission denied` on the `obj/` directory. This is a silent breaking change
37+
for all existing pipelines regardless of which non-root UID is chosen.
38+
39+
### Option 2: Require `--user $(id -u):$(id -g)` (chosen)
40+
41+
Leave the image running as root by default to preserve backward compatibility. Document that
42+
callers should pass `--user $(id -u):$(id -g)` to run as their own UID. This means the
43+
container process matches the volume owner, so `dotnet restore` can write `obj/` without
44+
permission errors, and output files are owned by the calling user.
45+
46+
This is not enforced by the image, but it is safe for all callers who adopt it, and it is
47+
the standard pattern for Docker tools that write back to mounted volumes.
48+
49+
## Decision
50+
51+
**Defer the default non-root user to the next major version**, where the breaking change
52+
can be communicated via release notes and a migration guide.
53+
54+
In the interim:
55+
56+
- The image continues to run as root by default.
57+
- The `DOTNET_CLI_HOME` and `NUGET_PACKAGES` directories (`/tmp/dotnet-home` and
58+
`/tmp/nuget-packages`) are created with `chmod 1777` (world-writable, sticky bit) so
59+
they are accessible to any UID — including one injected at runtime via `--user`.
60+
- Callers are recommended to pass `--user $(id -u):$(id -g)` (documented in the README).
61+
- The release workflow smoke-test already uses `--user $(id -u):$(id -g)`.
62+
63+
## Consequences
64+
65+
- No breaking change for existing pipelines.
66+
- Callers who adopt `--user $(id -u):$(id -g)` get rootless execution today.
67+
- The next major version should set a non-root `USER` in the Dockerfile and update
68+
documentation accordingly, accepting the breaking change with a migration note.

0 commit comments

Comments
 (0)