개요 — 무엇을 하나
인스턴스 SSH 터미널은 Assets ▸ Instances 카드의 [ssh] 액션으로 여는 브라우저 내장 대화형 셸입니다. 별도 SSH 클라이언트(PuTTY·터미널 앱)를 띄우지 않고, 등록된 OS 인스턴스(infr_instance)에 콘솔에서 바로 접속해 명령을 실행합니다.
핵심은 진짜 PTY(의사 터미널) 라는 점입니다. 단발 요청/응답 실행이 아니라 원격 sshd 에 PTY 채널을 할당하므로 top·vi·less·sudo·htop 같은 대화형·전체화면 프로그램이 그대로 동작합니다(원격에서 tty 를 찍으면 /dev/pts/* 가 나옵니다).
브라우저 xterm.js ⇄ (WebSocket) ⇄ 브리지 scripts/infr_ssh_bridge.cjs (ssh2) ⇄ 원격 sshd (PTY)
- Reverb 아님 — Reverb 는 pub/sub 라 PTY 바이트 스트림에 부적합합니다. 전용 WebSocket 브리지를 씁니다.
- node-pty 아님 —
ssh2(순수 JS)가 원격 sshd 에 직접 붙어shell({term,cols,rows})채널을 열므로 로컬 PTY 라이브러리가 필요 없습니다. Windows/Linux 공통.
같은 컨트롤러(
InstanceSshController)에는 요청/응답 콘솔용test·exec엔드포인트도 남아 있습니다. 이는 프로브 같은 단발 명령 실행용이며, UI 의 대화형 터미널은 항상 PTY(WS 브리지) 경로를 씁니다. 원격 에뮬레이트의 프로브 실행 등이 요청/응답 경로를 활용합니다.
언제 쓰나
- 장애 대응 중 즉시 인스턴스에 붙어
top/journalctl/systemctl status로 상태를 확인할 때. - 설정 파일을
vi/nano로 직접 수정하거나sudo로 서비스를 재시작할 때. - 버전 관리·서버 파라미터 프로브가 뽑은 값을 손으로 재확인할 때.
- 별도 배스천/점프 호스트 없이, 콘솔의 RBAC·감사 아래에서 통제된 셸 접근이 필요할 때.
반복 진단은 셸에서 손으로 치기보다 원격 에뮬레이트·버전 프로브로 자동화하는 편이 재현 가능하고 감사 추적에 남습니다. SSH 터미널은 탐색적·일회성 작업에 알맞습니다.
화면 구성
| 영역 | 내용 |
|---|---|
| 터미널 본체 | xterm.js 캔버스. 원격 PTY 출력을 그대로 렌더(색·커서·전체화면 TUI). |
| 상태 표시 | connecting → ready → closed/error. 연결 대상 user@host:port·오류 메시지. |
| 비밀번호 게이트 | password 인증 인스턴스에서 접속 전 비밀번호 입력. "이 브라우저에 기억"(선택) 토글. |
| 전체화면 토글 | 터미널을 확장/복원. 크기 변경 시 자동 fit() → 원격에 resize 전송. |
| 재접속 / 닫기 | 세션 재시작(새 티켓 발급) · 종료(WS·터미널 정리). |
상태 값
| 상태 | 의미 |
|---|---|
idle |
모달만 열림, 아직 미접속. |
connecting |
티켓 발급·WS·SSH 핸드셰이크 진행 중. |
ready |
원격 셸 준비 완료 — 입력 가능. |
error |
인증 실패·셸 열기 실패·WS 오류 등(메시지 표시). |
closed |
세션 종료(원격 exit·유휴 타임아웃·최대수명 초과·수동 닫기). |
사용법
1. 접속
- Assets ▸ Instances 에서 대상 카드의 [ssh] 아이콘을 클릭합니다.
- 인증 방식에 따라:
- key 인스턴스: 즉시 접속 시도(저장 개인키 또는 브리지 호스트 키/에이전트).
- password 인스턴스: 비밀번호 게이트에 비밀번호를 입력하고 접속. (선택) "이 브라우저에 기억" 체크 시 브라우저 로컬 볼트(암호화)에만 저장됩니다 — 서버에는 저장되지 않습니다.
- 상태가
ready가 되면 터미널에 입력할 수 있습니다.
2. 크기 조정 · 전체화면
- 브라우저 창/모달 크기를 바꾸면
ResizeObserver가fit()후{t:'resize',cols,rows}를 브리지로 보내 원격 PTY 창 크기를 맞춥니다(stream.setWindow). - 전체화면 토글로 넓은 로그·TUI 를 편하게 봅니다.
3. 종료
- 원격에서
exit또는 모달 닫기 → WS·xterm 정리. 세션 메모리의 비밀번호도 폐기됩니다. - 15분 무입력 시 자동 종료(
idle timeout), 2시간 하드 상한(max session time)에 도달하면 강제 종료됩니다.
인증 흐름(2단계)
1) Laravel: POST /asset/instances/ssh/session
└ level≥2 확인 → 서명 티켓 발급(20s TTL, 감사 로그)
payload = {iid, host, port, user, auth, uid, cols, rows, exp} ← 비밀번호 없음
2) 브라우저: WebSocket 접속 WebSocket/?ticket=<티켓>
└ 브리지가 verifyTicket() 로 서명·만료·필수필드 검증
└ 최초 프레임으로 자격 전달:
password 인스턴스 → {t:'auth', password:'…'} (WS 로만, 미저장)
key 인스턴스 → {t:'auth', privateKey:'…', passphrase:'…'}
- key 인증: 인스턴스에 개인키가 저장돼 있으면(
ssh_key_enc),/session이 복호화해 응답에 실어 브라우저로 내려주고, 브라우저가 WS 최초 프레임으로 브리지에 릴레이합니다 — 디스크·DB 에 다시 저장하지 않습니다. 저장 키가 없으면 브리지 호스트의~/.ssh/id_ed25519·id_rsa또는 ssh-agent 로 폴백합니다. - password 인증: 브라우저 입력값을 WS 최초 프레임으로만 전달합니다. sshd 설정에 따라
keyboard-interactive폴백 응답에도 같은 값을 씁니다.
브리지 프로토콜(WebSocket 프레임)
| 방향 | 형식 | 프레임 |
|---|---|---|
| client → bridge | JSON text | {t:'auth', password|privateKey|passphrase} · {t:'data', d} · {t:'resize', cols, rows} |
| bridge → client | binary | 터미널 출력 바이트(term.write) |
| bridge → client | JSON text | {t:'status', s, m} (connecting/ready/error/closed) |
데이터 소스
이 화면은 시계열 테이블을 읽지 않습니다. 접속 대상·자격의 원천은:
| 원천 | 용도 |
|---|---|
infr_instance |
접속 대상 — ssh_host·ssh_port·ssh_user·auth_method. 인스턴스 CMDB 소유. |
infr_instance.ssh_key_enc / ssh_key_pass_enc |
인스턴스별 개인키(Laravel Crypt 암호화). key 인증 시 /session 이 복호화·릴레이. has_ssh_key 불리언만 UI 에 노출. |
| 브리지 설정 | 접속 주소·수명 등. 설치 시 도입 담당자가 구성합니다. |
| 브라우저 로컬 볼트(clientVault) | (선택) 기억한 비밀번호 — 암호화되어 브라우저에만 저장, 서버 전송·저장 없음. |
브리지 실행(개발 런처가 ssh pane 으로 띄움 · 프로덕션은 supervised):
브리지는 설치 시 도입 담당자가 구성·기동합니다. 서명 키가 설정되지 않으면 대화형 셸은 동작하지 않습니다. 터미널이 열리지 않으면 도입 담당자에게 문의해 주세요.
권한
| 작업 | 최소 레벨 | 근거 |
|---|---|---|
/session 티켓 발급(대화형 접속) |
≥ 2 | InstanceSshController::allowed() — 세션 infr_user_level >= 2. |
/exec 원격 명령 실행(요청/응답) |
≥ 2 | 동일 게이트. 인증된 임의 원격 명령 실행이므로 전량 감사. |
/test 연결 프리플라이트 |
≥ 2 | 동일 게이트. |
- 레벨 매핑: demo=1 / staff=5 / admin=10. demo(1)은 SSH 터미널을 열 수 없습니다 — staff(5) 이상만 가능합니다(RBAC 레벨).
- 감사:
session·exec·test는 각각[audit] instance_ssh.{session,exec,test}로 기록됩니다(인스턴스 id·호스트·인증방식·행위자infr_login_id).exec는 실행한 명령의 앞부분(최대 300자)도 남깁니다. 비밀번호·개인키는 로그·티켓에 절대 남기지 않습니다. 감사 열람은 감사 로그 참조.
문제해결 · FAQ
대화형 셸이 안 열리고 503(pty_off) 이 뜹니다
- 대화형 셸 기능이 꺼져 있거나 브리지가 구성되지 않은 상태입니다. 도입 담당자에게 문의해 주세요.
Connection error / WS 가 바로 닫힙니다
- 브리지가 떠 있는지 확인:
node scripts/infr_ssh_bridge.cjs(개발 런처sshpane). 헬스체크는 브리지 헬스체크 주소 가infr-ssh-bridge ok를 반환합니다. - 티켓 TTL(기본 20s) 안에 접속해야 합니다. 발급 후 지연되면 만료로 거부됩니다.
authentication failed (인증 실패)
- password: 게이트 비밀번호가 틀렸습니다. UI 가 자동으로 비밀번호 게이트를 다시 띄우고, 기억해 둔 값이 있으면 폐기합니다.
- key: 저장 개인키가 대상
authorized_keys와 맞는지, 또는 브리지 호스트의~/.ssh키/에이전트가 유효한지 확인합니다. 저장 개인키의 복호화가 실패하면(APP_KEY 변경 등) 키 없이 진행돼 폴백을 시도합니다. - 대상 sshd 의
authorized_keys등록은 SE 수작업입니다(콘솔이 원격 키를 배포하지 않습니다).
too many active sessions (code 1013)
- 동시 세션 상한에 도달했습니다. 사용하지 않는 터미널 탭을 닫은 뒤 다시 접속해 주세요.
세션이 저절로 끊깁니다
- 둘 다 브리지가 강제 종료합니다. 재접속하면 새 티켓으로 다시 붙습니다.
비밀번호는 어디에 저장되나?
서버에는 저장하지 않습니다. 브라우저가 WS 최초 프레임으로만 브리지에 전달합니다. "이 브라우저에 기억"을 켜면 브라우저 로컬 볼트에 암호화되어 저장되며(clientVault), 서버로는 오지 않습니다. "잊기"로 지울 수 있습니다.
호스트키 검증은?
개발 브리지는 호스트키를 accept(hostVerifier 가 true)합니다. 프로덕션은 wss + 호스트키 핀닝을 권장합니다(브리지를 127.0.0.1 밖에 노출한다면 특히).
no_pw_tool 오류(요청/응답 test/exec)
요청/응답 경로에서 비밀번호 인증을 쓰려면 서버에 plink(Windows) 또는 sshpass(Linux)가 필요합니다. 대화형 PTY 경로는 이 도구가 필요 없습니다(브리지의 ssh2 가 직접 처리).
관련 항목
- 인스턴스 CMDB — 접속 대상 인스턴스(
infr_instance)와 SSH 접속 정보 등록. - 원격 에뮬레이트 — SSH 대신 tlx.php 릴레이로 프로브를 배포·실행(요청/응답).
- 인스턴스 버전 관리 · 서버 파라미터 — 셸에서 확인할 값의 자동 수집.
- RBAC 레벨 — SSH 게이트(≥2) 등 레벨 정책.
- 감사 로그 —
instance_ssh.*감사 이벤트 열람. - 실시간 아키텍처 — 왜 SSH 는 Reverb 가 아니라 전용 WS 브리지인가.
![카드의 [ssh] 버튼](/manual-shots/ko/instance-card.webp)