로컬 비밀 서비스의 다중 계층 방어

사이버 보안Beginner
지금 연습하기

소개

다중 계층 방어 (Defense in Depth) 란 여러 보안 제어를 결합하여 단 하나의 실수로 인해 중요한 자산이 노출되지 않도록 하는 전략입니다. 토큰 검사, 로컬 네트워크 바인딩, 파일 권한 설정, 그리고 로깅은 동일한 서비스의 서로 다른 부분을 보호합니다.

이번 실습에서는 비밀 파일을 읽어오는 소규모 파이썬 서비스를 강화해 보겠습니다. 한 번에 하나씩 작은 변경을 적용하고, 서비스를 실행한 뒤 curl로 테스트하여 각 계층이 어떻게 조화롭게 작동하는지 확인합니다.

노출된 비밀 정보에서 시작하기

이 단계에서는 서비스를 살펴보고 첫 번째 제어 장치를 확인합니다. 예상된 토큰이 없는 요청은 거부되고, 토큰이 포함된 요청은 성공하는지 확인합니다. 다중 계층 방어는 이미 보호되고 있는 부분과 추가적인 계층이 필요한 부분을 파악하는 것에서 시작됩니다.

프로젝트 디렉토리로 이동합니다:

cd /home/labex/project

준비된 파일 목록을 확인합니다:

ls -l

secret_service.py, secret.txt, service-check.py 파일이 보여야 합니다.

서비스를 백그라운드에서 실행합니다. 끝에 붙은 &는 명령어를 계속 입력하는 동안에도 서비스가 계속 실행되도록 셸에 지시합니다:

python3 secret_service.py > service.log 2>&1 &

나중에 깔끔하게 종료할 수 있도록 백그라운드 프로세스 ID 를 저장합니다:

echo $! > service.pid

토큰 없이 비밀 정보를 읽어보려 시도합니다:

curl -i http://127.0.0.1:8081/secret

응답에 403 Forbidden이 포함되어야 합니다. 이제 필수 토큰 헤더를 포함하여 요청을 보냅니다:

curl -s -H "X-Access-Token: lab-token-2026" http://127.0.0.1:8081/secret

다음과 같은 결과가 출력되어야 합니다:

Project launch code: ORBIT-742

다음 단계로 넘어가기 전에 서비스를 종료합니다:

kill "$(cat service.pid)"

토큰 계층에 대한 검사기를 실행합니다:

python3 service-check.py token

예상 결과는 다음과 같습니다:

token: complete

서비스 로컬 유지하기

이 단계에서는 네트워크 노출 계층을 확인합니다. 로컬 접근만 필요한 서비스는 127.0.0.1(localhost 라고도 함) 에 바인딩되어야 합니다. 해당 주소는 동일한 머신에서의 연결만 허용합니다.

서비스의 기본 호스트 설정을 확인합니다:

grep 'default="127.0.0.1"' secret_service.py

--host 옵션에 대한 인자 정의를 볼 수 있습니다. 이 기본값은 python3 secret_service.py를 실행하면 서비스가 localhost 에서 시작됨을 의미합니다.

서비스를 다시 시작합니다:

python3 secret_service.py > service.log 2>&1 &

프로세스 ID 를 저장합니다:

echo $! > service.pid

ss를 사용하여 수신 대기 중인 TCP 소켓을 확인합니다. -ltn 옵션은 수신 대기 (listening), TCP, 숫자 주소 (numeric addresses) 를 의미합니다:

ss -ltn | grep ':8081'

출력 결과에 127.0.0.1:8081이 표시되어야 하며, 이는 서비스가 로컬 전용임을 증명합니다:

LISTEN ... 127.0.0.1:8081 ...

서비스를 종료합니다:

kill "$(cat service.pid)"

localhost 계층에 대한 검사기를 실행합니다:

python3 service-check.py localhost

예상 결과는 다음과 같습니다:

localhost: complete

비밀 파일 제한하기

이 단계에서는 파일 시스템 권한 계층을 추가합니다. 서비스에 버그가 있더라도 비밀 파일은 모든 로컬 계정에서 읽을 수 없어야 합니다.

현재 파일 권한을 확인합니다:

ls -l secret.txt

파일 권한이 넓게 설정되어 있으므로 이를 개선할 수 있습니다. chmod 600을 사용하여 소유자만 읽고 쓸 수 있도록 변경합니다:

chmod 600 secret.txt

권한을 다시 확인합니다:

ls -l secret.txt

이제 권한 비트가 -rw-------로 시작해야 합니다:

-rw------- ... secret.txt

이 계층에 대한 검사기를 실행합니다:

python3 service-check.py permissions

예상 결과는 다음과 같습니다:

permissions: complete

거부된 요청 기록하기

이 단계에서는 탐지 계층을 테스트합니다. 권한 없는 요청을 차단하는 것도 유용하지만, 해당 거부 기록을 남기면 운영자에게 누군가 보호된 경로에 접근하려 했다는 증거를 제공할 수 있습니다.

이 단계를 깔끔하게 시작하기 위해 기존 로그 파일을 삭제합니다:

rm -f access.log

서비스를 시작합니다:

python3 secret_service.py > service.log 2>&1 &

프로세스 ID 를 저장합니다:

echo $! > service.pid

토큰 없이 요청을 보냅니다:

curl -s http://127.0.0.1:8081/secret

응답 본문은 다음과 같아야 합니다:

forbidden

접근 로그를 확인합니다:

cat access.log

로그에는 rejected, /secret, missing_or_bad_token이 포함되어 있어야 합니다.

서비스를 종료합니다:

kill "$(cat service.pid)"

로깅 계층에 대한 검사기를 실행합니다:

python3 service-check.py logging

예상 결과는 다음과 같습니다:

logging: complete

계층 통합 검증하기

이 단계에서는 다중 계층 방어의 최종 결과를 검증합니다. 서비스는 토큰을 요구하고, 기본적으로 로컬에서만 실행되며, 소유자 전용 권한으로 비밀 파일을 보호하고, 거부된 요청을 기록해야 합니다.

전체 검사기를 실행합니다:

python3 service-check.py all

모든 계층이 완료되어야 합니다:

token: complete
localhost: complete
permissions: complete
logging: complete

마지막으로 서비스를 시작합니다:

python3 secret_service.py > service.log 2>&1 &

프로세스 ID 를 저장합니다:

echo $! > service.pid

권한이 있는 요청에 대해 비밀 정보가 여전히 정상적으로 작동하는지 확인합니다:

curl -s -H "X-Access-Token: lab-token-2026" http://127.0.0.1:8081/secret

여전히 다음 결과가 보여야 합니다:

Project launch code: ORBIT-742

서비스를 종료합니다:

kill "$(cat service.pid)"

이 최종 확인은 다중 계층 방어가 의도된 정상적인 사용을 방해하지 않으면서 서비스를 보호해야 하기 때문에 중요합니다.

요약

하나의 소규모 서비스에 여러 제어 장치를 계층화하여 다중 계층 방어를 실습했습니다. 토큰 요구 사항, 로컬 전용 바인딩, 소유자 전용 파일 권한, 거부된 접근에 대한 로깅을 모두 검증했습니다.

핵심 교훈은 각 계층이 서로 다른 실패 지점을 보호하는 동시에, 서비스의 정상적인 동작은 여전히 유지되어야 한다는 점입니다.