Skip to content

[Preview] 프리뷰 컨테이너를 node 로 돌린다 [ #332 ] - #357

Merged
dldnsgkr merged 1 commit into
developfrom
unhak/preview-runs-as-node
Sep 14, 2026
Merged

dldnsgkr merged 1 commit into
developfrom
unhak/preview-runs-as-node

Conversation

@dldnsgkr

Copy link
Copy Markdown
Collaborator

#332 2·3단계. 둘은 나눌 수 없다 — 사용자를 뒤집으려면 워크스페이스가 먼저 그 사용자 소유여야 하고, 반대로 소유자만 바꾸면 root 명령이 전부 막힌다. 1단계는 #356.

프리뷰 컨테이너는 사용자가 연결한 저장소를 실제로 빌드해 돌린다. npm install 의 postinstall 과 빌드 스크립트는 저장소가 정하는 코드이고, 그것이 지금까지 root 로 돌았다.

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

방향이 요점이다

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

root 로 남는 것 이유
installPackages (신설) 설치라는 행위 자체를 root 로 고정. 호출부마다 판단하게 두면 한 곳만 빠뜨려도 조용히 깨진다
nginx 기동·종료 워크스페이스를 읽기만 한다. 끄는 것도 root — node 는 root 프로세스에 시그널을 못 보낸다
생성 직후 mkdir + chown / 가 root 소유라 node 스스로는 /workspace 를 못 만든다

이슈 목록에 없던 두 곳을 채웠다

템플릿 씨딩과 diff 기준 커밋apk 를 명령 체인 안에 품고 있었다. 그대로 두면 체인 전체가 root 로 돌아 작업 트리에 root 소유 파일이 섞이고, 그 뒤 node 명령이 막힌다. 설치만 떼어 root 로 보냈다.

빌드 경로는 한 줄도 안 바뀐다

1단계에서 역할을 가른 이유가 이것이다. BUILD 는 사용자 설정이 없어 그대로 root 이고, 소유자 준비도 하지 않는다.

dev 실측 — 전 경로를 실제 컨테이너로 태웠다

소유자 준비 → installPackages(root) → 씨딩(node) → 기준 커밋(node) → 수정 후 diff  전부 ok
git config --global · npm install    HOME=/home/node 로 정상
nginx                                 root 기동 exit=0, node 소유 파일 서빙 ok
                                      node 는 못 끔 / root 는 끔
격리                                  node 가 /etc/shadow 못 읽음, apk 거부
exec -u root                          no-new-privileges 와 무관하게 동작(데몬이 정하는 값)

nginx: [alert] could not open error log file: /var/lib/nginx/logs/error.log 는 설정의 error_log 를 읽기 전 컴파일 기본값을 먼저 여는 것이라 기존에도 있던 alert 다. 기동은 exit=0 이고 서빙도 정상이다.

기존 컨테이너

영향받지 않는다. 사용자 설정이 없어 그대로 root 로 돌고 execAsRoot 도 같은 결과라, 재생성 없이 이어진다.

검증

  • 전체 1682건 통과
  • 계약 고정: 프리뷰는 withUser("node") + HOME=/home/node, 빌드는 withUser(null) 이고 소유자 준비 exec 조차 하지 않음, 설치는 installPackages 로만, diff·씨딩 명령에 apk 가 섞이지 않음

남은 것 (#332 완료 기준 중)

  • JAVA_FULLSTACK 프리뷰 실제 구동 — 이슈가 "가장 덜 검증된 곳" 으로 지목한 경로. nginx·워크스페이스 권한은 위에서 확인했지만 ./gradlew bootRun 까지 태운 실측은 아직입니다
  • egress 허용목록 — 이슈가 별건으로 분리한 4단계

🤖 Generated with Claude Code

https://claude.ai/code/session_013y8USoCXTsRTATAhy88M93

#332 2·3단계. 둘은 나눌 수 없다 — 사용자를 뒤집으려면 워크스페이스가 먼저 그 사용자
소유여야 하고, 반대로 소유자만 바꾸면 root 명령이 전부 막힌다.

프리뷰 컨테이너는 사용자가 연결한 저장소를 실제로 빌드해 돌린다. npm install 의
postinstall 과 빌드 스크립트는 저장소가 정하는 코드이고, 그것이 지금까지 root 로
돌았다. cap-drop ALL + no-new-privileges 라 탈출로 바로 이어지지는 않지만, 지금
안전한 진짜 이유는 바인드 마운트가 없기 때문이다 — 나중에 누가 마운트를 하나 붙이는
순간 root 라는 사실이 갑자기 중요해지고, 그때는 이 전제가 기억나지 않는다.

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

- 패키지 설치 — installPackages 로 모았다. 호출부마다 "여긴 root 여야 하나" 를 다시
  판단하게 두면 한 곳만 빠뜨려도 그 경로가 조용히 깨지므로, 설치라는 행위 자체를
  root 로 고정했다
- nginx 기동·종료 — 워크스페이스를 읽기만 하므로 root 로 둔다. 끄는 것도 root 여야
  한다(node 는 root 프로세스에 시그널을 못 보낸다)
- 컨테이너 생성 직후의 mkdir + chown

이슈 본문의 root 목록에 없던 두 곳을 채웠다. 템플릿 씨딩과 diff 기준 커밋은 apk 를
명령 체인 안에 품고 있었다. 그대로 두면 체인 전체가 root 로 돌아 작업 트리에 root
소유 파일이 섞이고, 그 뒤 node 명령이 막힌다. 설치만 떼어 root 로 보냈다.

빌드 컨테이너는 한 줄도 바뀌지 않는다. 1단계에서 역할을 가른 이유가 이것이다.

dev 에서 실제 컨테이너로 전 경로를 태웠다:

- 소유자 준비 → installPackages(root) → 씨딩(node) → 기준 커밋(node) → 수정 후 diff
- git config --global · npm install 이 HOME=/home/node 로 정상 동작
- nginx: root 기동 exit=0, node 소유 파일 정상 서빙, node 는 못 끄고 root 는 끈다
- 격리: node 가 /etc/shadow 를 못 읽고 apk 가 거부된다
- exec -u root 는 no-new-privileges 와 무관하게 동작한다(데몬이 정하는 값)

기존 컨테이너는 영향받지 않는다 — 사용자 설정이 없어 그대로 root 로 돌고,
execAsRoot 도 같은 결과라 재생성 없이 이어진다.

테스트 1682건 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y8USoCXTsRTATAhy88M93
@dldnsgkr
dldnsgkr merged commit d9c15b3 into develop Sep 14, 2026
1 check passed
@dldnsgkr
dldnsgkr deleted the unhak/preview-runs-as-node branch September 14, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant