1P by GN⁺ | ★ favorite | 댓글 1개
  • NixOS의 최소 설치 ISO가 Hydra 배포본과 비트 단위로 동일하게 독립 재구축되며, 배포 바이너리와 소스의 일치 여부를 검증할 수 있음을 보여줌
  • 이번 검증은 ISO에 포함된 패키지뿐 아니라 ISO 생성 과정 자체까지 재현해, 단순 패키지 재현보다 더 넓은 범위를 확인함
  • 재구축은 NixOS 20.03 VirtualBox appliance에서 시작해 nixpkgs 리비전 63678e9f3d3a를 사용하고, --option substitute false바이너리 캐시 의존을 끔
  • 2020년 OVA나 다운로드한 git에 정교한 백도어가 있었다면 여전히 공격 벡터가 될 수 있어, 완전히 부트스트랩된 시스템 기반 검증은 아직 남아 있음
  • 최소 ISO 재구축은 중요한 이정표지만, 임시 우회책 제거와 더 많은 설치 미디어 재현, 정기 독립 재구축 인프라, 빌드 증명 도구가 다음 과제임

최소 ISO에서 검증된 재현성

  • Hydra가 게시한 nixos-minimal ISO 빌드를 독립적으로 다시 빌드해 비트 단위 동일 결과물을 얻음
  • 재현 범위는 두 축으로 나뉨
    • ISO에 들어가는 모든 패키지
    • ISO를 만드는 빌드 과정 자체
  • ISO 빌드에 필요하지만 ISO 안에 포함되지 않는 패키지도 함께 빌드했으며, 캐시된 바이너리에 의존하지 않음
  • 재현 가능한 빌드는 배포 바이너리가 소스에 충실한지, Hydra 같은 빌드 파이프라인에서 변조되지 않았는지 확인할 수 있는 신뢰 경로를 제공함

재구축 절차와 한계

  • 재구축은 새 VirtualBox appliance에서 NixOS 20.03을 시작해 수행함
    • CPU와 메모리를 충분히 할당하고 디스크를 약 65GB로 확장함
    • git을 설치한 뒤 nixpkgs를 클론하고 63678e9f3d3a 리비전을 체크아웃함
    • --option substitute false로 필요한 항목을 바이너리 캐시에서 가져오지 않고 로컬 머신에서 빌드함
  • 절차에는 알려진 이슈를 우회하기 위한 임시 조치가 포함됨
  • 공급망 신뢰 측면의 한계는 남아 있음
    • 2020년 OVA나 다운로드한 git에 정교한 백도어가 있었다면 여전히 공격 벡터가 될 수 있음
    • 완전히 bootstrapped된 시스템에서 재구축하는 편이 더 좋지만 아직 그 단계에는 도달하지 못함
    • 관련 진전은 nixpkgs supply-chain security project 스레드에서 이어지고 있음

2021년 발표와 이번 결과의 차이

  • 2021년에는 최소 ISO가 100% 재현 가능하다는 발표가 있었지만, 당시에는 ISO 빌드에 필요한 패키지를 개별적으로 재현했을 뿐 실제 ISO 재구축에는 차이가 남았음
    • 원인은 Hydra 캐시와 ISO 생성 방식의 남은 문제였음
    • 이후 문제가 수정되는 동안 Python 3.10의 upstream 문제 같은 회귀가 생겼고, 이번 주에야 전체 체인을 검증할 수 있는 상태로 돌아옴
  • 다음 단계는 임시 우회책 제거, 더 많은 패키지 재현, Gnome ISO 같은 다른 설치 미디어 재현으로 이어짐
  • 정기적인 독립 재구축 인프라와 trustix 같은 빌드 증명 공유·소비 도구도 필요함

댓글과 토론

