콘텐츠로 건너뛰기
Codex

서명과 공증 전에 자동 업데이트를 붙이면 안 되는 이유

TECH NOTE · AUTOMATION OPS

서명과 공증 없이 자동 업데이트를 붙이면 원격 코드 실행 경로가 됩니다. 단계로 쪼개 서명 전에 할 수 있는 것만 하는 방법입니다.

새 버전이 나왔다고 알리고는 싶다

앱에 확장 팩을 설치할 수 있게 만들어 뒀다. 팩에는 최소 앱 버전이 적혀 있어서, 구버전 앱에서 새 팩을 열면 이렇게 막힌다.

app-version-too-old
필요 버전: 0.2.0 / 현재 버전: 0.1.0

여기까지는 잘 동작한다. 그런데 사용자가 다음에 뭘 해야 하는지 알 방법이 없다. 어디서 새 버전을 받는지 앱이 안 알려 준다.

자동 업데이터를 붙이면 해결된다. Tauri에도 있고 붙이기도 어렵지 않다.

안 붙였다. 서명과 공증이 아직 안 됐기 때문이다.

서명 없는 자동 업데이트는 위험하다

자동 업데이트가 하는 일을 풀어 보면 이렇다.

  1. 어딘가에서 파일을 받는다
  2. 그걸 실행 가능한 위치에 놓는다
  3. 앱을 다시 띄운다

받은 파일이 진짜인지 확인하는 단계가 빠지면, 이건 원격 코드 실행 경로다. 중간에서 응답을 바꿔치기할 수 있으면 임의의 프로그램을 사용자 기계에 설치하게 만들 수 있다.

정상적인 업데이터는 서명으로 이걸 막는다. 배포자가 개인키로 서명하고, 앱이 내장한 공개키로 확인한다. 서명 체계가 아직 없다면 그 확인이 통째로 빠진다.

HTTPS만으로는 부족하다. HTTPS는 전송 구간을 지키지, 파일이 내가 만든 것인지는 보증하지 않는다. 배포 저장소 계정이 털리면 HTTPS는 아무 도움이 안 된다.

그래서 첫 구현의 금지 목록을 이렇게 못 박았다.

automatic background update checks
automatic download/install
Tauri updater integration
GitHub API polling without explicit release channel decision
platform-specific installer selection
silent telemetry

안 하는 것을 먼저 적었다

설계 문서에서 목표보다 비목표(non-goals) 를 먼저 확정한 게 이 작업의 특징이다.

이유가 있다. 업데이트 기능은 요구가 자연스럽게 불어난다. “버전만 비교하자” → “그럼 자동으로 확인하자” → “이왕이면 다운로드까지” → “설치도” 로 한 걸음씩 간다. 각 걸음이 다 합리적으로 보인다.

먼저 선을 그어 두면 그 걸음마다 문서를 다시 봐야 한다.

목표 쪽도 같이 적었다.

사용자에게 다음 행동을 명확히 알린다
업데이트 동작을 명시적이고 프라이버시 보존적으로 유지한다
0.1.x에서는 명시적으로 켜지 않는 한 백그라운드 네트워크 호출을 하지 않는다
서명·공증이 정리되기 전에는 자동 업데이트 지원을 표방하지 않는다
호환 안 되는 팩은 계속 로드되지 않게 한다

네 번째가 특히 중요하다. 할 수 있는 것보다 적게 표방한다. “업데이트 지원”이라고 적어 두면 사용자는 알아서 최신이 되는 줄 안다.

단계로 쪼갠다

전부 안 하는 것도 답이 아니다. 그래서 단계를 나눴다.

A단계 — 페이지를 열어 주기만 한다.

업데이트 페이지 열기

동작은 이게 전부다.

설정된 릴리스 페이지를 시스템 브라우저로 연다
앱 프로세스에서 네트워크 호출을 하지 않는다
기존 appMinVersion 오류 외의 버전 비교를 하지 않는다
업데이트 URL이 설정 안 됐으면 버튼은 보이되 비활성

