본문 바로가기
백수생활

Squid + NodeJS Proxy 설정

by WebHack 2025. 9. 22.

네, 아주 중요한 지적입니다! 이전 설정은 서버에 접근할 수 있는 모든 로컬 프로세스가 프록시를 사용할 수 있어 보안상 허점이 있었습니다. 프록시 서버에 계정 인증(Username/Password)을 추가하여 허가된 사용자/애플리케이션만 사용하도록 강화하는 방법을 안내해 드리겠습니다.

Tree of Thought 협업 프로세스

이전 단계에 이어, 인증 기능 추가라는 새로운 요구사항을 해결하기 위해 전문가들이 다시 협력했습니다.

  • DevOps 엔지니어: Squid의 인증 메커니즘을 분석하고, basic_ncsa_auth를 이용한 사용자 인증 설정 방법을 설계했습니다. 특히, 비밀번호 파일의 생성과 보안 권한 설정에 중점을 두었습니다.
  • Frontend 개발자: Axios에서 프록시 인증 정보를 안전하게 전달하는 방법을 제시했습니다. 하드코딩을 피하고 환경 변수를 활용하여 보안과 유지보수성을 높이는 방안을 강조했습니다.
  • 네트워크 보안 전문가: 인증 설정 후, 인증이 실제로 적용되었는지 확인하는 검증 절차를 설계했습니다. 인증 실패 시와 성공 시의 curl 테스트 방법을 구체적으로 제안하여 설정 오류를 쉽게 찾아낼 수 있도록 했습니다.

이들의 의견을 종합하여, 서버 설정부터 클라이언트 코드 수정, 그리고 최종 검증까지 완벽한 솔루션을 아래와 같이 구성했습니다.


해결책 요약

전체 프로세스는 3단계로 진행됩니다.

  1. Squid 프록시 서버에 인증 기능 추가: Ubuntu 서버에서 비밀번호 파일을 생성하고, squid.conf 파일을 수정하여 사용자 이름과 비밀번호를 확인하도록 설정합니다.
  2. 인증 기능 동작 테스트: curl 명령어를 사용하여 프록시 서버가 실제로 인증을 요구하는지, 그리고 올바른 계정 정보로 접근이 가능한지 확인합니다.
  3. Next.js 코드 수정: Axios 요청 설정에 프록시 사용자 이름과 비밀번호를 포함하도록 코드를 변경합니다.

1단계: Squid 프록시 서버에 인증 기능 추가 (서버 설정)

1. 인증에 필요한 도구 설치 (apache2-utils)

Squid는 사용자 인증을 위해 외부 프로그램을 사용합니다. 가장 기본적인 방식인 htpasswd를 사용하기 위해 관련 유틸리티를 설치합니다.

sudo apt-get update
sudo apt-get install apache2-utils -y

2. 프록시 사용자 계정 및 비밀번호 파일 생성

htpasswd 명령어를 사용하여 프록시 접속에 사용할 계정과 비밀번호를 생성합니다. 여기서는 proxyuser라는 사용자 이름으로 생성해 보겠습니다.

# /etc/squid/passwd 파일에 proxyuser 사용자를 생성합니다.
# -c 옵션은 파일이 없을 경우 새로 생성하는 옵션이므로, 최초 1회만 사용합니다.
sudo htpasswd -c /etc/squid/passwd proxyuser

명령을 실행하면 New password: 프롬프트가 나타납니다. 여기에 사용할 비밀번호를 입력하고 확인을 위해 한 번 더 입력합니다. 반드시 강력한 비밀번호를 사용하세요.

다른 사용자를 추가할 때는 -c 옵션을 제외하고 실행해야 기존 파일에 사용자가 추가됩니다.
sudo htpasswd /etc/squid/passwd anotheruser

3. 비밀번호 파일 권한 설정 (보안 강화)

보안을 위해 생성된 passwd 파일은 squid를 실행하는 proxy 사용자만 읽을 수 있도록 권한을 변경해야 합니다.

# 파일의 소유자를 proxy 사용자로 변경
sudo chown proxy:proxy /etc/squid/passwd

# 소유자만 읽기/쓰기 가능하도록 권한 변경
sudo chmod 640 /etc/squid/passwd

4. squid.conf 설정 파일 수정

