1편에서 72개 항목을 수동으로 점검했다. 이번엔 그 중 "중요도 :상"으로 판단한 항목에 대한 점검 과정을 쉘 스크립트로 자동화한다.

전체 스크립트:

K-Shield Jr 10기 E-01조 프로젝트


설계 방향

수동 점검을 스크립트로 바꿀 때 가장 먼저 생각한 건 출력 형태였다. 결과를 터미널에만 출력하면 나중에 참조하기 어렵다. 실행 시각이 포함된 파일명으로 결과를 저장하고, 각 항목이 시작/종료되는 구조로 만들었다.

RESULT_FILE="result_collect_`date +\"%Y%m%d%H%M\"`.txt"

실행할 때마다 타임스탬프가 붙은 파일이 생긴다. result_collect_202305101430.txt 형태다.

또 하나의 전제가 있었다. 이 스크립트는 root로 실행해야 의미 있는 결과가 나온다. Docker socket 접근, auditctl 명령, 컨테이너 inspect 등이 전부 권한을 필요로 한다.

user=`id | grep "uid=0"`
if [ "$user" == "" ]; then
    echo "Not root"
    exit 100
fi

root가 아니면 즉시 종료한다. 결과 파일에도 기록한다.


스크립트 구조

각 항목은 아래 패턴으로 동일하게 구성된다.

echo "[ D-XX ] : Check"
echo "===========================[ D-XX 항목명 START ]" >> $RESULT_FILE 2>&1

# 점검 명령 실행
# 조건 판단 → Good / Vulnerable / Review 기록

echo "" >> $RESULT_FILE 2>&1
echo "===========================[ D-XX 항목명 END ]" >> $RESULT_FILE 2>&1
echo "[ D-XX ] : End"

터미널에는 [ D-01 ] : Check[ D-01 ] : End 형태로 진행 상황을 출력하고, 상세 결과는 파일에만 기록한다. 결과값은 세 가지 중 하나로 통일했다.

  • Result : Good — 양호
  • Result : Vulnerable — 취약
  • Result : Review — 수동 검토 필요

항목별 구현 방식

D-01: Docker 최신 버전 확인

version=`docker version`
docker_v=`docker version | grep 'Version' | grep -v 'grep' \
  | awk -F":" '{print $2}' | awk -F"." 'NR<3 {print $1}'`

array=($docker_v)
# Client와 Server 버전 주요 번호 추출
if [ "${array[0]}" = "23" ] && [ "${array[1]}" = "23" ]; then
    echo "Result : Good" >> $RESULT_FILE
else
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

docker version으로 Client/Server 버전을 모두 가져와서, 메이저 버전이 23(당시 최신)인지 확인한다. Client와 Server 버전이 모두 23이어야 양호다.

실제 운영에서는 버전 숫자를 하드코딩하면 나중에 낡은 스크립트가 된다. 버전을 변수로 빼거나, docker version의 결과를 CVE 데이터베이스와 비교하는 방식이 더 좋다. 이번 실습 버전은 단순화했다.

D-02 ~ D-07: Audit 설정 확인

audit 계열 항목들은 패턴이 거의 동일하다. auditctl -l로 현재 활성화된 rule을 확인하고, /etc/audit/audit.rules 파일에도 설정이 있는지 두 곳 모두 확인한다.

daemon_flag=0

daemon_audit=`auditctl -l | grep /usr/bin/docker`
if [ "$daemon_audit" == "" ]; then
    daemon_flag=`expr $daemon_flag + 1`
else
    echo "$daemon_audit" >> $RESULT_FILE
fi

if [ -f '/etc/audit/audit.rules' ]; then
    rules="/etc/audit/audit.rules"
    if [ "`cat $rules | grep /usr/bin/docker`" == "" ]; then
        daemon_flag=`expr $daemon_flag + 1`
    else
        echo "`cat $rules | grep /usr/bin/docker`" >> $RESULT_FILE
    fi
else 
    echo "Not Found /etc/audit/audit.rules" >> $RESULT_FILE
fi

if [ "$daemon_flag" != 0 ]; then
    echo "Result : Vulnerable" >> $RESULT_FILE
else
    echo "Result : Good" >> $RESULT_FILE
fi

flag 변수를 카운터로 쓰는 패턴이다. 두 곳 중 하나라도 설정이 없으면 flag가 올라가고, 최종적으로 flag가 0이 아니면 취약으로 판단한다.

