Sat 08-08 · 오늘 5건 · 통산 1,394호
falette
스크랩 쪽지 로그인
업비트 자동매매 봇 만들기
 

⑤ 업비트 자동매매 봇 만들기 — "API 키를 평문으로 두지 마세요"라고 쓴 서버에, 평문 API 키가 있…

④편을 발행한 날, 글의 맨 아래에는 이런 문장이 있었다. "API 키는 평문으로 저장하지 마세요."

같은 날, 같은 서버의 봇 설정 파일에는 업비트 API 키와 시크릿, 텔레그램 봇 토큰, 외부 AI API 키가 전부 평문으로 들어 있었다. 남에게 하라고 쓴 걸 정작 내 서버에서는 안 하고 있었다.

⑤편은 그 파일을 고치는 과정이다. 그리고 이 시리즈가 네 편 내내 미뤄둔 주제 — "24시간 돌린다"는 문장이 실제로 무엇을 뜻하는지 — 를 같이 다룬다. 두 얘기는 따로 보이지만 원인은 하나다. 봇을 죽지 않게 만드는 데만 매달리는 동안, 살아 있는 봇이 무엇을 손에 들고 있는지는 오래 들여다보지 않았다.

시리즈 지도

  1. 전략을 코드로 옮기기까지
  2. 매수를 막는 다섯 개의 관문
  3. 손절선을 세 번 끌어올린 3층 구조
  4. 시장 국면을 셋으로 나눈 결과
  5. NAS에서 24시간 돌리기, 그리고 평문으로 놓여 있던 키 지금 이 편
  6. 4개월 결산 — 숫자를 다 모아놓고 다시 보기

"24시간 돌린다"가 실제로 뜻하는 것

자동매매 글에서 가장 가볍게 쓰이는 표현이 "24시간 자동으로 돌아갑니다"다. 코드를 다 짜고 나면 이 문장이 공짜로 따라오는 것처럼 들린다. 실제로는 여기서부터가 별개의 작업이었다.

처음 시도는 부팅 시 자동 실행이었다. NAS 재부팅 후 봇이 알아서 올라오게 만들면 끝나는 문제라고 봤다. 되지 않았다. 예약 실행 항목이 붙는 시점에는 아직 네트워크와 볼륨 마운트가 준비되지 않아서, 프로세스는 뜨는데 API 호출과 DB 접근이 전부 실패한 상태로 뜬다. 로그만 보면 "실행 중"이라 더 나쁘다.

그래서 구조를 바꿨다. 부팅 시 한 번 띄우는 방식을 버리고, 주기적으로 살아 있는지 확인해서 죽어 있으면 다시 띄우는 감시 스크립트로 옮겼다. 이 방식은 부팅 순서를 신경 쓸 필요가 없다. 준비가 안 됐으면 그 회차에 실패하고 다음 회차에 다시 시도한다.

감시 스크립트가 실제로 하는 일 세 가지

  1. 생존 확인 — 프로세스 이름만 보면 안 된다. 껍데기만 남아 아무 일도 안 하는 상태에서도 프로세스는 존재한다. 그래서 최근 로그가 갱신됐는지, 즉 일을 하고 있는지를 같이 본다.
  2. 중복 실행 차단 — 이게 가장 중요하다. 감시 스크립트가 판정을 잘못해서 봇을 하나 더 띄우면, 같은 계좌에 같은 신호로 두 번 주문이 들어간다. 실행 중 표식 파일을 두고 그 안의 프로세스 번호가 실제로 살아 있는지까지 확인한 뒤에만 새로 띄운다.
  3. 알림 — 재시작이 일어났다는 사실을 텔레그램으로 보낸다. 이게 없으면 봇이 하루에 열 번 죽었다 살아나도 모른다.

세 번째가 특히 중요했던 이유는, 감시 스크립트를 붙이고 나면 장애가 조용해진다는 데 있다. 자동 복구는 문제를 없애는 게 아니라 문제를 안 보이게 만든다. 알림을 붙이지 않으면 "잘 돌고 있다"와 "매 시간 죽었다가 되살아나는 중이다"가 구분되지 않는다.

봇이 죽는 방식은 생각보다 다양했다

프로세스는 살아 있는데 아무것도 못 하는 상태

가장 자주 본 형태다. 거래소 API 응답이 지연되거나 끊겼을 때, 요청 제한 시간을 걸지 않은 코드는 그 자리에서 무한정 기다린다. 프로세스는 정상, CPU 점유율은 0, 매매는 정지. 감시 스크립트가 프로세스 존재 여부만 봤다면 영원히 못 잡을 상태였다. 로그 갱신 시각을 생존 판정에 넣은 게 이 때문이다.

