TL;DR
OpenSSH의 원격 포트 포워딩과nginx를 조합하면 별도 터널 서비스 없이 로컬 웹 서비스를 공개하는 자체 호스팅 HTTP 터널 구축이 가능함.- 원격 포트를
0으로 지정하면sshd가 사용 가능한 포트를 할당하며,nginx가p<포트>.ssh.luffy.cx요청을 해당 포트로 프록시함. - 와일드카드 DNS 레코드와 Let’s Encrypt 인증서가 필요하며, Nginx의
ngx_http_secure_link_module로 만료 시각과 비밀값을 포함한 링크를 검증함. - 인증 실패 시
401, 만료 링크에는410을 반환하고, 프록시 요청에서Authorization헤더를 제거하며 WebSocket 연결도 처리함. - 보조 스크립트는
sshd-session프로세스에 연결된 포트를 찾아 인증 토큰을 포함한 URL을 출력하고 SSH 세션을 유지함.
기본 설정
- 로컬 서비스가
localhost:8080에서 실행되는 경우,ssh -N -R 0:localhost:8080 web02.luffy.cx로 원격 서버의 임의 포트를 통해 연결을 포워딩함. 원격 포트에0을 지정하면 서버가 사용 가능한 포트를 할당함. nginx는https://p41535.ssh.luffy.cx같은 요청을http://127.0.0.1:41535로 프록시하며, 서버 이름에서 포트 번호를 추출함.*.ssh.luffy.cx와일드카드 DNS 레코드와 Let’s Encrypt 와일드카드 인증서가 필요함.*.ssh.luffy.cx는web02.luffy.cx를 가리키고, 인증 기관 제한 및 DNS-01 인증을 위한 CNAME 레코드도 설정함.acme.luffy.cx는 Route 53에서 호스팅되는 DNS 영역이며, 와일드카드 인증서와 여러 웹 서버가 제공하는 도메인의 ACME DNS-01 챌린지에 사용됨. 이 설정에서는 NixOS가 인증서를 자동으로 가져옴.
접근 제어
- 기본 구성에서는 포트 번호가 콘텐츠를 기밀로 유지하는 유일한 ‘비밀’임. 다른 포워딩 솔루션은 가능한 값을 열거하지 못하도록 도메인 이름에 임의 문자열을 추가하기도 함.
ngx_http_secure_link_module은 비밀값을 포함한 여러 값의 해시를 계산해 요청의 해시와 비교함. 해시는 대소문자를 구분하지 않는 도메인 이름에 넣을 수 없는 Base64 문자열이므로, 만료 시각과 함께 URL의 사용자 이름에 포함함.- 예시 링크
https://6J3jK1WmB15c6WmjW_X-Wg--1789928654@p41535.ssh.luffy.cx/en/blog에서 사용자 이름은 해시와 만료 시각을 담고, 호스트 이름은 포트를, 경로는 요청 경로를 나타냄. - 클라이언트는 HTTP 기본 인증을 사용해 사용자 이름을 서버에 전송하며,
curl을 포함한 대부분의 HTTP 클라이언트에서 작동함. Nginx는 이를$remote_user변수로 제공하고,map지시문으로 해시와 만료 시각을 추출해 모듈이 요구하는 쉼표 구분 형식으로 결합함. - 해시 계산에는 만료 시각, 포트, 비밀값이 사용됨. 예시의 비밀값은
ZuPerS3cr3!임. - 검증 결과는
$secure_link에 저장됨. - 해시가 일치하지 않으면 빈 값임.
- 해시는 일치하지만 링크가 만료됐으면
0임. - 유효한 링크이면
1임. - 해시가 없거나 올바르지 않으면
WWW-Authenticate헤더와 함께401을 반환하고, 만료된 링크에는410을 반환함. 프록시로 요청을 전달하기 전에Authorization헤더를 제거하고 WebSocket 연결을 위한 지시문도 설정함. - 전체 Nginx 설정은 사용자 이름에서 해시와 만료 시각을 추출하고,
secure_link로 검증한 뒤 요청을 해당 포트로 전달함. 이때Host와X-Forwarded-For헤더를 설정하고, HTTP/1.1 및 WebSocket 업그레이드를 활성화하며, 프록시 버퍼링을 끄고 읽기 제한 시간을 30분으로 설정함.
보조 스크립트
- OpenSSH가 할당한 임시 포트는 환경 변수에 나타나지 않으므로, 스크립트는 상위 프로세스를 살펴
sshd-session프로세스를 찾음. SSH 세션이 아니면 오류를 출력하고 종료함. ss명령으로 해당sshd-session프로세스와 연결된 TCP 리스닝 포트를 찾음. 포트가 없으면ssh -R 0:localhost:PORT를 사용하라는 오류를 출력하고 종료함.- 스크립트는 수명을 86,400초로 설정하고 만료 시각, 포트, 비밀값으로 MD5 해시를 계산함. 해시는 Base64로 인코딩한 뒤 URL에 사용할 수 있도록 문자와 패딩을 변환함.
- 각 포트에 대해
https://<토큰>--<만료 시각>@p<포트>.ssh.luffy.cx/형식의 URL을 출력한 뒤sleep infinity로 SSH 세션을 유지함. - 서버에 스크립트를
http-over-ssh로 설치하고, SSH 설정에서 호스트 별칭http-over-ssh에 서버 이름과 원격 명령을 지정하며ControlPath none을 설정함. - 이 구성은 서버에서 이미 실행 중인
OpenSSH와nginx만 사용하며, 짧은 명령으로 자체 호스팅 터널과 공유 URL을 제공함. NixOS 환경에서는http-over-ssh.nix설정도 사용할 수 있음.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요