Skip to content

Issue BB: [Preview] 프리뷰 컨테이너 사용자 코드 실행 격리 — 워크스페이스 소유자를 node 로 통일 (설계 확정, 미구현) #332

Description

@Danto7632

인계 문서입니다. 설계는 실측으로 확정됐고 구현만 남았습니다. 처음 열었을 때 적었던 방법은 틀린 것으로 판명나 아래에서 대체했습니다(경위는 첫 코멘트).

🔥 Issue 개요

프리뷰 컨테이너는 사용자가 연결한 저장소를 실제로 빌드해 돌립니다. npm install 의 postinstall 스크립트와 빌드 스크립트는 저장소가 정하는 코드이고, 지금 그것이 컨테이너 안에서 root(uid 0)로 실행됩니다.

cap-drop ALL + no-new-privileges 라 컨테이너 탈출로 바로 이어지지는 않습니다. 지금 안전한 진짜 이유는 바인드 마운트가 없기 때문인데, 나중에 누가 마운트를 하나 붙이는 순간 root 라는 사실이 갑자기 중요해집니다. 그때 기억나지 않을 종류의 전제입니다.

급하지 않습니다. 실제로 위험했던 경로(컨테이너 → 호스트의 Qeploy API)는 PR #333 에서 닫았습니다. 이건 심층 방어입니다.

🔒 확정된 제약 — 이것이 설계를 결정합니다

cap-drop ALLDAC_OVERRIDE 를 뗍니다. 그래서 이 컨테이너의 root 는 파일 권한을 우회하지 못합니다.

$ docker exec <preview> grep CapEff /proc/self/status
CapEff:	00000000000000c1        # CHOWN(0) + SETGID(6) + SETUID(7) 뿐. DAC_OVERRIDE(1) 없음

$ docker exec <preview> sh -c 'chown -R node:node /workspace; echo x > /workspace/app/by-root.txt'
sh: can't create /workspace/app/by-root.txt: Permission denied      # exit=1

# 비교 — DAC_OVERRIDE 를 주면 된다
$ docker run --cap-drop ALL --cap-add DAC_OVERRIDE ... 'chown -R 1000:1000 /workspace; echo y > /workspace/app/x'
exit=0

결론: 워크스페이스는 주인이 하나여야 합니다. 중간에 node 로 넘기면 그 뒤의 root 명령(git push·diff·씨딩)이 전부 막힙니다. 절반만 옮기는 설계는 없습니다 — 그렇게 시도했다가 되돌린 것이 첫 코멘트에 있습니다.

✅ 목표 상태는 동작합니다 (실측)

적대적 postinstall 을 심어 확인했습니다.

확인 결과
저장소 코드의 uid 1000 (root 아님)
/etc/shadow 읽기 거부
/etc · /usr/local/bin 쓰기 거부
apk add 거부
워크스페이스 쓰기 허용 (필요)
빌드 산출물 생성 정상

git config --global · credential.helper · git init/commit · 실제 clone · npm install 전부 node 로 됩니다(git 을 root 로 미리 깔아 둔 상태에서).

처음 측정에서 이것들이 실패해 권한 문제로 읽을 뻔했는데, git 이 안 깔려 있어서였습니다. 설치 후 다시 재면 전부 통과합니다.

🎯 설계 — 이 순서로 하는 것이 중요합니다

1단계. 프리뷰 컨테이너와 빌드 컨테이너를 분리한다 (이것부터)

지금 createAndStartContainerexec 를 둘이 함께 씁니다.

preview/PreviewSessionService           프리뷰
preview/ProjectPreviewService           프리뷰
provisioning/NativeBuildService         빌드 (Gradle)
provisioning/DockerImageBuildService    빌드 (이미지)
provisioning/WebImageBuildService       빌드
provisioning/FrontendStaticHostingAdapter

분리하지 않고 기본 사용자를 뒤집으면 배포 파이프라인까지 영향을 받습니다. 분리해 두면 빌드 경로는 한 줄도 안 바뀌므로 재검증할 것이 없어집니다 — 이 작업에서 위험을 가장 크게 줄이는 한 걸음입니다.

2단계. 프리뷰 컨테이너의 워크스페이스를 처음부터 node 소유로 만든다

