오래간만에 리눅스 이야기로 찾아오네요. 하드웨어는 잘 몰라서 하드웨어 사이트에서 소프트웨어 이야기를 하는 나란 인간... 사랑스럽습니다. (네??)
언제나 그렇지만 아래의 내용은 제가 다니는 회사나 기타 등등 모든 저와 관련되어 있는 것과 관련이 없습니다. 오직 상상속의 일임을 알려드립니다.
===================================================
법적 책임의 부인(disclaimer): 아래에서는 특정 환경에서의 보안정책을 우회하여, 필요한 서비스를 제한적으로 구성하는 방법을 소개하고 있습니다. 특정 기업이나 조직의 보안정책은 나름의 근거와 이유로 책정된 것이며, 필수불가결한 경우 제한적으로 보안 정책 담당자나 망 관리자와의 협의와 함께, 적정한 책임 소재를 문서화하는 등, 적법한 절차를 거쳐 적용되어야 합니다. 해당 문서의 내용을 실행하여 일어나는 보안사고 및 실행자에게 주어지는 포괄적 피해에 대해서, 본 글의 게시자, 게시처 및 그에 관련된 이해관계자에게 책임이 존재하지 않음을 알려드립니다. 또한, 아래 시나리오에 적혀있는 상세 내용은 가상의 환경을 바탕으로 꾸며낸 것이며, 작성자와 일체의 관련이 없음을 알려드립니다.
===================================================
이런 서버가 있습니다:
(1) 인바운드는 방화벽을 거쳐서 전달. 포트 22(SSH), 포트 80(HTTP), 포트 443(HTTPS) 말고 나머지는 다 막혀있는 상황입니다. 따라서 서버로 들어갈 수 있는 포트는 저 3개가 전부입니다.
(2) 잘 알려진(well-known) 포트에 대한 아웃바운드가 모두 막혀 있습니다. paranoiac한 네트워크 관리자는 이 호스트가 탈취당해도 다른 곳에 피해를 끼치지 않게 만들기 위해서입니다.
따라서 이 서버는 접속해서 밖으로 나가야하는 모든 http https ftp ftps 요청은... 암튼 꿈도 꾸지 마십시오 휴먼.
그럼 OS 업데이트는 어떻게 합니까
그럼 프로그램 설치는 어떻게 합니까
...
뭐 그런거 이야기하면 한도 끝도 없으니 길게 이야기하지 않겠습니다. 이 글은 언제나처럼, 우리가 방법을 찾아가는 과정의 것입니다.
오늘의 문제는 다음과 같습니다:
한 회사가 있습니다. 이 회사의 조그만 서버 관리자인 A는 운영중인 웹 서버(w/apache)에 SSL을 공짜로 적용하기 위해 Let's encrypt를 사용하고 있었습니다.
... 그리고 위와 같이 왠만한 프로토콜에 대한 아웃바운드가 막혔지요.
그리고 A는 이 서버의 인증서를 갱신해야 집에 갈 수 있습니다. 왜냐하면 내일이 인증서 만료일이거든요.
네트워크 보안 담당자에게 어떻게 잠시라도 풀어줄 수 없냐고 물어봤지만, 돌아오는 대답은 항상 같습니다:
("응 안 돼 안 바꿔줘. 바꿀 생각 없어. 빨리 돌아가")
이런 상황에서, A는 오늘 집에 갈 수 있을까요?
문제 해결을 위해 사용할 수 있는 자원은 다음과 같습니다:
- SSH 서버가 설치되어 있어, 다른 컴퓨터에서 접속이 가능하고, 또 아래 문제의 서버에 접속할 수 있는 터미널. (이하 터미널로 통칭) 이 터미널은 다행히 외부 네트워크에 접근이 가능해서, 필요한 프로그램을 가져올 수 있습니다.
- 문제의 서버. (이하 서버로 통칭) 단, 해당 서버에는 기존에 표준적인 방법으로 설치되어 있는, Let's encrypt를 위한 certbot (또는 letsencryt-auto 실행파일)과 구동이 가능한 파이썬 가상환경이 준비되어 있습니다.
즉, 현재 상황은 대충 아래의 그림과 같군요. 아래의 그림을 또 들고오게 될 줄이야.. 아래 그림은 제가 기글에 가입하고 나서 글을 쓸 수 있는 두번째 날이 되던 때, 처음으로 올린 글에 들어간 그림입니다. 재활용이지요.
그림과 차이가 있다면, 위의 "서버"는 그림의 "서버 P", 그리고 "터미널"이 "서버 N"에 해당하는 정도일겁니다. 그리고 저 웹 서버가 HTTP/HTTPS까지도 아웃바운드를 할 수 없는 상황이구요.
1년이 지났음에도 여전히 돈도 없고 힘도 없고 가오도 없지만 머리가 아직은 남아 있다고 자부하는 관리자 A는 다음의 다섯 단계로 이 문제를 해결할 것입니다:
(1) 터미널 -> 서버, SSH 접속 및 Reverse Tunnel 개방
(2) reverse tunnel를 통한 서버 -> 터미널, 이 때 SOCKS5 over SSH를 개방
(3) letsencrypt 실행을 위해 python virtualenv 환경을 활성화
(4) pysocks 수동 설치, 그리고 pip를 사용한 letsencrypt 수동 업데이트
(5) letsencrypt 명령어를 사용한 인증서 renewal
이제 관리자 A가 작업한 내용을 하나씩 살펴보면, 여러분은 비슷한 상황이 닥쳤을 때 어떻게 할 수 있을지 알게 될 것입니다.
편의상, "(터미널) $" 로 시작하는 프롬프트가 터미널에서 실행될 명령어이고,
"(서버) $"로 시작하는 프롬프트가 서버에서 실행될 명령어입니다.
Step (1). 터미널 -> 서버, Reverse tunnel과 함께 SSH 접속
터미널에서 아래의 명령을 실행하여, 해당 서버에서 반대로 이 터미널로 접속할 수 있는 Reverse Tunnel을 만듭니다. 즉, 서버에서 다시 터미널로 SSH를 접속할 수 있게 만들어줍니다:
(터미널) $ ssh -p22 -R 30001:localhost:22 계정@서버
실행하고 적당한 인증 절차를 거치면, 서버의 셸이 떨어질 것입니다. 즉, 역으로 서버에서는 localhost:30001 포트에 접근하면, 터미널의 22번 포트로 접근할 수 있습니다. 만약 터미널의 SSH 서버가 다른 포트(예: 222)에 있다면, 22 대신 222를 적어주면 됩니다.
Step (2). reverse tunnel을 통한 서버 -> 터미널, SOCKS5 over SSH 개방
즉, 이 스텝에서는 반대로 서버에서 터미널로 ssh를 접속하면서, 동시에 socks5 프록시를 만드는 일을 하게 됩니다. 이제 서버에서 socks5를 통해 터미널에 연결된 외부망으로 나갈 수 있는 길을 확보하는 것이죠.
주의: 아래의 명령어를 실행하면, 더 이상 다른 액션을 취할 수 없기 때문에, 미리 screen이나 tmux를 사용하여 tty 멀티플렉싱을 할 수 있게 해 줘야 합니다. 그게 아니면, & 를 뒤에 붙여서 background로 실행시키고, 나중에 socks5 프록시를 제거할려면 수동으로 아래 프로세스를 죽여주면 됩니다.
아까 만든 localhost, 30001번 포트를 통해 터미널의 SSH 서버로 접근합니다. 접근하면서, 보너스로 1337 포트에 SOCKS5 프록시를 열게 될 겁니다.
(서버) $ ssh -D 1337 -q -C -p30001 -N 터미널계정@localhost
이제 서버의 로컬호스트(127.0.0.1), 1337 포트로 SOCKS5 프록시를 접근할 수 있습니다. 잘 되는지 확인해봅시다. 다시 서버의 셸에서 curl 명령을 실행하여 테스트해봅니다:
(서버) $ curl --socks5-hostname localhost:1337 https://www.google.com
뭐가 좌르륵 뜨면 정상적으로 google.com의 페이지를 가져왔다는 뜻이 되고, 정상적으로 socks5 프록시를 쓸 수 있게 되었음을 확인할 수 있습니다.
이제 ALL_PROXY 환경변수를 사용하면, 해당 셸에서, "왠만한" 프로그램에 대해서 socks5 proxy로 외부와 통신 할 수 있습니다:
bash 기준이라면,
(서버) $ export ALL_PROXY="socks5h://localhost:1337"
와 같이 실행하면 됩니다.
<참고사항: FAQ>
Q >> 왜 socks5://가 아니라 socks5h:// 인거죠? 그리고 왜 curl을 쓸 때 --socks5 대신 --socks5-hostname을 썼을까요?
A >> 기본적으로 socks5는 domain name을 ip로 변환하는 dns 질의가 생략되어 있습니다. 왠만한 경우에는 DNS 질의를 위한 포트 53번에 대한 아웃바운드를 막지 않습니다. (서비스만을 위한 웹 서버에서도 reverse dns resolving을 위해서 여전히 dns를 참조하는 경우가 많기 때문)
때문에, $ dig google.com @8.8.8.8 같은 명령을 내려서 DNS 질의가 잘 되는 환경이라면 socks5h://~ 대신 socks5://localhost:1337 이라고 해도 됩니다. curl을 호출할 때도, --socks5 localhost:1337 이라고 적어도 됩니다.
하지만 언제나 예외가 있지요. 그런 예외를 위해 간략하게 붙여놓겠습니다.
Q >> 만약 이게 막혀서 53번 아웃바운드도 막혀있는 상황일 경우인데다, 덧붙여서 socks5h:// URL을 이해하지 못하거나, ALL_PROXY 환경변수를 체크하지 않는 프로그램이라면 어떻게 해야 할까요?
A >> 최선의 정답은 socks5h wrapper인 proxychains 프로그램을 사용하는 것입니다. (물론 만능은 아닙니다! libc를 LD_PRELOAD_*와 같은 방식으로 후킹하는 것이기 때문에 시스템의 libc를 참조하지 않는 static binary에서는 동작하지 않고, 일부 irssi같이 child를 추가로 fork()하는 상황에서 irssi의 child, 즉 grandchild가 올바르게 resolve하지 못하는 문제를 예로 들 수 있겠습니다.)
proxychains를 설치하고, ubuntu/debian의 패키지 매니저인 apt에 적용하는 예시는 아래 보너스 Step으로 적어놓았습니다.
참고로,
남은 스텝 (3), (4), (5)를 실행하지 않고, 그냥 proxychains를 설정하고, ALL_PROXY 환경 변수를 unset 후 다음과 같이 실행해서 letsencrypt-auto를 실행해도 됩니다! 어쩌면 그게 더 편하실거에요!
실행 예시 -- (서버) $ sudo proxychains ./letsencrypt-auto renew --apache --dry-run
남은 스텝 (3), (4), (5)는 let's encrypt 스크립트가 동작하는 환경 상세의 일부분까지 설명하기 위해, proxychains를 사용하지 않고, ALL_PROXY 환경 변수 하에 SOCKS5 프록시를 사용하여 업데이트하는 방법을 소개하는 것입니다. proxychains를 설정하고 사용할 수 있다면, 아래 스텝 (3), (4), (5)를 쓰지 말아주세요.
Step (3). letsencrypt 실행을 위해 python virtualenv 환경을 활성화
언제나 let's encrypt 갱신을 위해 실행하던 letsencrypt-auto 스크립트는 sh 셸 스크립트로, 그 자신이 모든 것을 수행하는 것이 아니라, 시스템의 python 모듈과 충돌하지 않도록 python virtualenv 환경(이하 venv 환경으로 지칭)을 생성, 그 환경 아래에서 letsencrypt를 수행하게 만들어주는 wrapper 역할을 수행합니다.
즉, 실제 SSL 인증서 갱신과 관련되어 있는 certbot 역시 다 이 venv 환경 아래에서 구동됩니다.
따라서, 이 환경을 활성화 할 필요가 있습니다: 표준적인 방법으로 설치했다면, 이 venv 환경은 다음의 경로에서 억세스 가능합니다 (필요시, letsencrypt-auto 스크립트를 열어 30라인 근처, VENV_PATH 환경 변수를 참고해보면 됩니다):
/opt/eff.org/certbot/env
또는
~/.local/share/letsencrypt
아래 ~/.local/share/letsencrypt는 대개 /opt/eff.org/certbot/env를 참조하도록 소프트링크가 걸려 있을 것입니다. 암튼. letsencrypt 실행을 위해서는 이 환경의 venv를 활성화 시켜줄 필요가 있습니다. 서버의 셸은 여전히 bash라고 가정하겠습니다:
(서버) $ source /opt/eff.org/certbot/env/activate
활성화가 되면, 프롬프트에 다음과 같이 $ 앞에 (venv)가 더 붙을겁니다:
>> (서버) (venv) $ _
이와 같이 프롬프트에 (venv)가 포함되어 있으면, 이 venv 환경이 활성화 된 상태라고 이해하시면 되겠습니다. 이 상태를 종료하고 싶다면, deactivate라고 입력하시면 빠져나가집니다. 셸을 종료하지 않으셔도...
이제 다음 스텝으로 넘어갑시다.
Step (4). pysocks 수동 설치(최초 한번만), 그리고 pip를 사용한 letsencrypt 수동 업데이트
안타깝게도, 이 venv 환경에는 PySocks가 설치되어 있지 않을 것입니다. 그래서 socks5 프록시를 탈 수 없습니다. 저게 있어야 socks 프로토콜을 사용할 수 있거든요. 게다가 이 가상환경은 python 2.7을 사용하고 있기 때문에, 이 가상환경 안에서 pip로 한번에 PySocks를 설치할 수 없습니다. 따라서 PySocks 모듈을 터미널에서 다운로드(터미널에서, pip2 download pysocks라고 실행하면 됩니다.) 받아, 받은 .whl 파일을 서버로 복사해서 pip로 설치해야겠지요.
우리는 그게 좀 귀찮으니, pypi 사이트에서 이 venv 환경을 위한 pysocks, 즉 python 2.7용을 다운로드 받을 것입니다:
https://pypi.org/project/PySocks/#files <-- 이 페이지에서 최신 버전의 모듈 다운로드 URL을 얻을 수 있습니다:
제가 이 글을 쓰고 있는 시점에는 1.7.1 버전이네요.
이 경로를 따와서, 서버에서 바로 curl --socks5-hostname 옵션을 사용해서 받을 겁니다:
(서버) (venv) $ curl -o PySocks-1.7.1-py27-none-any.whl --socks5-hostname localhost:1337 'https://files.pythonhosted.org/packages/a2/4b/52123768624ae28d84c97515dd96c9958888e8c2d8f122074e31e2be878c/PySocks-1.7.1-py27-none-any.whl'
다운로드가 되었으니 이 가상환경에 반영합시다:
(서버) (venv) $ pip install PySocks-1.7.1-py27-none-any.whl
설치가 정상적으로 되면, 이제 letsencrypt 명령어가 ALL_PROXY 환경 변수로 세팅되어 있는 socks5h:// URL을 이해할 수 있을 것입니다.
이제 이 venv 환경에서 ALL_PROXY 환경변수를 세팅하고, 리뉴얼을 수행하면 됩니다.
(서버) (venv) $ export ALL_PROXY='socks5h://localhost:1337'
이제 letsencrypt를 최신 버전으로 업데이트 합시다:
(서버) (venv) $ pip install --upgrade letsencrypt
잘 실행이 되었다면, 이제 마지막 스텝으로 가서 SSL 인증서를 갱신하면 됩니다.
Step (5). letsencrypt 명령어를 사용한 인증서 renewal
저 venv 환경에서는 어느 경로에서건 상관없이 letsencrypt 명령어를 호출 할 수 있습니다. venv를 activate하면, /opt/eff.org/certbot/venv/bin이 PATH에 포함이 되기 때문이죠. 이제 리뉴얼을 수행하면 됩니다.
하기전에 --dry-run으로 업데이트가 잘 끝날지 확인만 먼저 하고
(서버) (venv) $ letsencrypt renew --dry-run
이상이 없으면 인증서를 업데이트 합시다.
(서버) (venv) $ letsencrypt renew
갱신이 무사히 실행되고, 관리자 A는 웹 페이지를 열어 SSL 인증서가 잘 설치되어 있는지, 갱신되어 있는지 확인했습니다. 모든 작업이 끝났습니다! (야 인마 터미널 접속 끊고 정리해야지--)
보너스 Step: proxychains를 사용한 debian/ubuntu 패키지 매니저(apt) 접근 활성화 방법
안타깝게도, apt는 ALL_PROXY 환경변수를 통해서 socks5 프록시를 사용할 수 없습니다. 이를 위해서, 나머지 통신 파트를 socks5로 해결할 수 있도록 proxychains라는 프로그램을 써야 합니다. 비슷한 역할을 해 주는 tsocks도 있지만, tsocks는 DNS resolving을 못하기 때문에, DNS까지 막혀있는 상황에서는 이것 역시 프록시를 타고 넘어갈 수 있게 proxychains를 쓸 것입니다:
당연히 서버에서는 이미 apt가 막혔을 것이니, debian/ubuntu인 터미널에서 프로그램을 미리 받아줍니다.
(터미널) $ apt download libproxychains3 proxychains
ubuntu 20.04라면 libproxychains4 proxychains로 받으셔야 할겁니다. 그 이전 버전이라면 저 명령어로 OK.
다운로드 된 2개의 deb 파일을, scp를 사용해서 서버로 복사해주고,
서버에서는 dpkg 명령어를 사용해 그 두 패키지를 설치하고, 그런 다음, /etc/proxychains.conf 파일을 열어 가장 아랫줄에 있는 socks4 ...로 되어 있는 예시 설정라인을 지우고, 우리가 사용할 socks5 프록시 정보를 기입합니다:
# 아랫줄로 내려가면 [ProxyList]가 보이실 겁니다.
[ProxyList]
# 나머지는 다 삭제해도 됩니다. 우리가 쓸 건 스텝(2)를 통해 개방한 socks5, localhost:1337 이니깐요.
socks5 127.0.0.1 1337
이제 저장이 잘 되었으면, 다음의 명령어로 수행하면 됩니다:
(서버) $ sudo proxychains apt-get update
(서버) $ sudo proxychains apt-get upgrade
만약 proxychains를 잘 세팅했다면, 이후에는 위의 스텝 (1), (2)만 실행해서 SOCKS5 프록시를 만들고, 그냥 $ proxychains ./letsencrypt-auto 명령어를 실행해도 됩니다.
....
이제 아무튼 관리자 A는 사랑하는 가족이 있는 집에 갈 수 있습니다. 그걸로 된거죠. 필요할 때 마다 저걸 기억해놨다가 실행 해 주면 됩니다.
- 끝 -
하지만 저는 아니네요. ... 여러분은 회사에 헌신하지 말고 오늘도 집에 빨리 가시길 기원합니다.
조만간 한 번 가상머신 만들어서 해봐야겠네요.. 좋은 정보 감사합니다.