
새 VPS를 첫 한 시간 안에 강화하기
공인 IPv4 주소는 첫 패킷을 내보낸 지 몇 분 만에, 여러분을 알지도 못하고 앞으로도 알 리 없는 기계들에게 탐색당합니다. 이는 좋은 소식입니다: 새 서버를 겨냥한 공격은 거의 전부가 범용적이며, 의도를 갖고 쓰는 한 시간이면 그 거의 전부를 막아낼 수 있습니다. 다만 같은 한 시간이라도 순서를 잘못 밟으면, 누구도 여러분을 대신해 로그인해 줄 수 없는 머신에서 스스로를 걸어 잠그게 됩니다.
결제가 확인되고 나서 약 47초 후 저희는 root 셸을 넘겨드리고, 그다음에는 의도적으로 손을 뗍니다. 저희는 에이전트를 설치하지 않고, 방화벽을 관리하지 않으며, 인증 정보의 사본도 보관하지 않습니다 — 저희가 쥐고 있는 키는 저희가 제출을 강요당할 수 있는 키이며, 이 원칙은 플랫폼 전체를 관통합니다. 결론은 단순하며 분명히 말해 둘 가치가 있습니다: 여러분 서버의 보안 수준은 첫 한 시간 동안 여러분이 만들어 놓은 상태, 바로 그것입니다.
이어지는 내용은 바로 그 한 시간을, 저희가 Debian 13과 Ubuntu 24.04에서 직접 실행하는 순서 그대로 담은 것입니다. 두 가지 설정이 대부분의 일을 해냅니다. 이 페이지의 나머지 부분이 존재하는 이유는, 겉보기에는 올바르고 모든 튜토리얼의 마지막 점검도 통과하면서 조용히 잘못되어 있는 세 가지 때문입니다: 소리 없이 무시당하는 드롭인 SSH 설정, 소켓으로 활성화되는 sshd가 무시해 버리는 포트 설정, 그리고 방화벽 밑으로 포트를 뚫어 버리는 컨테이너 런타임입니다. 이 셋 모두 다른 모든 것을 제대로 해낸 사람마저 물어뜯은 적이 있습니다.
실제로 문을 두드리고 있는 것은 무엇인가
한 시간 동안 온라인 상태였던 서버에서 journalctl -u ssh를 지켜보면, 처음 볼 때는 그 양이 걱정스러울 정도입니다. 하지만 걱정할 일이 아닙니다. 여러분이 보고 있는 것은 인터넷의 배경 잡음입니다: 학술적이거나 상업적이거나 범죄적인 소수의 스캔 작업들이 IPv4 공간 전체를 끊임없이 나열하고, 그 결과를 자격 증명을 추측하는 봇에게 넘겨줄 뿐입니다. 여러분의 주소가 닿은 이유는 그저 숫자 순서상 존재하기 때문이며, 여러분이 무엇을 하든 몇 시간 뒤면 또 닿게 됩니다.
이 사실은 문제의 형태를 유용한 방식으로 바꿔 놓습니다. 여러분은 여러분을 콕 집어 골라 상황에 맞춰 전략을 바꾸는 적을 상대하는 것이 아니라, 정해진 레퍼토리만 반복하는 스크립트를 상대하고 있는 것입니다 — root, admin, ubuntu, test, git, oracle, postgres, 그리고 유출 데이터에 등장한 2만 개의 비밀번호가 전부입니다. 이 스크립트에는 인내심도, 창의성도, 첫 시도에 응답하지 않는 머신에 대한 관심도 없습니다. 비밀번호 인증을 끄는 것은 이 공격자의 속도를 늦추는 정도가 아니라, 아예 판 자체에서 제거해 버립니다.
알아 둘 만한 세부 사항이 두 가지 있습니다. 첫째, IPv6는 눈에 띄게 조용한데, /64 전체를 훑기란 불가능하기 때문입니다 — 하지만 AAAA 레코드가 공개되거나, 머신 주소가 메일 헤더나 인증서 투명성 로그에 등장하는 순간 그 고요함은 끝납니다. IPv6를 숨을 곳으로 여기지 말고, 그저 좀 더 작은 건초 더미로 여기십시오. 둘째, IPv4 주소에는 과거가 있습니다. 여러분 이전에 그 주소를 쓰던 임차인이 있었고, 그 임차인이 메일 서버를 부실하게 운영했거나 블랙리스트에 오른 무언가를 호스팅했다면, 그 평판이 사그라들 때까지 여러분이 물려받게 됩니다. 메일이 중요하다면, 그 주소를 기반으로 무언가를 쌓아 올리기 전에 흔히 쓰이는 블록리스트에서 먼저 확인하십시오 — 5분이면 끝나는 이 확인이 2주에 걸친 전달률 디버깅을 막아 줍니다.
작업의 90%를 해내는 두 가지 설정
소규모 서버가 실제로 뚫리는 사고는 거의 전부 둘 중 한 곳에서 시작됩니다: 추측 가능한 비밀번호, 또는 열려 있어서는 안 될 서비스가 열려 있었던 경우입니다. 이에 대응하는 조치는 각각 PasswordAuthentication no와 기본 정책이 drop인 방화벽입니다. 화려할 것은 없고, 둘을 합쳐도 15분이면 끝나지만, 이 페이지의 다른 모든 조치를 합친 것보다 값어치가 큽니다.
그 이유는 통계적이라기보다 구조적입니다. 둘 다 기본값이 닫힘입니다 — 실패하더라도 안전한 쪽으로 실패합니다. 키 전용 sshd는 아무리 많은 시도가 들어와도 무차별 대입 공격으로 뚫을 수 없는데, 비밀번호를 받아들이는 코드 경로 자체가 존재하지 않기 때문입니다. 기본 차단 방화벽은 아직 설치하지도 않은 서비스까지 보호해 주는데, 석 달 뒤에 추가하고 나서 localhost에 바인딩하는 것을 깜빡할 데이터베이스도 여기에 포함됩니다. 강화 체크리스트에 나오는 나머지 항목은 전부 나쁜 것들의 목록화일 뿐입니다: 꺼야 할 구체적인 항목들의 목록이며, 그 완결성은 딱 그 목록만큼만 완전합니다.
그러니 이 페이지에서 딱 한 섹션만 읽고 탭을 닫을 생각이라면 이 섹션을 읽고, 아래 2단계부터 5단계까지만 실행하십시오. 그것만으로도 그 한 시간은 충분히 값어치를 합니다. 나머지도 분명 쓸모가 있지만, 어디까지나 부차적입니다.
지금 SSH 설정은 어디에 있는가, 그리고 그 안의 함정
Debian 13과 Ubuntu 24.04에서 /etc/ssh/sshd_config는 Include /etc/ssh/sshd_config.d/*.conf 줄로 시작하며, 클라우드 이미지는 그 디렉터리 안에 이미 PasswordAuthentication을 설정해 둔 파일 — 대개 50-cloud-init.conf — 을 함께 넣어 둡니다. 메인 파일을 직접 열어 맨 아래에 자신만의 지시어를 덧붙이는 방식은 자연스러워 보이지만, 결과적으로는 겉으로 말하는 것과 다르게 동작하는 설정이 만들어집니다.
거의 모든 사람이 놀라는 sshd의 규칙은 바로 이것입니다: 각 키워드에 대해 처음 얻어진 값이 우선한다는 점입니다. 이는 여러분이 써 본 다른 거의 모든 설정 병합 시스템과 정반대입니다. Include는 메인 파일 상단 가까이에 있으므로, 드롭인이 sshd_config 본문보다 먼저 읽히며 — 드롭인들 사이에서는 알파벳 순서가 결정합니다. 10-hardening.conf라는 이름의 파일은 50-cloud-init.conf를 이깁니다. 60-hardening.conf라는 이름의 파일은 조용히 그것에 밀리며, 성공했다고 알려 주는 재시작 결과만 봐서는 이 사실을 알 수 없습니다.
그래서 방금 작성한 파일을 절대 그대로 믿어서는 안 됩니다. 데몬에게 실제로 무엇을 읽었는지 직접 물어보십시오:
sshd -t # syntax check — silence means valid
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive|allowusers|port) 'sshd -T는 모든 include와 override, 기본값이 해석되고 난 뒤의 실제 유효 설정을 출력합니다. 그 출력이 passwordauthentication yes라고 말한다면, 디스크의 어떤 파일이 뭐라고 주장하든 상관없이 비밀번호 인증은 켜져 있는 것입니다. 그 외에는 어떤 것도 검증이라 할 수 없습니다.
두 번째 함정도 같은 부류입니다. Ubuntu 24.04는 소켓 활성화 방식으로 sshd를 시작합니다: ssh.socket이 대기 포트를 소유하며, sshd_config의 Port는 완전히 무시됩니다. systemctl is-enabled ssh.socket으로 확인하십시오. 이것이 활성화되어 있고 다른 포트를 쓰고 싶다면, sshd_config가 아니라 systemctl edit ssh.socket과 ListenStream= 오버라이드로 바꾸십시오 — sshd_config에 적으면 설정은 멀쩡해 보이지만 아무 효과도 없습니다.
포트 22 옮기기: 보여주기용이지만 값싼 보여주기
이 항목에 대해서는 솔직해집시다. 인터넷은 그렇지 않으니까요. sshd를 2222번이나 47000번 포트로 옮긴다고 해서 유능한 공격자를 막을 수 있는 것은 전혀 없습니다. 호스트 하나를 전체 TCP 스캔하는 데는 몇 초면 충분하고, 서비스는 배너로 자신을 알리며, 여러분을 콕 집어 공격하기로 마음먹은 사람이라면 커피 한 잔 다 마시기도 전에 그 포트를 찾아낼 것입니다.
실제로 이 조치가 해 주는 일은 인증 로그를 95%가량 줄여 주는 것이며, 이는 실제로 가치가 있습니다: 대충 훑어보는 로그와 실제로 읽는 로그의 차이를 만들어 내기 때문입니다. 파묻혀 있는 신호보다 눈에 보이는 신호가 낫습니다. 무엇이든 모니터링을 하고 있다면, 조용한 auth.log는 이상 징후를 눈에 띄게 만드는 가장 값싼 방법입니다.
비용은 작지만 실재하며, 그래서 저희는 이를 권장이 아니라 선택 사항이라고 부릅니다. 여러분은 8개월 뒤에 돌아왔을 때 그 포트를 잊어버릴 것입니다. 1024번 이상의 비표준 포트는, 원칙적으로 sshd가 대기하고 있지 않은 순간에는 권한 없는 프로세스가 가로챌 수도 있습니다. 클라이언트 네트워크의 제한적인 아웃바운드 방화벽은 흔치 않은 포트를 막아 버리므로, 사무실이나 호텔에서 자신의 서버에 접속하지 못하는 상황이 이따금 생길 것입니다. 그리고 소켓 활성화 시스템에서는 앞 섹션에서 설명한 대로 올바른 위치에서 바꿔야 합니다. 조용한 로그가 여러분에게 중요하다면 하십시오. 다만 이것을 해 놓고 안전하다고 느끼지는 마시고, 키 전용 설정 대신 이것만 하는 일은 절대 없어야 합니다.
기본 차단, 그리고 저희가 실제로 사용하는 규칙셋
무엇을 막을지 나열하는 방화벽은 서류철에 불과합니다. 무엇을 허용할지 나열하는 방화벽이라야 보안 통제 수단입니다. 이 구분이 전부라고 해도 과언이 아닌데, 다음 달에 설치할 서비스, 어느 금요일에 열어 둔 디버그 포트, 스스로를 노출시키기로 한 컨테이너까지 커버하는 것은 오직 후자뿐이기 때문입니다.
저희 플랫폼에는 독립적인 두 개의 계층이 있습니다. 엣지 필터는 선택 사항으로, 패널에서 서버별로 설정하며 하이퍼바이저 단에서 강제되므로 차단된 트래픽은 아예 게스트에 도달하지도 않습니다 — 커널이 사이클을 쓰기 전에 적용하고 싶은 4계층 규칙이나, 작업하는 동안 침해된 게스트에 외부에서 접근하지 못하게 막는 데 유용합니다. 게스트 방화벽은 전적으로 여러분의 몫입니다: nftables든 iptables든 pf든, 이미지가 제공하는 무엇이든 상관없습니다. 저희는 이것에 절대 손대지 않습니다. 머신이 중요하다면 둘 다 쓰시고, 최소한 후자는 항상 쓰십시오.
5단계의 규칙셋 중 두 가지는 설명이 필요합니다. 복사해서 붙여넣은 규칙셋 대부분이 여기서 틀리기 때문입니다. ICMP를 전부 차단하지 마십시오. 깔끔해 보이지만 경로 MTU 발견(path-MTU discovery)을 망가뜨리며, 이는 최악의 부류에 속하는 버그를 만들어 냅니다: 작은 요청은 되는데 큰 응답은 멈춰 버리고, 로그 어디에도 방화벽 얘기는 나오지 않습니다. 최소한 destination-unreachable, time-exceeded, parameter-problem은 허용하십시오. 목록을 정확히 알지 못한다면 ICMPv6를 타입별로 필터링하지 마십시오. IPv6는 이웃 탐색과 라우터 광고를 ICMPv6에 의존하므로, 이를 폭넓게 차단하면 라우팅 문제처럼 보이는 방식으로 IPv6 연결이 죽어 버립니다. 단일 호스트에서 ICMPv6를 전부 허용하는 것은 합리적인 타협이며, 아래 규칙셋도 정확히 그렇게 되어 있습니다.
flush ruleset을 실행하기 전에 한 가지 경고해 둘 것이 있습니다: Docker가 설치되어 있다면 이 줄은 Docker가 작성한 NAT 및 filter 규칙까지 지워 버리며, systemctl restart docker로 이를 되돌려 놓기 전까지 컨테이너 네트워킹은 멈춰 있습니다. 규칙셋을 먼저 적용하고, 그다음 Docker를 재시작하며, 컨테이너 포트를 단 하나라도 게시하기 전에 다음 섹션을 반드시 읽으십시오.
열려 있는지 몰랐던 포트들
머신에게 무엇을 대기하고 있는지 물어보십시오. 여러분이 설치했다고 생각하는 것 말고, 지금 실제로 바인딩되어 있는 것 말입니다:
ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'이 필터를 통과하고 살아남은 모든 것은 머신 자신이 아닌 다른 어딘가에서 접근 가능합니다. 기본 이미지에서 흔히 발견되는 것은 111번 포트의 rpcbind(여러분이 실행하는 그 무엇도 이를 필요로 하지 않습니다), 기본 설치에서 남은 exim4 리스너, 그리고 루프백에서는 무해하지만 그렇지 않으면 오픈 리졸버가 되어 버리는 systemd-resolved 스텁입니다. 위험한 것들은 나중에, 여러분이 직접 설치한 소프트웨어와 함께 찾아옵니다: 튜토리얼이 listen_addresses를 수정하라고 해서 0.0.0.0:5432에 올라간 PostgreSQL, 대부분의 역사에서 기본값이 비밀번호 없음이었기 때문에 비밀번호 없이 떠 있는 Redis, Elasticsearch 노드, Prometheus exporter, Jupyter 노트북 등입니다. 이들 하나하나가 실제 침해 사고의 첫걸음이 된 사례는 셀 수 없이 많습니다.
이제 신중한 사람들조차 걸려드는 함정을 소개합니다. Docker는 여러분의 방화벽에 허락을 구하지 않습니다. -p 5432:5432라고 쓰면, Docker는 nat 테이블의 PREROUTING 체인에 DNAT 규칙을 삽입하며, 커널은 이 체인을 input 체인이 패킷을 보기 전에 평가합니다. 그러고 나면 트래픽은 곧장 컨테이너로 전달됩니다. 여러분의 기본 차단 input 정책은 아예 참조되지 않고, ufw status는 그 포트가 닫혀 있다고 보여 주지만, 데이터베이스는 이미 공개 인터넷에 떠 있는 상태입니다. 이는 문서에도 명시된 Docker의 동작 방식이며, 10년 넘게 이래 왔고, 그동안 엄청나게 많은 데이터베이스를 노출시켜 왔습니다.
해결책은 문자열 하나이며, 몸에 익혀 둘 만한 습관입니다:
# wrong — reachable from the entire internet, whatever your firewall says
docker run -p 5432:5432 postgres
# right — bound to loopback, reachable only through an SSH tunnel or a proxy
docker run -p 127.0.0.1:5432:5432 postgres마지막으로, 외부의 시각도 확인하십시오. 서버에서 실행한 포트 스캔은 커널이 무엇을 생각하는지 알려줄 뿐이고, 다른 곳에서 실행한 스캔이라야 세상이 실제로 무엇을 보는지, 즉 정말로 중요한 숫자를 알려줍니다. 머신이 아니라 여러분의 노트북에서 nmap -Pn -p- <your-ip>를 실행해, 그 결과를 원래 예상했던 목록과 비교해 보십시오.
자동 업데이트: 켜고 제대로 실천하기
소규모 서버를 뚫리게 만드는 것은 제로데이가 아닙니다. CVE가 작동하는 개념 증명(PoC)과 함께 공개된 시점부터 여러분이 어쩌다 다시 로그인하게 되는 시점까지, 그 사이의 네 주가 문제입니다. 공격 도구는 새로 나온 원격 취약점을 며칠 만에 흡수하며, “때가 되면 하겠지”라며 미뤄 둔 머신은 그 기간 내내 고스란히 노출되어 있습니다. 그리고 그 기간이 실제로 얼마나 긴지는 정직한 운영자라면 누구나 알고 있습니다.
자동 업데이트에 대한 반론은 무언가 망가질지 모른다는 두려움이며, 이는 진지하게 받아들일 가치가 있습니다 — 그런 다음 범위를 좁히면 됩니다. 자동화를 배포판의 보안 저장소로만 한정하십시오. 이곳에서는 메인테이너들이 새로운 업스트림 릴리스를 내놓는 대신 패키징된 버전에 수정 사항만 백포트합니다. Debian의 openssl 보안 업데이트는 여러분이 이미 실행 중인 것과 같은 버전을 패치한 빌드일 뿐입니다. 동작이 바뀔 위험은 원격 취약점을 한 달간 열어 두는 위험에 비하면 훨씬 작습니다. 기능 업그레이드는 원래 있어야 할 자리인 수동 영역에 그대로 남겨 두십시오.
사람들이 흔히 빠뜨리는 부분은 재부팅입니다. 디스크에 있는 패치된 libssl은 시작 시점에 이전 버전을 이미 메모리에 매핑해 둔 프로세스에는 아무 도움이 되지 않으며, 커널 업데이트도 그것으로 부팅하기 전까지는 전혀 효과가 없습니다. 여러분이 직접 고른 시간대에 무인 재부팅을 허용하거나, needrestart를 설치해 어떤 서비스가 삭제된 라이브러리를 붙든 채 돌아가고 있는지 알려주게 하십시오 — 둘 중 하나는 반드시 하십시오. uptime이 340일을 가리키는 와중에 “자동 업데이트는 켜 놨다”고 말하는 것은 편안한 착각일 뿐이며, 저희가 가장 흔하게 보는 착각이기도 합니다.
fail2ban, CrowdSec, 아니면 아무것도 쓰지 않기
fail2ban은 로그를 읽어 특정 주소에서 반복적으로 실패가 발생하면 그 주소를 한동안 차단합니다. CrowdSec도 같은 일을 하면서 그 판정을 참여자 네트워크 전체와 공유하므로, 다른 곳에서 말썽을 부린 주소를 여러분에게 닿기도 전에 차단할 수 있습니다. 둘 다 좋은 소프트웨어입니다. 다만 키 전용 SSH 서버에서는 둘 다 대부분의 사람들이 생각하는 그 일을 해 주지 않습니다.
PasswordAuthentication이 no인 순간부터 SSH 무차별 대입 공격은 성공할 수 없습니다. “성공하기 어렵다”가 아니라, 그럴 수 있는 코드 경로 자체가 없습니다. 따라서 다섯 번 실패 후 주소를 차단하는 것은 아무것도 막아 주지 못합니다. 로그 양과 아주 미미한 CPU 사용량을 줄여 줄 뿐입니다. 이는 실질적인 이점이지만 보안상의 이점은 아니며, 그 대가도 공짜가 아닙니다: 엉뚱한 로그를 감시하도록 잘못 짠 jail은 공격자를 차단한 것보다 더 많은 관리자를 자기 서버에서 스스로 몰아낸 전적이 있습니다.
이런 도구들이 진짜로 제 몫을 하는 곳은 한 계층 위, 즉 여러분이 실제로 노출하는 서비스들입니다: 로그인 폼, WordPress 관리자 페이지, 속도 제한이 있는 API, SMTP AUTH를 쓰는 메일 서버 같은 곳입니다. 이런 곳은 비밀번호를 실제로 받아들이고, 실제로 무차별 대입 공격의 대상이 될 수 있으므로, 차단 목록이야말로 정확히 알맞은 통제 수단입니다. 그래서 저희의 결론은 좁고 구체적입니다. SSH용 jail은 건너뛰십시오. CrowdSec이나 fail2ban은 비밀번호 입력란이 있는 대상 앞에 두십시오. 그리고 무엇을 선택하든, 여러분 자신의 관리용 주소부터 화이트리스트에 올려 두십시오 — ignoreip는 그렇게 해 두지 않으면 두고두고 나쁜 의미로 기억하게 될 어느 저녁을 위해 존재하는 옵션입니다.
머신이 변화를 스스로 알리게 만들기
예방은 한 시간 안에 할 수 있는 일입니다. 탐지는 그 한 시간으로 충분하지 않았을 때 이를 알려 주는 일입니다. 거창할 필요는 없으며, 서버 한 대에서는 값싼 세 가지 조치만으로도 대부분의 영역을 커버할 수 있습니다.
로그를 보존하십시오. 많은 이미지에서 저널은 /run에 저장되어 재부팅하면 사라지는데, 이는 사고 기록이 바로 그 뒤에 이어지는 재부팅과 함께 사라진다는 뜻입니다. /var/log/journal을 만들어 두는 것은 한 줄짜리 조치이면서 이 섹션에서 가장 값어치가 큰 일입니다.
로그인을 알림받으십시오. 세션이 시작될 때마다 logger를 실행하는 한 줄을 /etc/ssh/sshrc에 넣어 두면 비용은 전혀 들지 않으면서, sshd 자체의 잡다한 로그와 분리된 깔끔하고 grep하기 쉬운 기록을 얻게 됩니다. 다만 한 가지 함정이 있습니다: 사용자에게 ~/.ssh/rc가 있으면 sshd는 /etc/ssh/sshrc 대신 그것을 실행하므로, 정작 커스텀 닷파일을 가지고 있을 가능성이 가장 큰 계정에서 시스템 전역 파일이 건너뛰어지고 맙니다. 가려질 수 없는 훅이 필요하다면 pam_exec를 대신 사용하십시오.
첫날의 파일시스템이 어떤 모습이었는지 알아 두십시오. AIDE는 바이너리, 라이브러리, 유닛 파일의 해시를 기록해 두고, 이후 무엇이 바뀌었는지 알려줍니다. 근본적이면서도 흔히 무시되는 함정은 이것입니다: 감시 대상 머신에 저장된 데이터베이스는 그 머신을 침해한 사람이 얼마든지 재생성할 수 있으며, 그러면 이후 검사는 영원히 “변경 없음”이라고만 보고하게 됩니다. 데이터베이스를 박스 밖으로 복사해 두거나, 최소한 그 해시만이라도 다른 곳에 기록해 두어야 이 도구에 들인 20분이 값어치를 합니다. 그대로 놓아두면 그저 마음의 위안일 뿐입니다.
일반 원칙 하나는 따로 짚어 둘 가치가 있습니다: 신뢰할 수 없는 로그란 곧 침해당한 머신 위에 있는 로그입니다. 저널이든 무결성 데이터베이스든 백업이든, 여러분이 진짜로 의지하려는 것이라면 같은 공격자가 통제하지 못하는 어딘가에 사본을 두어야 합니다. 다른 관할권에 있는 작은 서버 한 대면 이에 대한 정당한 답이 되며, 5달러짜리 서버 하나로도 충분합니다.
이 조치가 하지 못하는 것
지금까지의 모든 내용은 경계선을 다지는 작업이며, 잠긴 현관문을 금고로 착각하지 않도록 그 경계를 정확히 짚어 둘 가치가 있습니다.
실행 중인 머신의 데이터는 보호하지 못합니다. 강화된 커널과 닫힌 방화벽은 하이퍼바이저 접근 권한을 가진 사람에게는, 그리고 머신이 켜져 있는 동안의 RAM 내용에는 아무 의미가 없습니다. 서버가 꺼져 있거나 압수되거나 폐기되었을 때의 디스크가 걱정이라면, 그것은 디스크 암호화의 영역이며 실패 양상도 전혀 다른 별개의 절차입니다 — VPS 디스크를 LUKS로 암호화하기를 참고하십시오.
여러분을 익명으로 만들어 주지 않습니다. 서버를 흠잡을 데 없이 강화해 놓고도 WHOIS 기록, 재사용한 SSH 키, 분석 태그, 중간 경유지 없이 집에서 바로 접속하는 SSH 클라이언트, 또는 두 신원을 서로 엮어 버리는 인증서를 통해 소유주가 누구인지 알려 버릴 수 있습니다. Monero로 결제해 놓고 자신의 주소에서 로그인하면 그 결제는 무효가 됩니다. 이 실패 양상은 별도의 페이지에서 다룹니다: 익명성을 무너뜨리는 실수들.
애플리케이션을 고쳐 주지는 않습니다. SQL 인젝션, 인증되지 않은 관리자 엔드포인트, 백도어가 심긴 의존성은 여러분의 sshd 설정 따위는 신경 쓰지 않습니다. 잘 운영되던 서버가 뚫리는 사고의 대부분은 여러분이 직접 작성했거나 설치한 서비스를 위해 의도적으로, 일부러 열어 둔 바로 그 포트를 통해 들어옵니다.
백업이 아닙니다. 랜섬웨어든, 비어 있던 변수와 함께 실행된 rm -rf든, 실패한 업그레이드든 결말은 똑같습니다. 사본을 만들어 다른 곳에 두고, 필요해지기 전에 한 번 복원해 보십시오 — 다른 호스트로의 이전을 무사히 넘기게 해 주는 것과 같은 습관이 최악의 화요일도 무사히 넘기게 해 줍니다.
이 모든 것은 그 한 시간을 들이지 말라는 이야기가 아닙니다. 그 한 시간이 정확히 무엇을 사 주었는지 알고 있으라는 이야기입니다.
- 비밀번호가 아니라 키로 배포하기
키 쌍은 여러분 자신의 머신에서 생성하고, 공개 키 부분만 배포 폼에 붙여 넣으십시오. 이미지가 이를 설치해 주므로 잃어버릴 root 비밀번호 자체가 아예 없어집니다.
cloud-init표시가 있는 이미지로 배포한다면, 첫 한 시간의 설정 전체를 user-data로 — 최대 64 KiB까지 — 대신 전달할 수 있으며, 그러면 머신은 이미 강화된 상태로 부팅됩니다.# on your laptop, not on the server ssh-keygen -t ed25519 -a 100 -C "$(whoami)@laptop-cvh" cat ~/.ssh/id_ed25519.pub # paste this into the deploy form여러분의 툴체인이 거부하지 않는 한 ed25519를 사용하고, 만약 거부한다면 4096비트 RSA로도 충분합니다. 개인 키는 패스프레이즈로 보호하고 에이전트에 로드해 두십시오. 노트북에 패스프레이즈 없이 놓인 키 파일은 모니터에 비밀번호를 써 붙여 놓은 것과 다를 바 없습니다. 같은 키 세트가 복구 모드에도 함께 주입되며, 이것이 바로 4단계를 복구 가능하게 만들어 주는 이유입니다.
- 무엇이든 건드리기 전에 두 번째 세션부터 열기
이는 선택 사항이 아니며 과한 걱정도 아닙니다. 지금부터 여러분은 자신이 접속해 있는 그 데몬 자체와, 그 접속을 가능하게 해 주는 방화벽을 변경하게 됩니다. 한 세션은 머신으로 돌아올 밧줄로 삼아 열어 둔 채 그대로 두고, 모든 변경은 다른 세션에서 진행하십시오. 변경이 잘못되더라도 열어 둔 세션은 여전히 인증된 상태이므로 이를 되돌릴 수 있지만, 새 접속은 거부당할 것입니다.
# terminal A — the rope. Log in and leave it alone. ssh -i ~/.ssh/id_ed25519 root@<server-ip> # terminal B — where every command below runs. ssh -i ~/.ssh/id_ed25519 root@<server-ip>각 변경 사항은 지금 작업 중인 세션을 재접속해서가 아니라, 세 번째 접속을 새로 열어서 테스트하십시오. 세 번째 접속이 실패하더라도 여러분에게는 여전히 작동하는 셸 두 개와 위기가 아닌 하나의 문제만 남아 있게 됩니다.
- 실제로 사용할 계정 만들기
SSH로 접속하는 root 계정은 딱 한 시간 동안만 편리합니다. 그 이후로는, 이름이 있는 계정에 sudo를 부여하는 편이 감사 추적을 남겨 주고, 잘못 입력한 명령이 모든 권한을 가진 채로 실행되는 사고를 막아 주며, 머신 전체를 비활성화하지 않고도 침해된 계정만 비활성화할 수 있게 해 줍니다.
adduser --disabled-password --gecos "" deploy install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys chown deploy:deploy /home/deploy/.ssh/authorized_keys chmod 0600 /home/deploy/.ssh/authorized_keys usermod -aG sudo deploy # a long random password, stored in your manager, used only by sudo passwd deploy이 비밀번호는 반드시 설정해 두십시오. 비밀번호가 한 번도 설정되지 않은 sudo 계정은 sudo를 쓸 수 없으며, root 로그인을 비활성화한 뒤에야 이 사실을 알아차리는 것이야말로 파일은 전부 올바른데도 스스로를 걸어 잠그는 전형적인 방식입니다. 다음으로 넘어가기 전에 확인하십시오: 새 터미널에서
ssh deploy@<server-ip>를 실행한 다음sudo -v를 실행합니다. 둘 다 되어야 합니다. - SSH 드롭인을 작성한 뒤 데몬에게 무엇을 읽었는지 확인하기
앞서 설명한 정렬 규칙에 따라, 먼저 정렬되는 번호가 붙은 드롭인이 클라우드 이미지가 제공한 어떤 파일이든 이깁니다. 파일을 작성하고, 문법을 검증하고, 유효 설정을 다시 읽어 확인한 다음에야 reload하십시오.
cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF' PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes AuthenticationMethods publickey PermitEmptyPasswords no AllowUsers deploy MaxAuthTries 3 LoginGraceTime 20 X11Forwarding no AllowAgentForwarding no AllowTcpForwarding no # remove this line if you use ssh -L / -D tunnels ClientAliveInterval 300 ClientAliveCountMax 2 EOF chmod 0644 /etc/ssh/sshd_config.d/10-hardening.conf sshd -t # silence means the syntax is valid sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers) ' systemctl reload ssh그
sshd -T출력은 reload 이후가 아니라 이전에 읽어 보십시오.permitrootlogin no와passwordauthentication no가 나와야 합니다. 그렇지 않다면 여러분의 파일이 더 먼저 정렬되는 파일에 의해 덮어써지고 있는 것입니다 —ls /etc/ssh/sshd_config.d/로 확인하고 파일 이름을 더 뒤로 오도록 바꾸십시오. 이제 세 번째 터미널을 열어deploy로 로그인해 보십시오. 이것이 될 때에만 밧줄 역할을 하던 세션을 닫으십시오. - 기본 차단 방화벽 적용하기
nftables는 두 배포판 모두에 기본 포함되어 있으며, iptables 규칙셋을 읽기 쉬운 파일 하나로 대체해 줍니다. 실제로 실행 중인 서비스에 맞춰 허용 포트를 조정하십시오 — 아래 목록은 SSH와 웹 서버만 있다고 가정한 것입니다.
apt install -y nftables cat > /etc/nftables.conf <<'EOF' #!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept ct state invalid drop iif lo accept # ICMP must live: dropping it blackholes path-MTU discovery. ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept ip6 nexthdr icmpv6 accept tcp dport 22 ct state new accept tcp dport { 80, 443 } ct state new accept limit rate 5/minute burst 5 packets log prefix "nft-drop: " level info } chain forward { type filter hook forward priority filter; policy drop; } chain output { type filter hook output priority filter; policy accept; } } EOF nft -c -f /etc/nftables.conf # check before you commit to it systemctl enable --now nftables nft list ruleset | head -40Docker가 설치되어 있다면
flush ruleset은 Docker의 규칙까지 함께 지워 버립니다. 그 뒤에는systemctl restart docker를 실행하고 컨테이너들이 여전히 응답하는지 확인하십시오. 그런 다음 노트북에서nmap -Pn -p- <server-ip>를 실행해, 열려 있는 목록이 방금 허용한 포트와 정확히 일치하는지 — 더 많지도 더 적지도 않은지 — 확인하십시오. - 무인 보안 업데이트 켜기
보안 저장소만 대상으로 하되, 계속 미루게 될 시간이 아니라 여러분이 직접 고른 재부팅 시간대를 지정하십시오. 사용자들에게 한가한 시간이면서 정각은 피해, 다른 모두의 cron과 겹치지 않게 하십시오.
apt install -y unattended-upgrades needrestart cat > /etc/apt/apt.conf.d/51-cvh-auto <<'EOF' // Debian. On Ubuntu, replace the two Origins-Pattern lines with: // "${distro_id}:${distro_codename}-security"; Unattended-Upgrade::Origins-Pattern { "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"; "origin=Debian,codename=${distro_codename},label=Debian-Security"; }; Unattended-Upgrade::Automatic-Reboot "true"; Unattended-Upgrade::Automatic-Reboot-WithUsers "false"; Unattended-Upgrade::Automatic-Reboot-Time "04:17"; Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; EOF unattended-upgrade --dry-run --debug | tail -25 systemctl status unattended-upgrades --no-pager무인 재부팅을 정말로 받아들일 수 없다면 이를
"false"로 설정하고, 대신needrestart -b가 어떤 서비스가 삭제된 라이브러리를 붙든 채 돌아가고 있는지, 더 새로운 커널이 설치되어 있는지를 알려 주게 한 다음 — 그 결과에 따라 조치하십시오. 받아들일 수 없는 것은 둘 중 어느 쪽도 하지 않는 것뿐입니다. - 열려 있는 포트를 닫고 외부에서 확인하기
루프백이 아닌 곳에 바인딩된 것이 무엇인지 나열하고, 쓰지 않는 것은 제거하고, 그 결과를 이 머신이 아닌 다른 머신에서 확인하십시오.
ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]' # the usual leftovers on a base image systemctl disable --now rpcbind.socket rpcbind 2>/dev/null apt purge -y exim4-base exim4-config exim4-daemon-light 2>/dev/null apt autoremove --purge -y # what each running unit is allowed to reach systemd-analyze security --no-pager | head -20계속 실행되어야 하지만 외부에 보일 필요는 없는 것 — 데이터베이스, 캐시, 관리자 패널, 메트릭 exporter 같은 것 — 은 각자의 설정 안에서
127.0.0.1에 바인딩해 두고 SSH 터널로 접근하십시오:ssh -L 5432:127.0.0.1:5432 deploy@<server-ip>. 이는 포트를 열어 두고 그 서비스 자체의 인증이 튼튼하기만을 바라는 것보다 명백히 낫고, 기본적으로 택해야 할 패턴입니다. - 기준선을 기록하고 경보를 준비해 두기
무언가 잘못된 바로 그날에만 값어치를 하는 10분입니다 — 그리고 그날은 여러분이 기억만으로는 아무것도 재구성할 수 없는 날이기도 합니다.
# persistent journal, so a reboot stops erasing the evidence mkdir -p /var/log/journal systemd-tmpfiles --create --prefix /var/log/journal systemctl restart systemd-journald # a clean, greppable line per SSH session cat > /etc/ssh/sshrc <<'EOF' logger -t ssh-login "user=$USER from=${SSH_CONNECTION%% *} tty=${SSH_TTY:-none}" EOF # file-integrity baseline — then get the database off the machine apt install -y aide aideinit mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db sha256sum /var/lib/aide/aide.db # copy this hash somewhere else last -n 20 ; lastb -n 20 # who got in, and who tried마지막으로 서버 밖 어딘가에 네 가지를 적어 두십시오:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub로 확인한 SSH 호스트 키 지문, 위에서 구한 AIDE 해시, 여러분이 의도적으로 열어 둔 포트, 그리고 개인 키가 어디에 있는지입니다. 그 메모 하나가 나쁜 아침을 조사가 아니라 체크리스트로 바꿔 줍니다.
각 통제 수단이 실제로 주는 것을 기준으로 매긴 순위
| 조치 | 막아 주는 것 | 막아 주지 못하는 것 | 비용 | 평가 |
|---|---|---|---|---|
| 키 전용 SSH (PasswordAuthentication no) | 인터넷상의 자격 증명 추측 봇 전부를, 영구적으로, 구조적으로 | 개인 키를 쥐고 있는 사람, 또는 sshd 자체의 결함 | 5분, 한 번 | 타협 불가 |
| 기본 차단 방화벽 (nftables) | 여러분이 열어 둔 걸 잊어버린 서비스, 그리고 나중에 실수로 설치하게 될 서비스 전부 | 여러분이 의도적으로 열어 둔 포트를 노리는 공격 | 10분, 한 번 | 타협 불가 |
| 무인 보안 업데이트 | n-day 취약점 구간 — 공개된 익스플로잇과 여러분의 다음 로그인 사이의 몇 주 | 제로데이, 그리고 여러분이 절대 일정을 잡지 않는 재부팅이 필요한 모든 것 | 5분과 재부팅 시간대 | 타협 불가 |
| root 대신 sudo를 쓰는 이름 있는 사용자 | 모든 권한으로 실행되는 오타 명령, 그리고 이유 없이 root로 돌아가는 프로세스 | 이미 내부에 들어온 사람의 손에서 벌어지는 로컬 권한 상승 | 3분 | 가치 있음 |
| 파일 무결성 기준선 (AIDE) | 시스템 바이너리, 라이브러리, 유닛 파일에 대한 조용한 변조 | 데이터베이스가 감시 대상 머신에 그대로 남아 있다면, 그 무엇도 막지 못함 | 20분과 박스 밖 저장 공간 | 그 박스가 중요하다면 가치 있음 |
| SSH를 포트 22에서 옮기기 | 인증 로그 양의 약 95% | 포트 스캔을 실행하는 사람 — 즉 정말로 중요한 사람 전부 | 2분, 그리고 포트를 잊어버리는 어느 저녁 | 선택 사항, 조용한 로그를 위해 |
| sshd에 적용한 fail2ban / CrowdSec | 상습범, 그리고 그들이 낭비하게 만드는 CPU | 비밀번호 인증을 꺼 두면 아무것도 없음 | 10분, 그리고 실질적인 접속 차단 위험 | SSH에서는 건너뛰고, 애플리케이션에서 사용 |
자주 묻는 질문
배포 후 얼마 만에 스캔이 시작되나요?
보통 몇 분이면 시작됩니다. 인터넷 전역을 훑는 스캐너들은 IPv4 공간 전체를 끊임없이 훑고 있으므로, 그 위에서 무엇이 돌아가든 상관없이 새 주소는 다음 훑기 때에 곧바로 닿게 됩니다. 머신이 첫 패킷에 응답하는 그 순간부터 이미 탐색당하고 있다고 가정하십시오 — 강화 작업을 첫 주말이 아니라 첫 한 시간 안에 해야 하는 이유가 바로 이것입니다.
비밀번호 로그인을 비활성화했다면 fail2ban은 여전히 필요한가요?
SSH에서는 필요 없습니다. PasswordAuthentication no가 설정되어 있으면 무차별 대입 공격이 이길 수 있는 코드 경로 자체가 없으므로, 다섯 번 실패 후 차단하는 것은 아무것도 막아 주지 못합니다 — 로그 잡음을 줄여 준다는 점에서는 가치가 있지만 보안은 아닙니다. 이런 도구들이 제자리를 찾는 곳은 실제로 비밀번호를 받아들이는 대상 앞입니다: 웹 로그인 폼, 관리자 패널, SMTP AUTH 같은 것들입니다. 설치한다면 자신의 주소부터 화이트리스트에 올려 두십시오.
ufw, firewalld, nftables 중 무엇을 써야 하나요?
기본 정책이 drop이고 자신이 무엇을 배포했는지 이해하고 있다면 셋 중 무엇이든 상관없습니다. ufw가 가장 다루기 쉬우며, 현재의 Debian과 Ubuntu에서는 내부적으로 nftables 규칙을 작성합니다. 순수한 nftables는 diff를 뜨고 버전 관리를 할 수 있는 파일 하나로 되어 있어서 저희가 이를 사용합니다. 어떤 것을 고르느냐는 기본 정책이 무엇이냐보다 훨씬 덜 중요합니다 — 그리고 셋 중 무엇을 쓰든 Docker가 그 밑에서 포트를 게시해 버린다는 사실은 바뀌지 않습니다.
SSH를 포트 22에서 옮겨야 하나요?
로그를 조용하게 만들고 싶을 때만 옮기십시오. 유능한 공격자는 전혀 막지 못합니다 — 전체 스캔은 몇 초면 끝나고 배너가 서비스를 그대로 알려 줍니다. 다만 인증 로그의 잡음 대부분을 없애 주어 진짜 이상 징후를 눈에 띄게는 해 줍니다. 이를 보안 통제가 아니라 로그 위생 관리로 여기시고, 바꾸기 전에 sshd가 소켓으로 활성화되는지부터 확인하십시오: Ubuntu 24.04에서는 sshd_config의 Port 지시어가 무시되며 포트는 ssh.socket 소유입니다.
자동 업데이트가 새벽 4시에 제 사이트를 망가뜨리지는 않을까요?
보안 저장소로 한정해 두면 아주 드뭅니다. 그 패키지들은 새로운 업스트림 릴리스가 아니라, 여러분이 이미 실행 중인 버전에 백포트된 수정 사항일 뿐입니다. 현실적인 위험은 패치가 아니라 재부팅입니다 — 그러니 시간대는 직접 고르고, 누군가 로그인해 있는 동안에는 기다리도록 Automatic-Reboot-WithUsers를 false로 설정하십시오. 그리고 서비스가 정말로 무인 재시작을 견디지 못한다면 needrestart -b를 일정을 정해 실행하고 그 결과에 따라 조치하십시오. 변명의 여지가 없는 유일한 태도는 가동 시간이 몇 년 단위로 찍히는 자동 업데이트입니다.
스스로 접속이 차단됐습니다. 어떤 방법이 있나요?
패널에서 서버를 복구 모드로 부팅하십시오. RAM에서 실행되는 Alpine 환경이 시작되며, 여러분 계정과 동일한 SSH 키가 주입되고 디스크는 /dev/vda에 마운트되지 않은 상태로 남아 있으므로, 이를 마운트해 sshd 드롭인이나 방화벽 파일을 고친 다음 마운트 해제하고 재부팅할 수 있습니다. 복구 부팅 자체는 디스크의 그 무엇도 건드리지 않습니다. 복구 환경의 호스트 키 지문은 평소 것과 다른데, 이는 가로채기가 아니라 예상된 현상입니다.
이 대신 Lynis나 CIS 강화 스크립트를 돌리는 것도 좋은 방법인가요?
감사 도구로는 좋지만, 대체재로는 아닙니다. Lynis는 정말로 유용한 두 번째 의견을 제시해 주며, 이 페이지가 언급하지 않는 것들도 찾아낼 것입니다. 반면 자동화된 CIS 교정 스크립트는 다른 이야기입니다: 기업용 워크스테이션 함대를 위해 설계된 수백 가지 변경을 한꺼번에 적용하는데, 그중 몇몇은 몇 주 뒤에나 추적하기 힘든 방식으로 서버를 망가뜨립니다. 먼저 이 한 시간을 손수 해내서 자신의 머신이 무엇을 하고 있는지 이해한 다음, 감사 도구를 돌려 그 결과를 하나씩 읽어 보십시오.
강화 작업을 하면 제 서버가 익명이 되나요?
아닙니다. 이 둘을 혼동하는 것은 흔하면서도 값비싼 실수입니다. 강화는 누가 들어올 수 있는지를 통제하고, 익명성은 누가 그것을 여러분 것이라고 알아볼 수 있는지를 통제합니다. 완벽하게 잠가 둔 서버라도 WHOIS 기록, 재사용한 SSH 키, 분석 태그, 또는 집 주소에서의 관리자 로그인을 통해 소유권이 새어 나갈 수 있습니다. Monero로 결제해 놓고 자신의 회선으로 직접 접속하면 그 결제는 완전히 무효가 됩니다 — 이 내용은 익명성을 무너뜨리는 실수들에서 다룹니다.