
KISA 주요정보통신기반시설 기술적 취약점 분석·평가 가이드를 기준으로 UNIX · WEB · DBMS 서버의 취약점을 자동으로 점검하고, 위험도에 따라 자동조치 또는 관리자 승인조치까지 수행하는 플랫폼을 만들었다. 프로젝트 이름은 SSAP(System Security Automation Program). 2026년 8월 18일부터 31일까지 2주 동안 진행한 팀 프로젝트다.
- GitHub: moonimax/KISA-security-audit-automation
- 원본 저장소: fkrdud1125/SSAP
배경 선정 이유
정보보호 시장은 약 3.45조 원에서 4.01조 원으로 16.1% 성장했고, ISMS-P 실증 심사 강화나 2027년 정보보호 공시 의무화처럼 규제도 점점 강해지고 있다. 해외에서도 미국 CISA의 Cyber Hygiene Services, 영국 NCSC의 Active Cyber Defence처럼 취약점을 상시 점검하고 바로 조치하도록 돕는 체계가 운영되고 있고, 국내에는 KISA의 보안 취약점 클리닝 서비스가 있다.
그런데 실제 서버 취약점 진단은 여전히 사람이 서버마다 접속해서 점검하는 경우가 많다. 이 방식에는 분명한 한계가 있다.
| 서버마다 직접 접속해 반복 점검 | KISA 기준 점검 항목 자동화 |
| 점검 결과를 수작업으로 수집 | Ubuntu · Rocky Linux 환경 자동 식별 |
| 점검자마다 판단 기준이 다름 | 서버 일괄 등록, Ansible 다중 서버 병렬 점검 |
| 반복 작업으로 인한 시간 낭비 | UNIX · WEB · DBMS 항목을 서버/IP 단위로 선택 점검 |
| 조치 결과와 이력 관리가 어려움 | 위험도별 자동조치·승인조치, 결과 시각화와 보고서 자동 생성 |
점검 대상 목표
| UNIX | U-01 ~ U-67 | 계정, 파일·디렉터리, 서비스, 패치, 로그 관리 | Ubuntu 24.04 / 26.04, Rocky Linux 9 / 10 |
| WEB | WEB-01 ~ WEB-26 | 계정, 서비스, 보안 설정, 패치 및 로그 관리 | 웹 서버 |
| DBMS | D-01 ~ D-26 | 계정, 접근, 옵션, 패치 관리 | MySQL 8.x |
기술 스택은 Ansible, Shell Script, SSH CA(ed25519), FastAPI, MySQL, Vanilla JS(HTML/CSS)
서버 간 연결은 Tailscale 가상 네트워크로 구성했다.
전체 구조
[아키텍처]

관리자가 대시보드에서 점검 대상을 고르면 FastAPI 백엔드가 작업을 만들고, 진단 영역별 Ansible 플레이북을 실행한다. 대상 서버에서 나온 JSON 결과는 MySQL에 쌓이고, 대시보드와 보고서는 이 DB를 기준으로 만들어진다.
대상 서버에서 실행되는 과정은 네 단계다.
[흐름도]

