Skip to content

Commit 2210289

Browse files
Add non-root variant to Dockerfile for dual image publishing (#3520)
## Why make this change? Closes #3514 The published runtime image (`mcr.microsoft.com/azure-databases/data-api-builder`) runs as root because the Dockerfile never issues a `USER` instruction. Image scanners like Checkmarx One read the image's `Config.User` field and flag any final stage that's empty or `root`. Users who have to satisfy those scanners are blocked from adopting DAB unless they override `securityContext.runAsUser` in their pod spec. DAB is just an ASP.NET Core process and does not need root privileges, so the fix is to declare a non-root user explicitly. The published `mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0` base image already ships with a non-root user (UID/GID 1654, exposed via the `APP_UID` env var), so no `useradd` layer is needed. ## What is this change? Restructure the Dockerfile into a multi-target build that publishes **two** runtime variants from the same source: | Target | Tag (proposed) | Runs as | Backwards compatible? | Scanner clean? | |---|---|---|---|---| | `runtime` (default, last stage) | `:<version>` | `root` | identical to today's image | No (unchanged) | | `runtime-nonroot` (opt-in via `--target`) | `:<version>-nonroot` | UID 1654 (`$APP_UID`) | n/a new variant | Yes | The root variant is the **last stage in the Dockerfile**, so a plain `docker build .` with no `--target` argument still produces the existing root-running image. No existing user sees a behavior change. #### Safeguards on the non-root variant To minimize the chance of runtime breakage when users adopt the non-root tag: - **`chown $APP_UID:$APP_UID /App/logs`** (non-recursive) so the documented default file-sink path (`runtime.telemetry.file` → `logs/dab-log.txt`, relative to `WORKDIR /App`) is writable by the non-root user. Ownership of the published assemblies under `/App` is intentionally left unchanged — a recursive `chown -R` would duplicate every assembly layer and roughly double the non-root image size for no runtime benefit, since DAB only needs write access to `/App/logs`. Only ownership of `/App/logs` is changed, not file modes. - **Pre-create `/App/logs`** so the documented default file-sink path works with no extra volume or permission setup. - **Numeric `USER $APP_UID`** (rather than `USER app`) per .NET container guidance — a numeric UID is friendlier to image scanners and to Kubernetes `runAsNonRoot`/`runAsUser` checks, which cannot resolve a username to a UID at admission time. - **Default port stays at `5000`**, which is above 1024, so binding works without `CAP_NET_BIND_SERVICE`. Users overriding `ASPNETCORE_URLS` to a privileged port (<1024) must add `--cap-add=NET_BIND_SERVICE` or front DAB with a reverse proxy. - **OCI labels** on the non-root variant for clarity in registries/tooling. #### Known caveats for consumers of the non-root variant These are inherent to running any non-root container and cannot be fully eliminated in the image. They should be called out in release notes: - **Host bind-mounts** (config, logs, certs, etc.) must be readable and writable, if DAB needs to write them, by UID 1654 on the host. Either `chown -R 1654:1654 /host/path` or, in Kubernetes, set `securityContext.fsGroup: 1654`. - **`docker exec`** defaults to UID 1654. Use `docker exec --user 0` for administrative actions inside a running container. - **Downstream Dockerfiles** (`FROM <this image>`) that need to install packages or write outside `/App` should add `USER 0` before those instructions, then restore `USER $APP_UID` at the end. ## Manual testing performed Both variants were built locally and verified end-to-end: | Check | `:<version>` (default) | `:<version>-nonroot` | |---|---|---| | `docker build` succeeds | yes | yes | | `Config.User` (what scanners read) | empty (= root) | `app` | | Runtime UID inside container | `0(root)` | `1654(app)` | | `/App` ownership | `root:root` | `app:app` (recursive) | | `/App/logs` pre-created | n/a | yes | | DAB process starts | yes | yes | | File-sink telemetry writes a log file | n/a | yes (`dab-log20260519.txt` written to `/App/logs/` by UID 1654) | | Behavior matches previously published image | yes (byte-for-byte equivalent) | n/a (new variant) | #### Follow-up work (separate PR) The publish pipeline needs to be updated to build and push the second tag. That change lives outside this repo and will be done after this PR merges. --------- Co-authored-by: Souvik Ghosh <souvikofficial04@gmail.com>
1 parent 57a195c commit 2210289

1 file changed

Lines changed: 68 additions & 1 deletion

File tree

Dockerfile

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,78 @@ WORKDIR /src
77
COPY [".", "./"]
88
RUN dotnet build "./src/Service/Azure.DataApiBuilder.Service.csproj" -c Docker -o /out -r linux-x64
99

10-
FROM mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0 AS runtime
10+
# ---------------------------------------------------------------------------
11+
# Common runtime base.
12+
#
13+
# Not intended as a final build target. Both the `runtime` (root, default)
14+
# and `runtime-nonroot` (non-root, scanner-friendly) variants derive from
15+
# this stage so the shared setup stays in one place.
16+
# ---------------------------------------------------------------------------
17+
FROM mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0 AS runtime-base
1118

1219
COPY --from=build /out /App
1320
# Add default dab-config.json to /App in the image
1421
COPY --from=build /out/dab-config.json /App/dab-config.json
1522
WORKDIR /App
1623
ENV ASPNETCORE_URLS=http://+:5000
24+
EXPOSE 5000
1725
ENTRYPOINT ["dotnet", "Azure.DataApiBuilder.Service.dll"]
26+
27+
# ---------------------------------------------------------------------------
28+
# Non-root variant. Build explicitly with:
29+
# docker build --target runtime-nonroot -t <repo>:<version>-nonroot .
30+
#
31+
# Runs as the non-root user that ships with the
32+
# mcr.microsoft.com/dotnet/aspnet base image (UID/GID 1654, exposed via the
33+
# APP_UID env var, on the azurelinux3.0 variant). DAB does not require root,
34+
# and declaring USER explicitly sets the image's Config.User field so image
35+
# scanners (e.g. Checkmarx One) that require a non-root user in the final
36+
# stage are satisfied. We use the numeric `USER $APP_UID` form (rather than
37+
# `USER app`) per .NET container guidance: a numeric UID is friendlier to
38+
# image scanners and to Kubernetes `runAsNonRoot`/`runAsUser` checks, which
39+
# cannot resolve a username to a UID at admission time.
40+
#
41+
# Safeguards applied to minimize the chance of runtime breakage:
42+
# * Pre-create /App/logs and `chown app:app /App/logs` (non-recursive) so
43+
# the documented default file-sink path ("logs/dab-log.txt", relative to
44+
# WORKDIR /App) is writable. Ownership of the published assemblies under
45+
# /App is intentionally left unchanged - a recursive chown would
46+
# duplicate every assembly layer and roughly double the image size for
47+
# no runtime benefit, since DAB only needs write access to /App/logs.
48+
# * Default port stays at 5000, which is above 1024, so binding works
49+
# without CAP_NET_BIND_SERVICE. Users overriding ASPNETCORE_URLS to a
50+
# privileged port (<1024) must add `--cap-add=NET_BIND_SERVICE` to
51+
# `docker run` or front DAB with a reverse proxy.
52+
#
53+
# Notes for consumers of this image:
54+
# * Host bind-mounts (config, logs, certs, etc.) must be readable - and
55+
# writable, if DAB needs to write them - by UID 1654 on the host.
56+
# Either `chown -R 1654:1654 /host/path` or, in Kubernetes, set
57+
# `securityContext.fsGroup: 1654`.
58+
# * `docker exec` defaults to UID 1654. Use `docker exec --user 0`
59+
# for administrative actions inside a running container.
60+
# * Downstream Dockerfiles (FROM <this image>) that need to install
61+
# packages or write outside /App should add `USER 0` before those
62+
# instructions, then restore `USER $APP_UID` at the end.
63+
# ---------------------------------------------------------------------------
64+
FROM runtime-base AS runtime-nonroot
65+
66+
RUN mkdir -p /App/logs && chown $APP_UID:$APP_UID /App/logs
67+
USER $APP_UID
68+
69+
LABEL org.opencontainers.image.title="Data API builder (non-root)" \
70+
org.opencontainers.image.description="Data API builder running as the non-root 'app' user (UID 1654 on azurelinux3.0)." \
71+
org.opencontainers.image.source="https://github.com/Azure/data-api-builder"
72+
73+
# ---------------------------------------------------------------------------
74+
# Default (root-running) variant. Build with either:
75+
# docker build -t <repo>:<version> . # no --target needed
76+
# docker build --target runtime -t <repo>:<version> .
77+
#
78+
# This is the LAST stage in the file, so a plain `docker build` with no
79+
# --target argument produces this image. Keeping the root-running variant
80+
# as the default preserves backwards compatibility with the previously
81+
# published image - existing users see no behavior change. The non-root
82+
# variant is opt-in via `--target runtime-nonroot`.
83+
# ---------------------------------------------------------------------------
84+
FROM runtime-base AS runtime

0 commit comments

Comments
 (0)