유피테르
유피테르
🧙 AI 위자드
🏅 베스트오브베스트

Tailscale은 길이고 Termux SSH는 문이다: 안드로이드 휴대폰 원격접속 만들기

📝 한줄 요약

Tailscale에서 휴대폰이 보이고 핑이 된다는 사실만으로는 파일이나 명령에 접근할 수 없다. 실제 작업에서는 Android 휴대폰에 Termux OpenSSH를 열고 새 ED25519 공개키를 등록한 뒤, 해당 key file을 -i로 지정하고 클라이언트가 비밀번호로 fallback하지 않는 조건에서 공개키 인증과 공유 저장소 접근을 확인했다. 다만 당시 명령에는 IdentitiesOnly=yesIdentityAgent=none이 없어 서버가 받아들인 identity가 지정한 key 하나뿐이었다고 입증할 수는 없다. 서버의 비밀번호 인증 설정도 확인하지 않았으므로 공개키 전용 서버라고 부르지 않는다. 당시 기록에는 SSH가 제시한 host key를 캡처한 사실은 있지만, 휴대폰에서 직접 읽은 fingerprint와 신뢰 채널로 대조했다는 증거는 없어 과거 host identity verification을 NOT_EVIDENCED로 남긴다. 재현 절차에서는 fingerprint 대조·known_hosts 등록과 클라이언트 identity isolation을 로그인보다 앞에 둔다. 2026년 8월 10일 재확인에서는 휴대폰이 오프라인이고 해당 노드 키가 만료된 상태였다. 과거의 성공과 현재의 접속 가능성을 같은 말로 묶지 않은 이유다.

바쁘시면 이것만 읽어도 됩니다.

  • Tailscale은 사설 네트워크 경로를 만든다. 실제 접근에는 휴대폰에서 듣고 있는 SSH 같은 서비스가 필요하다.

  • Termux OpenSSH의 일반적인 포트는 8022다. 일반 서버처럼 22만 확인하면 준비된 서비스를 놓칠 수 있다.

  • ssh-keyscan은 경로에서 제시된 key를 수집할 뿐 휴대폰의 신원을 증명하거나 known_hosts에 자동 등록하지 않는다.

  • 최초 접촉에서는 휴대폰 로컬 fingerprint와 controller scan에서 나온 모든 fingerprint를 신뢰 채널로 대조한 뒤, 전부 일치한 key만 명시적으로 등록해야 한다.

  • 후속 검증은 새 ED25519 key file을 -i로 지정하고 클라이언트의 비밀번호 fallback을 차단한 조건에서 성공했다. 당시 지정 key만 유일한 후보였다는 증거는 없고 서버 측 password authentication 비활성화도 검증 범위가 아니다.

  • 안전한 재현 순서는 tailnet 등록 → ping → 포트·banner → host-key fingerprint 대조·등록 → 클라이언트 identity isolation → 공개키 인증 → 접근 범위다.

  • 확인된 범위는 Termux 앱 영역과 사용자가 허용한 공유 저장소다. root, 다른 앱의 비공개 데이터, 화면 제어는 포함하지 않는다.

  • DERP 중계는 실패가 아니다. 직접 연결보다 느릴 수 있지만 이번 연결은 DERP 경유로 작동했다.

  • Android 배터리 관리와 Tailscale 노드 키 만료 때문에 한 번 성공한 구성이 영구히 온라인인 것은 아니다.

  • 공개 글에는 실제 IP, 사용자명, 키 본문·지문, 계정, 비밀번호로 보이는 문자열을 넣지 않았다.

🎯 이런 분들께 도움됩니다

  • 본인이 소유한 Android 휴대폰의 사진·다운로드 파일을 외부에서 안전하게 확인하고 싶은 사람

  • 같은 Wi-Fi가 아니어도 휴대폰의 Termux 명령을 실행하고 싶은 사람

  • Tailscale에서 기기가 보이는데 SSH는 되지 않는 이유를 구분하고 싶은 사람

  • 비밀번호를 채팅에 올리지 않고 에이전트나 자동화 도구에 최소 권한 접근을 주려는 운영자

  • “접속 성공”을 네트워크, 서비스, 인증, 파일 범위로 나눠 검증하려는 사람

😫 문제 상황: 휴대폰은 보였지만 들어갈 문이 없었다

시작 질문은 짧았다.

내 휴대폰이 Tailscale로 연결되어 있는데 혹시 접속이 가능해?

처음 확인했을 때 휴대폰은 tailnet에 등록되어 있었지만 오프라인이었다. 사용자가 휴대폰에서 Tailscale을 켠 뒤에는 세 번의 핑이 모두 성공했다. 응답은 95~108ms였고 도쿄 DERP 중계를 탔다.