- 배포: 점검·조치 스크립트를 패키징해서 배포한다. 최신 버전이 이미 있으면 재배포를 건너뛴다.
- 점검 + 자동조치: 읽기 전용으로 점검하고, 자동조치 대상만 바로 수정한다.
- 승인조치 + 재점검: 관리자가 승인한 항목만 조치하고, 같은 판정 로직으로 다시 점검한다.
- 증적 보존: 조치 전후 증적을 남기고 보고서를 만든다.
핵심 설계 1 — 세 영역이 같은 구조를 쓴다
UNIX · WEB · DBMS를 각각 독립된 Ansible 프로젝트로 두되, 폴더 규칙은 똑같이 맞췄다.
unix/ · web/ · db/
check/ 읽기 중심 점검 스크립트
fix/ 취약 설정 조치 스크립트
lib/ 공통 Shell 함수 · 실행기 (run_checks.sh)
playbooks/ 배포 · 점검 · 조치 흐름
inventory/ 대상 호스트 · 영역 변수
tools/ 배포 릴리스 생성
이렇게 파일을 분리해두니 새 진단 영역을 추가하기 쉽고, 기존 영역도 각자 따로 배포하고 검증할 수 있었다.
핵심 설계 2 — 결과는 하나의 JSON 스키마로
진단 영역이 달라도 결과 필드 구조는 똑같다. DB, 백엔드, 프론트엔드, 채점 로직이 모두 이 스키마 하나를 공유하기 때문에 영역별로 같은 기능을 세 번 만들 필요가 없었다.
| code | WEB-04 | 코드 접두사로 진단 영역 구분 |
| title | 디렉터리 리스팅 비활성화 | 항목명 |
| status | 양호 · 취약 · fail | 판정 결과 |
| action | 점검 · 조치 | 호출 단계 |
| detail | manager 계정 없음 — 양호 | 판정 근거 |
| os_type | ubuntu 26.04 | OS · 서비스 버전 |
| timestamp | 2026-08-26T10:20:07+09:00 | 실행 시각 |
| action_tag | 자동조치 · 승인요청 | 조치 권한 게이트 |
| impact | 재접속 정보 갱신 필요 | 조치 시 예상 영향 |
| severity | 상 · 중 · 하 | 채점 가중치 (10 · 8 · 6) |
| release_hash | e28e739c… | 배포 릴리스 버전 (SHA-256) |
| duration_seconds | 1 | 실행 소요 시간 |
보안 점수는 severity 가중치를 반영해 100점 만점으로 환산한다.
핵심 설계 3 — 자동조치와 승인조치를 나눴다
모든 취약점을 자동으로 고치면 편하겠지만, SSH 설정처럼 잘못 바꾸면 관리자가 서버에 못 들어가게 되는 항목도 있다. 그래서 위험도를 기준으로 둘을 나눴다.
| 서비스 영향 | 영향 없이 적용 | 접속 · 세션에 영향 가능 |
| 변경 위험 | 저위험 · 가역적 | 고위험 · 비가역적 |
| 운영 상태 | 정상 운영 중 실행 | 장애 가능성 사전 검토 |
| 판단 · 복구 | 즉시 롤백 가능 | 관리자 승인 후 실행 |
승인조치 항목은 선택된 항목 코드와 Accept Flag가 모두 있어야만 실행된다.
핵심 설계 4 — SSH Host CA로 접속 대상부터 검증
자동화를 만들고 나서 문제를 하나 발견했다. 빠르게 실행하려고 Ansible에서 서버 지문 검증을 생략하고 있었는데, 이러면 엉뚱한 서버에도 점검·조치가 실행될 수 있다. 보안 점검 도구가 보안 구멍이 되는 셈이다.
서버마다 지문을 하나씩 관리하는 대신, CA가 서명한 인증서를 신뢰하는 방식으로 바꿨다.
| 1. CA 준비 | ed25519 Host CA 생성, 개인키는 0600 권한으로 격리 |
| 2. 서버 신원 서명 | hostname · IP를 principal로 포함, 유효기간 52주 |
| 3. 안전 배포 | 백업 → 설치 → sshd -t 문법 검사 → reload, 실패 시 즉시 원복 |
| 4. 재검증 후 실행 | CA 서명 · 대상 · 만료 확인, 통과한 서버만 점검 시작 |
이제 서명이 없거나, 만료됐거나, 정보가 맞지 않는 서버는 자동으로 차단된다. 서버가 늘어나도 CA 공개키 한 줄로 같은 신뢰 기준을 적용할 수 있다.
결과 화면
시연 환경(대상 서버 2대)에서 나온 결과는 다음과 같다. 스크린샷의 서버 IP는 마스킹했다.
| 전체 점검 항목 | 170건 |
| 취약 항목 | 13건 (승인 필요 11건 · 자동조치 2건) |
| 양호율 | 91.8% |
| 평균 보안 점수 | 91.5점 |
| 조치 후 점수 | 자동조치 후 92.9점 → 승인조치 후 100점 |
로그인
관리자 계정으로 인증하고, 비인가 사용자의 관리 기능 접근을 막는다.

