백엔드
인증·보안
비밀 관리: 주입 지점 선택과 무중단 키 교체

비밀 관리: 주입 지점 선택과 무중단 키 교체

들어가며

.env 파일로 잘 돌아가던 서비스에서 어느 날 API 키를 바꿔야 할 일이 생깁니다. 이 키를 쓰는 서버가 몇 대인지, 지금 지워도 되는지, 누가 언제 읽었는지 바로 답하기 어렵습니다. 순서를 잘못 잡으면 키 하나를 바꾸다 서비스가 멈추기도 합니다.

이번 글에서는 비밀 관리가 다루는 범위와 값을 앱에 넣는 지점을 고르는 기준을 정리합니다. 값이 새는 자리와 서비스를 끊지 않고 키를 교체하는 순서도 함께 다룹니다.


1. 어디에 두느냐보다 누가 읽고 어떻게 바꾸느냐

비밀 관리는 API 키나 DB 비밀번호를 코드 밖 중앙 저장소에 두고 접근 통제, 감사, 교체까지 다루는 체계입니다. HashiCorp Vault, OpenBao, AWS Secrets Manager 같은 도구가 여기에 해당합니다.

.env 도 값을 코드 밖으로 빼는 역할은 합니다. 다만 서버마다 사본이 생기고, 누가 읽었는지 남지 않으며, 한 번에 바꾸기도 어렵습니다. 중앙 저장소는 앱의 신원을 확인한 뒤 허용된 값만 내주고 그 기록을 남깁니다.

비밀 저장소는 회사의 열쇠 보관함이에요. 신원을 확인하고 필요한 열쇠만 내주며, 가져간 기록을 남겨요.

🧑‍💼담당자등록·교체🗄️비밀 저장소암호화 보관🛂권한 판정앱 신원·범위📦앱허용된 값만📜감사 기록누가·언제값 등록값 전달기록
값이 한 곳에 모여 통제와 교체가 쉬워진다. 대신 저장소 운영 부담이 생기고, 저장소 자체가 중요한 장애 지점이 된다.

2. 값을 앱에 넣는 지점

주입 방식은 크게 둘입니다. 환경변수 주입은 프로세스를 띄울 때 저장소에서 값을 받아 환경에 넣는 방식입니다. 앱을 고치지 않아도 되지만, 값을 바꾸면 재기동해야 반영됩니다.

API_KEY=$(vault kv get -field=key secret/app) exec python app.py

런타임 조회는 앱이 필요할 때 저장소 API로 값을 읽는 방식입니다. 재기동 없이 교체되는 대신, 저장소 장애가 앱으로 번질 수 있습니다. 그래서 짧은 TTL 로 캐시해 조회 횟수를 줄이고, 저장소가 잠깐 멈춰도 버티게 합니다.

def get_secret(name, ttl=300):
    hit = cache.get(name)
    if hit and time.time() - hit.at < ttl:
        return hit.value
    value = client.read(name)
    cache[name] = Entry(value, time.time())
    return value

쿠버네티스처럼 값을 파일로 마운트하는 방식도 있습니다. 앱이 파일을 다시 읽어야 새 값이 반영된다는 점만 챙기면 됩니다.

환경변수 주입은 출근할 때 받는 사원증, 런타임 조회는 문 앞에서 그때그때 받는 출입 확인이에요.

주입 방식교체 반영앱 수정저장소 장애
환경변수기동 시 주입재기동 뒤없음실행 중은 유지
런타임 조회API 호출TTL 뒤필요캐시 만료 뒤 실패
파일 마운트볼륨 주입다시 읽은 뒤조금실행 중은 유지
교체가 잦으면 런타임 조회가, 단순함이 중요하면 환경변수 주입이 맞는다. 교체 속도와 저장소 장애 영향을 맞바꾸는 선택이다.

3. 저장소를 둬도 값은 샙니다

중앙 저장소는 보관 단계를 지켜 줄 뿐, 앱이 값을 받은 뒤의 일까지 막아 주지는 않습니다. 자주 새는 자리는 이미지 레이어, git 히스토리, 로그와 예외 메시지, 접속 문자열이 담긴 URL 입니다.

# 나쁨: 이미지 레이어에 값이 남는다
ENV API_KEY=<값>

# 좋음: 빌드에만 쓰고 레이어에 남기지 않는다
RUN --mount=type=secret,id=npm_token ./build.sh

