]> git.99rst.org Git - sgasser-llm-shield.git/commitdiff
Harden the release container image (#147)
authorStefan Gasser <redacted>
Thu, 23 Jul 2026 16:58:25 +0000 (18:58 +0200)
committerGitHub <redacted>
Thu, 23 Jul 2026 16:58:25 +0000 (18:58 +0200)
* Harden the release container image

* Fix PII detector test isolation

.github/workflows/ci.yml
.github/workflows/release.yml
bun.lock
docker/Dockerfile
package.json
src/privacy/pipeline.test.ts

index 383d74cd10d92d627e84728a4d4c55f1c82a064a..0621696f099214d0b3d13d20183d16223d8f7384 100644 (file)
@@ -79,3 +79,13 @@ jobs:
       # Build the all-in-one image users actually run (proxy + detector).
       - name: Test Docker build
         run: docker build -f docker/Dockerfile -t pasteguard:test .
+
+      - name: Scan Docker image
+        uses: aquasecurity/trivy-action@v0.36.0
+        with:
+          image-ref: pasteguard:test
+          version: v0.72.0
+          exit-code: "1"
+          ignore-unfixed: false
+          severity: HIGH,CRITICAL
+          vuln-type: os,library
index 9feb27df12b0036b52b43b723a5c19294995b25e..a8096eece214977d4d0af6b7b0c4469e0b39cc9d 100644 (file)
@@ -36,7 +36,7 @@ jobs:
 
       - name: Extract version from tag
         id: version
-        run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
+        run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
 
       - uses: docker/build-push-action@v6
         with:
index a9c9a2c8774ddb2b46c57371b02ac4dd5ffab83b..5f8ad7d4472829e9013c11d922938514c49b18cf 100644 (file)
--- a/bun.lock
+++ b/bun.lock
@@ -6,7 +6,7 @@
       "name": "pasteguard",
       "dependencies": {
         "@hono/zod-validator": "^0.7.6",
-        "hono": "^4.12.23",
+        "hono": "^4.12.25",
         "hono-tailwind": "^2.2.0",
         "kysely": "^0.29.2",
         "pg": "^8.22.0",
@@ -99,7 +99,7 @@
 
     "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 
-    "hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
+    "hono": ["hono@4.12.31", "", {}, "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg=="],
 
     "hono-tailwind": ["hono-tailwind@2.2.0", "", { "dependencies": { "@tailwindcss/postcss": "^4.1.6", "postcss": "^8.5.3" }, "peerDependencies": { "hono": "^4.0.0", "tailwindcss": "^4.1.6" } }, "sha512-orA97f08l2nsKU4tu4EAa4/F5KIIscdvsFQkFi07U0WYCpCVEy+Pw4qAY+z4jYgHf7OSnXo7IzLwFxTq4tRH9Q=="],
 
index 7083cb97f7f8f235db7d7f8e9cb60af46cb5b108..eac6aa22ab0f8e796f8926c5c7dedbdf59db4bf3 100644 (file)
@@ -1,3 +1,5 @@
+# syntax=docker/dockerfile:1
+
 # PasteGuard — Docker image (multi-stage, single source of truth)
 #
 # Two build targets share ONE detector definition (the `detector` stage):
@@ -15,6 +17,8 @@
 #     -v ./data:/pasteguard/data \
 #     pasteguard:latest
 
+ARG AMAZON_LINUX_IMAGE=public.ecr.aws/amazonlinux/amazonlinux:2023-minimal@sha256:0caa7d3c2a199a700f488b055058b0798cff190bc0cf08bacd6003b9d03e9dfd
+
 # =============================================================================
 # Stage: bun-builder — build the Bun application
 # =============================================================================
@@ -29,39 +33,70 @@ COPY src ./src
 COPY tsconfig.json ./
 
 # =============================================================================
-# Stage: detector — the PII detector service (also a standalone build target)
+# Stage: detector-builder — install the detector and download its model
 # =============================================================================
-FROM python:3.11-slim AS detector
+FROM ${AMAZON_LINUX_IMAGE} AS detector-builder
+
+RUN microdnf install -y python3.11 python3.11-pip \
+    && microdnf clean all
 
-# CPU-only torch. The CPU index serves both x86_64 and aarch64 CPU wheels, so
-# pin it for every arch. (PyPI's DEFAULT index now ships CUDA builds for
-# linux/arm64 too — e.g. torch 2.x+cuXXX — which would drag ~6 GB of unused
-# CUDA libraries into the image; the explicit CPU index avoids that.)
+RUN python3.11 -m venv /opt/venv
+ENV PATH="/opt/venv/bin:${PATH}"
+
+# The explicit CPU index prevents PyPI from pulling unused CUDA libraries.
 RUN pip install --no-cache-dir typing-extensions \
     && pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
 
-# Install the detector package (pulls fastapi/uvicorn/gliner/stdnum/...).
+RUN pip install --no-cache-dir setuptools==83.0.0
+
+ENV PIP_DEFAULT_TIMEOUT=120
+ENV PIP_RETRIES=5
+
 COPY detector/pyproject.toml /srv/detector/
 COPY detector/detector /srv/detector/detector
-RUN pip install --no-cache-dir /srv/detector
+RUN pip install --no-cache-dir supervisor==4.3.0 /srv/detector \
+    && pip check \
+    && pip uninstall -y pip
 
-# Bake the default model into a location the all-in-one runtime user (UID 1000)
-# can also read, and force offline use at runtime (no network needed for models).
 ENV DETECTOR_MODEL=urchade/gliner_multi_pii-v1
 ENV HF_HOME=/opt/models
-# Retry the fetch: under multi-arch (QEMU) builds a transient HF rate-limit or
-# network blip should not fail the whole image build.
-RUN ok=""; for i in 1 2 3; do \
-        python -c "import os; from gliner import GLiNER; GLiNER.from_pretrained(os.environ['DETECTOR_MODEL'])" && { ok=1; break; }; \
+RUN --mount=type=cache,target=/tmp/huggingface \
+    ok=""; for i in 1 2 3; do \
+        HF_HOME=/tmp/huggingface python -c "import os; from gliner import GLiNER; GLiNER.from_pretrained(os.environ['DETECTOR_MODEL'])" && { ok=1; break; }; \
         echo "model fetch attempt $i failed; retrying in 10s"; sleep 10; \
     done; \
-    [ -n "$ok" ] && chown -R 1000:1000 /opt/models || exit 1
-ENV HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
+    [ -n "$ok" ] \
+    && mkdir -p /opt/models \
+    && cp -a /tmp/huggingface/. /opt/models/ \
+    || exit 1
+
+# =============================================================================
+# Stage: detector — the standalone PII detector runtime
+# =============================================================================
+FROM ${AMAZON_LINUX_IMAGE} AS detector
+
+RUN microdnf install -y python3.11 shadow-utils \
+    && useradd --uid 1000 --create-home --home-dir /home/pasteguard pasteguard \
+    && microdnf remove -y shadow-utils \
+    && microdnf clean all \
+    && mkdir -p /pasteguard/data \
+    && chown -R 1000:1000 /pasteguard
+
+COPY --from=detector-builder /opt/venv /opt/venv
+COPY --from=detector-builder --chown=1000:1000 /opt/models /opt/models
+
+ENV PATH="/opt/venv/bin:${PATH}"
+ENV DETECTOR_MODEL=urchade/gliner_multi_pii-v1
+ENV HF_HOME=/opt/models
+ENV HF_HUB_OFFLINE=1
+ENV TRANSFORMERS_OFFLINE=1
+ENV HOME=/home/pasteguard
 
 EXPOSE 5002
 HEALTHCHECK --interval=10s --timeout=3s --start-period=40s --retries=5 \
     CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:5002/health').status==200 else 1)"
 
+USER 1000
 CMD ["uvicorn", "detector.app:app", "--host", "0.0.0.0", "--port", "5002"]
 
 # =============================================================================
@@ -69,17 +104,9 @@ CMD ["uvicorn", "detector.app:app", "--host", "0.0.0.0", "--port", "5002"]
 # =============================================================================
 FROM detector AS allinone
 
-# supervisor manages both processes; curl backs the health check.
-RUN apt-get update && apt-get install -y --no-install-recommends \
-    supervisor \
-    curl \
-    && apt-get clean \
-    && rm -rf /var/lib/apt/lists/*
-
 # Copy the Bun binary from the official image (baseline build for x86_64
 # compatibility on older CPUs, see https://github.com/sgasser/pasteguard/issues/70).
 COPY --from=bun-builder /usr/local/bin/bun /usr/local/bin/bun
-ENV PATH="/usr/local/bin:${PATH}"
 
 # Copy the Bun application.
 WORKDIR /pasteguard
@@ -89,15 +116,7 @@ COPY --from=bun-builder /app/package.json ./
 COPY --from=bun-builder /app/tsconfig.json ./
 COPY config.example.yaml ./
 
-# Create a real UID-1000 user with a home dir. torch resolves its cache dir via
-# getpwuid() at import, which fails on a bare numeric USER with no passwd entry.
-RUN useradd --uid 1000 --create-home --home-dir /home/pasteguard pasteguard \
-    && mkdir -p /pasteguard/data && chown -R 1000:1000 /pasteguard
-
-COPY docker/supervisord.conf /etc/supervisor/conf.d/pasteguard.conf
-
-USER 1000
-ENV HOME=/home/pasteguard
+COPY docker/supervisord.conf /etc/supervisord.conf
 
 # The proxy talks to the in-container detector.
 ENV DETECTOR_URL=http://localhost:5002
@@ -107,4 +126,4 @@ EXPOSE 3000
 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
     CMD curl -f http://localhost:3000/health || exit 1
 
-CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/pasteguard.conf"]
+CMD ["/opt/venv/bin/supervisord", "-c", "/etc/supervisord.conf"]
index b6f6f8b81d040d06fb1564bc5971162f504f6e7e..a43a2d638e59fb39ac0328da7a204aedfad1f0f9 100644 (file)
@@ -18,7 +18,7 @@
   },
   "dependencies": {
     "@hono/zod-validator": "^0.7.6",
-    "hono": "^4.12.23",
+    "hono": "^4.12.25",
     "hono-tailwind": "^2.2.0",
     "kysely": "^0.29.2",
     "pg": "^8.22.0",
index 5fb19964cb98cb0bc5ce63ab126d0f771848d31d..72faae608d6d514be7f7112ed13359c75c3e0226 100644 (file)
@@ -1,6 +1,6 @@
 import { afterEach, describe, expect, mock, test } from "bun:test";
 import { openaiExtractor } from "../masking/extractors/openai";
-import type { PIIDetectionResult } from "../pii/detect";
+import { filterAllowlistedEntities, type PIIDetectionResult, PIIDetector } from "../pii/detect";
 import type { OpenAIRequest } from "../providers/openai/types";
 import type { PrivacyPipelineConfig } from "./pipeline";
 
@@ -22,6 +22,8 @@ const mockAnalyzeRequest = mock<
 );
 
 mock.module("../pii/detect", () => ({
+  PIIDetector,
+  filterAllowlistedEntities,
   getPIIDetector: () => ({
     analyzeRequest: mockAnalyzeRequest,
     detectPII: mock(() => Promise.resolve([])),
git clone https://git.99rst.org/PROJECT