카테고리 보관물: Linux

[Tip] 포트 포워딩 설정 상태에서 VNC SSH Tunneling

VNC SSH Tunneling에 대해 다룬 적이 있었는데, macOS에서 기본 제공되는 Screen Sharing 같은 app으로 접속하려면 SSH 터널링을 설정하는 GUI가 없으므로 해당 포스팅의 “SSH Tunneling 설정이 없는 경우” 항목의 안내에 따라 다음과 같은 명령어를 터미널에 입력하고 localhost의 5999번 포트로 연결을 시도하면 된다고 했었다.

# SSH 터널링.
# 22번 SSH 포트를 통해 리모트의 5901번 VNC 포트를 로컬의 5999번 포트에 연결
ssh -L 5999:localhost:5901 <user_id>@<vnc_server_ip>

그럼 만약 22번이 아닌 포트 포워딩을 사용하고 있는 경우라면 어떻게 해야 할까?

사내에 있는 워크스테이션들에 각각 65530 부터 65531까지 포트로 access할 때 포워딩이 이루어 지도록 설정해 둔 상태라고 하면, 22번 포트가 아닌 특정한 포트로 포워딩을 수행하고 있으므로 이 값을 -p 옵션과 함께 작성해 주어야 한다. 예를 들어 포워딩하고 있는 포트의 번호가 65530이고 이를 통해 5901에서 돌고 있는 VNC를 내 localhost의 5999번 포트에 연결하고자 한다면 명령어는 다음과 같다.

# SSH 터널링.
# 65530번 SSH 포트를 통해 리모트의 5901번 VNC 포트를 로컬의 5999번 포트에 연결
ssh -L 5999:localhost:5901 -p 65530 <user_id>@<vnc_server_ip>

그리고 나서 macOS의 Screen Sharing에서는 다음과 같이 설정하고 VNC로 접속한다.

명령어가 너무 길다면

~/.ssh/config 환경 설정파일에 LocalForward를 다음과 같이 추가해 주면 매번 긴 명령어를 타이핑하지 않아도 된다.

# ~/.ssh/config
Host <host_name>
  HostName <vnc_server_ip>
  Port 65530
  User <user_id>
  LocalForward 5999 localhost:5901

이후 부터는 터미널에서 간단히 ssh <host_name> 명령어만 수행해도 Screen Sharing을 통해 VNC로 접속할 수 있다.

OpenProject를 Docker로 설치

프로젝트 관리 도구로 많이 사용되는 Atlassian의 JIRA를 대체하기 위한 오픈소스 프로젝트들 중 가장 인기가 있다는 OpenProject를 회사에서 진행하는 프로젝트를 위해 사용해 보기로 했다.

Docker로 설치하기 준비

Source code clone

OpenProject는 GitHub repo에서 tag가 아닌 branch명을 stable/<version> 형식으로 릴리즈하고 있는 듯 하다. 2026년 4월 현재 가장 최신 버전인 17버전을 다음과 같이 GitHub에서 가져온다.

git clone https://github.com/opf/openproject-docker-compose.git --branch=stable/17 openproject
cd openproject

환경 설정 파일 수정

다운로드 받은 소스에는 환경 설정파일 예제인 .env.example 파일이 있는데, 이 파일 이름을 .env로 변경하고 필요한 사항을 수정해 준다.

  • SECRETE_KEY_BASE에 값을 안내된 명령어 결과 값으로 반영, 단 64 character
    • head /dev/urandom | tr -dc A-Za-z0-9 | head -c 64 ; echo
  • OPENPROJECT_HOST__NAME 값을 localhost에서 IP 혹은 domain이름으로 변경
  • PORT값을 모든 접속을 허용하도록 0.0.0.0:8080으로 변경
  • POSTGRES_VERSION17에서 13으로 변경 (Ubuntu에 설치된 버전)
##
# All environment variables defined here will only apply if you pass them
# to the OpenProject container in docker-compose.yml under x-op-app -> environment.
# For the examples here this is already the case.
#
# Please refer to our documentation to see all possible variables:
#   https://www.openproject.org/docs/installation-and-operations/configuration/environment/
#
TAG=17-slim
OPENPROJECT_HTTPS=false
# Please override the SECRET_KEY_BASE
# Please use a pseudo-random value for this and treat it like a password.
# If you are on Linux you can generate a scret key with the following command
# head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo ''
SECRET_KEY_BASE=<명령 수행 결과>
OPENPROJECT_HOST__NAME=<서버 IP>
PORT=0.0.0.0:8080
OPENPROJECT_RAILS__RELATIVE__URL__ROOT=
IMAP_ENABLED=false
DATABASE_URL=postgres://postgres:<database 비밀번호>@db/openproject?pool=20&encoding=unicode&reconnect=true
RAILS_MIN_THREADS=4
RAILS_MAX_THREADS=16
PGDATA="/var/lib/postgresql/data"
OPDATA="/var/openproject/assets"
COLLABORATIVE_SERVER_URL=ws://localhost:8080/hocuspocus
COLLABORATIVE_SERVER_SECRET=secret12345
POSTGRES_VERSION=13

