Skip to content

About

Credential-isolated, human-gated SFTP tools for OpenCode agents — 위험 분류와 사람 승인을 코드로 강제 (작업 모델: space-bunny-free / 게시: DSc dsclub)

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

opencode-sftp-guard

AI 에이전트가 원격 서버 파일을 다루되, 그 모든 행위가 "코드 안에서" 위험 분류되고 "사람의 승인"을 거쳐야만 실행되는 OpenCode 플러그인.

컨테이너 → 호스트 루프백 SFTP(127.0.0.1:22) 전용. 자격증명은 모델이 절대 볼 수 없다.

작업 모델 (모델명) space-bunny-free — 모델 ID opencode/space-bunny-free
작성 · 게시 주체 DSc (dsclub) — DSc GitHub 계정(tak2-08)을 통해 게시
대상 런타임 OpenCode 1.18.22 (Bun) · 런타임 독립 코어는 Node 20+ 로도 실행
의존성 [email protected] (→ [email protected]) 단 하나, 고정 버전
테스트 132개 전부 통과 — 단위 99 + 실제 SFTP 통신 통합 22 + 설정 fail-closed 11
라이선스 별도 LICENSE 파일 없음 (기본=rights reserved)

English summary — Credential-isolated, human-gated SFTP tools for OpenCode. An agent can list/read/write/move/delete files on a remote server over a loopback SFTP connection, but every mutating call passes an in-code risk classifier and a human-approval gate that fails closed. Credentials are read once at plugin init from a 600-mode secrets file and are structurally incapable of reaching any tool schema, return value, error, or log. Ships 13 tools, an opt-in read-only risk-auditor subagent, and a hash-chained local audit log. 132 tests pass, including integration tests against a real in-process SFTP server.


목차

  1. 왜 이 플러그인이 필요한가
  2. 위협 모델
  3. 아키텍처
  4. 설치
  5. 사용법
  6. 승인 매트릭스
  7. 위험 분류 규칙
  8. 감사자 서브에이전트
  9. 감사 로그
  10. 설정 참조
  11. 검증
  12. 이 플러그인이 막지 못하는 것
  13. 가정한 것 / 의도적 편차
  14. 코드 지도
  15. 저장소 구조

상세 문서:

문서 내용
docs/AUTHORSHIP.md 출처와 책임 — 작업 모델(space-bunny-free)과 게시 주체(DSc (dsclub))의 구분, 모델이 한 일/하지 않은 일
docs/SECURITY-REVIEW.md §0 불변식 자기 점검(파일:라인 대응), 위협 모델, 발견해 고친 취약점 이력, 우회 벡터와 대응
docs/IMPLEMENTATION.md 모듈별 구현 상세, 데이터 흐름, 경로 가드 3중 방제, 무결성 검증, 감사 로그 해시 체인
docs/OPENCODE-API-NOTES.md 실측 기반 OpenCode 1.18.22 API 노트 — 플러그인 개발자라면 이것부터 읽을 것
docs/OPERATIONS.md 운영: Docker 네트워크, sshd Match 블록, bind mount 권한, 감사 로그, 트러블슈팅

1. 왜 이 플러그인이 필요한가

AI 에이전트에게 원격 파일 쓰기 권한을 주면 대개 다음 중 하나가 벌어집니다.

  • 에이전트가 프롬프트 인젝션으로 임의 코드를 올린다 → 웹셸
  • .htaccess 를 슬쩍 고쳐 실행 규칙을 바꾼다 → 코드 실행
  • 대량 삭제를 "정리"라는 명목으로 수행한다 → 데이터 손실
  • 무엇을 했는지 나중에 아무도 모른다 → 무법륜 상태

이 플러그인의 답은 코드(프롬프트가 아니라):

  • 위험 분류는 각 툴의 execute() 안에서 수행된다(훅이 아니다 → 위임 경로로 우회 불가)
  • 분류 결과에 따라 사람의 승인을 거치거나, 없으면 거부한다
  • 모호함·타임아웃·연결 단절·내부 오류는 전부 DENY(fail closed)
  • 모든 행위를 해시 체인 감사 로그에 남긴다