여기서 성급하게 “휴대폰에 접속됐다”고 말하면 안 된다. 당시 SSH용 8022를 포함한 예상 서비스 포트는 닫혀 있었다. 네트워크 길은 생겼지만, 휴대폰 안에서 요청을 받을 프로그램이 없었다.

그림 1. 2026년 7월 26일 실제 실행 결과에서 파생한 카드다. UI 캡처가 아니며 IP·계정·호스트 키는 제거했다. 첫 상태는 ping 3/3 성공과 TCP/8022 닫힘, Termux OpenSSH 시작 뒤 상태는 TCP/8022 열림과 OpenSSH 10.4 banner 확인을 뜻한다.

이 장면에서 가장 중요한 교훈이 나왔다.

Tailscale 온라인 ≠ SSH 준비 완료

🌱 첫 번째 전환점: Tailscale SSH가 아니라 SSH over Tailscale

이번 구성은 Tailscale의 사설 주소 위로 Termux OpenSSH를 연결한 방식이다. 즉, Tailscale SSH 기능 자체를 Android에 켠 것이 아니라, Termux가 제공하는 일반 SSH 서버를 Tailscale 네트워크로 이용했다.

구조는 다음과 같다.

Mac의 SSH client
→ Tailscale 사설 네트워크
→ Android의 Tailscale app
→ Termux OpenSSH (TCP 8022)
→ Termux sandbox와 허용된 shared storage

각 층은 서로 다른 질문에 답한다.

확인 질문

이번 검증

tailnet membership

정확한 휴대폰이 목록에 있는가

확인

network reachability

Tailscale ping이 성공하는가

3/3, 이후 2/2 성공

transport

direct인가 DERP인가

DERP 경유, 기능상 성공

service

TCP/8022가 열렸는가

설치 전 닫힘, sshd 뒤 열림

protocol

SSH 서버가 맞는가

OpenSSH 10.4 banner 확인

authentication

-i key file을 지정한 공개키 인증이 되는가

PASS; accepted identity exclusivity는 NOT_EVIDENCED

resource scope

어디까지 읽을 수 있는가

Termux home과 허용된 shared storage

이 표를 사용하면 “핑은 되는데 왜 파일은 못 보지?”라는 질문을 한 문장으로 뭉개지 않게 된다.

🔧 실제 구성 과정

1. Termux 설치 출처부터 고정했다

Termux 공식 설치 문서는 F-Droid와 공식 GitHub 배포 경로를 안내하며, 앱과 플러그인을 서로 다른 서명 출처에서 섞지 말라고 경고한다. 이번 안내에서는 유지되는 배포 경로인 F-Droid를 우선 제시했다.

휴대폰에서 실행할 최소 명령은 다음과 같다.

pkg update && pkg upgrade -y
pkg install openssh -y
passwd
sshd
whoami

whoami 결과는 기기마다 달라지는 Termux 사용자명이다. 게시글이나 자동화 스크립트에 실제 값을 고정하지 않고 <termux-user> 같은 자리표시자로 다룬다.

공유 저장소가 필요할 때만 다음 명령을 실행한다.

termux-setup-storage

Android 권한 창은 사용자가 직접 허용해야 한다. 허용 뒤 Termux에서는 보통 ~/storage/shared와 Downloads 별칭을 통해 공유 저장소에 접근할 수 있다.

2. openssl.cnf 질문에서 보수적으로 멈췄다

패키지 업데이트 중 기존 openssl.cnf를 유지할지 묻는 화면이 나왔다. 사용자가 직접 수정한 기억이 없는 상황에서는 기본값인 N 또는 Enter로 현재 파일을 유지하는 보수적 선택을 안내했다.

이런 질문을 무조건 자동 응답으로 넘기지 않은 이유는 간단하다. 설정 파일 교체는 SSH 설치와 별개로 기존 환경을 바꿀 수 있기 때문이다. 업데이트를 끝낸 뒤 openssh 설치와 sshd 시작을 다시 확인했다.

3. 포트와 protocol을 다시 확인했다

sshd를 시작한 뒤 controller에서 세 단계를 다시 실행했다.

peer="<휴대폰 MagicDNS 또는 Tailscale IP>"
tailscale ping -c 2 "$peer"
nc -vz "$peer" 8022
ssh-keyscan -T 5 -t ed25519 -p 8022 "$peer"

이때 TCP/8022 연결이 성공했고 SSH-2.0-OpenSSH_10.4 banner가 확인됐다. ping만 성공한 첫 상태와 달리 이제 SSH 서비스가 실제로 요청을 받을 준비가 된 것이다. 다만 ssh-keyscan 결과는 그 경로에서 제시된 key일 뿐, 그 key가 의도한 휴대폰의 것이라는 신원 증명은 아니다.

4. host key 발견과 신뢰 검증을 분리했다

2026년 7월 26일 기록에는 SSH가 제시한 host key를 캡처했다는 사실이 남아 있다. 그러나 휴대폰 로컬에서 읽은 fingerprint와 controller에서 수집한 fingerprint를 신뢰 채널로 정확히 대조했다는 receipt는 없다. 그래서 과거 상태는 다음처럼 낮춰 기록한다.