Docker 실행

환경 설정이 끝나면 docker 명령어로 실행 시켜준다.

docker compose up -d

접속확인

이제 웹브라우져에서 ip나 도메인 이름을 입력하고 접속하면 다음과 같이 창이 뜨는데 기본 로그인 id / password는 admin/admin이고 접속하면 비밀번호를 설정하는 화면으로 넘어가게 된다.

HTTPS 적용

HTTPS를 위한 유료 인증서를 사용하고 싶지 않다면 Let’s encrypt를 고려할 텐데, 이 서비스는 public IP를 기준으로 인증서를 발급하는데 제한을 두고 있다. 정확히는 2026년 초부터 IP를 대상으로 발급하는 인증서를 제공하는데 유효기간이 6일로 매우 짧다.

Renew script에 의해 업데이트 되기는 하겠지만, 장기적으로 사용할 것 이라면 도메인을 사용하는게 여러모로 낫다. 대부분의 도메인 등록 기관에서는 기존 도메인이 있다면 추가 비용 없이 서브 도메인 등록이 가능하도록 하고 있으니 이 것을 활용하는 것도 좋을것 같다.

.env 변경

서브 도메인과 IP를 연결한 후에는 .env 파일도 조금 변경해 주어야 한다. 다른 설정들을 유지하고 다음의 세가지 설정을 변경해 준다.

...
OPENPROJECT_HTTPS=true
...
OPENPROJECT_HOST__NAME=<서브 도메인>
PORT=8080
...
  • OPENPROJECT_HTTPS를 true로 설정
  • OPENPROJECT_HOST__NAME을 서브도메인으로 설정
  • PORT를 포트번호만 8080으로 설정

docker-compose.yml 변경

proxy section에 ports 부분에 80번과 함께 https를 위한 443번을 추가해 준다.

  proxy:
    build:
      context: ./proxy
      args:
        APP_HOST: web
    image: openproject/proxy
    <<: *restart_policy
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - web
    networks:
      - frontend

Caddyfile.template 변경

소스코드와 함께 오는 Caddyfile.template은 예제 파일 인 줄 알고 복사해서 Caddyfile로 작성했다가 한참을 해맸는데 proxy/Caddyfile.template 파일을 직접 참조하기 때문에 파일명을 변경하지 않고 직접 수정해 주어야 한다. 다른 설정을다 날리고 아래의 세 줄만 남겼다.

{$OPENPROJECT_HOST__NAME} {
    reverse_proxy web:8080
}

proxy docker 재빌드 및 실행

방화벽이 80번과 443 포트를 열어 주는지 다시한번 확인하고,

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload

proxy docker를 다시 빌드 하고 실행 한다.

docker compose stop proxy
docker compose build --no-cache proxy
docker compose up -d proxy

이메일 설정

OpenProject에서 변경되는 notification을 이메일로 전송받도록 하기 위해서는 메일 설정을 해야 하는데, Postfix로 로컬에 SMTP 서버 설치를 시도 했다가 생각보다 복잡하고 번거로운 스팸 필터 피하는 절차들 때문에 결국 PTR(reverse DNS)를 설정하는 즈음에서 포기하고 말았다.

AWS의 SES같은 메일 서비스는 복잡한 설정 없이도 동작 할 수 있지만 전송량을 생각해보니 한달에 대략 5천원 정도가 나올것 같았다.

이 마저도 아끼려고 Gmail의 SMTP 서버를 사용하도록 등록해 주었다. 인증을 위해서는 “앱 비밀번호”를 생성해서 환경설정 파일에 넣어 주어야 한다.

다만 이 때는 메일을 수신하는 수신자가 회사 시스템으로 부터가 아닌 Gmail에 등록된 내 개인 계정으로 부터 noti를 받게 된다는 사소한 단점이 하나 있다.

뿐만 아니라 일일 사용량에 제한이 있으니 프로젝트 규모가 카서 이메일 전송량이 많은 경우라면 앞서 말한 AWS SES 같은 다른 메일 서비스들을 고려해 보는게 좋겠다.

Lightsail 서버가 자꾸 죽어요

Jetpack이 요즘처럼 많이 말을 건 적이 있었던가?

