자동 업데이트 기능의 보안 기본 - 나쁜 패턴과 베스트 프랙티스

· · Windows 개발, 보안, updater, 자동 갱신, 서명, MSIX, ClickOnce

1. 먼저 결론

꽤 거칠지만 실무에서 쓰기 쉬운 표현으로 하면 이렇습니다.

  • 요건이 맞는다면 우선 MSIX App Installer나 ClickOnce 같은 기존의 갱신 기반을 우선한다
  • 자체 updater가 필요하다면 먼저 넣어야 할 것은 UI가 아니라 서명 검증과 실패 시 복구
  • latest.json 같은 갱신 정보는 미서명 설정 파일이 아니라 서명된 metadata로서 다룬다
  • TLS는 필요하지만 충분 조건이 아니다
  • 갱신 판정은 「서버가 그렇게 말하고 있으니까」가 아니라 「클라이언트가 검증해 올바르다고 판단할 수 있으니까」로 한다
  • 서명 키는 개발용과 운영용을 분리하고, HSM이나 서명 서비스로 보호한다
  • 갱신 실패 시는 fail-open이 아니라 fail-closed로 한다
  • 롤백 대책이 없는 updater는 취약한 버전으로 되돌려지는 것을 전제로 생각하는 편이 안전
  • 서명 검증을 아직 넣을 수 없는 단계라면 자동 갱신보다 수동의 서명 완료 installer 배포 쪽이 안전

요컨대 자동 업데이트의 핵심은 「어떻게 다운로드할 것인가」가 아니라 「무엇을 신뢰하고, 어디서 검증하고, 부서졌을 때 어떻게 되돌릴 것인가」입니다.

2. 왜 자동 업데이트는 위험 영역인가

통상의 기능은 앱 안에 닫혀 있습니다. 한편 updater는 다음 3가지를 한꺼번에 가집니다.

  1. 외부에서 파일을 가지러 간다
  2. 그 파일을 신뢰한다
  3. 기존의 실행물을 치환한다

즉, 임의 코드 실행의 경로가 제품 안에 처음부터 엮여 있다는 것입니다.

여기서 흔한 오해가 「HTTPS니까 안전」이라는 것입니다. 물론 TLS는 필요합니다. 다만 그것으로 지킬 수 있는 것은 주로 통신 경로와 접속 대상의 정당성입니다. 갱신 서버 자체가 침해됐다, 잘못된 산출물이 정규 CDN에 놓였다, 미서명 manifest가 교체됐다는 이야기에는 그것만으로는 부족합니다.

실제로 TUF가 정리하고 있는 위협만으로도 갱신계에는 다음 같은 것이 있습니다.

  • 임의의 부정 소프트를 넣게 한다
  • 취약한 오래된 버전으로 되돌리는 rollback
  • 신판을 보이지 않게 하는 freeze
  • 서로 정합되지 않는 metadata와 산출물을 섞는 mix-and-match

즉 자동 업데이트는 「파일 전송」이 아니라 「신뢰의 배포」입니다. 여기를 설계해야 비로소 자동 갱신이 안전하게 돌기 시작합니다.

3. 나쁜 패턴

먼저 실무에서 자주 보는 위험한 형태를 정리합니다.

나쁜 패턴 무엇이 위험한가 최소한의 고치는 방식
version.json을 HTTPS로 취하고, URL의 zip / exe를 그대로 실행 origin 침해, 설정 교체, 오배포에 약하다 서명 붙은 metadata와 산출물의 클라이언트 검증으로 바꾼다
바이너리만 서명하고 manifest는 미서명 URL, version, channel, 필수 갱신 플래그를 개찬할 수 있다 version / hash / size / channel / expiry를 포함한 signed manifest로 한다
서명 키를 개발 PC나 CI의 파일에 둔다 침해되면 정규 서명 붙은 멀웨어를 배포받는다 HSM / 서명 서비스 + 승인 플로 + 감사 로그
갱신 실패 시에 「검증 에러를 무시하고 속행」 사고 시에 가장 약한 경로가 열린다 fail-closed로 한다
구판을 남기지 않고 덮어쓰기 갱신 전원 단절, 디스크 부족, 도중 실패로 기동 불능 staging + atomic activate + rollback
버전 비교만으로 오래된 버전을 허용 취약 버전으로의 rollback이 통한다 단조 증가하는 release version과 최고 기지 버전의 보존
updater 전체를 관리자 권한으로 움직인다 침해 시의 피해 범위가 넓다 다운로드/검증은 저권한, 치환만 최소 helper로 분리
차분 갱신부터 시작한다 구현이 복잡해 검증 누락이 늘어난다 우선은 풀패키지 갱신부터 시작

