Devin.KR

비밀 파일과 환경별 설정

95분 안팎

학습 목표

일반 설정과 비밀을 별도 경로로 전달합니다.

개념

비밀을 설정과 같은 파일로 취급하지 않습니다

환경마다 안내판 이름과 포트가 다른 것은 공개해도 되는 일반 설정입니다. 요청 인증 토큰은 값이 알려지면 접근을 허용하므로 다른 저장 경로와 전달 경로가 필요합니다. 앞 모듈의 CI는 이미지를 만들고 checksum을 남겼습니다. 이미지에 토큰을 함께 넣으면 토큰 교체 때 이미지를 다시 만들게 되고 이전 이미지 계층에도 값이 남을 수 있습니다. 이번 레슨은 이미지 내용과 비밀 전달을 분리하여 검증한 이미지 ID는 유지하면서 실행 시 전용 파일 경로를 연결하는 방법을 다룹니다.

환경 변수에는 값 대신 파일 경로를 넣습니다

NOTICE_SECRET_FILE은 /run/lab-secrets/token이라는 경로를 전달합니다. 토큰 자체를 HCL 변수나 env 목록에 넣지 않습니다. 환경 변수의 값은 컨테이너 inspect에서 보일 수 있고 IaC 입력은 state나 저장 plan에 남을 수 있기 때문입니다. sensitive 표시만으로 파일 저장 자체가 사라지는 것은 아닙니다. 이번 HCL은 secret_dir이라는 저장소 밖의 절대 경로만 입력으로 받습니다. 그 경로도 운영 환경에서는 민감한 메타데이터일 수 있으므로 state 보호는 그대로 유지합니다.

입력 디렉터리의 경계를 만듭니다

비밀 전용 디렉터리는 lab 압축 해제 위치 밖의 개인 경로에 둡니다. 그 안의 current 디렉터리에 token 파일 하나만 둡니다. 홈 전체나 회사 설정 폴더를 마운트하면 서비스가 필요 없는 자료까지 보게 됩니다. prepare.py는 CI 산출물의 무결성과 이미지 ID를 확인하고 외부 경로를 tfvars에 기록합니다. 경로가 실습 루트 안이면 거부합니다. 값은 이후 check.sh에서 생성하여 저장소 소스나 빌드 문맥에 들어갈 기회를 줄입니다. 입력 산출물 checksum은 신뢰한 CI 경계 안의 무결성 검사이며 발신자 인증은 아닙니다.

부모 경로 권한과 파일 권한을 함께 설계합니다

호스트 비밀 부모 디렉터리는 0700으로 만들어 다른 호스트 계정의 진입을 막습니다. 안쪽 current 디렉터리는 0755로 두어 컨테이너 UID가 진입할 수 있게 하고 이 경로만 연결합니다. token 파일은 0444로 두어 UID 10001이 읽게 합니다. 파일 단독으로 보면 읽기 범위가 넓지만 호스트 부모 경로의 진입 제한과 컨테이너 노출 범위를 함께 적용한 교육 구성입니다. 읽기 전용 마운트는 서비스가 token 내용을 바꾸지 못하게 합니다. Docker Desktop과 Linux의 파일 공유 동작 차이는 실제 외부 검사로 확인하며 문서 계산만으로 읽기 성공을 단정하지 않습니다.

HCL에는 디렉터리 마운트를 추가합니다

docker_container.notice의 volumes 블록은 host_path에 var.secret_dir 아래 current 경로, container_path에 /run/lab-secrets, read_only에 true를 넣습니다. 기존 /data 볼륨 블록은 유지합니다. 데이터 볼륨은 서비스가 써야 하고 비밀 볼륨은 읽기만 해야 하므로 두 블록의 의도가 다릅니다. user는 10001:10001로 명시합니다. port의 loopback 제한과 전용 네트워크도 앞 모듈대로 보존합니다. 보안을 추가하면서 기존 서비스 공개 범위나 데이터 보존 조건을 실수로 바꾸지 않도록 새 plan에서 영향 대상을 확인합니다.