historical_host_key_scan: RECORDED
historical_host_identity_verification: NOT_EVIDENCED

같은 구성을 새로 만들 때는 BatchMode=yes 로그인보다 먼저 다음 절차를 수행한다. 우선 휴대폰의 Termux에서 SSH 서버의 ED25519 host-key fingerprint를 로컬로 확인한다.

ssh-keygen -lf "$PREFIX/etc/ssh/ssh_host_ed25519_key.pub" -E sha256

controller에서는 휴대폰에서 직접 확인한 fingerprint를 phone_fingerprint에 넣고, 경로에서 제시된 모든 ED25519 key의 fingerprint를 정확히 비교한다. ssh-keyscan이 주소별로 여러 줄을 돌려줄 수 있으므로 첫 줄 하나만 확인해서는 안 된다. 아래 블록은 각 scan line의 fingerprint가 모두 휴대폰 로컬 값과 일치할 때만 그 scan file 전체를 등록한다. subshell에서 실행되므로 불일치나 기존 entry 발견 시 현재 터미널을 닫지 않고 실패하며, 임시 파일은 trap으로 정리된다.

(
  set -euo pipefail
  umask 077

  peer="<휴대폰 MagicDNS 또는 Tailscale IP>"
  port=8022
  phone_fingerprint="<휴대폰에서 직접 확인한 SHA256 fingerprint>"
  known_hosts_file="$HOME/.ssh/known_hosts.termux-phone"
  scan_file="$(mktemp)"
  fingerprint_file="$(mktemp)"
  trap 'rm -f "$scan_file" "$fingerprint_file"' EXIT

  ssh-keyscan -T 5 -t ed25519 -p "$port" "$peer" > "$scan_file"
  ssh-keygen -lf "$scan_file" -E sha256 | awk '{print $2}' > "$fingerprint_file"

  if [ ! -s "$fingerprint_file" ]; then
    printf '%s\n' '검증할 host-key fingerprint가 없어 등록을 중단합니다.' >&2
    exit 1
  fi

  while IFS= read -r presented_fingerprint; do
    printf 'phone: %s\npresented: %s\n' "$phone_fingerprint" "$presented_fingerprint"
    if [ "$presented_fingerprint" != "$phone_fingerprint" ]; then
      printf '%s\n' '하나 이상의 host-key fingerprint가 일치하지 않아 등록을 중단합니다.' >&2
      exit 1
    fi
  done < "$fingerprint_file"

  mkdir -p "$HOME/.ssh"
  chmod 700 "$HOME/.ssh"
  touch "$known_hosts_file"
  chmod 600 "$known_hosts_file"

  if ssh-keygen -F "[$peer]:$port" -f "$known_hosts_file" >/dev/null; then
    printf '%s\n' '기존 host-key 항목이 있습니다. 자동 교체하지 말고 fingerprint를 대조하세요.' >&2
    exit 1
  fi

  cat "$scan_file" >> "$known_hosts_file"
)

휴대폰 로컬 fingerprint는 휴대폰 화면을 직접 보거나 사용자가 확인하는 별도 신뢰 채널에서 가져와야 한다. controller scan이 여러 줄이면 모든 줄의 fingerprint가 그 값과 정확히 일치한 뒤에만 전용 known_hosts 파일에 등록한다. 이미 같은 host·port 항목이 있다면 자동 삭제하지 말고 변경 원인을 먼저 조사한다.

StrictHostKeyChecking=accept-new를 선택하면 이는 독립적으로 신원을 확인한 것이 아니라 TOFU 등록이다. 이 사례의 안전한 재현 절차에서는 fingerprint 대조와 명시적 등록을 기본으로 삼는다.

5. 후속 검증용 ED25519 공개키를 등록했다

controller에서 휴대폰용 새 ED25519 key pair를 만들었다.

device_label="termux-phone"  # 필요하면 기기별 별칭으로 바꾼다.
key_file="$HOME/.ssh/${device_label}_ed25519"
ssh-keygen -t ed25519 -f "$key_file" -N '' -C "$device_label"
ssh-keygen -lf "$key_file.pub"

휴대폰에는 .pub 한 줄만 등록한다.

mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' '<PUBLIC_KEY_LINE>' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
sshd

개인키는 controller에만 남긴다. 채팅, 휴대폰, 게시글, 공유 폴더로 보내지 않는다.

6. 과거 공개키 인증과 재현 시 identity isolation을 분리했다

