Перенесены секреты из .env в SM

This commit is contained in:
mi
2026-07-30 19:22:48 +03:00
parent 049c45db5c
commit e24ed9d8ef
58 changed files with 3350 additions and 1054 deletions
+74 -43
View File
@@ -44,11 +44,13 @@ class InfrastructureConfigTests(unittest.TestCase):
application = (ROOT / "infra/compose/application.yml").read_text(encoding="utf-8")
self.assertIn("networks: [backend, observability, egress]", application)
self.assertIn("networks: [public, backend, observability, egress]", application)
self.assertIn('KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY: ""', application)
self.assertIn(
"KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY_FILE: "
"/run/secrets/keycloak_yandex_captcha_server_key",
application,
)
self.assertEqual(
application.count(
"IDGTL_SMS_API_KEY: ${IDGTL_SMS_API_KEY:?IDGTL_SMS_API_KEY is required}"
),
application.count("IDGTL_SMS_API_KEY_FILE: /run/secrets/idgtl_sms_api_key"),
1,
)
@@ -56,7 +58,7 @@ class InfrastructureConfigTests(unittest.TestCase):
self.assertEqual(jobs.count("networks: [backend, egress]"), 5)
observability = (ROOT / "observability/docker-compose.yml").read_text(encoding="utf-8")
self.assertIn("networks: [observability, backend, egress]", observability)
self.assertIn("networks: [observability, egress]", observability)
redis = (ROOT / "redis/docker-compose.yml").read_text(encoding="utf-8")
self.assertNotIn("egress", redis)
@@ -64,6 +66,8 @@ class InfrastructureConfigTests(unittest.TestCase):
def test_only_nginx_fragment_publishes_ports(self) -> None:
forbidden = (
ROOT / "infra/compose/application.yml",
ROOT / "api-backend/docker-compose.yml",
ROOT / "keycloak/docker-compose.yml",
ROOT / "redis/docker-compose.yml",
ROOT / "observability/docker-compose.yml",
ROOT / "deployment/docker-compose.jobs.yml",
@@ -177,6 +181,10 @@ class InfrastructureConfigTests(unittest.TestCase):
self.assertIn("storage: file_storage", config)
self.assertIn("retry_on_failure:", config)
self.assertIn('insecure: "${env:OTEL_REMOTE_TLS_INSECURE}"', config)
self.assertIn(
"authorization: ${file:/run/secrets/otel_remote_auth_header}", config
)
self.assertNotIn("OTEL_REMOTE_AUTH_HEADER", config)
self.assertIn("tail_sampling:", config)
self.assertNotIn("probabilistic_sampler:", config)
self.assertIn('targets: ["sms-service:8080"]', config)
@@ -231,7 +239,7 @@ class InfrastructureConfigTests(unittest.TestCase):
def test_smoke_script_does_not_source_env_as_shell(self) -> None:
smoke = (ROOT / "deployment/scripts/smoke.sh").read_text(encoding="utf-8")
self.assertNotIn('. "./$ENV_FILE"', smoke)
self.assertNotIn('. "./$CONFIG_FILE"', smoke)
for variable in ("PUBLIC_HOST", "PUBLIC_WEB_URL", "KEYCLOAK_REALM"):
self.assertIn(f"{variable}=$(env_value {variable})", smoke)
@@ -298,18 +306,64 @@ class InfrastructureConfigTests(unittest.TestCase):
for variable in (
"KC_DB_SCHEMA",
"KEYCLOAK_OTP_MOCK_ENABLED",
"KEYCLOAK_OTP_MOCK_CODE",
"KEYCLOAK_YANDEX_CAPTCHA_ENABLED",
"KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY",
"KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY",
"KEYCLOAK_OTP_HMAC_KEY",
"KEYCLOAK_OTP_SETTINGS_MAX_STALE_SEC",
"KEYCLOAK_SETTINGS_BRIDGE_URL",
"KEYCLOAK_SETTINGS_BRIDGE_TOKEN",
"KEYCLOAK_SMS_SERVICE_URL",
"KEYCLOAK_SMS_SERVICE_TOKEN",
):
self.assertIn(f" {variable}:", application)
for variable in (
"KEYCLOAK_OTP_MOCK_CODE",
"KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY",
"KEYCLOAK_OTP_HMAC_KEY",
"KEYCLOAK_SETTINGS_BRIDGE_TOKEN",
"KEYCLOAK_SMS_SERVICE_TOKEN",
):
self.assertIn(f" {variable}_FILE:", application)
self.assertIn(
" KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY: "
"${KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY:-}",
application,
)
def test_compose_secrets_do_not_enter_config_environment(self) -> None:
compose_paths = (
ROOT / "infra/compose/application.yml",
ROOT / "redis/docker-compose.yml",
ROOT / "observability/docker-compose.yml",
ROOT / "deployment/docker-compose.jobs.yml",
)
combined = "\n".join(path.read_text(encoding="utf-8") for path in compose_paths)
self.assertNotIn("env_file:", combined)
for path in compose_paths:
self.assertIn(
"core: {soft: 0, hard: 0}",
path.read_text(encoding="utf-8"),
path,
)
for variable in (
"DATABASE_URL",
"REDIS_URL",
"SMS_SERVICE_TOKEN",
"KEYCLOAK_DB_PASSWORD",
"OTEL_REMOTE_AUTH_HEADER",
):
self.assertNotIn(f"${{{variable}", combined)
self.assertIn(
"${HAN_SECRETS_DIR:-/run/han-chat/secrets}/DATABASE_URL", combined
)
self.assertNotIn("entrypoint: []", combined)
for service in (
"api-backend",
"sms-service",
"message-safety",
"bitrix-local-app",
"bitrix-sync",
"keycloak",
):
dockerfile = (ROOT / service / "Dockerfile").read_text(encoding="utf-8")
self.assertIn("han-container-entrypoint", dockerfile, service)
def test_env_validator_accepts_materialized_example(self) -> None:
example = (ROOT / ".env.example").read_text(encoding="utf-8")
@@ -317,15 +371,10 @@ class InfrastructureConfigTests(unittest.TestCase):
self.assertNotIn("currentSchema=", example)
self.assertIn("KEYCLOAK_DB_SCHEMA=keycloak", example)
self.assertIn("HAN_PG_PORT=5433", example)
for required in (
"CURSOR_HMAC_SECRET=",
"BITRIX_TOKEN_ENCRYPTION_KEY=",
"KEYCLOAK_INTERNAL_URL=http://keycloak:8080/auth",
"KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080",
"IDGTL_SMS_CALLBACK_PUBLIC_URL=https://chat.example.ru/callbacks/idgtl/sms",
):
self.assertIn(required, example)
materialized = example.replace("change-me", "0123456789abcdef0123456789abcdef")
self.assertIn("SECRETS_SOURCE=file", example)
self.assertNotIn("CURSOR_HMAC_SECRET=", example)
self.assertNotIn("BITRIX_TOKEN_ENCRYPTION_KEY=", example)
materialized = example
materialized = materialized.replace(
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false",
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true",
@@ -341,11 +390,9 @@ class InfrastructureConfigTests(unittest.TestCase):
)
self.assertEqual(result.returncode, 0, result.stderr)
def test_env_validator_requires_captcha_keys_only_when_enabled(self) -> None:
def test_env_validator_keeps_captcha_server_key_out_of_config(self) -> None:
example = (ROOT / ".env.example").read_text(encoding="utf-8")
materialized = example.replace(
"change-me", "0123456789abcdef0123456789abcdef"
).replace(
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false",
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true",
)
@@ -353,33 +400,17 @@ class InfrastructureConfigTests(unittest.TestCase):
"KEYCLOAK_YANDEX_CAPTCHA_ENABLED=false",
"KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true",
)
enabled_with_keys = enabled_without_keys.replace(
"KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY=",
"KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY=client-key",
).replace(
"KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY=",
"KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY=server-key-0123456789",
)
with tempfile.TemporaryDirectory() as directory:
env_file = Path(directory) / ".env"
env_file.write_text(enabled_without_keys, encoding="utf-8")
missing = subprocess.run(
result = subprocess.run(
[sys.executable, str(ROOT / "scripts/validate-env"), str(env_file)],
text=True,
capture_output=True,
check=False,
)
env_file.write_text(enabled_with_keys, encoding="utf-8")
configured = subprocess.run(
[sys.executable, str(ROOT / "scripts/validate-env"), str(env_file)],
text=True,
capture_output=True,
check=False,
)
self.assertNotEqual(missing.returncode, 0)
self.assertIn("KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY", missing.stderr)
self.assertIn("KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY", missing.stderr)
self.assertEqual(configured.returncode, 0, configured.stderr)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY=", example)
if __name__ == "__main__":
@@ -0,0 +1,126 @@
from __future__ import annotations
import os
import runpy
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
VALIDATOR = ROOT / "scripts/validate-env"
def runtime_values() -> dict[str, str]:
symbols = runpy.run_path(str(VALIDATOR))
values = {key: "a" * 32 for key in symbols["REQUIRED_RUNTIME"]}
pg_tail = "?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem"
values.update(
{
"DATABASE_URL": f"postgresql+asyncpg://han_app:password@pg:5433/han_chat{pg_tail}",
"BITRIX_DATABASE_URL": f"postgresql://bitrix:password@pg:5433/han_chat{pg_tail}",
"BITRIX_SYNC_DATABASE_URL": f"postgresql://sync:password@pg:5433/han_chat{pg_tail}",
"MESSAGE_SAFETY_DATABASE_URL": f"postgresql://safety:password@pg:5433/han_chat{pg_tail}",
"SMS_DATABASE_URL": f"postgresql+asyncpg://sms:password@pg:5433/han_chat{pg_tail}",
"KEYCLOAK_DB_URL": f"jdbc:postgresql://pg:5433/han_chat{pg_tail}",
"REDIS_API_PASSWORD": "r" * 32,
"REDIS_SAFETY_PASSWORD": "s" * 32,
"REDIS_HEALTH_PASSWORD": "h" * 32,
"REDIS_URL": f"redis://api_backend:{'r' * 32}@redis:6379/0",
"REDIS_REALTIME_URL": f"redis://api_backend:{'r' * 32}@redis:6379/1",
"MESSAGE_SAFETY_REDIS_URL": f"redis://message_safety:{'s' * 32}@redis:6379/2",
"BITRIX_LOCAL_APP_INTERNAL_TOKEN": "b" * 32,
"BITRIX_INTERNAL_API_TOKEN": "b" * 32,
"BITRIX_API_FORWARD_TOKEN": "f" * 32,
"BITRIX_API_INBOX_TOKEN": "f" * 32,
"KEYCLOAK_SMS_SERVICE_TOKEN": "k" * 32,
"SMS_SERVICE_TOKEN": "k" * 32,
"KEYCLOAK_OTP_MOCK_CODE": "846271",
}
)
return values
class SecretHygieneTests(unittest.TestCase):
def run_validator(
self, config: Path, *args: str, environment: dict[str, str] | None = None
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(VALIDATOR), str(config), *args],
text=True,
capture_output=True,
check=False,
env=environment,
)
def test_example_is_non_secret_config(self) -> None:
example = (ROOT / ".env.example").read_text(encoding="utf-8")
symbols = runpy.run_path(str(VALIDATOR))
for key in symbols["FORBIDDEN_CONFIG_KEYS"]:
self.assertNotRegex(example, rf"(?m)^{key}=")
self.assertIn("SECRETS_SOURCE=file", example)
def test_config_rejects_secret_key_and_credential_url(self) -> None:
example = (ROOT / ".env.example").read_text(encoding="utf-8")
with tempfile.TemporaryDirectory() as directory:
config = Path(directory) / ".env"
config.write_text(
example
+ "\nSMS_SERVICE_TOKEN=not-for-config\n"
+ "EXTERNAL_URL=https://user:password@example.net/path\n",
encoding="utf-8",
)
result = self.run_validator(config)
self.assertNotEqual(result.returncode, 0)
self.assertIn("SMS_SERVICE_TOKEN", result.stderr)
self.assertIn("URL с credentials", result.stderr)
self.assertNotIn("not-for-config", result.stderr)
def test_runtime_environment_is_validated_without_value_disclosure(self) -> None:
values = runtime_values()
secret_canary = "canary-never-print-" + "x" * 20
values["CURSOR_HMAC_SECRET"] = secret_canary
environment = {**os.environ, **values}
with tempfile.TemporaryDirectory() as directory:
config = Path(directory) / ".env"
config.write_text(
(ROOT / ".env.example").read_text(encoding="utf-8").replace(
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false",
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true",
),
encoding="utf-8",
)
result = self.run_validator(config, "--runtime-env", environment=environment)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn(secret_canary, result.stdout + result.stderr)
def test_runtime_manifest_reads_protected_files_without_value_disclosure(self) -> None:
values = runtime_values()
secret_canary = "manifest-canary-" + "z" * 20
values["CURSOR_HMAC_SECRET"] = secret_canary
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
config = root / ".env"
config.write_text(
(ROOT / ".env.example").read_text(encoding="utf-8").replace(
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false",
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true",
),
encoding="utf-8",
)
entries = []
for index, (key, value) in enumerate(sorted(values.items())):
path = root / f"secret-{index}"
path.write_text(value + "\n", encoding="utf-8")
path.chmod(0o600)
entries.append(f"{key}={path.resolve()}")
manifest = root / "manifest"
manifest.write_text("\n".join(entries) + "\n", encoding="utf-8")
result = self.run_validator(config, "--runtime-manifest", str(manifest))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn(secret_canary, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,471 @@
from __future__ import annotations
import base64
import importlib.util
import io
import json
import os
import stat
import subprocess
import sys
import tempfile
import unittest
import urllib.error
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
LOADER_PATH = ROOT / "deployment" / "secrets" / "secrets_loader.py"
LAUNCHER_PATH = ROOT / "deployment" / "secrets" / "han-secrets"
SPEC = importlib.util.spec_from_file_location("han_secrets_loader", LOADER_PATH)
assert SPEC and SPEC.loader
loader = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = loader
SPEC.loader.exec_module(loader)
class FakeResponse:
def __init__(
self, status: int, body: bytes, headers: dict[str, str] | None = None
) -> None:
self.status = status
self._body = body
self.headers = headers or {}
def __enter__(self) -> "FakeResponse":
return self
def __exit__(self, *args: Any) -> None:
return None
def read(self, amount: int) -> bytes:
return self._body[:amount]
class FakeOpener:
def __init__(self, outcomes: list[Any]) -> None:
self.outcomes = outcomes
self.requests: list[Any] = []
def open(self, request: Any, timeout: float) -> FakeResponse:
self.requests.append((request, timeout))
outcome = self.outcomes.pop(0)
if isinstance(outcome, BaseException):
raise outcome
return outcome
def identity_response(*, project_scoped: bool = True) -> FakeResponse:
token: dict[str, Any] = {
"catalog": [
{
"type": "secrets-manager",
"endpoints": [
{
"region": "ru-test",
"interface": "public",
"url": "https://secrets.example",
}
],
}
]
}
if project_scoped:
token["project"] = {"id": "project-id"}
return FakeResponse(
201,
json.dumps({"token": token}).encode(),
{"X-Subject-Token": "iam-token-do-not-log"},
)
def secret_response(value: bytes) -> FakeResponse:
return FakeResponse(
200, json.dumps({"value": base64.b64encode(value).decode()}).encode()
)
def selectel_config(runtime_dir: Path) -> dict[str, Any]:
return {
"version": 1,
"mode": "selectel",
"runtime_dir": str(runtime_dir),
"http": {"timeout_seconds": 2, "retries": 1, "max_response_bytes": 4096},
"selectel": {
"account_id": "123456",
"username": "reader",
"project_name": "production",
"region": "ru-test",
"password_file": "selectel-password",
},
"secrets": {
"DATABASE_URL": {
"remote": "han/database-url",
"consumers": ["api", "migration"],
"max_bytes": 512,
},
"SHARED_TOKEN": {
"remote": "han/shared-token",
"consumers": ["api"],
"max_bytes": 128,
},
},
}
def write_json(path: Path, value: dict[str, Any]) -> None:
path.write_text(json.dumps(value), encoding="utf-8")
def make_private(path: Path) -> None:
os.chmod(path, 0o600)
class SecretsLoaderTests(unittest.TestCase):
def test_decodes_current_secret_from_nested_selectel_version(self) -> None:
encoded = base64.b64encode(b"current-secret").decode()
value = loader.decode_secret(
{"name": "DATABASE_URL", "version": {"version_id": 1, "value": encoded}},
"DATABASE_URL",
1024,
)
self.assertEqual(value, b"current-secret")
def test_empty_literal_never_requires_provider_or_fallback_value(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
fallback = root / "fallback.env"
fallback.write_text("REQUIRED_TOKEN=required-value\n", encoding="utf-8")
make_private(fallback)
config = {
"version": 1,
"mode": "file",
"runtime_dir": str(root / "run"),
"file": {"path": str(fallback)},
"secrets": {
"REQUIRED_TOKEN": {
"remote": "provider-token",
"consumers": ["service"],
},
"OPTIONAL_HEADER": {
"literal": "",
"consumers": ["service"],
},
},
}
config_path = root / "file.json"
write_json(config_path, config)
loader.run(config_path, environ={})
self.assertEqual((root / "run" / "OPTIONAL_HEADER").read_bytes(), b"")
self.assertIn(
'OPTIONAL_HEADER=""',
(root / "run" / "service.env").read_text(encoding="utf-8"),
)
def test_launcher_uses_public_source_switch_without_exporting_values(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
runtime = root / "runtime"
fallback = root / "fallback.env"
fallback.write_text("TEST_SECRET=canary-secret-value\n", encoding="utf-8")
make_private(fallback)
public = root / ".env"
public.write_text(
"SECRETS_SOURCE=file\nAPP_ENV=production\n", encoding="utf-8"
)
loader_config = root / "production.file.json"
write_json(
loader_config,
{
"version": 1,
"mode": "file",
"runtime_dir": str(runtime),
"file": {"path": str(fallback)},
"secrets": {
"TEST_SECRET": {
"remote": "unused-in-file-mode",
"consumers": ["test-service"],
}
},
},
)
probe = (
"import json,os,pathlib;"
"p=os.environ['TEST_SECRET_FILE'];"
"print(json.dumps({'raw':os.environ.get('TEST_SECRET'),"
"'value':pathlib.Path(p).read_text()}))"
)
result = subprocess.run(
[
sys.executable,
str(LAUNCHER_PATH),
"run",
"--config",
str(public),
"--loader-config",
str(loader_config),
"--",
sys.executable,
"-c",
probe,
],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
self.assertIsNone(payload["raw"])
self.assertEqual(payload["value"], "canary-secret-value")
self.assertEqual(
(runtime / "manifest").read_text(encoding="utf-8").strip(),
f"TEST_SECRET={(runtime / 'TEST_SECRET').resolve()}",
)
def test_selectel_flow_uses_project_scope_catalog_and_per_service_files(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
credentials = root / "credentials"
credentials.mkdir()
(credentials / "selectel-password").write_text(
"service-user-password\n", encoding="utf-8"
)
make_private(credentials / "selectel-password")
runtime = root / "run"
config_path = root / "config.json"
write_json(config_path, selectel_config(runtime))
opener = FakeOpener(
[
identity_response(),
secret_response(b"postgresql://user:password@db/app"),
secret_response(b'token with spaces and "quotes"'),
]
)
def factory(**kwargs: Any) -> Any:
return loader.HTTPClient(
**kwargs, opener=opener, sleeper=lambda _: None, jitter=lambda: 0
)
consumers = loader.run(
config_path,
environ={"CREDENTIALS_DIRECTORY": str(credentials)},
client_factory=factory,
)
self.assertEqual(consumers, ["api", "migration"])
auth_request = opener.requests[0][0]
auth_payload = json.loads(auth_request.data)
self.assertEqual(
auth_payload["auth"]["scope"]["project"]["name"], "production"
)
self.assertEqual(
auth_payload["auth"]["identity"]["password"]["user"]["domain"]["name"],
"123456",
)
self.assertEqual(
opener.requests[1][0].get_header("X-auth-token"),
"iam-token-do-not-log",
)
self.assertEqual(
opener.requests[1][0].full_url,
"https://secrets.example/v1/han%2Fdatabase-url",
)
api_text = (runtime / "api.env").read_text(encoding="utf-8")
migration_text = (runtime / "migration.env").read_text(encoding="utf-8")
self.assertIn('DATABASE_URL="postgresql://user:password@db/app"', api_text)
self.assertIn(
'SHARED_TOKEN="token with spaces and \\"quotes\\""', api_text
)
self.assertNotIn("SHARED_TOKEN", migration_text)
if os.name != "nt":
self.assertEqual(stat.S_IMODE((runtime / "api.env").stat().st_mode), 0o600)
self.assertEqual(stat.S_IMODE(runtime.stat().st_mode), 0o700)
def test_retry_uses_jitter_and_then_succeeds(self) -> None:
opener = FakeOpener(
[urllib.error.URLError("temporary"), FakeResponse(200, b"{}")]
)
sleeps: list[float] = []
client = loader.HTTPClient(
timeout=1,
retries=1,
max_response_bytes=1024,
opener=opener,
sleeper=sleeps.append,
jitter=lambda: 0.25,
)
result = client.request(
"GET", "https://provider.example/v1/key", expected=frozenset({200})
)
self.assertEqual(result.status, 200)
self.assertEqual(sleeps, [0.1875])
def test_paired_canonical_names_fetch_provider_version_once(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
credentials = root / "credentials"
credentials.mkdir()
credential = credentials / "selectel-password"
credential.write_text("password", encoding="utf-8")
make_private(credential)
config = selectel_config(root / "run")
config["secrets"]["SHARED_TOKEN"]["remote"] = "han/database-url"
config_path = root / "config.json"
write_json(config_path, config)
opener = FakeOpener([identity_response(), secret_response(b"same-value")])
def factory(**kwargs: Any) -> Any:
return loader.HTTPClient(**kwargs, opener=opener)
loader.run(
config_path,
environ={"CREDENTIALS_DIRECTORY": str(credentials)},
client_factory=factory,
)
self.assertEqual(len(opener.requests), 2)
output = (root / "run" / "api.env").read_text(encoding="utf-8")
self.assertIn('DATABASE_URL="same-value"', output)
self.assertIn('SHARED_TOKEN="same-value"', output)
def test_http_error_is_redacted_and_body_is_not_read(self) -> None:
leaked = b"postgresql://admin:secret@db/app iam-token response-body"
class ExplodingBody(io.BytesIO):
def read(self, *args: Any, **kwargs: Any) -> bytes:
raise AssertionError("HTTP error body must not be read")
error = urllib.error.HTTPError(
"https://provider.example/v1/key",
403,
"body contains a secret",
{},
ExplodingBody(leaked),
)
client = loader.HTTPClient(
timeout=1,
retries=0,
max_response_bytes=1024,
opener=FakeOpener([error]),
)
with self.assertRaises(loader.LoaderError) as caught:
client.request(
"GET",
"https://provider.example/v1/key",
headers={"X-Auth-Token": "iam-token"},
expected=frozenset({200}),
)
message = str(caught.exception)
self.assertEqual(message, "provider request failed with HTTP 403")
for forbidden in ("secret", "iam-token", "response-body", "postgresql://"):
self.assertNotIn(forbidden, message)
def test_unscoped_token_fails_without_replacing_existing_output(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
credentials = root / "credentials"
credentials.mkdir()
(credentials / "selectel-password").write_text("password", encoding="utf-8")
make_private(credentials / "selectel-password")
runtime = root / "run"
runtime.mkdir()
existing = runtime / "api.env"
existing.write_text('DATABASE_URL="old-value"\n', encoding="utf-8")
config_path = root / "config.json"
write_json(config_path, selectel_config(runtime))
opener = FakeOpener([identity_response(project_scoped=False)])
def factory(**kwargs: Any) -> Any:
return loader.HTTPClient(**kwargs, opener=opener)
with self.assertRaisesRegex(loader.LoaderError, "not project-scoped"):
loader.run(
config_path,
environ={"CREDENTIALS_DIRECTORY": str(credentials)},
client_factory=factory,
)
self.assertEqual(
existing.read_text(encoding="utf-8"),
'DATABASE_URL="old-value"\n',
)
def test_invalid_base64_and_size_limit_fail_closed(self) -> None:
with self.assertRaisesRegex(loader.LoaderError, "invalid base64"):
loader.decode_secret({"value": "not-base64!"}, "TOKEN", 128)
with self.assertRaisesRegex(loader.LoaderError, "configured limit"):
loader.decode_secret(
{"value": base64.b64encode(b"too-long").decode()}, "TOKEN", 3
)
with self.assertRaisesRegex(loader.LoaderError, "dotenv"):
loader.decode_secret(
{"value": base64.b64encode(b"line1\nline2").decode()}, "TOKEN", 128
)
def test_file_mode_is_explicit_strict_and_narrow(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "fallback.env"
source.write_text(
"# exact recovery set\n"
'DATABASE_URL="postgresql://user:pass@db/app"\n'
"SHARED_TOKEN='literal value'\n",
encoding="utf-8",
)
make_private(source)
runtime = root / "run"
config = selectel_config(runtime)
config["mode"] = "file"
config.pop("selectel")
config.pop("http")
config["file"] = {"path": str(source)}
config_path = root / "config.json"
write_json(config_path, config)
loader.run(config_path, environ={})
output = (runtime / "api.env").read_text(encoding="utf-8")
self.assertIn("literal value", output)
self.assertNotIn("selectel", output)
source.write_text(
"DATABASE_URL=ok\nSHARED_TOKEN=ok\nUNDECLARED=leak\n",
encoding="utf-8",
)
with self.assertRaisesRegex(loader.LoaderError, "undeclared key"):
loader.run(config_path, environ={})
def test_selectel_failure_never_falls_back_to_file_section(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
config = selectel_config(root / "run")
config["file"] = {"path": str(root / "fallback.env")}
config_path = root / "config.json"
write_json(config_path, config)
with self.assertRaisesRegex(loader.LoaderError, "forbidden"):
loader.run(config_path, environ={})
def test_relative_credential_requires_systemd_directory(self) -> None:
selectel = {"password_file": "credential"}
with self.assertRaisesRegex(loader.LoaderError, "CREDENTIALS_DIRECTORY"):
loader.credential_value(selectel, {})
def test_response_content_length_limit_is_enforced(self) -> None:
client = loader.HTTPClient(
timeout=1,
retries=0,
max_response_bytes=10,
opener=FakeOpener(
[FakeResponse(200, b"{}", {"Content-Length": "100"})]
),
)
with self.assertRaisesRegex(loader.LoaderError, "exceeds"):
client.request(
"GET", "https://provider.example/v1/key", expected=frozenset({200})
)
if __name__ == "__main__":
unittest.main()