Commit 2210289
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
11 | 18 | | |
12 | 19 | | |
13 | 20 | | |
14 | 21 | | |
15 | 22 | | |
16 | 23 | | |
| 24 | + | |
17 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
0 commit comments