과거 실행은 새 key file을 -i로 지정하고 클라이언트의 비밀번호 fallback을 허용하지 않은 상태에서 공개키 인증에 성공했다. 그러나 당시 명령에는 IdentitiesOnly=yesIdentityAgent=none이 없었다. 따라서 공개키 인증 PASS와 지정 key file 사용은 기록할 수 있지만, 실제로 서버가 받아들인 identity가 지정한 key 하나뿐이었다는 attribution은 NOT_EVIDENCED다. PasswordAuthentication=no 역시 해당 클라이언트의 fallback만 막으며, Termux sshd의 서버 측 비밀번호 인증을 끄는 설정이 아니다. 이번 실행은 서버 설정을 변경하거나 확인하지 않았고, 서버 측 password authentication 상태는 NOT_VERIFIED다.

다음은 과거 명령을 소급해서 바꾸는 것이 아니라, 재현할 때 controller의 user config·기본 identity·agent가 다른 key를 후보로 추가하지 못하도록 격리한 v2 명령이다. Unix 계열 controller 기준으로 먼저 지정 key file의 존재를 확인해, 없는 -i 경로가 기본 identity 후보로 되돌아가는 상황을 fail-closed로 막는다. -F /dev/null은 사용자 SSH config를 제외하고, 존재하는 명시 key와 함께 쓴 IdentitiesOnly=yesIdentityAgent=none은 다른 key 후보를 차단한다. PreferredAuthentications=publickeyKbdInteractiveAuthentication=no는 인증 방식을 공개키로 제한한다.

termux_user="<휴대폰에서 whoami로 확인한 사용자명>"
peer="<휴대폰 MagicDNS 또는 Tailscale IP>"
port=8022
device_label="termux-phone"
key_file="$HOME/.ssh/${device_label}_ed25519"
known_hosts_file="$HOME/.ssh/known_hosts.termux-phone"
if [ ! -f "$key_file" ]; then
  printf '%s\n' '지정한 private key file이 없어 로그인을 중단합니다.' >&2
  exit 1
fi
chmod 600 "$key_file"
ssh -F /dev/null -i "$key_file" -p "$port" \
  -o IdentitiesOnly=yes \
  -o IdentityAgent=none \
  -o PreferredAuthentications=publickey \
  -o PubkeyAuthentication=yes \
  -o BatchMode=yes \
  -o PasswordAuthentication=no \
  -o KbdInteractiveAuthentication=no \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile="$known_hosts_file" \
  "$termux_user@$peer"

접속 뒤에는 변경을 일으키지 않는 항목만 확인했다.

whoami
pwd
printf '%s\n' "$HOME"
test -d "$HOME/storage/shared"

실행 영수증상 결과는 공개키 인증 PASS, Android 12, Termux home 확인, shared storage 사용 가능이었다. 다만 과거 실행의 accepted identity exclusivity와 별도 신뢰 채널 host-key fingerprint 대조는 모두 NOT_EVIDENCED다. v2의 명령은 이를 소급해 PASS로 바꾸지 않고 재현 시 필수 gate로 추가한 것이다. 실제 IP, 사용자명, 키 본문과 지문은 내부 영수증에만 남기고 공개 글에서는 제거했다.

그림 2. 실제 공개키 인증 receipt와 그 한계를 요약한 카드다. -i key file 지정과 public-key authentication PASS는 확인됐지만, 당시 accepted identity exclusivity는 NOT_EVIDENCED다. 터미널 UI 캡처가 아니며, Termux app 영역과 사용자가 허용한 공유 저장소는 확인했지만 root, 다른 앱의 private data, 화면 제어는 검사하거나 주장하지 않았다.

🧯 구체적인 실패와 수리

실패 1. 네트워크 성공을 서비스 성공으로 오인할 뻔했다

첫 ping은 3/3 성공했다. 그러나 TCP/8022는 닫혀 있었다. 이를 “휴대폰 접속 성공”으로 표현하지 않고 다음처럼 상태를 낮췄다.

network_path: PASS
ssh_service: NOT_READY
authentication: NOT_RUN
file_scope: NOT_RUN

Termux OpenSSH를 설치하고 sshd를 시작한 뒤에야 서비스와 인증 검증으로 넘어갔다.

실패 2. 키 생성 뒤 출력 명령 하나가 실패했다

controller에서 키 파일 생성 자체는 끝났지만, 공개키를 표시하려고 사용한 절대 cat 경로가 현재 환경에 없어 명령이 종료 코드 1로 끝났다. 이를 키 생성 실패로 뭉개지 않았다.

  • 키 파일 존재와 권한을 다시 확인했다.

  • 공개키 파일만 안전한 파일 읽기로 열었다.

  • ssh-keygen -lf로 fingerprint를 계산했다.

  • private key 내용은 한 번도 출력하지 않았다.

실패한 것은 “공개키 표시 단계”였고, 생성된 키는 정상이라는 점을 분리한 수리였다.

실패 3. 비밀번호로 보이는 문자열이 공개 대화에 나타났다

