--- name: manager-security description: > GUARDiA Manager의 인증, 권한 제어, 보안 설정을 구현하는 스킬. GUARDiA ITSM JWT 재활용, React Route Guard, API Key 관리 UI, 감사 로그 시각화를 포함한다. 트리거: 인증 구현, 로그인 페이지, Route Guard, JWT 검증, API Key 발급/관리, 감사 로그 화면, 보안 설정 요청 시. --- # GUARDiA Manager 보안 구현 스킬 ## 인증 아키텍처 GUARDiA Manager는 별도 인증 서버 없이 **GUARDiA ITSM JWT를 공유**한다. ``` 1. 사용자 → Manager 로그인 페이지 2. POST /api/auth/login (GUARDiA ITSM 8001) 3. JWT 수신 → sessionStorage 저장 (localStorage 금지) 4. 이후 모든 요청: Authorization: Bearer {token} 5. GUARDiA ITSM과 Manager Backend 양쪽에서 동일 JWT 검증 ``` ## useAuth 훅 ```typescript // frontend/src/hooks/useAuth.ts import { useState, useEffect, createContext, useContext } from 'react'; import { guardiaApi } from '../api/guardiaClient'; interface AuthState { user: { username: string; role: string; display_name: string } | null; token: string | null; loading: boolean; } export function useAuth() { const [state, setState] = useState({ user: null, token: sessionStorage.getItem('guardia_token'), loading: true }); useEffect(() => { const token = sessionStorage.getItem('guardia_token'); if (!token) { setState(s => ({ ...s, loading: false })); return; } guardiaApi.get('/api/auth/me') .then(res => setState({ user: res.data, token, loading: false })) .catch(() => { sessionStorage.removeItem('guardia_token'); setState({ user: null, token: null, loading: false }); }); }, []); const login = async (username: string, password: string) => { const res = await guardiaApi.post('/api/auth/login', new URLSearchParams({ username, password }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); const { access_token, user } = res.data; sessionStorage.setItem('guardia_token', access_token); setState({ user, token: access_token, loading: false }); return user; }; const logout = () => { sessionStorage.removeItem('guardia_token'); setState({ user: null, token: null, loading: false }); window.location.href = '/login'; }; return { ...state, login, logout }; } ``` ## Route Guard 컴포넌트 ```tsx // components/common/ProtectedRoute.tsx import { Navigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../hooks/useAuth'; const ADMIN_ONLY_PATHS = [ '/api-keys', '/config', '/config/env', '/config/nginx' ]; export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { user, token, loading } = useAuth(); const location = useLocation(); if (loading) return
인증 확인 중...
; if (!token || !user) return ; // admin 전용 페이지 접근 제어 if (ADMIN_ONLY_PATHS.some(p => location.pathname.startsWith(p)) && user.role !== 'admin') { return
관리자 권한이 필요합니다.
; } return <>{children}; } ``` ## API Key 관리 화면 (M-05) NCloud 콘솔의 "API 인증키 관리" 화면을 참조: - 테이블: 키 이름, 스코프, 마지막 사용, 만료일, 상태 - 발급: 슬라이드 패널 → 이름/스코프/IP 제한 입력 → 발급 시 키 1회 표시 - 비활성화: 확인 모달 → `DELETE /api/external/keys/{id}` ```tsx // pages/ApiKeys.tsx 핵심 구조 export function ApiKeys() { const { data, loading, refetch } = useGuardiaApi('/api/external/keys'); const [creating, setCreating] = useState(false); const [newKey, setNewKey] = useState(null); const handleCreate = async (form: { name: string; scopes: string; expires_days: number }) => { const res = await guardiaApi.post('/api/external/keys', form); setNewKey(res.data.api_key); // 1회만 노출 refetch(); }; return ( <> {/* 발급된 키 1회 노출 모달 */} {newKey && (

⚠️ API Key가 발급되었습니다 — 지금만 확인 가능합니다

{newKey}
)} }, { key: 'expires_at', header: '만료일' }, ]} data={data ?? []} loading={loading} actions={} selectable /> ); } ``` ## 감사 로그 화면 (M-05) ```tsx // pages/AuditLog.tsx // - 타임라인 형태로 감사 이벤트 표시 (NCloud "활동 로그" 참조) // - 필터: 날짜 범위, 사용자, 액션 유형 // - 데이터: GET /api/audit?page=0&size=50 // - IP Hash 표시 (원본 IP는 ITSM 보안 정책상 제공 안 함) ``` ## Manager Backend JWT 검증 (core/auth.py) ```python # backend/core/auth.py import os from fastapi import Depends, HTTPException from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt SECRET = os.environ.get("GUARDIA_JWT_SECRET", "guardia-jwt-secret-2026-change-me!") ALGORITHM = "HS256" oauth2 = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) async def verify_guardia_token(token: str = Depends(oauth2)): if not token: raise HTTPException(status_code=401, detail="인증이 필요합니다.") try: payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM]) return payload except JWTError: raise HTTPException(status_code=401, detail="유효하지 않은 토큰입니다.") ``` ## 보안 체크리스트 - [ ] localStorage에 토큰 저장 금지 (sessionStorage 사용) - [ ] API Key, 비밀번호 화면 표시 마스킹 (`****`) - [ ] 클립보드 복사 후 즉시 메모리 해제 - [ ] admin 전용 라우트 보호 완료 - [ ] CORS 출처 명시적 설정 (wildcard 금지) - [ ] 모든 시스템 제어 API에 admin 역할 검증