6P by GN⁺ | ★ favorite | 댓글 1개
  • SSH는 원격 접속을 넘어 포트 포워딩, 점프 호스트, 설정 파일, 키 관리까지 묶어 다루는 도구이며, 웹 서버 예시는 RDP·SQL 같은 다른 서비스에도 그대로 응용 가능함
  • 로컬·원격·동적 포트 포워딩은 각각 -L, -R, -D로 구성되며, 포트가 어느 쪽에 열리고 트래픽이 어디로 흐르는지가 핵심 차이임
  • 직접 접근이 막힌 네트워크에서는 -J 점프 호스트ProxyJump로 여러 SSH 경유지를 연결해 목적지까지 들어갈 수 있음
  • ssh-agent-A 에이전트 포워딩은 원격 호스트에서도 로컬 키를 쓰게 해 편리하지만, 에이전트 오용에 따른 보안 위험을 먼저 확인해야 함
  • ~/.ssh/config, ssh-copy-id, ssh-keygen, SSH 콘솔 ~?·~C를 함께 쓰면 반복 옵션 입력을 줄이고 세션 중 포워딩 추가, 공개키 배포, 키 생성·검사가 쉬워짐

SSH 포트 포워딩을 이해하기 위한 전제

  • SSH 포트 포워딩은 다이어그램만으로는 흐름을 잡기 어려워, 실제 명령과 네트워크 시나리오를 함께 봐야 이해하기 쉬움
  • 예시는 웹 서버 접근을 기준으로 하지만, 같은 방식은 RDP, SQL 등 거의 모든 서비스에 적용 가능함
  • 반복해서 쓰는 옵션은 다음 의미를 가짐
    • -N: 원격 서버에서 명령을 실행하지 않고 셸도 열지 않음
    • -f: SSH를 백그라운드로 보냄
    • root@host: 터널을 만들 사용자와 호스트로 로그인함

로컬 포트 포워딩 -L

  • 로컬 포트 포워딩은 로컬 머신의 포트를 원격 서버의 포트로 전달함
  • 예시 상황
    • internal-web.int가 루프백 인터페이스에서만 접근 가능한 웹 페이지를 호스팅함
    • campfire.int에서 internal-web.int로 SSH 접근이 가능함
    • campfire.int의 로컬 포트를 통해 internal-web.int의 웹 서버에 접근하려 함
  • 사용 명령
    • ssh -N -f -L 1337:127.0.0.1:80 root@internal-web.int
  • 옵션 해석
    • -L: 로컬 포워딩을 지정함
    • 1337:127.0.0.1:80: 로컬 포트 1337을 원격의 127.0.0.1:80에 바인딩함
  • 터널 생성 후에는 campfire.int에서 로컬 포트 1337로 요청을 보내 internal-web.int의 포트 80과 상호작용할 수 있음
  • 기억할 점은 -L에서는 로컬 포트가 주소의 왼쪽에 온다는 것임

원격 포트 포워딩 -R

  • 원격 포트 포워딩은 로컬에서 접근 가능한 포트를 원격 서버의 포트로 노출함
  • 예시 상황
    • internal-web.int는 루프백에서만 접근 가능한 웹 페이지를 호스팅함
    • campfire.int는 방화벽 때문에 internal-web.int에 직접 접근할 수 없음
    • vuln-server.intcampfire.intinternal-web.int 양쪽에서 접근 가능함
  • 사용 명령
    • ssh -N -f -R 3000:127.0.0.1:80 root@vuln-server.int
  • 옵션 해석
    • -R: 원격 포워딩을 지정함
    • 3000:127.0.0.1:80: vuln-server.int의 포트 3000을 로컬의 127.0.0.1:80에 바인딩함
  • 이후 vuln-server.int:3000으로 curl 요청을 보내면 internal-web.int의 포트 80에서 동작하는 내부 웹 페이지에 접근할 수 있음
  • 기억할 점은 -R에서는 로컬 포트가 주소의 오른쪽에 온다는 것임