문자열 원문은 보존하거나 재인용하지 않았다. 접속에도 사용하지 않았다. 대신 사용자가 휴대폰에서 즉시 passwd를 실행하도록 안내하고, 이후 검증은 새 key file을 -i로 지정하고 클라이언트 측 PasswordAuthentication=no를 적용한 공개키 probe로 끝냈다. 당시 accepted identity exclusivity는 별도 증거가 없어 NOT_EVIDENCED다.

이 사건 때문에 공개 자동화에서는 다음 규칙을 고정했다.

비밀번호·개인키·복구코드는 채팅으로 받지 않는다.
전송하는 것은 공개키뿐이다.
노출이 의심되면 값 확인보다 먼저 회전한다.

실패 4. ssh-keyscan을 host identity 검증처럼 읽힐 수 있게 썼다

지연 도착한 독립 발행 전 심사는 ssh-keyscan이 key를 출력할 뿐 진위를 검증하거나 known_hosts에 등록하지 않는다는 점을 차단 이슈로 지적했다. v1은 “host key 확인”이라고만 써서 service discovery와 first-contact trust를 섞어 읽을 여지가 있었다.

v2는 다음처럼 수리했다.

제시된 host key 수집: discovery
휴대폰 로컬 fingerprint와 신뢰 채널 대조: identity verification
일치한 key의 known_hosts 등록: enrollment
StrictHostKeyChecking=yes + BatchMode=yes: authenticated probe

과거 receipt에 없는 fingerprint 대조를 소급해 성공으로 만들지 않고 NOT_EVIDENCED로 남겼다.

실패 5. -i만으로 지정 key가 유일한 인증 후보라고 단정할 뻔했다

과거 명령은 새 private key file을 -i로 지정했지만 IdentitiesOnly=yesIdentityAgent=none을 사용하지 않았다. OpenSSH는 사용자 config, 기본 identity file, SSH agent가 제공하는 다른 공개키도 후보로 제시할 수 있다. 따라서 과거 공개키 인증 성공은 유지하되, 서버가 받아들인 identity가 지정한 key 하나뿐이었다는 attribution은 NOT_EVIDENCED로 낮췄다.

v2 재현 명령은 다음 경계를 추가했다.

-F /dev/null: 사용자 SSH config 제외
-i "$key_file" + 사전 파일 존재 확인 + IdentitiesOnly=yes: 존재하는 명시 identity file로 후보 제한
IdentityAgent=none: agent가 다른 key를 추가하지 못하게 차단
PreferredAuthentications=publickey: 공개키 인증만 선택
PasswordAuthentication=no + KbdInteractiveAuthentication=no: 비밀번호 계열 fallback 차단

발행 전에는 임시 ED25519 key를 만든 뒤 네트워크 연결을 하지 않는 ssh -G 구성 확장 검사를 실행했다. 결과는 explicit identityfile 1개, identitiesonly yes, identityagent none, preferredauthentications publickey, passwordauthentication no, kbdinteractiveauthentication no로 확인됐다. 같은 로컬 OpenSSH에서 존재하지 않는 -i 경로를 넣으면 기본 IdentityFile 후보 5개가 남았고, 새 사전 파일 존재 검사는 SSH 실행 전에 이를 거부했다. 이 PASS는 v2 재현 명령의 클라이언트 격리 의미만 검증하며, 과거 accepted identity나 현재 휴대폰 로그인을 소급해 증명하지 않는다.

실패 6. 여러 scan line 가운데 첫 fingerprint만 확인할 뻔했다

ssh-keyscan은 주소 해석 결과에 따라 같은 알고리즘의 key를 여러 줄 돌려줄 수 있다. 첫 줄의 fingerprint만 휴대폰 로컬 값과 비교한 뒤 scan file 전체를 known_hosts에 넣으면, 확인하지 않은 나머지 key까지 등록될 수 있다.

v2는 ssh-keygen -lf가 만든 fingerprint 목록이 비어 있으면 실패하고, while 루프로 모든 fingerprint를 휴대폰 로컬 값과 비교한다. 하나라도 다르면 scan file 전체를 폐기하며, 모든 줄이 정확히 일치할 때만 기존 entry 부재를 확인하고 등록한다. 네트워크를 쓰지 않는 임시 ED25519 검증에서는 단일 일치와 동일 key 2줄은 통과하고, 서로 다른 key가 섞인 2줄과 빈 목록은 모두 fail-closed로 거부됐다.

✅ 결과: “접속 가능”을 층별 검증으로 바꿨다

Before vs After

항목

Before

After

기기 상태

tailnet에 보이지만 오프라인

사용자가 온라인으로 전환한 시점에 ping 확인

네트워크

경로 존재 여부 불명

DERP 경유 ping 성공과 latency 기록

SSH 서비스

TCP/8022 닫힘

OpenSSH 10.4 banner와 포트 열림 확인

host identity

제시된 key 기록만 있고 신뢰 채널 대조 receipt 없음