이하 조금 자세히 봅니다.

3.1 HTTPS니까 괜찮아, 로 멈춘다

이것이 가장 많습니다.

  • 기동 시에 latest.json을 읽는다
  • downloadUrl을 꺼낸다
  • zip / exe를 떨어뜨린다
  • 전개해 치환한다
  • 종료

겉보기는 그럴듯하지만 신뢰의 뿌리가 서버 응답에 너무 의지하고 있습니다. 갱신 서버나 배포 설정이 침해되면, 올바른 HTTPS 위에서 부정한 갱신을 배포받을 수 있습니다.

TLS는 필요합니다. 다만 TLS만으로 updater의 설계는 끝나지 않습니다.

3.2 서명은 하고 있지만 클라이언트 측에서 검증하지 않는다

릴리스 시에 파일에 서명하고 있어도 클라이언트가 그것을 보지 않으면 의미가 없습니다.

흔한 것은,

  • CI에서는 서명하고 있다
  • 하지만 updater는 hash밖에 보고 있지 않다
  • 게다가 그 hash 자체가 미서명 manifest에서 온다

는 형태입니다.

이것이면 manifest를 교체당한 시점에 hash도 함께 바꿔치기됩니다. 「hash를 보고 있으니까 안전」은 hash의 출처까지 지켜서야 비로소 성립합니다.

3.3 manifest가 미서명

갱신계에서 정말 지켜야 할 것은 실행 파일 그 자체만이 아닙니다. 적어도 다음 정보는 개찬되면 위험합니다.

  • version / release id
  • 다운로드 대상의 URL이나 파일명
  • hash / size
  • channel(stable / beta 등)
  • 필수 갱신인지 어떤지
  • 적용 가능한 OS / 아키텍처
  • metadata의 유효 기한
  • 최소 필요 updater version

갱신 판단에 쓰는 정보는 전부 signed metadata에 넣는 정도의 감각이 딱 맞습니다.

3.4 서명 키의 취급이 거칠다

갱신 기능의 안전성은 꽤 높은 비율로 키 관리의 안전성입니다.

운영 서명 키가 다음처럼 놓여 있다면 꽤 위험합니다.

  • 개발 PC의 증명서 스토어에 계속 들어 있다
  • CI의 secret으로서 .pfx를 업로드
  • 여러 사람이 같은 비밀 키를 로컬에 배포
  • 개발용 서명과 운영용 서명이 같은 신뢰 사슬

이것이면 updater 자체가 올바르더라도 「정규 서명된 부정 갱신」을 멈출 수 없습니다.

3.5 구판을 남기지 않는 덮어쓰기 갱신

갱신은 성공 시보다 실패 시의 설계가 중요합니다.

  • 다운로드 도중에 끊어졌다
  • 전개에 실패했다
  • 치환 도중에 전원 단절됐다
  • 신판은 기동됐지만 초회 migration에서 떨어졌다

이때 구판이 사라져 있으면 복구가 무거워집니다. 실무에서는 「갱신에 실패했다」는 사실보다 「현장에서 앱이 기동되지 않게 됐다」 쪽이 문제가 됩니다.

3.6 rollback을 생각하고 있지 않다

서명된 정규 버전이어도 오래된 취약 버전이면 공격자에게 편리할 수 있습니다.

예를 들어,

  • version 1.8에 기지 취약성이 있다
  • 현장은 2.3로 올라가 있다
  • 공격자가 1.8을 재배포한다

이것이 통하면 서명 자체는 올바른데 위험합니다.

「서명되어 있는가」만이 아니라 「그 버전을 지금 넣어도 되는가」까지 보지 않으면 부족합니다.

3.7 fail-open

운영에서 가장 해서는 안 되는 것이 이것입니다.

  • 서명 검증에 실패하면 경고만 내고 속행
  • 증명서 기한 에러를 무시할 수 있는 hidden flag가 있다
  • 디버그용의 skipVerify=true가 운영에도 남는다

장애 시나 공격 시일수록 이런 빠져나갈 길이 본명이 됩니다.

