한 일

보안 알림 오탐을 막았다. 어제 붙인 보안 감시는 수상한 경로(/.env 같은) 접근을 잡아 디스코드로 알린다. 그런데 내가 직접 접속해도 알림이 떴다. 그래서 신뢰하는 IP 목록(MONITOR_IP_ALLOWLIST)을 만들어, 그 IP는 감지에서 빼게 했다. 내 PC IP를 등록한 뒤, 그 IP로 /.env에 접근해도 카운터가 안 오르는 걸 라이브로 확인했다.

봇이 스스로 업데이트하게 만들었다. 이게 이날의 큰 일이었다. 그동안 봇 로직은 안드로이드 폰에 ADB로 밀어넣어 배포했다. 문제는 폰을 재부팅하면 ADB 연결이 끊겨서 배포가 막힌다는 거였다. 그래서 방향을 바꿨다. 폰이 서버에서 봇 로직을 직접 받아오게 했다.

  • 봇 로직을 payload.js로 떼어내고, 거기에 서명 파일(payload.js.sig)을 붙임
  • 폰에는 작고 안 바뀌는 bootstrap.js만 둠 — 5분마다 서버를 확인해서 새 로직을 받음
  • 받은 로직은 sha256 해시 + RSA 서명으로 검증한 뒤에만 실행. 통과 못 하면 거부하고 캐시로 폴백
  • RSA-2048 키쌍을 활성용(A)·예비용(B) 두 벌 만듦. 개인키는 저장소 밖에 두고, .gitignore*.pem 커밋을 막음
  • 서버에 봇 로직을 내려주는 API(/api/bot/version, /api/bot/script)를 추가

실기기에서 검증했다. 변조한 로직은 거부하고, 정상 로직은 서명검증 통과 후 잘 돌았다. 핵심은 재부팅 테스트였다. 폰을 껐다 켜도 ADB 없이 로직을 다시 받아 검증하고 실행하는 걸 확인했다. 이제 배포가 폰 재부팅에 안 묶인다.

테스트와 스테이징 환경을 깔았다. 운영에 바로 손대는 게 무서웠다. 그래서 안전망을 만들었다.

  • 서버 코드를 정리해서, 라우터·미들웨어를 app.js로 떼고 부팅 로직만 따로 둠
  • jest로 통합 테스트 37개를 작성. 임시 MariaDB를 띄워서 돌림. 37/37 통과
  • 운영과 똑같은 구성의 스테이징 VM을 따로 띄움(e2-small, 메모리 2GB). 전용 도메인도 연결
  • 배포 흐름을 나눔: main에 올리면 운영으로, dev에 올리면 스테이징으로 자동 배포. 배포 전에 테스트를 통과해야 넘어가게 게이트를 둠
  • Terraform을 환경 무관 모듈 + 환경별(prod/dev) 구성으로 다시 짬

스테이징은 5개 서비스가 다 뜨고 HTTPS 인증서까지 자동 발급되는 걸 확인했다. 마지막으로 devmain으로 합쳐(PR #29) 운영 배포까지 성공했다. 헬스체크 200, 폰은 ADB 없이 새 로직을 받는 것도 확인했다.

막힌 것, 고친 것

devmain으로 합치려는데 충돌이 났다. 내가 작업하는 사이 다른 변경(다이어그램·문서)이 먼저 main에 들어와 있었다. 그래서 순서를 바꿨다. main의 최신 내용을 dev로 먼저 가져와 충돌을 푼 다음, PR을 합쳤다. 한 가지 더, 직전 작업분이 최신 main보다 한참(약 30커밋) 뒤처진 베이스 위에 있었다. 그 위에 더 쌓는 대신, 최신 main 기준으로 테스트·스테이징을 통째로 다시 만들었다.

돌아보며

어제는 “옮기는 날"이었고, 오늘은 “단단하게 하는 날"이었다.

가장 마음에 드는 건 자가업데이트다. 그동안 봇 배포는 폰에 너무 묶여 있었다. 폰을 재부팅하면 끊기고, 그때마다 손이 갔다. 이제 폰은 서버를 보고 알아서 받아온다. 재부팅 후에도 ADB 없이 스스로 살아나는 로그를 봤을 때, 묶여 있던 걸 하나 풀어낸 기분이었다.

서명검증을 굳이 넣은 것도 마음에 든다. 폰이 서버에서 코드를 받아 실행한다는 건, 잘못하면 위험한 통로가 된다. 그래서 받은 코드가 진짜 내 것인지 매번 확인하게 했다. 변조한 로직을 거부하는 걸 눈으로 보니 안심이 됐다.

어제 “큰일을 하루에 몰지 말자"고 다짐했다. 오늘은 그래서 테스트와 스테이징을 먼저 깔았다. 이제 운영을 만지기 전에 한 번 걸러지는 단계가 생겼다. 안전하게 가는 길을 하나 더 깐 셈이다.