Bastion 서버 자동화

김유경·2025년 5월 28일

들어가며

클라우드 환경에서 인프라를 운영할 때, bastion 서버는 운영자나 배포 파이프라인이 내부 자원에 안전하게 접근할 수 있도록 제어하는 핵심 컴포넌트입니다. 외부 네트워크와 내부 네트워크 사이의 보안 경계 역할을 하며, 직접적으로 노출되면 안 되는 리소스들을 보호합니다.

일반적으로 클라우드에서는 다음과 같은 서브넷 구조를 사용합니다.

  • Public Subnet: 인터넷과 통신 가능한 공간으로, 주로 bastion 서버, NAT 게이트웨이, ALB 등을 배치
  • Private Subnet: 외부와 직접 통신하지 않는 내부 네트워크로, DB, 내부 API 서버, Kubernetes 노드 등을 배치

Bastion 서버는 Public Subnet에 위치하며, 운영자는 bastion 서버를 통해서만 Private Subnet에 있는 자원에 접근할 수 있도록 구성합니다.

이 글에서는 ubuntu 인스턴스 기반 bastion 서버를 자동으로 초기화하고, Nginx Proxy Manager를 설치하여 웹 기반 포트 포워딩 및 리버스 프록시 관리가 가능한 환경을 구성합니다.

📁 구성 결과

http://<서버 IP>:81 접속하면 아래와 같은 Nginx Proxy Manager 관리자 UI에 접속할 수 있습니다.

Nginx Proxy Manager란?

Nginx Proxy Manager는 Nginx 기반의 리버스 프록시 서버를 웹 UI로 간편하게 관리할 수 있는 오픈소스 도구입니다. 복잡한 nginx.conf 파일을 직접 수정하지 않고도, 웹 인터페이스를 통해 클릭 몇 번으로 프록시와 포트포워딩 설정을 완료할 수 있습니다.

주요 기능

  • 도메인 기반 라우팅
  • 포트 포워딩 설정
  • SSL 인증서 자동 발급 및 갱신
  • 리다이렉션, 접근 제어 설정

왜 Nginx Proxy Manager를 선택했을까?

  • 매번 SSH 접속 후 수동으로 포트포워딩 설정하는 과정이 번거로움
  • 많은 인스턴스를 관리해야 할 때, 각 내부 서비스에 대한 포트 매핑을 쉽게 관리하고 싶었음

그래서 Nginx Proxy Manager를 bastion 서버에 설치하고, Private Subnet에 위치한 여러 인스턴스의 SSH 포트를 각각 외부에 고정 포트로 노출했습니다.

SSH 접속 예시

ssh -i my-instance-key.pem -p port-number ubuntu@bastion-ip

‼️ 포트포워딩을 하지 않으면 각 인스턴스의 내부 IP를 기억하고 있어야 하며, 복수 서버 접속 시 번거로움이 발생합니다.

  • 매번 ssh -J (Jump Host) 옵션을 사용
  • Bastion 서버에 먼저 SSH로 접속한 뒤, 다시 내부 인스턴스로 SSH 접속

주요 스크립트 구성

1. Docker + Nginx Proxy Manager 구성

docker-compose.yaml을 통해 두 개의 컨테이너가 실행됩니다.

  • jc21/nginx-proxy-manager: 관리자 UI 포함 프록시 서버
  • jc21/mariadb-aria: 내부 설정 저장용 DB
services:
  app:
    image: 'jc21/nginx-proxy-manager:2.9.20'
    ports:
      - '80:80'
      - '443:443'
      - '81:81'
      - '10000-10199:10000-10199'
    environment:
      DB_MYSQL_HOST: "db"
      DB_MYSQL_USER: "npm"
      ...
  db:
    image: 'jc21/mariadb-aria:latest'
    environment:
      MYSQL_ROOT_PASSWORD: 'npm'

2. 방화벽 설정

불필요한 인바운드 트래픽을 모두 차단하고, bastion + Nginx Proxy Manager 운영에 필요한 포트만 명시적으로 허용합니다.

sudo ufw default deny incoming      # 들어오는 모든 요청 기본 차단
sudo ufw default allow outgoing     # 나가는 트래픽은 허용

