이번 글은 skill-creator나 Agent Skill을 사용하지 않습니다. 대신 주요정보통신기반시설 UNIX/Linux 기술 항목을 참고한 독립형 Python 스크립트 프로젝트를 계획하고, 코드를 구현하고, 실제 Linux에서 실행한 뒤 테스트 실패와 실측 결과를 바탕으로 보완합니다.
목표는 “취약 여부를 많이 찍는 스크립트”가 아닙니다. 기본 대상은 스크립트를 실행한 현재 Linux 시스템의 /이고, 설정을 바꾸지 않은 채 최소 증적만 수집합니다. 파일만으로 확정하기 어려운 항목은 REVIEW로 남기며, 보고서는 정식 취약점 분석·평가나 인증을 대체하지 않습니다.

먼저 범위를 정확히 잡기
KISA 보호나라에는 2021년 4월 등록된 주요정보통신기반시설 기술적 취약점 분석·평가 방법 상세가이드가 공개되어 있습니다. UNIX 서버 영역은 계정 관리, 파일 및 디렉터리 관리, 서비스 관리, 패치 관리, 로그 관리로 나뉩니다. 한편 국가법령정보센터의 현행 주요정보통신기반시설 취약점 분석·평가 기준은 2025년 12월 24일 시행된 과학기술정보통신부고시 제2025-62호이며, 관리기관이 자산과 세부 점검항목을 도출하고 발견된 취약점의 위험등급과 개선 방향을 수립하는 전체 과정을 규정합니다.
따라서 파일 몇 개를 검사하는 이 프로젝트를 “주요정보통신기반시설 진단 완료”라고 표현하면 안 됩니다. 정확한 표현은 다음과 같습니다.
공개된 UNIX/Linux 기술 항목 가운데 정적 증적으로 확인 가능한 일부 항목을 읽기 전용으로 점검하는 1차 자가진단 도구다.
1단계: 코딩보다 먼저 프로젝트 계획 세우기
진단 스크립트는 판정 기준과 실패 동작이 먼저 정해져야 합니다. 이번 프로젝트의 PROJECT_PLAN.md에는 다음 계약을 기록했습니다.
## 목표
- 현재 Linux 시스템의 /를 읽기 전용으로 1차 점검한다.
## 범위
- 출력: Markdown 또는 JSON
- 판정: PASS, FAIL, REVIEW, NOT_APPLICABLE, ERROR
- 금지: 자동 조치, 패키지 설치, 서비스 재시작, 네트워크 접속,
비밀번호 해시 출력
## 완료 기준
- Linux에서는 인수 없이 현재 /를 점검한다.
- 비 Linux에서는 기본 실행을 거부한다.
- 오프라인 rootfs 밖을 가리키는 심볼릭 링크를 거부한다.
- 기존 보고서를 덮어쓰지 않는다.
- FAIL은 종료 코드 1, 실행 오류는 2, 실패가 없으면 0을 반환한다.
프로젝트 구조는 복잡하게 만들지 않았습니다.
linux-ci-audit/
├── PROJECT_PLAN.md
├── README.md
├── audit_linux.py
├── test_audit_linux.py
└── VALIDATION_NOTES.md
PROJECT_PLAN.md는 범위와 완료 기준, audit_linux.py는 실제 점검기, test_audit_linux.py는 정상·취약·경계 조건, VALIDATION_NOTES.md는 실패 원인과 수정 기록을 담당합니다. Skill 호출 규칙이나 SKILL.md는 전혀 사용하지 않습니다.

그림: Linux 진단 스크립트 프로젝트의 검증 루프 · KISA 주요정보통신기반시설 기술적 취약점 분석·평가 방법 상세가이드를 바탕으로 재구성
2단계: 1차 구현 항목 선정하기
첫 버전은 전체 UNIX 항목을 자동화했다고 주장하지 않습니다. 정적인 파일과 설정에서 비교적 명확한 증적을 얻을 수 있는 8개 항목만 선택했습니다.
| ID | 점검 항목 | 구현 방식 |
| U-01 | root 계정 원격 접속 제한 | 전역 PermitRootLogin을 확인하고 Match가 있으면 REVIEW |
| U-04 | 패스워드 파일 보호 | shadow 존재와 passwd의 placeholder 개수만 확인 |
| U-07 | /etc/passwd 소유자·권한 | root 소유, 0644 이하 확인 |
| U-08 | /etc/shadow 소유자·권한 | root 소유, 0400 이하의 엄격 기준 확인 |
| U-09 | /etc/hosts 소유자·권한 | root 소유, 0600 이하 확인 |
| U-12 | /etc/services 소유자·권한 | root 소유, 0644 이하 확인 |
| U-44 | root 이외 UID 0 금지 | passwd에서 추가 UID 0 계정 탐지 |
| U-53 | 사용자 shell 점검 | 시스템 계정의 대화형 shell 후보를 REVIEW |
U-08처럼 가이드의 엄격 기준과 배포판 기본 정책이 다를 수 있는 항목은 특히 주의해야 합니다. 스크립트는 기준과 다른 증적을 FAIL로 보여주되, 곧바로 chmod를 실행하지 않습니다. 적용 기준과 배포판의 shadow 그룹 운영, 보완 통제를 사람이 검토해야 하기 때문입니다.
3단계: 읽기 전용 점검 구조 구현하기
결과 객체에는 ID, 위험도, 상태, 증적과 권고를 보존합니다.
STATUSES = ("PASS", "FAIL", "REVIEW", "NOT_APPLICABLE", "ERROR")
@dataclass(frozen=True)
class Finding:
item_id: str
severity: str
title: str
status: str
evidence: str
recommendation: str
오프라인 rootfs도 지원하지만, 내부 심볼릭 링크가 대상 밖으로 나가면 중단합니다. 테스트용 이미지가 호스트의 /etc/passwd 같은 파일을 의도치 않게 읽지 않도록 하는 경계입니다.
def ensure_safe(self, candidate: Path) -> Path:
if not self.live and (candidate.exists() or candidate.is_symlink()):
resolved = candidate.resolve()
try:
resolved.relative_to(self.root)
except ValueError as exc:
raise RuntimeError(
f"offline rootfs 밖을 가리키는 경로를 거부합니다: {candidate}"
) from exc
return candidate
권한 점검은 stat() 결과만 읽습니다. 허용 비트 밖의 권한이 있거나 UID가 0이 아니면 실패로 분류하지만 파일은 변경하지 않습니다.
info = path.stat()
mode = stat.S_IMODE(info.st_mode)
evidence = f"{name}: uid={info.st_uid}, mode={mode:04o}"
if info.st_uid != 0 or mode & ~allowed_mask:
status = "FAIL"
else:
status = "PASS"
U-04는 /etc/shadow의 내용을 읽지 않습니다. /etc/passwd의 두 번째 필드가 x, *, ! 같은 placeholder인지와 shadow 파일 존재 여부만 보고서에 넣습니다.
status = "PASS" if shadow.exists() and exposed == 0 and malformed == 0 else "FAIL"
evidence = (
f"shadow_exists={str(shadow.exists()).lower()}, "
f"passwd_non_placeholder={exposed}, malformed_lines={malformed}"
)

그림: 읽기 전용 진단 스크립트의 데이터 흐름 · 주요정보통신기반시설 취약점 분석·평가 기준을 바탕으로 재구성
4단계: 기본 실행 대상을 현재 Linux로 고정하기
인수 없이 실행하면 현재 시스템의 /를 점검합니다.
cd linux-ci-audit
python3 audit_linux.py --format markdown
JSON 보고서를 새 파일로 남기려면 상대경로를 지정합니다.
python3 audit_linux.py \
--format json \
--output reports/linux-audit.json
macOS나 Windows에서 같은 기본 명령을 실행하면 Linux처럼 흉내 내지 않고 종료 코드 2로 중단합니다. 실제 macOS 검증 출력은 다음과 같았습니다.
ERROR: 기본 진단 대상은 현재 Linux 시스템입니다.
오프라인 Linux rootfs는 --root로 명시하십시오.
승인된 오프라인 Linux rootfs를 검사할 때만 --root를 명시합니다.
python3 audit_linux.py \
--root /mnt/approved-linux-rootfs \
--format markdown
5단계: 단위 테스트 작성하고 첫 실패 고치기
테스트는 정상 설정만 확인하면 부족합니다. 중복 UID 0, 느슨한 shadow 권한, SSH Match 블록, 비밀정보 미노출, 심볼릭 링크 이탈, 기존 보고서 보호와 종료 코드를 함께 검증했습니다.
python3 -m unittest -v test_audit_linux.py
첫 실행에서는 9개 중 1개가 실패했습니다.
FAIL: test_secure_fixture_has_no_failures
AssertionError: 'FAIL' unexpectedly found
원인은 진단 코드가 아니라 테스트의 환경 의존성이었습니다. macOS 일반 사용자가 만든 임시 파일은 UID가 0이 아니므로, 내용이 안전해도 소유자 검사에서 실패합니다. “안전한 fixture는 모든 항목을 통과한다”는 가정이 잘못된 것입니다.
테스트를 다음처럼 내용 기반 판정과 메타데이터 판정으로 분리했습니다.
statuses = {item.item_id: item.status for item in audit.collect(ctx)}
self.assertEqual(statuses["U-01"], "PASS")
self.assertEqual(statuses["U-04"], "PASS")
self.assertEqual(statuses["U-44"], "PASS")
self.assertEqual(statuses["U-53"], "PASS")
root 소유권을 포함한 전체 판정은 Linux 컨테이너에서 별도로 확인합니다. 기준을 낮춘 것이 아니라 테스트 전제를 운영 환경과 분리한 것입니다.
6단계: 실제 Linux의 /에서 실행하기
검증 환경은 Podman 5.7.1과 python:3.12-slim 이미지였습니다. 프로젝트를 읽기 전용으로 마운트하고 컨테이너의 현재 /를 점검했습니다.
podman run --rm \
-v "$PWD:/work:ro" \
-w /work \
python:3.12-slim \
python audit_linux.py --format markdown
최종 단위 테스트 10개는 Linux에서 모두 통과했습니다.
test_absolute_output_is_rejected ... ok
test_duplicate_uid_zero_is_fail ... ok
test_existing_report_is_not_overwritten ... ok
test_failures_return_exit_code_one ... ok
test_match_block_requires_review ... ok
test_new_report_is_created ... ok
test_offline_rootfs_symlink_escape_is_rejected ... ok
test_permissive_shadow_mode_is_fail ... ok
test_secure_fixture_passes_content_checks ... ok
test_shadow_content_is_never_rendered ... ok
Ran 10 tests in 0.109s
OK
Debian GNU/Linux 13 컨테이너의 실제 결과는 다음과 같았습니다.
PASS: 5
FAIL: 2
REVIEW: 0
NOT_APPLICABLE: 1
ERROR: 0
U-01 | NOT_APPLICABLE | /etc/ssh/sshd_config 없음
U-04 | PASS | shadow_exists=true, passwd_non_placeholder=0
U-07 | PASS | /etc/passwd: uid=0, mode=0644
U-08 | FAIL | /etc/shadow: uid=0, mode=0640
U-09 | FAIL | /etc/hosts: uid=0, mode=0644
U-12 | PASS | /etc/services: uid=0, mode=0644
U-44 | PASS | 추가 UID 0 계정: 없음
U-53 | PASS | 대화형 shell 후보: 없음
종료 코드는 1입니다. FAIL이 있다는 신호이지 스크립트가 고장 났다는 뜻은 아닙니다. /etc/shadow의 0640은 Debian의 shadow 그룹 정책과 함께 검토해야 하고, 컨테이너의 /etc/hosts는 런타임이 제공하는 파일이라는 맥락을 함께 봐야 합니다. 반대로 U-01의 NOT_APPLICABLE도 “SSH가 안전하다”는 뜻이 아니라 이 이미지에 서버 설정 파일이 없다는 뜻입니다.
7단계: 실행 후 출력 안전성 보완하기
코드 검토에서 보고서 덮어쓰기 방지에도 경쟁 조건이 있음을 발견했습니다. 처음 구현은 exists()를 확인한 뒤 write_text()를 호출했습니다. 두 동작 사이에 같은 이름의 파일이 생기면 완료 기준을 원자적으로 보장할 수 없습니다.
최종 구현은 배타적 생성 모드 x를 사용합니다.
try:
with destination.open("x", encoding="utf-8") as stream:
stream.write(content)
except FileExistsError:
print(f"ERROR: 기존 보고서를 덮어쓰지 않습니다: {destination}", file=sys.stderr)
return 2
그리고 “새 보고서는 생성된다”와 “기존 보고서는 그대로 보존된다”를 각각 회귀 테스트로 남겼습니다. 이처럼 보완 기록은 최종 코드만큼 중요합니다. 어떤 실패가 있었고, 진단 기준을 바꾼 것인지 테스트 전제를 고친 것인지 구분할 수 있기 때문입니다.
상태별로 다음 행동 정하기
| 상태 | 의미 | 다음 행동 |
| PASS | 수집한 증적이 구현 기준을 충족 | 증적과 설정 유지 |
| FAIL | 증적이 구현 기준과 명확히 충돌 | 운영 맥락과 적용 기준을 확인한 뒤 조치 계획 수립 |
| REVIEW | 정적 파일만으로 확정하기 어려움 | 담당자와 런타임·정책 수동 확인 |
| NOT_APPLICABLE | 대상 파일이나 구성요소가 없음 | 실제 서비스 사용 여부 확인 |
| ERROR | 증적 수집이나 해석 실패 | 권한, rootfs 완전성, 파서 오류 확인 |
중요한 원칙은 FAIL과 자동 조치를 분리하는 것입니다. 점검 스크립트가 chmod, chown, 서비스 재시작까지 수행하면 진단과 변경의 승인 경계가 무너집니다. 이 프로젝트는 읽기와 보고서 생성만 담당하고, 조치는 관리자가 영향도를 검토한 뒤 별도 절차로 수행합니다.
다음 버전의 확장 계획
8개 항목은 프로젝트 골격을 검증하기 위한 시작점입니다. 실제 환경에서는 자산 특성과 승인 범위에 맞춰 다음 항목을 추가할 수 있습니다.
- 홈 디렉터리와 시작 파일 권한
- world-writable 파일과 소유자 없는 파일
- cron·at 접근통제
- 불필요한 서비스와 포트의 런타임 상태
- PAM 패스워드 복잡성·잠금 정책
- 로그 생성, 보관 기간과 중앙 전송 여부
- 패치 정책과 배포판 지원 상태
- 컨테이너 호스트와 불변 인프라의 예외 기준
항목을 추가할 때는 먼저 “파일만 보고 확정 가능한가”, “실행 중인 서비스 확인이 필요한가”, “기관 정책에 따라 달라지는가”를 분류해야 합니다. 자동화 개수를 늘리는 것보다 잘못된 확정을 줄이는 편이 더 중요합니다.
마무리
이번 프로젝트는 Skill을 만드는 실습이 아닙니다. 독립형 Linux 진단 스크립트의 요구사항을 계획서로 고정하고, 점검 로직과 테스트를 작성하고, 실제 Linux의 /에서 실행한 뒤, 환경 의존 테스트와 보고서 생성 경쟁 조건을 보완하는 전체 개발 흐름입니다.
완성도를 결정하는 것은 점검 항목 수가 아니라 세 가지입니다. 읽기 전용 안전 경계, 판정과 사람 검토의 분리, 실패를 재현하고 수정 이력을 남기는 검증 루프입니다. 이 세 가지가 있어야 스크립트가 일회성 명령 모음이 아니라 반복 가능한 보안 점검 프로젝트가 됩니다.
참고 자료
'일반IT > IT보안' 카테고리의 다른 글
| Claude Code를 보안 코파일럿으로 만드는 19개 Skill — Claude-Code-CyberSecurity-Skill 구조와 안전한 실습 (0) | 2026.08.08 |
|---|---|
| [보안 프롬프트 설계와 작성 실무 5] 악성코드 스크립트 난독화 정적 분석하기 (0) | 2026.08.07 |
| [보안 프롬프트 설계와 작성 실무 3] Agent Skill 실제 구성 살펴보기 (0) | 2026.08.07 |
| [보안 프롬프트 설계와 작성 실무 2] skill-creator로 보안 리뷰 스킬 만들기 (0) | 2026.08.07 |
| [보안 프롬프트 설계와 작성 실무 1] skills.sh에서 스킬 안전하게 내려받기 (0) | 2026.08.07 |