소프트웨어 재사용을 줄여라

16 hours ago 3
  • 공급망 공격은 소프트웨어 배포 비용이 매우 낮아지고 빌드·배포 자동화가 널리 쓰이면서 더 큰 문제로 커졌음
  • 1970년대에는 재사용 가능한 소프트웨어를 만들기 어려운 소프트웨어 위기가 있었지만, 지금은 패키지 저장소와 패키지 관리자가 이름과 버전만으로 코드를 가져오고 빌드함
  • 자동 의존성 갱신은 CI를 통해 악성 변경이 빠르게 퍼지게 만들며, 좋은 공급망 공격은 CI 러너가 실행되는 속도로 확산됨
  • 모든 의존성을 프로젝트 저장소에 함께 넣는 벤더링은 저장소를 키우지만, 자동 변경을 막고 의존성의 규모와 비용을 더 잘 보이게 함
  • 모든 소프트웨어에 맞는 해법은 아니지만, 많은 작은 소프트웨어는 외부에서 갑자기 바뀔 수 있는 의존성을 2~3개 수준으로 줄이는 이점을 얻을 수 있음

문제

  • 공급망 공격은 소프트웨어나 유지보수의 본질만 바뀌어서가 아니라, 소프트웨어 공유와 배포의 비용 모델이 매우 낮아지면서 점점 더 큰 문제가 됨
  • 배포 비용이 너무 낮아져 낭비가 있더라도 자동화를 많이 쓰게 되었고, 자동화 자체는 유용함
  • 몇 달마다 새로운 공급망 공격이 발생해 세계 코드의 큰 부분을 망가뜨리는 일이 생김

어떻게 여기까지 왔나

  • 1960년대 후반과 1970년대 초반에는 사람들이 재사용 가능한 소프트웨어를 만드는 방법을 잘 몰랐고, 이를 소프트웨어 위기라고 불렀음
  • 소프트웨어 수요는 지수적으로 증가했지만, 요구되는 복잡도에 맞는 새 소프트웨어를 만드는 능력은 그보다 느리게 증가했음
  • 이 시기는 모듈성, 구조적 프로그래밍 같은 연구로 이어졌고, 1990년 이후 만들어진 거의 모든 프로그래밍 언어 모듈 시스템은 Modula-2까지 계보를 거슬러 올라갈 수 있음
  • 1990년대와 2000년대에는 인터넷이 더 강력한 해법을 만들었고, 소프트웨어 빌드와 배포가 저렴해졌으며 실제로 쓰고 싶은 소프트웨어 상당수는 오픈소스였음
  • CPAN, CTAN, Linux 배포판을 바탕으로 많은 패키지 저장소패키지 관리자가 생겼고, 이 도구들은 매니페스트 파일, 이름, 대개 임의적인 버전 번호만으로 소프트웨어를 찾고 가져오고 빌드함
  • 수작업 통합에서 자동 의존성으로

    • 과거 복잡한 소프트웨어 시스템을 만드는 좋은 방법은 작동하는 조각들을 손으로 신중하게 조립하는 것이었고, Linux 배포판이 기본적으로 이런 일을 함
    • 2003년에 SDL을 모든 기능과 함께 빌드하려면 며칠이 걸릴 만큼 고통스러웠고, 그런 시절을 그리워할 필요는 없음
    • Linux 배포판이 알려진 기본 환경으로 있으면 많은 맞춤형 소프트웨어는 자체 세계 안에서 동작하고 시스템의 다른 부분을 크게 신경 쓰지 않아도 됨
    • 다른 소프트웨어와 통신할 때는 잘 알려진 프로토콜을 쓰는 파일이나 네트워크 소켓을 통하는 경우가 많음
    • Rust나 Go로 처음부터 빌드되거나 Docker 컨테이너로 배포되는 좋은 소프트웨어가 많아졌고, 이런 소프트웨어는 시스템 라이브러리와 거의 상호작용하지 않음
    • OS 배포판이 제공하는 소프트웨어 집합에 맞추기보다, 필요한 라이브러리를 빌드 시스템이 직접 가져오는 방식이 널리 쓰임
  • 반대 방향의 위기

    • 현재는 1970년대와 반대로 사람들이 소프트웨어를 너무 많이 재사용해 프로그램이 더 나빠지는 위기가 생김
    • 소프트웨어 배포는 여전히 매우 저렴하지만, 소프트웨어를 사용하는 데에는 여전히 비용이 있음
    • 오랫동안 가장 큰 비용은 소프트웨어를 빌드하고 컴퓨터에서 실행되게 하는 복잡성이었지만, 그 문제는 상당 부분 자동화로 사라졌음
    • 이제 훨씬 더 많은 소프트웨어를 빌드·배포·사용하게 되었고, 그 비용은 의존성 지옥, 비대화, 긴 빌드 시간, 패키지나 패키지 관리자의 실종 같은 형태로 나타남
    • 가장 큰 문제는 공급망 공격
  • 공급망 공격의 확산 구조

    • 공급망 공격은 오픈소스 소프트웨어만큼 오래된 문제임
    • 과거 Linux 커널에 uid == 0 대신 uid = 0을 넣으려던 악성 패치 시도는 야생에서 목격된 첫 악성 커널 패치였고, 공급망 공격 시도에 해당함
    • 최근 10년 동안 공급망 공격이 더 크고 문제가 된 이유는 빌드 시스템이 소스 코드를 가져오고 배포하도록 자동화되었기 때문임
    • CI 시스템은 보통 모든 코드 변경이나 큰 변경에서 실행되고, 이런 변경은 해당 코드에 의존하는 모든 사람에게 자동으로 사용 가능해짐
    • 의존하는 쪽의 CI 시스템도 변경을 가져와 새로 들어간 악성 코드를 포함하게 되고, 좋은 공급망 공격은 CI 러너가 실행되는 속도로 산불처럼 퍼짐
    • 의존성 쿨다운처럼 공급망 공격을 늦추는 방법도 있지만, 정책과 책임 소재를 둘러싼 논쟁이 생김

