fix: customize container user permissions using PUID and PGID. #9657 (#9833)

Add support for custom container user permissions via PUID and PGID
environment variables. When the container is started as root
(--user root), the pgadmin user is reassigned to the requested UID/GID
and all initialization runs under that user via su-exec, ensuring
files are created with correct ownership from the start.

Key changes:
- Dockerfile: add su-exec package, add chmod g=u for /run/pgadmin
  (fixes OpenShift random UID access)
- entrypoint.sh: add PUID/PGID validation and privilege dropping
  before initialization (not after), preserving OpenShift compatibility

Three modes supported:
- Default (USER 5050): unchanged behavior
- Custom UID (--user root -e PUID=N -e PGID=N): drops to target user
  before any init
- OpenShift (random UID, GID 0): passwd fixup + group permissions
dependabot/npm_and_yarn/runtime/globals-17.5.0
Ashesh Vashi 2026-04-13 14:34:18 +05:30 committed by GitHub
parent a3a0537277
commit 4ddb16f47a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 104 additions and 24 deletions

View File

@ -165,7 +165,8 @@ RUN apk update && apk upgrade && \
tzdata \
libedit \
libldap \
libcap && \
libcap \
su-exec && \
rm -rf /var/cache/apk/*
# Copy in the Python packages
@ -197,7 +198,7 @@ RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \
useradd -r -u 5050 -g root -s /sbin/nologin pgadmin && \
mkdir -p /run/pgadmin /var/lib/pgadmin && \
chown pgadmin:root /run/pgadmin /var/lib/pgadmin && \
chmod g=u /var/lib/pgadmin && \
chmod g=u /run/pgadmin /var/lib/pgadmin && \
touch /pgadmin4/config_distro.py && \
chown pgadmin:root /pgadmin4/config_distro.py && \
chmod g=u /pgadmin4/config_distro.py && \

View File

@ -1,12 +1,78 @@
#!/usr/bin/env bash
# Fixup the passwd file, in case we're on OpenShift
if ! whoami > /dev/null 2>&1; then
if [ "$(id -u)" -ne 5050 ]; then
if [ -w /etc/passwd ]; then
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
########################################################################
# PUID/PGID support
#
# When the container runs as root (e.g. --user root), the pgadmin user
# is reassigned to the requested UID/GID and all initialization +
# gunicorn run via su-exec as that user.
#
# When the container runs as non-root (default USER 5050, or OpenShift
# random UID), PUID/PGID are ignored and everything runs as the
# current user.
########################################################################
PUID=${PUID:-5050}
PGID=${PGID:-0}
# Validate PUID/PGID are numeric and in acceptable range
if ! echo "$PUID" | grep -qE '^[0-9]+$'; then
echo "ERROR: PUID must be a numeric value, got '$PUID'"
exit 1
fi
if ! echo "$PGID" | grep -qE '^[0-9]+$'; then
echo "ERROR: PGID must be a numeric value, got '$PGID'"
exit 1
fi
if [ "$PUID" -eq 0 ]; then
echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID."
exit 1
fi
if [ "$(id -u)" = "0" ]; then
# Ensure a group with the target GID exists
if ! getent group "$PGID" > /dev/null 2>&1; then
if ! addgroup -g "$PGID" pggroup; then
echo "ERROR: Failed to create group with GID=$PGID"
exit 1
fi
fi
# Reassign the pgadmin user to the desired UID/GID
if ! usermod -o -u "$PUID" -g "$PGID" pgadmin; then
echo "ERROR: Failed to set pgadmin user to UID=$PUID GID=$PGID"
exit 1
fi
# Fix ownership of runtime directories BEFORE any initialization
for dir in /run/pgadmin /var/lib/pgadmin; do
if [ -d "$dir" ]; then
chown -R "$PUID:$PGID" "$dir"
fi
done
# Fix ownership of individual files (no -R needed)
if [ -e /pgadmin4/config_distro.py ]; then
chown "$PUID:$PGID" /pgadmin4/config_distro.py
fi
if [ -d /certs ]; then
chown -R "$PUID:$PGID" /certs
fi
SU_EXEC="su-exec $PUID:$PGID"
echo "pgAdmin will run as UID=$PUID, GID=$PGID"
else
SU_EXEC=""
# Fixup the passwd file, in case we're on OpenShift
if ! whoami > /dev/null 2>&1; then
if [ "$(id -u)" -ne 5050 ]; then
if [ -w /etc/passwd ]; then
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
fi
fi
fi
fi
fi
# usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example'
@ -74,12 +140,17 @@ EOF
esac
echo "${var#PGADMIN_CONFIG_} = $val" >> "${CONFIG_DISTRO_FILE_PATH}"
done
# If running as root with custom config distro path, fix ownership
if [ "$(id -u)" = "0" ] && [ "${CONFIG_DISTRO_FILE_PATH}" != "/pgadmin4/config_distro.py" ]; then
chown "$PUID:$PGID" "${CONFIG_DISTRO_FILE_PATH}"
fi
fi
# Check whether the external configuration database exists if it is being used.
external_config_db_exists="False"
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI}" ]; then
external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db("${PGADMIN_CONFIG_CONFIG_DATABASE_URI}"); print(val)")
external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db(\"${PGADMIN_CONFIG_CONFIG_DATABASE_URI}\"); print(val)")
fi
# DRY of the code to load the PGADMIN_SERVER_JSON_FILE
@ -96,9 +167,9 @@ function load_server_json_file() {
# When running in Desktop mode, no user is created
# so we have to import servers anonymously
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
else
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
fi
fi
}
@ -124,7 +195,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
fi
email_config="{'CHECK_EMAIL_DELIVERABILITY': ${CHECK_EMAIL_DELIVERABILITY}, 'ALLOW_SPECIAL_EMAIL_DOMAINS': ${ALLOW_SPECIAL_EMAIL_DOMAINS}, 'GLOBALLY_DELIVERABLE': ${GLOBALLY_DELIVERABLE}}"
echo "email config is ${email_config}"
is_valid_email=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
if echo "${is_valid_email}" | grep "False" > /dev/null; then
echo "'${PGADMIN_DEFAULT_EMAIL}' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
echo "Validation output: ${is_valid_email}"
@ -140,7 +211,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
# Initialize DB before starting Gunicorn
# Importing pgadmin4 (from this script) is enough
/venv/bin/python3 run_pgadmin.py
$SU_EXEC /venv/bin/python3 run_pgadmin.py
export PGADMIN_PREFERENCES_JSON_FILE="${PGADMIN_PREFERENCES_JSON_FILE:-/pgadmin4/preferences.json}"
@ -150,22 +221,30 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
# Pre-load any required preferences
if [ -f "${PGADMIN_PREFERENCES_JSON_FILE}" ]; then
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
DESKTOP_USER=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
else
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
fi
fi
# Copy the pgpass file passed using secrets
if [ -f "${PGPASS_FILE}" ]; then
if [ -n "${PGPASS_FILE}" ] && [ -f "${PGPASS_FILE}" ]; then
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
cp ${PGPASS_FILE} /var/lib/pgadmin/.pgpass
cp "${PGPASS_FILE}" /var/lib/pgadmin/.pgpass
chmod 600 /var/lib/pgadmin/.pgpass
# Fix ownership when running as root
if [ "$(id -u)" = "0" ]; then
chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass
fi
else
PGADMIN_USER_CONFIG_DIR=$(echo "${PGADMIN_DEFAULT_EMAIL}" | sed 's/@/_/g')
mkdir -p /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}
cp ${PGPASS_FILE} /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
chmod 600 /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
mkdir -p "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
cp "${PGPASS_FILE}" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
chmod 600 "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
# Fix ownership when running as root
if [ "$(id -u)" = "0" ]; then
chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
fi
fi
fi
# If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file.
@ -180,7 +259,7 @@ fi
# Get the session timeout from the pgAdmin config. We'll use this (in seconds)
# to define the Gunicorn worker timeout
TIMEOUT=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
# Using --threads to have multi-threaded single-process worker
@ -196,7 +275,7 @@ else
fi
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
else
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
fi