아키텍처
아키텍처와 설계
왜 Rust인가
SSH-Frontière는 세 가지 이유로 Rust로 작성되었습니다:
-
메모리 안전성: 버퍼 오버플로 없음, use-after-free 없음, 널 포인터 없음. 로그인 셸로 실행되는 보안 컴포넌트에게 이것은 결정적입니다.
-
정적 바이너리:
x86_64-unknown-linux-musl타겟으로 컴파일하면(다른 타겟도 가능하나 동작 보장 없음) 바이너리는 약 1 Mo이며 시스템 의존성이 없습니다. 서버에 복사하면 바로 사용 가능합니다. -
성능: 프로그램이 시작, 검증, 실행, 종료를 밀리초 단위로 완료합니다. 런타임 없음, 가비지 컬렉터 없음, JIT 없음.
동기식이며 일회성
SSH-Frontière는 동기식 원샷 프로그램입니다. 데몬 없음, async 없음, Tokio 없음.
생명주기는 단순합니다:
sshd가 키로 SSH 연결을 인증sshd가 fork하고ssh-frontiere를 로그인 셸로 실행ssh-frontiere가 명령을 검증하고 실행- 프로세스 종료
각 SSH 연결은 새로운 프로세스를 생성합니다. 연결 간 공유 상태 없음, 동시성 문제 없음.
코드 구조
코드는 명확한 책임을 가진 모듈로 구성되어 있습니다:
| 모듈 | 책임 |
|---|---|
main.rs | 진입점, 인자 평탄화, 오케스트레이터 호출 |
orchestrator.rs | 메인 플로우: 배너, 헤더, 명령, 응답, 세션 루프 |
config.rs | TOML 구성 구조체, fail-fast 검증 |
protocol.rs | 헤더 프로토콜: 파서, 배너, 인증, 세션, body |
crypto.rs | SHA-256 (FIPS 180-4 구현), base64, 논스, 챌린지-응답 |
dispatch.rs | 명령 파싱 (따옴표, key=value), 해석, RBAC |
chain_parser.rs | 명령 체인 파서 (연산자 ;, &, |) |
chain_exec.rs | 체인 실행: 엄격 순차(;), 허용 순차(&), 복구(|) |
discovery.rs | help 및 list 명령: 도메인과 액션 탐색 |
logging.rs | 구조화된 JSON 로깅, 민감한 인자 마스킹 |
output.rs | JSON 응답, 종료 코드 |
lib.rs | proof 바이너리 및 fuzz 헬퍼를 위한 crypto 노출 |
각 모듈은 동일 디렉토리에 테스트 파일(*_tests.rs)이 있습니다.
보조 바이너리 proof (src/bin/proof.rs)는 E2E 테스트 및 클라이언트 통합을 위한 인증 proof를 계산합니다.
헤더 프로토콜
SSH-Frontière는 stdin/stdout 상의 텍스트 프로토콜을 사용합니다. 접두사는 방향에 따라 다릅니다:
클라이언트에서 서버로 (stdin):
| 접두사 | 역할 |
|---|---|
+ | 구성: 디렉티브 (auth, session, body) |
# | 주석: 서버에서 무시됨 |
| (일반 텍스트) | 명령: 도메인 액션 [인자] |
. (한 줄에 단독) | 블록 끝: 명령 블록 종료 |
서버에서 클라이언트로 (stdout):
| 접두사 | 역할 |
|---|---|
#> | 주석: 배너, 안내 메시지 |
+> | 구성: capabilities, 챌린지 논스 |
>>> | 응답: 최종 JSON 응답 |
>> | Stdout: 스트리밍 표준 출력 (ADR 0011) |
>>! | Stderr: 스트리밍 오류 출력 |
연결 흐름
클라이언트 서버
| |
| <-- 배너 + capabilities ------------- | #> ssh-frontiere 0.1.0
| | +> capabilities rbac, session, help, body
| | +> challenge nonce=a1b2c3...
| | #> type "help" for available commands
| |
| --- +auth (선택사항) ---------------> | + auth token=runner-ci proof=deadbeef...
| --- +session (선택사항) ------------> | + session keepalive
| |
| --- 명령 (일반 텍스트) -------------> | forgejo backup-config
| --- 블록 끝 -----------------------> | .
| <-- 스트리밍 stdout ---------------- | >> Backup completed
| <-- 최종 JSON 응답 ---------------- | >>> {"status_code":0,"status_message":"executed",...}
| |
| (세션 keepalive인 경우) |
| --- 명령 2 -----------------------> | infra healthcheck
| --- 블록 끝 -----------------------> | .
| <-- JSON 응답 2 ------------------ | >>> {"status_code":0,...}
| --- 세션 종료 (빈 블록) -----------> | .
| <-- 세션 종료 -------------------- | #> session closed
JSON 응답
각 명령은 >>>로 시작하는 한 줄의 최종 JSON 응답을 생성합니다. 표준 출력과 오류 출력은 >>와 >>!를 통해 스트리밍으로 전송됩니다:
>> Backup completed
>>> {"command":"forgejo backup-config","status_code":0,"status_message":"executed","stdout":null,"stderr":null}
- 최종 JSON 응답에서
stdout/stderr=null: 출력이>>와>>!를 통해 스트리밍으로 전송되었음 - 실행되지 않은 명령(거부, 구성 오류)에서도
stdout과stderr는null
body 프로토콜
+body 헤더를 사용하면 stdin을 통해 자식 프로세스에 여러 줄의 콘텐츠를 전송할 수 있습니다. 네 가지 구분 모드:
+body:.(마침표)만 있는 줄까지 읽음+body size=N: 정확히 N바이트를 읽음+body stop="구분자": 구분자가 있는 줄까지 읽음+body size=N stop="구분자": 먼저 도달하는 구분자(크기 또는 마커)에서 읽기 종료
TOML 구성
구성 형식은 선언적 TOML입니다. ADR 0001에 문서화된 선택:
- 왜 TOML인가: 사람이 읽을 수 있음, 네이티브 타입, Rust 생태계 표준, 유의미한 들여쓰기 없음(YAML과 달리), 구성에 JSON보다 표현력이 풍부함.
- 왜 YAML이 아닌가: 유의미한 들여쓰기가 오류의 원인, 위험한 암묵적 타입(
on/off→ 불리언), 복잡한 사양. - 왜 JSON이 아닌가: 주석 없음, 장황함, 사람의 구성을 위해 설계되지 않음.
구성은 로드 시 검증(fail-fast): TOML 문법, 필드 완전성, 플레이스홀더 일관성, 최소 하나의 도메인, 도메인당 최소 하나의 액션, 비어있지 않은 enum 값.
의존성 정책
SSH-Frontière는 필수적이지 않은 의존성 제로 정책을 가지고 있습니다. 각 외부 크레이트는 실질적인 필요로 정당화되어야 합니다.
현재 의존성
직접 의존성 3개, 전이 의존성 약 20개:
| 크레이트 | 용도 |
|---|---|
serde + serde_json | JSON 직렬화 (로깅, 응답) |
toml | TOML 구성 로드 |
평가 매트릭스
의존성을 추가하기 전에 8개의 가중 기준(5점 만점)으로 평가합니다: 라이선스(탈락 기준), 거버넌스(x3), 커뮤니티(x2), 업데이트 빈도(x2), 크기(x3), 전이 의존성(x3), 기능(x2), 비종속성(x1). 최소 점수: 3.5/5.
감사
cargo deny로 라이선스 및 알려진 취약점 확인cargo audit로 RustSec 데이터베이스에서 취약점 검색- 허용된 소스: crates.io만
프로젝트 개발 과정
SSH-Frontière는 체계적인 TDD 방법론과 함께 Claude Code 에이전트에 의해 주도된 순차적 단계(1~9, 중간 단계 2.5 및 5.5 포함)로 개발되었습니다:
| 단계 | 내용 |
|---|---|
| 1 | 기능적 디스패처, TOML 구성, 3단계 RBAC |
| 2 | 프로덕션 구성, 운영 스크립트 |
| 2.5 | SHA-256 FIPS 180-4, BTreeMap, 우아한 타임아웃 |
| 3 | 통합 헤더 프로토콜, 챌린지-응답 인증, 세션 |
| 4 | E2E SSH Docker 테스트, 코드 정리, 포지 통합 |
| 5 | 가시성 태그, 토큰별 수평 필터링 |
| 5.5 | 선택적 논스, 명명된 인자, proof 바이너리 (6단계 포함, 병합) |
| 7 | 구성 가이드, dry-run --check-config, 접두사 없는 help |
| 8 | 구조화된 오류 타입, clippy pedantic, cargo-fuzz, proptest |
| 9 | body 프로토콜, 자유 인자, max_body_size, 종료 코드 133 |
프로젝트 참여자:
- Julien Garderon (BO): 컨셉, 기능 사양, Rust 선택, 프로젝트 명명
- Claude 감독자 (PM/Tech Lead): 기술 분석, 아키텍처
- Claude Code 에이전트: 구현, 테스트, 문서화
사람과 기계가 함께 일하며, 더 좋고, 더 빠르고, 더 안전하게.