fix: security, data-integrity, and doc-drift findings from repo audit
Consolidates duplicate KIS API client implementations (governance tests were exercising an unused class instead of the one actually running in production), closes a SQL injection path in the DB admin page, fixes a migration that used MySQL-only syntax and had never actually applied (confirmed against production), resyncs docs/db/quantengine.dbml with all migrations, and removes a duplicate OMS·WMS·ERP frontend tree in favor of src/frontend/. Also corrects several unverifiable/inflated claims in the OMS planning docs and realigns CI/CD and architecture documentation with what's actually in the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
# QuantEngine Deployment Runbook
|
||||
|
||||
Full deployment procedure, extracted from CLAUDE.md (2026-07-30) to keep the main file within
|
||||
the character budget. CLAUDE.md keeps the CRITICAL rules (CI/CD-only mandate, DB secret
|
||||
management); this file has the complete step-by-step runbook.
|
||||
|
||||
**Production Server**: Hetzner Cloud `178.104.200.7` (kjh2064@178.104.200.7)
|
||||
|
||||
Projects on server:
|
||||
1. **TaxBaik** (홈페이지) — Nginx location `/taxbaik`
|
||||
2. **QuantEngine** (데이터 수집/분석) — Nginx location `/quantengine`
|
||||
|
||||
## ⚠️ CRITICAL: CI/CD-Only Deployment Mandate
|
||||
|
||||
**Rule**: ALL production deployments MUST go through Gitea Actions CI/CD. Manual SSH deployments are **FORBIDDEN**.
|
||||
|
||||
**Why**:
|
||||
- Automatic validation (build, health checks, version verification)
|
||||
- Audit trail (all deployments logged in Gitea Actions)
|
||||
- Consistent process (no manual errors)
|
||||
- Rollback safety (deployment history retained)
|
||||
- Release traceability (version control via git tags)
|
||||
|
||||
## ⚠️ CRITICAL: DB Secret Management (Incident 2026-07-12)
|
||||
|
||||
**Incident**: `quant.taxbaik.com/login`이 `28P01 password authentication failed`로 장애 발생.
|
||||
원인: `appsettings.Production.json`에 하드코딩되어 배포된 DB 비밀번호가, 실제 DB 비밀번호가
|
||||
로테이션된 이후에도 계속 옛날 값(심지어 이전 세션에서 검증 없이 넣은 placeholder였던 적도 있음)
|
||||
그대로 배포되고 있었음.
|
||||
|
||||
**Rule**: **DB 접속 문자열(`ConnectionStrings`)은 절대 `appsettings.Production.json`이나
|
||||
워크플로우 파일에 하드코딩하지 않는다.** `prepare-release.yml`이 생성하는
|
||||
`appsettings.Production.json`에는 `Logging` 설정만 있고 `ConnectionStrings`는 없다 —
|
||||
이는 의도된 설계다 (Gitea Release는 누구나 다운로드 가능한 아티팩트이므로 시크릿을
|
||||
담으면 안 됨).
|
||||
|
||||
**실제 DB 비밀번호의 출처**: 프로덕션 서버의 `/home/kjh2064/.config/quantengine.env`
|
||||
파일 (`ConnectionStrings__DefaultConnection=...` 형식) 하나뿐이며,
|
||||
`quantengine.service.d/env.conf` drop-in의 `EnvironmentFile=` 지시자로 systemd가
|
||||
이 값을 환경변수로 주입한다. ASP.NET Core 설정 우선순위상 **환경변수가
|
||||
`appsettings.Production.json`을 오버라이드**하므로, 배포되는 아티팩트 자체에는
|
||||
DB 정보가 없어도 서비스는 정상 동작한다.
|
||||
|
||||
**DB 비밀번호가 바뀌면** (로테이션 등): `/home/kjh2064/.config/quantengine.env` 파일만
|
||||
갱신하고 `sudo systemctl restart quantengine`. 워크플로우 파일이나 Gitea Secrets는
|
||||
건드릴 필요 없음 (배포 파이프라인은 DB 비밀번호를 모른 채로 동작해야 정상).
|
||||
|
||||
**배포 전 체크리스트에 추가**:
|
||||
- ✅ 새 릴리즈 배포 후 반드시 `/Account/Login` 실제 HTTP 응답 + `journalctl -u quantengine`에서
|
||||
`28P01`/`password authentication failed` 부재 확인 (단순 프로세스 `active` 상태만으로는
|
||||
DB 연결 실패를 못 잡음 — ASP.NET Core는 DB 없이도 기동은 되고 로그인 요청 시점에야 실패함)
|
||||
- ✅ `.config/quantengine.env`의 존재와 `quantengine.service.d/env.conf`의
|
||||
`EnvironmentFile=` 배선이 서버에 유지되고 있는지 (systemd unit 자체를 재생성/덮어쓰는
|
||||
배포 방식으로 전환할 경우 이 drop-in이 날아가지 않는지 확인 필요)
|
||||
|
||||
## Production Deployment Strategy (Release-Based)
|
||||
|
||||
**Architecture**: Two-Workflow System (Release Creation → Deployment)
|
||||
|
||||
### Workflow 1: prepare-release.yml (Release Creation)
|
||||
|
||||
**Purpose**: Create a release with built artifact
|
||||
|
||||
**Trigger**: Manual (`workflow_dispatch`)
|
||||
```bash
|
||||
# Visit Gitea Actions and select prepare-release.yml
|
||||
# Input version: v0.1.20260711 (or any semantic version)
|
||||
```
|
||||
|
||||
**What it does**:
|
||||
1. ✓ Build (restore, build, publish)
|
||||
2. ✓ Generate `appsettings.Production.json`
|
||||
3. ✓ Package artifact: `.tar.gz`
|
||||
4. ✓ Create git tag: `v0.1.20260711`
|
||||
5. ✓ Create Gitea Release with artifact attached
|
||||
6. ✓ Notify: Release ready for deployment
|
||||
|
||||
**Output**: Gitea Release with downloadable artifact
|
||||
|
||||
### Workflow 2: deploy-prod.yml (Deployment)
|
||||
|
||||
**Purpose**: Deploy a release to production
|
||||
|
||||
**Trigger**: Manual (`workflow_dispatch`)
|
||||
```bash
|
||||
# Visit Gitea Actions and select deploy-prod.yml
|
||||
# Input release: v0.1.20260711 (optional — uses latest if empty)
|
||||
```
|
||||
|
||||
**What it does**:
|
||||
1. ✓ Fetch Release (from Gitea Releases)
|
||||
2. ✓ Download artifact
|
||||
3. ✓ Verify SSH credentials
|
||||
4. ✓ Upload to production server
|
||||
5. ✓ Extract and symlink
|
||||
6. ✓ Restart service
|
||||
7. ✓ 6-point health checks
|
||||
8. ✓ Report deployment status
|
||||
|
||||
**Deployment Pipeline (5 Stages)**:
|
||||
|
||||
| Stage | Purpose | Timeout |
|
||||
|-------|---------|---------|
|
||||
| 1. Fetch Release | Query Gitea Releases, download artifact | 10min |
|
||||
| 2. Pre-Check | Verify SSH keys, secrets, release | 5min |
|
||||
| 3. Deploy | Upload, extract, symlink, restart service | 30min |
|
||||
| 4. Health Check | 6-point verification (HTTP, CSS, login, service, release, DB auth) | 10min |
|
||||
| 5. Report | Final deployment status | Auto |
|
||||
|
||||
**Health Checks (Automatic)**:
|
||||
- ✓ HTTP 200 on `/Account/Login`
|
||||
- ✓ Login page content verification
|
||||
- ✓ CSS file loads (`/css/admin.css`)
|
||||
- ✓ Service status (systemctl active)
|
||||
- ✓ Release verification (deployed release tag matches)
|
||||
- ✓ **DB authentication check** (`journalctl`에서 `28P01`/`password authentication failed`
|
||||
부재 확인 — GET `/Account/Login`은 DB가 끊겨도 200을 반환하므로 이 체크가 없으면
|
||||
DB 장애를 배포 파이프라인이 놓친다. 2026-07-12 사고 이후 추가됨)
|
||||
|
||||
**Complete Deployment Flow**:
|
||||
```
|
||||
1. Code committed to main branch
|
||||
2. Create release: prepare-release.yml workflow_dispatch (manual)
|
||||
→ Builds code
|
||||
→ Creates Gitea Release with artifact
|
||||
→ Tags repository
|
||||
3. Deploy release: deploy-prod.yml workflow_dispatch (manual)
|
||||
→ Selects release version
|
||||
→ Downloads artifact from Gitea Release
|
||||
→ Deploys to production server
|
||||
→ Runs health checks
|
||||
→ Reports status
|
||||
```
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
**Before creating a release**, verify:
|
||||
1. ✅ Local build: `dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release` (0 errors, 0 warnings)
|
||||
2. ✅ E2E tests pass: `npx playwright test`
|
||||
3. ✅ Admin pages verified (200 status, no 500 errors)
|
||||
4. ✅ All changes committed and pushed to main branch
|
||||
5. ✅ No uncommitted changes: `git status`
|
||||
|
||||
## Release & Deployment Workflow
|
||||
|
||||
**Step 1: Create Release (prepare-release.yml)**
|
||||
```bash
|
||||
# Visit Gitea Actions
|
||||
# https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions
|
||||
|
||||
# Run prepare-release.yml workflow
|
||||
# Input: version = v0.1.20260711
|
||||
|
||||
# Workflow will:
|
||||
# - Build and publish
|
||||
# - Package artifact
|
||||
# - Create git tag
|
||||
# - Create Gitea Release
|
||||
# - Attach artifact
|
||||
```
|
||||
|
||||
**Step 2: Deploy Release (deploy-prod.yml)**
|
||||
```bash
|
||||
# Visit Gitea Actions (same page)
|
||||
# Run deploy-prod.yml workflow
|
||||
# Input: release = v0.1.20260711 (leave empty for latest)
|
||||
|
||||
# Workflow will:
|
||||
# - Download artifact from release
|
||||
# - Deploy to production server
|
||||
# - Run health checks
|
||||
# - Report status
|
||||
```
|
||||
|
||||
## SSH Key Configuration (Required)
|
||||
|
||||
**Setup (One-time)**:
|
||||
1. Generate ED25519 key locally (or reuse existing):
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/quantengine_deploy -C "QuantEngine CI/CD"
|
||||
```
|
||||
|
||||
2. Add public key to production server:
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/quantengine_deploy.pub kjh2064@178.104.200.7
|
||||
```
|
||||
|
||||
3. Get private key in base64 format:
|
||||
```bash
|
||||
# macOS/Linux
|
||||
base64 -w 0 ~/.ssh/quantengine_deploy > /tmp/key_b64.txt
|
||||
cat /tmp/key_b64.txt | pbcopy
|
||||
|
||||
# Or Windows PowerShell
|
||||
$key = Get-Content ~/.ssh/quantengine_deploy -Raw
|
||||
[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($key)) | Set-Clipboard
|
||||
```
|
||||
|
||||
4. Configure in Gitea:
|
||||
- URL: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/settings/secrets
|
||||
- Add secret: `DEPLOY_SSH_KEY_B64` (base64-encoded private key)
|
||||
- Or: `DEPLOY_SSH_KEY` (raw PEM format)
|
||||
- Also add: `GITEA_TOKEN` (for release API access)
|
||||
- Generate at: https://gitea.taxbaik.com/user/settings/applications
|
||||
- Required permissions: `repo` + `read:actions`
|
||||
|
||||
## Deployment Monitoring
|
||||
|
||||
**During Deployment**:
|
||||
- Watch live in Gitea Actions UI
|
||||
- Jobs complete in order: Build → Pre-Check → Deploy → Health Check → Report
|
||||
|
||||
**After Deployment**:
|
||||
```bash
|
||||
# SSH into server
|
||||
ssh kjh2064@178.104.200.7
|
||||
|
||||
# Check active deployment
|
||||
readlink ~/quantengine_active
|
||||
|
||||
# View service status
|
||||
systemctl status quantengine
|
||||
|
||||
# Tail live logs
|
||||
journalctl -u quantengine -f
|
||||
|
||||
# Health check
|
||||
curl -I http://127.0.0.1:5000/Account/Login
|
||||
```
|
||||
|
||||
## Automatic Rollback (if health check fails)
|
||||
|
||||
If health check fails, deployment stops automatically:
|
||||
1. Service restart may fail
|
||||
2. Symlink update reverts to previous deployment
|
||||
3. Gitea Actions marks deployment as FAILED
|
||||
4. Logs include failure details
|
||||
|
||||
Manual rollback (if needed):
|
||||
```bash
|
||||
# List deployments
|
||||
ls -lht ~/deployments/quantengine_*
|
||||
|
||||
# Revert symlink to previous version
|
||||
ln -sfn /home/kjh2064/deployments/quantengine_YYYYMMDD_HHMMSS_COMMIT ~/quantengine_active
|
||||
|
||||
# Restart service
|
||||
sudo systemctl restart quantengine
|
||||
|
||||
# Verify
|
||||
curl http://127.0.0.1:5000/Account/Login
|
||||
```
|
||||
|
||||
## Troubleshooting Deployment Failures
|
||||
|
||||
**Issue**: Build fails
|
||||
- Check: `dotnet build` locally first
|
||||
- Ensure: No compilation errors, 0 warnings
|
||||
|
||||
**Issue**: Health check timeout
|
||||
- Check: Service logs: `journalctl -u quantengine -n 50`
|
||||
- Check: Port 5000 listening: `ss -tlnp | grep 5000`
|
||||
- Check: DB connectivity in appsettings.Production.json
|
||||
|
||||
**Issue**: SSH key error
|
||||
- Verify: `DEPLOY_SSH_KEY_B64` or `DEPLOY_SSH_KEY` in Gitea Secrets
|
||||
- Check: Public key added to `~/.ssh/authorized_keys` on server
|
||||
- Test: `ssh -i ~/.ssh/key_file kjh2064@178.104.200.7 echo OK`
|
||||
|
||||
## Git Repository
|
||||
|
||||
**Gitea Server** (동일 호스트):
|
||||
- **HTTP**: `https://gitea.taxbaik.com/kjh2064/QuantEngineByItz.git`
|
||||
- **SSH**: `ssh://git@gitea.taxbaik.com:2222/kjh2064/QuantEngineByItz.git`
|
||||
|
||||
## Active Gitea Workflows (summary)
|
||||
|
||||
1. **prepare-release.yml** — Release creation (workflow_dispatch only)
|
||||
- Build → Publish → Package → Tag → Gitea Release
|
||||
- Does NOT write ConnectionStrings into the artifact (see DB Secret Management above) —
|
||||
only `Logging` config ships in `appsettings.Production.json`
|
||||
2. **deploy-prod.yml** — Production deployment (workflow_dispatch only, takes a release tag)
|
||||
- 5 stages: Fetch Release → Pre-Check → Deploy → Health Check → Report
|
||||
- 6-point health checks (HTTP, login page, CSS, service, release, DB auth)
|
||||
- SSH-based deployment with artifact validation
|
||||
3. **ci.yml** — PR validation (on:pull_request), 29 validators, runs on every pull request
|
||||
|
||||
**Accessing Gitea Actions**:
|
||||
- Web UI: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions
|
||||
- Runs API: https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs
|
||||
|
||||
## API Monitoring (CLI)
|
||||
|
||||
Monitor deployment status from command line:
|
||||
|
||||
```powershell
|
||||
# Setup (one-time)
|
||||
$env:GITEA_TOKEN_TAXBAIK = "your_gitea_personal_token"
|
||||
|
||||
# List recent deployment runs
|
||||
$token = $env:GITEA_TOKEN_TAXBAIK
|
||||
$response = Invoke-WebRequest `
|
||||
-Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs?limit=5" `
|
||||
-Headers @{ "Authorization" = "token $token" }
|
||||
($response.Content | ConvertFrom-Json).workflow_runs | ForEach-Object {
|
||||
Write-Host "Run #$($_.id): $($_.display_title) [$($_.conclusion)]"
|
||||
}
|
||||
|
||||
# Get specific run details
|
||||
$run_id = 1234 # Replace with actual run ID
|
||||
$response = Invoke-WebRequest `
|
||||
-Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs/$run_id" `
|
||||
-Headers @{ "Authorization" = "token $token" }
|
||||
$run = $response.Content | ConvertFrom-Json
|
||||
Write-Host "Commit: $($run.head_sha)"
|
||||
Write-Host "Status: $($run.status) / $($run.conclusion)"
|
||||
```
|
||||
|
||||
See `docs/GITEA_ACTIONS_API_GUIDE.md` for the complete API reference.
|
||||
|
||||
## Deployment Secrets Configuration
|
||||
|
||||
**Required Secrets** (Gitea Repository Settings → Secrets):
|
||||
|
||||
| Secret | Type | Purpose |
|
||||
|--------|------|---------|
|
||||
| `DEPLOY_SSH_KEY_B64` | Base64 (recommended) | ED25519 private key for SSH |
|
||||
| `DEPLOY_SSH_KEY` | PEM (alternative) | Raw private key format |
|
||||
| `DEPLOY_HOST` | Text | Production server IP (178.104.200.7) |
|
||||
| `DEPLOY_USER` | Text | SSH username (kjh2064) |
|
||||
|
||||
**How to add secrets**:
|
||||
1. Go to: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/settings/secrets
|
||||
2. Click "Add Secret"
|
||||
3. Name: `DEPLOY_SSH_KEY_B64`
|
||||
4. Value: `base64 -w 0 ~/.ssh/deploy_key | pbcopy` (macOS) or `certutil -encode deploy_key deploy_key.b64` (Windows)
|
||||
5. Save
|
||||
Reference in New Issue
Block a user