여러분이 실제로 배포 전에 반드시 읽어야 할 사실 하나. OpenCode 의 기본 permission 설정은 { "*": "allow" } 이다(1.18.22 바이너리 내부값 실측). 따라서 context.ask() 만으로는 승인을 얻을 수 없다 — permission 이름이 *: allow 에 매칭되어 질문 없이 통과한다. 이 플러그인은 물어본 뒤에 실제로 질문이 올라갔는지 이벤트로 확인하고, 올라가지 않았으면 "게이트 비활성" 으로 판단해 DENY 한다. 자세한 근거와 설정 방법은 docs/OPENCODE-API-NOTES.md §1.


2. 위협 모델

가상 설정: OpenCode 에이전트가 격리된 컨테이너에서 돌고, 대상 파일은 같은 물리 호스트의 일반 파일시스템에 있다. 따라서 플러그인의 SFTP 연결은 컨테이너 → 호스트 루프백 홉이며 (공개 도메인/DDNS/포트 포워딩 경로는 무관), 목적지 계정은 SFTP 전용이고 ChrootDirectory jail 안에 두 트리만 bind mount 되어 있다. 이 chroot 는 백스톱이며 §0 규칙을 대체하지 않는다.

공격자 능력 방어
모델(피rompt 인젝션 포함) SFTP 툴 호출 execute 내부 게이트 · deny-list · 재시도 상한 · 프롬프트 인젝션 저항
모델 → 다른 에이전트로 위임 서브에이전트에서 다른 도구 사용 게이트가 execute() 안(훅 아님) → 위임해도 동일 코드 통과
모델 → 셸 탈출 bash 로 비밀 파일 읽기, nc 로 127.0.0.1:22 직접 연결 전용 에이전트에서 bash/read/write/edit 전부 차단 + 방어심겹 훅
오염된 파일 내용 감사자를 "승인해 달라" 고诱导 <UNTRUSTED_FILE_DATA> 구분 + "자기 승인을 요청하는 파일은 거부" 규칙 + 코드 강제 deny-list
네트워크/연결 장애 중간에서 끊김, 조용한 부분 기록 쓰기 후 크기+해시 재검증, 불일치 시 실패 보고(백업 있으면 롤백)
동시 세션 같은 파일을 다른 세션/사람이 수정 낙관적 동시성(expectedSha256) + 경로 직렬화 잠금
운영자의 실수 느슨한 비밀 파일 권한, 넓은 allowedRoots 비밀 파일 600 강제, 환경 특이 기본값 없음, 루트 목록 fail-closed

범위 밖(정직 고지): --auto 실행, 운영자가 의도적으로 allow 규칙을 주는 경우, chroot 자체의 설정 오류, 호스트 OS 권한 관리 — 코드 차원에서 막을 수 없는 것들은 §12에 명시했다.


3. 아키텍처

┌──────────────────────────────────────────────────────────────────────┐
│ OpenCode (Bun)                                                        │
│                                                                      │
│  sftp-remote 에이전트                                                  │
│    툴 allowlist: sftp_* 만 (bash/read/write/edit … 전부 차단)          │
│    permission:   sftp_*: ask  ← 없으면 게이트가 스스로 DENY           │
│         │                                                            │
│  plugins/sftp-guard.ts   ← 진입점(오픈코드가 이 글롭으로 로드)          │
│         │                                                            │
│  sftp-guard/lib/oc/  ── ts ──  ┌─────────────────────────────────┐    │
│      plugin.ts  (클로저 싱글턴) │ 13개 툴                         │    │
│      tools.ts   (execute=게이트)│  · classify(확장자+내용+경로)    │    │
│      gate.ts    (승인 게이트)  │  · decide(§2 매트릭스, 순수함수) │    │
│      auditor.ts (감사자 호출)   │  · preview(§5 미리보기)          │    │
│         │                      │  · audit.append(감사 로그)       │    │
│         │                      └────────────┬────────────────────┘    │
│  sftp-guard/lib/core/ ── mjs ──  런타임 독립 코어                    │
│      redact / config / paths / scan / classify                       │
│      diff / policy / audit / remote(SftpTransport)                   │
└─────────┼────────────────────────────────────────────────────────────┘
          │  SFTP (127.0.0.1:22, 전용 계정, chroot jail)
┌─────────▼────────────────────────────────────────────────────────────┐
│ 호스트: sshd ─ Match User <sftp-user> (ChrootDirectory + ForceCommand) │
│          └─ jail ─ bind mount 2개 트리                                │
└──────────────────────────────────────────────────────────────────────┘