봇이 아니라 밑바닥이 내려가는 시간대

운영하면서 알게 된 것 중 가장 황당한 항목이다. 이 NAS에는 하루 한 번, 정해진 20분쯤의 구간에 웹서버 계열 프로세스가 함께 내려가는 증상이 있다. 시스템 시각 동기화 동작과 맞물린 펌웨어 쪽 문제로, 동기화 주기를 어떻게 바꿔도 재현됐다. 사용자 설정으로 해결되는 항목이 아니었다.

다행인 점은 봇 본체가 웹서버와 별개 프로세스라는 것이다. 이 구간에 죽는 건 대시보드와 웹 화면이고, 매매 루프 자체는 영향을 받지 않는다. 대신 그 시간대에는 서버에서 아무 작업도 하지 않는다는 운영 규칙이 하나 생겼다. 하필 그때 파일을 고치고 있으면, 내 변경 때문에 웹이 죽은 건지 원래 죽는 시간인지 구분할 방법이 없다.

이 항목은 자동매매와 직접 관계가 없어 보이지만, 개인 서버에서 봇을 돌리는 사람에게는 결국 같은 종류의 문제다. 내가 통제할 수 없는 계층이 하나 밑에 깔려 있다는 것. 클라우드로 옮기면 이 문제는 사라지지만 다른 종류의 통제 불가 계층으로 바뀔 뿐이다.

같은 종류의 프로세스가 두 벌 떠 있던 문제

대시보드 쪽에서 발견한 건데, 패키지로 설치한 PHP 실행기와 시스템에 원래 있던 PHP 실행기가 동시에 살아 있었다. 설정 파일을 고쳐도 반응이 없다가 어느 순간 반영되는 이상한 증상의 원인이 이거였다. 어느 쪽이 실제로 요청을 처리하는지 모르는 상태로 한쪽 설정만 계속 고치고 있었다.

개인 서버 운영에서 자주 나오는 실수 유형이다. 설치 경로가 여러 개인 환경에서는 고친 파일이 정말 그 프로세스가 읽는 파일인지부터 확인해야 한다. 이걸 건너뛰면 아무 효과 없는 수정을 며칠 반복한다.

4개월 동안 실제로 몇 번 죽었나

인상이 아니라 기록으로 확인했다.

구분발생 횟수비고
감시 스크립트에 의한 자동 재시작
응답 없음(무반응) 상태로 판정된 건
수동 개입이 필요했던 건
웹/대시보드만 내려간 건

그리고 설정 파일을 열었다

④편을 쓰려고 국면별 성과를 집계하던 중이었다. 봇이 어떤 기준으로 국면을 판정하는지 확인하려고 설정 파일을 열었고, 필요한 항목 바로 위에 업비트 API 키와 시크릿이 그냥 문자열로 있었다. 텔레그램 봇 토큰도, 외부 AI API 키도 같은 파일에 나란히 있었다.

처음 만들 때는 이게 자연스러웠다. 설정을 한 파일에 모아두면 값을 바꿔도 코드를 안 건드려도 되고, 봇을 재시작하지 않고 설정만 다시 읽는 구조를 만들기도 쉽다. 편의를 위한 선택이었고, 그 선택을 한 뒤로 4개월 동안 그 파일을 보안의 관점에서 다시 본 적이 없었다.

여기서 중요한 건 "평문이라서 위험하다"는 문장이 아니다. 실질적인 위험의 크기는 그 파일에 누가 닿을 수 있는지로 결정된다. 아무도 닿을 수 없는 곳의 평문과, 웹 브라우저로 주소만 치면 열리는 곳의 평문은 완전히 다른 사건이다.

확인해야 했던 세 가지 경로

  1. 웹으로 직접 열리는가 — 이 설정 파일이 웹 문서 폴더 안쪽에 있었다. 그러면 브라우저에서 주소를 치는 것만으로 파일 내용이 그대로 나올 수 있다. 가장 나쁜 경우이고, 가장 먼저 확인해야 하는 항목이다.
  2. 다른 취약점을 통해 읽히는가 — 웹으로 직접 안 열려도, 같은 서버의 다른 스크립트에 파일 읽기 취약점이 하나 있으면 결국 같은 결과가 된다. 웹 서비스와 봇이 한 서버에 있다는 게 이 지점에서 대가를 요구한다.
  3. 백업 안에 남아 있는가 — 원본만 고치면 끝이 아니다. 그동안 만든 백업 파일들 안에 옛 키가 평문으로 계속 살아 있다. 백업은 보통 원본보다 관리가 느슨하다.