D-09 ~ D-20: 파일 소유권 및 접근 권한

파일 소유권과 접근 권한 확인은 stat 명령으로 통일했다.

# 소유권 확인
owner=`stat -c %U:%G /lib/systemd/system/docker.service`
if [ "$owner" == "root:root" ]; then
    echo "Result : Good" >> $RESULT_FILE
else
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

# 접근 권한 확인 (644 이하 체크)
permission_val=`stat -c '%a' /lib/systemd/system/docker.service`
owner_perm_val=`echo "$permission_val" | awk '{ print substr($0, 1, 1) }'`
group_perm_val=`echo "$permission_val" | awk '{ print substr($0, 2, 1) }'`
other_perm_val=`echo "$permission_val" | awk '{ print substr($0, 3, 1) }'`

if [ "$owner_perm_val" -le 6 ] && [ "$group_perm_val" -le 4 ] && [ "$other_perm_val" -le 4 ]; then
    echo "Result : Good" >> $RESULT_FILE
else
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

권한 값을 세 자리로 분리해서 각각 비교하는 방식이다. stat -c '%a'는 숫자 권한값(예: 644)을 반환한다. awk로 첫째, 둘째, 셋째 자리를 분리해서 개별 비교한다.

파일이 없을 수도 있는 항목(daemon.json, /etc/default/docker 등)은 파일 존재 여부를 먼저 확인한다.

if [ -e "$DOCKER_DEFAULT" ]; then
    # 점검 로직
else
    echo "docker /etc/default/docker not found."
    echo "Result: Review"
fi

D-21: 컨테이너 SSH 사용 금지

SSHD=`$systemctl_cmd status sshd | grep "Active" | awk -F":" '{print substr($2,2,6)}'`

if [ "$SSHD" == "active" ]; then
    echo "SSH active"
    echo "Result : Vulnerable" >> $RESULT_FILE
else
    DOCKER_RUNNING_INSTANCE=$(docker ps --quiet)
    for var in $DOCKER_RUNNING_INSTANCE
    do 
        DOCKER_SSH_PROCESS=$(docker exec $var ps -el)
        echo "$DOCKER_SSH_PROCESS" >> $RESULT_FILE
    done
fi

호스트 SSH 상태를 먼저 확인하고, 그 다음 실행 중인 컨테이너 각각에 docker exec로 들어가서 프로세스 목록을 확인하는 구조다.

D-22: 호스트 주요 디렉토리 마운트 제한

DOCKER_CONTAINER_MAPPED_DIRECTORY=$(docker ps --quiet --all | \
  xargs docker inspect --format 'Volumes={{.Mounts}}')
echo "$DOCKER_CONTAINER_MAPPED_DIRECTORY" > dir.txt

Host_cnt=0
while IFS= read -r line
do
    if [[ "$line" =~ (/boot|/dev|/etc|/lib|/proc|/sys|/usr) ]]; then
        Host_cnt=`expr $Host_cnt + 1`
    fi
done < dir.txt

if [ "$Host_cnt" == "0" ]; then
    echo "Result : Good" >> $RESULT_FILE
else
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

rm -f dir.txt

마운트 목록을 임시 파일에 저장하고, 한 줄씩 읽으면서 정규표현식으로 위험한 경로(/boot, /dev, /etc 등)가 포함되어 있는지 확인한다. 임시 파일은 실행 후 삭제한다.


D-23: 인증-권한 제어 (docker 그룹 사용자 확인)

bash

get_group_ps=`getent group docker`
if [ "$get_group_ps" != "" ]; then
    echo "$get_group_ps" >> $RESULT_FILE
    echo "Result : Review" >> $RESULT_FILE
else
    echo "NOT FOUND" >> $RESULT_FILE
fi

docker 그룹에 속한 사용자 목록을 getent group docker로 출력한다. docker 그룹 멤버는 사실상 root에 준하는 권한을 가지기 때문에, 불필요한 사용자가 포함되어 있는지 사람이 직접 확인해야 한다. 자동으로 Good/Vulnerable을 판단하기 어려워서 Result : Review로 처리했다. authorization-plugin 적용 여부까지 확인하려면 ps -ef | grep dockerd에서 플래그를 추가로 파싱해야 한다.

D-24: SSL/TLS 적용 (Docker daemon TLS)

bash

get_ps=`ps -ef | grep dockerd | grep -i "tlsverify"`

