- 37개 파일 IP → zioinfo.co.kr 치환 (소스/매뉴얼/설정/하네스) - Manager DrConsole/NetworkConsole/CsapConsole 빌드 + /var/www/manager/ 배포 - 테스트: Manager HTTP 200, ITSM 신규 API 7개 전체 200 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.6 KiB
6.6 KiB
| name | description |
|---|---|
| manager-security | 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 훅
// 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<AuthState>({
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 컴포넌트
// 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 <div style={{ padding: 40 }}>인증 확인 중...</div>;
if (!token || !user) return <Navigate to="/login" state={{ from: location }} replace />;
// admin 전용 페이지 접근 제어
if (ADMIN_ONLY_PATHS.some(p => location.pathname.startsWith(p))
&& user.role !== 'admin') {
return <div style={{ padding: 40, color: '#ef4444' }}>
관리자 권한이 필요합니다.
</div>;
}
return <>{children}</>;
}
API Key 관리 화면 (M-05)
NCloud 콘솔의 "API 인증키 관리" 화면을 참조:
- 테이블: 키 이름, 스코프, 마지막 사용, 만료일, 상태
- 발급: 슬라이드 패널 → 이름/스코프/IP 제한 입력 → 발급 시 키 1회 표시
- 비활성화: 확인 모달 →
DELETE /api/external/keys/{id}
// pages/ApiKeys.tsx 핵심 구조
export function ApiKeys() {
const { data, loading, refetch } = useGuardiaApi<APIKey[]>('/api/external/keys');
const [creating, setCreating] = useState(false);
const [newKey, setNewKey] = useState<string | null>(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 && (
<div className="one-time-key-modal">
<h4>⚠️ API Key가 발급되었습니다 — 지금만 확인 가능합니다</h4>
<code style={{ userSelect: 'all', background: '#f0f2f5', padding: '10px 14px',
borderRadius: 6, display: 'block', wordBreak: 'break-all' }}>{newKey}</code>
<button onClick={() => { navigator.clipboard.writeText(newKey); }}>복사</button>
<button onClick={() => setNewKey(null)}>확인 완료</button>
</div>
)}
<DataTable
columns={[
{ key: 'name', header: '키 이름' },
{ key: 'scopes', header: '권한' },
{ key: 'use_count', header: '사용 횟수' },
{ key: 'is_active', header: '상태',
render: r => <StatusBadge status={r.is_active ? 'running' : 'stopped'} /> },
{ key: 'expires_at', header: '만료일' },
]}
data={data ?? []}
loading={loading}
actions={<button onClick={() => setCreating(true)}>+ API Key 발급</button>}
selectable
/>
</>
);
}
감사 로그 화면 (M-05)
// pages/AuditLog.tsx
// - 타임라인 형태로 감사 이벤트 표시 (NCloud "활동 로그" 참조)
// - 필터: 날짜 범위, 사용자, 액션 유형
// - 데이터: GET /api/audit?page=0&size=50
// - IP Hash 표시 (원본 IP는 ITSM 보안 정책상 제공 안 함)
Manager Backend JWT 검증 (core/auth.py)
# 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 역할 검증