이 오류가 정확히 말하는 것
크롬은 “사이트에 연결할 수 없음 / example.com에서 연결을 거부했습니다.”와 함께 ERR_CONNECTION_REFUSED(내부 코드 -102)를 보여 줍니다. 파이어폭스는 “연결할 수 없음(Unable to connect)“으로, 프로그램에서는 ECONNREFUSED, “Connection refused”로 나타납니다.
모든 TCP 연결은 내 쪽이 SYN 패킷을 보내는 것으로 시작합니다. “443 포트로 이야기하고 싶습니다.” 거부란 그 주소의 기기가 SYN을 받고 RST로 답했다는 뜻입니다. “이 포트로는 아무도 전화를 받지 않습니다.” 연결은 열리지 않았습니다.
그래서 거부는 의외로 연결 오류 중 정보가 가장 많은 오류입니다. IP 주소가 무언가에는 닿는다는 것, 그리고 그 무언가가 일부러 거절했다는 것을 알 수 있습니다. 형제 오류들과 비교하면 이렇습니다.
| 오류 | 일어난 일 | 체감 | 볼 곳 |
|---|---|---|---|
ERR_CONNECTION_REFUSED | SYN에 RST가 돌아옴 | 즉시 실패 | 그 포트에서 듣는 프로그램이 없음, 거부하는 방화벽 |
ERR_CONNECTION_TIMED_OUT | SYN에 아무 응답 없음 | 한참 멈췄다 실패 | 패킷을 버리는 방화벽, 잘못된 IP, 라우팅 |
ERR_CONNECTION_RESET | 연결은 열렸고 그 뒤 RST가 옴 | 연결 후 실패 | 애플리케이션, 프록시, 트래픽을 검사하는 필터 |
RST는 대개 대상 기기의 커널이 보냅니다. 그 포트에서 듣고 있는 소켓이 없으면 커널이 자동으로 이렇게 답합니다. 그보다 드물게, 조용히 버리지 않고 거부하도록 설정된 방화벽이 보내기도 합니다.
오류가 ERR_NAME_NOT_RESOLVED라면 한 단계 앞입니다. 브라우저가 연결할 IP 주소를 찾지 못한 것이고 DNS 문제입니다. ERR_NAME_NOT_RESOLVED를 보세요.
localhost인가, 실제 사이트인가
이 오류를 보는 사람 상당수는 자기 개발 PC에서 ‘localhost에서 연결을 거부했습니다’를 보고 있습니다. 이 경우는 따로 짧은 목록이 있으니 먼저 다룹니다.
localhost에서 연결을 거부함
서버가 안 떠 있거나 시작하다 죽었습니다. 서버를 띄운 터미널을 보세요. 포트 충돌(EADDRINUSE)이나 빠진 의존성 때문에 듣기 전에 죽는 경우가 많습니다.
포트가 다릅니다. 개발 서버는 시작할 때 고른 포트를 출력합니다. Vite는 5173, Create React App은 3000, Django는 8000, Flask는 5000입니다. 원하는 포트가 이미 쓰이고 있으면 다음 빈 포트를 고르고 그 사실을 출력에 적는 도구가 있는데, 놓치기 쉽습니다.
IPv6와 IPv4. localhost는 127.0.0.1뿐 아니라 ::1(IPv6)로도 해석될 수 있습니다. 127.0.0.1에서만 듣는 서버는 ::1로 오는 연결을 거부합니다. Node.js의 익숙한 메시지 connect ECONNREFUSED ::1:3000이 이것입니다. Node 17부터 리졸버가 운영체제가 준 순서대로 주소를 돌려주는데, ::1이 먼저 오는 경우가 많습니다. 브라우저는 대개 다른 주소로 다시 시도하지만 스크립트나 도구는 그러지 않을 수 있습니다. 127.0.0.1을 직접 쓰거나 서버가 둘 다에서 듣게 하세요.
컨테이너나 VM 안에서 돌고 있습니다. Docker, WSL, VM 안의 서버는 브라우저와 다른 네트워크 스택에 있습니다. Docker라면 포트를 공개해야 하고(-p 3000:3000), 안의 앱은 127.0.0.1이 아니라 0.0.0.0에서 들어야 합니다. 많은 개발 서버가 기본으로 localhost에서만 들으므로 모든 인터페이스에서 듣게 하는 옵션이 필요합니다. vite --host, flask run --host=0.0.0.0, python manage.py runserver 0.0.0.0:8000, rails server -b 0.0.0.0입니다.
실제로 무엇이 듣고 있는지는 리눅스에서 ss -ltnp, macOS에서 lsof -iTCP -sTCP:LISTEN -n -P, 윈도우에서 netstat -ano | findstr LISTENING으로 확인합니다.
남의 사이트를 방문하는 중이라면
그 사이트가 내가 요청한 포트에서 서비스를 하지 않을 수 있습니다. 80 포트(평문 http://)에서는 아예 답하지 않고 443에서만 서비스하는 사이트가 있습니다. https://를 직접 입력해 보세요. 최근 크롬은 스킴 없이 입력한 주소를 HTTPS로 먼저 시도하지만, 북마크한 http:// 링크는 여전히 80 포트로 갑니다. 주소에 포트가 명시돼 있다면(example.com:8080) 그 포트가 맞는지 확인하세요.
hosts 파일. 광고 차단용 hosts 목록이나 예전 개발용 항목이 이름을 127.0.0.1이나 0.0.0.0으로 보냅니다. 그러면 브라우저는 내 PC에 연결하고, 듣는 프로그램이 없으니 멀쩡한 사이트를 ‘거부’로 보고합니다. 리눅스·macOS는 /etc/hosts, 윈도우는 C:\Windows\System32\drivers\etc\hosts에서 그 사이트 이름을 찾아보세요.
내 IP가 차단됐습니다. fail2ban 같은 도구는 로그인 실패가 반복되는 등 이상한 행동을 한 주소에 방화벽 규칙을 겁니다. 거부 규칙이면 차단된 주소만 거부되고 다른 사람은 정상입니다. 회사나 이동통신사처럼 여러 사람이 공유하는 IP라면 다른 사람이 받은 차단을 함께 받을 수도 있습니다. 다른 네트워크와 HTTP 점검에서는 열린다면, 내 IP 주소(IP 확인에서 볼 수 있습니다)를 적어 사이트 운영자에게 문의하세요.
어디에도 해당하지 않고 어디서나 거부된다면 서버 쪽이 내려가 있는 것입니다. 운영자만 고칠 수 있습니다.
서버를 운영한다면
내 네트워크 밖의 기기에서 curl 상세 출력을 보세요.
curl -v https://www.example.com/ -o /dev/null
* Trying 203.0.113.10:443...
* connect to 203.0.113.10 port 443 from 192.0.2.5 port 51234 failed: Connection refused
* Failed to connect to www.example.com port 443 after 31 ms: Couldn't connect to server
curl: (7) Failed to connect to www.example.com port 443 after 31 ms: Couldn't connect to server
curl 버전에 따라 문구는 조금 다르니 “Connection refused”와 시도한 IP를 찾으세요. 그 IP가 내가 생각하는 서버인지 DNS 조회로 확인한 뒤, 서버에서 아래를 차례로 봅니다.
1. 그 포트에서 듣는 프로그램이 없음
sudo ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=812,fd=6))
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=1290,fd=21))
이 예에서는 443에서 듣는 것이 없습니다. HTTP로만 설정된 웹 서버, 즉 HTTPS server 블록을 추가하지 않았거나 인증서 단계가 실패한 경우가 정확히 이렇게 보이고, HTTPS 방문자는 거부됩니다.
떠 있어야 할 서비스가 없다면 이유를 물어보세요.
systemctl status nginx
journalctl -u nginx -n 50 --no-pager
sudo nginx -t
설정을 바꾼 뒤, 또는 설정이 가리키는 인증서 파일이 사라진 뒤 웹 서버가 시작을 거부하는 경우가 흔합니다. nginx -t가 문제의 줄을 알려 줍니다.
2. 127.0.0.1에서만 듣고 있음
위 출력에서 Node 앱은 127.0.0.1:3000에 있습니다. 서버 안에서 curl localhost:3000은 되고, 다른 모든 기기는 거부됩니다. nginx가 로컬에서 프록시하는 구조라면 이것이 맞고, 바깥에서 3000에 직접 붙으려 했다면 틀린 것입니다.
데이터베이스는 일부러 이렇게 기본 설정돼 있습니다. PostgreSQL의 listen_addresses 기본값은 localhost이고, 데비안·우분투의 MySQL 패키지는 bind-address = 127.0.0.1로 설정합니다. 정말 원격 접속이 필요하면 bind 주소를 바꾸고 방화벽으로 접속할 수 있는 곳을 제한하세요. 대부분은 loopback에 그대로 두고 SSH 터널을 쓰는 편이 낫습니다.
IPv6도 확인하세요. nginx에 listen 80;만 있고 listen [::]:80;이 없으면 IPv4는 되는데 IPv6 클라이언트는 거부됩니다. 브라우저는 대개 IPv4로 넘어가 이를 가려 주므로 각각 따로 시험해 보세요.
curl -4 -sI https://www.example.com/ | head -1
curl -6 -sI https://www.example.com/ | head -1
IPv6로 서비스하지 않는다면 AAAA 레코드를 올리지 마세요.
3. 방화벽이 버리지 않고 거부함
방화벽 규칙은 패킷을 조용히 버리거나(클라이언트는 시간 초과), 거부합니다(클라이언트가 즉시 알게 됨). 거부는 ‘연결 거부’로 나타나거나, 규칙이 ICMP ‘관리상 금지(administratively prohibited)‘로 답하면 “No route to host”로 나타납니다. firewalld의 기본 zone이 이렇게 답합니다.
sudo iptables -S | grep -i reject
sudo nft list ruleset | grep -i reject
sudo firewall-cmd --list-all
sudo fail2ban-client status
특정 클라이언트만 거부된다면 fail2ban jail이나 속도 제한기가 1순위 용의자입니다. fail2ban-client status <jail>이 차단된 IP를 보여 줍니다.
4. DNS가 더는 사이트를 돌리지 않는 서버를 가리킴
이전 후에도 A 레코드(또는 어딘가에 캐시된 옛 값)가 예전 서버를 가리키는 경우입니다. 예전 서버가 살아 있으니 응답은 하지만, 거기서는 더 이상 443에서 듣는 것이 없습니다. curl 출력의 IP와 내가 설정한 서버를 비교하세요. 전파 확인으로 아직 예전 주소를 내주는 공용 리졸버가 있는지 볼 수 있습니다.
502 페이지가 뜬다면
리버스 프록시와 애플리케이션 사이의 거부는 브라우저에 ERR_CONNECTION_REFUSED로 오지 않습니다. 프록시는 브라우저 연결을 잘 받았고, 프록시가 백엔드로 연결하다 실패했으니 502 Bad Gateway를 돌려줍니다. nginx 로그에는 connect() failed (111: Connection refused) while connecting to upstream으로 남습니다. 해결은 위 목록을 백엔드 포트에 적용하는 것입니다. Cloudflare 뒤라면 Cloudflare와 원 서버 사이의 같은 상황이 Cloudflare 521 오류로 보입니다.
DechoNet으로 바깥에서 확인하기
포트 점검은 DechoNet 서버에서 호스트로 TCP 연결을 열어 포트마다 열림·닫힘을 보여 줍니다. 특정 포트 하나만 보려면 호스트:포트로 입력하세요. 결과를 읽을 때 세 가지를 기억하세요.
- ‘닫힘’은 거부와 시간 초과를 구분하지 않습니다. 거부, reset, 3초 안에 응답 없음을 모두 ‘닫힘’으로 표시합니다. 바깥에서는 닫힌 포트와 방화벽에 막힌 포트를 늘 구분할 수는 없습니다. 차이가 필요하면
curl -v를 돌려 즉시 실패하는지(거부), 한참 멈추는지(시간 초과) 보세요. - 내 네트워크 밖에서 시험합니다. 여기서는 열림인데 나만 거부된다면 문제는 내 쪽입니다. hosts 파일, 차단, 보안 프로그램입니다.
- Cloudflare IP의 호스트는 점검할 수 없습니다. 점검이 Cloudflare 네트워크에서 돌기 때문에 Cloudflare 자체 주소 대역으로는 TCP 연결을 열 수 없습니다. 그래서 모든 포트를 닫힘으로 보여 주는 대신 점검 불가로 표시합니다. 프록시를 켠 사이트라면 원 서버 IP를 직접 시험하세요.
HTTPS 사이트라면 443 포트 점검 체크리스트가 웹 서버 쪽을 더 자세히 다룹니다.
비슷해 보이는 다른 오류
ERR_CONNECTION_TIMED_OUT: 거부가 아니라 아무 응답이 없습니다.ERR_CONNECTION_RESET: 연결은 열렸고, 그 뒤에 끊겼습니다.ERR_NAME_NOT_RESOLVED: DNS가 주소를 주지 않아 연결을 시도조차 하지 않았습니다.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.