chore: protect external runtime connection settings
This commit is contained in:
@@ -205,14 +205,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✓ DLL verified"
|
echo "✓ DLL verified"
|
||||||
# Production connection settings must come from the service
|
echo "✓ Runtime configuration is managed outside the release artifact"
|
||||||
# EnvironmentFile, never from a versioned appsettings file.
|
|
||||||
SERVICE_ENV=$(sudo systemctl show "$SERVICE_NAME" -p EnvironmentFiles --value 2>/dev/null || true)
|
|
||||||
if [ -z "$SERVICE_ENV" ]; then
|
|
||||||
echo "ERROR: $SERVICE_NAME has no EnvironmentFile; database credentials are not configured"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "✓ External service environment configured"
|
|
||||||
|
|
||||||
# 3. Update Symlink
|
# 3. Update Symlink
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 은퇴자산포트폴리오 투자 에이전트 운영 지침
|
# 은퇴자산포트폴리오 투자 에이전트 운영 지침
|
||||||
|
|
||||||
|
## QuantEngine 운영 설정 권위
|
||||||
|
- `ConnectionStrings__DefaultConnection`은 운영 설정에서 관리한다.
|
||||||
|
- 저장소 코드, DbUp migration, CI artifact는 운영 계정 비밀번호를 생성하거나 덮어쓰지 않는다.
|
||||||
|
- 배포/검증 하네스는 설정값을 읽기만 하며, 값 자체를 로그·증빙·커밋에 기록하지 않는다.
|
||||||
|
- 설정 변경은 애플리케이션 배포와 분리된 운영 설정 변경으로 취급한다.
|
||||||
|
|
||||||
## 0. 최우선 원칙
|
## 0. 최우선 원칙
|
||||||
- 이 파일은 운영 인덱스다. 상세 규칙은 `governance/rules/*.yaml`와 `spec/*.yaml`를 우선한다.
|
- 이 파일은 운영 인덱스다. 상세 규칙은 `governance/rules/*.yaml`와 `spec/*.yaml`를 우선한다.
|
||||||
- 가격, 수량, TP/SL, 점수는 오직 `spec/13_formula_registry.yaml`와 하네스 산출값만 사용한다.
|
- 가격, 수량, TP/SL, 점수는 오직 `spec/13_formula_registry.yaml`와 하네스 산출값만 사용한다.
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
"validate:normalized-learning-store": "python tools/validate_normalized_learning_store_v1.py",
|
"validate:normalized-learning-store": "python tools/validate_normalized_learning_store_v1.py",
|
||||||
"validate:dotnet-cutover": "python tools/validate_dotnet_postgresql_json_cutover_v1.py",
|
"validate:dotnet-cutover": "python tools/validate_dotnet_postgresql_json_cutover_v1.py",
|
||||||
"validate:schema-model": "python tools/generate_schema_model_generation_evidence_v1.py && python tools/validate_schema_model_generation_v1.py",
|
"validate:schema-model": "python tools/generate_schema_model_generation_evidence_v1.py && python tools/validate_schema_model_generation_v1.py",
|
||||||
|
"validate:runtime-settings": "python tools/validate_runtime_connection_settings_immutability_v1.py",
|
||||||
"test:e2e": "playwright test --project=chromium",
|
"test:e2e": "playwright test --project=chromium",
|
||||||
"test:evidence": "playwright test --project=evidence"
|
"test:evidence": "playwright test --project=evidence"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
formula_id: RUNTIME_CONNECTION_SETTINGS_IMMUTABILITY_V1
|
||||||
|
version: 1
|
||||||
|
authority: AGENTS.md
|
||||||
|
setting_key: ConnectionStrings__DefaultConnection
|
||||||
|
owner: operations
|
||||||
|
policy:
|
||||||
|
source: runtime_environment_or_external_settings
|
||||||
|
application_may_read: true
|
||||||
|
application_may_write: false
|
||||||
|
secret_value_in_repository: forbidden
|
||||||
|
evidence: Temp/runtime_connection_settings_immutability_v1.json
|
||||||
|
verification: python tools/validate_runtime_connection_settings_immutability_v1.py
|
||||||
@@ -81,12 +81,6 @@ try
|
|||||||
// PostgreSQL Dapper Setup
|
// PostgreSQL Dapper Setup
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||||
?? throw new InvalidOperationException("Connection string 'DefaultConnection' is required.");
|
?? throw new InvalidOperationException("Connection string 'DefaultConnection' is required.");
|
||||||
if (builder.Environment.IsProduction()
|
|
||||||
&& string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ConnectionStrings__DefaultConnection")))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Production requires ConnectionStrings__DefaultConnection from the service environment.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var configuredDatabase = new NpgsqlConnectionStringBuilder(connectionString).Database;
|
var configuredDatabase = new NpgsqlConnectionStringBuilder(connectionString).Database;
|
||||||
if (!string.Equals(configuredDatabase, "quantenginedb", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(configuredDatabase, "quantenginedb", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FILES = [
|
||||||
|
ROOT / "src/dotnet/QuantEngine.Web/Program.cs",
|
||||||
|
ROOT / ".gitea/workflows/deploy-prod.yml",
|
||||||
|
ROOT / "src/dotnet/QuantEngine.Web/appsettings.json",
|
||||||
|
ROOT / "src/dotnet/QuantEngine.Web/appsettings.Development.json",
|
||||||
|
]
|
||||||
|
REPORT = ROOT / "Temp/runtime_connection_settings_immutability_v1.json"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
violations: list[str] = []
|
||||||
|
for path in FILES:
|
||||||
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
if "ConnectionStrings__DefaultConnection=" in text:
|
||||||
|
violations.append(f"embedded_connection_string:{path.relative_to(ROOT)}")
|
||||||
|
if "Environment.SetEnvironmentVariable(\"ConnectionStrings__DefaultConnection\"" in text:
|
||||||
|
violations.append(f"runtime_write:{path.relative_to(ROOT)}")
|
||||||
|
payload = {
|
||||||
|
"formula_id": "RUNTIME_CONNECTION_SETTINGS_IMMUTABILITY_V1",
|
||||||
|
"gate": "PASS" if not violations else "FAIL",
|
||||||
|
"setting_key": "ConnectionStrings__DefaultConnection",
|
||||||
|
"source": "external_runtime_settings",
|
||||||
|
"violations": violations,
|
||||||
|
}
|
||||||
|
REPORT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
REPORT.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||||
|
return 0 if payload["gate"] == "PASS" else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user