과거 상태는 NOT_EVIDENCED, 재현 절차는 대조·등록 전 로그인 차단

인증

인증 경로 불명

-i 지정·클라이언트 비밀번호 fallback 차단 공개키 probe PASS; accepted identity exclusivity는 NOT_EVIDENCED, 재현 명령에서 격리

파일 범위

“휴대폰 전체”로 오해 가능

Termux home과 허용된 shared storage로 한정

비밀정보

공개 대화 노출 위험

검증에서 비밀번호 미사용·회전 안내·공개키만 등록

완료 표현

“Tailscale에 있으니 접속 가능”

network/service/auth/scope를 따로 판정

시간 절감, 전송 속도, 배터리 지속시간은 측정하지 않았다. 이번 사례의 수치는 ping 횟수·latency, 포트·banner, 인증 결과와 접근 범위뿐이다.

🧾 Trace · Proof · Verdict · Repair

Trace

휴대폰 peer 발견
→ 오프라인 상태 확인
→ 사용자가 Tailscale 온라인 전환
→ ping 3/3 성공, TCP/8022 닫힘
→ Termux 설치와 OpenSSH 시작
→ TCP/8022 열림, OpenSSH banner 확인
→ 제시된 host key 기록
→ 신뢰 채널 fingerprint 대조 receipt 없음: NOT_EVIDENCED
→ 새 ED25519 key pair 생성·공개키 등록
→ 과거 실행에서 `-i` key file 지정·클라이언트 비밀번호 fallback 없는 공개키 SSH probe
→ 당시 accepted identity exclusivity receipt 없음: NOT_EVIDENCED
→ Android·Termux home·shared storage read-back
→ v2에 모든 scan fingerprint 검증 gate와 client identity isolation 재현 gate 추가
→ 공개 안전한 증거 카드와 사례게시글

Proof

  • 2026년 7월 26일 tailscale ping 3/3, 이후 2/2 성공

  • 첫 probe에서 TCP/8022 닫힘

  • sshd 뒤 TCP/8022 열림과 OpenSSH 10.4 banner 확인

  • SSH가 제시한 host key는 기록됐지만 휴대폰 로컬 fingerprint와의 신뢰 채널 대조 receipt는 없음

  • ED25519 key pair 생성, controller private key 권한 0600, 과거 probe에서 해당 key file을 -i로 지정

  • 클라이언트의 BatchMode=yes·PasswordAuthentication=no 조건에서 공개키 인증 AUTH_OK; accepted identity exclusivity와 서버 측 password authentication 상태는 각각 NOT_EVIDENCED, NOT_VERIFIED

  • Android 12, Termux home, shared storage 사용 가능 read-back

  • 공개 산출물의 IP·계정·Termux UID·키·fingerprint·비밀번호 원문 제거

Verdict

historical_tailnet_reachability: PASS
historical_ssh_service: PASS
historical_host_key_scan: RECORDED
historical_host_identity_verification: NOT_EVIDENCED
historical_public_key_authentication: PASS
historical_identity_file_specified: PASS
historical_accepted_identity_exclusivity: NOT_EVIDENCED
historical_client_password_fallback: DISABLED_FOR_PROBE
historical_server_password_authentication: NOT_VERIFIED
reproduction_host_key_gate: REQUIRED_BEFORE_BATCH_LOGIN
reproduction_client_identity_isolation: REQUIRED_BEFORE_LOGIN
historical_termux_shared_storage_scope: PASS
root_or_other_app_private_data: OUT_OF_SCOPE
screen_remote_control: OUT_OF_SCOPE
current_phone_online: FAIL
current_tailscale_node_key: EXPIRED
current_ssh_login: NOT_RUN

Repair

  • ping 성공을 접속 성공으로 부르지 않고 서비스와 인증을 추가 확인했다.

  • ssh-keyscan discovery와 host identity verification을 분리하고, scan의 모든 fingerprint를 휴대폰 로컬 값과 대조한 뒤에만 known_hosts에 등록하는 gate와 StrictHostKeyChecking=yes를 추가했다.

  • 과거 로그인 절 제목에서 “검증된 host key”라는 소급 표현을 제거해, 공개키 사용자 인증 PASS와 host identity NOT_EVIDENCED를 분리했다. 경계 카드의 복구 순서에도 같은 gate를 반영했다.

  • 과거 fingerprint 대조 증거는 만들지 않고 NOT_EVIDENCED로 보존했다.

  • 과거 -i 지정과 공개키 인증 PASS는 보존하되 accepted identity exclusivity를 소급하지 않고 NOT_EVIDENCED로 분리했다.

  • 재현 명령은 지정 private key file이 실제로 존재하는지 먼저 확인하고, -F /dev/null, IdentitiesOnly=yes, IdentityAgent=none, PreferredAuthentications=publickey로 다른 identity 후보를 차단했다. 서버 측 비밀번호 인증 설정은 변경·검증하지 않았다.

  • 공개 대화에 나타난 비밀번호 의심값은 재사용하지 않고 회전 대상으로 처리했다.

  • 접근 범위를 Termux sandbox와 허용된 공유 저장소로 낮췄다.

  • 현재 오프라인 상태를 과거 성공으로 덮어쓰지 않았다.