Docker Compose 의 변수 치환도 자주 걸리는 지점입니다. ${API_KEY} 를 치환할 때는 셸 환경변수가 .env 파일보다 우선합니다. 파일은 고쳤는데 셸에 남은 옛 값이 들어가면 원인을 찾기 어려우므로, 컨테이너가 실제로 받는 최종 값까지 확인합니다.

4. 끊김 없는 키 교체

키 교체(로테이션)의 원칙은 새 키를 먼저 깔고, 옛 키는 마지막에 끄는 것입니다. 옛 키를 먼저 폐기하면 앱이 새 키를 받을 때까지 모든 요청이 실패합니다.

키 교체는 건물 출입카드 교체예요. 새 카드를 다 나눠 준 뒤에 옛 카드를 꺼요.

옛 키부터 폐기5분 장애
옛 키유효새 키유효앱옛 키401새 키05101520분

옛 키가 먼저 꺼져 앱이 재기동될 때까지 요청이 실패한다.

겹쳐서 교체무중단
옛 키유효폐기새 키유효앱옛 키새 키05101520분

두 키가 함께 유효한 동안 앱을 옮기고, 사용이 끊긴 뒤 옛 키를 끈다.

겹치는 구간이 끊김을 없애지만 그동안 옛 키도 계속 통한다. 유출이 확실하면 겹침을 두지 않는다(시간은 어림).
# 나쁨: 재기동이 끝날 때까지 전부 401
revoke(old_key)
put_secret("app/key", issue_key())
restart_all()
 
# 좋음: 겹쳐 두고 옮긴 뒤 폐기
put_secret("app/key", issue_key())
restart_rolling()
wait_until(no_recent_use(old_key))
revoke(old_key)

옛 키를 지워도 되는지는 키별 마지막 사용 시각으로 판단합니다. 여러 앱이 키 하나를 나눠 쓰면 어느 앱이 남았는지 구분할 수 없으므로, 키는 앱마다 따로 발급합니다.

⚠️

유출이 확실하면 순서가 바뀝니다. 잠깐의 장애보다 유출된 키가 계속 통하는 쪽이 더 위험하므로 폐기부터 하고, 감사 기록으로 사용 흔적과 영향 범위를 확인합니다.

헷갈리기 쉬운 점

  • 저장소 값을 바꾸면 앱도 바로 새 값을 쓴다는 오해. 기동할 때 주입한 값은 재기동 전까지 옛 값 그대로입니다.
  • 시크릿 매니저를 두면 유출이 끝난다는 오해. 앱이 받은 값은 여전히 로그, 예외, URL 로 샐 수 있습니다.
  • git 히스토리에서 지우면 해결된다는 오해. 이미 복제됐다고 봐야 하므로 키 폐기가 먼저입니다.

면접에서 자주 받는 질문

저장소에 접근하는 첫 번째 키는 어디에 두나요?

이른바 비밀 제로(secret zero) 문제입니다. 클라우드에서는 IAM 역할이나 워크로드 신원처럼 플랫폼이 보증하는 신원을 써서 정적 키를 없앱니다. 온프레미스에서는 범위를 좁히고 수명을 짧게 한 키를 배포 때 넣어 위험을 줄입니다.

키가 git 에 커밋된 것을 발견하면 무엇부터 하나요?

폐기와 재발급이 먼저입니다. 그다음 감사 기록으로 사용 흔적을 확인하고, 히스토리를 정리한 뒤 커밋 전 검사 도구를 걸어 재발을 막습니다.

봉투 암호화는 무엇인가요?

데이터는 데이터 키로 암호화하고, 데이터 키는 다시 마스터 키로 암호화하는 방식입니다. 마스터 키만 KMS 나 HSM 에 두면 되므로 대량의 데이터를 빠르게 처리하면서 키 교체 부담도 줄어듭니다.

정리

  • 비밀 관리는 보관뿐 아니라 접근 통제, 감사, 교체까지 다루는 체계입니다.
  • 환경변수 주입은 단순하지만 재기동이 필요하고, 런타임 조회는 교체가 빠른 대신 저장소 장애를 함께 떠안습니다.
  • 키는 앱마다 따로 발급하고, 사용 기록을 확인한 뒤 옛 키를 폐기합니다.

평소의 교체는 새 키를 먼저 깔고 옛 키를 마지막에 끄면 됩니다. 유출됐을 때만 그 순서를 뒤집습니다.