fix(immich): URI-encode DB credentials for psql connection strings (#2980)

The addon builds every psql connection as a postgres:// URI with the raw
username and password interpolated in. libpq percent-decodes the userinfo
part of a URI, so a password containing '%' (or '@', '/', '?', '#') is
decoded into different bytes before it reaches the server, and every
connection fails with "password authentication failed for user".

Encode the credentials with jq's @uri once and use the encoded copies in
the URIs only; the raw password is still what gets handed to Immich via
export_db_env and what is written by CREATE/ALTER USER. Those SQL
statements now double single quotes so a password containing a single
quote no longer breaks the statement either.

This is the same approach already used by the postgres_15 and postgres_17
addons in this repo.

Closes #1614

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Alexandre
2026-08-19 10:29:50 +02:00
committed by GitHub
parent 48ac78c59d
commit 2fc1ea84be
3 changed files with 25 additions and 12 deletions

View File

@@ -1,4 +1,7 @@
## 3.1.0.1 (2026-08-17)
- Fix `password authentication failed for user` when `DB_PASSWORD` contains special characters. Passwords are now URI-encoded before being used in the psql connection string, and SQL-escaped before being used in `CREATE`/`ALTER USER` statements
## 3.1.0 (2026-08-01)
- Update to latest version from immich-app/immich (changelog : https://github.com/immich-app/immich/releases)

View File

@@ -141,6 +141,6 @@ slug: immich
udev: true
url: https://github.com/alexbelgium/hassio-addons
usb: true
version: "3.1.0"
version: "3.1.0.1"
video: true
webui: http://[HOST]:[PORT:8080]

View File

@@ -98,10 +98,11 @@ setup_root_user() {
fi
# Check if the root user exists.
if ! psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" -tAc "SELECT 1 FROM pg_roles WHERE rolname='root'" | grep -q 1; then
if ! psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" -tAc "SELECT 1 FROM pg_roles WHERE rolname='root'" | grep -q 1; then
bashio::log.info "Root user does not exist. Creating root user with DB_ROOT_PASSWORD..."
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
CREATE ROLE root WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '${DB_ROOT_PASSWORD}';
local root_password_sql="${DB_ROOT_PASSWORD//\'/\'\'}"
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
CREATE ROLE root WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '${root_password_sql}';
EOF
else
bashio::log.info "Root user exists with a non-default password. No migration needed."
@@ -113,10 +114,10 @@ setup_database() {
bashio::log.info "Setting up external PostgreSQL database..."
# Create the database if it does not exist
if ! psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/postgres" -tAc \
if ! psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}/postgres" -tAc \
"SELECT 1 FROM pg_database WHERE datname='${DB_DATABASE_NAME}';" | grep -q 1; then
bashio::log.info "Database does not exist. Creating it now..."
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
CREATE DATABASE ${DB_DATABASE_NAME};
EOF
else
@@ -124,20 +125,21 @@ EOF
fi
# Ensure the user exists and update its password
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
local db_password_sql="${DB_PASSWORD//\'/\'\'}"
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USERNAME}') THEN
CREATE USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${DB_PASSWORD}';
CREATE USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${db_password_sql}';
ELSE
ALTER USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${DB_PASSWORD}';
ALTER USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${db_password_sql}';
END IF;
END
\$\$;
EOF
# Ensure the user has full privileges on the database
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
GRANT ALL PRIVILEGES ON DATABASE ${DB_DATABASE_NAME} TO ${DB_USERNAME};
EOF
@@ -147,7 +149,7 @@ EOF
# Function to check if the vectors (pgvecto.rs) extension is available on the server
check_vector_extension() {
echo "Checking if 'vectors' extension is available for database '${DB_DATABASE_NAME}'..."
RESULT=$(psql "postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vectors';")
RESULT=$(psql "postgres://$DB_USERNAME_URI:$DB_PASSWORD_URI@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vectors';")
if [[ "$RESULT" == "1" ]]; then
echo "✅ 'vectors' extension is available."
return 0
@@ -163,7 +165,7 @@ check_vector_extension() {
# itself on first startup; checking pg_extension would false-warn on every fresh install.
check_vchord_extension() {
echo "Checking if 'vchord' extension is available for database '${DB_DATABASE_NAME}'..."
RESULT=$(psql "postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vchord';")
RESULT=$(psql "postgres://$DB_USERNAME_URI:$DB_PASSWORD_URI@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vchord';")
if [[ "$RESULT" == "1" ]]; then
echo "✅ 'vchord' extension is available."
return 0
@@ -187,6 +189,14 @@ export DB_PORT="$(bashio::config 'DB_PORT')"
export JWT_SECRET="$(bashio::config 'JWT_SECRET')"
export DB_HOSTNAME="$(bashio::config 'DB_HOSTNAME')"
# libpq percent-decodes the userinfo part of a postgres:// URI, so credentials
# containing reserved characters (% @ / : ? #) are misread and every psql call
# below fails with "password authentication failed". Encode them once here and
# use the encoded copies in URIs only - the app itself still gets the raw value
# through export_db_env. Same approach as the postgres_15/postgres_17 addons.
export DB_USERNAME_URI="$(jq -rn --arg x "$DB_USERNAME" '$x|@uri')"
export DB_PASSWORD_URI="$(jq -rn --arg x "$DB_PASSWORD" '$x|@uri')"
if bashio::config.true 'VIPS_NOVECTOR'; then
export VIPS_NOVECTOR="1"
fi