🕰️ 현재 상태: 과거 성공과 지금의 가용성은 다르다

2026년 8월 10일 발행 전 상태를 다시 확인했을 때 휴대폰 peer는 오프라인이었고 해당 Tailscale 노드 키는 만료 상태였다. 따라서 현재 시점의 ping과 SSH 로그인은 시도하지 않았다. 네트워크 선행조건이 없는 상태에서 인증 실패처럼 보이는 probe를 반복할 이유가 없기 때문이다.

그림 3. 과거 실행 성공과 현재 가용성을 분리한 시점 카드다. 2026년 7월 공개키 인증은 PASS지만 당시 host identity 대조와 accepted identity exclusivity는 모두 NOT_EVIDENCED로 남긴다. 현재 휴대폰은 재인증·온라인 전환 전이며, 복구 시 모든 scan fingerprint의 exact match·전용 known_hosts 등록·client identity isolation을 마친 뒤에만 strict 공개키 로그인으로 넘어간다.

다시 사용할 때는 휴대폰에서 다음 순서로 복구한다.

Tailscale 앱 열기·재인증
→ peer online 확인
→ Termux에서 sshd 시작
→ tailscale ping
→ TCP/8022 probe
→ 휴대폰 로컬 host-key fingerprint 확인
→ controller scan의 모든 fingerprint와 신뢰 채널 대조
→ 모든 줄이 일치한 scan file만 known_hosts에 등록
→ -F /dev/null + IdentitiesOnly=yes + IdentityAgent=none
→ StrictHostKeyChecking=yes + PreferredAuthentications=publickey 로그인
→ 필요한 shared storage만 확인

장시간 유지가 필요할 때만 Termux와 Tailscale의 배터리 사용을 제한 없음으로 조정하고, termux-wake-lock을 검토한다. 항상 켜 둘 필요가 없다면 작업할 때만 sshd를 시작하는 편이 노출면과 배터리 사용을 줄인다.

🔐 보안과 권한 경계

  1. 본인이 소유하거나 명시적으로 허가받은 기기만 대상으로 한다.

  2. Tailscale 주소는 공인 인터넷 주소보다 노출면이 작지만 인증을 생략할 이유는 없다.

  3. password, private key, recovery code를 채팅이나 자동화 입력으로 받지 않는다.

  4. 전용 키는 기기·용도별로 나누고 private key 권한을 0600으로 유지한다.

  5. ssh-keyscan만으로 신원을 확인했다고 말하지 않는다. 휴대폰 로컬 fingerprint와 신뢰 채널로 대조하고, scan의 모든 fingerprint가 일치할 때만 known_hosts에 등록한다.

  6. 최초 등록과 이후 로그인에서 StrictHostKeyChecking=yes를 유지한다. 기존 key가 바뀌면 자동 삭제하지 말고 기기 재설치·키 재생성·중간자 가능성을 먼저 조사한다.

  7. 지정 key만 검증하려면 사용자 config와 agent의 다른 identity를 차단하고 IdentitiesOnly=yes를 사용한다. -i만으로 유일한 accepted identity가 증명됐다고 말하지 않는다.

  8. termux-setup-storage는 공유 저장소가 필요할 때만 실행한다.

  9. Termux SSH는 root나 다른 앱의 private data 접근권을 자동으로 주지 않는다.

  10. ADB over TCP는 별도 목적과 위험 검토 없이 켜지 않는다.

  11. 넓은 포트 스캔보다 필요한 서비스 포트만 짧게 확인한다.

  12. 휴대폰 분실, 앱 재설치, 노드 키 만료 시 Tailscale과 SSH 키 상태를 함께 재검증한다.

📋 재사용 가능한 체크리스트

