From d03bf98994f080c83d3bf0a6d08b475752907987 Mon Sep 17 00:00:00 2001 From: Alessandro Bitetto Date: Fri, 12 Jun 2026 12:16:49 +0200 Subject: [PATCH] fix: fail closed on insecure default secrets in cluster entrypoints The orchestrated (cluster/k8s) entrypoints fell back to known upstream placeholder secrets when none were supplied: the Docs entrypoint defaulted JWT_SECRET to "secret" (with JWT enabled by default) and the secure-link secret to "verysecretstring", and the proxy entrypoint defaulted the same secure-link value plus the /info basic-auth password to "password". A known JWT secret makes inbox tokens trivially forgeable, and a known secure-link secret defeats the nginx signature on cached file URLs. Unlike the standalone image, cluster pods are ephemeral and run as multiple replicas behind a shared proxy, so secrets cannot be generated per-pod and must be supplied explicitly (e.g. via a Kubernetes Secret). Refuse to start with the placeholder values when the corresponding feature is enabled, and drop the insecure literal defaults from the generated config so a known secret can never reach the running configuration. This is a deliberate breaking change: clusters relying on the old defaults must now provide JWT_SECRET, SECURE_LINK_SECRET, and INFO_ALLOWED_PASSWORD (when INFO_ALLOWED_USER is set). Signed-off-by: Alessandro Bitetto --- .../scripts/orchestrated/docker-entrypoint.sh | 40 +++++++++++++++++-- .../orchestrated/proxy-docker-entrypoint.sh | 18 ++++++++- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/build/scripts/orchestrated/docker-entrypoint.sh b/build/scripts/orchestrated/docker-entrypoint.sh index 2be78aad84..11773a7943 100755 --- a/build/scripts/orchestrated/docker-entrypoint.sh +++ b/build/scripts/orchestrated/docker-entrypoint.sh @@ -74,6 +74,40 @@ else REDIS_CLUSTER='' fi +# -------------------------------------------------------------------- +# Fail closed on insecure default secrets. +# +# The standalone image generates and persists random secrets, but cluster +# pods are ephemeral and run as multiple replicas behind a shared proxy: a +# per-pod generated secret would differ across replicas and break JWT / +# secure-link verification. Secrets must therefore be supplied explicitly +# (e.g. via a Kubernetes Secret). Refuse to start with the known upstream +# placeholder values ("secret" / "verysecretstring") when the corresponding +# feature is enabled, rather than silently accepting forgeable tokens. +# -------------------------------------------------------------------- +JWT_ENABLED_EFF="${JWT_ENABLED:-true}" +JWT_ENABLED_INBOX_EFF="${JWT_ENABLED_INBOX:-$JWT_ENABLED_EFF}" +JWT_ENABLED_OUTBOX_EFF="${JWT_ENABLED_OUTBOX:-$JWT_ENABLED_EFF}" + +# browser/session verify with JWT_SECRET directly; inbox/outbox fall back to it. +if [ "$JWT_ENABLED_EFF" = "true" ] && [ -z "${JWT_SECRET:-}" ]; then + echo "ERROR: JWT browser verification is enabled (JWT_ENABLED=true) but JWT_SECRET is not set. Provide a strong JWT_SECRET (e.g. via a Kubernetes Secret) or set JWT_ENABLED=false. Refusing to start with an insecure default." >&2 + exit 1 +fi +if [ "$JWT_ENABLED_INBOX_EFF" = "true" ] && [ -z "${JWT_SECRET_INBOX:-}" ] && [ -z "${JWT_SECRET:-}" ]; then + echo "ERROR: inbox JWT is enabled but neither JWT_SECRET_INBOX nor JWT_SECRET is set. Refusing to start with an insecure default." >&2 + exit 1 +fi +if [ "$JWT_ENABLED_OUTBOX_EFF" = "true" ] && [ -z "${JWT_SECRET_OUTBOX:-}" ] && [ -z "${JWT_SECRET:-}" ]; then + echo "ERROR: outbox JWT is enabled but neither JWT_SECRET_OUTBOX nor JWT_SECRET is set. Refusing to start with an insecure default." >&2 + exit 1 +fi + +if [ -z "${SECURE_LINK_SECRET:-}" ]; then + echo "ERROR: SECURE_LINK_SECRET is not set. It signs internal cache/file URLs and must be identical across all Docs and proxy replicas. Provide a strong shared value (e.g. via a Kubernetes Secret). Refusing to start with an insecure default." >&2 + exit 1 +fi + # -------------------------------------------------------------------- # NODE_CONFIG (exported for Docs services) # -------------------------------------------------------------------- @@ -135,7 +169,7 @@ export NODE_CONFIG='{ }, "secret": { "inbox": { - "string": "'${JWT_SECRET_INBOX:-${JWT_SECRET:=secret}}'" + "string": "'${JWT_SECRET_INBOX:-${JWT_SECRET}}'" }, "outbox": { "string": "'${JWT_SECRET_OUTBOX:-${JWT_SECRET}}'" @@ -190,14 +224,14 @@ export NODE_CONFIG='{ "storage": { "fs": { "folderPath": "/var/lib/'${COMPANY_NAME}'/documentserver/App_Data/cache/files/'${STORAGE_SUBDIRECTORY_NAME:-latest}'", - "secretString": "'${SECURE_LINK_SECRET:-verysecretstring}'" + "secretString": "'${SECURE_LINK_SECRET}'" }, "storageFolderName": "files/'${STORAGE_SUBDIRECTORY_NAME:-latest}'" }, "persistentStorage": { "fs": { "folderPath": "/var/lib/'${COMPANY_NAME}'/documentserver/App_Data/cache/files", - "secretString": "'${SECURE_LINK_SECRET:-verysecretstring}'" + "secretString": "'${SECURE_LINK_SECRET}'" }, "storageFolderName": "files" } diff --git a/build/scripts/orchestrated/proxy-docker-entrypoint.sh b/build/scripts/orchestrated/proxy-docker-entrypoint.sh index 0f9dbab2ec..dfe06fd02c 100755 --- a/build/scripts/orchestrated/proxy-docker-entrypoint.sh +++ b/build/scripts/orchestrated/proxy-docker-entrypoint.sh @@ -1,6 +1,16 @@ #!/usr/bin/env bash set -e +# Fail closed on an insecure default secure-link secret. This value signs the +# internal cache/file URLs and MUST be identical to the one the Docs replicas +# use (see docker-entrypoint.sh), so it cannot be generated per-pod — it has to +# be supplied explicitly (e.g. via a Kubernetes Secret). Refuse to start with +# the known upstream placeholder rather than serve forgeable signed URLs. +if [ -z "${SECURE_LINK_SECRET:-}" ]; then + echo "ERROR: SECURE_LINK_SECRET is not set. Provide a strong shared value matching the Docs replicas (e.g. via a Kubernetes Secret). Refusing to start with an insecure default." >&2 + exit 1 +fi + if ! [ -d /tmp/proxy_nginx ]; then mkdir /tmp/proxy_nginx fi @@ -19,7 +29,7 @@ fi envsubst < /tmp/proxy_nginx/includes/http-upstream.conf > /tmp/http-upstream.conf envsubst < /etc/nginx/includes/ds-common.conf | tee /tmp/proxy_nginx/includes/ds-common.conf > /dev/null -sed "s,\(set \+\$secure_link_secret\).*,\1 "${SECURE_LINK_SECRET:-verysecretstring}";," -i /tmp/proxy_nginx/conf.d/ds.conf +sed "s,\(set \+\$secure_link_secret\).*,\1 "${SECURE_LINK_SECRET}";," -i /tmp/proxy_nginx/conf.d/ds.conf sed "s/\(client_max_body_size\).*/\1 $NGINX_CLIENT_MAX_BODY_SIZE;/" -i /tmp/proxy_nginx/includes/ds-common.conf if [[ ! -f "/proc/net/if_inet6" ]]; then @@ -53,7 +63,11 @@ if [[ -n "$INFO_ALLOWED_IP" ]]; then fi if [[ -n "$INFO_ALLOWED_USER" ]]; then - htpasswd -c -b /tmp/auth "${INFO_ALLOWED_USER}" "${INFO_ALLOWED_PASSWORD:-password}" + if [[ -z "${INFO_ALLOWED_PASSWORD:-}" ]]; then + echo "ERROR: INFO_ALLOWED_USER is set but INFO_ALLOWED_PASSWORD is empty. Refusing to expose /info behind a default password." >&2 + exit 1 + fi + htpasswd -c -b /tmp/auth "${INFO_ALLOWED_USER}" "${INFO_ALLOWED_PASSWORD}" sed -i '/(info)/a\ auth_basic \"Authentication Required\"\;' /tmp/proxy_nginx/includes/ds-docservice.conf sed -i '/auth_basic/a\ auth_basic_user_file \/tmp\/auth\;' /tmp/proxy_nginx/includes/ds-docservice.conf fi