앱은 네트워크를 안 탄다. 브라우저를 띄우는 것뿐이다. 위험이 거의 없다.

B단계 — 사용자가 누르면 확인한다.

업데이트 상태 확인
사용자가 버튼을 누른다
앱이 관리자가 승인한 JSON 매니페스트를 가져온다
현재 버전과 최신 안정 버전을 비교한다
결과와 릴리스 노트 주소를 보여 준다
자동 다운로드/설치는 하지 않는다

매니페스트는 이렇게 생겼다.

{
  "desktop": {
    "latest": "0.1.1",
    "minimumSupported": "0.1.0",
    "releaseNotesUrl": "https://example.invalid/releases/desktop-v0.1.1",
    "downloads": {
      "macos": "https://example.invalid/App-macos.zip",
      "windowsMsi": "https://example.invalid/App.msi",
      "windowsNsis": "https://example.invalid/App.exe"
    }
  }
}

minimumSupported가 따로 있는 이유는 “새 버전 있음”과 “이 버전은 더 이상 안전하지 않음”이 다른 이야기라서다.

C단계는 서명이 끝난 뒤로 미뤘다. 자동 다운로드와 설치가 여기 들어간다.

URL을 렌더러가 못 만지게 한다

A단계에도 함정이 하나 있다. “브라우저로 URL을 연다”는 기능은 그 자체로 공격 표면이다.

프론트엔드가 임의의 주소를 넘길 수 있으면, 어딘가에서 주입된 문자열이 그대로 브라우저로 간다.

그래서 권한을 안 열었다.

현재 capability가 허용하는 것:
  opener:allow-reveal-item-in-dir

대신 백엔드 명령을 하나 만들고, 주소는 백엔드가 환경변수에서 읽는다. 프론트엔드는 “열어 줘”라고만 말할 수 있고 어디를 열지는 못 정한다.

백엔드는 받은 주소도 검사한다. 로컬 테스트용 loopback HTTP와 배포용 특정 도메인의 HTTPS만 통과시킨다.

무엇을 남겨 뒀는지 적는다

설계 문서 끝에 아직 안 정한 것을 남겨 뒀다.

정확한 HTTPS 서브도메인
미서명/내부 빌드에서 이 URL을 노출할지 여부
PET_UPDATE_URL의 배포 환경 값

이게 있어야 다음에 이어서 할 수 있다. 결정된 것만 적힌 문서는 “왜 여기서 멈췄나”를 안 알려 준다.

두 번째 항목이 실제 고민이었다. 서명 안 된 내부 빌드에서 업데이트 페이지를 열어 주면, 그 페이지의 정식 빌드를 받아 덮어쓰는 사람이 생긴다. 설정이 섞인다.

정리

  • 자동 업데이트는 받은 파일이 진짜인지 확인하는 단계가 핵심이다. 서명 없이 붙이면 원격 코드 실행 경로가 된다
  • HTTPS는 전송 구간만 지킨다. 파일의 출처를 보증하지 않는다
  • 업데이트 기능은 요구가 자연스럽게 불어난다. 비목표를 먼저 확정하면 걸음마다 다시 보게 된다
  • 할 수 있는 것보다 적게 표방한다. “업데이트 지원”은 자동인 줄 알게 만든다
  • 페이지 열기 → 수동 확인 → 자동 설치 순으로 쪼개면 서명 전에도 첫 둘은 할 수 있다
  • 매니페스트에 최신 버전과 최소 지원 버전을 따로 둔다. 다른 이야기다
  • URL을 프론트엔드가 정하게 두지 않는다. 백엔드가 설정에서 읽고 도메인을 검사한다
  • 안 정한 것을 문서에 남긴다. 없으면 왜 멈췄는지 모른다

“나중에 제대로 하자”를 문서 없이 넘기면, 나중에 그 자리를 아무도 못 찾는다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다