Sandboxing with minimal effort
최소한의 코드로 애플리케이션 샌드박싱하기
Inko에 운영체제별 샌드박싱 기능을 감싼 공통 API가 추가됐습니다. 예제 서버 shost는 필요한 디렉터리 읽기와 TCP 포트 바인딩만 허용하고 나머지 접근을 차단합니다. 다만 FreeBSD의 Capsicum은 프로그램 구조를 바꿔야 해 API가 지원하지 않으며, 댓글에서는 이런 간편한 추상화의 한계와 Capsicum 활용법을 두고 논쟁이 이어집니다.
- 주제
AI 요약
Inko에 애플리케이션이 스스로 권한을 제한하는 샌드박싱 API가 추가됐습니다. 메모리 안전성만으로는 프로그램이 접근할 수 있는 파일이나 네트워크 권한까지 제한하지 못합니다. 컨테이너 설정에 보안 규칙을 넣는 방법도 있지만, 프로그램 자체가 실행 환경과 무관하게 필요한 권한만 요청하면 추가 방어층을 만들 수 있습니다.
운영체제별 샌드박싱 기능을 공통 API로
운영체제마다 애플리케이션을 제한하는 기능이 다릅니다. Linux에는 Landlock, macOS에는 Seatbelt와 sandbox_init, FreeBSD에는 Capsicum, OpenBSD에는 pledge와 unveil이 있습니다. Inko의 API는 이 기능들을 감싸 플랫폼 차이를 가능한 한 처리합니다.
예를 들어 실행 파일을 허용하는 규칙은 macOS에서 간단하지만, Landlock에서는 ELF 인터프리터도 허용해야 합니다. 보통 /lib64/ld-linux-x86-64.so.2에 있는 인터프리터와 표준 위치가 아닌 공유 라이브러리도 읽을 수 있어야 합니다.
글쓴이는 Inko로 만든 정적 파일 서버 shost를 Podman 컨테이너에서 실행합니다. quadlet 설정에서는 모든 capability를 제거하고 CAP_NET_BIND_SERVICE만 허용합니다. 파일은 읽기 전용 볼륨으로 연결하고 컨테이너 메모리도 1GB로 제한합니다. 여기에 애플리케이션 자체의 샌드박스를 더하면 컨테이너 설정과 별도로 프로세스 권한을 제한할 수 있습니다.
shost 코드에서 필요한 설정은 간단합니다. TLS를 쓸 때는 인증서 디렉터리를 읽도록 허용하고, 서비스할 파일이 있는 디렉터리도 읽기 권한을 줍니다. 서버가 사용하는 TCP 포트에는 bind 권한을 허용한 뒤 샌드박스를 활성화합니다. 지정하지 않은 권한은 거부합니다.
플랫폼별 제약과 Capsicum
이 API에는 플랫폼마다 기능 차이가 있습니다. 특히 FreeBSD에서는 샌드박스가 아무 동작도 하지 않습니다. 글쓴이는 Capsicum이 프로그램의 자원 접근 방식을 바꿔야 한다는 점을 이유로 듭니다. cap_enter를 호출하면 일반적인 open 호출로 파일을 열 수 없습니다. 필요한 파일을 미리 열거나 디렉터리 파일 디스크립터를 준비한 뒤 openat으로 상대 경로를 열어야 합니다. 경우에 따라 libcasper도 필요합니다. 간단한 프로그램에는 큰 부담이 아닐 수 있지만, 규모가 큰 프로그램은 구조를 상당히 바꿔야 할 수 있습니다.
글쓴이는 다른 플랫폼에서는 규칙을 선언하는 코드만 추가하면 기존 프로그램 구조를 크게 바꾸지 않고 샌드박스를 적용할 수 있다고 설명합니다. 반면 Capsicum은 프로그램이 권한을 다루는 방식에 더 많은 수정이 필요하다는 입장입니다. 보안 기능의 가치는 할 수 있는 일뿐 아니라 사용하기 쉬운지에 달려 있다고 글을 맺습니다.
Lobsters 반응
- @decentstates — Sandbox 타입은 백엔드가 어떤 기능을 지원하지 않으면, 예를 들어 UDP 접근 제한을 지원하지 않으면 그 접근을 허용합니다. 그러면 최악의 경우 특정 플랫폼에서 샌드박스가 아무것도 하지 않습니다. 저도 샌드박싱을 더 쓰기 쉽고 간단하게 만드는 작업을 하고 있지만, 서로 충돌하는 두 가지 ‘쉬움’이 있다고 봅니다. 프로그램이나 사용자에게 영향을 주지 않고 샌드박스를 추가하기 쉬운 것과, 필요할 때 샌드박스가 실제로 작동하기 쉬운 것입니다. 첫 번째 기준에서는 샌드박스가 아무것도 하지 않고 프로그램이 실행되는 게 최악의 경우입니다. 두 번째 기준에서도 똑같은 상황이 최악입니다. Inko의 API는 훌륭한 시도라고 생각하지만, 간단한 경우를 넘어서 얼마나 유용한지는 의문입니다. 샌드박싱은 세부 사항이 중요합니다. 예를 들어 Landlock ABI 8에서는 프로그램이 명명된 소켓 접근을 허용하지 않아도 그 접근이 차단되지 않습니다. systemd, gpg, tmux, 데이터베이스 소켓이 여기에 해당하며 사용자나 개발자가 놀랄 수 있습니다. 장치, 오디오, GPU, 윈도 매니저 접근까지 필요하면 더 복잡해집니다. 애플리케이션을 신뢰하지 않고 OS나 윈도 매니저, 셸이 샌드박싱해야 한다고 생각합니다. 다만 외부 입력을 처리하거나 믿기 어려운 라이브러리를 쓸 때 애플리케이션 일부를 격리하는 기능은 있으면 좋겠습니다. 하지만 어느 쪽이든 간단한 문제는 아닙니다.
- @yorickpeterse — 결국 선택의 문제입니다. 샌드박스를 사용하지만 플랫폼이 지원하지 않을 때 규칙을 무시할지, 오류를 낼지 결정해야 합니다. 두 번째 방식을 택하면 개발자들이 오류를 무시하고 진행할 테니 결과는 첫 번째 방식과 같아진다고 봅니다. 모든 플랫폼이 샌드박싱을 지원한다면 다르겠지만 현실은 그렇지 않습니다. 샌드박싱 기능마다 제약이 다르므로 피할 방법도 없습니다. 예를 들어 OpenBSD의 pledge에는 바인딩할 포트를 제한하는 기능이 없습니다. 네트워크를 전부 차단하거나 전부 허용해야 합니다. 이 API는 Linux에서 seccomp 같은 대체 기능을 구현하기보다, 웹의 점진적 향상처럼 지원하지 않는 플랫폼에서 투명하게 아무 동작도 하지 않도록 하는 게 목표입니다. 배포를 맡은 패키지 관리자가 애플리케이션을 샌드박싱하도록 맡기는 방식은 시대에 뒤처졌다고 봅니다. 제대로 처리할 자원이 없는 관리자가 많고, 한 플랫폼에서는 안전하게 샌드박싱되지만 다른 플랫폼에서는 전혀 그렇지 않을 수 있습니다. 이상적인 구성은 애플리케이션이 메모리 안전 언어와 OS 기능을 활용해 가능한 한 스스로 권한을 제한하고, Flatpak 같은 배포 수단이 관련 권한을 추가로 제한하며, 필요하다면 OS가 가상화 같은 방식으로 더 제한하는 것입니다.
- @vinipsmaker — Capsicum을 쓰려면 프로그램 구조를 근본적으로 바꿔야 한다는 말은 사실이 아닙니다. 제가 링크한 Capsicumizer는 주변 권한 흐름을 추론할 수 있게 합니다. 개발자가 모든 자원과 권한을 같은 영역에 두는 방식을 고집한다면 보안 구획을 만드는 의미가 없습니다. 그런 방식이라면 글에 나온 Podman quadlet을 쓰면 됩니다. 프로그램 API로 quadlet을 그대로 노출하면 프로그램을 바꾸면서도 같은 동작만 반복하게 됩니다. 대신 접근법을 조합할 수 있습니다. Super Capsicumizer 9000은 주변 권한에 의존하는 기존 코드를 실행하는 방법을 보여줍니다. Inko처럼 런타임을 통제한다면 실행 파일에서 open 함수를 직접 구현해 glibc 대신 쓰도록 하는 등 자동으로 보완할 수도 있습니다. 사용자는 저수준 구현을 신경 쓰지 않아도 됩니다. Emilua에서 이런 방식을 구현했습니다. NodeJS, Deno, Go, Inko처럼 런타임을 직접 관리하는 곳이라면 구현하기 쉽습니다. 사후에 샌드박싱 라이브러리를 덧붙이면 Landlock 같은 기능을 감싼 거친 단위의 라이브러리에 그치기 쉽고, 권한을 여러 묶음으로 나누거나 따로 관리하는 방법을 제공하기 어렵습니다. Capsicum에서도 일반적인 open 호출은 cap_enter 뒤 실패하지만, 누구도 시스템 호출을 직접 쓰지 않습니다. libc 심볼을 재정의할 수 있습니다. Inko처럼 런타임을 통제하면 더 쉽습니다. openat 방식이나 파일을 미리 여는 방식만 가능한 것도 아닙니다. 링크한 예제에는 GTK+ 앱을 여는 방법이 있고, 제 예제에는 라이브러리를 불러오기 전에 샌드박스를 만드는 방법과 Qt 라이브러리로 이미지를 격리된 영역에서 디코딩하는 방법이 있습니다. Capsicum의 저수준 기능을 고수준 추상화로 감쌀 수 있습니다. Inko처럼 자체 런타임을 만들면 더 쉬운데, 왜 사람들이 이걸 이해하기 어려워하는지 모르겠습니다. 제 예제에서는 termbin.com 같은 특정 주소만 이름 해석을 허용하고, 그 다음 해석된 IP로만 연결을 허용하는 동적 규칙도 보여줍니다. Inko도 그런 기능을 제공할 수 있습니다.
- @yorickpeterse — LD_PRELOAD로 애플리케이션을 샌드박싱하는 방법은 알고 있습니다. 하지만 그 방법도 애플리케이션 로직을 바꿔야 한다는 제 말을 뒤집지는 않습니다. 소스 코드를 고치는 대신 실행 중에 심볼이나 시스템 호출을 가로채는 차이가 있을 뿐입니다. 결국 프로그램 동작을 근본적으로 바꿔야 합니다. 런타임을 통제해도 open과 openat을 사용하는 프로그램 구조의 차이는 그대로입니다. 이를 뒤에서 자동으로 처리하는 시스템을 만들 수는 있겠지만, Capsicum에 맞추려고 프로그램 작동 방식을 바꿔야 한다는 점은 같습니다. Capsicum이 열등하거나 덜 강력하다고 주장하는 게 아닙니다. 개발자가 직접 프로그램을 수정하거나 LD_PRELOAD 라이브러리를 써야 하는 등 더 많은 작업이 필요하다는 뜻입니다. 다른 플랫폼에서는 프로그램 구조를 크게 바꾸지 않고 샌드박스를 적용할 수 있습니다.
- @fanf — 옵트인 방식의 샌드박싱 기술은 대부분 애플리케이션이 기대하는 동작을 보안 계층에 알려야 합니다. Unix 커널 대부분은 애플리케이션이 사용자 공간에서 스스로 동작을 제한하는 보안 기능을 제공하지 않아 보안 정책 로직을 커널에 구현합니다. Capsicum은 libc를 확장하는 shim 같은 방식으로 정책 로직 상당 부분을 구현할 수 있다는 점이 다릅니다. 애플리케이션이 cap_enter()를 쓰든 pledge()를 쓰든, 평소 가능했던 일부 작업을 금지하는 다른 보안 모델을 선택합니다. pledge() 뒤에는 커널 로직이 금지된 작업을 막습니다. cap_enter() 뒤에는 금지된 작업을 커널에 요청할 수 없게 되므로 라이브러리가 시스템 호출 전에 오류를 돌려줄 수 있습니다. 적용 위치는 달라도 효과는 같습니다. 커널에서 동작하는 OpenBSD 샌드박싱 로직도 libc API 일부의 예상 동작을 바탕으로 한 임시방편이 많습니다. 반대로 Capsicum이 사용자 공간의 라이브러리 shim으로 libc 동작을 제한하면, capability가 없을 때 실패를 닫힌 상태로 처리합니다. 커널의 강제 접근 제어 로직에서 실수하면 권한 상승 취약점으로 이어질 수 있습니다.
원문: yorickpeterse.com / 번역·요약: Trawling