
이 블로그 프로젝트 백엔드는 관리자 인증이나 세션처럼 민감한 부분이 있어서 EC2 서버에 직접 올리고 프론트는 굳이 AWS? 라는 생각으로 배포가 편한 Vercel을 썼다.
그런데 정말 기가 막히게도, 그렇게 하고 몇일 안된 지금 Vercel 내부 시스템 침해 소식이 떴다. 편하게 가려다가 이런 이슈를 맞으니 솔직히 좀 어이없고 짜증이 났다.
오늘 대응의 시작은 GeekNews에서 본 이 뉴스였다.
기사 내용을 간단히 정리하면 이랬다.
- Vercel 내부 시스템에 무단 접근이 있었다.
- 일부 고객이 영향을 받았을 가능성이 있다.
- 고객들에게 활동 로그 확인과 환경변수 교체가 권고됐다.
- 원인으로는 third-party AI tool의 Google Workspace OAuth 앱 침해가 언급됐다.
이걸 보고 가장 먼저 든 생각은 단순했다.
"프론트 쪽 환경변수는 당연히 바꿔야 하고, 문제는 백엔드 secret이 어디까지 분리돼 있느냐"였다.
내 서비스는 프론트와 백엔드 인증 구조가 완전히 같은 방식은 아니다.
- 프론트는 Next.js 관리자 세션 쿠키용 secret을 쓴다.
- 실제 관리자 로그인은 백엔드
/api/v1/admin/login과 백엔드 세션에도 의존한다. - 즉 프론트 secret을 바꿨다고 해서 백엔드 관리자 인증까지 같이 바뀌는 구조는 아니다.
그래서 오늘은 프론트 환경변수만 바꾸고 끝내지 않고, 백엔드도 따로 점검했다.
백엔드는 JWT signing key를 쓰는 구조가 아니고, ADMIN_PASSWORD와 Redis 기반 관리자 세션 토큰으로 인증이 이루어진다.
즉 백엔드 쪽에서 실제로 중요한 값은 최소한 아래 정도였다.
ADMIN_PASSWORDDATABASE_URLPOSTGRES_PASSWORDREDIS_URL- 스토리지 관련 값
- Cloudflare Access 관련 값
대신 중요한 점도 하나 있었다. Vercel에 올라가 있는 구조가 아니었다. 실제 운영은구조였고, 프론트와는 분리돼 있었다.
백엔드는 EC2 + Docker Compose + GitHub Actions self-hosted runner 으로 올려둬서 사실 이번 Vercel 사고가 곧바로 백엔드 secret 유출로 이어지는 구조는 아닐거라 생각했지만. 내가 틀릴수도 있으니깐... 그냥 넘길 수는 없었다.
나도 모르게 같은 값을 다른 곳에 복사해뒀을 수도 있고, 그냥 환경변수를 다 교체해야 내마음이 편할거 같았기 때문이다.
그래서 실제 운영 서버에 들어가 EC2 쪽 .env와 로그를 직접 확인했다.
내가 확인한 건 대략 이런 것들이었다.
- 운영
.env에 어떤 값들이 들어 있는지 - 관리자 비밀번호와 DB 연결 정보가 어떻게 되어 있는지
- 스토리지 access key를 실제로 쓰는지
- 2026-04-19 ~ 2026-04-20 사이 수상한 로그인이나 배포 흔적이 있는지
- 최근 env 변경 흔적이 있는지
이 과정에서 몇 가지를 확인했다.
첫째, 운영 백엔드는 정적 S3 access key를 쓰고 있지 않았다.
S3_ACCESS_KEY, S3_SECRET_KEY는 비어 있었고, 문서상으로도 EC2 IAM Role 기반 운영을 전제로 하고 있었다.
적어도 스토리지 키까지 당장 회전해야 하는 상황은 아니었다.
둘째, 백엔드 관리자 secret은 따로 존재했다.
즉 프론트에서 secret을 바꿨다고 해서 백엔드 관리자 인증이 자동으로 안전해지는 구조는 아니었다.
그래서 백엔드 ADMIN_PASSWORD도 바로 교체했다.
셋째, 관리자 세션도 따로 무효화가 필요했다.
비밀번호만 바꿔서는 이미 발급된 Redis 기반 세션이 남아 있을 수 있었기 때문이다.
그래서 Redis에 저장된 admin_session:* 키들을 삭제해서 기존 관리자 세션을 무효화했다.
여기까지는 비교적 예상한 대응이었다. 문제는 그 다음에 생겼다.
운영 .env를 바꾸고 배포 스크립트를 다시 돌렸는데, 헬스체크가 계속 실패했다.
처음엔 컨테이너가 덜 떴나 싶었는데, 로그를 보니 원인은 더 명확했다.
PostgreSQL 비밀번호 인증 실패였다.
처음엔 조금 헷갈렸다.
분명 .env 안의 POSTGRES_PASSWORD와 DATABASE_URL은 같은 새 값으로 맞췄는데, 왜 인증이 실패하는지 바로 이해가 안 됐다.
그런데 원인은 생각보다 단순했고, 동시에 아주 전형적인 운영 이슈였다.
원인은 이거였다.
.env의POSTGRES_PASSWORD는 Postgres가 처음 만들어질 때만 쓰인다.- 이미 데이터 볼륨이 살아 있는 Postgres는 나중에
.env를 바꿔도 DB 내부 사용자 비밀번호를 자동으로 바꾸지 않는다. - 그래서 앱은 새 비밀번호로 붙으려 하고, DB는 예전 비밀번호를 유지하고 있어서 인증 실패가 난다.
즉 이번 장애는 단순히 환경변수 값을 바꿨다고 끝나는 문제가 아니었다. 운영 중인 DB는 실제로 DB 안의 사용자 비밀번호도 같이 바꿔줘야 했다.
그래서 DB 컨테이너에 직접 들어가서 실제 사용자 비밀번호를 명시적으로 변경했다.
psql접속ALTER USER hbr WITH PASSWORD '새비밀번호';- 이후 다시 재배포
이 작업은 데이터 삭제와는 무관하다. 테이블이나 게시글 데이터가 날아가는 게 아니라, PostgreSQL 사용자 계정의 비밀번호만 바꾸는 작업이다.
DB 내부 비밀번호를 맞춘 뒤 다시 배포하니, API 컨테이너가 정상적으로 올라왔고 헬스체크도 통과했다. 결국 이번 대응은 단순한 secret rotation이 아니라, 운영 중인 상태 저장 시스템의 비밀번호를 어떻게 안전하게 바꾸는지까지 다시 확인하는 과정이 됐다.
그리고 여기서 끝내지 않고 프론트 버전도 확인했다.
내 프론트 상태는 대략 이랬다.
next:16.1.1eslint-config-next:16.1.1app/디렉터리를 사용하는 App Router 구조
문제는 이 버전이 최신 보안 패치 기준으로는 낮았다는 점이었다.
확인 결과, 16.1.1은 App Router 관련 취약점 대응 버전보다 낮았고, 패치 버전은 16.2.3이었다.
그래서 프론트도 바로 업그레이드했다.
next16.1.1→16.2.3eslint-config-next16.1.1→16.2.3
업그레이드 후에는 빌드까지 직접 확인했고, 아래 핵심 흐름도 다시 점검했다.
/post 관리자 로그인과 /api/admin/session 기반 세션 유지가 정상인지 확인글 작성, 임시저장, 수정, 발행 상태 변경까지 관리자 핵심 플로우를 한 번씩 확인.
결과적으로 오늘 한 대응을 정리하면 이렇다.
- Vercel 보안 사고 뉴스 확인
- 프론트 환경변수 교체
- 백엔드 secret 구조 분리 여부 확인
- EC2 운영 환경변수 점검
- 백엔드
ADMIN_PASSWORD교체 - Redis 관리자 세션 전량 무효화
- 운영 로그와 로그인 이력 확인
- DB 비밀번호 변경 과정에서 생긴 장애 원인 확인
- PostgreSQL 내부 사용자 비밀번호 직접 변경
- 백엔드 재배포 및 정상화
- 프론트 Next.js
16.2.3업그레이드 - 프론트 핵심 관리자 플로우 재확인
오늘 대응하면서 다시 느낀 건 두 가지였다.
첫째, secret rotation은 그냥 값만 바꾸는 일이 아니다. 그 값이 어디에 저장돼 있는지, 어떤 프로세스가 그 값을 쓰는지, 상태를 가지고 있는 시스템이 있는지까지 같이 봐야 한다. 특히 DB 비밀번호는 환경변수만 바꾼다고 끝나지 않았다.
둘째, 프론트만 외부 플랫폼에 올려두는 선택도 이제는 단순히 "배포가 편하다" 정도로만 생각하면 안 되겠다는 생각이 들었다. 내 코드에 문제가 없어도, 플랫폼 쪽 incident가 터지면 결국 내가 직접 구조를 이해하고 영향 범위를 판단해야 한다.
백엔드를 EC2에 두고 프론트를 Vercel에 둔 선택이 완전히 틀렸다고 생각하진 않는다. 하지만 적어도 앞으로는 "어디를 외부 서비스에 맡길지"를 지금보다 더 보수적으로 보게 될 것 같다.
오늘은 일단 필요한 대응은 모두 끝냈다. 환경변수도 바꿨고, 관리자 세션도 무효화했고, 백엔드도 정상화했고, 프론트 버전 업과 핵심 기능 확인도 마쳤다.
이런 날이 오면 늘 드는 생각은 비슷하다.
기능 개발보다 더 중요한 건, 무슨 일이 터졌을 때 지금 내 서비스가 어떤 구조인지 정확히 이해하고, 어디까지 영향을 받는지 판단한 뒤, 안전하게 복구하는 능력이라는 것.