해법

  • npm, cargo 같은 빌드 시스템이 매번 네트워크 위치에서 의존성을 자동으로 가져오게 하지 말고, 모든 의존성을 소프트웨어와 함께 넣는 방식이 핵심임
  • 프로젝트에 모든 의존성을 벤더링하고, 업스트림 소스 제어 내용을 git 저장소에 복사해 커밋함
  • 업스트림 업데이트가 있으면 내려받아 다시 복사하고, 수작업이 지겨워지면 빌드 도구가 이를 자동화하게 하면 됨
  • 이미 lockfile이 있다면, 소스 제어 안의 전체 소스 트리와 연결되게 만들면 됨
  • 모든 소스 코드 줄을 강하게 통제하는 방식으로 소유함
  • 비용과 트레이드오프

    • 저장소는 커지지만 디스크 공간은 저렴함
    • 전송 비용은 디스크보다 덜 저렴하지만, 이 논의에서는 감수해야 할 요소로 남음
    • 빌드 시간은 커질 것처럼 보이지만, 어차피 그 의존성들을 다시 빌드하고 있었기 때문에 반드시 늘어나지는 않음
    • 코드 재사용은 더 어려워질 수 있으며, 공유 프로토콜 라이브러리를 쓰는 클라이언트와 서버 같은 프로그램에서는 실제 문제가 될 수 있음
    • 그런 프로그램은 이미 버전 불일치 문제를 갖고 있고 이를 처리해야 하므로, 실제로 주의를 기울이게 만드는 일이 장기적으로 더 나쁘지는 않음
  • 공급망 공격의 방화선

    • 의존성을 자동으로 갱신하지 않으면 생태계의 모든 패키지가 공급망 공격의 방화선이 됨
    • 같은 방식은 버그 수정과 패치 전파도 막지만, 중요한 수정이라면 어차피 사람이 수동으로 찾아보게 됨
    • 사람이 찾아보지 않는 수정은 대개 중요하지 않은 경우가 많음
    • semver나 “서로 다른 두 코드가 같은 방식으로 동작해야 한다”는 개념을 빌드 시스템에서 버리고, 모든 버전 번호를 서로 무관한 고유한 것으로 다뤄도 비슷한 효과를 낼 수 있음
    • semver의 문제는 실제 현실이 아니라 사람의 의도를 표현하며, 그마저도 어느 정도 올바르게 쓰일 때만 작동한다는 점임
    • 버전 번호를 고유하게 다루는 방식은 의존성이 사라지거나, 변조되거나, 패키지 내용이 다른 방식으로 훼손되는 문제를 해결하지 못함
  • 의존성 가시성

    • 모든 의존성을 벤더링하면 자동 변경을 늦추는 것 외에도 의존성 사용 비용이 조금 올라감
    • 비용 증가는 회복 불가능한 수준이 아니며, 업스트림 코드를 사용할 때 조금 더 생각하게 만드는 정도임
    • 새 의존성을 추가할 때 “정말 필요한가”를 다시 묻게 만드는 부드러운 장치가 됨
    • 의존성의 가시성이 높아지고, 의존성 뒤에 숨은 비대함이 덜 숨겨짐
    • 200줄 정도일 것 같은 단순 라이브러리를 추가했는데 50,000줄이었다면, 멈춰서 이유를 물어야 한다는 점이 더 명확해짐
    • 의존성의 마법 같은 성격이 줄어들고, 코드베이스 안의 버그가 다른 사람의 코드로 이어지는 경로를 더 쉽게 추적할 수 있음
  • 의존성 트리와 공유 문제

    • 모든 것을 기본으로 벤더링하면 더 평평하고 넓은 의존성 트리를 유도할 수 있음
    • C++의 Boost나 Qt 같은 거대 라이브러리 수준까지 가는 것은 바람직하지 않음
    • 그런 거대 라이브러리는 작은 C/C++ 라이브러리를 만들고 쓰는 일이 너무 어렵기 때문에 존재함
    • Boost나 Qt 같은 것을 직접 빌드 방법까지 파악하기보다, Linux 배포판 같은 시스템 통합자가 한 번만 해주는 편이 낫다는 전제가 있음
    • 실제 단점은 전이 의존성이 공유되지 않는다는 점임
    • lib A와 lib B가 모두 Z에 의존할 때 중복 제거는 불가능하지 않지만 더 어려워지고, 사람이 직접 하거나 더 정교한 도구가 필요함
    • 전이 의존성이 공유될 때도 문제가 생기며, 전이 의존성을 갖는 것 자체가 문제의 일부임
    • 라이브러리가 전이 의존성을 지정하도록 허용하는 일은 프로그램에 대한 통제를 다른 사람에게 넘기는 행위가 됨