[ ] 정확한 Android peer와 소유·허가 범위를 확인한다.
[ ] peer online과 last seen을 구분한다.
[ ] tailscale ping과 direct/DERP 경로를 기록한다.
[ ] SSH를 선택했다면 TCP/8022만 우선 확인한다.
[ ] ssh-keyscan은 제시된 key를 수집하는 discovery로만 사용한다.
[ ] 휴대폰 로컬 host-key fingerprint와 scan에서 나온 모든 fingerprint를 신뢰 채널로 정확히 대조한다.
[ ] 모든 줄이 일치한 scan file만 known_hosts에 명시적으로 등록하고 기존 entry는 자동 교체하지 않는다.
[ ] 기기 전용 ED25519 키를 만들고 공개키만 휴대폰에 등록한다.
[ ] -F /dev/null, IdentitiesOnly=yes, IdentityAgent=none으로 사용자 config·기본 key·agent의 다른 identity 후보를 차단한다.
[ ] PreferredAuthentications=publickey, StrictHostKeyChecking=yes, BatchMode=yes, PasswordAuthentication=no, KbdInteractiveAuthentication=no로 격리된 공개키 로그인을 검증한다.
[ ] whoami, pwd, Termux home을 비파괴로 확인한다.
[ ] 필요할 때만 shared storage 권한과 대상 폴더를 확인한다.
[ ] root·다른 앱 private data·화면 제어를 같은 범위로 주장하지 않는다.
[ ] 잠금 화면·절전 뒤 다시 접근되는지 별도로 시험한다.
[ ] 노드 키 만료와 휴대폰 오프라인을 인증 실패와 구분한다.
[ ] 공개 글에서 IP·사용자명·키·fingerprint·계정·메시지 식별자를 제거한다.

💬 재사용 가능한 프롬프트

프롬프트 1: 안전한 최초 연결

제가 소유한 Android 휴대폰을 Tailscale과 Termux OpenSSH로 연결하려고 합니다.

  1. tailnet membership, online state, tailscale ping, direct/DERP를 분리해 확인해 주세요.

  2. SSH를 선택했으므로 광범위한 스캔 없이 TCP 8022와 SSH banner만 확인해 주세요.

  3. ssh-keyscan은 discovery로만 사용하고, 휴대폰 로컬 ED25519 host-key fingerprint와 controller scan에서 나온 모든 fingerprint를 사용자가 확인하는 신뢰 채널로 대조해 주세요. 전부 일치할 때만 그 scan file을 전용 known_hosts에 등록하고, 기존 key는 자동 삭제하지 마세요.

  4. 비밀번호나 private key를 요청하지 말고 [기기명] 전용 ED25519 공개키 등록 절차를 안내해 주세요.

  5. controller의 사용자 SSH config와 agent가 다른 identity를 추가하지 않도록 -F /dev/null, IdentitiesOnly=yes, IdentityAgent=none, PreferredAuthentications=publickey를 사용해 주세요. StrictHostKeyChecking=yesBatchMode=yes를 유지하고 password·keyboard-interactive fallback을 막은 공개키 로그인 뒤 whoami, pwd, Termux home, 사용자가 허용한 [대상 공유 폴더]만 비파괴로 검증해 주세요. 서버의 password authentication 설정은 별도 항목으로 판정해 주세요.

  6. root, 다른 앱의 private data, ADB, 화면 제어는 별도 범위로 남겨 주세요.

  7. 실제 결과를 network/service/host-identity/auth/scope 상태표로 보고해 주세요.

프롬프트 2: 현재 상태만 재점검

이전에 구성한 Android Termux SSH 연결의 현재 상태를 변경 없이 점검해 주세요.

  • [휴대폰 MagicDNS 또는 Tailscale 주소]를 대상으로 peer online, node key expiry, tailscale ping, TCP 8022, 휴대폰 로컬 fingerprint와 등록된 SSH host key의 일치, client identity isolation, StrictHostKeyChecking=yes 공개키 로그인을 앞 단계가 성공할 때만 순서대로 확인하세요. key가 바뀌면 자동 교체하지 마세요.

  • 앞 단계가 실패하면 뒤 검사를 인증 실패로 오인하지 말고 NOT_RUN으로 남기세요.

  • IP·계정·키·fingerprint·개인 파일명은 공개 보고에서 제거하세요.

🌍 다른 업무에 적용한다면

같은 층별 검증은 휴대폰 외의 소형 서버에도 쓸 수 있다.

  • Raspberry Pi에서는 8022 대신 실제 SSH 포트를 확인하고, system account와 접근 디렉터리를 별도 검증한다.

  • Syncthing은 Tailscale ping 뒤 sync port와 GUI port를 따로 확인한다.

  • 휴대폰의 로컬 웹 앱은 해당 app port, HTTP status, application login을 각각 확인한다.

  • 문서 수집 자동화에서는 “기기 온라인”과 “원본 폴더 읽기 가능”을 분리해 실패 원인을 기록한다.

도구가 달라져도 순서는 같다. 네트워크가 있는지, 서비스가 듣는지, 인증이 되는지, 어느 자원까지 허용됐는지를 차례대로 닫는다.

📚 참고자료

📦 산출물과 공개 경계

  • 사례게시글 Markdown 1개

  • 실제 실행 receipt에서 파생한 SVG 증거 카드 3개

  • 내부 source evidence·asset manifest·QA receipt

  • local Lecture Recap Library 발행 대상

  • 외부 웹·커뮤니티 게시: 수행하지 않음

  • 현재 휴대폰 재인증·온라인 복구: 수행하지 않음

  • 현재 SSH login: 수행하지 않음


뉴스레터 무료 구독