4. 베스트 프랙티스

4.1 우선 기존의 갱신 기반에 탄다

자체 updater가 정말 필요한가는 먼저 의심하는 편이 안전합니다.

Windows라면 요건이 맞는 한 다음을 우선 검토하기 쉽습니다.

  • MSIX + App Installer
  • ClickOnce
  • Store / MDM / 사내 배포 기반
  • MSI + 기업 측의 배포 관리

이유는 단순해서 갱신 자체의 책임 범위를 어느 정도 플랫폼에 치우칠 수 있기 때문입니다. 물론 자유도는 내려가지만, 갱신 UI, 배포 manifest, 패키지 서명, 운영과의 정합이 취하기 쉬워집니다.

자체 updater가 필요해지는 것은 예를 들어 다음 같은 때입니다.

  • stable / beta / preview의 복수 채널을 엄밀히 제어하고 싶다
  • 단계 배포나 rollout 율을 가지고 싶다
  • 독자의 업무 사정으로 갱신 타이밍을 세밀하게 제어하고 싶다
  • MSIX / ClickOnce에 탈 수 없는 구성이 있다

이 경우에도 「자유도가 필요하다」가 아니라 「갱신 책임을 우리가 가진다」고 이해하는 편이 흔들리지 않습니다.

4.2 신뢰의 기점을 클라이언트 측에 가진다

안전한 updater는 서버 응답을 그대로 신용하지 않습니다. 클라이언트 측에 적어도 다음 2가지가 필요합니다.

  1. 신뢰하는 공개 키나 증명서 체인
  2. 그 키로 서명된 metadata를 검증하는 구조

요컨대 「서버가 최신판이라고 말하고 있다」가 아니라 「이 metadata는 신뢰하고 있는 서명자가 낸 최신판이다」라고 클라이언트가 확인할 수 있는 상태를 만들 필요가 있습니다.

4.3 signed metadata를 중심으로 설계한다

최소한 갱신 metadata에는 다음을 넣어 서명 대상으로 합니다.

항목 넣는 이유
release version / release id rollback 방지, 감사
artifact 이름, URL, package type 어느 파일을 취할지를 고정한다
hash, size 개찬 검지, 망가진 배포의 검지
channel stable에 beta를 섞지 않는다
대상 OS / architecture 오배포 방지
minimum updater version protocol 변경 시에 오래된 updater를 멈춘다
expires_at freeze 대책
published_at 감사, 구분
mandatory / optional 갱신 UX의 분기도 개찬 불가로 한다

여기서 중요한 것은 갱신의 판단 재료를 전부 signed metadata에 집약하는 것입니다. 로직은 클라이언트에 있고, 정보의 진정성은 서명으로 지킨다는 형태로 치우치면 사고가 줄어듭니다.

4.4 산출물 그 자체도 검증한다

metadata를 검증한 뒤, 다운로드한 산출물에서도 다음을 확인합니다.

  • size
  • hash
  • 패키지 서명 / 코드 서명
  • 발행원이나 기대하는 식별자

Windows의 PE / MSI / MSIX를 다룬다면 AuthentiCode나 패키지 서명의 검증을 클라이언트 측에서 하는 전제로 하는 편이 안전합니다. macOS라면 Developer ID와 notarization을 갱신 경로에서도 전제로 하는 편이 흔들리지 않습니다.

4.5 키는 기능이 아니라 운영으로 지킨다

키 관리는 구현보다 운영에서 차이가 납니다.

최소한 다음은 나누는 편이 안전합니다.

  • 개발용 서명 키
  • 스테이징용 서명 키
  • 운영용 서명 키

또한 운영용은,

  • HSM
  • 클라우드 서명 서비스
  • 승인 플로 붙은 signing system
  • 감사 로그
  • key rotation 절차
  • timestamp 붙은 서명

까지 포함해 설계하고 싶습니다.

「운영 빌드가 통과하면 CI가 자동 서명한다」는 편리하지만 침해 시의 피해 반경도 커집니다. 적어도 누가 무엇을 언제 서명했는지는 쫓을 수 있게 해 둬야 합니다.

운영이 올라오면 좀처럼 바꾸지 않는 root trust와 빈번하게 재서명하는 갱신 metadata의 키를 나누면 더욱 안전합니다. root를 offline 쪽에 유지하고 갱신 metadata에는 별도 키를 쓰는 설계는 키 침해 시의 피해 반경을 내리기 쉬워집니다.

