Files
magdevandClaude Opus 5.5 47138ddfbe BentoPDF for bau-pm, without third-party hosts (bau-pm BAU-119)
BentoPDF v2.8.8 built from source at its pinned commit with BentoPDF's own
air-gapped build options, every module it would load from a CDN vendored and
checked against vendor.lock, a Content-Security-Policy without a single host,
digital-sign-pdf and timestamp-pdf disabled because both need a third party,
and the hand-off glue on every page: the AGPL source link, the hand-in of a
file from bau-pm through a one-minute cookie into sessionStorage, confirmed by
the tool's reaction, the hand-back of a result from every window, and the
signing tool's flatten switch locked on.

The zero-host gate drives all 117 enabled tool pages in Chromium and fails on
any request to another origin, any CSP violation, a CSP that names a host, and
a disabled id that names no page. Green on this image (3 min 52 s on a private
network), red on the stock upstream image (115 findings) and on a misspelled
id. CI runs it on every push, the release workflow before it pushes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:36:02 +02:00

110 lines
4.7 KiB
Docker

# syntax=docker/dockerfile:1
#
# BentoPDF for bau-pm: BentoPDF built from source at a pinned commit with
# BentoPDF's own air-gapped build options, every module it would otherwise load
# from a CDN served from this image, and bau-pm's hand-off glue added to every
# page. Nothing here edits BentoPDF's code: its documented build arguments, its
# runtime config.json, its generated header file with the hosts taken out, and
# our files beside its own.
#
# The images are upstream's: the builder and the runtime stage are the ones
# BentoPDF's Dockerfile uses.
ARG BENTOPDF_REF=v2.8.8
ARG BENTOPDF_COMMIT=f96cd4e5166f3d51393dfe9f3c440b5bb77802f1
ARG NODE_IMAGE=public.ecr.aws/docker/library/node:20-alpine
ARG NGINX_IMAGE=quay.io/nginx/nginx-unprivileged:alpine-slim
# --- BentoPDF's source, at the commit the tag must point to ---
FROM ${NODE_IMAGE} AS source
ARG BENTOPDF_REF
ARG BENTOPDF_COMMIT
RUN apk add --no-cache git
RUN git clone --quiet --depth 1 --branch "${BENTOPDF_REF}" https://github.com/alam00000/bentopdf.git /src \
&& actual=$(git -C /src rev-parse HEAD) \
&& if [ "$actual" != "${BENTOPDF_COMMIT}" ]; then \
echo "BentoPDF ${BENTOPDF_REF} is ${actual}, expected ${BENTOPDF_COMMIT}" >&2; exit 1; \
fi \
&& rm -rf /src/.git
# --- The CDN modules, packed from npm and checked against vendor.lock ---
FROM ${NODE_IMAGE} AS vendor
COPY --from=source /src/bentopdf-airgap-bundle /src/bentopdf-airgap-bundle
COPY vendor.lock /vendor.lock
COPY --chmod=755 scripts/vendor.sh /usr/local/bin/vendor.sh
RUN vendor.sh /src /vendor.lock /wasm
# --- BentoPDF's build, as BentoPDF's Dockerfile runs it ---
FROM ${NODE_IMAGE} AS builder
WORKDIR /app
COPY --from=source /src/package.json /src/package-lock.json ./
COPY --from=source /src/vendor ./vendor
ENV HUSKY=0
RUN npm config set fetch-retries 5 \
&& npm config set fetch-retry-mintimeout 60000 \
&& npm config set fetch-retry-maxtimeout 300000 \
&& npm config set fetch-timeout 600000 \
&& npm ci
COPY --from=source /src ./
# The air-gapped build: every module path is this image's own, the CORS proxy
# (used only by the X.509 tool, which config.json disables) is switched off,
# and Simple Mode drops the marketing pages. Relative paths, so the image does
# not know the hostname it will be served under.
ENV SIMPLE_MODE=true \
DISABLE_GITHUB_STARS=true \
COMPRESSION_MODE=all \
BASE_URL= \
VITE_DEFAULT_LANGUAGE=de \
VITE_CORS_PROXY_URL= \
VITE_WASM_PYMUPDF_URL=/wasm/pymupdf/ \
VITE_WASM_GS_URL=/wasm/gs/ \
VITE_WASM_CPDF_URL=/wasm/cpdf/ \
VITE_TESSERACT_WORKER_URL=/wasm/ocr/worker.min.js \
VITE_TESSERACT_CORE_URL=/wasm/ocr/core \
VITE_TESSERACT_LANG_URL=/wasm/ocr/lang-data \
VITE_TESSERACT_AVAILABLE_LANGUAGES=deu,fra,ita,eng \
VITE_OCR_FONT_BASE_URL=/wasm/ocr/fonts \
VITE_EMBEDPDF_FONTS_URL=/wasm/embedpdf \
NODE_OPTIONS=--max-old-space-size=3072
RUN npm run build
COPY --chmod=755 scripts/finish.sh /usr/local/bin/finish.sh
RUN finish.sh /app
# --- Upstream's runtime stage, with our files beside BentoPDF's ---
FROM ${NGINX_IMAGE}
ARG BENTOPDF_REF
ARG SOURCE_URL=https://src.bundespruefstelle.ch/magdev/bau-pm-pdf-tools
ARG VERSION=dev
LABEL org.opencontainers.image.title="bau-pm-pdf-tools" \
org.opencontainers.image.description="BentoPDF ${BENTOPDF_REF} for bau-pm, without third-party hosts" \
org.opencontainers.image.source="${SOURCE_URL}" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.licenses="AGPL-3.0-only"
ENV DISABLE_IPV6=false \
PORT=8080
USER root
RUN apk upgrade --no-cache
USER nginx
COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html
COPY --chown=nginx:nginx --from=builder /app/nginx.conf /etc/nginx/nginx.conf
COPY --chown=nginx:nginx --from=builder /app/security-headers.conf /etc/nginx/security-headers.conf
COPY --chown=nginx:nginx --from=builder /app/security-headers-docs.conf /etc/nginx/security-headers-docs.conf
COPY --chown=nginx:nginx --chmod=755 --from=builder /app/nginx-ipv6.sh /docker-entrypoint.d/99-disable-ipv6.sh
COPY --chown=nginx:nginx --chmod=755 --from=builder /app/nginx-noindex.sh /docker-entrypoint.d/98-noindex.sh
COPY --chown=nginx:nginx --from=vendor /wasm /usr/share/nginx/html/wasm
COPY --chown=nginx:nginx config/config.json /usr/share/nginx/html/config.json
COPY --chown=nginx:nginx glue/ /usr/share/nginx/html/bau-pm/
RUN sed -i "s#__BAU_PM_SOURCE_URL__#${SOURCE_URL}/src/tag/${VERSION}#" /usr/share/nginx/html/bau-pm/handoff.js \
&& grep -q "${SOURCE_URL}" /usr/share/nginx/html/bau-pm/handoff.js \
&& mkdir -p /etc/nginx/tmp
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD wget --spider -q http://127.0.0.1:8080/ || exit 1
CMD ["nginx", "-g", "daemon off;"]