diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 777b0b9..04ac71c 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -108,6 +108,12 @@ on: node-mirror-token: description: Authorization header value for `node-mirror`. required: false + ca-certificates: + description: >- + Extra CA certificates (PEM) the image build trusts while `apk upgrade` + fetches Alpine fixes — a TLS-intercepting proxy's CA, say. Written + to trusted_certs/ before `docker build`; see the Dockerfile. + required: false outputs: image: description: 'The first pushed image reference, when one was pushed.' @@ -251,9 +257,15 @@ jobs: KB_IMAGE: ${{ inputs.image-name }} KB_TAGS: ${{ inputs.image-tags }} KB_SHA: ${{ github.sha }} + KB_CA_CERTIFICATES: ${{ secrets.ca-certificates }} run: | set -euo pipefail + # The Dockerfile trusts every *.pem in trusted_certs/ for its apk step. + if [ -n "${KB_CA_CERTIFICATES:-}" ]; then + printf '%s\n' "$KB_CA_CERTIFICATES" > trusted_certs/ca-certificates.pem + fi + tags="${KB_TAGS:-$KB_SHA}" # Accept either separator; normalise to one tag per line. tags="$(printf '%s' "$tags" | tr ',' '\n' | sed '/^[[:space:]]*$/d')" diff --git a/.gitignore b/.gitignore index 75d67aa..ce0da41 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ test-results/ public/ # Copied from node_modules by scripts/setup-test-apps.mjs on every run. tests/fixtures/single-page-bundle/*/assets/mermaid.min.js + +# Local CA certificates for the image build (see Dockerfile); never committed. +trusted_certs/* +!trusted_certs/.gitkeep diff --git a/Dockerfile b/Dockerfile index 69b3752..36679f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,22 @@ FROM nginxinc/nginx-unprivileged:1.31-alpine@sha256:b5047b9b76845eac124358373c68 # digest pin keeps nginx itself fixed, but upstream rebuilds only every few # weeks, and a CVE fixed in an Alpine package fails the Trivy scan until then. # The image runs as UID 101, so switch to root for the upgrade and back after. +# +# +# Behind a TLS-intercepting proxy apk fails with "server certificate not +# trusted". Drop the proxy's CA (PEM, *.pem or *.crt) into trusted_certs/, or +# point TRUSTED_SSL_CERTS at another directory in the build context +# (`docker build --build-arg TRUSTED_SSL_CERTS=./my-certs .`); every file there +# is appended to the system CA bundle before apk runs. The committed directory +# is empty, so a build without one trusts the public CAs only. +ARG TRUSTED_SSL_CERTS=./trusted_certs +COPY $TRUSTED_SSL_CERTS /opt/certs/ USER root -RUN apk upgrade --no-cache +RUN for CERT_PATH in /opt/certs/*.pem /opt/certs/*.crt; do \ + [ -e "$CERT_PATH" ] || continue; \ + { cat "$CERT_PATH"; echo; } >> /etc/ssl/certs/ca-certificates.crt; \ + done \ + && apk upgrade --no-cache USER 101 # Overwrite the stock config rather than `RUN rm`-ing it: this image drops to a diff --git a/contract/DEPLOYMENT.md b/contract/DEPLOYMENT.md index e004092..9313c7b 100644 --- a/contract/DEPLOYMENT.md +++ b/contract/DEPLOYMENT.md @@ -178,6 +178,7 @@ three more things and changes nothing else: npm-token: ${{ secrets.ARTIFACTORY_TOKEN }} registry-username: ${{ secrets.ARTIFACTORY_USER }} registry-password: ${{ secrets.ARTIFACTORY_TOKEN }} + ca-certificates: ${{ secrets.CORP_CA_PEM }} # behind a TLS-intercepting proxy ``` | Input | What it covers | @@ -185,6 +186,7 @@ three more things and changes nothing else: | `runs-on` | A single label as it is; a JSON array or object for several labels or a runner group. Default `ubuntu-latest`. | | `npm-registry`, `npm-token` (secret) | The `npm ci` of the build's own dependencies. Empty means whatever npm resolves on the runner. | | `node-mirror`, `node-mirror-token` (secret) | Where `setup-node` downloads Node when it can reach neither the github.com release assets it tries first nor nodejs.org. A mirror of `https://nodejs.org` — an Artifactory generic remote. Not needed when the runner image carries Node 24 in its tool cache. | +| `ca-certificates` (secret) | Extra CA certificates (PEM) for the image build's `apk upgrade`, which otherwise fails behind a TLS-intercepting proxy with `TLS: server certificate not trusted` (exit 99): the base image trusts public CAs only. Written to `trusted_certs/` before `docker build`; the Dockerfile appends every `*.pem`/`*.crt` there to the system CA bundle. A pipeline running `docker build` itself drops the files into `trusted_certs/`, or passes `--build-arg TRUSTED_SSL_CERTS=`. The certificates stay in the image. | The publishing actions take the same four inputs, so docs repos on the same runners publish the same way (`actions/publish-docs/README.md`, diff --git a/tests/private-registry.spec.js b/tests/private-registry.spec.js index 97d8d0f..a57f167 100644 --- a/tests/private-registry.spec.js +++ b/tests/private-registry.spec.js @@ -92,13 +92,16 @@ test.describe('the private-registry inputs exist on every shared CI piece', () = for (const input of ['runs-on', 'npm-registry', 'node-mirror']) { expect(text).toContain(`\n ${input}:\n`); } - for (const secret of ['npm-token', 'node-mirror-token']) { + for (const secret of ['npm-token', 'node-mirror-token', 'ca-certificates']) { expect(text).toContain(`\n ${secret}:\n`); } expect(text).toContain('KB_NPM_REGISTRY: ${{ inputs.npm-registry }}'); expect(text.split('KB_NPM_TOKEN: ${{ secrets.npm-token }}').length - 1).toBe(2); expect(text).toContain('mirror: ${{ inputs.node-mirror }}'); expect(text).toContain('mirror-token: ${{ secrets.node-mirror-token }}'); + // The proxy CA reaches the Dockerfile's apk step through trusted_certs/. + expect(text).toContain('> trusted_certs/ca-certificates.pem'); + expect(read('Dockerfile')).toContain('ARG TRUSTED_SSL_CERTS=./trusted_certs'); expect(text).toMatch(/runs-on: \$\{\{ .*fromJSON\(inputs\.runs-on\).*inputs\.runs-on \}\}/); // The workflow inlines the script's logic; keep the two token lines identical // so a fix to one cannot silently miss the other. diff --git a/trusted_certs/.gitkeep b/trusted_certs/.gitkeep new file mode 100644 index 0000000..e69de29