컨테이너 생성 시 root 가 mkdir -p /workspace/app && chown -R node:node /workspace. 중간에 넘기지 않습니다.

nodenode:20-alpine 에 이미 있는 uid 1000 이라 전용 이미지가 필요 없습니다. (/ 가 root 소유라 node 스스로는 /workspace 를 못 만듭니다 — 그래서 root 가 만들어 넘깁니다.)

3단계. exec 기본 사용자를 node 로 뒤집고, root 가 필요한 것만 표시한다

방향이 중요합니다. 워크스페이스를 만지는 곳을 하나씩 찾아 node 로 바꾸면 40곳을 훑어야 하고, 하나 빠뜨리면 그 경로만 조용히 깨집니다. 반대로 기본을 뒤집으면 root 가 필요한 목록이 짧습니다.

  • apk add ... (4곳)
  • nginx 기동·종료 (PreviewRuntimeLauncher, FrontendStaticHostingAdapter)
  • 컨테이너 생성 시 mkdir + chown

HOME=/home/node 를 함께 주세요. 없으면 npm 캐시와 git config --global 이 root 홈을 쓰려다 죽습니다.

4단계. (별건) egress 허용목록

이 저장소에 이미 답이 있습니다 — 코딩 에이전트가 internal 네트워크 + tinyproxy CONNECT 허용목록으로 돕니다(CodingAgentProperties.Egress). 같은 방식을 프리뷰에 적용하면 되지만 허용목록이 넓어야 하고(npm·yarn·pnpm·bun 레지스트리, GitHub, Maven Central, Gradle 배포처) 빌드를 깨뜨릴 위험이 실질적이라, 3단계와 분리해서 실제 프로젝트로 태워 보며 하는 편이 낫습니다.

⚠️ 함정 — 실측으로 확인한 것들

컨테이너는 재사용됩니다. /tmp/qeploy-build.log 가 이전 실행에서 root 소유로 남아 있으면 node 의 tee 가 거기서 죽습니다. 빌드가 아니라 로깅 때문에 실패하고, 원인은 로그에 안 남습니다.

정리 전 tee → exit=1
정리 후 tee → exit=0

빌드가 git 자격증명을 물려받지 않게 됩니다. git config --global 로 설정한 credential.helper 는 root 의 것이라, 빌드를 node 로 돌리면 저장소의 빌드 스크립트가 우리 GitHub 자격증명에 닿지 못합니다. 보안상 이득이지만 private git 의존성을 쓰는 프로젝트는 설치가 깨질 수 있습니다 — 그런 사례가 나오면 자격증명을 어느 사용자에 둘지 따로 정해야 합니다.

JAVA_FULLSTACK 경로가 가장 덜 검증된 곳입니다. PreviewRuntimeLauncher 가 프리뷰 컨테이너 안에서 nginx·openjdk 를 돌리는데, 여기는 제가 재보지 못했습니다. nginx 는 root 로 두면 되지만(워크스페이스를 읽기만 하므로), 실제로 그런지는 돌려 봐야 합니다.

📋 이미 되어 있는 것 (다시 하지 마세요)

항목 상태
cap-drop ALL + no-new-privileges 적용
메모리·swap·CPU·PID 상한 적용
컨테이너 간 통신 차단(enable_icc=false) 적용
게시 포트 루프백 전용 적용
docker socket · 바인드 마운트 없음
exec 타임아웃 10분 적용
MySQL 루프백 바인딩 적용
Qeploy API 루프백 바인딩 적용 (PR #333)

✅ 완료 기준

  • 프리뷰 컨테이너와 빌드 컨테이너가 분리되어, 빌드 경로가 이 변경의 영향을 받지 않는다
  • 저장소의 코드(npm install postinstall·빌드 스크립트)가 uid 0 이 아닌 사용자로 실행된다
  • 워크스페이스의 소유자가 하나다(권한 충돌 없음)
  • apk add·nginx 등 플랫폼 명령이 그대로 동작한다
  • JAVA_FULLSTACK 프리뷰가 정상 동작한다
  • (별건) 프리뷰 egress 가 허용목록으로 좁혀진다

📎 참고

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

backendbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions