운영 기록을 읽으려던 명령이 실패하면 먼저 원격 서버 상태를 의심하기 쉽습니다. 세 번의 실패를 다시 보니 Git에 인수를 넘기기 전, SSH에 인수를 넘기는 순간, SSH가 원격 명령을 시작하기 전이라는 서로 다른 위치에서 막혔습니다.

zsh가 만든 인수 확인

첫 사례에서는 변수에 든 Git ref 뒤에 콜론과 경로를 바로 붙였습니다. zsh에서 뒤 문자열이 :a로 시작하면 변수 확장의 경로 수정자로 읽힐 수 있습니다. git show "${ref}:archive/report.txt"처럼 중괄호로 변수 이름을 닫으면 콜론은 Git이 받는 문자열에 그대로 남습니다. zsh 확장 문서

두 번째 사례에서는 SSH 옵션 여러 개를 문자열 하나에 보관했습니다. 기본 zsh에서는 일반 스칼라 변수를 확장한다고 공백마다 인수가 나뉘지 않습니다. 각각의 인수가 하나의 배열 원소가 되게 만들고 "${ssh_args[@]}"로 확장하면 경계가 보입니다.

ssh_args=(-o BatchMode=yes -o ConnectTimeout=5)
ssh "${ssh_args[@]}" host 'uptime'

SSH 포워딩 실패 추적

세 번째 사례에서는 SSH 별칭이 설정 파일의 로컬 포트 포워딩을 자동으로 열었습니다. 포트가 이미 쓰이는 상태에서 ExitOnForwardFailure가 적용돼 접속이 끝났고, 원격 명령은 실행되지 않았습니다. 포워딩을 쓸 이유가 없던 조회는 ssh -o ClearAllForwardings=yes host 'uptime'처럼 접속할 때만 설정을 비우는 방식으로 분리했습니다. 포워딩이 필요한 호출에는 기존 별칭 설정을 사용합니다. OpenSSH 설정 문서

LocalForward 설정의 앞쪽 주소와 포트는 SSH 클라이언트가 실행되는 쪽의 입구입니다. 뒤쪽 주소와 포트는 원격 SSH 서버가 새로 접속할 목적지입니다. 양쪽에 같은 루프백 주소를 써도 각 연결이 시작되는 쪽의 로컬 서비스를 가리킵니다. 이번 충돌은 앞쪽 수신 포트를 열 때 일어났으므로 목적지 서비스가 열려 있어도 원격 명령까지 진행할 수 없었습니다.

접속 없이 재현한 출력

로컬에서는 가상의 ref와 호스트 이름을 쓰고, SSH는 접속 없이 적용 설정을 출력하는 ssh -G로 확인했습니다. 실제 서버의 주소나 설정은 사용하지 않았습니다.

확인한 단계격리 재현에서 확인한 출력
zsh의 ref 확장"$ref:archive/report.txt"는 현재 폴더로 시작하고 콜론이 없는 경로가 됐습니다. "${ref}:archive/report.txt"의 출력은 main:archive/report.txt였습니다.
SSH의 옵션 해석문자열은 인수 1개로 넘어가 keyword batchmode extra arguments at end of line과 종료 코드 255를 반환했습니다. 배열은 인수 4개로 넘어가 batchmode yes와 connecttimeout 5를 출력했습니다.
포워딩 설정 적용같은 가상 설정에서 ClearAllForwardings=yes를 주자 ssh -G 출력에서 localforward 줄이 사라졌습니다.

앞의 두 행은 인수 문제를 재현한 값입니다. 마지막 행에서 확인한 것은 포워딩 설정 제거까지이며, 당시 포트 충돌과 원격 명령의 미실행은 원래 기록에 근거합니다. 실제 접속이 시작된 뒤 어느 네트워크 구간에서 막혔는지는 컨테이너에서만 타임아웃 난 MySQL SSH 터널 추적기에서 다룹니다.