TT Lab
블로그 시작하기
배우기 러닝패스 코스

비밀번호·키·비밀값 다루기

비밀값은 밖에서 읽고, 찍혀도 보이지 않게

TT Lab 에서 이어서 보기

목표

서비스 설정의 비밀값을 환경 변수·파일로 분리하고 찍히지 않게 감싸는 요구를 시험으로 적고 고칩니다.

왜 중요한가

비밀값의 개발용 기본값은 운영에서 설정을 빠뜨렸을 때 조용히 그 값으로 뜨게 만들고, 그 값은 저장소를 본 모두가 압니다. 설정 객체가 로그나 디버그 화면에 찍히는 순간 비밀값이 나가므로, 값이 애초에 찍힐 수 없게 상자에 넣습니다.

단계

  1. /opt/app/appsec/sk-config-lab/ 의 파일을 /root/appsec/sk-config-lab/ 로 복사하고 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는지 봅니다. REQUIREMENTS.md 를 끝까지 읽습니다.
  2. /root/appsec/sk-config-lab/test_security.py 에 요구 A(비밀값은 NAME 이나 NAME_FILE 한 곳에서만, 없거나 둘 다 있으면 SettingsError)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 씁니다. 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 합니다.
  3. service.py 를 고쳐 요구 A 를 지킵니다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 함께 돌립니다.
  4. 요구 B(비밀값 파일은 주인 전용 권한만, repr·public_dict 에는 *** 만)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더합니다. 판정은 2단계와 같습니다.
  5. service.py 를 고쳐 요구 B 도 지킵니다.
  6. test_functional.py 가 그대로 통과하는지 봅니다. 채점기는 숨은 기능 시험도 함께 돌립니다.
  7. REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부릅니다(읽기는 config.loaded(출처만), 오류는 config.error, 값과 파일 경로는 남기지 않음).
  8. 채점기가 그럴듯한 오답을 하나씩 끼워 여러분의 test_security.py 를 돌립니다. 오답마다 시험이 하나 이상 실패해야 합니다.

참고

재료를 복사하고 기능 시험을 돌린다

/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 를 돌린다. 오답마다 시험이 하나 이상 실패하도록 필요한 시험을 더한다.

채점 메시지가 어떤 오답이 빠져나갔는지 알려 줍니다. 한 경로만 고친 수정이면 다른 경로를, 비교 방법이 틀린 수정이면 그 비교를 확인하는 시험을 더하세요. 요구사항의 문장 하나하나가 시험 하나가 될 수 있습니다.