기본값이 없어야 누락이 드러난다
한 줄 요약
비밀값은 소스에 기본값을 두지 않고, 환경 변수나 파일 한 곳에서만 읽으며, 없으면 서비스가 뜨지 않게 합니다. 읽은 비밀값은 상자(Secret)에 넣어 repr 이나 디버그 화면에 찍혀도 *** 만 보이게 합니다.
왜 이 요구가 필요한가
os.environ.get("DB_PASSWORD", "devpass") 같은 코드는 편합니다. 개발자의 노트북에서는 아무 설정 없이 뜨기 때문입니다. 그런데 운영에서 환경 변수를 빠뜨리면 서비스는 아무 말 없이 개발용 값으로 뜹니다. 그 값은 저장소에 있으므로 저장소를 본 모든 사람이 압니다. 기본값이 '안전한 실패' 가 아니라 '조용한 성공' 을 만드는 셈입니다.
설정 객체를 찍는 순간도 문제입니다. 시작할 때 설정을 로그에 한 줄 남기거나, /debug/config 같은 화면을 두거나, 예외 메시지에 설정 객체가 들어가면 비밀값이 그대로 나갑니다. 아무도 비밀값을 로그에 남기려 하지 않았는데도 그렇게 됩니다.
The Twelve-Factor App 은 설정을 코드와 엄격히 나눠 환경에 두라고 권합니다. 컨테이너 오케스트레이터는 비밀값을 환경 변수 대신 파일로 넣어 주기도 합니다. 환경 변수는 같은 컨테이너의 다른 프로세스, 크래시 덤프, 디버깅 화면에 보이기 쉬워서 파일을 더 선호하는 팀도 있습니다. 그래서 NAME 과 NAME_FILE 둘 다 받는 관례가 생겼습니다.
어떻게 지키나
def read_secret(env, name):
direct, path = env.get(name), env.get(name + "_FILE")
if direct and path: # 어느 쪽이 이길지 추측하지 않는다
raise SettingsError(name, "ambiguous", ...)
... # 파일이면 권한을 보고, 끝 줄바꿈을 뗀다
if not value:
raise SettingsError(name, "missing", "%s 가 없습니다" % name)
규칙은 네 가지입니다. 비밀값에는 기본값이 없습니다. 둘 다 있으면 오류입니다. 한쪽이 조용히 이기면 운영자가 바꾼 값이 반영되지 않아도 아무도 모릅니다. 파일은 끝의 줄바꿈을 뗍니다. 편집기와 echo 가 붙이는 줄바꿈 하나 때문에 비밀번호가 맞지 않는 장애가 흔합니다. 파일 권한에 그룹이나 다른 사용자의 비트가 있으면(mode & 0o077) 받지 않습니다. 누구나 읽을 수 있는 비밀값 파일은 이미 비밀이 아닙니다.
오류 메시지에는 어느 변수가 문제인지 적고 값은 넣지 않습니다. 설정 오류는 서비스가 뜨기 전에 나므로 그 메시지는 배포 도구의 화면과 로그에 그대로 남습니다. 운영자가 바로 고칠 수 있으면서, 그 메시지가 로그로 퍼져도 안전합니다.
환경 변수와 파일 가운데 무엇을 고를지는 운영 환경이 정합니다. 중요한 것은 코드가 둘 다 받아 주되 둘이 섞이지 않게 하는 것입니다.
읽은 값은 상자에 넣습니다. Secret.reveal() 만 값을 돌려주고, repr 과 str 은 *** 를 보입니다. DB 에 접속하거나 서명하는 코드만 settings.secret("db_password") 로 꺼내 쓰므로, 비밀값이 쓰이는 자리가 코드에서 눈에 띕니다. pydantic 의 SecretStr 이 같은 생각입니다.
현장에서 만나는 모습
비밀값이 저장소에 한 번 들어가면 지우는 것으로 끝나지 않습니다. 커밋 이력과 포크와 사본에 남으므로 그 값을 바꾸는(회전하는) 것이 유일한 해결입니다. 이 저장소도 비밀값을 쿠버네티스 Secret 으로만 다루고, 소스에 API 키 모양의 글자가 있으면 배포를 막는 검사를 둡니다. 서비스가 비밀값 없이 뜨지 않게 하는 것은 그 검사의 반대편, 실행할 때의 안전장치입니다.
감사 로그에는 비밀값을 어디서 읽었는지(env 인지 file 인지)만 남기고 값과 파일 경로는 남기지 않습니다. 파일 경로에는 비밀의 이름이나 용도가 드러나기도 합니다. 앞 모듈의 '가리는 함수' 가 로그에 들어가는 값을 막는다면, 이 모듈의 상자는 값이 애초에 찍힐 수 없게 합니다.
English corner
- 「fail fast」 필요한 설정이 없으면 시작할 때 바로 실패하는 것입니다. "Fail fast on missing secrets" 처럼 씁니다.
- 「store config in the environment」 The Twelve-Factor App 의 설정 원칙을 줄인 말입니다.
- 「never committed to source code repositories」 OWASP Key Management Cheat Sheet 가 키와 비밀값에 대해 쓰는 표현입니다. 저장소에 들어갔다면 'rotate the secret' 이 다음 할 일입니다.
다음 실습에서 할 것
설정 읽기가 비밀값 두 개에 개발용 기본값을 두고, /debug/config 와 repr 이 비밀값을 그대로 보여 줍니다. 요구 A(환경 변수나 파일 한 곳, 없으면 실패)와 요구 B(파일 권한, 상자로 가리기)를 tmp_path 의 파일과 os.chmod 로 시험하고 고칩니다.