IP 등록
서버 IP, 호스트명, 진단 영역을 하나씩 또는 파일로 한꺼번에 등록한다. 등록하면 인벤토리가 자동으로 동기화되고, 서버별 CA 인증 상태와 접속 상태도 바로 보인다.

점검 · 조치
진단 영역별 취약 현황과 분류별 취약 건수를 보여준다. 자동조치와 승인요청 항목이 나뉘어 있고, 아래 조치 업데이트에서 어떤 설정이 어떻게 바뀌었는지와 백업 경로를 확인할 수 있다.

상세 분석
IP별 보안 점수와 등급을 비교하고, 취약 항목의 상세 내용과 조치 상태를 볼 수 있다.

SSH CA 인증
등록된 서버에 Host CA 인증서를 배포하고 인증 상태를 확인하는 화면이다.

보고서
같은 점검 데이터를 목적에 따라 두 가지 보고서로 뽑을 수 있게 했다.
PDF는 상세 분석 탭에서 필요한 IP나 취약점 코드만 골라 빠르게 뽑는 용도다. 서버·영역별 점수와 취약 현황을 요약하고, 항목별 판정 근거와 조치 내용을 페이지로 정리한다.

[IP 통합 점검 결과]

Excel은 전체 서버 결과를 한 번에 정리하는 관리용 파일이다. 버튼을 누른 시점의 DB 결과로 매번 새로 만들고, 파일명에 생성 일시가 들어간다. 시트는 대시보드, 자산현황, 조치 항목, 서버 상세, 참고 가이드, 작업 증적(로그와 SHA-256 검증 결과)으로 구성했다.