레이어 분리가 이 플러그인의 핵심 설계다.

  • lib/core/*.mjs — 런타임 독립. opencode 를 전혀 모른다. 그래서 Node 테스트 하네스로 실제 SFTP 통신까지 검증할 수 있고, Bun 에서도 같은 코드가 돈다. 순수 Node 빌트인(node:crypto/fs/path/os/module)만 쓴다.
  • lib/oc/*.ts — 오픈코드 결합부. 게이트·감사자·툴·훅.

플러그인 디렉터리 글롭 주의(중요): OpenCode 의 로더는 {plugin,plugins}/*.{ts,js} 로 한 단계만 훑는다. plugins/ 아래에 보조 모듈을 두면 그것도 각각 별도 플러그인으로 로드된다. 그래서 진입점 하나만 plugins/ 에 두고 구현은 sftp-guard/lib/ 아래에 둔다.


4. 설치

4.1 요구사항

  • OpenCode 1.18.22 이상 (Bun 런타임)
  • 호스트에 SFTP 전용 OS 계정 + ChrootDirectory jail (아래 §4.3)
  • Node 20+ (테스트 실행용 선택 — 런타임은 Bun 이다)

4.2 설치

git clone https://github.com/tak2-08/opencode-sftp-guard.git
cd opencode-sftp-guard
./install.sh                      # 기본 ~/.config/opencode
DRY_RUN=1 ./install.sh            # 무엇을 할지 미리보기

수동 설치라면 (구조가 .opencode 와 동일하므로 그대로 복사하면 된다):

C=~/.config/opencode
mkdir -p $C/{plugins,agents}
cp plugins/sftp-guard.ts   $C/plugins/
cp -R sftp-guard           $C/
cp agents/*.md             $C/agents/
# 의존성은 opencode 가 시작 시점에 설치한다(Bun). 수동 설치가 필요하면:
cd $C && bun install

4.3 호스트 쪽 준비(플러그인이 아니라 sshd 설정)

# /etc/ssh/sshd_config — ★ 포트 조건을 절대 넣지 않는다
Match User <sftp-user>
    ChrootDirectory /srv/sftp-jail
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
    PermitTunnel no

jail 안에는 접근할 트리만 bind mount 한다.

# 컨테이너 → 호스트 루프백이므로 포트 포워딩/DDNS 는 필요 없다.
mkdir -p /srv/sftp-jail/var/www /srv/sftp-jail/srv/remote-sandbox
mount --bind /var/www /srv/sftp-jail/var/www
mount --bind /srv/remote-sandbox /srv/sftp-jail/srv/remote-sandbox
  • AllowUsers/.firewall 로 이 계정은 127.0.0.1 에서만 오게 제한하는 것을 권한다.
  • 쓰기 권한은 계정 그룹 소속으로 얻는다. sftp_doctor({probeWrite:true}) 가 실제로 확인한다.

4.4 비밀 파일 (저장소 밖, 권한 600)

cp sftp-guard/sftp-secrets.example.json ~/.config/opencode/sftp-secrets.json
$EDITOR ~/.config/opencode/sftp-secrets.json     # host/port/username/password
chmod 600 ~/.config/opencode/sftp-secrets.json
  • 권한이 600 이 아니면 플러그인은 시작을 거부한다. (group/other 가 읽으면 비밀은 이미 유출된 것)
  • 환경변수로 비밀을 넘기는 것은 의도적으로 거부된다. 프로세스 환경은 모델의 bash 도구로 노출되므로(브리프 §0), 비밀은 파일로만 받는다. 비밀이 아닌 값(호스트/포트/계정)만 환경변수 허용.
  • 개인키 인증을 쓰면 개인키 파일도 600 이어야 한다.

4.5 정책 파일 (선택)

cp sftp-guard/sftp-guard.config.example.json ~/.config/opencode/sftp-guard.json

allowedRoots 는 반드시 실제 환경에 맞게 고쳐야 한다. 기본값은 예시값이며 실패-닫힘(fail closed)이라 잘못 두면 그 경로가 안 보일 뿐 동작은 안전하다. §10 설정 참조

4.6 실행 — 반드시 --auto 없이

opencode --agent sftp-remote

--auto 는 모든 권한 요청에 자동 승인한다. 이 플러그인은 이를 막을 수 없다(§12).

출처 표기 — 이 코드를 작성한 AI 작업 모델은 space-bunny-free (모델 ID opencode/space-bunny-free) 이고, 공개한 주체는 DSc (dsclub) 다. 모델이 한 일과 하지 않은 일, 책임 구분은 docs/AUTHORSHIP.md 에 적어 두었다.


5. 사용법

5.1 첫 세션: 상태 확인부터

sftp_doctor({ probeWrite: true, probeGate: true })
  • 연결 — 127.0.0.1:22 도달 여부, chroot 루트 실경로
  • 루트 — allowedRoots 각 경로가 jail 안에서 보이는지 + 쓰기 프로브(임시 파일 생성 후 삭제)
  • 게이트 — 승인 게이트가 실제로 살아 있는지(실제 질문이 뜨는지)

게이트가 비활성이면 여기서 DENY 되며 원인이 그대로 나온다. 그게 정상이다(설정 빠졌다는 뜻).

5.2 평상시 작업 순서

① sftp_propose_scope(basePath)            읽기 전용 구조 확인 (첫 1회 사람 승인)
② sftp_request_scope_approval([...])      구체적 경로 목록으로 범위 승인
③ sftp_read(...)                          실제 내용 확인
④ sftp_edit_existing(path, edits, expectedSha256)   최소 변경
⑤ 결과 보고 (무결성 검증 통과 여부 포함)

5.3 툴 목록

툴 하는 일 기본 게이트
sftp_list 디렉터리 1단계 목록 읽기 규칙
sftp_read 파일 읽기 (바이너리 → base64 일부, 텍스트 → UTF-8, 자격증명류 값 자동 마스킹) 읽기 규칙
sftp_stat 속성 + 경로 위험 분류 (내용은 읽지 않음) 읽기 규칙
sftp_write_new 새 파일 생성 (원자적 no-clobber) §2 매트릭스
sftp_edit_existing 수정 (전체 교체 또는 edits[{find,replace,all}]) §2 매트릭스
sftp_delete 파일 1개 삭제 (디렉터리는 거부) 항상 사람
sftp_move 이동 / 이름변경 항상 사람
sftp_mkdir 디렉터리 생성 범위 안이면 자동
sftp_chmod 권한 변경 (실행 비트 추가는 고위험) §1 실행비트 규칙
sftp_propose_scope 읽기 전용 트리 워크 → 범위 후보 첫 1회 사람
sftp_request_scope_approval 구체적 경로 범위 승인 항상 사람
sftp_set_mode 세션 모드 조회/변경 아래 5.4
sftp_doctor 연결·chroot·쓰기 권한·게이트 진단 probeWrite/probeGate 는 사람

어떤 툴의 인자 스키마에도 자격증명 필드가 없다(브리프 §0).

5.4 sftp_set_mode — 권한을 넓힐 수는 없다

인자 모델이 가능한가 이유
approveAll: false (끄기) ✅ 즉시 권한 감소
batchCap 낮추기 ✅ 즉시 권한 감소
auditorMode: true (켜기) ✅ 즉시 권한 감소
approveAll: true (켜기) ❌ 기본 거부 모델이 자기 권한을 넓히면 승인의 의미가 없어진다

브리프에는 approve-all 이 sftp_set_mode 토글로 되어 있으나, 이를 그대로 허용하면 에이전트가 스스로 승인 규칙을 느슨하게 만들 수 있다. 그래서 정책 allowModelApproveAll: true 로 켜면 게이트(sftp_mode 승인)를 거쳐서만 가능하고, 기본값은 거부다. 브리프 원문 동작이 필요하면 그 플래그를 켜면 된다.

5.5 자주 쓰는 패턴

// 최소 수정 (권장) — find 는 정확히 1곳이어야 한다(중복 시 all:true 요구)
sftp_edit_existing({ path: "/var/www/html/plugin/x.js",
                     edits: [{ find: "old(", replace: "new(" }],
                     expectedSha256: "<sftp_read 결과의 sha256>" })

// 바이너리는 base64 로 전체 제공(문자열 교체 불가)
sftp_write_new({ path: "/var/www/html/img/logo.png", contentBase64: "<...>" })

// 실행 비트는 위험 — 반드시 사람이 직접 본다
sftp_chmod({ path: "/var/www/html/tool.sh", mode: 493 })   // 0755

6. 승인 매트릭스

액션 기본값 비고
사용자 지정 기준 경로 안의 목록/읽기 (최초 탐색) 자동 허용 범위 협상이 성립하려면 열려 있어야 함
승인 범위 밖의 목록/읽기 질문 범위 확장 플로우로 이어짐
static 파일 생성/수정 — 범위 밖, 또는 approve-all 꺼짐 개별 질문
static 파일 생성/수정 — 승인 범위 안, 또는 approve-all 켬 자동 허용 + 로그 배치 상한 적용
고위험 파일 생성/수정 항상 승인 사람 직접, 또는 사람이 세션에서 켠 경우에만 감사자. 범위로 예외되지 않는다
파일 삭제 항상 파일별 질문 scope/approve-all/감사자 예외 없음
이동/이름변경 항시 파일별 질문 위와 동일
mkdir 범위 밖이면 질문, 안이면 자동
기존 고위험 파일의 수정 신규 생성과 동일 "파일 추가" 라는 표현이 면제를 주지 않는다

판정 로직은 sftp-guard/lib/core/policy.mjs 의 decide() 한 곳에 있고 순수 함수다 (테스트가 §2 매트릭스 전수 행을 표로 고정한다). 각 툴의 execute() 가 이를 직접 호출한다.

6.1 승인 요청에 붙는 내용(§5 UX)

execute() 안에서 순서를 지킨다: context.metadata({title}) → 판정 → context.ask(). 사람이 볼 질문에는 항상 내용이 붙는다(파일 이름만 묻는 일은 없다).

  • 쓰기/수정: +n -m, 이전/이후 sha256, 분류 근거, 치명적 시그니처 발췌, unified diff 본문
  • 삭제: 크기 + sha256 + 내용 앞 20줄
  • 이동: 출발/도착/크기/해시/덮어쓰기 여부
  • 범위: 경로 목록을 명시적으로 나열
  • chmod: 0644 → 0755, 실행 비트 추가 경고

타이머스웃 = DENY (기본 10분). 사람이 오프라인이어도 세션이 영원히 뜨지 않는다. 추적해 둔 permission id 로 서버에 reject 를 보내 대화 상자에 고아 요청을 남기지 않는다.

"Allow always" 는 무력화한다 — context.ask() 에 always: [] 를 넘긴다. 한 번의 "항상" 클릭으로 어떤 패턴도 영구 허용되지 않는다(사실상 "Allow once" 와 동일). 범위 확대는 오직 sftp_request_scope_approval 과 sftp_set_mode 경로로만 되고, 그 둘도 게이트를 거친다.

세션당 동시 승인 요청은 1개 — OpenCode 의 reject 는 같은 세션의 다른 대기 요청을 전부 실패시키므로, 플러그인이 세션별 뮤텍스로 직렬화한다.


7. 위험 분류 규칙

확장자 + 경로 + 내용 + chmod 를 함께 본다.

high — 실행 가능 확장자(.php .phtml .php3-8 .cgi .pl .py .sh .rb .jsp .asp .aspx .exe .jar .war 등), 이중 확장자 위장(shell.php.jpg), cgi-bin 경로, 의심 내용(eval( system( exec( shell_exec( passthru( proc_open( popen( base64_decode( gzinflate( str_rot13( …), 실행 비트 추가 chmod.

hard — 웹셸 하드 시그니처(언어와 무관한 <?php / ASP 태그 / $_GET[...] 를 코드 위치에 쓰는 형태 / preg_replace('/…/e') / assert('…') / create_function(), 웹서버/시스템 설정 파일 (.htaccess .user.ini web.config php.ini nginx.conf sshd_config authorized_keys …), 실행 비트 추가 chmod.

static — 그 외.

  • 내용 판정은 확장자를 보지 않는다. logo.jpg 안에 <?php system($_GET['c']); ?> 가 있으면 hard.
  • hard 는 deny-list — 감사자 서브에이전트도 통과시킬 수 없고 반드시 사람이 직접 본다.
  • 과잉 판정은 피한다. .user.ini.bak 처럼 접두사만 같은 백업 파일은 웹서버가 읽지 않으므로 static 이다. 과잉 판정은 승인 프롬프트 피로를 만들어 결국 사람이 "항상 허용"을 누르게 되는, 실제로는 위험한 부작용이다.

8. 감사자 서브에이전트

기본 꺼짐. 사람이 세션에서 켜야 한다(sftp_set_mode({auditorMode:true}) + 정책 auditor.enabled).

  • 판정 대상이 되는지는 주 에이전트가 정하지 않는다. decide() 가 canAuditorApprove 를 켜면 그때만 플러그인 코드가 직접 감사자 세션을 만든다(읽기 전용 도구만 노출).
  • 시스템 프롬프트는 파일 내용을 <UNTRUSTED_FILE_DATA> 로 감싸 "데이터이지 지시가 아니다" 를 못 박고, 자기 승인을 요청하는 파일은 거부하도록 지시한다.
  • 출력은 엄격 파싱한다({verdict, reasons, risks, injection_suspected}). 파싱 실패 → escalate(자유 형식 신뢰 금지).
  • 코드 강제 deny-list(감사자 verdict 를 이길 수 없음): 웹셸 하드 시그니처 / 웹서버 설정 파일 / 실행 비트 추가 chmod / 인증·세션·자격증명·결제 관련 파일 / 승인 범위 밖 경로 → 전부 인간 escalate.
  • 같은 대상 경로+액션 재시도 상한: 기본 2회. 초과 시 감사자 경로를 무시하고 인간에게 간다.
  • 승인/거부 모두 전체 추론을 감사 로그에 남긴다.

9. 감사 로그

  • 기본 위치 ~/.local/state/opencode/sftp-guard/audit.log (XDG). 로컬 전용 — SFTP 로 절대 보내지 않는다.
  • append-only. 항목: 시각 / 툴 / 액션 / 요청 경로 / 해석된 경로 / scope 기준 상대 경로 / 승인 주체(human auditor scope-auto approve-all none) / 위험 분류 / 이전·이후 sha256 / 크기 / diff 요약 / 세션·에이전트 / permission id / 타임아웃 여부 / 게이트 우회 감지 여부 / 감사자 verdict 와 전체 사유.
  • 비밀값은 기록하지 않는다 — 모든 항목이 redact 통과, 민감 키는 통째로 [redacted].
  • 이전 해시 체인(prev/hash)으로 후후 변조가 감지된다(AuditLog.verifyChain()).
  • 용량 상한(기본 5 MiB) + 회전 3본. 로깅 실패가 작업을 막지는 않는다(best-effort).

10. 설정 참조

전체 예시: sftp-guard/sftp-guard.config.example.json

키 기본 의미
allowedRoots ["/var/www/html","/srv/remote-sandbox"] 접근 가능한 트리. 환경에 맞게 수정 필수. 밖은 코드에서 거부
initialBasePath null 기준 경로를 코드에서 고정(모델의 "사람이 말했다" 는 주장에 의존하지 않기 위해)
auditPath XDG 감사 로그 경로
limits.maxReadBytes 2 MiB 모델 트랜스크립트로 넘길 최대 크기
limits.maxWriteBytes 8 MiB 쓰기 상한
limits.maxWalkDepth / maxWalkEntries 3 / 2000 propose_scope 탐색 한도
approvalTimeoutMs 600000 승인 타임아웃(초과 시 DENY)
requireLiveGate true 게이트 실측성 검사. 끄면 allow 규칙 우회를 막지 못함
allowModelApproveAll false 모델이 approve-all 을 켤 수 있게 할지
approveAllBatchCap 20 세션 자동 허용 예산
connectRetries 1 연결 실패 재시도
defaultFileMode 420 (0644) 신규 파일 모드
maxScanBytes 4 MiB 내용 시그니처 검사 상한
redactSecretLookingContentOnRead true 읽기 결과의 자격증명류 값 마스킹
denyHardContentWrites false true 면 hard 시그니처 쓰기를 outright DENY
auditor.enabled / .agent / .maxRetries / .timeoutMs / .deleteChildSession false / risk-auditor / 2 / 120000 / true 감사자
audit.enabled / .maxBytes / .keepRotations / .hashChain true / 5 MiB / 3 / true 감사 로그

환경변수: SFTP_GUARD_SECRETS_PATH, SFTP_GUARD_CONFIG_PATH, SFTP_GUARD_AUDIT_LOG, 그리고 비밀이 아닌 값만 SFTP_GUARD_HOST / SFTP_GUARD_PORT / SFTP_GUARD_USERNAME. 비밀(SFTP_GUARD_PASSWORD)은 의도적으로 거부된다.


11. 검증

# 테스트 (Node 20+, 오프라인)
cd sftp-guard && node --test test/
#   의존성 설치 후: 132 tests / 132 pass   (unit 99 + integration 22 + config 11)
#   의존성 없이:   111 tests / 111 pass   (통합 22개는 "의존성 미설치" 로 건너뜀 — 실패가 아님)

# 타입 점검
npx -y [email protected] tsc -p sftp-guard/tsconfig.json

통합 테스트는 ssh2 의 서버 구현으로 진짜 SFTP 서버를 in-process 에 띄우고(임시 호스트키, ephemeral 포트) 전송 계층을 상대로 실제 통신한다. 검증 항목:

  • 원자적 no-clobber(있으면 실패), 쓰기 후 크기+해시 검증, 전송 중단 시 성공으로 오인하지 않음
  • 낙관적 동시성 충돌 시 덮어쓰지 않음, 검증 실패 시 백업 롤백
  • 심볼릭 링크로 허용 루트 탈출 차단(디렉터리 링크 / 파일 링크 모두, 실제 파일시스템으로 검증)
  • 바이너리·비 UTF-8 왕_round 무손실, 권한 거부(EACCES) 명확 표면화
  • 삭제/이동(덮어쓰기 미지원 서버 안전 실패)/재귀 mkdir/chmod 실행비트 판정
  • 경로별 직렬화 잠금(진입/종료가 겹치지 않음), 닫은 뒤 재연결
  • 설정 fail-closed: 비밀 파일 없음 / 644 / JSON 오류 / 환경변수 비밀 / allowedRoots 공백

Bun 런타임 검증 절차는 docs/SECURITY-REVIEW.md §6 참조.


12. 이 플러그인이 막지 못하는 것

정직 고지 — 이 목록은 감추지 않는다.

  1. --auto 실행. TUI 가 모든 권한 요청에 자동 승인하고, 이벤트는 정상 발생한다. 플러그인은 이를 감지할 방법이 없다. → --auto 없이 쓰고, sftp_doctor({probeGate:true}) 로 확인.
  2. 운영자가 의도적으로 allow 규칙을 준 경우. requireLiveGate 가 DENY 로 막지만 원인은 운영자가 알아야 한다(오류 메시지에 필요한 설정 줄이 그대로 나온다).
  3. chroot jail 자체의 설정 오류. 백스톱일 뿐. Match User 에 포트 조건이 없는지가 중요하다 (같은 계정으로 다른 포트를 경유한 우회 방지).
  4. 모델이 읽은 사실 자체. SFTP 자격증명은 이 플러그인이 절대 주지 않는다. 애플리케이션 자격증명(DB 비밀 등)은 읽기 결과에 «masked» 로 가린다 — 그 원문을 알아낼 우회는 막지 않는다.
  5. 타 프로세스와의 완전한 배타성. in-process 잠장은 이 프로세스 안에서만 성립한다. 다른 사람이 SSH 로 같은 파일을 편집하는 경쟁은 expectedSha256 로 "덮어쓰지 않는다" 까지만 완화.
  6. 성능/대용량. 대용량(>10 MiB) 전송, 수백 개 동시 쓰기의 스케일은 검증 범위 밖.

13. 가정한 것 / 의도적 편차

브리프에 없어서 스스로 정하고, 되돌릴 수 있는 지점을 전부 밝힌다.

  1. 루프백 주소 기본값 127.0.0.1:22. host.docker.internal 이 필요한 배치(맥/리눅스, host-gateway)에서는 비밀 파일의 host 만 바꾸면 된다 — 코드 변경 불필요.
  2. 환경에 맞지 않는 기본값을 넣지 않았다. allowedRoots 기본값은 예시값이고 계정명 기본값은 비었다(비밀 파일에서 반드시 명시). "그냥 넣으면 돌아가는" 편이 인적 오류를 줄인다.
  3. 읽기는 고위험 파일도 허용. .php 읽기를 막으면 작업이 불가능하다. 대신 읽은 결과로 판단해 쓰려면 반드시 승인 게이트를 거친다.
  4. "배치" 정의 — 모델의 "배치 의도"를 볼 수 없다(툴이 하나씩 호출된다). 그래서 배치 상한은 "세션 누적 예산"으로 근사했다.
  5. approve-all 을 모델이 못 켠다(§5.4). 정책 플래그로 브리프 원문대로 되돌릴 수 있다.
  6. 하드 웹셸 시그니처 쓰기를 outright DENY 하지 않고 인간에게 escalate. 브리프 §4 가 "escalate" 를 명시했다. denyHardContentWrites: true 로 DENY 가능.
  7. 개인키 인증 지원 — 비밀번호가 주 경로지만 privateKeyPath(+passphrase) 도 지원(600 강제).
  8. 로그에 본문을 담지 않는다 — diff 요약(줄 수/해시)만. 본문 excerpt 는 한 줄 추가로 켤 수 있다.
  9. .env 계열 읽기 — 읽기는 허용하되 값이 «masked» 로 가려진다.

14. 코드 지도

파일 역할
sftp-guard/lib/core/redact.mjs 자격증명 격리. 등록/제거/스크럽(평문·base64·hex·URL 4형태 독립 제거), 민감 키 마스킹, 오류 안전화
sftp-guard/lib/core/config.mjs 비밀 파일(600 강제) + 정책 로드/검증, 환경변수 비밀 거부
sftp-guard/lib/core/paths.mjs 경로 가드 3중 방어(어휘론적 / 루트 경계 / realpath 해석)
sftp-guard/lib/core/scan.mjs 내용 시그니처(hard / suspect / 난독화)
sftp-guard/lib/core/classify.mjs 최종 판정 + deny-list
sftp-guard/lib/core/diff.mjs unified diff, 정확한 문자열 교체
sftp-guard/lib/core/policy.mjs §2 매트릭스 decide() + §5 미리보기 빌더
sftp-guard/lib/core/audit.mjs 감사 로그(로테이션·해시 체인·스크럽)
sftp-guard/lib/core/remote.mjs SFTP 전송 계층(무결성 검증·동시성·재연결·오류 표면화)
sftp-guard/lib/oc/gate.ts 승인 게이트(ask + 실측성 + 타임아웃 + 세션 직렬화 + 원격 reject)
sftp-guard/lib/oc/auditor.ts 감사자 호출 + 엄격 파싱 + 코드 강제 deny-list
sftp-guard/lib/oc/tools.ts 13개 툴. 각 execute() 가 게이트를 직접 호출
sftp-guard/lib/oc/plugin.ts 클로저 싱글턴 구성, 이벤트 관측, 방어심겹 훅, 초기화 실패 시 전 도구 비활성
plugins/sftp-guard.ts 진입점
agents/sftp-remote.md SFTP 작업 에이전트 (로컬 위험 도구 전부 차단 + permission 고정)
agents/risk-auditor.md 읽기 전용 감사자

15. 저장소 구조

opencode-sftp-guard/            ← opencode 설정 디렉터리를 그대로 미러링한다
├── README.md                    이 문서
├── install.sh                   설치 스크립트 (DRY_RUN 지원, package.json 은 의존성만 병합)
├── package.json                 의존성 1개 고정 + test/typecheck 스크립트
├── docs/                        상세 문서 5종 (출처·보안·구현·API·운영)
├── plugins/sftp-guard.ts        오펜코드가 로드하는 진입점 (여기 하나만 둔다)
├── agents/                      sftp-remote.md · risk-auditor.md
└── sftp-guard/
    ├── lib/core/*.mjs           런타임 독립 코어 (Node/Bun 공통)
    ├── lib/oc/*.ts              오픈코드 결합부
    ├── test/                    132개 테스트 + in-process SFTP 서버 헬퍼
    ├── tsconfig.json            타입 점검용(실행 시 의존성 아님)
    ├── sftp-secrets.example.json
    └── sftp-guard.config.example.json

출처 · 게시

  • 작업 모델 (모델명): space-bunny-free — 모델 ID opencode/space-bunny-free. 이 저장소의 구현·테스트·문서 작성·게시 준비를 수행한 AI 작업 모델이다. 아래 코드는 전부 이 모델이 작성했다.
  • 작성 및 게시: DSc (dsclub) — DSc GitHub 계정(tak2-08)을 통해 공개 저장소에 게시.
  • 책임 구분: 설계·코드·문서 초안 = AI 작업 모델(space-bunny-free)이 작성, 공개 결정·게시·유지 책임 = DSc (dsclub) 계정 주체. 모델명과 게시 주체는 서로 대체되지 않는다. 자세한 내용은 docs/AUTHORSHIP.md.
  • 대상 위협 프로파일은 PHP + JS 웹 애플리케이션(예: 그누보드5 기반 웹 에디터)이며, 웹셸 · .htaccess 남용 · 실행 비트 부여가 주요 위험이다.
  • 이 저장소에는 자격증명·계정명·호스트 식별자·내부 경로가 포함되어 있지 않다. 소스에 계정명 기본값이 없고, 경로 기본값은 예시값이며, 비밀은 저장소 밖 ~/.config/opencode/sftp-secrets.json(600)에서만 읽는다.

About

Credential-isolated, human-gated SFTP tools for OpenCode agents — 위험 분류와 사람 승인을 코드로 강제 (작업 모델: space-bunny-free / 게시: DSc dsclub)

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages