포트 없이 Tor onion service로 SSH 설정하기
VPS의 인바운드 포트를 열지 않고 SSH를 운영하는 방법을 설명합니다. Tor onion service 설정, v3 client authorisation, 잠금 방지를 위한 정확한 적용 순서와 재부팅 검증까지 다룹니다.
Tor onion service를 통한 SSH에서 달라지는 점
Tor onion service를 통한 SSH를 사용하면 어떤 포트에서도 인바운드 연결을 허용하지 않는 VPS를 관리할 수 있다. 서버가 Tor 네트워크로 아웃바운드 연결을 시작하고 해당 연결을 계속 유지한다. SSH 세션은 이 연결을 통해 다시 서버로 들어오므로, 공인 IP 주소에서 수신 대기할 필요가 없다.
로그에는 즉시 변화가 나타난다. 공용 SSH 포트가 있는 서버는 스캐너가 보내는 수천 건의 암호 인증 실패를 매일 기록한다. sshd을 onion service 뒤에 두고 방화벽에서 인바운드 트래픽을 차단하면 /var/log/auth.log에는 관리자가 시작한 세션만 기록된다.
대신 모든 관리 세션의 경로에 tor이(가) 포함된다. tor은(는) 사용자 공간 데몬이며, 재부팅할 때마다 로그인하기 전에 시작되고 부트스트랩되어야 한다. 포트를 닫기 전에 이 점을 계획에 반영해야 한다. 여기서 발생할 수 있는 장애는 물리적으로 접근할 수 없는 장비에 대한 접근 권한을 잃는 것이기 때문이다.
무엇을 변경하기 전에 복구 경로를 확보합니다
SSH를 사용하지 않는 복구 경로를 확보하기 전에는 작업을 시작하지 마십시오.
지금 공급자의 콘솔을 열고 제어판의 VNC 또는 serial console로 로그인하십시오. root password를 모르는 경우 먼저 제어판에서 root password를 재설정하고 정상적으로 작동하는지 확인하십시오. 한 번도 테스트하지 않은 콘솔은 복구 경로가 아닙니다.
아래 순서가 중요합니다. 각 단계가 검증된 후에 다음 단계가 실행됩니다. onion route가 작동할 때까지 port 22는 열어 둡니다.
- tor를 설치하고 bootstrap이 완료되는지 확인합니다.
- onion service를 정의하고 주소를 확인합니다.
- port 22가 아직 열려 있는 동안 onion을 통해 연결합니다.
- client authorisation을 추가한 다음 다시 연결합니다.
sshd을 loopback에 bind하고 port 22를 닫습니다.- 재부팅한 다음 onion을 통해 다시 연결합니다.
현재 SSH session은 전체 과정이 끝날 때까지 열어 두십시오. 이미 수립된 session은 새 연결을 차단하는 firewall 변경 후에도 유지되므로 첫 번째 복구 수단이 됩니다.
서버에 Tor 설치
Ubuntu는 자체 저장소에서 tor를 제공하지만, 이 저장소의 빌드는 최신 버전보다 뒤처지는 경우가 많다. Tor Project의 저장소에는 해당 프로젝트의 문서에서 설명하는 버전이 포함되어 있다. apt 저장소 안내서의 명령으로 저장소를 추가한다.
sudo apt update
sudo apt install -y apt-transport-https wget gpg
wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | sudo tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null/etc/apt/sources.list.d/tor.sources을 작성한다. Suites는 시스템의 릴리스 코드명을 사용하며, lsb_release -cs로 확인할 수 있다( Ubuntu 24.04에서는 noble).
Types: deb deb-src
URIs: https://deb.torproject.org/torproject.org/
Suites: noble
Components: main
Signed-By: /usr/share/keyrings/deb.torproject.org-keyring.gpgsudo apt update
sudo apt install -y tor deb.torproject.org-keyring
sudo journalctl -u tor@default -n 20 --no-pager로그는 Bootstrapped 100% (done)로 끝나야 한다. 이 메시지에 도달하지 못하면 tor가 네트워크에 연결할 수 없는 상태다. 거의 항상 외부로 나가는 연결을 차단하는 방화벽 규칙이나 크게 잘못된 시스템 시간이 원인이다.
unit 이름에 주의해야 한다. 모든 상태가 정상이어도 systemctl status tor는 Active: active (exited)을 보고한다. Debian과 Ubuntu는 tor를 다중 인스턴스 master unit으로 패키징하며, 이 unit의 유일한 역할은 실제 인스턴스를 가져오는 것이기 때문이다. 데몬 자체는 tor@default.service로 실행된다. status와 journalctl에는 이 이름을 사용한다. tor의 시작, 중지, reload는 여전히 해당 인스턴스에 전달되므로 sudo systemctl reload tor도 예상대로 작동한다.
포트 22에 onion 서비스를 정의합니다
/etc/tor/torrc에 다음 2줄을 추가합니다.
HiddenServiceDir /var/lib/tor/ssh/
HiddenServicePort 22 127.0.0.1:22두 번째 줄은 tor가 onion 주소에서 가상 포트 22를 수신한 다음, 해당 시스템의 127.0.0.1:22에 연결하도록 지정합니다. Tor는 loopback을 통해 sshd에 연결합니다. 따라서 나중에 sshd가 공용 주소에서 수신 대기하지 않도록 설정할 수 있습니다.
sudo systemctl reload tor
sudo cat /var/lib/tor/ssh/hostname이 명령은 56개의 base32 문자 뒤에 .onion를 출력합니다. 이 문자는 인코딩된 서비스 공개 키입니다. 인증 기관도 없고, 어디에도 이름을 등록하지 않습니다.
tor가 /var/lib/tor/ssh/을 직접 생성하도록 합니다. 잘못된 소유자로 직접 만들거나 0700보다 느슨한 모드를 지정하면 tor는 해당 디렉터리를 사용하지 않습니다. journal에는 디렉터리 권한이 너무 느슨하다는 메시지가 기록됩니다. 내부 파일은 서비스의 식별 정보입니다. hs_ed25519_secret_key is 주소입니다. 해당 디렉터리는 mode 600으로 백업하고 복사본은 시스템 외부에 보관합니다. 이 디렉터리를 잃으면 새 주소를 만들고 모든 클라이언트의 설정을 수정해야 합니다.
워크스테이션에서 연결하기
워크스테이션에는 tor client가 필요하며, 별도 설정은 필요하지 않다. Debian 또는 Ubuntu에서는 sudo apt install -y tor netcat-openbsd이다. 그러면 Tor가 127.0.0.1:9050에서 SOCKS5 proxy로 수신 대기한다. SOCKS는 범용 proxy protocol이며, version 5에서는 IP address 대신 hostname을 전달할 수 있다. 여기서 중요한 부분은 이 기능이다.
OpenSSH에는 자체 SOCKS client가 없으므로 helper program을 사용해 연결한다. 다음 내용을 ~/.ssh/config에 추가한다.
Host myvps
HostName xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion
User admin
ProxyCommand /usr/bin/nc -X 5 -x 127.0.0.1:9050 %h %p
ServerAliveInterval 30-X 5은 SOCKS5를 선택하고, -x 127.0.0.1:9050는 로컬 tor를 지정한다. %h은 onion name을 이름으로 tor에 전달하므로, tor가 네트워크 내부에서 이를 resolve한다. 이때는 OpenBSD netcat을 사용해야 한다. GNU netcat에는 -X option이 없으므로 nc: invalid option -- 'X'에서 중단된다.
ssh myvps첫 번째 연결은 느리다. 다른 작업을 수행하기 전에 tor가 circuit을 구성하기 때문이다. 다른 환경에서와 같은 방식으로 host key fingerprint를 수락한다. 이제부터는 일반적인 SSH key 처리를 그대로 적용한다. 전송 방식만 변경되었고, 인증 방식은 변경되지 않았다.
일회성 연결이라면 config 항목을 생략할 수 있다. torsocks ssh admin@xxxxx.onion이 같은 작업을 수행한다.
v3 클라이언트 인증 추가
현재 상태에서는 주소를 알아낸 누구나 SSH 배너에 연결하여 추측을 시작할 수 있다. Onion 주소는 디렉터리 시스템에서 열거할 수 없으므로 주소가 비밀처럼 동작하지만, 셸 기록이나 git 저장소에 커밋한 구성 파일을 통해 일반적인 방식으로 유출될 수 있다. 클라이언트 인증을 사용하면 이 문제가 해결된다. 서비스는 클라이언트 키로 암호화한 디스크립터를 게시하므로, 주소만 가지고 키가 없는 사용자는 서비스의 위치조차 확인할 수 없다.
클라이언트에서 x25519 키 쌍을 생성한다. 다음은 Tor Project의 클라이언트 인증 가이드에 있는 파이프라인이며, 한 가지 변경 사항이 있다.
openssl genpkey -algorithm x25519 -out /tmp/k1.prv.pem
grep -v " PRIVATE KEY" /tmp/k1.prv.pem | base64 -d | tail --bytes=32 | base32 | sed 's/=//g' > /tmp/k1.prv.key
openssl pkey -in /tmp/k1.prv.pem -pubout | grep -v " PUBLIC KEY" | base64 -d | tail --bytes=32 | base32 | sed 's/=//g' > /tmp/k1.pub.key해당 명령의 게시된 버전은 base64pem -d을 사용하지만, 기본 Ubuntu 설치에는 이 명령이 포함되어 있지 않다. 따라서 명령이 base64pem: command not found에서 중단된다. GNU base64 -d는 동일한 PEM 본문을 디코딩하므로 대신 사용한다.
서버에 공개 키를 설치한다.
sudo install -d -m 700 -o debian-tor -g debian-tor /var/lib/tor/ssh/authorized_clients
echo "descriptor:x25519:PASTE_PUBLIC_KEY_HERE" | sudo tee /var/lib/tor/ssh/authorized_clients/laptop.auth >/dev/null
sudo chown debian-tor:debian-tor /var/lib/tor/ssh/authorized_clients/laptop.auth
sudo systemctl reload tor.auth으로 끝나는 파일만 읽는다. laptop.auth.txt로 저장해야 한다. 그렇지 않으면 tor가 오류를 출력하지 않고 해당 파일을 무시하며, 서비스는 주소를 아는 누구에게나 계속 공개된다.
클라이언트에 개인 키를 설치한다. Ubuntu에서는 tor 데몬이 debian-tor 사용자로 실행되므로 홈 디렉터리의 파일을 읽을 수 없다. 따라서 해당 사용자가 접근할 수 있는 위치에 디렉터리를 둔다.
sudo install -d -m 700 -o debian-tor -g debian-tor /var/lib/tor/onion_auth
echo "ADDRESS_WITHOUT_DOT_ONION:descriptor:x25519:PASTE_PRIVATE_KEY_HERE" | sudo tee /var/lib/tor/onion_auth/myvps.auth_private >/dev/null
sudo chown debian-tor:debian-tor /var/lib/tor/onion_auth/myvps.auth_private
sudo chmod 600 /var/lib/tor/onion_auth/myvps.auth_private클라이언트의 /etc/tor/torrc에 ClientOnionAuthDir /var/lib/tor/onion_auth을 추가하고 tor를 다시 로드한다. macOS의 Homebrew 빌드처럼 tor를 자신의 사용자로 실행한다면 ClientOnionAuthDir가 ~/.tor/onion_auth을 가리키도록 설정하고 모드를 0700으로 지정한다.
해당 파일 안의 주소는 .onion 접미사를 제외한 56자 전체다. 작업을 마치면 /tmp/k1.prv.pem과 /tmp/k1.prv.key를 삭제한다.
이제 양방향을 테스트한다. ssh myvps은 계속 연결되어야 한다. 키가 없는 컴퓨터에서는 동일한 주소로 연결할 수 없어야 한다. 이 연결 실패가 인증이 활성화되었다는 증거다.
포트 22를 다음 순서로 닫습니다
먼저 안전장치를 설정합니다. 아래 두 변경 사항으로 인해 접속이 차단되면 이 명령 하나가 15분 후 변경 사항을 모두 되돌립니다.
sudo systemd-run --on-active=15m --unit=ssh-rescue \
/bin/sh -c 'ufw allow 22/tcp; rm -f /etc/systemd/system/ssh.socket.d/override.conf; systemctl daemon-reload; systemctl restart ssh.socket'onion 경로가 계속 작동하는 것을 확인한 뒤 sudo systemctl stop ssh-rescue.timer로 취소합니다.
다음으로 공용 주소에서 수신 대기하는 sshd를 중지합니다. Ubuntu 24.04에서는 socket unit이 SSH를 활성화하므로 sshd_config의 ListenAddress는 무시됩니다. 수신 대기 소켓을 소유하는 것은 sshd가 아니라 ssh.socket입니다. 현재 어떤 경우인지 확인합니다.
systemctl is-enabled ssh.socket출력이 enabled이면 sudo systemctl edit ssh.socket을 실행하고 다음 내용을 추가합니다.
[Socket]
ListenStream=
ListenStream=127.0.0.1:22빈 ListenStream=은 패키지에서 제공한 unit에서 상속된 값을 지웁니다. 이 줄을 생략하면 공용 수신 대기 소켓을 유지한 채 두 번째 수신 대기 소켓이 추가됩니다. 이 단계가 별도 오류 없이 실패하는 가장 흔한 원인입니다.
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
ss -tlnp | grep ':22'ss의 출력에는 127.0.0.1:22가 표시되고 0.0.0.0:22에는 아무것도 표시되지 않아야 합니다. ssh.socket이 비활성화되어 있었다면 /etc/ssh/sshd_config.d/10-onion.conf에 ListenAddress 127.0.0.1를 넣고 sudo systemctl restart ssh를 실행한 다음, 같은 ss 줄로 확인합니다. 두 경우 모두 이 출력이 완료되었다는 증거입니다.
그다음은 방화벽입니다. 이는 VPS의 일반적인 ufw 규칙 관리입니다. 먼저 sudo ufw status numbered을 실행하고, 출력된 SSH 규칙을 삭제합니다.
sudo ufw status numbered
sudo ufw delete allow OpenSSH
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw status verbose송신 트래픽은 허용된 상태로 둡니다. Tor는 443 및 9001 같은 포트로 릴레이에 송신 연결을 수행하므로, 송신 기본 정책을 deny로 설정하면 tor의 부트스트랩이 중단되고 남아 있던 유일한 접속 경로도 동시에 사라집니다. 대부분의 공급자는 제어판에서 별도의 네트워크 방화벽도 운영합니다. 해당 방화벽에서도 22를 닫아야 합니다. 그렇지 않으면 ufw의 출력과 관계없이 포트에 계속 접근할 수 있습니다.
이 서버에서 Docker를 실행한다면 작업을 완료했다고 판단하기 전에 게시된 포트를 확인합니다. Docker는 동일한 테이블에 자체 규칙을 기록하고 ufw를 우회해 컨테이너 포트를 직접 게시하므로, ufw의 deny 정책만으로는 전체 구성을 확인할 수 없습니다.
재부팅 후 정상 작동을 확인한다
systemctl is-enabled tor@default
sudo reboot첫 번째 명령이 서비스가 enabled 상태라고 보고하지 않으면 재부팅하기 전에 sudo systemctl enable tor@default을 실행한다. 2분 동안 기다린 다음 ssh myvps을 실행한다. Tor는 부팅 후 bootstrap해야 하므로 머신 자체가 시작된 뒤에도 일정 시간이 지나야 onion 주소가 응답하기 시작한다.
서비스가 끝내 다시 시작되지 않으면 콘솔을 열고 sudo journalctl -u tor@default -b를 확인한다. torrc 구문 오류나 디렉터리 권한 문제는 해당 출력에 표시된다. 적용하기 전에 torrc 수정 사항을 검사할 수도 있다.
sudo -u debian-tor tor --verify-configWireGuard 터널과 비교한 비용
자체 VPS에서 WireGuard VPN을 사용하는 방법과 비교하면 onion service는 더 느리고 예측하기 어렵다. 사용하기로 결정하기 전에 이 트레이드오프를 냉정하게 판단해야 한다.
지연 시간. 클라이언트 회로는 3개의 릴레이로 구성되고 서비스 측에서도 3개가 추가되므로, 입력한 키는 전 세계에서 무작위로 선택된 대략 6대의 시스템을 거친다. 대화형 입력에서는 지연이 눈에 띄고 파일 복사는 느리다. WireGuard는 홉이 1개 추가된다. 실제 환경은 time ssh myvps 'echo ok'으로 측정해야 한다. 측정값은 tor가 구성한 회로에 따라 달라지며, tor가 다른 회로를 구성하면 다시 바뀐다.
사용자 공간 데몬이 핵심 경로에 포함된다. WireGuard는 커널에서 동작하며 네트워크와 함께 시작된다. Tor는 시작하고 부트스트랩한 뒤 guard 릴레이에 연결해야 하는 프로세스다. 이 과정이 실패하면 provider 콘솔에서 작업해야 한다.
시계 정확도. onion service descriptor는 시간 구간을 기준으로 게시되므로, 시스템 시간이 크게 어긋나면 어디에도 명확한 메시지가 표시되지 않은 채 주소 조회가 실패한다. timedatectl의 결과는 System clock synchronized: yes여야 한다.
그 대신 방화벽 규칙이 올바른지에 의존하지 않는 노출 경로를 얻는다. 스캔할 포트가 없고 가져갈 배너도 없다. 주소 자체가 공개 키이므로 SSH가 시작되기 전에 endpoint가 자신의 신원을 증명한다.
실용적인 답은 대개 둘 다 사용하는 것이다. 일상적인 경로로 WireGuard를 실행하고, WireGuard 설정이 잘못되었을 때도 작동하는 경로로 onion service를 유지한다. 그러면 공개 SSH 포트 대신 UDP 포트 1개만 열어 두면 된다. 그렇다고 sshd 자체를 강화하는 작업이 필요 없어진다는 뜻은 아니다. 키 기반 인증과 non-root 로그인은 여전히 중요하다. onion service는 네트워크 경로를 보호할 뿐 그 너머의 요소는 보호하지 않기 때문이다.
실패 유형 및 표시되는 오류
Tor가 Bootstrapped 0%을(를) 절대 통과하지 못합니다. 아웃바운드 트래픽이 차단되었거나 시스템 시간이 크게 어긋나 있습니다. sudo ufw status verbose로 아웃바운드 정책을 확인한 다음 timedatectl을(를) 실행합니다.
systemctl status tor에 active (exited)이(가) 표시됩니다. Debian과 Ubuntu에서는 정상입니다. 대신 tor@default을(를) 확인합니다.
디스크립터를 찾을 수 없습니다. Tor가 SOCKS 확장 오류 F0, "Onion Service Descriptor Can Not be Found"를 반환합니다. 아직 디스크립터가 게시되지 않았을 수 있습니다. reload 후 게시까지 잠시 걸립니다. 또는 서버의 tor가 실행 중이지 않을 수 있습니다.
F4, "Onion Service Missing Client Authorization". 클라이언트에 tor가 사용할 수 있는 일치하는 .auth_private이(가) 없습니다. torrc에 ClientOnionAuthDir이(가) 있는지, 디렉터리 모드가 0700인지, 파일 이름이 .auth_private로 끝나는지, debian-tor이(가) 해당 파일을 읽을 수 있는지 확인합니다.
F5, "Onion Service Wrong Client Authorization". 개인 키가 서버의 .auth 파일과 일치하지 않습니다. base32 문자열 끝에 =이(가) 붙어 있거나 문자열 내부에 불필요한 줄바꿈이 있으면 이 문제가 발생합니다.
nc: invalid option -- 'X'. OpenBSD 버전이 아니라 GNU netcat이 설치되어 있습니다. sudo apt install -y netcat-openbsd을(를) 실행합니다.
Could not resolve hostname. ssh가 일반 DNS를 조회했지만 .onion에 대한 응답이 없어서 ProxyCommand이(가) 실행되지 않았습니다. ~/.ssh/config의 Host 패턴이 입력한 이름과 일치하지 않습니다.
Permission denied (publickey). 터널은 정상적으로 동작했고 tor의 작업도 끝났습니다. 이 문제는 일반적인 permission denied publickey 문제로 처리하고 tor는 원인에서 제외합니다.
FAQ
onion service를 사용하면 VPS에서 열린 포트가 정말 없어집니까?
그렇습니다. sshd가 127.0.0.1에 바인딩되고 방화벽이 인바운드 트래픽을 차단하면 됩니다. Tor는 릴레이로 아웃바운드 TCP 연결을 만들고 세션은 릴레이를 통해 다시 전달됩니다. 따라서 서버의 public address에서 연결을 수락하는 프로세스가 없습니다. 서버에서 ss -tlnp를 실행하고 다른 위치에서 포트 스캔을 수행해 이를 확인할 수 있습니다. 제어판에 있는 provider 자체의 network firewall도 잊지 마십시오. 이 방화벽은 ufw와 별도로 제어되며 역시 닫아야 합니다.
SSH에서 .onion 주소만으로 충분히 안전합니까?
아닙니다. 주소는 56자이며 directory system에서 추측하거나 열거할 수 없습니다. 따라서 비밀처럼 동작하지만 shell history와 config 파일을 통해 유출될 수 있습니다. v3 client authorisation을 추가하십시오. 이를 사용하면 service descriptor가 client key로 암호화됩니다. 따라서 주소만 가진 사용자는 extended error F4를 받고 sshd에 전혀 도달하지 못합니다.
재부팅 후 tor가 시작되지 않으면 어떻게 됩니까?
SSH access를 완전히 잃게 됩니다. 이때 접속할 수 있는 유일한 경로가 onion address이기 때문입니다. 따라서 port 22를 닫기 전에 provider console을 테스트해야 합니다. 또한 Tor는 부팅 후 bootstrap할 시간이 필요하므로, 서버가 ping에 응답한 뒤에도 주소가 응답하기까지 시간이 더 걸립니다. 끝내 응답하지 않으면 console에 로그인하고 sudo journalctl -u tor@default -b를 확인하십시오. 여기에 torrc syntax error 또는 /var/lib/tor/ssh의 permissions 문제가 표시됩니다.
SSH over Tor가 WireGuard보다 느립니까?
그렇습니다. 차이가 큽니다. onion service로 연결하면 무작위로 선택된 약 6개의 릴레이를 거칩니다. 반면 WireGuard는 서버까지 직접 연결되는 하나의 암호화된 hop입니다. 입력할 때 지연이 느껴지고 파일 전송도 느립니다. 일반적인 구성은 일상적인 작업에 WireGuard를 사용하고, VPN 설정이 손상되어도 사용할 수 있는 비상 경로로 onion service를 유지하는 것입니다.