fix: harden evidence secret file handoff
This commit is contained in:
@@ -223,6 +223,37 @@ def test_signed_http_rejects_reordered_extra_mismatch_userinfo_and_duplicate_pro
|
||||
assert_no_canaries(caught.value)
|
||||
|
||||
|
||||
def test_s3_rejects_inline_credentials_and_never_discloses_them(tmp_path):
|
||||
path = write_config(tmp_path, {
|
||||
"type": "s3", "bucket": "clinical-evidence",
|
||||
"access_key": ACCESS_CANARY,
|
||||
"secret_key": SECRET_CANARY,
|
||||
"session_token": TOKEN_CANARY,
|
||||
})
|
||||
|
||||
with pytest.raises(ConfigError) as caught:
|
||||
load_config(path)
|
||||
assert "file" in str(caught.value).lower()
|
||||
assert_no_canaries(caught.value)
|
||||
assert_no_canaries("".join(traceback.format_exception(caught.value)))
|
||||
|
||||
|
||||
def test_s3_scalar_secret_files_are_bounded(tmp_path):
|
||||
access = tmp_path / "oversized-access-key"
|
||||
access.write_bytes(b"A" * (64 * 1024 + 1))
|
||||
secret = tmp_path / "secret-key"
|
||||
secret.write_text("bounded-secret")
|
||||
path = write_config(tmp_path, {
|
||||
"type": "s3", "bucket": "clinical-evidence",
|
||||
"access_key_file": str(access), "secret_key_file": str(secret),
|
||||
})
|
||||
|
||||
with pytest.raises(ConfigError) as caught:
|
||||
load_config(path)
|
||||
assert "secret file" in str(caught.value).lower()
|
||||
assert "bounded-secret" not in str(caught.value)
|
||||
|
||||
|
||||
def test_s3_ambient_and_static_file_credentials_are_secret_typed(tmp_path):
|
||||
ambient = load_config(write_config(tmp_path, {
|
||||
"type": "s3", "bucket": "clinical-evidence", "prefix": "published/",
|
||||
|
||||
+18
-7
@@ -49,13 +49,17 @@ def _expand_env(value: Any) -> Any:
|
||||
_MAX_SIGNED_URL_FILE_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
def _resolve_http_signed_url_files(value: Any) -> Any:
|
||||
def _resolve_evidence_secret_files(value: Any) -> Any:
|
||||
"""Resolve only signed HTTP URL arrays, keeping their values out of public errors."""
|
||||
if isinstance(value, dict):
|
||||
resolved = {
|
||||
key: _resolve_http_signed_url_files(item)
|
||||
key: _resolve_evidence_secret_files(item)
|
||||
for key, item in value.items()
|
||||
}
|
||||
if resolved.get("type") == "s3" and any(
|
||||
name in resolved for name in ("access_key", "secret_key", "session_token")
|
||||
):
|
||||
raise ConfigError("S3 Evidence credentials require *_file references")
|
||||
if resolved.get("type") != "http" or "signed_urls_file" not in resolved:
|
||||
return resolved
|
||||
if "urls" in resolved:
|
||||
@@ -94,10 +98,13 @@ def _resolve_http_signed_url_files(value: Any) -> Any:
|
||||
resolved["urls"] = parsed
|
||||
return resolved
|
||||
if isinstance(value, list):
|
||||
return [_resolve_http_signed_url_files(item) for item in value]
|
||||
return [_resolve_evidence_secret_files(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
_MAX_SCALAR_SECRET_FILE_BYTES = 64 * 1024
|
||||
|
||||
|
||||
def _resolve_secret_files(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
resolved = {key: _resolve_secret_files(item) for key, item in value.items()}
|
||||
@@ -109,9 +116,13 @@ def _resolve_secret_files(value: Any) -> Any:
|
||||
raise ConfigError(f"{secret_name} and {file_name} are mutually exclusive")
|
||||
path = Path(resolved.pop(file_name))
|
||||
try:
|
||||
secret = path.read_text()
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise ConfigError(f"Cannot read secret file: {path}") from exc
|
||||
with path.open("rb") as stream:
|
||||
payload = stream.read(_MAX_SCALAR_SECRET_FILE_BYTES + 1)
|
||||
if len(payload) > _MAX_SCALAR_SECRET_FILE_BYTES:
|
||||
raise OSError
|
||||
secret = payload.decode("utf-8")
|
||||
except (OSError, UnicodeError):
|
||||
raise ConfigError(f"Cannot read secret file: {path}") from None
|
||||
if not secret or any(char.isspace() for char in secret) or "\x00" in secret:
|
||||
raise ConfigError(f"Invalid secret file: {path}")
|
||||
resolved[secret_name] = secret
|
||||
@@ -532,7 +543,7 @@ def load_config(path: Path) -> Config:
|
||||
raise ConfigError(f"Configurazione YAML non valida: {path}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ConfigError(f"Configurazione non valida (atteso un mapping YAML): {path}")
|
||||
expanded = _resolve_secret_files(_resolve_http_signed_url_files(_expand_env(raw)))
|
||||
expanded = _resolve_secret_files(_resolve_evidence_secret_files(_expand_env(raw)))
|
||||
_validate_internal_embedding_contract(expanded, path)
|
||||
_validate_internal_vector_contract(expanded, path)
|
||||
translated, used_legacy = translate_legacy_config(expanded)
|
||||
|
||||
Reference in New Issue
Block a user