동적 포트 포워딩 -D와 SOCKS 프록시

  • 동적 포트 포워딩-D 옵션으로 SOCKS 프록시를 만들고 트래픽을 SSH 경유지로 보냄
  • 예시 상황
    • internal-web.int는 내부 네트워크에서만 접근 가능한 웹 애플리케이션을 호스팅함
    • vuln-server.int는 같은 내부 네트워크에 있고 internal-web.int에 접근 가능함
    • campfire.int에서 vuln-server.int를 통해 트래픽을 프록시하려 함
  • /etc/proxychains.conf 설정은 SSH 명령의 포트와 맞아야 함
    • socks5: proxychains가 SOCKS5를 사용하게 함
    • 127.0.0.1: 로컬호스트를 사용함
    • 8080: SSH -D에 지정한 포트와 일치해야 함
  • 사용 명령
    • ssh -N -f -D 8080 root@vuln-server.int
  • 포워딩 생성 후 socks5 127.0.0.1 8080을 설정하면 proxychains curl 192.168.1.185로 내부 웹 페이지에 접근할 수 있음
  • SOCKS를 통한 DNS는 환경에 따라 잘 되지 않을 수 있어 예시에서는 호스트명 대신 IP 주소를 사용함
  • Firefox에서도 수동 프록시 설정으로 SOCKS 프록시를 사용할 수 있음
    • 경로: Settings → Privacy & Security → Network Settings
    • Manual proxy configuration 선택
    • “Proxy DNS when using SOCKS V5” 체크
    • SOCKS host를 127.0.0.1, port를 8080으로 설정함

점프 호스트 -J

  • 점프 호스트는 현재 호스트에서 직접 접근할 수 없는 목적지에 여러 SSH 경유지를 거쳐 접속하게 함
  • 예시 체인은 campfire.intvuln-server.intinternal-web.intdns.int
  • 사용 명령
    • ssh -J root@vuln-server.int,root@internal-web.int root@dns.int
  • 여러 점프 대상은 쉼표로 구분함

에이전트 포워딩 -A

  • ssh-agent는 로컬 머신에서 ssh-add <private_key_file>로 개인 키나 ID를 추가할 수 있게 함
  • 추가된 키는 ssh-add -l로 확인 가능함
  • ssh-agent에 키를 추가하면 비밀번호를 다시 입력하지 않고 해당 키로 SSH 접속할 수 있어 사람과 서비스 계정 모두에 유용함
  • -A 에이전트 포워딩은 접속한 원격 머신에서도 로컬 에이전트의 키를 사용할 수 있게 함
  • 사용 전 보안 위험을 확인하려면 Zero Effort Private Key Compromise: Abusing SSH-Agent for Lateral Movement를 참고해야 함
  • 예시 명령
    • ssh -A -J root@vuln-server.int root@internal-web.int
  • 이 명령은 vuln-server.int를 거쳐 internal-web.int에 접속하면서 로컬 campfire.int의 SSH 에이전트에 있는 키를 사용할 수 있게 함
  • 이후 internal-web.int에서 ssh root@dns.int를 실행할 때 개인 키 지정이나 자격 증명 입력 없이 접속할 수 있음

TTY 명령 할당 -t

  • -t 옵션은 원격 서버에서 상호작용이 필요한 명령을 빠르게 실행할 때 유용함
  • 예시는 Vim이나 top처럼 TTY가 필요한 명령임
  • 사용 명령
    • ssh root@internal-web.int -t top
  • 실행하면 원격 서버에서 top 명령을 포함한 TTY를 받게 됨

외부 호스트도 로컬 포워딩 포트에 붙게 하는 -g

  • -g 옵션은 원격 호스트가 로컬 포워딩된 포트에 접속할 수 있게 함
  • 로컬 포트 포워딩 -L과 비슷하지만, 외부 머신에서도 해당 “로컬” 포트를 사용할 수 있게 하는 차이가 있음
  • 예시 상황
    • vuln-server.int에 셸 접근이 있음
    • vuln-server.int의 포트 2222로 들어오는 연결을 internal-web.int의 포트 22로 프록시하려 함
  • 사용 명령
    • ssh -N -f -g -L 2222:localhost:22 root@internal-web.int
  • 옵션 해석
    • -g: 원격 호스트가 로컬 포워딩 포트에 연결할 수 있게 함
    • -L: 로컬 포워딩을 지정함
  • vuln-server.int의 포트 2222로 SSH 접속했더라도 실제 셸은 internal-web.int에 있게 됨

