비밀값을 다루는 손
설정 파일은 가려서 출력하는 물건이 아니다
작은 조직 AI·서버 보안 기본기의 첫째 갈래입니다. AI와 함께 서버를 만지다 보면 API 키와 토큰이 든 설정 파일을 매일 엽니다. 그 손이 비밀을 흘린 자리를 모았습니다.
1. 가려서 출력하면 고장 난 줄에서 샌다
현장 기록에 따르면 한 번은 AI가 설정 파일을 「값만 가려서」 열었는데, 줄 하나의 형식이 깨져 있어서 API 키가 그대로 화면에 찍혔습니다. 가림 처리는 파일이 정상이라는 가정 위에 서 있습니다. 그런데 우리가 설정 파일을 여는 까닭은 대개 무언가 고장 났기 때문입니다.
알려진 패턴을 더 넣어 막았더니 모르는 형식의 봇 토큰이 하나 더 있었고, 두 번째 키도 새어 다시 발급받았습니다. 기록에 남은 결론은 필터를 촘촘하게 다듬는 쪽이 답이 아니라는 것입니다. 이 규칙은 키 21개를 한꺼번에 화면에 띄운 첫 사고에서 시작됐습니다.
따라 하기
- 키가 있는지만 볼 때는 그 이름이 몇 번 나오는지 개수만 셉니다.
- 한 줄이 꼭 필요하면 라벨로 줄 번호를 먼저 찾고 그 줄만 엽니다. 「앞뒤 몇 줄」을 함께 출력하지 않습니다.
- 값 대신 변수 이름만 뽑아 보면 형식이 깨진 줄도 바로 드러납니다.
- 설정 파일을 읽어 들이는 스크립트를 디버깅 추적 모드로 돌리면 추적 출력에 모든 키가 찍히므로 그렇게 돌리지 않습니다.
2. 로그와 예외, 프로세스 목록으로 샌다
외부 API의 예외 객체는 요청 주소 전체를 품고 있습니다. 사고 기록에는 봇 토큰이 들어간 주소가 예외와 함께 서비스 로그에 고스란히 남은 일이 적혀 있습니다. 로그에는 예외를 통째로 넣지 않고 가림 함수를 거칩니다. 호출하는 쪽이 잊어도 잡히도록 로그 포매터 단계에서 한 번 더 바꿔 줍니다.
원격 명령 문자열 안에 비밀번호를 넣으면 원격 기계의 프로세스 목록에 평문으로 뜹니다. 외부 명령의 인자에 토큰을 넣어도 마찬가지입니다. 이런 값은 표준입력으로 넘기고, 더 나은 길은 애초에 비밀번호가 필요 없게 만드는 것입니다.
환경변수로 넘긴 토큰은 그 프로세스 자신이 다시 읽을 수 있습니다. AI 에이전트에게 파일 읽기 권한을 줄 때는 읽을 수 있는 경로를 좁히고, 프로세스 정보가 드러나는 시스템 경로를 막습니다. 막혔는지는 에이전트에게 실제로 읽어 보게 해서 「막힘」인지 「읽힘」인지만 받아 확인합니다.
코딩 에이전트의 작업 폴더도 격리가 아닙니다. 시험 기록에 따르면 에이전트는 폴더 밖의 절대경로를 그대로 읽었습니다. 외부 클라우드 모델을 쓸 때는 운영체제 수준에서 가둡니다. 전용 비특권 계정을 만들고, 홈 디렉터리를 잠그고, 쓸 수 있는 도구를 줄인 뒤, 가짜 비밀 파일을 하나 놓고 정말 막히는지 시험합니다.
3. 비밀이 숨는 자리: 훅, 백업 사본, 채팅창
저장할 때마다 자동으로 도는 훅 스크립트에 키가 평문으로 박혀 있던 일이 있었습니다. 늘 실행되는 코드일수록 키가 드러나는 면이 넓습니다. 이 키를 걷어내려고 고치기 직전에 떠 둔 복사본이 또 하나의 평문 사본이 됐습니다.
비밀을 걷어내는 수정의 백업 사본은 그 자리에서 지웁니다. 그리고 코드에서 걷어냈어도 발급처에서 새로 발급받기 전에는 고친 것이 아닙니다.
토큰 발급을 사람에게 안내할 때는 「받은 값은 파일에 저장하고 그 경로만 알려 달라」를 같은 자리에서 말합니다. 그 말이 없으면 사람은 받은 값을 채팅창에 붙여 넣습니다. 가능하면 토큰 대신 SSH 키를 등록하게 합니다. 쌓인 토큰 목록을 정리할 때는 넷으로 나눕니다. 바로 폐기할 것, 만료로 정리할 것, 배포에 쓰고 있어 절대 건드리지 말 것, 그대로 둘 것입니다.
4. 「저장했다」는 되읽기의 결과다
사고 기록에 따르면 새 비밀번호를 자격 파일에 정규식으로 저장한 일이 있었습니다. 파일의 구분자를 잘못 짐작해서 한 건도 바뀌지 않았는데 오류는 나지 않았고, 그 상태로 새 값이 담긴 임시 파일을 지웠는데, 그 파일이 유일본이었습니다.
파일 형식을 눈으로 먼저 보고, 치환이 몇 건 일어났는지 확인하고, 다시 읽어서 대조한 뒤에만 유일본을 지웁니다. 저장은 쓰기의 결과가 아니라 되읽기의 결과입니다.
5. 접속이 막히면 쥔 열쇠부터 전부 대 본다
가진 자격증명 셋 가운데 둘만 대 보고 「없다, 새로 만들자」로 넘어간 일이 있었습니다. 접속이 막히면 형식과 상관없이 쥔 열쇠 목록을 먼저 만들고 하나도 빠짐없이 시도합니다. 비밀번호 재설정처럼 되돌리기 어려운 변경은 그다음에 합니다.
6. 연동 로그인은 권한을 넘겨주는 일이다
도메인 소유를 확인하는 화면이 「DNS 업체에 로그인」 버튼을 첫째로 권한 일이 있었습니다. 필요한 것은 TXT 레코드 한 줄이었지만, 그 버튼이 넘기는 범위는 도메인 영역 전체의 쓰기 권한이었습니다. 다음 단계에서는 메일 수신 설정 전환까지 자동으로 따라 들어갈 수 있었습니다. 그래서 「다른 방법」을 골라 값만 받고, 그 한 줄을 직접 넣었습니다.
필요한 최소값과 버튼이 넘기는 범위가 다르면 값만 받아 내 손으로 넣습니다. 이것은 남에게 주는 권한 이야기입니다. 우리가 직접 쓸 토큰은 반대로 발급할 때 필요한 권한을 다 담아 둡니다. 방향이 반대인 두 원칙을 섞지 않습니다.
7. 사람이 매번 비밀번호를 전달하는 구조는 없앤다
링크와 비밀번호를 사람이 손으로 전달하는 단계는 그 자리에 손이 비는 날 조용히 실패합니다. 가입은 조직 코드로 하게 하고, 비밀번호 찾기는 본인의 복구 메일로 하게 합니다. 구조를 짤 때 「잊으면 누가 불려 나오는가」를 먼저 따져 봅니다.
도구 메모
- 키가 있는지 세기:
grep -c '^키이름=' .env - 변수 이름만 보기:
grep -oE '^[A-Za-z_][A-Za-z0-9_]*=' .env - 원격에 비밀번호 넘기기: 명령 문자열에 넣지 말고 표준입력으로