Skip to content

fix(security): APPS_JSON_BASE64 build-arg leaks private repo tokens in image layer metadata #1860

@OmarElaraby26

Description

@OmarElaraby26

Labels

security, bug, docker, docs


Description of the issue

When building custom images with private apps, apps.json contains GitHub PATs embedded in repository URLs. The current Containerfiles (images/custom/Containerfile and images/layered/Containerfile) pass this as a Docker build argument (--build-arg=APPS_JSON_BASE64). Docker build arguments are permanently recorded in image layer metadata, meaning anyone with access to the image can trivially extract private repo tokens.

This is a known Docker anti-pattern documented by Docker themselves: SecretsUsedInArgOrEnv.

Impact: Any image built with private apps using the documented workflow has credentials permanently embedded. Images pushed to registries (even private ones) expose PATs to all users with pull access.

Affected files:

  • images/custom/Containerfile
  • images/layered/Containerfile
  • docs/02-setup/02-build-setup.md
  • docs/02-setup/08-single-server-nginxproxy-example.md

Context information (for bug reports)

  • frappe_docker: main branch (latest as of 2026-04-05)
  • Docker Engine: any version with BuildKit support
  • Host OS: any (issue is in Containerfile / docs, not host-specific)

Steps to reproduce the issue

  1. Create apps.json with a private repo URL containing a PAT:
    [
      {
        "url": "https://USER:ghp_XXXX@github.com/org/private-app.git",
        "branch": "main"
      }
    ]
  2. Build the custom image following the current docs:
    docker build \
      --build-arg=APPS_JSON_BASE64=$(base64 -w 0 apps.json) \
      --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
      --build-arg=FRAPPE_BRANCH=version-16 \
      --tag=custom:latest \
      --file=images/custom/Containerfile .
  3. Inspect the image:
    docker image history --no-trunc custom:latest

Observed result

The full base64-encoded apps.json (including PATs) is visible in the ARG layer metadata. Decoding it reveals the plain-text GitHub PAT:

echo "<base64_from_history>" | base64 -d
# {"url": "https://USER:ghp_XXXX@github.com/org/private-app.git", ...}

Expected result

Private repository tokens should never be persisted in image layers or metadata. The image should be safe to push to registries or share with team members without leaking credentials.

Suggested fix

Replace ARG / --build-arg with a Docker BuildKit secret mount. I have a PR ready for this.

Containerfile (remove ARG APPS_JSON_BASE64 and the decode RUN, replace with):

RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1000 \
  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"; \
  fi && \
  bench init ${APP_INSTALL_ARGS} ...

Build command:

DOCKER_BUILDKIT=1 docker build \
  --secret=id=apps_json,src=apps.json \
  --build-arg=FRAPPE_BRANCH=version-16 \
  --tag=custom:latest \
  --file=images/custom/Containerfile .

Key points:

  • --mount=type=secret makes the file available only during that RUN step -- it is never committed to any layer
  • uid=1000,gid=1000 ensures the frappe user can read the secret
  • The -f and -s checks handle the case where no apps.json is provided (backward compatible)
  • Requires DOCKER_BUILDKIT=1 or docker buildx build

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions