시스템 보안 · 취약점 › A. Linux 서버 보안 설정 · 44/50편 (전체 044/450)
학습 단계: 4단계 · 자동 점검·로그 분석
실습 표기: 이 글의 명령어·출력·로그는 로컬 VMware 테스트 VM(Rocky Linux 9 / Ubuntu 22.04) 기준의 「실습 예시」이며, IP·계정·호스트명은 가상의 값입니다.
OpenSCAP·Lynis가 다루지 않는 조직 고유 항목(예: 관제 에이전트 상태, 사내 NTP 서버, 역할별 허용 포트)은 자체 스크립트로 점검합니다. 이때 중요한 것은 기능보다 설계 원칙입니다.
설계 원칙
① 읽기 전용 : 점검만 하고 절대 설정을 바꾸지 않음 (조치는 별도 절차)
② 멱등성 : 몇 번 실행해도 같은 상태면 같은 결과
③ 표준 출력 : item= expected= actual= result= 형식 (key=value)
④ 종료 코드 : 0 = 모두 PASS, 1 = 실행 오류, 2 = FAIL 존재
⑤ 자기 보호 : 스크립트 파일 권한(700 root) + FIM 감시
실행 흐름
systemd timer → check.sh → logger -t hardening-check → journald/rsyslog → SIEM
| 요소 | 선택 | 이유 |
|---|---|---|
| 실행 주기 | systemd timer | 실행 이력·실패가 journal에 남고 systemctl list-timers로 확인 가능 |
| 출력 | logger -t hardening-check | 별도 파일 없이 기존 로그 경로·원격 전송 재사용 |
| 형식 | key=value | Wazuh·Logstash에서 kv 필터로 바로 추출 |
| 위치 | /usr/local/sbin/hardening-check.sh (700 root) | 일반 사용자 수정 불가 |
sudo tee /usr/local/sbin/hardening-check.sh >/dev/null <<'EOF'
#!/bin/bash
# 읽기 전용 점검 스크립트 (설정 변경 없음)
FAIL=0
chk() { # chk 항목 기대값 실제값
local r=PASS; [ "$2" = "$3" ] || { r=FAIL; FAIL=1; }
logger -t hardening-check "item=$1 expected=$2 actual=$3 result=$r"
}
chk ssh_permitrootlogin no "$(sshd -T 2>/dev/null | awk '/^permitrootlogin /{print $2}')"
chk selinux Enforcing "$(getenforce 2>/dev/null)"
chk audit_enabled 2 "$(auditctl -s | awk '/^enabled/{print $2}')"
chk uid0_count 1 "$(awk -F: '$3==0' /etc/passwd | wc -l)"
chk ip_forward 0 "$(sysctl -n net.ipv4.ip_forward)"
chk agent_active active "$(systemctl is-active wazuh-agent 2>/dev/null)"
logger -t hardening-check "summary result=$([ $FAIL -eq 0 ] && echo PASS || echo FAIL)"
exit $(( FAIL * 2 ))
EOF
sudo chmod 700 /usr/local/sbin/hardening-check.sh
# systemd service + timer (매일 05:00)
sudo tee /etc/systemd/system/hardening-check.service >/dev/null <<'EOF'
[Unit]
Description=Read-only hardening check
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/hardening-check.sh
SuccessExitStatus=2
EOF
sudo tee /etc/systemd/system/hardening-check.timer >/dev/null <<'EOF'
[Timer]
OnCalendar=*-*-* 05:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now hardening-check.timer
sudo systemctl start hardening-check.service && journalctl -t hardening-check -n 8 --no-pager
SuccessExitStatus=2는 "FAIL 존재(2)"를 서비스 실행 실패로 취급하지 않게 합니다. 실행 오류(1)와 점검 결과(2)를 구분하기 위한 설정입니다.
Oct 1 05:00:01 rocky9-web01 hardening-check[10101]: item=ssh_permitrootlogin expected=no actual=no result=PASS
Oct 1 05:00:01 rocky9-web01 hardening-check[10103]: item=selinux expected=Enforcing actual=Enforcing result=PASS
Oct 1 05:00:01 rocky9-web01 hardening-check[10107]: item=audit_enabled expected=2 actual=2 result=PASS
Oct 1 05:00:02 rocky9-web01 hardening-check[10115]: summary result=PASS
$ systemctl list-timers hardening-check.timer --no-pager | head -2
NEXT LEFT LAST PASSED UNIT
Fri 2026-10-02 05:00:00 KST 23h left Thu 2026-10-01 05:00:00 KST 1min ago hardening-check.timer
$ systemctl list-timers hardening-check.timer --no-pager
0 timers listed.
$ sudo sha256sum -c /root/baseline/hardening-check.sha256
/usr/local/sbin/hardening-check.sh: FAILED
$ sudo grep -n 'logger' /usr/local/sbin/hardening-check.sh | head -2
6: logger -t hardening-check "item=$1 expected=$2 actual=$2 result=PASS"
점검 결과와 점검 체계 자체의 이상을 함께 봅니다(가상의 예시 로그).
Oct 1 05:00:01 rocky9-web01 hardening-check[10101]: item=ssh_permitrootlogin expected=no actual=yes result=FAIL
Oct 1 05:00:01 rocky9-web01 hardening-check[10107]: item=uid0_count expected=1 actual=2 result=FAIL
Oct 1 05:00:02 rocky9-web01 hardening-check[10115]: summary result=FAIL
type=PATH msg=audit(1759742400.200:3101): item=0 name="/usr/local/sbin/hardening-check.sh" nametype=NORMAL key="check_script"
| 관찰 | 해석 |
|---|---|
result=FAIL 항목 | 해당 하드닝 항목 변화 → 037편 원인 분류 |
| 스크립트 파일 쓰기 이벤트 | 점검 체계 변조 시도 |
다음 날 hardening-check 로그 없음 | 타이머 중지 또는 서비스 삭제 |
summary result=FAIL을 1차 Alert, 개별 item을 상세 근거로 사용합니다.check_script 키)로 보호합니다.<group name="local,hardening_check,">
<rule id="100490" level="9">
<program_name>hardening-check</program_name>
<match>summary result=FAIL</match>
<description>자체 하드닝 점검 FAIL 발생</description>
</rule>
<rule id="100491" level="12">
<if_sid>550</if_sid>
<field name="file">^/usr/local/sbin/hardening-check.sh$|^/etc/systemd/system/hardening-check</field>
<description>하드닝 점검 스크립트/유닛 변경</description>
</rule>
</group>
Logstash를 쓴다면 kv { source => "message" } 필터로 item, expected, actual, result 필드를 자동 추출할 수 있습니다.
| 구분 | 핵심 내용 |
|---|---|
| 설계 원칙 | 읽기 전용 · 멱등 · key=value · 종료 코드(0/1/2) · 자기 보호 |
| 실행 | systemd timer (SuccessExitStatus=2) |
| 출력 경로 | logger -t → journald/rsyslog → SIEM |
| 변조 대응 | 스크립트 FIM, 점검 로그 수신 공백 감시 |
| 면접 포인트 | "점검 결과뿐 아니라 점검 체계 자체도 감시한다" |
다음 편 045. Linux 서버 보안 — Wazuh SCA로 설정 준수 모니터링 에서는 5단계로 넘어가 에이전트 기반 상시 점검인 Wazuh SCA로 설정 준수 모니터링을 다룹니다.
이전 편: 043. Linux 서버 보안 — Lynis 하드닝 점검 결과 해석
📚 시리즈 전체 보기: 시스템 보안 · 취약점