fix(config): unwrap quoted secrets that podman-compose passes through
bcrypt hashes and most OIDC client secrets contain `$`, so `.env.example` tells operators to single-quote them or Docker Compose eats the `$`. Docker Compose then strips those quotes itself when reading `env_file`, and so does python-dotenv on the `env_file=` path — but podman-compose (1.0.6) forwards them verbatim. The container receives `'$2b$12$...'`, quotes included, every bcrypt verify fails, and the logged error blames the operator's hash. Unwrap one matched pair of surrounding quotes from SECRET_KEY, AUTH_PASSWORD_HASH and OIDC_CLIENT_SECRET before validating them, so the same .env works under either container runtime. Only a matched pair is removed: a value with a single stray quote still reaches the existing validation instead of being silently patched up, and SECRET_KEY is unwrapped before the 32-byte OIDC floor is checked so quotes cannot pad a short key past it. Fixes #410 ha-relevant: no
This commit is contained in:
committed by
Pouzor - Rémy Jardient
parent
4e8d841807
commit
6b11e7ed34
@@ -19,6 +19,8 @@ AUTH_MODE=local
|
||||
# ⚠️ Change before exposing on a network.
|
||||
# Generate a new hash: python3 -c "import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt()).decode())"
|
||||
# ⚠️ Keep the single quotes around the hash — bcrypt hashes contain \$ which Docker misinterprets without them.
|
||||
# podman-compose does not strip those quotes the way Docker Compose does, so the
|
||||
# backend unwraps one matched pair itself — quoted or not, both work everywhere.
|
||||
AUTH_USERNAME=admin
|
||||
AUTH_PASSWORD_HASH='$2b$12$RtMbyw17l4N5UGzeXMNAWuzCaVV.XFBY7ZetWheQhxcBDcxahapkG'
|
||||
|
||||
|
||||
@@ -37,6 +37,26 @@ def app_base_path_of(redirect_uri: str) -> str:
|
||||
base = path[: -len(OIDC_CALLBACK_PATH)]
|
||||
return f"{base}/" if base else "/"
|
||||
|
||||
def unquote_env(value: str) -> str:
|
||||
"""
|
||||
Drop one matched pair of surrounding quotes from an env value.
|
||||
|
||||
Secrets that contain `$` (bcrypt hashes, most OIDC client secrets) have to be
|
||||
single-quoted in `.env`, or Docker Compose eats the `$`. Compose then strips
|
||||
those quotes itself when it reads `env_file`, and so does python-dotenv on the
|
||||
`env_file=` path — but `podman-compose` (1.0.6) passes them through verbatim,
|
||||
so the container receives `'$2b$12$…'`, quotes included, and every bcrypt
|
||||
verify fails. Unwrap here rather than asking operators to quote differently
|
||||
per container runtime.
|
||||
|
||||
Only a *matched* pair is removed, so a genuinely malformed value still reaches
|
||||
the validation below instead of being silently patched up.
|
||||
"""
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
||||
return value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
def _read_version() -> str:
|
||||
for candidate in [
|
||||
Path(__file__).parent.parent.parent.parent / "VERSION", # repo root (dev)
|
||||
@@ -81,6 +101,12 @@ class Settings(BaseSettings):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_auth_settings(self) -> "Settings":
|
||||
# Runtime-dependent quoting: see unquote_env. Applied to the three values
|
||||
# that routinely contain a `$` and so get quoted in .env.
|
||||
self.secret_key = unquote_env(self.secret_key)
|
||||
self.auth_password_hash = unquote_env(self.auth_password_hash)
|
||||
self.oidc_client_secret = unquote_env(self.oidc_client_secret)
|
||||
|
||||
h = self.auth_password_hash
|
||||
if h and not h.startswith("$2"):
|
||||
logger.error(
|
||||
|
||||
@@ -255,3 +255,57 @@ def test_valid_oidc_configuration_is_accepted():
|
||||
configured = _valid_oidc_settings()
|
||||
assert configured.auth_mode == "oidc"
|
||||
assert configured.oidc_cookie_secure is True
|
||||
|
||||
|
||||
# --- Env values arriving still quoted (podman-compose, issue #410) ---
|
||||
|
||||
|
||||
def test_quoted_password_hash_is_unwrapped():
|
||||
"""podman-compose passes .env quotes through verbatim; Docker Compose strips them."""
|
||||
configured = Settings(
|
||||
_env_file=None,
|
||||
secret_key="test-secret",
|
||||
auth_password_hash="'$2b$12$RtMbyw17l4N5UGzeXMNAWuzCaVV.XFBY7ZetWheQhxcBDcxahapkG'",
|
||||
)
|
||||
assert configured.auth_password_hash == "$2b$12$RtMbyw17l4N5UGzeXMNAWuzCaVV.XFBY7ZetWheQhxcBDcxahapkG"
|
||||
|
||||
|
||||
def test_double_quoted_password_hash_is_unwrapped():
|
||||
configured = Settings(
|
||||
_env_file=None,
|
||||
secret_key="test-secret",
|
||||
auth_password_hash='"$2b$12$RtMbyw17l4N5UGzeXMNAWuzCaVV.XFBY7ZetWheQhxcBDcxahapkG"',
|
||||
)
|
||||
assert configured.auth_password_hash.startswith("$2b$12$")
|
||||
|
||||
|
||||
def test_unquoted_password_hash_is_left_alone():
|
||||
raw = "$2b$12$RtMbyw17l4N5UGzeXMNAWuzCaVV.XFBY7ZetWheQhxcBDcxahapkG"
|
||||
configured = Settings(_env_file=None, secret_key="test-secret", auth_password_hash=raw)
|
||||
assert configured.auth_password_hash == raw
|
||||
|
||||
|
||||
def test_unmatched_quote_is_not_stripped():
|
||||
"""Only a matched pair is unwrapped, so a truly broken value still fails validation."""
|
||||
configured = Settings(
|
||||
_env_file=None,
|
||||
secret_key="test-secret",
|
||||
auth_password_hash="'$2b$12$RtMbyw17l4N5UGzeXMNAWu",
|
||||
)
|
||||
assert configured.auth_password_hash.startswith("'")
|
||||
|
||||
|
||||
def test_quoted_secret_key_is_unwrapped():
|
||||
configured = _valid_oidc_settings(secret_key="'test-secret-key-that-is-at-least-32-bytes'")
|
||||
assert configured.secret_key == "test-secret-key-that-is-at-least-32-bytes"
|
||||
|
||||
|
||||
def test_secret_key_length_is_checked_after_unwrapping():
|
||||
"""The quotes must not pad a too-short key past the 32-byte OIDC floor."""
|
||||
with pytest.raises(ValueError, match="SECRET_KEY must be at least 32 bytes"):
|
||||
_valid_oidc_settings(secret_key="'" + "a" * 31 + "'")
|
||||
|
||||
|
||||
def test_quoted_oidc_client_secret_is_unwrapped():
|
||||
configured = _valid_oidc_settings(oidc_client_secret="'sup3r$ecret'")
|
||||
assert configured.oidc_client_secret == "sup3r$ecret"
|
||||
|
||||
Reference in New Issue
Block a user