From c9cbe9dc4ecd0cf709ce7feffdc09a3ebbad4c9a Mon Sep 17 00:00:00 2001 From: William Joy Date: Fri, 15 May 2026 19:33:58 -0700 Subject: [PATCH 1/2] Harden prod Railway runtime secrets --- docs/prod-railway-runtime-secrets.md | 31 +++++++++ .../prod-railway/common/config/main-local.php | 24 +++---- tests/check-prod-railway-runtime-secrets.py | 63 +++++++++++++++++++ 3 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 docs/prod-railway-runtime-secrets.md create mode 100644 tests/check-prod-railway-runtime-secrets.py diff --git a/docs/prod-railway-runtime-secrets.md b/docs/prod-railway-runtime-secrets.md new file mode 100644 index 00000000..4335f3b4 --- /dev/null +++ b/docs/prod-railway-runtime-secrets.md @@ -0,0 +1,31 @@ +# Production Railway Runtime Secrets + +Production Railway connection secrets must be supplied by Railway environment variables, not committed config literals. + +## Required variables + +Primary database: + +- `DB_DSN` +- `DB_USERNAME` +- `DB_PASSWORD` + +Wallet database: + +- `WALLET_DB_DSN` +- `WALLET_DB_USERNAME` +- `WALLET_DB_PASSWORD` + +Redis: + +- `REDIS_HOSTNAME` +- `REDIS_USERNAME` when the Redis service requires a username +- `REDIS_PASSWORD` +- `REDIS_PORT` optional, defaults to `6379` +- `REDIS_DATABASE` optional, defaults to `0` + +Error reporting: + +- `SENTRY_DSN` + +Store the real values in Railway's secret/environment UI. Keep screenshots, support evidence, and PR comments limited to variable names only. diff --git a/environments/prod-railway/common/config/main-local.php b/environments/prod-railway/common/config/main-local.php index f8b14d07..089d31af 100644 --- a/environments/prod-railway/common/config/main-local.php +++ b/environments/prod-railway/common/config/main-local.php @@ -3,9 +3,9 @@ 'components' => [ 'db' => [ 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=mysql.railway.internal;dbname=railway', - 'username' => 'root', - 'password' => 'JImnisvcRDpKLdWpoMECoHHoCbutPhQC', + 'dsn' => getenv('DB_DSN'), + 'username' => getenv('DB_USERNAME'), + 'password' => getenv('DB_PASSWORD'), 'charset' => 'utf8mb4', // Enable Caching of Schema to Reduce SQL Queries 'enableSchemaCache' => true, @@ -16,9 +16,9 @@ ], 'walletDb' => [ 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=mysql-5abl.railway.internal;dbname=railway', - 'username' => 'root', - 'password' => 'mECIXVloEolvFJXnDTcuLGUtvbwzoCgS', + 'dsn' => getenv('WALLET_DB_DSN'), + 'username' => getenv('WALLET_DB_USERNAME'), + 'password' => getenv('WALLET_DB_PASSWORD'), 'charset' => 'utf8', // Enable Caching of Schema to Reduce SQL Queries @@ -34,11 +34,11 @@ ], 'redis' => [ 'class' => 'yii\redis\Connection', - 'hostname' => 'redis.railway.internal', - 'username' => 'default', - 'password' => 'VjCTsdeqMTNwmzBidlzbciDRVceiFXYS', - 'port' => 6379, - 'database' => 0, + 'hostname' => getenv('REDIS_HOSTNAME'), + 'username' => getenv('REDIS_USERNAME') ?: null, + 'password' => getenv('REDIS_PASSWORD'), + 'port' => getenv('REDIS_PORT') ? (int)getenv('REDIS_PORT') : 6379, + 'database' => getenv('REDIS_DATABASE') ? (int)getenv('REDIS_DATABASE') : 0, ],/* 'redis' => [ 'class' => 'yii\redis\Connection', @@ -191,7 +191,7 @@ 'targets' => [ [ 'class' => 'notamedia\sentry\SentryTarget', - 'dsn' => 'https://6cbd2100e1ff41e7875352655ffbf50d:e18336b09d864b29aa12aca3fbc6706c@sentry.io/168200', + 'dsn' => getenv('SENTRY_DSN') ?: null, 'levels' => ['error', 'warning'], 'except' => [ 'yii\web\BadRequestHttpException', diff --git a/tests/check-prod-railway-runtime-secrets.py b/tests/check-prod-railway-runtime-secrets.py new file mode 100644 index 00000000..417f6b71 --- /dev/null +++ b/tests/check-prod-railway-runtime-secrets.py @@ -0,0 +1,63 @@ +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CONFIG = ROOT / "environments/prod-railway/common/config/main-local.php" +DOCS = ROOT / "docs/prod-railway-runtime-secrets.md" + + +def require(condition, message): + if not condition: + raise SystemExit(message) + + +def component_block(config, component): + marker = f"'{component}' => [" + start = config.index(marker) + next_component = re.search(r"\n '[^']+' => \[", config[start + len(marker):]) + end = start + len(marker) + next_component.start() if next_component else len(config) + return config[start:end] + + +config = CONFIG.read_text(encoding="utf-8") +docs = DOCS.read_text(encoding="utf-8") + +expected_vars = [ + "DB_DSN", + "DB_USERNAME", + "DB_PASSWORD", + "WALLET_DB_DSN", + "WALLET_DB_USERNAME", + "WALLET_DB_PASSWORD", + "REDIS_HOSTNAME", + "REDIS_USERNAME", + "REDIS_PASSWORD", + "REDIS_PORT", + "REDIS_DATABASE", + "SENTRY_DSN", +] + +for var in expected_vars: + require(f"getenv('{var}')" in config, f"{var} must be read from the environment.") + require(f"`{var}`" in docs, f"{var} must be documented.") + +for component in ("db", "walletDb"): + block = component_block(config, component) + for key in ("dsn", "username", "password"): + require( + re.search(rf"'{key}'\s*=>\s*getenv\('[A-Z0-9_]+'\)", block), + f"{component}.{key} must be env-backed.", + ) + +redis = component_block(config, "redis") +for key in ("hostname", "password"): + require( + re.search(rf"'{key}'\s*=>\s*getenv\('[A-Z0-9_]+'\)", redis), + f"redis.{key} must be env-backed.", + ) + +log = component_block(config, "log") +require("'dsn' => getenv('SENTRY_DSN') ?: null" in log, "Sentry DSN must be env-backed.") + +print("Production Railway runtime secret check passed.") From f5fcf1cee758afa1129fda4bebc3144bd33e207b Mon Sep 17 00:00:00 2001 From: William Joy Date: Fri, 15 May 2026 22:57:19 -0700 Subject: [PATCH 2/2] Harden runtime secret check component lookup --- tests/check-prod-railway-runtime-secrets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/check-prod-railway-runtime-secrets.py b/tests/check-prod-railway-runtime-secrets.py index 417f6b71..7bf972f6 100644 --- a/tests/check-prod-railway-runtime-secrets.py +++ b/tests/check-prod-railway-runtime-secrets.py @@ -14,8 +14,9 @@ def require(condition, message): def component_block(config, component): marker = f"'{component}' => [" - start = config.index(marker) - next_component = re.search(r"\n '[^']+' => \[", config[start + len(marker):]) + start = config.find(marker) + require(start != -1, f"Component '{component}' must exist in config.") + next_component = re.search(r"\n\s{8}'[^']+'\s*=>\s*\[", config[start + len(marker):]) end = start + len(marker) + next_component.start() if next_component else len(config) return config[start:end]