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

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

기본값이 없어야 누락이 드러난다

TT Lab 에서 이어서 보기

한 줄 요약

비밀값은 소스에 기본값을 두지 않고, 환경 변수나 파일 한 곳에서만 읽으며, 없으면 서비스가 뜨지 않게 합니다. 읽은 비밀값은 상자(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

다음 실습에서 할 것

설정 읽기가 비밀값 두 개에 개발용 기본값을 두고, /debug/config 와 repr 이 비밀값을 그대로 보여 줍니다. 요구 A(환경 변수나 파일 한 곳, 없으면 실패)와 요구 B(파일 권한, 상자로 가리기)를 tmp_path 의 파일과 os.chmod 로 시험하고 고칩니다.