이제 Squid가 인증을 사용하도록 설정 파일을 수정합니다.

sudo nano /etc/squid/squid.conf

파일의 상단 부분(ACL 관련 설정이 모여있는 곳, acl CONNECT method CONNECT 근처)에 다음 내용을 추가합니다.

# --- 아래 3줄을 추가합니다 ---

# 1. 인증 프로그램 경로 지정
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd

# 2. 인증을 통과한 사용자를 'authenticated'라는 이름의 ACL로 정의
acl authenticated proxy_auth REQUIRED

# --- 여기까지 추가 ---

# http_access 규칙을 수정/추가합니다.
# ... 기존 acl 설정들 ...
acl localnet src 127.0.0.1
# ...

# --- 아래 줄을 'http_access deny all' 바로 위에 추가합니다 ---
# 인증된(authenticated) 사용자의 접근을 허용
http_access allow authenticated

중요: http_access 규칙은 위에서부터 순서대로 적용됩니다. 따라서 http_access allow authenticated 규칙은 반드시 http_access deny all 보다 위에 있어야 합니다.

수정이 완료되면 파일을 저장하고 종료합니다.

5. Squid 서비스 재시작

설정을 적용하기 위해 Squid를 재시작합니다.

sudo systemctl restart squid
sudo systemctl status squid

active (running) 상태를 다시 한번 확인합니다.


2단계: 인증 기능 동작 테스트 (curl)

코드를 변경하기 전에 터미널에서 인증 기능이 제대로 작동하는지 반드시 확인해야 합니다.

  1. 인증 없이 요청 (실패해야 정상):
    이전과 동일한 명령어를 실행하면, 이제는 407 Proxy Authentication Required 라는 오류가 발생해야 합니다.이 오류가 발생하면 1단계 설정이 성공적으로 적용된 것입니다.
  2. curl -x http://127.0.0.1:3128 https://api.ipify.org
  3. 인증 정보 포함하여 요청 (성공해야 정상):
    -x 옵션에 사용자이름:비밀번호@ 형식으로 인증 정보를 추가하여 요청합니다.서버의 IP 주소가 정상적으로 출력된다면, 인증 기능이 완벽하게 작동하는 것입니다.
  4. # proxyuser와 설정한 비밀번호로 변경하여 실행하세요. curl -x http://proxyuser:your_password@127.0.0.1:3128 https://api.ipify.org

이로써 허가되지 않은 접근을 차단하고 훨씬 더 안전한 시스템을 구축하게 되었습니다.


테스트 방법 <= 중요

curl -x http://proxyuser:your_strong_password@127.0.0.1:3128 https://api.ipify.org

Node Proxy(Fetch)

const { ProxyAgent } = require('proxy-agent');

const proxyHost = '127.0.0.1';
const proxyPort = 3128;
const proxyUsername = 'proxyuser';
const proxyPassword = 'your_strong_password';

const proxyUrl = `http://${proxyUsername}:${proxyPassword}@${proxyHost}:${proxyPort}`;


const targetUrl = 'https://api.ipify.org';
const timeout = 5000; // 타임아웃 5초

console.log(`[Fetch 테스트 시작] 프록시(${proxyHost}:${proxyPort})를 통해 요청을
 보냅니다...`);

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

fetch(targetUrl, {
  agent: agent,
  signal: controller.signal // 타임아웃을 위한 signal
})
.then(response => {
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.text(); // 응답 본문을 텍스트로 변환
})
.then(data => {
  console.log('✅✅✅ [Fetch 테스트 성공!] ✅✅✅');
  console.log('서버 IP 주소:', data);
})
.catch(error => {
  console.error('❌❌❌ [Fetch 테스트 실패!] ❌❌❌');
  if (error.name === 'AbortError') {
    console.error('오류 원인: 타임아웃 발생 (Timeout)');
  } else {
    // EPROTO 오류는 cause 속성에 원본 오류를 포함하는 경우가 많습니다.
    console.error('오류 이름:', error.name);
    console.error('오류 메시지:', error.message);
    if (error.cause) {
      console.error('Cause:', error.cause.code, error.cause.message);
    }
  }
})
.finally(() => {
  clearTimeout(timeoutId);
});

Node Proxy(AXIOS)