SSH 콘솔 ~?와 세션 중 포워딩

  • SSH 콘솔은 원격 시스템과 직접 상호작용하지 않고 SSH 자체를 제어할 수 있는 숨겨진 기능임
  • 셸이 깨졌거나 SSH 세션 자체를 제어해야 할 때 유용함
  • 도움말 콘솔은 ~?로 열 수 있음
  • 유용한 옵션
    • ~.: 현재 SSH 세션을 종료함
    • ~C: SSH 콘솔을 열어 포워딩 옵션을 추가할 수 있음
  • 이미 일반 ssh 명령으로 vuln-server.int에 접속한 상태에서도 ~C를 누르고 -D 8080을 입력하면, 해당 세션을 동적 포워딩 세션처럼 사용할 수 있음
  • campfire.int에서 /etc/proxychains.conf가 포트 8080을 사용하도록 설정되어 있으면, 처음부터 ssh -D로 시작한 것처럼 proxychains를 활용할 수 있음

SSH 설정 파일 ~/.ssh/config

  • SSH 설정 파일~/.ssh/config에 있으며, 반복해서 입력하는 SSH 옵션을 저장해 시간을 줄여줌
  • SSH 접속 시 이 파일을 파싱하고, 접속 대상에 맞는 host 설정이 있으면 해당 옵션을 사용함
  • 명령줄 인자가 설정 파일보다 우선함
    • 예를 들어 설정 파일에서 internal-web.int 사용자를 root로 지정했더라도 ssh graham@internal-web.int를 실행하면 graham으로 로그인 시도함
  • 기본 설정 예시
# You can put comments with a `#` at the beginning of the line only.
host internal-web.int
    User root
    IdentityFile /home/smores/ssh_agent/internal-web-no-pw
    Port 2222
  • ssh internal-web.int 실행 시 처리 흐름
    • 명령줄의 internal-web.int~/.ssh/confighost internal-web.int를 매칭함
    • 매칭되면 명령줄에서 지정하지 않은 옵션을 설정 파일에서 가져옴
    • 매칭되지 않으면 명령줄에서 정의한 옵션만 사용함

자주 쓰는 SSH config 키워드

  • IdentityFile /path/to/private_key
    • 호스트에 사용할 개인 키를 지정함
    • ssh -i와 같은 역할을 함
  • ForwardAgent
  • ProxyJump root@internal-web.int
    • 트래픽을 프록시할 서버를 지정함
    • -J 옵션과 같은 역할을 함
    • 예시에서는 vuln-server.int 인증을 먼저 요구해 트래픽이 해당 호스트를 거친다는 점을 보여줌
  • Match
    • 조건에 따라 SSH config 키워드를 적용함
    • 예시에서는 export | grep PROXYME=TRUE 명령의 종료 코드가 0이면 Match 블록 아래의 ProxyJump를 사용함
    • PROXYME 환경 변수가 없으면 일반 host internal-web.int 블록만 사용함
    • export PROXYME=TRUE 설정 후 같은 ssh internal-web.int를 실행하면 vuln-server.int 인증을 거친 뒤 internal-web.int 셸을 받음
  • scp와 일부 SSH 기반 유틸리티도 보통 SSH config를 사용할 수 있음
    • 자동으로 사용하지 않는 환경에서는 -F ~/.ssh/config로 명시할 수 있음

공개키 복사 ssh-copy-id

  • ssh-copy-id 는 공개키를 서버에 빠르게 업로드하는 작은 유틸리티임
  • 사용 명령
    • ssh-copy-id -i internal-web root@internal-web.int
  • 옵션 해석
    • -i internal-web: 서버 인증에 사용할 개인 키 이름을 지정함
    • root@internal-web.int: 공개키를 업로드할 서버를 지정함

키 생성과 검사 ssh-keygen

  • ssh-keygen 은 개인키·공개키 쌍을 생성하는 유틸리티임
  • -b 옵션으로 더 큰 키 크기를 지정하는 방식이 일반적으로 권장됨
  • 기본 키 크기는 예시 환경에서 3072였음
  • 기본 알고리듬은 RSA지만, -t 플래그로 다른 알고리듬을 지정할 수 있음
    • 예: ssh-keygen -t ecdsa -b 521
  • 키의 지문과 바이트 크기는 다음 명령으로 확인할 수 있음
    • ssh-keygen -lf <file-name>

