포트와 방화벽, 0.0.0.0/0으로 열면 생기는 일
보안 그룹 인바운드 규칙에 0.0.0.0/0을 넣으면 인터넷의 누구나 그 포트로 연결을 시도할 수 있다.
포트, 인바운드와 아웃바운드, IP 범위 표기부터 차례로 정리한다.
이 글의 목차포트: 서버 안에서 프로그램을 가리키는 번호7
AWS에서 EC2 인스턴스를 만들다 보면 보안 그룹을 정하는 단계가 나온다.
인바운드 규칙, 아웃바운드 규칙, 프로토콜, 포트 범위, 소스까지 처음 보면 낯선 칸이 많다.
튜토리얼을 따라 하다 보면 소스 칸에 0.0.0.0/0을 넣으라는 안내가 자주 나온다.
넣으면 접속이 바로 되니 그대로 두고 넘어가기 쉽다.
하지만 이 값의 뜻을 알고 넣는 것과 모르고 넣는 것은 차이가 크다.
포트가 무엇인지부터 정리한다.
포트: 서버 안에서 프로그램을 가리키는 번호
IP 주소는 인터넷에서 서버 한 대를 찾아가는 주소다.
그런데 서버 한 대에는 보통 프로그램이 여러 개 돈다.
웹 서버, 데이터베이스, 원격 접속용 SSH가 한 기계에 함께 있는 식이다.
그래서 같은 서버로 온 연결이 어느 프로그램으로 가야 하는지 정하는 번호가 하나 더 필요하다.
이 번호가 포트다.
포트 번호는 0부터 65535까지 쓴다.
0번부터 1023번까지를 시스템 포트, 1024번부터 49151번까지를 사용자 포트, 그 위를 동적 포트라고 부른다.
자주 쓰는 서비스는 번호가 정해져 있다.
SSH는 22번, HTTP는 80번, HTTPS는 443번이다.
MySQL은 3306번, PostgreSQL은 5432번, Redis는 6379번을 기본으로 쓴다.
프로그램이 어떤 포트로 들어오는 연결을 기다리는 것을 리슨(listen)이라고 한다.
서버에 접속이 된다는 것은 그 포트에서 리슨하는 프로그램이 있고, 중간의 방화벽도 그 연결을 통과시켰다는 뜻이다.
인바운드와 아웃바운드는 연결의 방향이다
보안 그룹 규칙은 방향에 따라 두 가지로 나뉜다.
인바운드는 밖에서 서버로 들어오는 연결이다.
사용자가 웹사이트에 접속하거나, 노트북에서 SSH로 서버에 붙는 경우가 여기에 들어간다.
아웃바운드는 서버에서 밖으로 나가는 연결이다.
서버가 외부 결제 API를 부르거나 패키지를 내려받을 때가 그렇다.
보안 그룹을 처음 만들면 인바운드 규칙은 하나도 없고, 아웃바운드에는 모든 트래픽을 허용하는 규칙 하나가 들어 있다.
아무것도 바꾸지 않으면 밖에서는 못 들어오고, 안에서는 어디로든 나갈 수 있는 상태다.
여기서 흔히 생기는 의문이 있다.
인바운드를 다 막아 두면 서버가 외부 API를 불렀을 때 응답은 어떻게 들어오는가.
보안 그룹은 연결 상태를 기억한다(stateful). 안에서 내보낸 요청의 응답은 인바운드 규칙과 상관없이 들어온다.
그래서 인바운드 규칙은 밖에서 먼저 연결을 걸어오는 경우만 생각하면 된다.
응답이 돌아오는 자리도 알아 둘 만하다.
서버가 밖으로 연결을 걸 때는 운영체제가 남는 번호 하나를 잠깐 골라 출발지 포트로 쓰는데, 이것을 임시 포트(ephemeral port)라고 한다.
리눅스는 기본으로 32768번부터 60999번 사이에서 이 번호를 고른다.
응답은 이 임시 포트로 돌아온다.
연결 상태를 기억하지 않는 네트워크 ACL에서는 이 번호가 따로 문제가 되는데, 그 내용은 보안 그룹과 네트워크 ACL 글에서 이어진다.
0.0.0.0/0은 ‘인터넷 전체’라는 뜻이다
이제 소스 칸을 볼 차례다.
소스에는 연결을 허용할 IP 주소 범위를 적는다.
203.0.113.10/32처럼 주소 뒤에 슬래시와 숫자를 붙이는데, 이런 표기를 CIDR이라고 한다.
슬래시 뒤의 숫자는 IPv4 주소 32비트 가운데 앞에서부터 몇 비트를 고정하는지를 뜻한다.
/32면 32비트를 전부 고정하니 주소 딱 하나다.
/24면 앞 24비트만 고정하니 마지막 자리 256개를 묶은 범위가 된다.
AWS의 보안 그룹 규칙 문서도 주소 하나만 허용하려면 /32를 붙여 적으라고 안내한다.
그렇다면 /0은 무엇일까.
고정하는 비트가 하나도 없으니, 0.0.0.0/0은 IPv4 주소 42억 9천여 개 전체를 덮는 범위가 된다.
IPv6에서 같은 뜻은 ::/0이다.
그러니 인바운드 규칙의 소스가 0.0.0.0/0이면, 인터넷에 연결된 누구든 그 포트로 연결을 시도할 수 있다는 뜻이 된다.
다 열어 두면 생기는 일
내 서버 주소를 누가 알겠느냐는 생각이 들 수 있다.
하지만 열린 포트를 찾는 것은 사람이 아니라 자동화된 스캐너다.
2013년에 공개된 스캐너 ZMap은 장비 한 대로 IPv4 주소 전체를 45분 안에 훑을 수 있었다.
벌써 10년도 더 지난 일이다.
공인 IP에 포트를 열어 두면 곧 누군가 두드린다고 보는 편이 맞다.
어떤 포트가 열렸느냐에 따라 위험의 크기가 다르다.
SSH 22번을 0.0.0.0/0으로 열어 두면 세계 어디서든 로그인을 시도할 수 있다.
비밀번호가 약하거나 키가 새면, 권한 없는 사람이 그 서버에서 명령을 실행하게 된다.
데이터베이스와 캐시는 더 조심해야 한다.
이런 프로그램은 애플리케이션 서버만 접속한다는 전제로 만들어진다.
Redis는 믿을 수 있는 환경 안에서 믿을 수 있는 클라이언트만 접속한다는 전제로 설계되었다.
그래서 Redis 개발진은 인터넷에 직접 노출하지 말고, EC2 같은 가상 서버라면 Redis 포트를 방화벽으로 막으라고 안내한다.
그런데도 인터넷에 그대로 노출된 Redis가 많았다.
그래서 3.2.0부터는 보호 모드가 생겼다.
비밀번호 없이 모든 인터페이스에서 리슨하면, 같은 서버가 아닌 곳에서 온 접속에는 오류로 답한다.
방화벽이 처음부터 막아 두는 이유
보안 그룹이 처음에 인바운드를 전부 막아 두는 것은 우연이 아니다.
허락할 것만 골라 적고 나머지는 막는 방식을 기본 거부(default deny)라고 하는데, 이 생각은 꽤 오래되었다.
1975년 Jerome Saltzer와 Michael Schroeder는 정보 보호 시스템의 설계 원칙을 정리하면서, 접근은 막을 것을 골라내기보다 허락할 것을 골라 주는 방식으로 정해야 한다고 썼다.
실수했을 때의 결과가 다르기 때문이다.
막을 것을 적는 목록에서 하나를 빠뜨리면 그 항목은 열린 채로 남는다.
허락할 것을 적는 목록에서 하나를 빠뜨리면 닫힌 채로 남는다.
앞의 실수는 사고로 이어질 수 있다.
뒤의 실수는 대개 “이거 왜 안 돼요?” 하는 문의로 끝난다.
인터넷이 실제로 이쪽으로 움직인 계기는 사건이었다.
1988년 11월, 인터넷에 풀린 자기 복제 프로그램이 버클리 유닉스를 쓰는 기계들을 줄줄이 감염시켰다.
그 뒤로 망과 망 사이에 관문을 세우는 방식이 자리 잡았다.
1990년 Bill Cheswick은 AT&T 벨 연구소에서 쓰던 게이트웨이 설계를 발표하면서, 믿을 수 있는 안쪽 기계와 믿을 수 없는 바깥 기계를 나누었다.
이런 관문을 방화벽이라고 부르고, AWS의 보안 그룹도 같은 역할을 한다.
그 논문에는 바깥 경계만 단단하게 지키면 안쪽은 무방비로 남는다는 경고도 함께 들어 있다.
방화벽 하나에만 기대지 말라는 뜻이고, 서버 안에서 할 수 있는 일이 바로 다음에 나오는 바인드 주소다.
0.0.0.0이 또 나온다: 바인드 주소
헷갈리기 쉬운 것이 하나 있다.
서버 프로그램 설정에도 0.0.0.0이 자주 나오는데, 보안 그룹의 0.0.0.0/0과는 뜻이 다르다.
프로그램이 포트에서 리슨할 때는 서버의 어느 주소로 들어온 연결을 받을지도 함께 정한다.
이 주소를 바인드 주소라고 한다.
서버 한 대에도 네트워크와 만나는 접점인 네트워크 인터페이스가 여러 개 있을 수 있기 때문이다.
바인드할 때 0.0.0.0(INADDR_ANY)을 지정하면 소켓이 이 서버의 모든 로컬 인터페이스에 묶인다.
보안 그룹의 0.0.0.0/0이 ‘인터넷의 모든 주소’였다면, 바인드 주소의 0.0.0.0은 ‘이 서버의 모든 인터페이스’다.
반대로 127.0.0.1 같은 루프백 주소에 바인드하면 같은 서버 안에서만 연결할 수 있다.
데이터베이스와 애플리케이션이 한 서버에 있다면 이렇게 두는 편이 안전하다.
같은 프로그램이라도 바인드 대상에 따라 직접 연결할 수 있는 범위가 달라진다.
같은 컴퓨터의 요청
루프백으로 연결 가능외부 기기의 요청
직접 연결 불가외부 주소에서 받지 않음
같은 컴퓨터의 요청
연결 가능외부 기기의 요청
직접 연결 가능이 예시에서는 경로·방화벽 허용
루프백에만 바인드하면 외부 주소로 온 연결을 직접 받지 않는다. 모든 인터페이스에 바인드하면 받을 범위가 넓어진다.
방화벽을 열어도 외부 주소에서 받는 소켓은 없다. 루프백은 같은 컴퓨터를 향한다.
외부 경로와 방화벽은 허용되어 있고 프록시·포트 전달은 없는 예시다. 실제 주소 조회나 네트워크 점검은 하지 않는다.
바인드 주소와 보안 그룹은 서로를 대신하지 못한다.
같은 포트라도 어디에 바인드했느냐에 따라, 보안 그룹을 잘못 설정했을 때 피해가 미치는 범위가 달라진다.
| 바인드 대상 | 직접 연결할 수 있는 범위 | 남아 있는 경계 |
|---|---|---|
| 모든 로컬 인터페이스 | 해당 주소로 경로가 있는 곳 | 방화벽과 접근 인증 |
| 특정 사설 주소 | 사설 주소로 경로가 이어진 곳 | 라우팅, 방화벽과 접근 인증 |
| 루프백 | 같은 컴퓨터 안 | 로컬 프로세스와 별도 전달 통로 |
마지막 줄의 ‘별도 전달 통로’는 프록시나 포트 전달처럼 다른 프로그램이 대신 연결을 넘겨주는 경우다.
이런 통로를 따로 만들면 루프백에 바인드한 프로그램도 밖에서 닿을 수 있다.
실무에서 여는 방법
웹 서버의 80번과 443번은 0.0.0.0/0으로 열어도 괜찮다.
누구나 접속하라고 만든 서비스이기 때문이다.
앞에 로드밸런서를 둔다면 로드밸런서만 이 포트를 열고, 서버는 로드밸런서에서 오는 연결만 받게 하면 된다.
SSH 22번은 0.0.0.0/0으로 열지 않는다.
꼭 열어야 한다면 내 IP 하나만 /32로 허용한다.
AWS Systems Manager의 Session Manager를 쓰면 인바운드 포트를 열지 않고도 서버에 접속할 수 있다.
데이터베이스와 캐시는 소스에 IP 범위 대신 애플리케이션 서버의 보안 그룹을 적는다.
다른 보안 그룹을 소스로 지정하면, 그 보안 그룹이 붙은 인스턴스들만 사설 IP로 접속할 수 있다.
이렇게 해 두면 애플리케이션 서버를 늘리거나 바꿔도 규칙의 IP를 일일이 고칠 필요가 없다.
새 서버에 같은 보안 그룹만 붙이면 된다.
아웃바운드는 기본값인 전체 허용으로 두는 경우가 많다.
다만 민감한 데이터를 다루는 서버라면 나가는 목적지도 필요한 곳으로 좁힐 수 있다.