개요 — 무엇을 하나
보안 진단(Security Diagnostics) 은 등록된 서버들에 루트킷 흔적 점검 + 시스템 하드닝 점검을 실행하고, 그 결과를 0–100 점수 + A–F 등급으로 채점해 한 화면에서 서버 함대(fleet) 전체의 보안 자세를 보여줍니다. 각 점검이 무엇을·왜·어떻게 고치는지 해설을 붙이고, 불합격이면서 함의가 위급(fail + crit)한 항목은 자동으로 문제 해결 케이스로 승격합니다(디둡 + 이후 통과 시 자동 해소).
핵심 구성:
- 프로브
infr_seccheck_probe.cjs— 대상 호스트에서 로컬·읽기 전용으로 도는 Node 스크립트(크리덴셜 불필요). 36개 점검을 수행해 JSON 을 출력. - 정본 카탈로그
App\Support\SecurityCheckbook— 9개 영역·점검 키·가중치·심각도를 소유(로케일 독립, 캐시 안전). 해설 텍스트는resources/lang/{ko,en}/seccheck.php소유. - 엔진
App\Services\Security\DiagnosticsService— 접수 → 채점 → 케이스 승격/해소 → 대시보드 집계. - 저장
infr_sec_scan(마이그레이션000766) — 한 행 = 한 호스트 1회 점검.findings를 JSON 인라인.
프로브를 root 로 실행하면
/etc/shadow·sudoers·sshd -T같은 특권 점검까지 수행합니다. root 가 아니면 특권 점검은skip으로 강등되어 점수 분모에서 제외됩니다(불이익 없음).
언제 쓰나
- 신규 서버를 운영에 편입하기 전 베이스라인 하드닝 검증.
- 침해 의심 시 루트킷·백도어 흔적(숨은 프로세스,
ld.so.preload인젝션, 무차별 모드 NIC 등) 신속 점검. - 정기 컴플라이언스 리듬 — 크론으로 매일 프로브를 돌려 API 리포터로 점수 추이를 자동 축적.
- 감사 대비 — 어느 서버가 EOL OS·미적용 보안 업데이트·root SSH 로그인 허용 상태인지 한눈에.
화면 구성
| 영역 | 내용 |
|---|---|
| KPI 스트립 | 플릿 평균 점수 · 점검 호스트 수 · 미해결 위급 케이스 · 불합격 항목 합계 · 최근 점검 시각 |
| 등급 분포 | A/B/C/D/F 호스트 수 히스토그램 |
| 영역별 점수 | 9개 영역 롤업(전 호스트 평균, 카탈로그 순서, tone 색) |
| 점수 추이 | 최근 60개 스캔 시간순 라인 |
| 호스트별 등급 | 호스트 카드 그리드 — 점수가 낮은(위험) 순 정렬. 각 카드에 등급·pass/warn/fail/crit 카운트·접수 경로·점검 시각. 클릭 → 리포트 모달 |
| 리포트 모달 | 종합 점수·등급·요약(OS/커널) + 점검 상세(무엇/왜/조치/근거) + 승격 케이스 링크 |
상단 도구: [점검 실행](다중 서버 원격 실행) · [결과 붙여넣기] · 새로고침 · 자동 갱신 토글 · 전체화면.
사용법
접수 4트랙
infr_sec_scan.source 는 접수 경로를 4가지로 구분합니다.
| source | 진입점 | 방법 |
|---|---|---|
emulate |
[점검 실행] 모달 | 인스턴스를 골라 원격 프로브 배포·실행. instance_id 를 실은 [붙여넣기]/[remote emulate apply]도 emulate 로 기록 |
paste |
[결과 붙여넣기] 모달 | 대상에서 직접 실행한 프로브 JSON 을 인스턴스 연결 없이 붙여넣기 |
api |
POST /api/v1/security/scan |
각 호스트 크론이 프로브를 실행 → API 키로 자동 푸시(리포터 트랙) |
manual |
폴백 | source 값이 유효하지 않을 때의 기본값 |
1) 다중 서버 원격 실행 — 비동기 폴링
동기 전체 실행은 다수 호스트에서 PHP 실행시간을 초과하므로, 등록 → 워커(fire-and-forget) → 폴링 패턴을 씁니다.
- [점검 실행] → 대상 인스턴스 체크(도달 가능/불가 표시).
POST /security/diagnostics/run→ 진행 레코드 생성 후run_id즉시 반환(Cache, TTL 30분).- 프런트가
POST /security/diagnostics/run-worker/{runId}를 await 없이 호출 → 워커가set_time_limit(0)·ignore_user_abort로 전 호스트를 순차 실행(호스트 하나 실패해도 나머지 계속). - 프런트는
GET /security/diagnostics/run-status/{runId}를 폴링해 진행률(done/total/ok)을 갱신.
도달 불가로 표시된 인스턴스는
run_report수집기 노드와 매칭되지 않아 원격 실행할 수 없습니다. 대상에서 프로브를 직접 실행해 [결과 붙여넣기]로 접수하세요. 원격 실행 배선은 원격 에뮬레이트 참조.
2) 결과 붙여넣기 / JSON 접수
대상 호스트에서 프로브를 실행하고 그 JSON 을 붙여넣습니다.
# 대상 호스트에서(권장: root)
node infr_seccheck_probe.cjs --min
--min 은 1행 JSON 출력입니다. 출력 형태:
{
"host": "web-01",
"generated_at": "2026-08-13T03:30:00Z",
"os": "Rocky Linux 9.3",
"kernel": "5.14.0-362.el9",
"root": true,
"checks": [
{ "key": "ssh_root_login", "category": "ssh", "status": "fail", "severity": "crit", "detail": "PermitRootLogin yes", "evidence": "/etc/ssh/sshd_config:32" },
{ "key": "svc_firewall", "category": "services", "status": "pass", "severity": "crit", "detail": "firewalld active" }
],
"summary": {}
}
POST /security/diagnostics/ingest 로 접수됩니다. 인스턴스를 연결하면 source=emulate, 연결하지 않으면 source=paste 로 기록됩니다.
3) API 리포터(자동화)
각 호스트 크론이 프로브를 실행해 결과를 InfraGuard 로 푸시합니다. API 키는 Administration ▸ API 발급에서 발급하고, 키에 scan.write 스코프가 있어야 합니다.
30 3 * * * node /opt/infr/infr_seccheck_probe.cjs --min | curl -sS -X POST https://infraguard.example.com/api/v1/security/scan -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" --data @-
응답(201):
{ "ok": true, "scan_id": 812, "host": "web-01", "score": 74, "grade": "C" }
채점 방법 — SecurityCheckbook
점수는 각 점검의 가중치와 상태를 합산한 비율입니다.
상태 계수(STATUS_FACTOR) — 이 계수만 점수에 반영되고 skip/info 는 분모에서 제외:
| 상태 | 계수 |
|---|---|
pass 합격 |
1.0 |
warn 주의 |
0.6 |
fail 불합격 |
0.0 |
skip·info |
점수 제외 |
심각도 배수(SEV_MUL) — 불합격 시 함의가 클수록 점수 영향 확대:
| fail_severity | 배수 |
|---|---|
crit 위급 |
1.6 |
warn 주의 |
1.0 |
info 정보 |
0.5 |
항목 가중치 w = 점검weight(1–3) × 심각도배수, 종합 점수 = round(100 × Σ(w × 상태계수) / Σw). 영역별 점수도 같은 공식을 영역 단위로 계산합니다.
등급·톤:
| 점수 | 등급 | 톤 |
|---|---|---|
| ≥ 90 | A | good |
| ≥ 80 | B | good |
| ≥ 70 | C | warn |
| ≥ 60 | D | warn |
| < 60 | F | crit |
(등급은 A/B/C/D/F, 톤은 good ≥ 80 · warn ≥ 60 · crit < 60.)
9개 영역 · 36개 점검
각 영역은 표시 중요도(weight)와 아이콘을 가지며, 실제 채점은 위의 점검별 weight × 심각도배수로 이루어집니다.
| 영역 | 라벨 | 점검 수 | 대표 점검(불합격 시 함의) |
|---|---|---|---|
rootkit |
루트킷·멀웨어 | 6 | rk_known_files·rk_ld_preload·rk_hidden_proc(crit) |
patch |
패치·업데이트 | 3 | pt_security_updates·pt_eol_os(crit) |
ssh |
SSH 설정 | 5 | ssh_root_login·ssh_empty_pass(crit) |
accounts |
계정·권한 | 5 | acc_uid0·acc_empty_pass(crit) |
network |
네트워크 노출 | 3 | net_legacy_services·net_db_exposed(crit) |
filesystem |
파일시스템 권한 | 5 | fs_shadow_perm(crit)·fs_world_writable·fs_suid(warn) |
hardening |
시스템 하드닝 | 5 | hd_selinux·hd_aslr·hd_syncookies(warn) |
services |
서비스·방화벽 | 2 | svc_firewall(crit)·svc_unnecessary(info) |
logging |
감사·로깅 | 2 | log_auditd·log_syslog(warn) |
미등록 key 는 other 영역으로 흡수됩니다. 개별 점검의 "무엇을/왜/조치" 해설은 리포트 모달에서 seccheck.check.* 로 렌더됩니다.
위급 항목 → 문제 해결 케이스 승격
접수마다 reconcileCases() 가 호스트의 케이스를 정합화합니다.
- 승격 대상은
status = failANDseverity = crit인 항목만. 내부fingerprint로 디둡. - 열린 케이스가 있으면
occurrence_count++·last_occurred_at갱신, 없으면 신규 케이스 생성:infr_incident(area=security,source=detection,entity_type=seccheck,entity_key=host,severity=crit,status=open) +infr_incident_event(kind=created). - 자동 해소: 이전엔 열려 있었으나 이번 점검에서 더 이상 fail+crit 이 아닌 케이스 →
status=resolved·resolved_at·자동 해소 코멘트.
승격된 케이스는 리포트 모달의 각 항목에 "문제 해결로 승격됨" 배지 + [케이스 보기] 링크로 나타납니다. 케이스는 Security ▸ 문제 해결 보드에서 다룹니다.
스캔을 삭제해도(
POST /security/diagnostics/delete) 승격된 케이스는 독립적으로 유지됩니다(같이 지워지지 않음).
데이터 소스
| 소스 | 역할 |
|---|---|
infr_sec_scan(000766) |
스캔 결과 저장(호스트별 1회). score·grade·pass/warn/fail/crit/total_count·category_scores·summary·findings JSON. 인덱스 (host,scanned_at)·(instance_id,scanned_at)·grade·score |
infr_instance |
대상 인스턴스(alias/hostname 으로 host → instance_id 해석) |
infr_incident / infr_incident_event |
승격된 위급 케이스와 타임라인 |
infr_api_key |
API 리포터 인증(scope scan.write) |
| Cache | 대시보드 집계 20초 캐시(infr:sec:diag:build) · 비동기 실행 진행 레코드(TTL 1800초) |
infr_seccheck_probe.cjs |
원천 프로브(로컬 read-only) |
- 대시보드 집계 규율: 최근 500 스캔 내에서 호스트별 최신 1건만 dedupe → 플릿 목록·평균·영역 롤업·추이·미해결 위급 계산.
- 프로브의 category/severity 는 힌트일 뿐이며, 서버가
SecurityCheckbook을 정본으로 재정규화합니다.
권한
- 읽기: Security 역할 영역 접근이 필요합니다(현재 티어에서 staff = 5 / admin = 10, 데모 = 1 은 Security 영역 미노출). 대시보드·리포트 조회.
- 쓰기(실행/접수/삭제): 세션
infr_user_level >= 2필요(canWrite()).run·ingest·delete는 미달 시 403(seccheck.toast.forbidden). - API 리포터:
infr.apikey미들웨어 + 키 스코프scan.write. 미달 시 403forbidden_scope. - 감사: 모든 쓰기가
infr_audit_log에 기록 —sec.diag_run(대상 수),sec.diag_ingest(scan_id·score·source),sec.diag_delete(scan id). 감사 로그에서 확인.
레벨 체계 일반은 RBAC 레벨 참조(infr.level:N 미들웨어).
문제해결 · FAQ
Q. [점검 실행]에서 인스턴스가 "도달 불가"로 뜹니다.
해당 인스턴스가 run_report 수집기 노드와 매칭되지 않아 원격 실행 배선이 없는 경우입니다. 대상에서 프로브를 직접 실행하고 [결과 붙여넣기]로 접수하세요.
Q. "점검 항목(checks)이 없습니다" 오류.
붙여넣은 JSON 에 checks(또는 items) 배열이 없거나 각 항목에 key 가 없습니다. infr_seccheck_probe.cjs --min 출력을 그대로 붙여넣으세요.
Q. 점수가 낮은데 fail 개수가 적습니다.
심각도 배수 때문입니다. crit 불합격 1건은 warn 1건보다 점수에 1.6배 크게 반영됩니다. 영역별 점수와 리포트 모달로 어느 항목이 크게 깎는지 확인하세요.
Q. 특권 점검이 전부 skip 입니다.
프로브를 root 가 아닌 계정으로 실행했기 때문입니다. /etc/shadow·sudoers·sshd -T 점검은 root 권한이 필요합니다. skip 은 점수 분모에서 제외되므로 불이익은 없지만, 완전한 진단을 위해 root(또는 게이트웨이 경유)로 실행하세요.
Q. 실행이 오래 걸리는데 브라우저를 닫아도 되나요?
네. 워커는 fire-and-forget 로 돌고 ignore_user_abort 로 요청 중단에도 계속 진행합니다. 다시 열어 run-status 폴링으로 결과를 확인하거나, 완료 후 대시보드에 반영된 등급을 보면 됩니다. 진행 레코드는 30분 보관됩니다.
Q. 케이스가 자동으로 사라졌습니다.
이후 점검에서 해당 항목이 더 이상 fail+crit 이 아니면(수정 완료·항목 미검출) 엔진이 케이스를 resolved 로 자동 해소합니다. 정상 동작입니다.
관련 항목
- Security ▸ 문제 해결 — 승격된 위급 케이스 워크플로우
- 원격 에뮬레이트 — 다중 서버 프로브 배포·실행 배선
- API 발급 — 리포터 트랙 키(scope
scan.write) - Assets ▸ Instances — 대상 OS 인스턴스 대장
- Security ▸ Overview — 방어 퍼널 전체 서사
- 감사 로그 — 실행·접수·삭제 이력
