SSH에 대한 극도로 상세한 가이드(하지만 나에게 유용한 것들만)
(grahamhelton.com)- 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.int는campfire.int와internal-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.int→vuln-server.int→internal-web.int→dns.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/config의host internal-web.int를 매칭함 - 매칭되면 명령줄에서 지정하지 않은 옵션을 설정 파일에서 가져옴
- 매칭되지 않으면 명령줄에서 정의한 옵션만 사용함
- 명령줄의
자주 쓰는 SSH config 키워드
IdentityFile /path/to/private_key- 호스트에 사용할 개인 키를 지정함
ssh -i와 같은 역할을 함
ForwardAgentssh -A와 같은 역할을 함- 에이전트 포워딩 사용 전 Zero Effort Private Key Compromise: Abusing SSH-Agent for Lateral Movement 확인이 필요함
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 인증서를 쓰는 게 좋음
- Amazon Linux도 비슷한 일을 하는데, 아마 운영용 품질이라는 이유로 훨씬 복잡함
-
OpenSSH 설정 파서는 중복 지시어를 무시하고, 같은 지시어 중 첫 번째 것만 효과가 있다는 걸 모르는 사람이 많을 듯함
보통 설정 파서나 규칙 엔진에서는 뒤에 나온 지시어가 앞의 것을 덮어쓰는 경우가 많아서 꽤 직관에 어긋남
사소해 보일 수 있지만/etc/ssh/sshd_config같은 기본값을 바꿀 때 사람과 소프트웨어는 변경 사항을 파일이나 지시어 블록의 끝에 덧붙이는 경향이 있고, 그게 적용되리라 기대함
보안 회사나 조직, 여러 SSH 배스천 제품도 이걸 틀리며, CIS Benchmarks 권고와 대부분의 서드파티 CIS 감사 도구도 우선순위를 고려하지 않거나 잘못 처리함
OpenSSH 설정 지시어가 기대대로 정의됐는지 확인하려면 설정 파일을 직접 훑지 말고sshd -T나ssh -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.conf나client1.conf에 각각host,hostname,user설정을 나눠둘 수 있음Host지시어도 와일드카드를 지원함
예를 들어host *_work를 추가하고host1_work같은 모든 업무용 호스트에 공통인 설정을 넣을 수 있음- 추가 요령으로
Include를Host/Match지시어 안에 넣을 수 있음
예를 들어~/.ssh/config에Host proj1.*.corp와Include ~/.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년 전부터curl은ALL_PROXY문법socks5://localhost/path또는socks5h를 통해AF_UNIXSOCKS를 사용할 수 있는 듯함
Tor가AF_UNIXSOCKS 프록시를 쓰기 때문에 추가된 것으로 보임
표준 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가 선호됨