고친 순서

이 작업에서 순서를 잘못 잡으면 고치는 중에 사고가 난다. 예를 들어 키를 먼저 재발급하면 봇이 즉시 인증 실패로 멈추는데, 새 키를 넣을 안전한 자리가 아직 없는 상태다. 반대로 저장 구조만 예쁘게 바꾸고 재발급을 생략하면, 이미 노출됐을 가능성이 있는 키를 계속 쓰는 셈이 된다.

  1. 접근 차단부터. 설정 파일을 웹으로 열 수 없게 웹서버 단계에서 차단하고, 파일 권한을 소유자만 읽을 수 있게 줄인다. 코드를 한 줄도 고치지 않고 즉시 할 수 있는 조치라 가장 먼저 한다.
  2. 저장 자리를 분리. 키를 설정 파일에서 빼내 별도 환경변수 파일로 옮기고, 코드는 그 값을 환경에서 읽게 바꾼다. 설정 파일에는 전략 파라미터만 남는다. 이렇게 하면 앞으로 설정 파일을 열어 보여주거나 백업해도 키가 따라가지 않는다.
  3. 키 재발급. 새 구조가 준비된 뒤에 거래소에서 키를 새로 발급하고, 이전 키를 폐기한다. 노출 여부가 확실하지 않을 때는 노출됐다고 가정하는 쪽이 맞다. 재발급 비용은 거의 없고 사고 비용은 계좌 전체다.
  4. 흔적 정리. 옛 키가 남아 있는 백업 파일과 로그를 찾아 정리한다. 이 단계를 빼면 앞의 세 단계가 무의미해질 수 있다.

정직하게 남겨두는 부분

지금 한 것은 암호화가 아니다. 환경변수 파일도 결국 평문이다. 달라진 건 그 평문에 닿을 수 있는 경로가 줄었다는 것뿐이고, 이걸 "키를 암호화했다"고 말하면 안 된다. 제대로 하려면 운영체제 수준의 자격증명 저장소나 별도 시크릿 관리 도구를 써야 하는데, 이 장비 환경에서는 그 선택지가 마땅치 않았다.

더 근본적으로는, 웹사이트와 매매 봇이 같은 장비에 있다는 구조 자체가 이 문제의 배경이다. 웹 쪽에 구멍이 하나 생기면 그 구멍이 계좌 권한까지 이어질 수 있다. 이건 파일 하나 고쳐서 없어지는 문제가 아니고, 이 시리즈 안에서 해결할 계획도 아직 없다.

이번 편에서 배운 것

네 편 동안 이 시리즈가 다룬 건 전부 돈을 잃지 않는 방법이었다. 매수 조건을 좁히고, 손절선을 계단으로 쌓고, 국면에 따라 파라미터를 바꿨다. 그러는 동안 정작 계좌 자체를 남에게 넘겨줄 수 있는 파일 하나는 4개월 내내 그냥 놓여 있었다.

손절선을 1%p 조정해서 얻는 기대값과, 키가 유출돼서 잃는 금액은 자릿수가 다르다. 그런데 전자에는 몇 주를 썼고 후자는 발견하기까지 4개월이 걸렸다. 자동매매를 만들 때 시간을 어디에 쓰는지가 왜곡되기 쉽다는 걸, 이번에는 내 서버로 확인했다.

개인 서버에서 봇을 돌리는 사람에게 권하는 최소 점검

키가 들어 있는 파일이 웹 주소로 열리는지 브라우저로 직접 확인해 보기. 그 파일 권한이 소유자 전용인지 보기. 그리고 백업 폴더에 옛 키가 남아 있는지 찾아보기. 세 가지 다 10분 안에 끝나고, 셋 중 하나라도 걸리면 그게 지금 가장 급한 일이다.

다음 편

⑥편은 결산이다. ①편부터 여기까지 나온 숫자를 한자리에 모아놓고, 4개월을 통과한 이 봇이 결국 무엇이었는지 정리한다. 승률과 순수익, 벤치마크 대비 성과, 그리고 지금까지 다섯 편에서 발견한 설계 착오들이 각각 얼마만큼의 손익으로 환산되는지. 구조를 하나씩 뜯어봤으니, 이제 합쳐서 볼 차례다.

댓글 0
첫 번째 댓글을 남겨보세요
댓글 작성
LOGIN