U-01 root 계정 원격 접속 제한
점검 스크립트는 PermitRootLogin 값을 보고 양호·취약을 판정한다. 설정이 아예 없거나 해석할 수 없는 값이면 보수적으로 취약 처리한다.
case "$val" in
no|prohibit-password|without-password)
CHECK_DETAIL="PermitRootLogin 이 '${val}' 로 설정되어 root 계정의 직접 원격(SSH) 접속이 제한되어 있음."
return "$KISA_EXIT_GOOD" ;;
yes)
CHECK_DETAIL="PermitRootLogin 이 'yes' 로 설정되어 root 계정의 SSH 직접 접속이 허용되어 있음."
return "$KISA_EXIT_VULN" ;;
"")
CHECK_DETAIL="PermitRootLogin 설정이 존재하지 않아 OpenSSH 기본값이 적용됨. 명시적 제한이 없어 취약으로 판단."
return "$KISA_EXIT_VULN" ;;
*)
CHECK_DETAIL="PermitRootLogin 값 '${val}' 을(를) 해석할 수 없어 취약으로 보수적으로 판단."
return "$KISA_EXIT_VULN" ;;
esac
조치 스크립트는 승인요청 항목이면 승인이 없을 때 조치를 보류하고, 승인된 경우에만 설정을 바꾼다.
if [ "$ACTION_TAG" = "승인요청" ] && ! is_approved; then
FIX_DETAIL="관리자 승인 필요: sshd 설정 변경 및 서비스 reload가 필요한 항목이라 자동 조치를 보류함."
return 1
fi
if grep -qiE '^[[:space:]]*PermitRootLogin[[:space:]]' "$SSHD_CONFIG"; then
sed -i -E 's/^[[:space:]]*PermitRootLogin[[:space:]].*/PermitRootLogin no/I' "$SSHD_CONFIG"
else
printf '\nPermitRootLogin no\n' >> "$SSHD_CONFIG"
fi
점검 플레이북은 67개 항목을 원격 호출 한 번으로 실행하고, 결과가 정확히 67개인지, 중복이나 누락은 없는지까지 검증한다.
- name: 배치 runner로 67개 점검 실행 (원격 호출 1회)
ansible.builtin.command:
cmd: "{{ kisa_remote_active_dir }}/lib/run_checks.sh"
register: kisa_check_batch
changed_when: false
failed_when: kisa_check_batch.rc != 0
- name: 배치 점검 결과 완전성 검증 (67개, 중복/누락 없음)
ansible.builtin.assert:
that:
- kisa_results | length == 67
- kisa_results | map(attribute='code') | unique | list | length == 67
- kisa_results | map(attribute='code') | list == query('sequence', 'start=1 end=67 format=U-%02d')
이렇게 나온 결과 JSON은 다음과 같다.
{
"code": "U-01",
"title": "root 계정 원격 접속 제한",
"status": "취약",
"action": "점검",
"action_tag": "승인요청",
"severity": "상",
"detail": "PermitRootLogin 이 'yes' 로 설정되어 root 계정의 SSH 직접 접속이 허용되어 있음.",
"impact": "sshd 설정 적용을 위해 서비스 reload/restart 가 필요하며, 원격 접속 정책이 잘못 변경되면 관리자의 원격 접속 경로에 영향을 줄 수 있음",
"os_type": "ubuntu 26.04",
"timestamp": "2026-08-28T11:20:19+09:00"
}
트러블슈팅
처음에는 영역마다 결과를 각자 편한 형식으로 출력했다.
그러다 보니 DB, 대시보드, 채점 로직을 영역마다 따로 맞춰야 했다.
실제로 컨트롤 노드의 리포트 폴더를 열어보면 하루 사이에 두 세대의 형식이 섞여 있었다.
// 레거시 (2026-08-25) — 필드 4개, 영문 status, 승인 게이트 없음
{
"rule": "D-01",
"title": "기본 계정의 비밀번호, 정책 등을 변경하여 사용",
"status": "PASS",
"detail": "공백 비밀번호 계정 없음"
}
// 통일 스키마 (2026-08-26) — 필드 10개, 한글 status, action_tag 추가
{
"code": "D-01",
"title": "기본 계정의 비밀번호, 정책 등을 변경하여 사용",
"status": "양호",
"action": "점검",
"detail": "공백 비밀번호 계정 없음",
"impact": "불필요한 기본 계정의 사용 제한",
"os_type": "mysql 8.0.46",
"severity": "상",
"action_tag": "자동조치",
"timestamp": "2026-08-25T23:41:01-04:00"
}
필드명을 rule에서 code로 통일하고, impact · os_type · severity · action_tag 등 6개 필드를 추가했다.
특히 action_tag가 들어가면서 자동조치와 승인조치를 나누는 게이트를 결과 데이터 안에서 처리할 수 있게 됐다.
이 작업 이후로 새 기능을 붙일 때 영역별로 따로 손댈 일이 확 줄었다.
프로젝트 총평
잘한 점
- 스크립트 배포 방식으로 취약 점검 절차를 표준화했다.
- KISA 공개 가이드를 기준으로 양호·취약을 객관적으로 판정하고 점수화했다.
- UNIX · WEB · DBMS라는 서로 다른 환경을 하나의 플랫폼에서 다룰 수 있게 했다.
- 보고서 양식을 실무에서 바로 쓸 수 있는 수준으로 다듬었다.
아쉬운 점
- 초기 아키텍처 설계가 늦어지면서 연동한 서버 범위가 줄었다.
- Windows 점검 환경은 구현하지 못했고, DBMS도 MySQL만 지원한다.
- 예외 상황(Edge Case)에 대한 테스트가 더 필요하다.
추후 개선안
서버 장애나 네트워크 단절 같은 이상 상황에서의 예외 처리를 보완하고, Windows와 다른 DBMS까지 범위를 넓혀보고 싶다. 무엇보다 수동 점검 과정을 자동화 시스템으로 바꾸는 전 과정, 그리고 Ansible부터 프론트엔드·백엔드·대시보드까지 이어지는 파이프라인을 직접 만들어본 경험이 가장 크게 남았다.