Java 시작 코드의 호환 범위를 읽습니다

SecretGate는 notice.secret-file 설정으로 파일 경로를 받습니다. 경로 미설정은 앞 모듈의 공개 서비스와 기존 테스트를 유지하기 위한 호환 모드입니다. 이번 미션에서는 설정이 필수이고 check.sh가 토큰 없는 /notices 요청의 401을 확인하므로 설정 누락이 성공으로 처리되지 않습니다. 설정된 경로를 못 읽거나 내용이 짧으면 503으로 닫습니다. 읽기 오류를 빈 문자열과 같다고 취급하여 허용하면 보호 장치가 실패했을 때 서비스가 공개됩니다. 호환 코드와 배포의 보안 조건을 서로 구분합니다.

요청 인증은 좁은 경로에서 시작합니다

교육 서비스는 /notices만 X-Lab-Token 헤더로 보호합니다. /health는 배포 상태 확인을 위해 공개하고 토큰을 반환하지 않습니다. 잘못된 헤더 또는 누락된 헤더는 401이며 파일 자체가 잘못되면 503입니다. 비교는 표준 라이브러리 MessageDigest.isEqual을 사용합니다. 이 코드는 클라우드 IAM이나 전체 사용자 인증 체계를 구현한 것이 아닙니다. 로그인·세션·권한 모델을 대신한다고 주장하지 않고 서비스에 비밀을 전달하여 접근 성공과 실패를 검증하는 제한된 실험으로 설명합니다.

브라우저 주소와 명령 인자에 토큰을 넣지 않습니다

쿼리 문자열의 토큰은 접근 로그와 브라우저 기록에 남기 쉽습니다. curl 명령 인자에 토큰을 직접 넣으면 셸 기록이나 프로세스 목록에 남을 가능성도 있습니다. 제공 검사기는 Python에서 파일로 읽어 HTTP 헤더를 구성하며 값은 stdout에 출력하지 않습니다. Authorization을 포함한 헤더 전체 덤프도 피합니다. 요청 결과를 조사할 때 상태 코드와 승인된 경로 레이블만 남깁니다. 편의를 위해 env 전체를 로그로 저장하는 진단 코드는 이번 과제의 노출 검사에서 실패해야 합니다.

서비스 변경은 CI를 다시 거칩니다

m05의 이미지는 SecretGate를 포함하지 않습니다. 따라서 기존 image_id만 다시 사용하면 토큰 없는 요청이 200으로 성공해 인증 검사에서 실패합니다. m06 app에서 ./mvnw test를 실행하고 자기 변경 ID로 bash ci.sh를 실행하여 새 이미지와 산출물을 만듭니다. prepare.py는 이 새 artifacts를 입력으로 받습니다. 기존 CI의 테스트 실패 차단과 archive checksum을 유지합니다. 새 코드가 필요한 보안 변경과 실행 시 비밀 교체를 구분하면 어떤 경우 이미지 재빌드가 필요한지 판단할 수 있습니다.

서비스 교체 계획과 데이터 보존을 검토합니다

m05에서 실행 중인 컨테이너를 이어받으면 이미지나 마운트 변경 때문에 service 교체가 나올 수 있습니다. 미션 검사는 docker_container.notice의 삭제와 생성이 함께 있는 교체만 허용하고 네트워크와 볼륨 삭제는 거부합니다. 이는 이번 교육 변경의 허용 범위이며 모든 삭제를 허용하는 규칙이 아닙니다. 저장 plan을 검토한 뒤 그 파일만 apply합니다. 새 개인 project로 시작하면 초기 생성 계획을 확인합니다. 기존 state를 잃어버린 채 같은 이름으로 자원을 새로 만들려 하지 않습니다.

실패 상태로 원인을 좁힙니다

