Skip to content

WebShell

WebShell 은 플랫폼 나머지가 사용하는 같은 geminio 터널을 통해 모든 등록된 edge 에 도달하는 브라우저 대면 터미널입니다. 별도의 SSH bastion 없음, 점프 호스트 없음, 인바운드 포트 없음. Edge 는 계속 다이얼 아웃; 매니저는 셸 I/O 용 멀티플렉스 스트림 클래스를 엽니다.

사용 사례:

  • 에이전트가 수정을 제안; "edge-prod-04 에 셸 열기" 를 클릭하고 SPA 를 떠나지 않고 변경 확인.
  • 벤더 / 계약자가 VPN 등록 없이 한 호스트에 일회성 확인 필요.
  • Incident 대응: 모든 명령은 발원 세션의 감사 행과 함께 기록됨.

아키텍처

text
browser ──WebSocket──> manager:/v1/webshell/ws

                          ├─ Router.Register(sessionID, sink, ActiveSession)

                          └─ geminio Stream (shell class)


                              edge agent
                                  └─ pty.Start("/bin/bash")

매니저 측 라우터는 internal/manager/biz/webshell/router.go 에. sessionID → Sink 디렉터리 유지: WebSocket 핸들러가 연결 시 등록, 터널 인커밍 디스패처가 edge 의 출력 / exit push 를 올바른 브라우저로 라우팅.

go
// internal/manager/biz/webshell/router.go:57
type Router struct {
    mu          sync.RWMutex
    sinks       map[string]Sink
    meta        map[string]*ActiveSession // sessionID → metadata
    stdoutBytes sync.Map                  // sessionID → *uint64
}

HTTP / WebSocket 핸들러는 옆 internal/manager/server/webshell 에 있어 라우터는 HTTP 비종속이고 단위 테스트 가능한 상태를 유지.

두 스트림 클래스

geminio 터널이 멀티플렉스:

  1. 컨트롤 클래스 — JSON RPC (기능 실행, 플러그인 시그널링, 알림 evaluator 프로브).
  2. 셸 클래스 — 원시 바이트 스트림 (WebShell 세션당 하나, tail -f follower 당 하나 등).

터널 레벨에서 분할이 중요한 이유는 셸 I/O 가 버스트 있고 프레임이 없기 때문; 컨트롤 RPC 와 혼합하면 후자를 굶주리게 만듭니다. 각 클래스는 자체 backpressure 예산을 가짐.

세션 메타데이터

각 라이브 세션은 ActiveSession 가짐:

go
// router.go:37
type ActiveSession struct {
    SessionID    string
    OngridUserID uint64
    SSHUser      string
    DeviceID     uint64
    EdgeID       uint64
    StartedAt    time.Time
    LastInputAt  time.Time // updated on every browser → edge frame
}

LastInputAt 이 모든 키 입력 (Router.TouchInput) 에 틱. 유휴 타임아웃 watchdog 이 최근 입력 없이 구성 제한보다 오래된 세션을 축출 — "실행 중인 명령과 함께 브라우저 탭을 닫았다" 누수를 방어.

감사 녹화

두 레이어:

  1. 헤더 행webshell_sessions 테이블: 누가, 언제, 어느 edge, exit code, 총 in/out 바이트.
  2. 스트림 녹화 — 매니저 측 Recorder 인터페이스가 와이어를 가로지르는 모든 바이트 (양방향, 타임스탬프) 를 받아 /var/lib/ongrid/webshell-recordings/<session_id>.cast 아래의 asciinema 호환 cast 파일에 append. Admin /admin/webshell 페이지가 재생.

Recorder 인터페이스는 의도적으로 좁음 — 프로덕션은 파일 sink 사용; 테스트는 가짜 주입; 향후 클라우드 blob 백엔드가 나머지 스택을 건드리지 않고 들어옴.

동시성 제한

사용자별 상한: Router.CountByUser 가 WebSocket open 핸들러에서 호출됨; 상한 초과 연결은 HTTP 429 로 거부. 기본 상한 5 (구성 가능). Edge 별 상한은 runaway 에이전트가 100 동시 셸을 여는 것을 방어.

세션 종료

세 경로가 세션 종료:

  1. 브라우저 닫기 — WebSocket disconnect 가 edge 로 전파, edge 는 pty 에 kill -HUP.
  2. 관리자 종료 — admin SPA 가 Sink 에서 Killer.Kill(reason="admin terminated") 호출, edge 로 닫기 터널링. 세션의 exit 행에 reason 기록.
  3. 유휴 축출 — watchdog 이 LastInputAt 이 상한을 초과한 세션에 Kill("idle timeout") 발화.
go
// router.go:50
type Killer interface {
    Kill(reason string)
}

매니저 측 핸들러가 Sink 를 등록할 때 Killer 를 설치. Opt-in 하는 모든 Sink 는 관리자 종료 가능; 나머지는 브라우저 닫기만 가능.

Role 게이팅

WebShell 은 admin role 에 게이트 (ADR-022 RBAC). user role 은 에이전트와 채팅 가능하지만 셸을 열 수 없음; viewer 는 과거 세션의 녹화를 읽을 수 있지만 새로 열 수 없음. 게이트는 WebSocket 업그레이드 전에 HTTP 핸들러 진입에서 실행.

같이 보기

  • 기능bash 기능은 WebShell 의 일회성 등가물 (단일 명령, pty 없음). 같은 감사 기질.
  • Edge install — WebShell 이 도달할 수 있도록 호스트 의 edge 에이전트를 띄우기.
  • 아키텍처 — geminio 터널이 어디에 위치하는지.