From 0df5d2d31cca3c8eed97fe0dff6769cf0d78e288 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sat, 27 Jun 2026 01:42:48 +0900 Subject: [PATCH] docs: harden ops guidance and CI smoke test --- .gitea/workflows/deploy.yml | 3 +- CLAUDE.md | 56 +++++++++++++++++++++++++++++++++++-- DEPLOYMENT_GUIDE.md | 5 ++-- README.md | 1 + 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index efededb..dcf73e1 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -66,9 +66,10 @@ jobs: sleep 5 HOME_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5001/taxbaik/ || echo "000") LOGIN_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5001/taxbaik/admin/login || echo "000") + ADMIN_TEST_PASSWORD="${{ secrets.TAXBAIK_ADMIN_TEST_PASSWORD }}" AUTH_BODY=$(curl -s -X POST http://127.0.0.1:5001/taxbaik/api/auth/login \ -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' || echo "") + -d "{\"username\":\"admin\",\"password\":\"${ADMIN_TEST_PASSWORD}\"}" || echo "") echo "Home Status: $HOME_STATUS" echo "Login Status: $LOGIN_STATUS" echo "Auth Body: $AUTH_BODY" diff --git a/CLAUDE.md b/CLAUDE.md index f57bc0c..bd95869 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,12 @@ TaxBaik.Web ASP.NET Core 앱 (포트 5001) - 관리자: `/taxbaik/admin` (Blazor Server) - 로그인: `/taxbaik/admin/login` +**운영 원칙:** +- 단일 앱, 단일 서비스, 단일 배포 경로를 유지한다. +- 운영 변경은 코드 또는 CI에서만 반영한다. +- 서버에 임시 수동 수정이나 파일 드리프트가 생기지 않도록 한다. +- 공개 사이트와 관리자 UI는 같은 앱에서 처리하되, 보안 경계는 인증과 권한으로 분리한다. + ### 2.2 계층 책임 - **Domain**: 비즈니스 규칙, 엔티티 정의 - **Infrastructure**: DB 접근, Dapper 구현체, 마이그레이션 실행 @@ -65,6 +71,11 @@ TaxBaik.Web ASP.NET Core 앱 (포트 5001) - 복잡한 조인, 페이징, 성능 제어 용이 - EF Core 대비 SQL 완전 제어 가능 +**왜 이 운영 모델인가?** +- 운영 복잡도를 낮춰 장애 포인트를 줄인다. +- 배포를 CI로 고정하면 서버 간 상태 드리프트를 줄인다. +- 민감 정보는 코드/문서/로그에 남기지 않고 환경 변수와 서버 비밀 저장소에만 둔다. + --- ## 3. 로컬 개발 환경 설정 @@ -139,6 +150,11 @@ dotnet run **중요**: 로컬 appsettings.json은 버전 관리에서 제외 또는 .local suffix 사용 +**보안 규칙**: +- `appsettings.Production.json`에는 비밀값을 두지 않는다. +- JWT Secret, DB 비밀번호, 외부 API 키는 환경 변수 또는 서버 전용 비밀 경로에서만 읽는다. +- 값이 비어 있으면 조용히 넘어가지 말고 시작 시 즉시 실패시킨다. + ```bash # 로컬 오버라이드 appsettings.Development.json # gitignore에 추가 @@ -168,6 +184,10 @@ CREATE TABLE IF NOT EXISTS new_table ( - 테스트 블로그 포스트 5개 - 테스트 카테고리 5개 +**운영 보안 주의**: +- 시드 계정은 운영 초기화용이다. 배포 후에는 반드시 별도 강한 비밀번호로 교체한다. +- 테스트 계정이 운영에 남아 있으면, 배포 후 즉시 비밀번호 재설정 또는 계정 비활성화를 수행한다. + 수동 추가: ```sql -- Admin 추가 @@ -209,6 +229,11 @@ git push "http://kjh2064:${token}@localhost:3000/kjh2064/taxbaik.git" master - ✅ 안전 (token은 로컬 루프백) - ✅ 신뢰성 높음 +**보안 규칙**: +- 토큰은 채팅/문서/스크린샷에 붙이지 않는다. +- push URL에 토큰이 남아 있으면 즉시 제거한다. +- 가능하면 SSH key 기반 인증을 우선 사용한다. + #### 방법 B: SSH로 직접 Push (SSH key 필요) ```bash @@ -261,10 +286,13 @@ ssh kjh2064@178.104.200.7 - 로컬 또는 서버에서 수동 `dotnet publish`로 운영 배포하지 않는다 - `rsync`로 직접 아티팩트를 올리지 않는다 - 배포 실패 시 CI 로그를 먼저 본다 +- 배포된 아티팩트는 CI가 만든 것만 신뢰한다 +- 배포 후 검증은 홈, 관리자 로그인 페이지, 로그인 API를 모두 포함한다 **롤백**: - 이전 정상 커밋을 `master`에 revert 또는 hotfix로 되돌린다 - 서버 파일을 수동으로 복구하지 않는다 +- 롤백은 커밋 단위로 추적 가능해야 한다 ### 3.4 서비스 파일 위치 ``` @@ -298,6 +326,11 @@ location /taxbaik { **참고**: 단일 `/taxbaik` 블록이 공개 사이트와 관리자(Blazor WebSocket)를 모두 처리합니다. 운영은 `5001` 통합 앱 기준이며, 설정 반영은 CI 배포로만 수행한다. +**Nginx 보안**: +- `Upgrade` 헤더는 Blazor WebSocket 경로에만 허용하고, 필요 없는 location에는 넣지 않는다. +- `Host`와 `X-Forwarded-Proto`는 유지해 원본 URL과 스킴을 보존한다. +- `/taxbaik/admin`는 robots.txt에서 차단한다. + --- ## 6. 데이터베이스 @@ -311,6 +344,12 @@ Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Usernam **절대 appsettings.Production.json에 비밀값을 하드코딩하지 말 것.** +**운영 보안 규칙**: +- DB 계정은 애플리케이션 전용 최소 권한으로 둔다. +- 관리자 비밀번호는 bcrypt로 해시하고, 평문 저장/전송을 금지한다. +- `PasswordHash`는 null이 되면 안 되며, null이면 인증 실패로 즉시 처리한다. +- 로그인 실패 로그는 사용자 이름만 남기고 비밀번호/해시를 절대 남기지 않는다. + ### 3.2 Dapper 사용 패턴 **DbConnectionFactory.cs**: @@ -342,6 +381,7 @@ public async Task GetBySlugAsync(string slug, CancellationToken ct) - 항상 `@ParameterName` 파라미터 사용 (SQL injection 방지) - 절대 문자열 연결 금지 - PostgreSQL `snake_case` 컬럼은 Dapper underscore 매핑을 전제로 함 +- 조회 쿼리는 필요한 컬럼만 명시한다. `SELECT *`는 스키마 변경 시 매핑 사고를 만든다. ### 3.3 마이그레이션 @@ -495,6 +535,11 @@ builder.Services.AddAuthorizationCore(); 토큰은 localStorage에 저장되며, `CustomAuthenticationStateProvider`가 자동으로 복원: +**보안 규칙**: +- JWT 만료 시간을 짧고 명확하게 유지한다. +- localStorage 토큰은 XSS가 없다는 전제 없이 다뤄야 한다. +- 관리자 기능은 `[Authorize]`로 감싸고, 클라이언트 렌더링만으로 권한을 믿지 않는다. + ```csharp // CustomAuthenticationStateProvider.cs public async Task LoginAsync(string token) @@ -578,6 +623,9 @@ Admin 로그인 페이지만 [AllowAnonymous]: - [x] 카테고리 목록 캐시 (IMemoryCache, 10분 유효) - [x] 비밀값은 환경 변수에서 읽기 - [x] `[ValidateAntiForgeryToken]` POST 메서드에 추가 +- [x] 운영 배포는 CI-only +- [x] 관리자 로그인은 서버에서 직접 bypass하지 않기 +- [x] DB/인증 문제는 로그와 쿼리로 먼저 확인 ### DON'T ❌ - [ ] 비밀값을 appsettings.Production.json에 하드코딩 @@ -589,6 +637,9 @@ Admin 로그인 페이지만 [AllowAnonymous]: - [ ] robots.txt에서 `/taxbaik/admin` allow 금지 (disallow 필수) - [ ] 폼 제출 후 redirect (fire-and-forget 또는 same-page 응답) - [ ] 절대 `Thread.Sleep` 또는 `Task.Delay` in request handler +- [ ] 운영 서버에서 수동 publish/rsync/파일 교체 +- [ ] 비밀번호/토큰을 로그에 출력 +- [ ] `SELECT *`로 인증/권한 테이블 조회 --- @@ -651,7 +702,7 @@ dotnet build TaxBaik.sln ssh kjh2064@178.104.200.7 # DB 확인 -psql -U kjh2064 -d taxbaikdb -c "\dt" +psql -U taxbaik -d taxbaikdb -c "\dt" # 서비스 상태 (통합 Web 앱만) systemctl status taxbaik @@ -672,7 +723,7 @@ curl -X POST http://178.104.200.7/taxbaik/contact \ # 관리자 DB에서 확인 ssh kjh2064@178.104.200.7 -psql -U kjh2064 -d taxbaikdb +psql -U taxbaik -d taxbaikdb SELECT * FROM inquiries ORDER BY created_at DESC LIMIT 1; ``` @@ -687,6 +738,7 @@ SELECT * FROM inquiries ORDER BY created_at DESC LIMIT 1; | 404 /taxbaik | Nginx 설정 재로드: `sudo nginx -t && sudo systemctl reload nginx` | | Blazor WebSocket 안 됨 | `/taxbaik/admin` 경로에 Upgrade 헤더 필요 (Nginx 설정 확인) | | 배포 후 503 | 서비스 시작 대기 (startup 시간 ~5초), `systemctl status taxbaik` 확인 | +| 로그인 실패 | `admin_users.password_hash`와 bcrypt 해시, `AuthService` 로그, `/api/auth/login` 응답 확인 | --- diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index 0771847..eb8ef3b 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -57,6 +57,7 @@ sudo systemctl reload nginx - `DEPLOY_USER`: `kjh2064` - `DEPLOY_HOST`: `178.104.200.7` - `DEPLOY_SSH_KEY`: SSH 개인키 (줄바꿈 포함) + - `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호 2. 배포 워크플로우는 자동으로 실행: ``` @@ -153,7 +154,6 @@ tail -f /var/log/nginx/access.log | grep taxbaik # 애플리케이션 로그 journalctl -u taxbaik -f -journalctl -u taxbaik -f ``` ## 트러블슈팅 @@ -171,7 +171,7 @@ journalctl -u taxbaik -f - **username**: `admin` - **password**: `admin123` (bcrypt 해시됨) -- 초기 로그인 후 비밀번호 변경 권장 +- 초기 로그인 후 비밀번호 즉시 변경 권장 ### 블로그 포스트 @@ -189,3 +189,4 @@ V003 마이그레이션에서 5개 포스트 자동 생성: - [ ] 관리자 인증 로직 구현 (현재는 플레이스홀더) - [ ] 블로그 포스트 CRUD 기능 완성 - [ ] Naver/Google Search Console 등록 +- [ ] 운영 관리자 비밀번호를 초기 시드값에서 교체하고 `TAXBAIK_ADMIN_TEST_PASSWORD` 갱신 diff --git a/README.md b/README.md index b8718b8..1a02b14 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ master 브랜치에 푸시하면 자동으로: - `DEPLOY_USER`: kjh2064 - `DEPLOY_HOST`: 178.104.200.7 - `DEPLOY_SSH_KEY`: SSH 개인키 (줄바꿈 포함) +- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호 수동 배포는 사용하지 않습니다. 실패 시 [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)의 CI 점검 절차를 따릅니다.