Fix: second double-interpolation bug in envparse.py - add export_raw/export_raw_merged (no escape_dollar) for the docker-compose-config render path, which already does its own dollar-escaping. Fixes IMMICH_KIOSK_BASICAUTH bcrypt hash corruption.
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
2026-08-26 23:25:46 -07:00
parent a557d9fb07
commit 4af1209565
+50 -4
View File
@@ -57,15 +57,52 @@ def merge_envs(base_path, override_path):
# one level of escaping ('$$' -> literal '$'), landing on the correct # one level of escaping ('$$' -> literal '$'), landing on the correct
# original single '$' with no leftover variable-reference lookalike. # original single '$' with no leftover variable-reference lookalike.
# #
# Only applied in export/export_merged (which feed `eval` to set the # ONLY applies to the single-file path (no `docker compose config` step
# actual values envsubst reads) — NOT in vars/vars_merged, which just # downstream). See export_raw/export_raw_merged below for why the
# build envsubst's space-separated $VARNAME allowlist string and have # folder+extras path must NOT use this.
# nothing to do with actual values.
# ────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
def escape_dollar(v): def escape_dollar(v):
return v.replace('$', '$$') return v.replace('$', '$$')
# ──────────────────────────────────────────────────────────────────────────
# 2026-08-26 FIX #2 — SECOND double-interpolation bug, folder+extras path
# (discovered fixing IMMICH_KIOSK_BASICAUTH, a bcrypt hash full of '$'):
#
# The folder+extras render path is:
# docker compose <files> config | strip | envsubst "$VARS"
# -> docker stack deploy -c -
#
# `docker compose config` performs its OWN ${VAR} interpolation AND its
# own re-escaping of the output: any literal '$' character that ends up
# in the rendered YAML — escaped or not — gets doubled to '$$' by
# `docker compose config` itself, unconditionally, as part of producing
# spec-safe output. Confirmed by isolated test:
# raw MYVAR='a$b$c' -> docker compose config -> "a$$b$$c" (correct,
# one level added)
# escaped MYVAR='a$$b$$c' (i.e. pre-doubled by escape_dollar) ->
# docker compose config -> "a$$$$b$$$$c" (WRONG, doubled twice)
#
# `docker stack deploy -c -` still only removes exactly ONE level of
# escaping on its way in (confirmed: "a$$b$$c" -> container label
# "a$b$c", correct). So across the whole folder+extras pipeline there is
# exactly ONE implicit escaping step (`docker compose config`) and ONE
# implicit un-escaping step (`docker stack deploy`) already built in —
# pre-escaping the exported value on top of that leaves one extra,
# uncollapsed level of '$$' in the final container label/env value.
#
# Confirmed impact (2026-08-26): IMMICH_KIOSK_BASICAUTH
# ("BabyBryan:$2y$05$...") rendered as "BabyBryan:$$2y$$05$$..." in the
# final container label — Traefik basic auth would never match the real
# password hash, silently locking out the kiosk with no error.
#
# Fix: use export_raw / export_raw_merged (NO escape_dollar) whenever the
# render path goes through `docker compose config` — i.e. any stack with
# extension files. Use export / export_merged (WITH escape_dollar) only
# for the single-file path, which has no `docker compose config` step and
# therefore only Swarm's own interpolation pass to protect against.
# ──────────────────────────────────────────────────────────────────────────
# ────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
# 2026-08-26 FIX — depends_on long-form vs Swarm short-form: # 2026-08-26 FIX — depends_on long-form vs Swarm short-form:
# #
@@ -201,6 +238,15 @@ elif mode == 'export_merged':
# export_merged <global.env> <stack.env> # export_merged <global.env> <stack.env>
for k, v in merge_envs(sys.argv[2], sys.argv[3]): for k, v in merge_envs(sys.argv[2], sys.argv[3]):
print('export {}={}'.format(k, repr(escape_dollar(v)))) print('export {}={}'.format(k, repr(escape_dollar(v))))
elif mode == 'export_raw':
# export_raw <env-file> — NO escape_dollar. Use for the docker-compose-
# config render path (folder+extras), which does its own '$' escaping.
for k, v in parse_env(sys.argv[2]):
print('export {}={}'.format(k, repr(v)))
elif mode == 'export_raw_merged':
# export_raw_merged <global.env> <stack.env> — NO escape_dollar.
for k, v in merge_envs(sys.argv[2], sys.argv[3]):
print('export {}={}'.format(k, repr(v)))
elif mode == 'vars': elif mode == 'vars':
print(' '.join('$' + k for k, v in parse_env(sys.argv[2]))) print(' '.join('$' + k for k, v in parse_env(sys.argv[2])))
elif mode == 'vars_merged': elif mode == 'vars_merged':