From 2e3feca05f2c98b5c468f7eade85569fba5d20aa Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Thu, 18 Sep 2025 08:18:51 +0100 Subject: [PATCH 1/5] security: run Docker container as non-root user Running containers as a non-root user is a long standing security practice. The changes ensure that the sourcebot user is created and has the correct level of permissions to run all its dependencies (postgres, redis and node). Please note that as a side effect, existing mounted volumes would need to have their ownership reviewed or it may not be able to access the files. This is specially the case for previous versions that would create said files as 0:0. To fix that, users can run chown -R 1500:1500 /path/.sourcebot. The chmod may also need to be a bit more strict in such cases, so changing that is advised: chown -R 0750 /path/.sourcebot. Signed-off-by: Paulo Gomes --- Dockerfile | 14 ++++++++++++++ entrypoint.sh | 31 ++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae90c8c7..76eadbe0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -225,12 +225,26 @@ RUN mkdir -p /run/postgresql && \ chown -R postgres:postgres /run/postgresql && \ chmod 775 /run/postgresql +# To run as non-root, the user must be part of postgres, redis and node groups +RUN addgroup -g 1500 sourcebot && \ + adduser -D -u 1500 -h /app -S sourcebot && \ + adduser sourcebot postgres && \ + adduser sourcebot redis && \ + adduser sourcebot node && \ + chown -R sourcebot /data && \ + chown -R sourcebot /app && \ + mkdir /var/log/sourcebot && \ + chown sourcebot /var/log/sourcebot + COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY prefix-output.sh ./prefix-output.sh RUN chmod +x ./prefix-output.sh COPY entrypoint.sh ./entrypoint.sh RUN chmod +x ./entrypoint.sh + +USER sourcebot + EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" diff --git a/entrypoint.sh b/entrypoint.sh index cf90a377..14fdb908 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -77,19 +77,20 @@ fi # Check if DATA_CACHE_DIR exists, if not create it if [ ! -d "$DATA_CACHE_DIR" ]; then - mkdir -p "$DATA_CACHE_DIR" + mkdir -m 0750 -p "$DATA_CACHE_DIR" fi # Check if DATABASE_DATA_DIR exists, if not initialize it if [ "$DATABASE_EMBEDDED" = "true" ] && [ ! -d "$DATABASE_DATA_DIR" ]; then - echo -e "\e[34m[Info] Initializing database at $DATABASE_DATA_DIR...\e[0m" - mkdir -p $DATABASE_DATA_DIR && chown -R postgres:postgres "$DATABASE_DATA_DIR" - su postgres -c "initdb -D $DATABASE_DATA_DIR" + echo -e "\e[34m[Info] Initializing database at $DATABASE_D\ATA_DIR...\e[0m" + mkdir -m 0750 -p $DATABASE_DATA_DIR + + initdb -D "$DATABASE_DATA_DIR" fi # Create the redis data directory if it doesn't exist if [ "$REDIS_EMBEDDED" = "true" ] && [ ! -d "$REDIS_DATA_DIR" ]; then - mkdir -p $REDIS_DATA_DIR + mkdir -m 0750 -p $REDIS_DATA_DIR fi if [ -z "$SOURCEBOT_ENCRYPTION_KEY" ]; then @@ -180,13 +181,25 @@ echo "{\"version\": \"$NEXT_PUBLIC_SOURCEBOT_VERSION\", \"install_id\": \"$SOURC # Start the database and wait for it to be ready before starting any other service if [ "$DATABASE_EMBEDDED" = "true" ]; then - su postgres -c "postgres -D $DATABASE_DATA_DIR" & - until pg_isready -h localhost -p 5432 -U postgres; do + postgres -D "$DATABASE_DATA_DIR" & + until pg_isready -h localhost -p 5432 -d sourcebot -U postgres; do echo -e "\e[34m[Info] Waiting for the database to be ready...\e[0m" sleep 1 + + # As postgres runs in the background, we must check if it is still + # running, otherwise the "until" loop will be running indefinitely. + if ! pgrep -x "postgres" > /dev/null; then + echo "postgres failed to run" + exit 1 + break + fi done - # Check if the database already exists, and create it if it dne + # Running as non-root we need to ensure the postgres account is created. + psql -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='postgres'" | grep -q 1 \ + || createuser postgres -s + + # Check if the database already exists, and create it if it doesn't EXISTING_DB=$(psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname = 'sourcebot'") if [ "$EXISTING_DB" = "1" ]; then @@ -205,4 +218,4 @@ DATABASE_URL="$DATABASE_URL" yarn workspace @sourcebot/db prisma:migrate:prod mkdir -p /var/log/sourcebot # Run supervisord -exec supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf From 60ed95e2baa032b2408f923ba7f9c016d9671271 Mon Sep 17 00:00:00 2001 From: bkellam Date: Mon, 3 Nov 2025 14:02:38 -0800 Subject: [PATCH 2/5] wip --- Dockerfile | 63 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/Dockerfile b/Dockerfile index 76eadbe0..80514f1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -182,14 +182,30 @@ ENV SOURCEBOT_LOG_LEVEL=info # Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable. # ENV SOURCEBOT_TELEMETRY_DISABLED=1 -COPY package.json yarn.lock* .yarnrc.yml public.pem ./ -COPY .yarn ./.yarn +# Configure dependencies +RUN apk add --no-cache git ca-certificates bind-tools tini jansson wget supervisor uuidgen curl perl jq redis postgresql postgresql-contrib openssl util-linux unzip + +ARG UID=1500 +ARG GID=1500 + +# To run as non-root, the user must be part of postgres, redis and node groups +RUN addgroup -g $GID sourcebot && \ + adduser -D -u $UID -h /app -S sourcebot && \ + adduser sourcebot postgres && \ + adduser sourcebot redis && \ + adduser sourcebot node && \ + chown -R sourcebot /app && \ + mkdir /var/log/sourcebot && \ + chown sourcebot /var/log/sourcebot + +COPY --chown=sourcebot:sourcebot package.json yarn.lock* .yarnrc.yml public.pem ./ +COPY --chown=sourcebot:sourcebot .yarn ./.yarn # Configure zoekt -COPY vendor/zoekt/install-ctags-alpine.sh . +COPY --chown=sourcebot:sourcebot vendor/zoekt/install-ctags-alpine.sh . RUN ./install-ctags-alpine.sh && rm install-ctags-alpine.sh -RUN mkdir -p ${DATA_CACHE_DIR} -COPY --from=zoekt-builder \ +RUN mkdir -p ${DATA_CACHE_DIR} && chown -R sourcebot ${DATA_CACHE_DIR} +COPY --chown=sourcebot:sourcebot --from=zoekt-builder \ /cmd/zoekt-git-index \ /cmd/zoekt-indexserver \ /cmd/zoekt-mirror-github \ @@ -202,20 +218,18 @@ COPY --from=zoekt-builder \ /usr/local/bin/ # Copy all of the things -COPY --from=web-builder /app/packages/web/public ./packages/web/public -COPY --from=web-builder /app/packages/web/.next/standalone ./ -COPY --from=web-builder /app/packages/web/.next/static ./packages/web/.next/static +COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/public ./packages/web/public +COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/.next/standalone ./ +COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/.next/static ./packages/web/.next/static -COPY --from=backend-builder /app/node_modules ./node_modules -COPY --from=backend-builder /app/packages/backend ./packages/backend +COPY --chown=sourcebot:sourcebot --from=backend-builder /app/node_modules ./node_modules +COPY --chown=sourcebot:sourcebot --from=backend-builder /app/packages/backend ./packages/backend -COPY --from=shared-libs-builder /app/node_modules ./node_modules -COPY --from=shared-libs-builder /app/packages/db ./packages/db -COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas -COPY --from=shared-libs-builder /app/packages/shared ./packages/shared +COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/node_modules ./node_modules +COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/db ./packages/db +COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/schemas ./packages/schemas +COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/shared ./packages/shared -# Configure dependencies -RUN apk add --no-cache git ca-certificates bind-tools tini jansson wget supervisor uuidgen curl perl jq redis postgresql postgresql-contrib openssl util-linux unzip # Fixes git "dubious ownership" issues when the volume is mounted with different permissions to the container. RUN git config --global safe.directory "*" @@ -225,21 +239,10 @@ RUN mkdir -p /run/postgresql && \ chown -R postgres:postgres /run/postgresql && \ chmod 775 /run/postgresql -# To run as non-root, the user must be part of postgres, redis and node groups -RUN addgroup -g 1500 sourcebot && \ - adduser -D -u 1500 -h /app -S sourcebot && \ - adduser sourcebot postgres && \ - adduser sourcebot redis && \ - adduser sourcebot node && \ - chown -R sourcebot /data && \ - chown -R sourcebot /app && \ - mkdir /var/log/sourcebot && \ - chown sourcebot /var/log/sourcebot - -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY prefix-output.sh ./prefix-output.sh +COPY --chown=sourcebot:sourcebot supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY --chown=sourcebot:sourcebot prefix-output.sh ./prefix-output.sh RUN chmod +x ./prefix-output.sh -COPY entrypoint.sh ./entrypoint.sh +COPY --chown=sourcebot:sourcebot entrypoint.sh ./entrypoint.sh RUN chmod +x ./entrypoint.sh From 4957de3b799026c1777178f1c5393cbd7e3a2043 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 5 Nov 2025 16:10:40 -0800 Subject: [PATCH 3/5] make non-root optional and configurable via the --user param --- Dockerfile | 46 +++++++++++++++++++++++---------------------- entrypoint.sh | 52 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index 80514f1a..ce01bd4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -188,24 +188,24 @@ RUN apk add --no-cache git ca-certificates bind-tools tini jansson wget supervis ARG UID=1500 ARG GID=1500 -# To run as non-root, the user must be part of postgres, redis and node groups +# Always create the non-root user to support runtime user switching +# The container can be run as root (default) or as sourcebot user using docker run --user RUN addgroup -g $GID sourcebot && \ adduser -D -u $UID -h /app -S sourcebot && \ adduser sourcebot postgres && \ adduser sourcebot redis && \ adduser sourcebot node && \ - chown -R sourcebot /app && \ mkdir /var/log/sourcebot && \ chown sourcebot /var/log/sourcebot -COPY --chown=sourcebot:sourcebot package.json yarn.lock* .yarnrc.yml public.pem ./ -COPY --chown=sourcebot:sourcebot .yarn ./.yarn +COPY package.json yarn.lock* .yarnrc.yml public.pem ./ +COPY .yarn ./.yarn # Configure zoekt -COPY --chown=sourcebot:sourcebot vendor/zoekt/install-ctags-alpine.sh . +COPY vendor/zoekt/install-ctags-alpine.sh . RUN ./install-ctags-alpine.sh && rm install-ctags-alpine.sh -RUN mkdir -p ${DATA_CACHE_DIR} && chown -R sourcebot ${DATA_CACHE_DIR} -COPY --chown=sourcebot:sourcebot --from=zoekt-builder \ +RUN mkdir -p ${DATA_CACHE_DIR} +COPY --from=zoekt-builder \ /cmd/zoekt-git-index \ /cmd/zoekt-indexserver \ /cmd/zoekt-mirror-github \ @@ -218,18 +218,17 @@ COPY --chown=sourcebot:sourcebot --from=zoekt-builder \ /usr/local/bin/ # Copy all of the things -COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/public ./packages/web/public -COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/.next/standalone ./ -COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/.next/static ./packages/web/.next/static - -COPY --chown=sourcebot:sourcebot --from=backend-builder /app/node_modules ./node_modules -COPY --chown=sourcebot:sourcebot --from=backend-builder /app/packages/backend ./packages/backend +COPY --from=web-builder /app/packages/web/public ./packages/web/public +COPY --from=web-builder /app/packages/web/.next/standalone ./ +COPY --from=web-builder /app/packages/web/.next/static ./packages/web/.next/static -COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/node_modules ./node_modules -COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/db ./packages/db -COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/schemas ./packages/schemas -COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/shared ./packages/shared +COPY --from=backend-builder /app/node_modules ./node_modules +COPY --from=backend-builder /app/packages/backend ./packages/backend +COPY --from=shared-libs-builder /app/node_modules ./node_modules +COPY --from=shared-libs-builder /app/packages/db ./packages/db +COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas +COPY --from=shared-libs-builder /app/packages/shared ./packages/shared # Fixes git "dubious ownership" issues when the volume is mounted with different permissions to the container. RUN git config --global safe.directory "*" @@ -239,14 +238,17 @@ RUN mkdir -p /run/postgresql && \ chown -R postgres:postgres /run/postgresql && \ chmod 775 /run/postgresql -COPY --chown=sourcebot:sourcebot supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY --chown=sourcebot:sourcebot prefix-output.sh ./prefix-output.sh +# Make app directory accessible to both root and sourcebot user +RUN chown -R sourcebot:sourcebot /app + +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY prefix-output.sh ./prefix-output.sh RUN chmod +x ./prefix-output.sh -COPY --chown=sourcebot:sourcebot entrypoint.sh ./entrypoint.sh +COPY entrypoint.sh ./entrypoint.sh RUN chmod +x ./entrypoint.sh - -USER sourcebot +# Default to root user, but can be overridden at runtime with --user flag +# No USER directive = runs as root by default EXPOSE 3000 ENV PORT=3000 diff --git a/entrypoint.sh b/entrypoint.sh index 14fdb908..0f8ec7ed 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,6 +5,18 @@ set -e # Disable auto-exporting of variables set +a +# Detect if running as root +IS_ROOT=false +if [ "$(id -u)" -eq 0 ]; then + IS_ROOT=true +fi + +if [ "$IS_ROOT" = "true" ]; then + echo -e "\e[34m[Info] Running as root user.\e[0m" +else + echo -e "\e[34m[Info] Running as non-root user.\e[0m" +fi + # If a CONFIG_PATH is set, resolve the environment overrides from the config file. # The overrides will be written into variables scopped to the current shell. This is # required in case one of the variables used in this entrypoint is overriden (e.g., @@ -82,10 +94,14 @@ fi # Check if DATABASE_DATA_DIR exists, if not initialize it if [ "$DATABASE_EMBEDDED" = "true" ] && [ ! -d "$DATABASE_DATA_DIR" ]; then - echo -e "\e[34m[Info] Initializing database at $DATABASE_D\ATA_DIR...\e[0m" + echo -e "\e[34m[Info] Initializing database at $DATABASE_DATA_DIR...\e[0m" mkdir -m 0750 -p $DATABASE_DATA_DIR - - initdb -D "$DATABASE_DATA_DIR" + if [ "$IS_ROOT" = "true" ]; then + chown -R postgres:postgres "$DATABASE_DATA_DIR" + su postgres -c "initdb -D $DATABASE_DATA_DIR" + else + initdb -D "$DATABASE_DATA_DIR" + fi fi # Create the redis data directory if it doesn't exist @@ -181,8 +197,13 @@ echo "{\"version\": \"$NEXT_PUBLIC_SOURCEBOT_VERSION\", \"install_id\": \"$SOURC # Start the database and wait for it to be ready before starting any other service if [ "$DATABASE_EMBEDDED" = "true" ]; then - postgres -D "$DATABASE_DATA_DIR" & - until pg_isready -h localhost -p 5432 -d sourcebot -U postgres; do + if [ "$IS_ROOT" = "true" ]; then + su postgres -c "postgres -D $DATABASE_DATA_DIR" & + else + postgres -D "$DATABASE_DATA_DIR" & + fi + + until pg_isready -h localhost -p 5432 -U postgres; do echo -e "\e[34m[Info] Waiting for the database to be ready...\e[0m" sleep 1 @@ -191,13 +212,14 @@ if [ "$DATABASE_EMBEDDED" = "true" ]; then if ! pgrep -x "postgres" > /dev/null; then echo "postgres failed to run" exit 1 - break - fi + fi done - - # Running as non-root we need to ensure the postgres account is created. - psql -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='postgres'" | grep -q 1 \ - || createuser postgres -s + + if [ "$IS_ROOT" = "false" ]; then + # Running as non-root we need to ensure the postgres account is created. + psql -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='postgres'" | grep -q 1 \ + || createuser postgres -s + fi # Check if the database already exists, and create it if it doesn't EXISTING_DB=$(psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname = 'sourcebot'") @@ -214,8 +236,10 @@ fi echo -e "\e[34m[Info] Running database migration...\e[0m" DATABASE_URL="$DATABASE_URL" yarn workspace @sourcebot/db prisma:migrate:prod -# Create the log directory -mkdir -p /var/log/sourcebot +# Create the log directory if it doesn't exist +if [ ! -d "/var/log/sourcebot" ]; then + mkdir -m 0750 -p /var/log/sourcebot +fi # Run supervisord -exec supervisord -c /etc/supervisor/conf.d/supervisord.conf +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file From 0ca1b7e62e14bdbe58877c6cc50b7b9bd81076a4 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 5 Nov 2025 16:29:45 -0800 Subject: [PATCH 4/5] nits --- Dockerfile | 5 +++-- entrypoint.sh | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index ce01bd4d..ddfb2925 100644 --- a/Dockerfile +++ b/Dockerfile @@ -247,8 +247,9 @@ RUN chmod +x ./prefix-output.sh COPY entrypoint.sh ./entrypoint.sh RUN chmod +x ./entrypoint.sh -# Default to root user, but can be overridden at runtime with --user flag -# No USER directive = runs as root by default +# Note: for back-compat cases, we do _not_ set the USER directive here. +# Instead, the user can be overridden at runtime with --user flag. +# USER sourcebot EXPOSE 3000 ENV PORT=3000 diff --git a/entrypoint.sh b/entrypoint.sh index 0f8ec7ed..ce9461f6 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -89,13 +89,13 @@ fi # Check if DATA_CACHE_DIR exists, if not create it if [ ! -d "$DATA_CACHE_DIR" ]; then - mkdir -m 0750 -p "$DATA_CACHE_DIR" + mkdir -p "$DATA_CACHE_DIR" fi # Check if DATABASE_DATA_DIR exists, if not initialize it if [ "$DATABASE_EMBEDDED" = "true" ] && [ ! -d "$DATABASE_DATA_DIR" ]; then echo -e "\e[34m[Info] Initializing database at $DATABASE_DATA_DIR...\e[0m" - mkdir -m 0750 -p $DATABASE_DATA_DIR + mkdir -p $DATABASE_DATA_DIR if [ "$IS_ROOT" = "true" ]; then chown -R postgres:postgres "$DATABASE_DATA_DIR" su postgres -c "initdb -D $DATABASE_DATA_DIR" @@ -106,7 +106,7 @@ fi # Create the redis data directory if it doesn't exist if [ "$REDIS_EMBEDDED" = "true" ] && [ ! -d "$REDIS_DATA_DIR" ]; then - mkdir -m 0750 -p $REDIS_DATA_DIR + mkdir -p $REDIS_DATA_DIR fi if [ -z "$SOURCEBOT_ENCRYPTION_KEY" ]; then @@ -221,7 +221,7 @@ if [ "$DATABASE_EMBEDDED" = "true" ]; then || createuser postgres -s fi - # Check if the database already exists, and create it if it doesn't + # Check if the database already exists, and create it if it doesn't exist EXISTING_DB=$(psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname = 'sourcebot'") if [ "$EXISTING_DB" = "1" ]; then @@ -237,9 +237,7 @@ echo -e "\e[34m[Info] Running database migration...\e[0m" DATABASE_URL="$DATABASE_URL" yarn workspace @sourcebot/db prisma:migrate:prod # Create the log directory if it doesn't exist -if [ ! -d "/var/log/sourcebot" ]; then - mkdir -m 0750 -p /var/log/sourcebot -fi +mkdir -p /var/log/sourcebot # Run supervisord exec supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file From 5cb1295b9016997cc095cf2fe1555a5506343d33 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 5 Nov 2025 22:20:13 -0800 Subject: [PATCH 5/5] feedback --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index ce9461f6..22a733e3 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -100,7 +100,7 @@ if [ "$DATABASE_EMBEDDED" = "true" ] && [ ! -d "$DATABASE_DATA_DIR" ]; then chown -R postgres:postgres "$DATABASE_DATA_DIR" su postgres -c "initdb -D $DATABASE_DATA_DIR" else - initdb -D "$DATABASE_DATA_DIR" + initdb -D "$DATABASE_DATA_DIR" -U postgres fi fi