TECH NOTE · AUTOMATION OPS
서명과 공증 없이 자동 업데이트를 붙이면 원격 코드 실행 경로가 됩니다. 단계로 쪼개 서명 전에 할 수 있는 것만 하는 방법입니다.
새 버전이 나왔다고 알리고는 싶다
앱에 확장 팩을 설치할 수 있게 만들어 뒀다. 팩에는 최소 앱 버전이 적혀 있어서, 구버전 앱에서 새 팩을 열면 이렇게 막힌다.
app-version-too-old
필요 버전: 0.2.0 / 현재 버전: 0.1.0
여기까지는 잘 동작한다. 그런데 사용자가 다음에 뭘 해야 하는지 알 방법이 없다. 어디서 새 버전을 받는지 앱이 안 알려 준다.
자동 업데이터를 붙이면 해결된다. Tauri에도 있고 붙이기도 어렵지 않다.
안 붙였다. 서명과 공증이 아직 안 됐기 때문이다.
서명 없는 자동 업데이트는 위험하다
자동 업데이트가 하는 일을 풀어 보면 이렇다.
- 어딘가에서 파일을 받는다
- 그걸 실행 가능한 위치에 놓는다
- 앱을 다시 띄운다
받은 파일이 진짜인지 확인하는 단계가 빠지면, 이건 원격 코드 실행 경로다. 중간에서 응답을 바꿔치기할 수 있으면 임의의 프로그램을 사용자 기계에 설치하게 만들 수 있다.
정상적인 업데이터는 서명으로 이걸 막는다. 배포자가 개인키로 서명하고, 앱이 내장한 공개키로 확인한다. 서명 체계가 아직 없다면 그 확인이 통째로 빠진다.
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을 프론트엔드가 정하게 두지 않는다. 백엔드가 설정에서 읽고 도메인을 검사한다
- 안 정한 것을 문서에 남긴다. 없으면 왜 멈췄는지 모른다
“나중에 제대로 하자”를 문서 없이 넘기면, 나중에 그 자리를 아무도 못 찾는다.