fix(security): replace APPS_JSON_BASE64 build-arg with BuildKit secret mount

APPS_JSON_BASE64 is stored in image layer metadata, permanently exposing
private repo tokens (GitHub PATs) to anyone with image pull access.

Replace --build-arg with --mount=type=secret so that apps.json is only
available during the RUN step and never committed to any layer.

Refs: https://docs.docker.com/reference/build-checks/secrets-used-in-arg-or-env/
This commit is contained in:
OmarElaraby26 2026-04-05 22:24:53 +02:00
parent 65d9510a2b
commit ae275df161
4 changed files with 21 additions and 36 deletions

View file

@ -42,23 +42,19 @@ To include custom apps in your image, create an `apps.json` file in the reposito
] ]
``` ```
Then generate a base64-encoded string from this file:
```bash
export APPS_JSON_BASE64=$(base64 -w 0 apps.json)
```
# Build the image # Build the image
Choose the appropriate build command based on your container runtime and desired image type. This example builds the `layered` image with the custom `apps.json` you created. Choose the appropriate build command based on your container runtime and desired image type. This example builds the `layered` image with the custom `apps.json` you created.
> **Security note:** The `apps.json` file is passed as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repository tokens are **never** stored in image layer metadata. Do not use `--build-arg` for `apps.json` — build arguments are permanently visible via `docker image history`.
`Docker`: `Docker`:
```bash ```bash
docker build \ DOCKER_BUILDKIT=1 docker build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=version-15 \ --build-arg=FRAPPE_BRANCH=version-15 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ --secret=id=apps_json,src=apps.json \
--tag=custom:15 \ --tag=custom:15 \
--file=images/layered/Containerfile . --file=images/layered/Containerfile .
``` ```
@ -69,7 +65,7 @@ docker build \
podman build \ podman build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=version-15 \ --build-arg=FRAPPE_BRANCH=version-15 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ --secret=id=apps_json,src=apps.json \
--tag=custom:15 \ --tag=custom:15 \
--file=images/layered/Containerfile . --file=images/layered/Containerfile .
``` ```
@ -82,7 +78,7 @@ podman build \
| FRAPPE_PATH | Repository URL for Frappe framework source code. Defaults to https://github.com/frappe/frappe | | FRAPPE_PATH | Repository URL for Frappe framework source code. Defaults to https://github.com/frappe/frappe |
| FRAPPE_BRANCH | Branch to use for Frappe framework. Defaults to version-15 | | FRAPPE_BRANCH | Branch to use for Frappe framework. Defaults to version-15 |
| **Custom Apps** | | | **Custom Apps** | |
| APPS_JSON_BASE64 | Base64-encoded JSON string from apps.json defining apps to install | | (secret) apps_json | Passed via `--secret=id=apps_json,src=apps.json`. Never use `--build-arg` for this file. |
| **Dependencies** | | | **Dependencies** | |
| PYTHON_VERSION | Python version for the base image | | PYTHON_VERSION | Python version for the base image |
| NODE_VERSION | Node.js version | | NODE_VERSION | Node.js version |

View file

@ -84,25 +84,17 @@ cat > ~/gitops/apps.json <<'EOF'
EOF EOF
``` ```
Generate the BASE64 value and build: Build the image, passing `apps.json` as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repo tokens are never stored in image layers:
```shell ```shell
export APPS_JSON_BASE64=$(base64 -w 0 ~/gitops/apps.json) DOCKER_BUILDKIT=1 docker build \
docker build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=version-16 \ --build-arg=FRAPPE_BRANCH=version-16 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ --secret=id=apps_json,src=$HOME/gitops/apps.json \
--tag=my-erpnext-prod-image:16.0.0 \ --tag=my-erpnext-prod-image:16.0.0 \
--file=images/layered/Containerfile . --file=images/layered/Containerfile .
``` ```
If `base64 -w 0` is not available on your system, use:
```shell
export APPS_JSON_BASE64=$(base64 ~/gitops/apps.json | tr -d '\n')
```
### Configure environment ### Configure environment
Create an environment file for the bench: Create an environment file for the bench:

View file

@ -113,18 +113,17 @@ RUN apt-get update \
libbz2-dev \ libbz2-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# apps.json includes # apps.json is passed as a BuildKit secret so that private repo tokens
ARG APPS_JSON_BASE64 # are never baked into any image layer. The secret is mounted only for
RUN if [ -n "${APPS_JSON_BASE64}" ]; then \ # this RUN step and is not present in the final image.
mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \
fi
USER frappe USER frappe
ARG FRAPPE_BRANCH=version-16 ARG FRAPPE_BRANCH=version-16
ARG FRAPPE_PATH=https://github.com/frappe/frappe ARG FRAPPE_PATH=https://github.com/frappe/frappe
RUN export APP_INSTALL_ARGS="" && \ RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1000 \
if [ -n "${APPS_JSON_BASE64}" ]; then \ export APP_INSTALL_ARGS="" && \
if [ -f /opt/frappe/apps.json ] && [ -s /opt/frappe/apps.json ]; then \
export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \ export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \
fi && \ fi && \
bench init ${APP_INSTALL_ARGS}\ bench init ${APP_INSTALL_ARGS}\

View file

@ -4,18 +4,16 @@ FROM frappe/build:${FRAPPE_BRANCH} AS builder
ARG FRAPPE_BRANCH=version-16 ARG FRAPPE_BRANCH=version-16
ARG FRAPPE_PATH=https://github.com/frappe/frappe ARG FRAPPE_PATH=https://github.com/frappe/frappe
ARG APPS_JSON_BASE64
USER root # apps.json is passed as a BuildKit secret so that private repo tokens
# are never baked into any image layer. The secret is mounted only for
RUN if [ -n "${APPS_JSON_BASE64}" ]; then \ # this RUN step and is not present in the final image.
mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \
fi
USER frappe USER frappe
RUN export APP_INSTALL_ARGS="" && \ RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1000 \
if [ -n "${APPS_JSON_BASE64}" ]; then \ export APP_INSTALL_ARGS="" && \
if [ -f /opt/frappe/apps.json ] && [ -s /opt/frappe/apps.json ]; then \
export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \ export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \
fi && \ fi && \
bench init ${APP_INSTALL_ARGS}\ bench init ${APP_INSTALL_ARGS}\