Hacker News 의견들
  • 소스에서 최소 ISO를 다시 빌드한 건, 소스 기반으로 재현 가능하게 빌드되는 시스템으로 가는 여정에서 인상적인 이정표임
    Guix도 최근 같은 여정에서 직교적이지만 똑같이 인상적인 성과를 냈는데, 다른 바이너리 컴파일러 블롭 없이 단 하나의 재현 가능한 357바이트 바이너리에서 전체 컴파일러 도구체인을 부트스트랩함
    언젠가 곧 이 둘이 결합되어 전체 배포판을 소스에서 재현 가능하게 빌드할 수 있을지도 모름
    https://guix.gnu.org/en/blog/2023/the-full-source-bootstrap-...
    • 대단하고, “백도어가 있으면 어차피 모두가 같은 백도어를 받는 것 아닌가?” 같은 반응 속에서도 좋은 싸움을 하는 사람들이 있다는 게 반가움
      많은 사람이 이해하지 못하는 핵심은 결과물이 100% 신뢰 가능하다는 걸 증명하는 게 아니라, 결과물이 소스에 100% 충실함을 증명하는 데 있음
      즉 은밀한 백도어 같은 수상한 짓이 발견되면, 그걸 항상 결정적으로 재현할 수 있게 됨
      나쁜 쪽 입장에서는 도망갈 곳도 숨을 곳도 없어지는 셈임
    • 아직 Guix의 stage0만큼 간 건 아니지만, NixCon에서 TinyCC로부터 Nix를 부트스트랩하는 흥미로운 발표가 있었음: https://media.ccc.de/v/nixcon-2023-34402-bootstrapping-nix-a...
    • 부트스트랩 컴파일러 바이너리가 357바이트라는 건 정말 인상적임
    • 357바이트라면 굳이 재현 가능한 바이너리가 필요한가 싶음
      기계어 357바이트 전체를 손으로 문서화해서 사람이 이해할 수 있게 만들 수도 있을 것 같음
  • 이런 걸 해본 적이 없어서 어리석은 질문일 수 있지만, 왜 재현 가능성이 기본 동작이 아닌지 궁금함
    같은 소스에서 소프트웨어 두 벌을 컴파일했다면 매번 완전히 동일해지는 걸 막는 요소가 무엇인지 잘 모르겠음
    움직이는 부분이 많다는 건 알지만, 차이가 어떻게 생기는지는 여전히 이해가 잘 안 됨
    • 구체적인 원인은 많고, 아마 타임스탬프가 가장 흔한 문제일 것임
      흔한 문제 목록은 여기서 볼 수 있음: https://reproducible-builds.org/docs/
      전반적인 핵심은 개발자들이 재현되는지 테스트하지 않는다는 데 있음
      릴리스 테스트에 포함되면 보통 계속 재현 가능한 상태로 유지됨
    • 원인은 아주 많음
      명시적으로 재현 불가능하게 만드는 대표적인 예로는 타임스탬프와 작성자 정보가 있음
      암묵적으로 기본 재현성을 깨는 곳도 있는데, 예를 들어 많은 런타임은 해시맵 항목 순서를 정의하지 않고, 컴파일러가 그 해시맵을 순회해 바이너리를 만들기도 함
    • 병렬성 때문일 수 있음
      순서와 무관하지 않은 작업이 있을 수 있고, CPU 상태에 따라 조금씩 다른 바이너리가 나오지만 모두 올바른 결과일 수 있음
    • Go 팀이 Go 도구체인을 완전히 재현 가능하게 만들기 위해 무엇을 했는지 최근 글을 올렸음: https://go.dev/blog/rebuild
    • 때로는 무작위 알고리즘, 때로는 성능 문제, 예를 들어 뭔가를 정렬하지 않는 편이 더 빠른 경우 때문임
      때로는 시간이나 환경에 의존하는 메타데이터, 스레드 실행 순서 같은 것도 원인이 됨
  • 잘 몰라서 미안하지만, NixOS가 존재하는 주요 이유 중 하나가 재현 가능성이라고 생각했음
    이런 문제들은 이미 해결된 줄 알았음
    NixOS는 2시간 정도만 써봤고 Hyprland를 써보고 싶었는데, Hyprland는 설정이 좀 필요하니 다른 배포판보다 NixOS에서 남의 설정을 가져다 쓰는 게 쉬울 거라고 생각했음
    그런데 설정 찾기도 어려웠고, 임의의 GitHub gist에서 3개 정도 찾았지만 하나도 동작하지 않아 포기했음
    • NixOS는 모든 것이 자체 샌드박스 안에서, 명시적으로 선언되고 해시된 의존성만 가지고 빌드된다는 장점이 있음
      일반 배포판처럼 전체 시스템 환경에 의존하지 않으므로 많은 경우 이미 매번 같은 바이너리가 나옴
      하지만 빌드 과정 자체가 여러 패키지에서 비결정적일 수 있어서, 이것만으로 곧장 완전한 재현 가능성이 생기지는 않음
    • 재현 가능성에는 두 가지 의미가 있음
      생각하고 있는 의미는 바이너리 패키지를 쉽게 다시 빌드했을 때 같은 의존성 버전과 빌드 옵션 등을 쓰는 것임
      “내 노트북에서는 됐는데” 같은 컴파일 오류가 새로 생길 여지가 없어야 한다는 뜻임
      여기서 말하는 의미는 모든 빌드 산출물이 바이트 단위로 동일한 바이너리가 되는 것임
      머신 이름, 컴파일 시각, 병렬 빌드에서 파일 컴파일이 끝나는 순서 등에 의존하지 않아야 하며, 이쪽이 훨씬 더 어려움
    • Nix는 배우기 어려운 도구임
      익숙하지 않다면 “지금 당장 작동하는 걸 원한다”는 상황에서 고를 도구는 거의 아님
      NixOS에서 “재현 가능”하다는 말은 “같은 Nix 코드로 같은 프로그램 동작을 얻는다”는 뜻에 가까움
      사람들이 Dockerfile에 기대하는 것과 비슷하고, “내 머신에서는 됐다”거나 “지난번에는 됐다”는 문제를 해결하려는 수준임
      반면 “재현 가능한 빌드”는 서로 다른 머신에서 만든 산출물이 비트 단위로 동일한 것을 목표로 함
      이렇게 되면 특정 소스 집합에서 코드가 빌드됐는지 검증할 수 있어 보안 계층이 하나 생김
      설정을 찾을 때 어떤 검색어를 썼는지도 궁금함
      “nixos configuration”으로 검색하면 https://github.com/search?q=nixos%20configuration&type=repos... 같은 결과가 있고, Hyprland만 봐도 https://github.com/search?q=wayland.windowManager.hyprland&t...처럼 꽤 많이 보임
    • https://github.com/donovanglover/nix-config를 보면 좋음
      Flake 기반 설정이고 Hyprland와 여러 괜찮은 것들이 들어 있음
      현재 NixOS는 약한 사람이나 시간이 부족한 사람에게 맞는 도구는 아님
      언젠가는 달라지길 바라지만, 버티고 넘어가면 이점은 얻을 수 있음
    • 설정을 3개 정도밖에 못 찾았다는 건 좀 이상함
      GitHub 코드 검색을 썼는지 궁금함
      관련 Home Manager 옵션은 여기서 찾을 수 있음: https://mipmip.github.io/home-manager-option-search/?query=h...
      그런 다음 GitHub에서 검색하면 됨: https://github.com/search?utf8=%E2%9C%93&q=lang%3Anix+hyprla...
      일부 옵션 검색은 더 캐주얼하거나 더 고급 사용자 쪽을 암시할 수 있음
  • Nix / NixOS / Nixpkgs의 재현 가능성은 소스의 재현 가능성이라는 점을 기억해야 함
    소스가 바뀌면 경고를 받지만, 빌드할 때마다 달라질 수 있는 바이너리의 재현 가능성과는 다름
    Nix / NixOS / Nixpkgs의 바이너리 재현 가능성은 적어도 체계적으로는 잘 테스트되지 않는 편임
    Guix, Arch Linux, Debian은 Nix / NixOS / Nixpkgs보다 바이너리 재현 가능성을 더 잘 다룸
    자료: https://r13y.com/ (Nix*) / https://tests.reproducible-builds.org/debian/reproducible.ht... (Debian) / https://tests.reproducible-builds.org/archlinux/archlinux.ht... (Arch Linux) / https://data.guix.gnu.org/repository/1/branch/master/latest-... (Guix, 로딩이 느릴 수 있으며 캐시 사본은 https://archive.is/lTuPk)
    • 이 맥락에서 “재현 가능성”에는 두 가지 정의가 있음
      입력 재현 가능성은 “입력에 대한 완벽한 캐시 무효화”를 뜻함
      Nix와 Guix는 설계상 이를 완벽하게 수행하고, 가끔 너무 많은 재빌드를 유발하기도 함
      Debian과 Arch Linux는 이걸 주요 관심사로 두지 않고, 특정 소스 파일이 갱신되면 어떤 패키지를 다시 빌드할지라는 문제를 수동 재빌드 트리거 같은 임시 방식으로 처리함
      출력 재현 가능성은 “빌드 과정이 결정적이며 항상 같은 바이너리를 만든다”는 뜻이고, 원 글의 주제임
      Nix는 패키지를 샌드박스에서 빌드하므로 도움이 되지만 만능 해법은 아님
      이 점에서는 Nix도 Debian, Arch Linux와 같은 배에 타고 있음
      실제로 배포판들은 재현성을 높이는 패치를 upstream에 자주 보내고, 다른 배포판들도 그 이익을 얻음
      이 맥락에서 https://reproducible.nixos.org가 제시한 다른 링크들의 대응물이며, Nix 보고서가 덜 상세하다는 데는 동의하지만 그렇다고 Nix의 바이너리 재현성이 더 나쁘다는 뜻은 아님
      “Nix는 입력 재현 가능성만 잘하고 바이너리 재현 가능성은 잘 못 한다”는 식으로 읽히면 틀린 말임
      바로 그 이정표를 여기서 축하하는 중임
    • 글을 읽어보는 게 좋을 것 같음
      이 글은 바이너리뿐 아니라 그것들이 ISO로 패키징되는 방식까지 비트 단위로 재현하는 얘기임
      r13y.com은 오래된 상태이고, 빠진 1% 미만도 기억상 upstream Python 회귀 때문이었음
      바이너리 자체의 재현 가능성은 ISO 패키징을 제외하면 이미 몇 년 전에 달성됐음
      핵심 ISO를 넘어선 패키지들로 가면 비교가 복잡해짐
      패키지를 다루는 방식이 미묘하지만 이 맥락에서는 중요하게 다르고, Arch의 AUR에 있을 법한 많은 패키지가 Nix에서는 일반 패키지로 들어가 있으며 대부분의 -bin upstream 패키지도 Nix에서는 단순히 필요하지 않음
      일반적으로 Nix는 재현 가능한 빌드를 만들기 쉽게 하지만, Nix와 무관하게 항상 가능한 것은 아니고 종종 패치가 필요함
      Nix 기본 패키지 저장소가 8만 개 이상인 반면 Arch는 AUR 제외 1만5천 개 미만이라는 점까지 합치면 백분율 비교는 별로 유용하지 않음
      아주 흔한 오해 중 하나는 Nix store 경로의 해시가 빌드 출력 기반이라고 생각하는 것인데, 실제로는 격리된 환경에서 바이너리를 빌드하는 데 쓰인 모든 소스와 입력, 바이너리 여부와 무관하게 그 전체에 기반함
      그래서 사람들이 기대하는 보안 이점이 그대로 생기지는 않지만, 대신 재현 가능하게 빌드되지 않는 소프트웨어도 기능, 컴파일러 설정, 의존성 버전, 사용자, 구성 등이 동일한 합리적으로 재현 가능한 배포 형태로 사용할 수 있게 해줌
    • 그게 첫 번째 자료와 원 글이 다루는 바로 그 내용 아닌가 싶음
      같은 소스에서 서로 다른 머신으로 빌드했을 때 바이너리가 같은지 확인하는 것임
      요점은 바이너리가 빌드할 때마다 바뀌지 않는다는 데 있음
      테스트 방식도 각 빌드를 서로 다른 시점, 서로 다른 하드웨어, 서로 다른 커널에서 두 번 실행한다고 되어 있음
    • Arch Linux가 재현 가능성을 테스트한다는 건 몰랐음
      보니 85.6% 재현 가능하다고 나옴: https://reproducible.archlinux.org
      공식 저장소에 8만 개 넘는 패키지가 있는 NixOS에서는 얼마나 많은 작업이 필요할지 궁금함
    • nixpkgs의 목표나 현실을 놓고 보면 전혀 사실이 아님
      원 글은 여러 바이너리 패키지가 들어 있는 바이너리 최소 ISO를 재현하는 이야기임
  • OpenBSD 프로젝트가 정반대 방향으로 열심히 가고 있다는 점이 아이러니해서 웃김
    OpenBSD는 모든 설치마다 고유하고 무작위화된 주소 오프셋을 가짐
    재현 가능한 빌드와 고유한 설치라는 두 목표가 서로 직교하고 동시에 달성 가능하다는 건 이해하지만, 이 이중성이 여전히 웃김
    • 주소 오프셋을 제공된 시드로 무작위화할 수 있다면 재현 가능성을 입증하는 건 여전히 가능함
      또는 프로그램 시작 시 오프셋을 무작위화하는 방법도 재현 가능성을 유지하면서 보안을 더 높일 수 있음
      그러면 실행할 때마다 오프셋이 바뀜
    • OpenBSD는 부팅 시점에 무작위화된 링킹을 함
      패키지 자체는 여전히 재현 가능할 수 있음
      모든 무작위화는 패키지를 내려받고 체크섬을 검증한 뒤 로컬에서 수행됨
  • 이제 거의 모든 다른 Linux 배포판이 90년대부터 해온 것처럼, 유지보수자가 패키지에 서명만 해주면 좋겠음
    그래야 모두가 빌드하는 코드가 알려진 개인들이 제출하고 검토한 동일한 코드인지 어느 정도 알 수 있음
    서명이 표준화되기 전에는 가치 있는 무언가를 보호하는 프로덕션 용도에서 Nix를 쓰기는 상상하기 어려움
    • Nix 패키지 유지보수자들은 소프트웨어를 쉽게 조합할 수 있게 해주는 유용한 인터페이스를 제공하는 쪽에 가깝다고 봄
      대부분은 패키지 내용에 대해 어떤 보증도 하지 않음
      그들의 서명에서 의미 있는 보장을 기대하는 건 배송 기사에게 제품 지원을 기대하는 것과 비슷하게 느껴짐
      악의적으로 패키징되지 않았다고 믿을 필요도 없음
      Nix는 재현 가능한 빌드를 하므로, 바이너리 캐시에 의존하고 싶지 않다면 derivation을 보고 직접 빌드하면 됨
      근본 내용물이 악의적인지는 결국 개발자와 사용자 사이의 문제임
      다른 배포판이 그 반대로 믿게 만들었다면, 그건 오도한 것에 가깝다고 생각함
      떠오르는 예외는 Tails 정도인데, Tails는 Nix만큼 폭넓지는 않음
    • 어떤 배포판이 그렇게 하는지 궁금함
      보기로는 Debian은 더 이상 그렇게 하지 않고 빌드 시스템이 빌드에 서명하며, Fedora도 하지 않고, Arch는 확실치 않지만 하지 않는 것 같음
      NixOS 빌드 시스템은 모든 빌드 산출물을 자체 키로 서명하고 다운로드 시 서명을 검증함
      편집증적으로 접근한다면, Nix는 적어도 모든 것을 소스에서 직접 빌드하기 쉽게 만들어줌
  • 매우 인상적인 이정표이고, 가능하게 만든 사람들에게 축하를 보냄
    ISO를 실제로 다시 빌드할 때도 차이가 생겼고, 원인은 Hydra 캐시에 남은 문제와 ISO 생성 방식이었다고 되어 있음
    “ISO가 생성된 방식”을 어떻게 고쳤는지 설명해줄 수 있는 사람이 있는지 궁금함
    예전에 재현 가능한 ISO를 만들어보려 했지만 파일 시스템이 extent를 결정적으로 만들게 할 수 없었음
    • NixOS의 경우 글의 “어떻게 재현했는가” 섹션에 있음
      그 과정의 마지막 단계가 ./result/iso 디렉터리에 ISO를 생성함
      찾는 건 그 빌드가 호출한 명령인 것 같은데, 어떤 단계를 찾는지는 잘 모르겠음
      예를 들어 xorriso 호출은 여기에 있음: https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/make-...
  • 이렇게 하려면 시스템 시간을 속여야 하지 않나?
    시간은 이런저런 방식으로 바이너리 안에 들어가는 경우가 많음
    • 실제로 타임스탬프는 비결정성의 가장 흔한 원인일 가능성이 큼
      너무 흔해서, 많은 컴파일러가 타임스탬프를 속이기 위한 사실상 표준 변수인 SOURCE_DATE_EPOCH를 구현했음: https://reproducible-builds.org/docs/source-date-epoch/
    • 어떤 식으로, 그리고 어떤 이유로 그런 일이 생기는지 예를 들 수 있는지 궁금함
    • 모든 빌드에서 타임스탬프를 아예 포함하지 않거나, 0으로 설정해서 빌드 날짜가 어디서나 1970년이 되게 하면 됨
  • 이게 Ken Thompson이 “Reflections on Trusting Trust”에서 쓴 문제를 해결하는 데 도움이 되지 않나?
    시스템 전체를 소스 코드에서 완전히 부트스트랩할 수 있으면, 백도어가 심어진 컴파일러 같은 걸 넣기가 더 어려워질 것 같음
    • 실제로 도움이 되지만 완전한 “해결책”은 아님
      이론상 ISO가 빌드되는 환경 안에 정교한 백도어가 있을 수도 있음
      그 문제를 정말 해결하고 싶다면 Diverse Double Compiling(https://dwheeler.com/trusting-trust/)이나 전체 환경 부트스트랩(https://bootstrappable.org/)을 볼 수 있음
      글의 “위 접근에는 부트스트랩 문제가 없나?” 섹션도 관련 있음
      그래도 빌드를 재현하는 것만으로도 그런 공격이 점점 가능성이 낮아지게 만드는 데 큰 도움이 됨
  • 최근 일 때문에 Red Hat 생태계 안에서 지냈음
    이건 Fedora Silverblue, Ansible, Fedora Silverblue + Ansible 같은 것과 비교하면 어떤지 궁금함
    • Fedora 생태계에서 NixOS ISO 빌더와 그 재현 가능성에 가장 가까운 건 osbuild / imagebuilder임: https://www.osbuild.org/guides/introduction.html
      Imagebuilder는 재현 가능성을 주장하지만, 내가 알기로는 대부분 rpm 패키지를 소스가 아니라 바이너리로 설치함
      그래서 모든 입력 패키지도 재현 가능하지 않다면 엄밀한 재현 가능성은 아님
      소스에서 패키지를 빌드하고, 배포판 이미지를 만들고, 재현 가능성을 다루는 설명이 잘 와닿지 않았다면 아마 주 대상 독자는 아닐 가능성이 큼
    • Nix는 선언형 OS로, OS가 어떤 모습이어야 하는지를 기술함
      반면 Ansible은 OS가 따라야 할 단계를 지정함
      Silverblue와 Nix는 Linux 배포판이라는 점 말고는 서로 직교적임
      Silverblue는 불변 호스트 위에서 컨테이너만 사용해 소프트웨어 전달 방식을 바꾸려는 시도임
      Jsonnet과 상태 추적을 사용해 Nix를 어느 정도 흉내 내는 Ansible 대안을 찾는다면 Etcha를 볼 만함: https://etcha.dev
    • Ansible은 OS에 작업 단위로 가변 변경을 적용함
      Nix는 불변적임
      새 변경은 완전히 새로 만들어지고, 빌드가 성공한 뒤에야 모든 패키지가 현재 시스템으로 “심볼릭 링크”됨
      Fedora Silverblue는 ostree 기반임 https://github.com/ostreedev/ostree
      루트 트리에 대해 git과 비슷하게 동작하지만, 변경사항을 적용하려면 전체 시스템을 재부팅해야 함
      Nix는 패키지를 심볼릭 링크하는 방식이므로 시스템을 재부팅할 필요가 없음
      더 자세한 설명은 여기 있음: https://dataswamp.org/~solene/2023-07-12-intro-to-immutable-...