if [ "$get_ps" != "" ]; then
    echo "--tlsverify --tlscacert --tlscert --tlskey USE" >> $RESULT_FILE
    echo "Result : GOOD" >> $RESULT_FILE
else
    echo "--tlsverify --tlscacert --tlscert --tlskey USE NOT FOUND" >> $RESULT_FILE
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

dockerd 프로세스 실행 옵션에 --tlsverify가 포함되어 있는지 확인한다. TLS가 없으면 Docker API가 평문으로 노출된다. grep -i로 대소문자 구분 없이 매칭한다.

D-25: 컨테이너 권한 제어 (no-new-privileges)

bash

get_ps=`ps -ef | grep dockerd | egrep -i "no-new-privileges"`

if [ "$get_ps" != "" ]; then
    echo "$get_ps" >> $RESULT_FILE
    echo "Result : GOOD" >> $RESULT_FILE
else
    echo "no-new-privileges NOT FOUND" >> $RESULT_FILE
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

--no-new-privileges 옵션이 dockerd에 적용되어 있는지 확인한다. 이 옵션이 없으면 컨테이너 내 프로세스가 setuid 바이너리 등을 통해 추가 권한을 획득할 수 있다.

D-26: 인증제어 (Swarm 비활성화 확인)

bash

get_ps=`docker info | grep "Swarm" | awk '{ print $2 }'`

if [ "$get_ps" = "inactive" ]; then
    echo "Result : Good" >> $RESULT_FILE
else
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

docker info로 Swarm 상태를 확인하고 awk로 상태값만 추출한다. inactive면 양호, 그 외는 취약으로 판정한다. Swarm을 사용하지 않는데 활성화되어 있으면 불필요한 공격 면이 열려있는 셈이다.

스크립트에 Swarm manager 노드 수 확인(docker info --format '{{.Swarm.Managers}}')과 자동 잠금 키 확인(docker swarm unlock-key) 로직도 있었지만, 주석 처리했다. Swarm을 실제로 쓰는 환경이 아니면 명령 자체가 오류를 반환하기 때문이다.

D-27: SSL/TLS 적용 (Swarm 오버레이 네트워크)

bash

val=0
flag=0

get_ps=`ps -ef | grep dockerd | grep -i "tlsverity"`
if [ "$get_ps" != "" ]; then
    val=`expr $val + 1`
    flag=`expr $flag + 1`
else
    echo "Result : Vulnerable" >> $RESULT_FILE
fi

if [ "$val" = 1 ]; then
    get_network=`docker network ls --filter driver=overlay --quiet | \
      xargs docker network inspect --format '{{.Name}} {{ .Options }}' | grep "encrypted"`
    # overlay 네트워크에 encrypted 옵션이 있는지 확인
fi

if [ "$flag" = 3 ]; then
    echo "RESULT : Good" >> $RESULT_FILE
fi

D-24와 비슷하지만 Swarm의 오버레이 네트워크 TLS를 별도로 점검한다. D-24가 Docker daemon 전체의 TLS라면, D-27은 Swarm 클러스터 간 통신의 암호화 여부다.

valflag 두 카운터를 써서 단계적으로 검증한다. TLS가 확인되면 추가로 오버레이 네트워크의 encrypted 옵션과 인증서 만료 기간(Expiry Duration)을 확인하는 구조다. flag가 3이 되어야 최종 Good으로 판정한다.

고민했던 부분들

netstat vs ss: 환경에 따라 netstat이 없을 수 있다. 스크립트 초반에 두 명령 중 사용 가능한 것을 감지해서 port_cmd 변수에 담는 방식을 택했다.

if [ "command -v netstat 2>/dev/null" != '' ] || [ "which netstat 2>/dev/null" != "" ]; then
    port_cmd="netstat"
else
    port_cmd="ss"
fi

파일이 없을 때 처리: Docker 환경에 따라 존재하지 않는 파일이 있다. daemon.json이 없는 경우, /etc/default/docker가 없는 경우 등을 전부 분기 처리해야 했다. 없는 경우는 Result : Review로 수동 확인을 유도했다.

버전 비교 한계: D-01의 버전 비교 로직은 버전 번호를 하드코딩했다. 스크립트를 오래 쓰려면 외부에서 버전 정보를 받아오거나 변수로 분리하는 게 낫다.


3편에서는 이 스크립트 결과를 파싱해서 자동으로 보고서를 만드는 과정을 다룬다.