댓글과 토론

Hacker News 의견들
  • 여기에는 놀랄 만큼 단순한 지시어 하나가 빠져 있음: sshd_config에서 AuthorizedKeysCommand /usr/bin/php /etc/ssh/auth.php %u처럼 설정하고, 스크립트에서 GitHub의 https://github.com/{$user}.keys를 가져오면 됨
    물론 운영용 품질의 코드는 아니지만 핵심은 보여줌
    GitHub 조직/그룹 소속 여부를 확인한 뒤 사용자가 존재하고 nss-ato 같은 것으로 매핑돼 있으면 서버 로그인을 허용할 수 있음
    사람을 온보딩/오프보딩할 때 GitHub 그룹에 추가/삭제하는 것만으로 장비 접근을 부여하거나 회수할 수 있어 번거로움이 줄어듦

    • Amazon Linux도 비슷한 일을 하는데, 아마 운영용 품질이라는 이유로 훨씬 복잡함
      Amazon Linux 2 이하에서는 authorized_keys 파일의 개별 키 형식을 확인하려고 openssl 명령줄을 호출하는데, RSA로 하드코딩돼 있어서 OpenSSH 버전은 ed25519를 지원해도 Amazon Linux 2 호스트에는 ed25519로 인증할 수 없었음
      이론상으로는 멋진 기능¹을 가능하게 해 좋지만, 실제로는 그런 기능을 쓰지 않는 사람에게도 기본 기능을 깨뜨려 Amazon Linux를 덜 신뢰하게 됨
      처음 이 문제를 만났을 때 클라우드 우선 DevOps 동료가 소유한 머신에 SSH 접속하려던 중이었고, 직접 만질 수 없어 진단하기 어려웠음
      그는 AWS는 잘 알지만 Linux는 덜 알아 어디를 봐야 할지 몰랐고, 클라우드 플랫폼 소유자가 만든 배포판이니 “더 호환”될 거라 생각해 Amazon Linux를 골랐지만 여기서 “더 호환”은 사실 “어리석은 surprises가 더 많다”는 뜻이었음
      ¹ https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-...
    • 이런 걸 보면 네트워크 쪽에 있는 게 아쉬움
      “네트워크가 제대로 안 된다”가 업무 범위라서 이런 멋진 기능을 놓치게 됨
    • 이런 용도라면 대신 SSH 인증서를 쓰는 게 좋음
  • OpenSSH 설정 파서는 중복 지시어를 무시하고, 같은 지시어 중 첫 번째 것만 효과가 있다는 걸 모르는 사람이 많을 듯함
    보통 설정 파서나 규칙 엔진에서는 뒤에 나온 지시어가 앞의 것을 덮어쓰는 경우가 많아서 꽤 직관에 어긋남
    사소해 보일 수 있지만 /etc/ssh/sshd_config 같은 기본값을 바꿀 때 사람과 소프트웨어는 변경 사항을 파일이나 지시어 블록의 끝에 덧붙이는 경향이 있고, 그게 적용되리라 기대함
    보안 회사나 조직, 여러 SSH 배스천 제품도 이걸 틀리며, CIS Benchmarks 권고와 대부분의 서드파티 CIS 감사 도구도 우선순위를 고려하지 않거나 잘못 처리함
    OpenSSH 설정 지시어가 기대대로 정의됐는지 확인하려면 설정 파일을 직접 훑지 말고 sshd -Tssh -G로 파생된 내부 설정을 덤프해야 함

    • sudoers 파일도 같은 방식으로 동작함
      사용자 인증/인가 소프트웨어에서는 이런 방식이 바람직하다고 봄
      whatever.conf.d 디렉터리에 새 파일을 추가해도 덮어쓸 수 없는 설정을 만들기 쉽기 때문임
      메인 설정 파일에서 whatever.conf.d/*를 불러오기 전에 해당 설정을 정의하고, 그 파일에 특별한 보호를 걸어두면 됨
      누군가 통제를 우회하는 상황이 아니더라도, 전체 맥락을 모르는 신입이 새 파일을 추가하거나 어떤 패키지가 자기 서비스용 이상한 기본값을 설치해도 기준 설정이 우선권을 유지한다는 점에서 설정 관리에 유리함
      다른 맥락에서 반대 동작을 더 자주 보는 이유는 원하는 것이 “기준 설정”이 아니라, 사용자/개발자/관리자가 명시적으로 설정하지 않았을 때 쓰는 엄밀한 의미의 기본값이기 때문임
    • 일부 지시어는 중복 가능하다고 봄. 예를 들면 AllowUsers가 그렇다
      다만 어제 NixOS가 갑자기 병합 순서를 바꾸면서 내 파일의 AllowUsers가 다른 파일의 Match 아래로 내려가 잠겨버린 적이 있음
  • -L은 로컬 포워딩에서 로컬이 주소의 왼쪽에 있고, -R은 원격 포워딩에서 로컬 포트가 주소의 오른쪽에 있다고 기억하는 설명이 글에서 가장 중요하고 간결한 문장임
    처음부터 -L-R이 헷갈렸고, 어느 포트 인스턴스가 로컬인지가 L/R에 따라 바뀌는 건 꽤 성가심
    -L-R이 의도 방향, 즉 시작자와 응답자가 어디 있는지를 바꾼다는 건 이해하지만, port:address:port는 항상 local:binding:remote를 뜻하게 하고 -L/-R이 어느 쪽이 수신이고 어느 쪽이 송신인지 정하게 했어도 괜찮았을 듯함

    • -L은 바로 뒤에 나오는 번호의 로컬 포트에서 수신하고, -R은 바로 뒤에 나오는 번호의 원격 포트에서 수신한다고 기억하는 게 가장 쉬움
      나머지 host:port는 어디로 연결할지 알려주는 일반적인 형식일 뿐임
      SSH 터널을 통해 포트 포워딩을 하는 것이므로, 호스트는 수신 포트가 있는 쪽과 터널 반대편에서 접촉된다는 점도 자연스럽게 따라옴
  • SSH에서 덜 알려졌지만 유용한 기능으로 연결 다중화가 있음
    새 TCP 연결을 만들고 인증 절차를 다시 거치는 대신 기존 연결을 재사용하도록 할 수 있음
    프로토콜 자체에 채널 개념이 있고, 각 데이터 프레임에 서로 다른 스트림을 구분하는 메타데이터가 붙으며 이 기능이 그걸 사용함
    이후 세션에서는 전체 인증을 다시 하지 않아도 되는 게 큰 장점임
    원격에 tmux 등이 없고 여러 터미널 창으로 여러 패널을 쓰는 경우 특히 좋고, 인증에 암호구문이나 HSM 터치 같은 몇 초 걸리는 절차가 있으면 더 체감됨
    “연결 지속” 설정도 있어서 몇 대의 서버를 오갈 때마다 매번 인증하지 않아도 됨
    전반적으로 있으면 좋지만 인생을 바꿀 정도는 아닌 기능으로 봄
    일부 서버는 이 기능이 꺼져 있는데, 켜져 있을 때보다 꺼져 있을 때 부재가 더 잘 느껴짐
    더 보기: https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing

    • 클라이언트와 서버 사이에 지연 시간이 크면 차이가 매우 큼
      SSH는 왕복 횟수를 줄이도록 최적화된 TLS와 달리 꽤 수다스러운 프로토콜이고, 이 다중화 옵션이 거의 유일한 예외임
    • ProxyJump 배스천 호스트를 거칠 때 Ansible을 훨씬 쓰기 좋게 만들 수 있음
  • ~/.ssh/config에 호스트가 많다면 와일드카드를 지원하는 Include 지시어로 파일이 너무 지저분해지는 걸 막을 수 있음
    예를 들어 ~/.ssh/config에는 Include config.d/*.conf를 두고, ~/.ssh/config.d/work.confclient1.conf에 각각 host, hostname, user 설정을 나눠둘 수 있음

    • Host 지시어도 와일드카드를 지원함
      예를 들어 host *_work를 추가하고 host1_work 같은 모든 업무용 호스트에 공통인 설정을 넣을 수 있음
    • 추가 요령으로 IncludeHost/Match 지시어 안에 넣을 수 있음
      예를 들어 ~/.ssh/configHost proj1.*.corpInclude ~/.ssh/proj1.conf를 두면, 프로젝트별 매치를 위쪽 가까이에 둘 수 있고 검토할 때 수많은 개별 파일을 뒤질 필요도 줄어듦
  • 포워딩할 때는 거의 -f를 쓰지 않음
    어떤 포워딩이 아직 열려 있고 동작 중인지 파악하기 어렵게 만들어 발밑의 총이 될 수 있음
    -t는 멋진 요령이고, 몰랐던 기능임
    ~ 이스케이프 명령 목록에서 놓치기 쉬운 중요한 점은 중첩 세션 안에서도 이스케이프를 중첩할 수 있다는 것임
    어떤 이유로든 -J를 쓰지 않을 때 유용함
    목록은 유용한 것들과 잘 맞고, 추가로 배운 것도 몇 가지 있었음

    • -t는 훌륭함. 나는 ssh -t my-dev-vps 'tmux new-session -A -s main'처럼 써서 실행할 때마다 tmux 세션의 이전 위치로 바로 돌아감
    • 대상 서버에 여러 셸을 열어둔 경우에도 여전히 같은 문제가 있음
      프로세스 목록을 직접 뒤지는 것 말고 SSH가 합리적인 방식으로 포워딩 상태를 내보내주면 좋겠음
  • AF_UNIX 지원을 추가하는 현재 풀 리퀘스트가 있는데, 이게 들어가면 온갖 흥미로운 포워딩이 가능해질 것임
    임의의 로컬 프로세스를 통해 SSH 연결을 프록시하기 쉬워지고, 그 프로세스가 원격 끝까지 데이터를 전달하는 일을 마음대로 처리할 수 있기 때문임
    https://github.com/openssh/openssh-portable/pull/431

    • 관심 있는 건 AF_UNIX를 쓰는 -D지만, 모든 것이 AF_UNIX 위에서 동작 가능해지는 건 좋음
      약 1년 전부터 curlALL_PROXY 문법 socks5://localhost/path 또는 socks5h를 통해 AF_UNIX SOCKS를 사용할 수 있는 듯함
      Tor가 AF_UNIX SOCKS 프록시를 쓰기 때문에 추가된 것으로 보임
      표준 Unix 권한으로 네트워크 접근을 설정할 수 있으면 좋겠고, 개인적으로는 TCP/IP를 커널에서 완전히 밀어낼 수 있으면 더 좋겠음
  • SSH 콘솔을 처음 봤을 때 충격적이었음
    동료가 ~#을 보여줬는데, SEGA Genesis 게임에서 나올 법한 비밀 치트 메뉴를 발견한 느낌이었음

  • 왜 틸드인가? rlogin, rsh가 썼기 때문임
    rlogin, rsh가 틸드를 썼나? cu가 썼기 때문임
    cu인가? 모뎀이나 직렬 회선이 있으면 cu로 통신했고, Hayes 코드를 보내야 했는데 Hayes 코드의 이탈 시퀀스를 쓰면 모뎀으로 빠져버리므로 cu로 빠져나갈 별도 신호가 필요했음
    ^[가 아닌가? 그건 telnet이기 때문임
    그래서 telnet으로 호스트에 접속한 뒤 cu로 모뎀에 연결했다면, telnet으로 빠져나가지 않고 cu로 돌아갈 별도 이탈 문법이 필요했음
    결국 이탈 문법이 끝없이 쌓인 구조임
    그리고 실제로는 틸드가 아니라, 틸드임

    • CR, 틸드, . 순서라고 생각했는데, 그동안 잘못 쓰고 있었던 건가?
  • ssh-copy-id 섹션은 명령이 공개 키를 업로드한다고 설명하다가 갑자기 개인 키를 업로드한다고 바뀌는데, 오타로 보임
    또한 이 명령은 단순히 키를 업로드하는 게 아니라 ~/.ssh/authorized_keys에 덧붙이므로 훨씬 유용함
    마지막으로 ssh-keygen 섹션에서는 요즘은 읽어본 자료상 ecdsa보다 ed25519가 선호됨