분석

  • 모든 소프트웨어가 이 방식을 쓸 수 있는 것은 아님
  • 웹앱 백엔드 배포의 일부로 Redis 전체를 벤더링하고 빌드하는 방식은 특별히 합리적이지 않음
  • 다만 배포가 Ansible이나 Docker 이미지 등으로 자동화되어 있다면, 이미 사실상 비슷한 일을 하고 있을 가능성이 있음
  • 이 방식이 견딜 수 있는 복잡도에는 상한이 있지만, Google과 Facebook 같은 거대 모노레포 기업은 그 상한이 생각보다 높을 수 있음을 보여줌
  • 어느 시점에서는 의존성이 운영체제와 만나며, 운영체제는 자체 문제가 많은 큰 의존성임
  • 웹 백엔드용 unikernel 아이디어는 매력적이지만, 실제 도구 문제가 있고 아직 그 단계에 도달하지 못함
  • Linux 배포판과 빌드 환경

    • 이 방식은 Linux 배포판이나 BSD 같은 완전한 상호작용 시스템을 만드는 방법이 아님
    • 그런 시스템은 함께 동작해야 하는 많은 프로그램과 라이브러리가 있으므로 다른 문제에 해당함
    • 이 원칙을 끝까지 밀어붙이면 Nix나 Guix 같은 방식에 가까워짐
    • “빌드 환경”을 올바르게 조립해야 한다는 개념은 “소프트웨어를 어떻게 빌드할 것인가”라는 문제를 게으르고 불충분하게 푼 방식에 가까움
    • 이 개념은 어떤 미니컴퓨터에서 소프트웨어를 한 번 빌드한 뒤 바이너리로 널리 공유하던 시절의 잔재임
    • 오늘날에는 1970년대보다 훨씬 더 많은 소프트웨어를 즉석에서 빌드함
  • 적용 가능한 범위

    • 이 방식은 만능 해법이 아니지만, 많은 소프트웨어는 적용할 수 있고 이점을 얻을 수 있음
    • 대부분의 소프트웨어는 작고, 큰 프로젝트는 이미 이런 문제를 많이 해결해야 함
    • 순수 계산만 하거나 파일과 네트워크 소켓 같은 기본적이고 이식 가능한 I/O로만 외부와 접촉하는 라이브러리가 많이 있음
    • 압축 라이브러리, libcurl, TUI 라이브러리, Django 같은 예시는 벤더링 대상으로 다룰 수 있음
    • 벤더링하면 버전 충돌이나 갑작스러운 패치로 들어온 버그 때문에 새 시스템에 배포하거나 빌드할 때 원인을 알 수 없게 깨지는 일을 거의 피할 수 있음
    • 목표는 외부에서 예고 없이 바뀔 수 있는 의존성을 200~300개가 아니라 많아도 2~3개 수준으로 줄이는 것임

결론

  • 의존성 자동 갱신을 줄이고 프로젝트가 직접 의존성 소스까지 보유하면 공급망 공격의 자동 확산을 늦출 수 있음
  • 의존성 사용 비용을 조금 높이고 가시성을 높이면, 불필요한 재사용과 숨은 비대함을 더 쉽게 발견할 수 있음
  • 이 방식은 모든 시스템에 맞지 않지만, 작은 소프트웨어와 많은 라이브러리에는 실용적인 이점이 있음
Read Entire Article