SSH 제한 계정 권한 사고, 뿌리를 따라가 보니 (연재 6/6)
오늘은 두 가지 작업을 했는데, 하나는 프로젝트 목록에 개인/팀 구분을 추가하는 프론트/백엔드 작업이었고 다른 하나는 팀원이 계속 겪는다는 SSH 접속 문제를 추적하는 것이었습니다. 특히 후자를 파고들면서 SSH 강제 명령(forced command)과 git 파일 권한에 대해 생각보다 많이 배웠습니다.
학습 주제
- 웹사이트 프로젝트 데이터에
project_type(개인/팀) 필드를 추가하고 프론트/백엔드를 동기화하는 작업 - 제한된 SSH 계정(
dbclient)에서 반복적으로 발생한 접속 문제의 원인 추적 - 학습 날짜: 2026-07-24
탐구 과정
첫 번째 작업은 비교적 단순했습니다. 기존 프로젝트 테이블에 project_type 컬럼을 백필하고, 서비스/컨트롤러 계층에 필드를 노출하고, 프론트엔드 목록 페이지를 개인/팀 두 섹션으로 나눠 렌더링하도록 고쳤습니다. 카드 렌더링 로직을 헬퍼 함수로 빼고, 추가/수정 모달과 상세 페이지 편집 모달 양쪽에 선택 UI를 넣은 다음 빌드하고 화면으로 직접 확인했습니다. 이 부분은 손에 익은 CRUD 작업이라 크게 막힌 곳은 없었습니다.
진짜 흥미로웠던 건 두 번째 작업이었습니다. 팀원이 "같은 문제가 계속 반복된다"고 했는데, 저는 처음에 이게 문서화 부족 문제일 거라 짐작했습니다. 그래서 레포 커밋 이력을 뒤져서 그 팀원이 작성한 SSH/DB 관련 스킬이나 문서가 있는지 찾아봤는데 — 없었습니다. 대신 발견한 건 infra/db-access.md라는 온보딩 문서였고, 절차 자체는 꽤 명확했습니다. GitHub에 등록된 SSH 키가 있으면 https://github.com/{아이디}.keys로 그대로 재활용하고, 없으면 새로 만들어서 공개키만 전달하는 방식이었습니다.
그런데 문서를 자세히 보니 "현재 등록 상태" 기록이 한참 전 날짜 그대로 멈춰 있었습니다. 문서 자체는 문제가 없어 보이는데 뭔가 이상했습니다. 여기서 계정 이름이 dbclient라는 걸 힌트 삼아 pm/docs/learnings.md를 뒤져봤더니, 이게 진짜였습니다 — 같은 계정을 둘러싼 권한 사고가 이번이 처음이 아니라 최소 여섯 번째였습니다.
핵심 학습 내용
SSH 강제 명령(forced command)의 허점
제한된 계정을 만들 때 authorized_keys에 command= 옵션을 걸어서 로그인해도 특정 스크립트만 실행되게 강제하는 방식을 씁니다. 그런데 그 스크립트가 sqlite3를 감싸는 형태였는데, sqlite3 자체에 .shell이라는 dot-command가 있어서 그걸 통해 셸을 탈출할 수 있었다는 기록이 있었습니다. 셸 명령만 막는 걸로는 부족하고, 실행되는 프로그램 자체가 내부적으로 셸을 열 수 있는 기능을 갖고 있는지까지 확인해야 한다는 걸 처음 알았습니다.
git이 실행 권한(mode bit)을 추적한다는 것
가장 인상 깊었던 건 이 부분이었습니다. 가드 스크립트(dbclient-sqlite-guard.sh)가 git에 644(실행 불가) 모드로 커밋되어 있었는데, 이 상태에서는 로컬에서 chmod +x로 아무리 고쳐놔도 배포 파이프라인이 브랜치를 다시 체크아웃할 때마다 git이 기억하는 모드(644)로 조용히 되돌아갑니다.
git ls-tree HEAD -- infra/dbclient-sqlite-guard.sh # git이 기억하는 모드
ls -la infra/dbclient-sqlite-guard.sh # 서버 실제 파일 모드
이 둘을 비교해서 100755인지 확인하는 게 진단 방법이었습니다. 그리고 이 버그가 오래 숨어있던 이유는, infra 폴더 자체는 CD 트리거 대상이 아니었는데도 백엔드나 공용 코드 PR이 머지될 때마다 배포 과정에서 브랜치 전체가 재체크아웃되면서 실행권한이 같이 벗겨졌기 때문입니다. 즉 원인이 되는 변경과 증상이 나타나는 위치가 완전히 달라서 추적이 어려웠던 겁니다.
git update-index --chmod=+x infra/dbclient-sqlite-guard.sh
이렇게 git 트리에 직접 실행권한을 커밋해야 근본적으로 해결된다는 걸 배웠습니다.
그 외 발견한 사고 유형들
기록을 보니 같은 계정을 둘러싸고 이런 문제들이 있었습니다.
- 계정 이름이
dbviewer였는데 실제로는 쓰기(DML)까지 가능해서 명칭을dbclient로 바꾼 사례 - 문서엔 "ALTER/CREATE/DROP 금지"라고 써놨지만 실제 스크립트는 그걸 강제하지 않고 있었던 정책-기술 간 괴리
- 세미콜론으로 SQL을 이어붙이면 필터를 우회할 수 있었던 문제
- 같은 공개키를 stage용/prod용으로
authorized_keys에 두 줄 등록해도 첫 줄만 적용되고 나머지는 무시되는 것
이해한 내용
이번에 확실히 이해한 건, 문서화와 실제 강제(enforcement)는 별개라는 점입니다. 문서에 "금지"라고 적어놓는 것과 스크립트가 실제로 그걸 막는 것 사이에는 간극이 있고, 이 간극이 사고의 대부분을 만든다는 걸 알게 됐습니다. 또 하나는 권한 문제의 원인이 항상 접속하는 사람 쪽에 있는 게 아니라는 것입니다. 이번 건도 문서 미비도, 팀원 개인의 설정 실수도 아니라 배포 파이프라인이 파일 권한을 조용히 되돌리는 구조적 버그였습니다. "같은 문제가 반복된다"는 증상만 보고 사람 쪽 원인을 먼저 의심했던 제 접근이 성급했다는 것도 느꼈습니다.
실전 적용
제한된 SSH 계정을 만들 때 체크할 목록을 정리해봤습니다.
- forced command로 감싼 프로그램이 내부적으로 셸 탈출 기능을 갖고 있는지 확인
- 실행 스크립트를 git에 올릴 때 실행권한(755)이 실제로 커밋됐는지 확인 (
git ls-tree로 검증) - CD가 특정 폴더만 트리거하도록 설계했더라도, 다른 변경으로 인한 재배포 시 전체 재체크아웃이 파일 상태를 되돌릴 수 있다는 걸 감안
authorized_keys에 여러 줄로 같은 키를 등록하는 방식에 의존하지 않기- 문서에 적어놓은 정책이 실제 스크립트/설정으로 강제되는지 별도로 검증
추가 학습 계획
authorized_keys의 다른 제한 옵션들(restrict,no-port-forwarding,no-X11-forwarding등)을 더 정리해보고 싶습니다.- git 파일 모드 관리를 pre-commit 훅이나 CI 검사로 자동화하는 방법을 찾아볼 계획입니다.
- CD 파이프라인이 변경 범위를 벗어난 파일까지 건드리지 않도록 설계하는 패턴을 더 공부해보려 합니다.