From d9d6ec82ab7ad5279ba8ebc059f58bd663bad87f Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 5 Aug 2026 19:13:29 +0000 Subject: [PATCH] feat: register mounted CA certificates in windmill_extra at startup (#10545) * feat: register mounted CA certificates in windmill_extra at startup Co-Authored-By: Claude Opus 5 (1M context) * fix: only claim a CA update when update-ca-certificates can read the mount Co-Authored-By: Claude Opus 5 (1M context) * fix: detect mounted CA certificates the way update-ca-certificates finds them Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- debugger/README.md | 15 ++++++++++++--- docker-compose.yml | 4 ++++ docker/entrypoint-extra.sh | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/debugger/README.md b/debugger/README.md index 0adb3b4809..31599f4db8 100644 --- a/debugger/README.md +++ b/debugger/README.md @@ -118,9 +118,18 @@ not enough on its own, since `requests` carries its own bundle and Node reads on `NODE_EXTRA_CA_CERTS`. Registry settings are deliberately not forwarded: they carry credentials and only the service needs them. -To install that CA into the container's system store in the first place, set `INIT_SCRIPT` on the -`windmill_extra` container (e.g. `INIT_SCRIPT=update-ca-certificates`). It runs before any service -starts and aborts startup if it fails, the same hook a worker offers. +Registering that CA in the container's system store happens on its own: mount it into +`/usr/local/share/ca-certificates/` **named `*.crt`**, the only extension `update-ca-certificates` +reads, and `windmill_extra` runs it before starting any service. `RUN_UPDATE_CA_CERTIFICATE_AT_START=true` forces the same thing whether or not +certificates are mounted there, and `RUN_UPDATE_CA_CERTIFICATE_PATH` overrides the tool, matching +the server and worker. Both are best-effort: a UID that cannot write `/etc/ssl/certs` logs a warning +and the container still boots. `INIT_SCRIPT` remains the hook for anything more involved, and unlike +the CA update it aborts startup when it fails. + +Note what the system store does *not* cover, which is most of what a debug session installs with: +uv trusts its own bundled roots unless `PY_NATIVE_CERT`/`UV_NATIVE_TLS` is `true`, Bun and Node read +only `NODE_EXTRA_CA_CERTS`, and `requests` carries certifi. Registering the CA fixes Python's stdlib +`ssl`, `curl` and `git`; the rest still needs the variables above. Keeping the settings out of the session's environment only bounds what the debugged script can read from itself. An unsandboxed session runs under the same user as the service and can still read the diff --git a/docker-compose.yml b/docker-compose.yml index 93fbf28e8d..a801a0ce7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -187,6 +187,10 @@ services: # - DEBUG_ALLOWED_ORIGINS=https://your-windmill-host # Optional CSWSH hardening: comma-separated allowlist of browser Origins permitted to open debug WebSockets volumes: - lsp_cache:/pyls/.cache + # Behind a TLS-intercepting proxy, mount its CA here (as .crt) and it is registered in the + # system trust store before any service starts. That alone does not cover dependency + # installation — see debugger/README.md for the variables it also needs + # - ./corp-ca.crt:/usr/local/share/ca-certificates/corp-ca.crt:ro logging: *default-logging caddy: diff --git a/docker/entrypoint-extra.sh b/docker/entrypoint-extra.sh index e5cc4e61fc..b32ad2dd94 100644 --- a/docker/entrypoint-extra.sh +++ b/docker/entrypoint-extra.sh @@ -33,6 +33,44 @@ if [ ! -w "$HOME" ]; then fi export HOME +# Register CA certificates mounted into the image before anything opens a TLS connection. +# Best-effort on purpose, unlike INIT_SCRIPT below: a non-root UID cannot write /etc/ssl/certs, and +# a deployment that never needed a custom CA must still boot. Env var names and the default-off +# behavior match the server/worker binary, so one setting covers every container. What the system +# trust store does and does not reach is documented in debugger/README.md. +CA_CERT_DIR=/usr/local/share/ca-certificates + +update_ca_certificates() { + local reason="$1" + local tool="${RUN_UPDATE_CA_CERTIFICATE_PATH:-/usr/sbin/update-ca-certificates}" + local output + if [ ! -x "$tool" ]; then + echo "[entrypoint] $reason but $tool is not executable, skipping CA update" + return + fi + echo "[entrypoint] $reason, running $tool" + if output=$("$tool" 2>&1); then + echo "[entrypoint] CA certificates updated" + else + # Carry the tool's own message: the usual cause is an unwritable /etc/ssl/certs under a + # non-root UID, but guessing that in place of the real error hides everything else. + echo "[entrypoint] WARNING: $tool failed (UID $(id -u)): ${output:-no output}; continuing" >&2 + fi +} + +if [ "$(echo "${RUN_UPDATE_CA_CERTIFICATE_AT_START:-false}" | tr '[:upper:]' '[:lower:]')" = "true" ]; then + update_ca_certificates "RUN_UPDATE_CA_CERTIFICATE_AT_START=true" +elif [ -n "$(find -L "$CA_CERT_DIR" -type f -name '*.crt' -print -quit 2>/dev/null)" ]; then + # Certificates mounted there are unambiguous intent, and they do nothing until registered, so + # take the same action without making the operator also find the env var. + update_ca_certificates "Found certificates in $CA_CERT_DIR" +elif [ -n "$(ls -A "$CA_CERT_DIR" 2>/dev/null)" ]; then + # Reporting success over a mount update-ca-certificates ignores would be worse than saying + # nothing: .pem is the spelling people reach for, and only .crt is read. + echo "[entrypoint] WARNING: $CA_CERT_DIR has files but none named *.crt, the only extension" \ + "update-ca-certificates reads; they will be ignored" >&2 +fi + # INIT_SCRIPT is the documented hook for preparing the host before anything reaches the network # (CA certificates, proxies, mounts), matching the worker's INIT_SCRIPT. It must therefore complete # before any service starts, and a failure has to abort: services that come up with an unprepared