비밀값은 밖에서 읽고, 찍혀도 보이지 않게
목표
서비스 설정의 비밀값을 환경 변수·파일로 분리하고 찍히지 않게 감싸는 요구를 시험으로 적고 고칩니다.
왜 중요한가
비밀값의 개발용 기본값은 운영에서 설정을 빠뜨렸을 때 조용히 그 값으로 뜨게 만들고, 그 값은 저장소를 본 모두가 압니다. 설정 객체가 로그나 디버그 화면에 찍히는 순간 비밀값이 나가므로, 값이 애초에 찍힐 수 없게 상자에 넣습니다.
단계
/opt/app/appsec/sk-config-lab/의 파일을/root/appsec/sk-config-lab/로 복사하고python3 -m pytest -q test_functional.py로 기존 기능 시험이 통과하는지 봅니다.REQUIREMENTS.md를 끝까지 읽습니다./root/appsec/sk-config-lab/test_security.py에 요구 A(비밀값은 NAME 이나 NAME_FILE 한 곳에서만, 없거나 둘 다 있으면 SettingsError)를 확인하는 시험을test_a_로 시작하는 이름으로 씁니다. 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 합니다.service.py를 고쳐 요구 A 를 지킵니다. 채점기는 숨은 요구 A 시험과 여러분의test_a_시험을 함께 돌립니다.- 요구 B(비밀값 파일은 주인 전용 권한만, repr·public_dict 에는 *** 만)를 확인하는 시험을
test_b_로 시작하는 이름으로 더합니다. 판정은 2단계와 같습니다. service.py를 고쳐 요구 B 도 지킵니다.test_functional.py가 그대로 통과하는지 봅니다. 채점기는 숨은 기능 시험도 함께 돌립니다.REQUIREMENTS.md의 '감사 로그' 표대로audit(...)을 부릅니다(읽기는 config.loaded(출처만), 오류는 config.error, 값과 파일 경로는 남기지 않음).- 채점기가 그럴듯한 오답을 하나씩 끼워 여러분의
test_security.py를 돌립니다. 오답마다 시험이 하나 이상 실패해야 합니다.
참고
load_settings에는 사전을 넘깁니다. 비밀값 파일은tmp_path에 쓰고os.chmod(path, 0o600)처럼 권한을 정하세요.- 실습 파드에서 여러분은 root 지만 파일 권한 비트는 그대로 읽힙니다. 권한 확인은
os.stat(path).st_mode로 합니다. - 흔한 실수: 개발용 기본값을 남기는 것, 파일 끝 줄바꿈을 떼지 않는 것, 둘 다 있을 때 한쪽이 조용히 이기는 것, public_dict 에서 값을 꺼내 싣는 것.
- 시험은
cd /root/appsec/sk-config-lab && python3 -m pytest -q test_security.py로 돌립니다.-k test_a_를 붙이면 요구 A 시험만 돕니다. - 정답 보기의
python3 /opt/app/appsec/apply_fix.py service.py는 조각에 든 함수만 이름으로 찾아 바꿉니다. 직접 고친 다른 함수는 그대로 남습니다. - 실습 파드에는 볼륨이 없어 세션이 끝나면
/root/appsec가 사라집니다. 남기고 싶은 시험은 세션 안에서 옮겨 두세요.
재료를 복사하고 기능 시험을 돌린다
/opt/app/appsec/sk-config-lab/ 의 파일을 모두 /root/appsec/sk-config-lab/ 로 복사하고, 그 디렉터리에서 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는 것을 본다. REQUIREMENTS.md 를 끝까지 읽는다.
cp 로 디렉터리의 파일을 통째로 옮깁니다. 고치기 전에 지금 무엇이 되는지부터 확인해 두면, 나중에 무엇을 깨뜨렸는지 가릴 수 있습니다.
요구 A 를 시험으로 쓴다
/root/appsec/sk-config-lab/test_security.py 에 요구 A(비밀값은 NAME 이나 NAME_FILE 한 곳에서만, 없거나 둘 다 있으면 SettingsError)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 쓴다. 이 시험은 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 한다.
DB_PASSWORD 가 없을 때 SettingsError 가 나는지(variable·reason 까지), 끝에 줄바꿈이 있는 파일에서 읽은 값이 줄바꿈 없이 나오는지, 환경 변수와 파일이 둘 다 있을 때 ambiguous 인지 확인하세요.
요구 A 를 지키도록 고친다
/root/appsec/sk-config-lab/service.py 를 고쳐 요구 A 를 지킨다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 고친 코드에 돌린다.
SettingsError 에 variable 과 reason 을 두고, read_secret(env, name) 이 두 출처를 가려 (값, 출처) 를 돌려주게 하세요. 파일은 rstrip("\r\n") 으로 끝 줄바꿈을 떼고, 빈 값은 missing 입니다.
요구 B 를 시험으로 쓴다
/root/appsec/sk-config-lab/test_security.py 에 요구 B(비밀값 파일은 주인 전용 권한만, repr·public_dict 에는 *** 만)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더한다. 02 단계처럼 지금 코드에서 실패하고 올바른 코드에서 통과해야 한다.
권한 0o644·0o640 인 파일은 permissions 오류, 0o600·0o400 은 통과인지 보세요. repr(settings) 와 json.dumps(settings.public_dict()) 에 비밀값이 없는지도 확인합니다.
요구 B 를 지키도록 고친다
/root/appsec/sk-config-lab/service.py 를 고쳐 요구 B 도 지킨다. 채점기는 숨은 요구 B 시험과 여러분의 test_b_ 시험을 고친 코드에 돌린다.
stat.S_IMODE(os.stat(path).st_mode) & 0o077 이 0 이 아니면 거절합니다. Secret 클래스의 reveal() 만 값을 주고 repr·str 은 *** 를 돌려주게 한 뒤, Settings.secret 은 reveal() 을, public_dict 는 Secret 이면 *** 를 내게 합니다.
기존 기능이 그대로인지 본다
고친 service.py 로 python3 -m pytest -q test_functional.py 가 그대로 통과하는지 본다. 채점기는 재료의 원본 기능 시험에 숨은 기능 시험을 더해 돌린다.
보안 수정이 정상 사용자를 막으면 그 수정은 되돌려집니다. 실패한 기능 시험의 이름이 어떤 정상 동작을 막았는지 알려 줍니다. 막을 것만 막았는지 요구사항의 '그대로 된다' 문장과 견주어 보세요.
감사 로그를 남긴다
REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부른다(읽기는 config.loaded(출처만), 오류는 config.error, 값과 파일 경로는 남기지 않음). 남기지 말라고 한 값은 어떤 항목에도 넣지 않는다.
load_settings 에서 SettingsError 를 잡아 audit('config.error', variable=..., reason=...) 를 남기고 다시 냅니다. 다 읽으면 config.loaded 에 이름별 출처(env 또는 file)만 남깁니다.
그럴듯한 오답을 시험이 붙잡는지 본다
채점기가 기준 코드에 그럴듯한 오답을 하나씩 끼워 /root/appsec/sk-config-lab/test_security.py 를 돌린다. 오답마다 시험이 하나 이상 실패하도록 필요한 시험을 더한다.
채점 메시지가 어떤 오답이 빠져나갔는지 알려 줍니다. 한 경로만 고친 수정이면 다른 경로를, 비교 방법이 틀린 수정이면 그 비교를 확인하는 시험을 더하세요. 요구사항의 문장 하나하나가 시험 하나가 될 수 있습니다.