import axios from 'axios';
import { NextResponse } from 'next/server';
import { ProxyAgent } from 'proxy-agent'; // 1. proxy-agent를 import 합니다.

export async function GET() {
  const proxyHost = '127.0.0.1';
  const proxyPort = 3128;
  const proxyUsername = 'proxyuser';
  const proxyPassword = 'your_strong_password';

  // 2. proxy-agent가 요구하는 형식의 URL 문자열을 만듭니다.
  const proxyUrl = `http://${proxyUsername}:${proxyPassword}@${proxyHost}:${proxyPort}`;

  // 필수 설정 값 확인 (환경 변수 사용을 권장)
  if (!proxyHost || !proxyPort || !proxyUsername) {
    console.error('[PROXY API ERROR] Proxy configuration is missing');
    return NextResponse.json(
      { error: '서버에 프록시 설정이 올바르지 않습니다.' },
      { status: 500 }
    );
  }

  const targetUrl = 'https://api.ipify.org'; // HTTPS 타겟 URL

  try {
    console.log(`[PROXY] Attempting to connect to ${targetUrl} via proxy agent`);

    // 3. ProxyAgent 인스턴스를 생성합니다.
    const agent = new ProxyAgent(proxyUrl as any);

    const response = await axios.get(targetUrl, {
      // 4. targetUrl이 https이므로 httpsAgent에 agent를 할당합니다.
      // 이렇게 하면 axios의 내장 프록시 기능 대신 proxy-agent를 사용하게 됩니다
.
      httpsAgent: agent,
      timeout: 5000,
      // proxy 객체는 더 이상 필요 없으므로 제거합니다.
    });

    return NextResponse.json({ ip: response.data });

  } catch (error) {
    if (axios.isAxiosError(error)) {
        console.error('[PROXY AXIOS ERROR]', {
            message: error.message,
            code: error.code,
            status: error.response?.status,
            // proxy-agent를 사용하면 상세한 오류가 cause에 담길 수 있습니다.
            cause: error.cause,
            config: {
              url: error.config?.url,
              method: error.config?.method,
            },
        });
    } else {
        console.error('[PROXY UNKNOWN ERROR]', error);
    }

    return NextResponse.json(
      { error: '프록시를 통한 외부 API 호출에 실패했습니다.', details: (error as any).message },
      { status: 500 }
    );
  }
}

 

이렇게 기록을 하는 이유는

const axios = require('axios');

// 실패했던 API Route의 설정과 완전히 동일하게 만듭니다.
const proxyConfig = {
  host: '127.0.0.1',
  port: 3128, // port는 숫자로 지정하는 것이 좋습니다.
  auth: {
    username: 'proxyuser',
    password: 'your_strong_password'
  }
};

const targetUrl = 'https://api.ipify.org';
const timeout = 5000; // 타임아웃 5초

console.log(`[테스트 시작] 프록시(${proxyConfig.host}:${proxyConfig.port})를 통>해 요청을 보냅니다...`);

axios.get(targetUrl, {
  proxy: proxyConfig,
  timeout: timeout
})
.then(response => {
  console.log('✅✅✅ [테스트 성공!] ✅✅✅');
  console.log('서버 IP 주소:', response.data);
})
.catch(error => {
  console.error('❌❌❌ [테스트 실패!] ❌❌❌');
  if (error.code === 'ECONNABORTED') {
    console.error('오류 원인: 타임아웃 발생 (Timeout)');
  } else {
    console.error('오류 코드:', error.code);
    console.error('오류 메시지:', error.message);
  }
});

위 소스는

node ./test.js                             
[테스트 시작] 프록시(192.168.0.129:3128)를 통해 요청을 보냅니다...
❌❌❌ [테스트 실패!] ❌❌❌
오류 코드: EPROTO
오류 메시지: write EPROTO 403835E3497F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:354:

이런 에러가 발생 하기 때문임

'백수생활' 카테고리의 다른 글

개인사업자 세금 신고 기간  (31) 2024.03.11
갑자기 오늘 supabase URL 접속 안될 경우 해결 방법  (2) 2024.01.31
백수생활 15  (0) 2023.11.28
백수생활 15(정부지원 포함)  (0) 2023.11.16
백수생활 13  (0) 2023.10.19