
foxcage — Updated!
루트리스 Podman 컨테이너에서 capabilities를 제거하고 네트워킹을 격리하며 임시 스토리지를 사용하여 Firefox를 실행함으로써 샌드박스 탈출을 격리하고 호스트 손상을 방지합니다.
foxcage
루트리스 Podman 컨테이너에서 Firefox를 실행하여 보안 격리를 제공합니다. 브라우저는 거의 모든 Linux capability 없이, 자체 사용자 및 네트워크 네임스페이스에서 호스트와 격리된 상태로 실행되면서도 완전한 GPU 가속, 오디오, DRM 지원을 유지합니다.
왜 foxcage인가?
Firefox는 이미 Linux 네임스페이스와 seccomp-bpf를 사용하여 웹 콘텐츠 렌더러를 격리하는 다중 프로세스 샌드박스를 갖추고 있습니다. 대부분의 위협에 대해 이는 효과적입니다. foxcage는 두 번째 방어벽을 추가합니다: 공격자가 Firefox의 샌드박스를 탈출하는 취약점을 악용하면(실제로 발생하며, 이에 대한 CVE가 존재합니다), 전체 사용자 세션 대신 잠긴 컨테이너 안에 갇히게 됩니다.
foxcage가 보호하는 것
- 익스플로잇 후 파일 접근. 일반 Firefox에서 샌드박스 탈출은 사용자가 읽을 수 있는 모든 것에 접근할 수 있게 합니다:
~/.ssh,~/.gnupg, 다른 브라우저의 프로필, 비밀번호 관리자 데이터베이스, 문서, 소스 코드. foxcage에서는 공격자가 명시적으로 마운트한 것만 볼 수 있습니다. - 디스크 추적 잔여물.
@tmp임시 케이지는 창을 닫은 후 디스크에 흔적을 남기지 않습니다 — Firefox의 개인 브라우징이 여전히 유지하는 확장 프로그램, HSTS 상태, TLS 세션 캐시, DNS 캐시를 포함합니다. 여러@tmp케이지는 서로 간섭 없이 동시에 실행됩니다. - 지속성. 일반 Firefox에서는 악성 코드가
~/.config/autostart,~/.bashrc, cron 또는 다른 곳에 작성하여 재부팅 후에도 생존할 수 있습니다. foxcage의 임시 컨테이너(--rm)는 바인드 마운트하지 않는 한 아무것도 유지되지 않음을 의미합니다. - 네트워크 측면 이동. 기본적으로 컨테이너는
localhost의 서비스를 탐색할 수 없습니다. 일반 Firefox에서는 샌드박스 탈출이 전체 네트워크 접근 권한을 갖습니다. (케이지가 localhost 접근이 필요한 경우, 예: 로컬 개발,[network] mode = "host"를 사용하세요 — 단, "네트워킹" 아래의 주의 사항을 참조하세요: 호스트 모드는 호스트의 추상 Unix 소켓도 노출합니다.) - 권한 상승. 컨테이너는
CAP_SYS_CHROOT을 제외한 모든 Linux capability를 제거하고 새로운 권한 획득을 차단합니다. Setuid 바이너리, 난해한 syscall을 통한 커널 익스플로잇 및 유사한 상승 경로가 차단됩니다.
foxcage가 보호하지 않는 것
- 브라우저 수준 공격. 피싱, 악성 확장 프로그램 및 Firefox의 정상 기능 내에서 작동하는 모든 것은 영향을 받지 않습니다 — foxcage는 컨테이너를 호스트로부터 격리할 뿐, 사용자를 브라우저로부터 격리하지 않습니다.
- 바인드 마운트된 디렉터리. 마운트한 모든 것(
profile,downloads_dir, 추가 바인드 마운트)은 손상된 브라우저가 완전히 접근할 수 있습니다. 호스트 프로필 디렉터리를 마운트하면 공격자가 일반 Firefox에서와 마찬가지로 이를 변조할 수 있습니다. - PulseAudio를 통한 오디오 캡처. PulseAudio 소켓이 컨테이너에 바인드 마운트됩니다. 파일시스템 수준에서 읽기 전용으로 마운트되지만, Unix 도메인 소켓은 양방향입니다 — 손상된 프로세스가 여전히 소켓을 통해 녹음 요청을 보낼 수 있습니다. 브라우저 샌드박스 탈출은 호스트 마이크에서 오디오를 녹음할 수 있습니다.
- Wayland 컴포지터 익스플로잇. Wayland 소켓이 전달됩니다. Wayland 컴포지터는 설계상 클라이언트를 서로 격리하지만, 컴포지터 자체의 취약점은 도달 가능합니다.
보안 구성
컨테이너는 다음으로 실행됩니다:
- 모든 Linux capability 제거(
CAP_SYS_CHROOT만 Firefox 콘텐츠 샌드박스를 위해 다시 추가;init.root가 구성된 경우CAP_SETUID/CAP_SETGID가 일시적으로 추가) - 권한 상승을 방지하는
no-new-privileges - 루트리스 사용자 네임스페이스(
--userns keep-id) - 호스트와 공유되지 않는 전용
/dev/shm—shm_size로 크기 구성 가능 - 호스트 루프백이 기본적으로 차단된 pasta를 통한 격리된 네트워킹
- DNS는 기본적으로 호스트 DNS 사용(
network.dns로 구성 가능) XDG_RUNTIME_DIR의 특정 소켓만 바인드 마운트됨(Wayland, PulseAudio, PipeWire 및 필터링된 D-Bus 프록시) — 전체 호스트 런타임 디렉터리는 절대 노출되지 않음- 호스트의 D-Bus 세션 버스 접근은 항상 호스트에서 실행되는 필터링된
xdg-dbus-proxy를 통해 중재됩니다.org.freedesktop.Notifications,org.freedesktop.portal.Desktop,org.mozilla.*및 (포크의 경우) 포크 자체 네임스페이스(예:org.librewolf.*)만 도달 가능합니다 — 키링 및 SSH/GPG 에이전트와 같은 세션 서비스는 차단됩니다 - 포털 접근은 광범위합니다.
org.freedesktop.portal.Desktop전체가 허용됩니다. 파일 선택기, "다른 앱에서 링크 열기", 화면 공유가 이 방식으로 작동하기 때문입니다. 또한RemoteDesktop(전체 세션에 대한 가상 키보드/마우스),Camera및Location도 노출합니다. 이들은 foxcage가 아닌 데스크톱 자체의 승인 대화 상자에 의해 제어됩니다 — 그리고RemoteDesktop프롬프트는 화면 공유 프롬프트와 유사하므로 승인 대화 상자를 수락하기 전에 읽어보세요.xdg-dbus-proxy에는 "인터페이스 하나 거부" 규칙이 없으므로, 이를 좁히려면 Firefox가 필요로 하는 모든 인터페이스를 열거해야 합니다. 기본적으로 이 작업을 수행하지 않는 이유는docs/DESIGN.md를 참조하세요 - 모든 바인드 마운트(
profile,downloads_dir, 추가[mounts] bind)는nosuid,noexec사용 - 브라우저 다운로드는 GPG 서명으로 검증: Firefox는 Mozilla의 서명된 SHA-512 체크섬, LibreWolf는 LibreWolf 유지관리자의 분리 서명과 함께 SHA-256을 검증합니다. 검증은
gpg --verify보다 엄격합니다.gpg --verify는 해지된 키로 만든 서명과 키링의 모든 키에 대해 종료 코드 0을 반환하기 때문입니다. foxcage는 추가로 서명이 고정된 기본 키로 체이닝되도록 요구하며, 소유자가 손상된 것으로 해지한 하위 키로 서명된 릴리스를 거부합니다 — 해지된 서명 키 참조 - 임시 컨테이너(
--rm) — 파일시스템 쓰기는 종료 시 손실됨 - 명시적으로 활성화하지 않는 한 호스트 장치(웹캠, 보안 키, 프린터) 전달 없음
활성화하는 각 [network] 및 [mounts] 옵션은 편의성을 위해 일부 격리를 희생합니다. 기본값은 여전히 사용 가능한 브라우저를 제공하는 가장 제한적인 구성입니다.
요구 사항
- Python 3.11+
- Podman(루트리스)
- Wayland 컴포지터(X11은 지원되지 않음)
- pasta(
sudo apt install passt) —network.mode = "host"가 아닌 경우 - xdg-dbus-proxy(
sudo apt install xdg-dbus-proxy) - PulseAudio 또는 PulseAudio 호환성을 갖춘 PipeWire(오디오용)
- DRI 지원 GPU — 선택 사항;
/dev/dri가 없으면 foxcage가 경고하고 Firefox가 소프트웨어로 렌더링합니다. Intel, AMD 및 nouveau용 VA-API 드라이버가 이미지에 설치되어 있어 호스트 드라이버 패키지 없이 하드웨어 비디오 디코딩이 작동합니다 — 하드웨어 비디오 디코딩 참조
foxcage를 루트나 sudo가 아닌 일반 데스크톱 사용자로 실행하세요 — 샌드박스가 사용자를 컨테이너에 매핑하며, 루트로 실행하면 foxcage가 존재하는 이유인 격리가 제거됩니다. 루트로 시작하는 것을 거부합니다.
테스트 환경: GNOME 3이 포함된 Debian 13(Trixie). 다른 Linux 배포판과 Wayland 컴포지터는 작동할 수 있지만 테스트되지 않았습니다.
설치
foxcage는 Python 표준 라이브러리 외부에 의존성이 없는 단일 Python 스크립트입니다. PATH의 디렉터리에 복사하세요:```sh
sudo cp foxcage /usr/local/bin/foxcage
사용자 로컬 설치의 경우:```sh
cp foxcage ~/.local/bin/foxcage
스크립트를 실행 가능하게 만드세요(chmod +x foxcage).
foxcage --version으로 어떤 리비전을 사용 중인지 확인하세요 — foxcage는 단일 파일을 복사하여 설치되므로 문제를 보고할 때 유용합니다.
사용법```sh
./foxcage
첫 실행 시 스크립트가 컨테이너 이미지를 빌드하고(Mozilla에서 Firefox를 다운로드하고, 최소한의 Debian 의존성을 설치) 그다음 Firefox를 시작합니다. 이후 실행에서는 foxcage가 Firefox 업데이트를 확인하고, 새 버전이 사용 가능하면 이미지를 자동으로 다시 빌드합니다. 이미지는 시스템 패키지 업데이트를 반영하기 위해 주기적으로(기본적으로 7일마다) 다시 빌드됩니다. 업데이트 확인이 실패하면(네트워크 오류, 시간 초과) 경고가 기록되고 기존 이미지가 사용됩니다 — 시작이 차단되는 일은 없습니다.
Firefox에 인수를 전달합니다:```sh
./foxcage https://example.com
명명된 케이지를 Firefox 플래그와 결합:```sh ./foxcage @work --kiosk https://example.com
케이지가 이미 실행 중이면, 두 번째 컨테이너를 시작하는 대신 URL이 기존 브라우저의 새 탭에서 열립니다. 실행 중인 케이지에 대해 URL 없이 `foxcage`(또는 `foxcage @cage`)를 실행하면 "cage is already running" 메시지와 함께 정상적으로 종료됩니다. foxcage는 컨테이너 외부에서 기존 Wayland 창을 띄울 수 없으므로 시도하지 않습니다.
케이지가 이미 실행 중일 때는 실행 시 플래그가 적용되지 **않습니다**. `--dns`, `--ipv4-only`, `--lifetime`, `--color`, `--fork`는 컨테이너가 시작될 때 사용되며, 실행 중인 컨테이너의 설정은 외부에서 변경할 수 없으므로 경고와 함께 무시됩니다. 적용하려면 케이지를 닫고 다시 실행하세요.
> 비공개 모드 세션에는 `private_browsing` 구성 키를 사용하세요. Firefox의 원시 `--private-window` CLI 플래그가 **아닙니다**. 구성 키는 세션 전체에 비공개 모드를 설정하므로(`browser.privatebrowsing.autostart`), 이후 `foxcage @cage URL` 호출로 탭에서 다시 열 수 있습니다. Firefox 패스스루로서의 `--private-window`는 첫 번째 창만 비공개로 만들고 위의 탭 재열기 동작을 깨뜨립니다.
>
> **참고:** `private_browsing = true`로 활성화된 세션은 Firefox의 일반적인 비공개 창 UI 표시(보라색 강조 막대, 마스크 아이콘, 제목의 "(Private Browsing)")를 보여주지 않습니다. 세션의 모든 창이 비공개이므로 Firefox가 시각적으로 대비할 비공개가 아닌 창이 없어 표시기를 숨기기 때문입니다. 세션은 실제로 진정한 비공개입니다. 확인하려면 케이지에서 `about:privatebrowsing`(표준 비공개 브라우징 정보 페이지 표시) 또는 `about:config`에서 `browser.privatebrowsing.autostart = true`를 확인하세요.
### `@tmp`로 임시 브라우징
흔적을 남기지 않아야 하는 일회성 링크에는 예약된 `tmp` 케이지를 사용하세요.```sh
./foxcage @tmp https://somewhere-suspicious.example
Every @tmp 실행은 프로필이 유지되지 않는 일회용 Firefox입니다. 창이 닫히면 모든 것이 사라집니다 — 쿠키, 캐시, 기록, 확장 프로그램, HSTS 상태, TLS 세션 캐시, DNS 캐시, 저장된 탭 상태. 이는 확장 프로그램과 상당량의 디스크 상태를 여전히 유지하는 Firefox 비공개 브라우징보다 더 철저합니다.
여러 @tmp 격리 환경이 동시에 실행되며, 각각 서로 격리됩니다. 메뉴 표시줄에 FoxCage - tmp (<짧은 id>)가 표시되어 동시에 실행되는 임시 창을 구분할 수 있습니다.
임시 격리 환경은 시작 시 빈 페이지와 빈 새 탭을 엽니다 — 기본 Firefox 홈 페이지와 새 탭 콘텐츠(인기 사이트, Pocket 추천, 활동 스트림)는 곧 폐기될 새 프로필에서 순수한 노이즈일 뿐이므로 억제됩니다. 영구 격리 환경은 Firefox 기본값을 유지합니다.
이름이 지정된 임시 격리 환경
일회용 세션에 의미 있는 이름을 붙이고 싶다면(예: 새 탭에서 다시 열고 싶은 연구 토끼굴), @tmp-<이름>을 사용하세요:```sh
./foxcage @tmp-research https://example.com # first call → new window
./foxcage @tmp-research https://another.example # second call → new tab in the existing window
`@tmp-<name>`은 여전히 임시적입니다. 창을 닫으면 모든 것이 사라집니다. 일반 `@tmp`와 다른 점은, 같은 이름으로 두 번째 실행하면 **기존 창을 재사용**한다는 것입니다(영구 케이지와 동일). 따라서 나중에 병렬 복사본을 시작하지 않고도 탭을 더 추가할 수 있습니다. 일반 `@tmp`는 "실행할 때마다 새로 생성되는 일회용" 동작을 유지합니다.
메뉴 바 레이블에는 선택한 이름(`FoxCage - tmp-research`)이 표시되므로 창이 의미 있게 라벨링됩니다.
#### 임시 기본값 사용자 지정
`~/.config/foxcage/tmp.toml`을 생성하여 모든 임시 케이지(일반 `@tmp` 및 모든 `@tmp-<name>`)의 기본값을 설정할 수 있습니다. 예를 들어:```toml
private_browsing = true
lifetime = "30m"
extensions = ["ublock-origin"]