최근에 Bitnami에서 Amazon Linux 2023으로 이사하면서 Lightsail instance를 한 단계 비싼 것으로 올리고 나서부터 웹사이트가 다운되었다는 Jetpack의 노티가 계속 뜬다.

더 비싸면 더 잘돌아야 되는 것 아닌가?

이런 노티를 받고나서 확인해 보면 블로그가 접속되지 않을 뿐만 아니라 SSH 조차도 뜨지 않는 상태가 되어 있어서 서버를 강제 재시작 하는 방법 밖에 없다. Lightsail의 Metrics 메뉴에서 확인해 보면 이때마다 CPU 사용률이 치솟고 있는 것도 보인다.

적습! – XML RPC

따로 cron task를 걸어 놓은 것도 없는데 이렇게 많은 CPU 자원이 소모되는 이유는 뭘까?

Nginx의 access log를 들여다 봤더니 짧은시간 동안에 한 IP에서 아주 많은 xmlrpc.php에 대한 접속시도가 있었다. XML RPC는 REST API로 대체되어 요즘에는 실제로 사용되는 경우가 거의 없는데, 공격자들은 system.multicall 기능을 활용해 다수의 비밀번호 무차별 대입 공격을 수행하는데 자주 쓴다고 한다.

xmlrpc를 막는 여러가지 방법이 있으나, WordPress에서 막지 않고 아예 Nginx에서 xmlrpc접속을 막아버리도록 다음과 같이 설정해 주었다.

# xmlrpc.php 차단
location = /xmlrpc.php {
    deny all;          # xmlrpc.php 접속을 차단
    access_log off;    # 접속 로그를 남기지 않아서 IO 자원을 아낌
    log_not_found off; # Not found(404) 로그도 남기지 않는다
    return 444;        # Nginx 비표준, 404 응답을 보내지 않고 연결을 끊어버림
}

두번째 적습! – Search Flood

이렇게 막고 나서 하루 정도는 잠잠했었는데, 바로 다음날 저녁에 다시 서버에 접속할 수 없다는 Jetpack의 알람이 왔다.

이번에도 CPU 사용량이 치솟으며 SSH접속도 안될 정도로 무언가를 엄청나게 하고 있었다. 분명히 xmlrpc는 막아 두었는데 이번엔 또 뭘까?

로그를 보니 이번에도 하나의 IP에서 짧은 시간동안에 태그, 검색어, 저자, 문서번호 등으로 엄청난 조회(GET) 요청을 받고 있었다. 이번 것은 XML RPC 공격때와 같이 로그인 비밀번호를 알아 내기 위한 것 보다는 서버에 많은 부하를 주어서 서비스를 방해하려는 목적인 것 같다.

외부에서 들어오는 조회가 진짜인지 가짜인지 확인하는 뾰족한 방법은 없고, 다만 너무 잦은 것이 문제가 되는 상황이니 Rate limiting을 걸어서 이런 경우를 걸러내기로 했다.

Nginx 설정파일을 변경해서 먼저 http 영역에서 비정상적인 검색을 시도하는 IP들을 추적하도록 설정한다.

http {
    ...

    # 검색을 요청하는 경우 IP를 저장해 둔다.
    map $arg_s $search_traffic {
        default "";              # 일반적인 접속, track안함.
        ~.+ $binary_remote_addr; # 검색요청하는 IP는 track.
    }

    # 분당 10회 이상의 검색 시도가 있으면 $search_traffic zone에 추가.
    limit_req_zone $search_traffic zone=search_block:10m rate=10r/m;
    ...
}

그리고 나서 server 영역에는 이러한 시도가 5번을 넘기면 차단하도록 다음과 같이 설정한다. 해당 IP는 Service Unavailable(503) 응답을 받게 될 것이다.

server {
    ...
    location / {
        # 5번까지의 burst시도까지는 허가.
        limit_req zone=search_block burst=5 nodelay;
    
        try_files $uri $uri/ /index.php$is_args$args;
    }
    ...
}

Swap 설정 추가

그리고 조금 놀라웠던 사실인데 AL2023 instance에는 기본적으로 swap 영역이 설정되어 있지 않았다. 서비스가 조금 느려지더라도 응답 못하는 일은 없도록 swap영역을 설정해 주었다. swap 설정하는 방법은 인터넷에 많으니 패스.

결론

Bitnami에서 설정해 준 것으로 안락하게 지내다가, 직접 서버를 설정하겠다고 나선지 며칠만에 무작위 공격들로 가득찬 인터넷을 겪고 보니 새삼 살벌한 세상이 실감된다.

하지만 그 덕에 다양한 공격 방법들과 설정 방법들에 대해 더 알게되니 재미있기도 하다. 상용서비스가 아니라서 문제가 생기면 재부팅이라도 할 수 있으니 그나마 다행이랄까.