starter는 NOTICE_SECRET_FILE이 존재하지 않는 missing 경로를 가리킵니다. 정상 토큰을 보냈는데 503이면 파일 경로·마운트·읽기 권한을 먼저 봅니다. 401이면 헤더가 전달되었는지와 현재 토큰이 맞는지 봅니다. 200이면서 헤더가 없으면 설정 누락이나 옛 이미지 여부를 조사합니다. 토큰 값 자체를 화면에 찍어 비교하는 방식은 노출을 새로 만듭니다. 상태 코드와 이미지 ID, 파일 존재, UID처럼 값을 드러내지 않는 정보로 원인을 좁히는 습관을 익힙니다.

교체 가능한 디렉터리 연결을 선택합니다

파일 하나만 bind mount한 뒤 호스트에서 원자적으로 rename하면 컨테이너가 이전 파일을 계속 볼 수 있습니다. 이 실습은 디렉터리를 연결하고 같은 디렉터리의 임시 파일을 token으로 교체합니다. Java는 매 요청마다 현재 파일을 읽으므로 새 토큰이 다음 요청부터 적용됩니다. 실제 시스템의 캐시나 재로딩 주기는 별도 설계 대상입니다. read_only는 컨테이너 쪽 쓰기를 막고 호스트 배포 담당자의 교체까지 막지 않습니다. 두 주체가 같은 경로에서 수행하는 작업의 차이를 이 실행으로 확인합니다.

마운트와 실행 사용자 속성의 출처: Docker provider 4.6 공식 문서. 실제 HCL 파싱과 마운트 접근 결과는 외부 검증에서 확인합니다.

따라하기

설정 항목 분리

경로는 설정으로, 값은 외부 파일로 관리한다는 계약을 자료로 확인합니다.

settings={'NOTICE_NAME':'demo-board','NOTICE_SECRET_FILE':'/run/lab-secrets/token'}
print('token value in settings:', any('lab-token-value' in v for v in settings.values()))
print(settings['NOTICE_SECRET_FILE'])

실행 결과

token value in settings: False
/run/lab-secrets/token

새 서비스 테스트

주입 실습 zip의 app에서 Java 테스트를 실행합니다. SecretGateTest는 누락 파일·오류 헤더·교체·빈 파일을 검사합니다.

./mvnw test

이미지와 비밀 경로 준비

app에서 자기 변경 ID로 CI를 완료한 뒤 zip 루트로 돌아옵니다. 외부 전용 경로를 입력합니다. 값은 아직 입력하지 않습니다.

bash app/ci.sh 자기_40자리_변경ID
python3 prepare.py app/artifacts bc-yourname /절대경로/비밀디렉터리

마운트 경로 수정과 검사

README대로 provider lock을 만들고 main.tf의 missing 경로를 token으로 고칩니다. bash check.sh에서 정상 요청 200·헤더 누락 401·비밀 쓰기 거부·교체 검사를 확인합니다.

bash check.sh

확인 문제

실습

m05에서 확장한 zip의 Java 테스트와 CI를 실행하여 새 artifacts를 만듭니다. prepare.py에 외부 전용 디렉터리를 입력하고 README의 lock 생성 절차를 따릅니다. starter의 잘못된 비밀 경로를 수정합니다. check.sh는 실제 마운트·UID·HTTP·교체·노출을 확인합니다. 앞 단계를 한 번에 실행하려면 bash prepare-and-check.sh를 사용합니다. 비밀 디렉터리는 홈 폴더 아래 SECRET_DIR로 지정하며, 명령 앞의 BOOTCAMP_AUTOCLEAN=1은 자동 검증기가 끝난 뒤 자원을 지우는 용도라 학습자는 붙이지 않습니다.

시작 코드·테스트 내려받기

실행 명령

BOOTCAMP_AUTOCLEAN=1 bash prepare-and-check.sh

기대 결과

저장소 밖 파일의 읽기 전용 주입·정상 헤더 200·누락 헤더 401·파일 누락 503과 실제 교체를 검사합니다.

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 환경마다 달라지는 설정을 주입하는 방법을 설명합니다.
  • 비밀 값을 이미지와 IaC 상태 파일에서 분리하는 이유는 무엇인가요?