From 7c43fd976dafe6a1692855968dcb5a25c283e482 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 29 Sep 2026 10:44:22 +0200 Subject: [PATCH 1/4] build(sentry): Prepare for a non-root sentry image The upstream sentry image is moving to run as the non-root sentry user and will drop gosu (getsentry/sentry#125848). Make self-hosted work with both the current root image and the upcoming non-root one: - Run build customizations (nodestore-s3, enhance-image.sh, requirements.txt) as root, then switch to the sentry user. - Trust custom CAs without update-ca-certificates, which needs root: build a combined bundle in /tmp and point the TLS env vars at it. - Fix sentry-data ownership once during install instead of at every container start. - Install gosu in the cleanup image instead of relying on the sentry image. --- cron/Dockerfile | 2 +- install.sh | 1 + install/ensure-sentry-data-ownership.sh | 20 ++++++++++++++++++++ sentry/Dockerfile | 6 ++++++ sentry/entrypoint.sh | 24 ++++++++++++++++++++++-- 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 install/ensure-sentry-data-ownership.sh diff --git a/cron/Dockerfile b/cron/Dockerfile index 26c46a6f68f..0f023e539ab 100644 --- a/cron/Dockerfile +++ b/cron/Dockerfile @@ -5,7 +5,7 @@ RUN if [ -n "${HTTP_PROXY}" ]; then echo "Acquire::http::proxy \"${HTTP_PROXY}\" RUN if [ -n "${HTTPS_PROXY}" ]; then echo "Acquire::https::proxy \"${HTTPS_PROXY}\";" >> /etc/apt/apt.conf; fi RUN if [ -n "${http_proxy}" ]; then echo "Acquire::http::proxy \"${http_proxy}\";" >> /etc/apt/apt.conf; fi RUN if [ -n "${https_proxy}" ]; then echo "Acquire::https::proxy \"${https_proxy}\";" >> /etc/apt/apt.conf; fi -RUN apt-get update && apt-get install -y --no-install-recommends cron && \ +RUN apt-get update && apt-get install -y --no-install-recommends cron gosu && \ rm -r /var/lib/apt/lists/* COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/install.sh b/install.sh index 9bbe33f10b0..c0ead4943b6 100755 --- a/install.sh +++ b/install.sh @@ -37,6 +37,7 @@ source install/check-memcached-backend.sh source install/ensure-relay-credentials.sh source install/generate-secret-key.sh source install/build-docker-images.sh +source install/ensure-sentry-data-ownership.sh source install/migrate-seaweedfs-kek.sh source install/upgrade-postgres.sh source install/bootstrap-s3-nodestore.sh diff --git a/install/ensure-sentry-data-ownership.sh b/install/ensure-sentry-data-ownership.sh new file mode 100644 index 00000000000..41bc4e1f0d1 --- /dev/null +++ b/install/ensure-sentry-data-ownership.sh @@ -0,0 +1,20 @@ +echo "${_group}Ensuring sentry-data volume ownership ..." + +# Sentry containers run as the non-root `sentry` user (uid 999). Older sentry +# images fixed /data ownership at every container start while running as root; +# do it once here instead. This uses python3 rather than shell tools so it keeps +# working on images without a shell. +$dcr --no-deps --user 0 --entrypoint python3 web -c ' +import os + +SENTRY_UID = 999 + +os.makedirs("/data/files", exist_ok=True) +if any(os.stat(p).st_uid != SENTRY_UID for p in ("/data", "/data/files")): + for root, dirs, files in os.walk("/data"): + for path in (root, *(os.path.join(root, name) for name in dirs + files)): + if os.lstat(path).st_uid != SENTRY_UID: + os.lchown(path, SENTRY_UID, -1) +' + +echo "${_endgroup}" diff --git a/sentry/Dockerfile b/sentry/Dockerfile index 62a490e4de4..7c917097212 100644 --- a/sentry/Dockerfile +++ b/sentry/Dockerfile @@ -1,6 +1,10 @@ ARG SENTRY_IMAGE FROM ${SENTRY_IMAGE} +# Customizations below need root. Newer sentry images default to the non-root +# `sentry` user, so switch explicitly and switch back at the end. +USER 0 + RUN pip install https://github.com/getsentry/sentry-nodestore-s3/archive/main.zip COPY . /usr/src/sentry @@ -13,3 +17,5 @@ RUN if [ -s /usr/src/sentry/requirements.txt ]; then \ echo "sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://develop.sentry.dev/self-hosted/#enhance-sentry-image"; \ pip install -r /usr/src/sentry/requirements.txt; \ fi + +USER sentry diff --git a/sentry/entrypoint.sh b/sentry/entrypoint.sh index 7be738fdd7e..af8f800fab5 100755 --- a/sentry/entrypoint.sh +++ b/sentry/entrypoint.sh @@ -1,8 +1,28 @@ #!/bin/bash set -e -if [ "$(ls -A /usr/local/share/ca-certificates/)" ]; then - update-ca-certificates +# Trust custom CA certificates mounted from ./certificates. Sentry containers run +# as the non-root `sentry` user, so instead of update-ca-certificates (which +# needs root) build a combined bundle in /tmp and point the TLS env vars at it. +# Messages go to stderr so they don't pollute the output of commands like `cat`. +custom_certs=$(find -L /usr/local/share/ca-certificates -type f -name '*.crt' -not -path '*/.generated/*' 2>/dev/null | sort) +if [ -n "$custom_certs" ]; then + system_bundle=/etc/ssl/certs/ca-certificates.crt + bundle=/tmp/sentry-ca-certificates.crt + cat "$system_bundle" >"$bundle" + while IFS= read -r cert; do + cat "$cert" >>"$bundle" + echo >>"$bundle" + done <<<"$custom_certs" + echo "Added $(wc -l <<<"$custom_certs") custom CA certificate(s) to $bundle" >&2 + + # Only redirect variables that are unset or still point at the system bundle, + # so explicit user overrides keep working. + for var in SSL_CERT_FILE REQUESTS_CA_BUNDLE DEFAULT_CA_BUNDLE GRPC_DEFAULT_SSL_ROOTS_FILE_PATH_ENV_VAR; do + if [ -z "${!var:-}" ] || [ "${!var}" = "$system_bundle" ]; then + export "$var=$bundle" + fi + done fi if [ -e /etc/sentry/requirements.txt ]; then From 582885b10aea774ba499d70177d6c339ffe9923b Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 29 Sep 2026 10:59:42 +0200 Subject: [PATCH 2/4] fix(sentry): Keep update-ca-certificates for custom CAs Replace the /tmp CA bundle with making /etc/ssl/certs writable by the sentry user, so the existing entrypoint can run update-ca-certificates unchanged. The bundle approach left the standard bundle path and the hashed certs directory without the custom CAs, and did not apply to docker compose exec, so clients that don't read the TLS env vars (e.g. librdkafka, capath lookups, paths set in sentry.conf.py) would stop trusting them. --- sentry/Dockerfile | 5 +++++ sentry/entrypoint.sh | 24 ++---------------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/sentry/Dockerfile b/sentry/Dockerfile index 7c917097212..f4b56b19200 100644 --- a/sentry/Dockerfile +++ b/sentry/Dockerfile @@ -18,4 +18,9 @@ RUN if [ -s /usr/src/sentry/requirements.txt ]; then \ pip install -r /usr/src/sentry/requirements.txt; \ fi +# Let the non-root sentry user run update-ca-certificates (sentry/entrypoint.sh) +# for custom CAs mounted from ./certificates. It only needs to create symlinks +# and replace the bundle in this directory. +RUN chown sentry:sentry /etc/ssl/certs + USER sentry diff --git a/sentry/entrypoint.sh b/sentry/entrypoint.sh index af8f800fab5..7be738fdd7e 100755 --- a/sentry/entrypoint.sh +++ b/sentry/entrypoint.sh @@ -1,28 +1,8 @@ #!/bin/bash set -e -# Trust custom CA certificates mounted from ./certificates. Sentry containers run -# as the non-root `sentry` user, so instead of update-ca-certificates (which -# needs root) build a combined bundle in /tmp and point the TLS env vars at it. -# Messages go to stderr so they don't pollute the output of commands like `cat`. -custom_certs=$(find -L /usr/local/share/ca-certificates -type f -name '*.crt' -not -path '*/.generated/*' 2>/dev/null | sort) -if [ -n "$custom_certs" ]; then - system_bundle=/etc/ssl/certs/ca-certificates.crt - bundle=/tmp/sentry-ca-certificates.crt - cat "$system_bundle" >"$bundle" - while IFS= read -r cert; do - cat "$cert" >>"$bundle" - echo >>"$bundle" - done <<<"$custom_certs" - echo "Added $(wc -l <<<"$custom_certs") custom CA certificate(s) to $bundle" >&2 - - # Only redirect variables that are unset or still point at the system bundle, - # so explicit user overrides keep working. - for var in SSL_CERT_FILE REQUESTS_CA_BUNDLE DEFAULT_CA_BUNDLE GRPC_DEFAULT_SSL_ROOTS_FILE_PATH_ENV_VAR; do - if [ -z "${!var:-}" ] || [ "${!var}" = "$system_bundle" ]; then - export "$var=$bundle" - fi - done +if [ "$(ls -A /usr/local/share/ca-certificates/)" ]; then + update-ca-certificates fi if [ -e /etc/sentry/requirements.txt ]; then From b309d5bcdfec0fbe707a50411c9df04bfe462dbb Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 29 Sep 2026 11:50:38 +0200 Subject: [PATCH 3/4] fix(install): Chown sentry-data bottom-up so interrupted runs resume Chown /data last, so if the walk is interrupted the ownership guard still triggers a full walk on the next install. --- install/ensure-sentry-data-ownership.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/ensure-sentry-data-ownership.sh b/install/ensure-sentry-data-ownership.sh index 41bc4e1f0d1..c88e37a6f21 100644 --- a/install/ensure-sentry-data-ownership.sh +++ b/install/ensure-sentry-data-ownership.sh @@ -11,8 +11,8 @@ SENTRY_UID = 999 os.makedirs("/data/files", exist_ok=True) if any(os.stat(p).st_uid != SENTRY_UID for p in ("/data", "/data/files")): - for root, dirs, files in os.walk("/data"): - for path in (root, *(os.path.join(root, name) for name in dirs + files)): + for root, dirs, files in os.walk("/data", topdown=False): + for path in (*(os.path.join(root, name) for name in dirs + files), root): if os.lstat(path).st_uid != SENTRY_UID: os.lchown(path, SENTRY_UID, -1) ' From a61a82a00f92003ba75b2c9f24d54db0477ae160 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 29 Sep 2026 12:02:13 +0200 Subject: [PATCH 4/4] fix(install): Fail sentry-data ownership step on unreadable directories os.walk skips directories it cannot list by default, which would leave them root-owned without any error. Raise instead so install.sh stops, like the old entrypoint's find under set -e. --- install/ensure-sentry-data-ownership.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/install/ensure-sentry-data-ownership.sh b/install/ensure-sentry-data-ownership.sh index c88e37a6f21..7c7c6a45bf1 100644 --- a/install/ensure-sentry-data-ownership.sh +++ b/install/ensure-sentry-data-ownership.sh @@ -9,9 +9,14 @@ import os SENTRY_UID = 999 + +def fail(error): + raise error + + os.makedirs("/data/files", exist_ok=True) if any(os.stat(p).st_uid != SENTRY_UID for p in ("/data", "/data/files")): - for root, dirs, files in os.walk("/data", topdown=False): + for root, dirs, files in os.walk("/data", topdown=False, onerror=fail): for path in (*(os.path.join(root, name) for name in dirs + files), root): if os.lstat(path).st_uid != SENTRY_UID: os.lchown(path, SENTRY_UID, -1)