4.6 fail-closed와 staged update

갱신 플로는 다음 순서가 기본입니다.

  1. metadata를 취득
  2. 서명과 유효 기한과 version을 검증
  3. 산출물을 staging 영역에 다운로드
  4. hash / size / 서명을 검증
  5. 구판을 남긴 채 activation 준비
  6. 재기동 시나 전용 helper에서 전환
  7. 초회 기동의 건전성 확인
  8. 문제가 있으면 rollback

여기서 중요한 것은, 검증이 끝나기 전에 치환하지 않는다 실패하면 진행하지 않는다 의 2가지입니다.

4.7 updater의 권한을 좁힌다

updater 전체를 관리자 권한으로 움직이는 것은 피하고 싶습니다.

이상은 다음 분리입니다.

  • 다운로드와 검증: 저권한
  • 실제 파일 치환만: 최소 권한을 가진 helper
  • helper는 「검증 완료 package를 소정 장소에 두는」 것 이상의 일을 하지 않는다

권한 승격이 필요한 설계일수록 승격 전에 무엇을 검증 완료로 했는지를 분명히 나누지 않으면 위험해집니다.

4.8 rollback / freeze / mix-and-match를 처음부터 막는다

여기는 나중에 더하면 괴로우므로 처음에 넣는 편이 좋습니다.

  • rollback 대책
    클라이언트는 「지금까지 본 최고의 metadata version / release version」을 보유하고, 그것보다 오래된 것을 거부

  • freeze 대책
    metadata에 expiry를 가지게 하고, 너무 오래된 metadata를 거부

  • mix-and-match 대책
    metadata끼리의 정합성을 가지게 한다. 적어도 manifest 자체에 대상 artifact의 hash / size / version을 고정한다

또한 특정 build를 거부하는 blocklist나 minimum allowed version을 signed metadata로 배포할 수 있으면 사고 시의 봉쇄가 빨라집니다.

TUF를 그대로 채택하지 않아도 이 3가지 성질은 꽤 중요합니다.

4.9 처음은 풀 갱신부터 시작한다

차분 갱신은 대역에는 효과가 있지만 처음의 구현으로서는 복잡합니다.

  • 어느 구판에서 어느 신판으로 적용하는 차분인가
  • 차분 적용 전의 전제 hash
  • 차분 적용 후의 최종 hash
  • 도중 실패 시의 복구
  • 부분 적용이나 오래된 차분의 청소

이쯤이 한꺼번에 늘어납니다. 초기 버전에서는 서명 완료 풀패키지를 안전하게 교체하는 곳까지로 충분합니다.

5. 최소 안전 구성

풀 TUF만큼 거창하게 하지 않더라도 자체 updater의 최소 안전 구성은 대체로 다음이 됩니다.

5.1 클라이언트가 가지는 것

  • 신뢰하는 root 공개 키 또는 고정한 증명서 체인
  • 현재 동작 중인 version
  • 과거에 본 최고의 metadata version / release version
  • 허가하는 channel
  • rollback용의 직전 버전

5.2 서버가 돌려주는 것

  • 서명된 update metadata
  • 서명 완료 또는 platform 서명된 산출물
  • 필요하다면 blocklist / minimum allowed version 정보

5.3 전형 플로

metadata를 취한다
  ↓
서명·expiry·version·channel을 검증
  ↓
산출물을 staging에 떨어뜨린다
  ↓
size / hash / package signature를 검증
  ↓
구판을 남긴 채 activation
  ↓
초회 기동에 실패하면 rollback

여기서 중요한 것은 갱신 서버의 응답만으로는 아무것도 성립하지 않는 것입니다. 성립시키는 것은 클라이언트가 가지는 trust anchor와 검증 로직입니다.

6. Windows 안건에서는 어떻게 생각할까

Windows 앱에서는 우선 배포 방식부터 역산하는 편이 정리하기 쉽습니다.

  • 요건이 맞는다면 MSIX App Installer
  • .NET의 사내 앱에서 per-user가 맞는다면 ClickOnce
  • 서비스, driver, shell extension, 독자 채널 제어까지 필요하다면 MSI + 독자 updater도 비교 대상