# 꼭 필요한 포트만 허용
sudo ufw allow 22/tcp               # SSH (운영자 접속)
sudo ufw allow 80/tcp               # HTTP 
sudo ufw allow 443/tcp              # HTTPS 
sudo ufw allow 81/tcp               # Nginx Proxy Manager 관리자 UI
sudo ufw allow 10000:10199/tcp      # 포트포워딩용 범위

⚙️ 전체 script

#!/bin/bash

# =============================================================================
# Bastion 서버 초기화 스크립트
# =============================================================================

# 로그 설정
LOG_FILE="/var/log/bastion-init.log"
exec > >(tee -a "$LOG_FILE")
exec 2>&1

# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# 로깅 함수
log_info() {
    echo -e "${GREEN}[INFO $(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN $(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR $(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}

log_step() {
    echo -e "${BLUE}[STEP $(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}

# 에러 처리 함수
handle_error() {
    log_error "$1"
    log_error "스크립트 실행 실패. 로그를 확인하세요: $LOG_FILE"
    exit 1
}

# 재시도 함수
retry_command() {
    local max_attempts=$1
    local delay=$2
    shift 2
    local command="$@"
    local attempt=1
    
    while [ $attempt -le $max_attempts ]; do
        log_info "명령 실행 시도 ($attempt/$max_attempts): $command"
        if eval "$command"; then
            log_info "명령 실행 성공"
            return 0
        else
            if [ $attempt -eq $max_attempts ]; then
                log_error "명령 실행 실패 (최대 시도 횟수 초과): $command"
                return 1
            fi
            log_warn "명령 실행 실패. ${delay}초 후 재시도..."
            sleep $delay
            ((attempt++))
        fi
    done
}

# 시스템 정보 출력
print_system_info() {
    log_step "시스템 정보 확인"
    echo "======================================"
    echo "호스트명: $(hostname)"
    echo "OS: $(lsb_release -d | cut -f2)"
    echo "커널: $(uname -r)"
    echo "아키텍처: $(uname -m)"
    echo "CPU 코어: $(nproc)"
    echo "총 메모리: $(free -h | awk '/^Mem:/ {print $2}')"
    echo "사용 가능한 메모리: $(free -h | awk '/^Mem:/ {print $7}')"
    echo "디스크 사용량: $(df -h / | awk 'NR==2 {print $3"/"$2" ("$5")"}')"
    echo "======================================"
}

# APT 업데이트
update_system() {
    log_step "시스템 업데이트"
    
    # APT 업데이트
    log_info "패키지 목록 업데이트 중..."
    retry_command 3 10 "sudo apt-get update" || handle_error "APT 업데이트 실패"
    
    # 필수 패키지 설치
    log_info "필수 패키지 설치 중..."
    retry_command 3 5 "sudo apt-get install -y curl wget git htop vim nano unzip" || handle_error "필수 패키지 설치 실패"
    
    log_info "✅ 시스템 업데이트 완료"
}

# Docker 설치
install_docker() {
    log_step "Docker 설치"
    
    # Docker가 이미 설치되어 있는지 확인
    if command -v docker &> /dev/null; then
        local docker_version=$(docker --version)
        log_info "✅ Docker가 이미 설치되어 있습니다: $docker_version"
        return 0
    fi
    
    # 필수 패키지 설치
    log_info "Docker 설치를 위한 필수 패키지 설치 중..."
    retry_command 3 5 "sudo apt-get install -y ca-certificates curl gnupg lsb-release" || handle_error "필수 패키지 설치 실패"
    
    # Docker GPG 키 추가
    log_info "Docker GPG 키 추가 중..."
    sudo mkdir -p /etc/apt/keyrings
    retry_command 3 5 "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg" || handle_error "Docker GPG 키 추가 실패"
    
    # Docker 저장소 추가
    log_info "Docker 저장소 추가 중..."
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    # 패키지 목록 업데이트
    retry_command 3 5 "sudo apt-get update" || handle_error "Docker 저장소 업데이트 실패"
    
    # Docker 설치
    log_info "Docker 설치 중..."
    retry_command 3 10 "sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin" || handle_error "Docker 설치 실패"
    
    # Docker Compose 설치
    log_info "Docker Compose 설치 중..."
    retry_command 3 5 "sudo apt-get install -y docker-compose" || log_warn "Docker Compose standalone 설치 실패"
    
    # Docker 서비스 설정
    log_info "Docker 서비스 설정 중..."
    sudo systemctl start docker || handle_error "Docker 서비스 시작 실패"
    sudo systemctl enable docker || handle_error "Docker 서비스 자동 시작 설정 실패"
    
    # 사용자를 docker 그룹에 추가
    sudo usermod -aG docker ubuntu || handle_error "사용자 docker 그룹 추가 실패"
    
    log_info "✅ Docker 설치 완료"
}

# Nginx Proxy Manager 설정
setup_nginx_proxy_manager() {
    log_step "Nginx Proxy Manager 설정"
    
    local npm_dir="$HOME/nginx-proxy-manager"
    
    # 디렉토리 생성
    log_info "Nginx Proxy Manager 디렉토리 생성 중..."
    mkdir -p "$npm_dir"
    cd "$npm_dir"
    
    # Docker Compose 파일 생성
    log_info "Docker Compose 설정 파일 생성 중..."
    cat << 'EOF' > docker-compose.yaml
version: "3.8"
services:
  app:
    image: 'jc21/nginx-proxy-manager:2.9.20'
    restart: unless-stopped
    ports:
      - '80:80'      # HTTP
      - '443:443'    # HTTPS
      - '81:81'      # Admin Web UI
      - '10000-10199:10000-10199'  # Port forwarding range
    environment:
      DB_MYSQL_HOST: "db"
      DB_MYSQL_PORT: 3306
      DB_MYSQL_USER: "npm"
      DB_MYSQL_PASSWORD: "npm"
      DB_MYSQL_NAME: "npm"
      DISABLE_IPV6: 'true'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - db

  db:
    image: 'jc21/mariadb-aria:latest'
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: 'npm'
      MYSQL_DATABASE: 'npm'
      MYSQL_USER: 'npm'
      MYSQL_PASSWORD: 'npm'
    volumes:
      - ./data/mysql:/var/lib/mysql
EOF
    
    # Docker Compose 실행
    log_info "Nginx Proxy Manager 시작 중..."
    sudo docker-compose up -d || handle_error "Nginx Proxy Manager 시작 실패"
    
    # 서비스 시작 대기
    log_info "서비스 시작 대기 중..."
    sleep 30
    
    # 컨테이너 상태 확인
    log_info "컨테이너 상태:"
    sudo docker-compose ps
    
    log_info "✅ Nginx Proxy Manager 설치 완료"
    log_info "관리 페이지: http://$(curl -s ifconfig.me):81"
    log_info "기본 로그인: admin@example.com / changeme"
}

# 방화벽 설정
setup_firewall() {
    log_step "방화벽 설정"
    
    # UFW 설치 및 설정
    sudo apt-get install -y ufw
    
    # 기본 정책 설정
    sudo ufw --force reset
    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    
    # 필요한 포트 허용
    sudo ufw allow 22/tcp      # SSH
    sudo ufw allow 80/tcp      # HTTP
    sudo ufw allow 443/tcp     # HTTPS
    sudo ufw allow 81/tcp      # NPM Admin
    sudo ufw allow 10000:10199/tcp  # Port forwarding range
    
    # UFW 활성화
    sudo ufw --force enable
    
    log_info "✅ 방화벽 설정 완료"
    sudo ufw status
}

# 메인 실행 함수
main() {
    log_info "🚀 Bastion 서버 초기화 시작"
    
    print_system_info
    update_system
    install_docker
    setup_nginx_proxy_manager
    setup_firewall
    
    log_info "✅ Bastion 서버 초기화 완료!"
    log_info "📋 다음 단계:"
    log_info "1. Nginx Proxy Manager 관리 페이지 접속: http://$(curl -s ifconfig.me):81"
    log_info "2. 기본 로그인: admin@example.com / changeme"
    log_info "3. 포트 포워딩 설정 진행"
}

# 스크립트 실행
main "$@" 

🚀 실행 방법

# 권한 부여
chmod +x bastion-init.sh

# 초기화 실행
sudo ./bastion-init.sh

0개의 댓글