다만 독자 updater를 고르더라도 해야 할 일은 줄어들지 않습니다. 오히려 늘어납니다.

  • Authenticode / 패키지 서명의 검증
  • signed manifest
  • rollback 대책
  • 갱신 helper의 권한 분리
  • updater 자신의 갱신 전략

Windows에서 흔한 위험한 형태는 DownloadFile -> unzip -> kill process -> overwrite -> restart의 일직선입니다. 이것은 동작하는 경우가 있지만 보안과 복구성의 양쪽이 약합니다.

SmartScreen이나 UAC의 경고를 「자세한 정보 → 실행」으로 넘기게 하는 운용은 갱신 설계가 아니라 경고 길들이기입니다. 올바른 갱신 경로를 만든다면 경고를 익숙하게 하는 것이 아니라 경고가 나오기 어려운 배포와 검증의 구성으로 치우쳐야 합니다.

배포 방식의 비교 그 자체는 다음 기사에서도 정리하고 있습니다.
Windows 앱의 배포 방식을 어떻게 고를까 - MSI / MSIX / ClickOnce / xcopy / 자체 updater의 판단표

7. 최소한의 체크리스트

자체 updater를 내기 전에 최소한 다음은 확인하고 싶습니다.

  • 갱신 metadata는 서명되어 있다
  • metadata에는 version / hash / size / channel / expiry가 들어 있다
  • 클라이언트 측에서 서명과 version을 검증하고 있다
  • 산출물의 hash와 platform 서명을 검증하고 있다
  • 운영 서명 키는 개발 환경에서 분리되어 있다
  • 키의 이용 로그와 승인 기록이 남는다
  • timestamp 붙은 서명을 쓰고 있다
  • staging 갱신으로 구판을 남긴 채 전환한다
  • rollback의 조건과 절차가 있다
  • 검증 실패 시는 fail-closed로 멈춘다
  • updater 자체의 갱신 방침이 있다
  • blocklist / minimum allowed version을 배포할 수 있다
  • 단계 배포를 멈추는 kill switch가 있다
  • 실패율, rollback 율, 서명 검증 실패를 관측할 수 있다

이 체크리스트에 빈 곳이 많다면 먼저 updater의 UI를 만들기보다 배포 신뢰 모델을 파고드는 편이 효과가 있습니다.

8. 정리

자동 업데이트 기능의 보안은 다음 한 문장에 꽤 집약됩니다.

갱신의 편리함이 아니라,
누구를 신뢰하고, 그 신뢰를 클라이언트가 어떻게 검증할지를 설계한다.

그 위에서 실무용의 판단을 거칠게 말하면 이렇습니다.

  • 기존 기반으로 충분하다면 우선 그것에 탄다
  • 자체 updater를 만든다면 HTTPS보다 먼저 서명 붙은 metadata와 키 관리를 넣는다
  • 실패 시의 복구와 rollback을 설계하지 않는 updater는 운영에서 괴롭다
  • updater는 배포 기능이 아니라 제품의 보안 경계 그 자체

만약 지금의 구성이 latest.json + zip 교체에 가깝다면 먼저 고쳐야 할 것은 다운로드 처리보다 신뢰의 두는 방식입니다. 여기를 고치는 것만으로도 위험도는 꽤 바뀝니다.

9. 참고 자료

관련 토픽

이 주제와 가까운 토픽 페이지입니다. 기사를 기점으로 관련 서비스나 다른 기사로 진행할 수 있습니다.

Windows 기술 토픽

Windows 개발, 불량 조사, 기존 자산 활용의 기술 토픽을 모은 입구입니다.

이 주제가 이어지는 서비스

Windows 앱 개발

자동 갱신은 UI만의 이야기가 아니라 배포 방식, 권한, 복구, 운영까지 포함하는 설계입니다. Windows 앱의 신규 개발이나 기존 소프트의 재검토에서 갱신 방식의 정리부터 대응할 수 있습니다.

기술 상담·설계 리뷰

「독자 updater가 필요한가」, 「MSIX / ClickOnce로 충분한가」, 「지금의 갱신 설계의 어디가 위험한가」 같은 정리 단계부터 상담할 수 있습니다.

저자 프로필

고무라 고(小村 豪)

합동회사 고무라소프트 대표

Windows 소프트 개발, 기술 상담, 불량 조사를 중심으로, 기존 자산이 남은 안건이나 원인이 보이기 어려운 장애 조사에 강점이 있습니다.

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기