Cloudflare Tunnel로 WordPress 공개하기: 포트포워딩 없이 wp-admin 보호까지

홈서버와 클라우드 사이의 보호된 연결을 표현한 대표 이미지

작성자

카테고리:

2편을 마쳤을 때 MariaDB와 WordPress 컨테이너는 둘 다 healthy였지만 브라우저에서는 사이트가 열리지 않았다. 실패한 것이 아니라 의도한 상태였다. WordPress의 80번 포트를 Ubuntu나 공유기에 공개하지 않았기 때문이다.

이번에는 그 상태를 그대로 유지한 채 cloudflared 컨테이너만 추가한다. 공유기에서 80·443 포트를 열어 외부 요청을 직접 받는 대신, cloudflared가 Cloudflare 쪽으로 먼저 연결하고 방문 요청을 WordPress에 전달하게 한다.

가장 신경 쓴 부분은 Tunnel을 켜는 명령보다 WordPress 초기 설치 화면을 언제 공개할지였다. 공개 주소부터 만든 다음 관리자 계정을 천천히 설정하면, 그 사이 초기 설치 화면이 인터넷에 노출될 수 있다. 그래서 먼저 사이트 전체에 임시 Cloudflare Access를 만들고, WordPress 설치가 끝난 뒤 공개 글과 관리자 경로를 분리했다.

공유기 포트를 열지 않고 Cloudflare Tunnel로 WordPress를 공개하는 구성

그림 1. 방문자는 Cloudflare를 거쳐 접속하고, Ubuntu 안의 cloudflared가 이미 만들어 둔 연결을 통해 WordPress로 요청을 전달한다. MariaDB는 외부 요청 경로에 연결하지 않는다.

이번 글을 끝내면 다음 상태가 된다.

  • https://내도메인으로 WordPress가 열린다.
  • 공유기·Mac·Ubuntu에 웹 포트포워딩을 만들지 않는다.
  • Tunnel 토큰은 명령줄이나 .env가 아니라 권한이 제한된 파일에 저장된다.
  • WordPress 초기 설치 중에는 사이트 전체를 소유자만 볼 수 있다.
  • 설치 후 홈페이지와 글은 로그인 없이 공개된다.
  • /wp-login.php, /wp-admin, /wp-admin/*는 Cloudflare Access 인증을 먼저 거친다.
  • /wp-admin/admin-ajax.php는 기본적으로 보호하고, 로그인하지 않은 공개 기능이 실제로 이 경로를 써야 할 때만 정확한 경로에 Access 예외를 둔다.

여기서 Access는 브라우저로 들어오는 관리자 입구에 한 겹을 더하는 장치다. WordPress 계정과 비밀번호를 대신하지 않으며, XML-RPC나 REST API처럼 별도 경로를 사용하는 모든 인증 요청까지 자동으로 보호하는 구성도 아니다.

전체 구조와 이 방식을 선택한 이유는 맥미니를 24시간 홈서버로 써도 될까?에서 먼저 볼 수 있다. Ubuntu VM과 Docker 구성이 아직 없다면 1편과 2편부터 진행해야 한다.

이 글은 2026년 9월 1일의 Cloudflare 화면과 실제 구성에서 사용한 cloudflare/cloudflared:2026.8.2를 기준으로 한다. Cloudflare 메뉴 이름은 계정별 UI 전환 시점에 따라 조금 다를 수 있고, 컨테이너 버전은 나중에 바뀔 수 있다.

시작 전에 확인할 조건

이 글은 2편의 완료 상태를 전제로 한다.

  • Ubuntu Server 24.04 LTS ARM64 VM이 실행 중이다.
  • Mac에서 Ubuntu로 SSH 키 접속이 된다.
  • /opt/ttakdap에 .env, compose.yaml, config, secrets가 있다.
  • db와 wordpress 컨테이너가 모두 healthy다.
  • wordpress와 db에 호스트 공개 포트가 없다.
  • 사용할 도메인이 Cloudflare 계정에 추가돼 있고 Zone 상태가 Active다.
  • Cloudflare DNS를 쓰도록 도메인의 네임서버 변경이 끝났다.
  • 공유기와 Ubuntu에 80·443 포트포워딩을 만들지 않았다.

도메인 상태가 아직 Pending이면 Tunnel 설정보다 도메인 등록기관의 네임서버부터 확인한다. 기존 DNS 레코드를 정리할 때는 같은 호스트명의 A·AAAA·CNAME 충돌만 살펴보고, 메일에 쓰는 MX·TXT 레코드는 용도를 모른 채 지우면 안 된다.

작업 시간은 Cloudflare 계정과 도메인이 준비된 상태에서 약 45~75분으로 잡는 편이 좋다. 인증서나 DNS 반영 시간은 계정과 도메인 상태에 따라 달라질 수 있다.

먼저 구분할 것: Cloudflare 화면도 두 군데다

이번 작업에서는 Ubuntu 터미널뿐 아니라 Cloudflare 화면 위치도 헷갈리기 쉽다. Tunnel과 Access가 서로 다른 메뉴에 있기 때문이다.

화면이 글에서 하는 일2026년 기준 메뉴 예시
일반 Cloudflare 대시보드Tunnel, DNS, HTTPS, RedirectNetworking → Tunnels, DNS, SSL/TLS, Rules
Cloudflare Zero Trust로그인 정책과 보호 경로Access controls → Applications, Integrations → Identity providers

일부 계정에서는 Tunnel이 Zero Trust의 Networks → Connectors에 보이거나, Published application이 이전 이름인 Public Hostnames로 보일 수 있다. 메뉴 이름이 조금 달라도 원격 관리 Tunnel, Published application, Self-hosted Access application이라는 기능을 찾으면 된다.

Cloudflare의 세 기능도 역할이 다르다.

기능하는 일하지 않는 일
DNS도메인 요청을 Cloudflare로 안내WordPress 컨테이너에 직접 연결하지 않음
TunnelCloudflare와 cloudflared 사이 연결로 요청을 운반누가 관리자 화면에 들어갈지 결정하지 않음
Access특정 사용자·경로의 통과 여부를 결정WordPress 자체 관리자 비밀번호를 대신하지 않음

Access를 통과한 뒤 WordPress 로그인 화면이 다시 나오는 것은 정상이다. Cloudflare Access와 WordPress 계정은 서로 다른 두 단계다.

명령을 입력하는 화면 확인하기

코드 블록 위에 실행 장소를 표시한다.

글의 표시명령을 입력하는 화면명령이 적용되는 곳
Mac에서 실행macOS 터미널외부 주소 점검
SSH로 Ubuntu에서 실행Mac 터미널에서 Ubuntu에 접속한 화면Ubuntu VM과 Docker
Cloudflare 화면에서 설정웹브라우저의 Cloudflare 대시보드DNS·Tunnel·Access

지금 터미널 위치가 헷갈리면 먼저 확인한다.

uname -s
pwd

Linux와 /home/... 또는 /opt/...가 보이면 Ubuntu다. 이 글의 Docker 명령은 Linux가 표시되는 SSH 화면에서 실행한다.

sudo 비밀번호나 Tunnel 토큰을 입력할 때 화면에 글자나 별표가 보이지 않는 것은 정상이다. 비밀번호와 토큰을 채팅창에 보내거나 코드 블록 뒤에 붙이면 안 된다.

0. 공개 전 상태와 복구 지점 확인하기

Tunnel을 추가하기 전에 2편의 내부 구동 상태를 다시 확인한다. 이 단계가 통과해야 나중에 문제가 생겼을 때 Tunnel 쪽과 WordPress 쪽을 나눠 볼 수 있다.

[SSH로 Ubuntu에서 실행]

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  config --quiet &&
echo "기존 Compose 문법 확인 완료"

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  ps

db와 wordpress가 모두 healthy여야 한다. Restarting, unhealthy, Exit가 보이면 Tunnel을 추가하지 말고 2편의 상태 검사부터 다시 확인한다.

호스트에 공개된 포트가 없는지도 다시 확인한다.

[SSH로 Ubuntu에서 실행]

PUBLISHED_PORTS="$(
  for CONTAINER_ID in $(
    sudo docker compose \
      --env-file /opt/ttakdap/.env \
      -f /opt/ttakdap/compose.yaml \
      ps -q db wordpress
  ); do
    sudo docker port "$CONTAINER_ID"
  done
)"

if [[ -z "$PUBLISHED_PORTS" ]]; then
  echo "호스트 공개 포트 없음"
else
  echo "확인 필요한 포트 매핑:"
  printf '%s\n' "$PUBLISHED_PORTS"
fi

unset PUBLISHED_PORTS CONTAINER_ID

정상 결과는 호스트 공개 포트 없음이다.

Compose 원본도 별도로 보관한다. 같은 이름의 백업을 덮어쓰지 않도록 한 번만 만든다.

[SSH로 Ubuntu에서 실행]

if sudo test -e /opt/ttakdap/compose.before-cloudflared.yaml; then
  echo "기존 Compose 백업을 그대로 보존합니다."
else
  sudo cp -a \
    /opt/ttakdap/compose.yaml \
    /opt/ttakdap/compose.before-cloudflared.yaml
  echo "Tunnel 추가 전 Compose 백업 완료"
fi

Parallels에서는 Ubuntu를 정상 종료한 뒤 2편 내부 구동 완료 같은 이름으로 스냅샷을 하나 더 만들어 두면 좋다. Compose 백업은 설정 파일만 되돌리고, VM 스냅샷은 Ubuntu 전체 상태를 되돌린다는 차이가 있다.

1. 공개 주소보다 임시 Access를 먼저 만들기

Cloudflare 공식 안내도 공개 호스트를 Tunnel에 연결하기 전에 Access 애플리케이션을 먼저 만들 것을 권장한다. Published application을 저장하는 순간 도메인이 인터넷에서 열릴 수 있기 때문이다.

이 단계에서는 WordPress 관리자 경로만 고르는 것이 아니라 사이트 전체를 임시로 보호한다. 아직 WordPress 관리자 계정을 만들지 않았기 때문이다.

One-time PIN 로그인 준비

Google Workspace나 Microsoft Entra ID 같은 별도 로그인 공급자를 이미 연결했다면 그 IdP를 사용해도 된다. 개인 사이트에서 이메일로 일회용 코드를 받을 경우에는 Zero Trust에서 다음 위치를 확인한다.

[Cloudflare 화면에서 설정]

  1. Zero Trust → Integrations → Identity providers로 이동한다.
  2. Add new identity provider를 누른다.
  3. One-time PIN을 추가한다.

2026년의 새 Zero Trust 조직에는 OTP가 자동으로 추가되지 않을 수 있다. Access 애플리케이션에서 로그인 방식 목록에 One-time PIN이 없다면 이 단계부터 진행한다.

사이트 전체 임시 보호 만들기

[Cloudflare 화면에서 설정]

  1. Zero Trust → Access controls → Applications로 이동한다.
  2. Create new application을 누른다.
  3. Self-hosted and private를 선택한다.
  4. 이름을 WordPress 초기 설치 임시 보호처럼 알아보기 쉽게 적는다.
  5. Add public hostname에서 사용할 도메인을 선택한다.
  6. 사이트 전체가 대상이 되도록 Path는 비워 둔다.
  7. 정책 Action은 Allow로 둔다.
  8. Include는 Emails를 선택하고 실제 소유자 이메일을 정확히 입력한다.
  9. 로그인 방식으로 One-time PIN 또는 사용 중인 IdP를 선택한다.
  10. 애플리케이션을 저장한다.

Include → Login Methods → One-time PIN만 넣으면 “OTP를 쓸 수 있는 모든 유효한 이메일”을 허용하는 넓은 정책이 될 수 있다. 반드시 Emails에 허용할 주소를 정확히 지정한다.

아직 Published application을 만들지 않았으므로 도메인이 열리지 않아도 정상이다. 이 단계의 목표는 외부 연결이 생기는 순간 먼저 적용될 잠금장치를 준비하는 것이다.

2. 원격 관리 Named Tunnel 만들기

이번 구성은 Cloudflare 대시보드에서 설정을 관리하는 remotely-managed Tunnel을 사용한다. cloudflared tunnel login, 로컬 config.yml, Tunnel UUID의 JSON 자격 증명을 사용하는 locally-managed 절차와 섞지 않는다.

[Cloudflare 화면에서 설정]

  1. 일반 Cloudflare 대시보드의 Networking → Tunnels로 이동한다.
  2. Create a tunnel을 누른다.
  3. Tunnel 이름을 wordpress-home-server처럼 용도가 보이게 적는다.
  4. Tunnel을 만든 뒤 실행 환경에서 Docker를 선택한다.
  5. 화면에 표시된 Docker 실행 명령은 아직 실행하지 않는다.

Docker 명령 안의 --token 뒤에 있는 긴 eyJ... 값이 Tunnel 토큰이다. 이 토큰을 가진 사람은 해당 Tunnel의 connector를 실행할 수 있다.

  • 화면 전체를 캡처하지 않는다.
  • 토큰을 채팅이나 메모 앱에 붙이지 않는다.
  • .env나 compose.yaml에 직접 적지 않는다.
  • 셸 명령 뒤에 토큰을 붙여 실행하지 않는다.
  • 노출했다면 Cloudflare에서 토큰을 회전한 뒤 connector를 다시 만든다.

다음 단계에서 이 긴 값만 복사해 화면에 표시되지 않는 입력으로 저장한다.

3. cloudflared 이미지와 실행 사용자를 먼저 확인하기

이 글에서는 실제 서버에서 검증한 cloudflare/cloudflared:2026.8.2 이미지를 고정해 사용한다. 2026년 9월 1일에는 더 새 태그가 보일 수 있지만, 이 글의 설정과 UID를 확인한 기준은 2026.8.2다.

[SSH로 Ubuntu에서 실행]

sudo docker pull cloudflare/cloudflared:2026.8.2

sudo docker image inspect \
  --format 'cloudflared 실행 사용자={{.Config.User}}' \
  cloudflare/cloudflared:2026.8.2

정상 결과는 다음과 같다.

cloudflared 실행 사용자=65532

뒤에서 토큰 파일의 소유자를 65532:65532로 만드는 이유다. 다른 값이 나오면 아래 권한 명령을 그대로 실행하지 말고, 선택한 이미지의 공식 정보와 실행 사용자를 다시 확인한다.

4. Tunnel 토큰을 권한 제한 파일로 저장하기

이 블록은 Cloudflare가 보여 준 Docker 명령 전체가 아니라, --token 뒤의 긴 값만 입력받는다. read -s를 사용하므로 붙여넣어도 화면과 셸 기록에 토큰이 나타나지 않는다.

[SSH로 Ubuntu에서 실행]

sudo -v
ORIGINAL_UMASK="$(umask)"
umask 077

TOKEN_TMP="$(mktemp)"
trap 'rm -f -- "$TOKEN_TMP"; umask "$ORIGINAL_UMASK"' EXIT HUP INT TERM

IFS= read -r -s -p "Cloudflare Tunnel token 붙여넣기: " TUNNEL_TOKEN
printf '\n'

if (( ${#TUNNEL_TOKEN} < 80 )) ||
   [[ "$TUNNEL_TOKEN" =~ [[:space:]] ]]; then
  unset TUNNEL_TOKEN
  echo "토큰 형식 확인이 필요합니다. 파일은 만들지 않았습니다."
else
  printf '%s' "$TUNNEL_TOKEN" > "$TOKEN_TMP"
  unset TUNNEL_TOKEN

  sudo install -d -m 0700 -o root -g root \
    /opt/ttakdap/secrets

  sudo install -m 0400 -o 65532 -g 65532 \
    "$TOKEN_TMP" \
    /opt/ttakdap/secrets/cloudflare_tunnel_token.txt

  sudo stat \
    -c 'uid=%u gid=%g mode=%a bytes=%s %n' \
    /opt/ttakdap/secrets/cloudflare_tunnel_token.txt
fi

rm -f -- "$TOKEN_TMP"
umask "$ORIGINAL_UMASK"
trap - EXIT HUP INT TERM
unset ORIGINAL_UMASK TOKEN_TMP TUNNEL_TOKEN

정상 결과에는 다음 값이 보인다.

uid=65532 gid=65532 mode=400 bytes=충분히_긴_숫자 /opt/ttakdap/secrets/cloudflare_tunnel_token.txt

바이트 수는 토큰마다 달라질 수 있다. UID와 GID가 65532, mode가 400, bytes가 80보다 큰지만 확인한다. 내용을 확인하겠다며 cat을 실행하면 토큰이 화면과 터미널 기록에 남으므로 하지 않는다.

root:root 0400으로 만들면 비root UID 65532로 실행되는 cloudflared가 읽지 못한다. 반대로 모두가 읽을 수 있는 0644로 만들면 같은 Ubuntu의 다른 사용자와 프로세스에 토큰이 노출될 수 있다.

5. cloudflared를 포함한 Compose 완성본 만들기

긴 YAML 중간에 몇 줄을 끼워 넣으면 들여쓰기 위치를 틀리기 쉽다. 아래에는 2편의 Compose 내용을 유지하면서 cloudflared 서비스와 Tunnel secret만 추가한 전체 파일을 싣는다.

2편에서 만든 ${SITE_URL:?SITE_URL is required in .env} 검사와 WordPress 설정은 그대로 두었다. WP-Cron은 4편에서 systemd 타이머를 만든 뒤 전환하므로 이번 파일에서 미리 비활성화하지 않는다.

아래 블록은 첫 줄부터 마지막 EOF까지 한 번에 복사한다.

[SSH로 Ubuntu에서 실행]

sudo tee /opt/ttakdap/compose.yaml >/dev/null <<'EOF'
name: ttakdap

services:
  db:
    image: mariadb:11.4.13
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD_FILE: /run/secrets/db_password
      MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
      TZ: ${TZ:-Asia/Seoul}
    secrets:
      - db_password
      - db_root_password
    volumes:
      - mariadb_data:/var/lib/mysql
      - ./config/mariadb.cnf:/etc/mysql/conf.d/zz-ttakdap.cnf:ro
    networks:
      - db_internal
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 40s
    stop_grace_period: 1m
    mem_limit: 2g
    pids_limit: 200
    security_opt:
      - no-new-privileges:true
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: "5"

  wordpress:
    image: wordpress:7.1.0-php8.4-apache
    restart: unless-stopped
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-wp_}
      WORDPRESS_CONFIG_EXTRA: |
        define( 'WP_HOME', '${SITE_URL:?SITE_URL is required in .env}' );
        define( 'WP_SITEURL', '${SITE_URL:?SITE_URL is required in .env}' );
        define( 'FORCE_SSL_ADMIN', true );
        define( 'DISALLOW_FILE_EDIT', true );
        define( 'WP_AUTO_UPDATE_CORE', 'minor' );
        define( 'WP_POST_REVISIONS', 20 );
        define( 'WP_ENVIRONMENT_TYPE', 'production' );
        define( 'WP_DEBUG_DISPLAY', false );
        @ini_set( 'display_errors', 0 );
      TZ: ${TZ:-Asia/Seoul}
    secrets:
      - db_password
    volumes:
      - wordpress_data:/var/www/html
      - ./config/php.ini:/usr/local/etc/php/conf.d/zz-ttakdap.ini:ro
    networks:
      - db_internal
      - edge
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          php -r '$$socket = @fsockopen("127.0.0.1", 80);
          exit($$socket ? 0 : 1);'
      interval: 15s
      timeout: 5s
      retries: 12
      start_period: 60s
    stop_grace_period: 30s
    mem_limit: 2g
    pids_limit: 300
    security_opt:
      - no-new-privileges:true
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: "5"

  cloudflared:
    image: cloudflare/cloudflared:2026.8.2
    profiles: ["tunnel"]
    restart: unless-stopped
    command: ["tunnel", "--no-autoupdate", "run"]
    environment:
      TUNNEL_TOKEN_FILE: /run/secrets/cloudflare_tunnel_token
    secrets:
      - cloudflare_tunnel_token
    networks:
      - edge
    depends_on:
      wordpress:
        condition: service_healthy
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=16m
    mem_limit: 256m
    pids_limit: 100
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: "5"

networks:
  db_internal:
    internal: true
  edge:

volumes:
  mariadb_data:
  wordpress_data:

secrets:
  db_password:
    file: ./secrets/db_password.txt
  db_root_password:
    file: ./secrets/db_root_password.txt
  cloudflare_tunnel_token:
    file: ./secrets/cloudflare_tunnel_token.txt
EOF

sudo chmod 0640 /opt/ttakdap/compose.yaml
sudo chown root:root /opt/ttakdap/compose.yaml

추가된 핵심 설정의 의미는 다음과 같다.

설정의미
profiles: ["tunnel"]토큰 준비 전 일반 up 명령으로 Tunnel이 실수로 시작되지 않게 분리
TUNNEL_TOKEN_FILE토큰 값을 Compose나 환경변수 내용에 직접 적지 않고 파일에서 읽음
edgecloudflared와 WordPress만 공유하는 Docker 네트워크
http://wordpress:80뒤에서 Published application이 사용할 Docker 내부 서비스 주소
depends_onWordPress가 healthy가 된 뒤 connector 시작
read_only, cap_drop, no-new-privilegescloudflared 컨테이너 권한과 쓰기 범위를 줄임
--no-autoupdate컨테이너 안에서 자체 갱신하지 않고 검토한 이미지 태그로 교체
ports 없음VM과 공유기에 WordPress·DB 포트를 계속 공개하지 않음

Cloudflared는 edge에만 연결하고 db_internal에는 연결하지 않는다. Tunnel 요청은 WordPress까지 갈 수 있지만 MariaDB로 직접 갈 수 없다.

6. Compose 문법과 토큰 참조 확인하기

파일을 저장했다고 바로 시작하지 않는다. Tunnel profile까지 포함해 YAML과 파일 참조를 먼저 검사한다.

[SSH로 Ubuntu에서 실행]

sudo test -s \
  /opt/ttakdap/secrets/cloudflare_tunnel_token.txt &&
echo "Tunnel token 파일 존재 확인"

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  --profile tunnel \
  config --quiet &&
echo "Tunnel 포함 Compose 문법 확인 완료"

정상 결과는 다음 두 줄이다.

Tunnel token 파일 존재 확인
Tunnel 포함 Compose 문법 확인 완료

이 검사는 토큰이 Cloudflare에서 유효한지까지 확인하지 않는다. 파일 존재와 Compose 문법을 먼저 확인해, connector가 실패했을 때 확인 범위를 줄이는 단계다.

7. cloudflared connector 시작하기

Tunnel profile의 cloudflared만 시작한다. WordPress와 DB는 설정이 바뀌지 않았으므로 상태가 정상이라면 다시 만들 필요가 없다.

[SSH로 Ubuntu에서 실행]

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  --profile tunnel \
  up -d cloudflared

잠시 뒤 세 컨테이너 상태를 본다.

[SSH로 Ubuntu에서 실행]

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  --profile tunnel \
  ps

db와 wordpress는 healthy, cloudflared는 Up 또는 running이어야 한다. Cloudflared에는 이 글에서 별도 healthcheck를 넣지 않았으므로 running만 보고 끝내지 않는다.

재시작 횟수도 확인한다.

[SSH로 Ubuntu에서 실행]

CLOUDFLARED_ID="$(
  sudo docker compose \
    --env-file /opt/ttakdap/.env \
    -f /opt/ttakdap/compose.yaml \
    --profile tunnel \
    ps -q cloudflared
)"

if [[ -n "$CLOUDFLARED_ID" ]]; then
  sudo docker inspect \
    --format '상태={{.State.Status}} 재시작 횟수={{.RestartCount}}' \
    "$CLOUDFLARED_ID"
else
  echo "cloudflared 컨테이너를 찾지 못했습니다."
fi

unset CLOUDFLARED_ID

정상이라면 상태=running 재시작 횟수=0처럼 나온다. 재시작 횟수가 계속 늘면 최근 로그부터 본다.

[SSH로 Ubuntu에서 실행]

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  --profile tunnel \
  logs --tail 50 cloudflared 2>&1 |
sed -E \
  's/eyJ[A-Za-z0-9._=-]+/[TUNNEL_TOKEN_REDACTED]/g'

Registered tunnel connection과 연결 정보가 반복해서 보이면 connector가 Cloudflare에 연결된 것이다. 로그 전체를 그대로 온라인에 올리면 계정·Tunnel·네트워크 식별 정보가 포함될 수 있으므로 필요한 줄만 남기고 가린다.

[Cloudflare 화면에서 확인]

Networking → Tunnels에서 방금 만든 Tunnel 상태가 Healthy인지 확인한다. Healthy는 Cloudflare와 connector가 연결됐다는 뜻이지, 아직 도메인과 WordPress가 정상이라는 뜻은 아니다.

8. Published application을 wordpress:80에 연결하기

Tunnel이 Healthy가 된 뒤 공개 호스트를 추가한다.

[Cloudflare 화면에서 설정]

  1. Networking → Tunnels에서 만든 Tunnel을 선택한다.
  2. Routes 탭을 연다.
  3. Add route → Published application을 선택한다.
  4. Hostname은 루트 도메인을 선택하고 Subdomain과 Path는 비워 둔다.
  5. Service type은 HTTP를 선택한다.
  6. Service URL에는 wordpress:80을 입력한다.
  7. 최종 표시가 http://wordpress:80인지 확인하고 저장한다.

여기서 localhost:80을 입력하면 안 된다. Cloudflared가 별도 컨테이너에서 실행되므로 그 안의 localhost는 WordPress가 아니라 cloudflared 컨테이너 자신이다. 두 컨테이너가 함께 연결된 edge 네트워크에서 Docker 서비스 이름 wordpress를 사용해야 한다.

VM의 사설 IP나 Mac 주소도 넣지 않는다. 컨테이너 이름을 쓰면 VM 주소가 바뀌어도 같은 Compose 네트워크 안에서 WordPress를 찾을 수 있다.

Cloudflare 전체 DNS 설정을 사용하는 도메인이라면 Published application을 저장할 때 <Tunnel UUID>.cfargotunnel.com을 향하는 DNS 레코드가 자동으로 만들어진다. 루트 도메인은 Cloudflare의 CNAME Flattening으로 처리된다.

An A, AAAA, or CNAME record already exists 같은 충돌 메시지가 나오면 같은 루트 호스트명의 기존 A·AAAA·CNAME 용도를 확인한다. 메일용 MX·TXT 레코드는 이 오류를 해결하겠다며 삭제하지 않는다.

저장 직후 도메인에 접속하면 앞에서 만든 임시 Access 로그인 화면이 먼저 나와야 한다. WordPress 초기 설치 화면이 인증 없이 바로 보이면 다음 단계로 가지 말고, 전체 사이트 Access 애플리케이션의 Domain과 Path부터 확인한다.

9. Access를 통과해 WordPress 초기 설치하기

기존 브라우저에는 Cloudflare 로그인 쿠키가 남아 있을 수 있으므로 새 시크릿 창을 사용한다.

[브라우저에서 확인]

  1. 새 시크릿 창에서 https://내도메인을 연다.
  2. Cloudflare Access 로그인 화면이 나오는지 확인한다.
  3. Allow 정책에 등록한 정확한 이메일을 입력한다.
  4. 받은 One-time PIN으로 로그인한다.
  5. 그 다음 WordPress 초기 설치 화면이 나오는지 확인한다.

OTP 메일은 정책에 허용된 이메일에만 발송된다. 허용되지 않은 주소를 입력해도 계정 존재 여부를 숨기기 위해 화면에는 “코드를 보냈다”고 표시될 수 있다.

메일이 오지 않으면 다음 순서로 확인한다.

  • Access Allow 정책의 이메일 철자와 입력한 주소가 완전히 같은지 확인한다.
  • One-time PIN identity provider가 애플리케이션에 선택됐는지 확인한다.
  • 스팸함과 메일 보안 필터를 확인한다.
  • 새 PIN을 요청했다면 이전 PIN은 무효가 됐으므로 가장 최근 코드만 사용한다.
  • PIN은 발급 후 10분 안에 사용한다.

WordPress 설치 화면에서는 다음 항목을 정한다.

항목권장 방식
사이트 제목운영할 실제 사이트 이름
관리자 사용자명admin, 도메인명처럼 추측하기 쉬운 이름을 피함
관리자 비밀번호다른 서비스와 겹치지 않는 긴 무작위 비밀번호
관리자 이메일실제 알림을 받을 주소를 정확히 입력
검색 엔진 표시공개 준비 상태에 맞춰 선택하고 발행 전에 다시 확인

Cloudflare 계정 비밀번호와 WordPress 비밀번호를 같게 만들지 않는다. 생성한 WordPress 비밀번호는 비밀번호 관리자에 저장하고, 비밀번호가 표시된 설치 화면은 캡처하지 않는다.

설치가 끝나면 Access를 통과한 같은 창에서 WordPress 관리자 로그인이 되는지 확인한다. 주소 표시줄이 https://이고, WordPress가 HTTP 주소로 되돌리지 않는지도 본다.

10. 전체 잠금에서 관리자 전용 잠금으로 전환하기

WordPress 설치가 끝났다고 임시 전체 사이트 Access부터 지우면 안 된다. 관리자 보호 경로를 먼저 만든 뒤 새 시크릿 창에서 확인하고, 필요한 공개 예외가 있는지 판단한 다음 마지막에 전체 잠금을 제거한다.

Mac에서 현재 WordPress 공개 페이지와 관리자 경로의 Cloudflare Access 상태를 확인한 결과

그림 2. 2026년 9월 1일 Mac에서 쓰기 없는 HEAD 요청으로 확인한 현재 상태다. 홈페이지는 바로 열리고 로그인·관리자·admin-ajax 경로는 Access 인증을 먼저 거친다. Access 주소와 식별 정보는 화면에서 제외했다.

이 결과에서 ADMIN_AJAX ACCESS_LOGIN은 오류가 아니다. 현재 공개 화면에서 이 경로에 의존하는 기능을 쓰지 않기 때문에 더 넓게 열지 않았다. 댓글, 문의 폼, 검색 필터 같은 공개 기능이 실제로 멈추고 그 요청이 admin-ajax.php로 가는 것이 확인될 때만 아래 예외를 추가한다.

관리자 경로 Access 만들기

[Cloudflare 화면에서 설정]

  1. Zero Trust → Access controls → Applications로 이동한다.
  2. 새 Self-hosted and private 애플리케이션을 만든다.
  3. 이름을 WordPress 관리자 보호로 적는다.
  4. 같은 도메인에 아래 세 경로를 Public hostname으로 추가한다.
  5. 정책 Action은 Allow, Include는 소유자의 정확한 Emails로 설정한다.
  6. One-time PIN 또는 사용 중인 IdP를 선택해 저장한다.
/wp-login.php
/wp-admin
/wp-admin/*

/wp-admin/*만 등록하면 부모 경로인 /wp-admin 자체는 포함되지 않는다. WordPress가 보통 /wp-admin/으로 리디렉션하더라도 두 경로를 모두 명시하는 편이 안전하다. 쿼리 문자열은 Access path 판정 대상이 아니므로 /wp-login.php?action=logout도 같은 로그인 경로 규칙으로 보호된다.

admin-ajax.php 공개 예외는 필요할 때만 만들기

일부 테마와 플러그인은 로그인하지 않은 방문자의 요청도 /wp-admin/admin-ajax.php로 보낸다. 넓은 /wp-admin/* 보호만 두면 이런 기능이 Access 로그인 화면으로 리디렉션될 수 있다. 반대로 그런 기능이 없다면 예외를 미리 만들 이유도 없다.

[Cloudflare 화면에서 설정]

  1. 새 시크릿 창에서 댓글, 문의 폼, 검색 필터처럼 로그인하지 않은 방문자가 쓰는 기능을 실행한다.
  2. 브라우저 개발자 도구의 Network에서 실패한 요청이 정확히 /wp-admin/admin-ajax.php인지 확인한다.
  3. 그 요청이 Cloudflare Access 로그인으로 리디렉션돼 기능이 멈춘 경우에만 이 경로를 대상으로 별도의 Self-hosted Access 애플리케이션을 만든다.
  4. 정책 Action은 Bypass, Include는 Everyone으로 설정한다.
  5. 경로가 정확히 /wp-admin/admin-ajax.php인지 다시 확인하고 저장한다.
  6. 같은 공개 기능을 새 시크릿 창에서 다시 실행해 정상 동작하는지 확인한다.

더 구체적인 경로가 넓은 /wp-admin/* 규칙보다 우선한다. Bypass는 해당 요청에 Access 인증과 Access 로그를 적용하지 않으므로 /wp-admin/* 전체에 사용하면 안 된다.

/wp-admin/admin-post.php는 공개 폼이나 플러그인이 실제로 요구할 때만 같은 방식의 정확한 경로 예외를 추가한다. 필요 여부를 확인하지 않고 미리 넓히지 않는다.

새 규칙을 먼저 시험하기

아직 임시 전체 사이트 Access는 남아 있으므로 공개 페이지까지 인증을 요구하는 것이 정상이다. 먼저 관리자 경로 세 개가 저장됐는지 확인한다. admin-ajax.php 예외는 앞 단계에서 필요성이 확인된 경우에만 함께 확인한다.

그 다음 임시 전체 사이트 애플리케이션을 제거하거나 비활성화해 공개 전환한다. 전환 직후 새 시크릿 창에서 다시 확인한다.

주소기대 결과
/와 대표 글Access 로그인 없이 공개
이미지와 /wp-json/Access 로그인 없이 공개
/robots.txt, sitemapAccess 로그인 없이 공개
/wp-login.phpCloudflare Access 로그인으로 이동
/wp-admin, /wp-admin/Cloudflare Access 로그인으로 이동
/wp-admin/admin-ajax.php기본은 Access 로그인으로 이동. 필요한 경우에만 정확한 경로 Bypass

Admin AJAX 예외를 만든 경우, 이 주소를 아무 동작 없이 직접 열면 WordPress가 0이나 400 응답을 줄 수 있다. 완료 기준은 무조건 HTTP 200이 아니라 Access 로그인으로 이동하지 않고 실제 공개 기능이 정상 동작하는지다. 예외를 만들지 않았다면 Access 로그인으로 이동하는 것이 현재 보호 범위와 일치한다.

사이트 전체 Access를 계속 남겨 두면 일반 독자뿐 아니라 검색엔진과 광고 crawler도 로그인 화면을 만나게 된다. 공개 사이트에서는 홈페이지와 글을 인증 없이 열고, 관리자 입구만 보호한다.

11. www 주소와 HTTPS 마무리하기

루트 도메인만 운영 주소로 쓸 경우 www 요청도 같은 Tunnel을 통과할 수 있는 Proxied DNS 또는 Published application이 먼저 있어야 한다.

[Cloudflare 화면에서 설정]

  1. 같은 Tunnel에 www Published application을 추가한다.
  2. Service URL은 루트 도메인과 같은 http://wordpress:80으로 둔다.
  3. Rules → Redirect Rules → Single Redirects로 이동한다.
  4. Wildcard 패턴으로 다음 값을 입력한다.
Request URL: http*://www.example.com/*
Target URL:  https://example.com/${2}
Status code: 301
Preserve query string: On

첫 번째 *는 http*의 s 여부를, 두 번째 *는 경로를 잡기 때문에 대상 경로에 ${2}를 사용한다. 실제 설정에서는 example.com을 자신의 도메인으로 바꾼다.

이 규칙을 사용하면 다음처럼 경로와 쿼리를 보존할 수 있다.

https://www.example.com/post/?page=2
→ https://example.com/post/?page=2

HTTP 요청을 HTTPS로 보내는 설정은 원본 Apache가 아니라 Cloudflare에서 처리한다.

[Cloudflare 화면에서 설정]

  1. SSL/TLS → Edge Certificates에서 Universal SSL이 Active인지 확인한다.
  2. SSL/TLS 모드가 Off가 아닌지 확인한다.
  3. 루트 주소, www 리디렉션, 관리자 Access, 이미지가 모두 정상인지 시험한다.
  4. Always Use HTTPS를 켠다.
  5. HTTP 주소가 HTTPS로 한 번만 리디렉션되는지 다시 확인한다.

브라우저와 Cloudflare 사이는 HTTPS이고, Cloudflare와 cloudflared 사이는 Tunnel 연결로 보호된다. 같은 Docker edge 네트워크 안에서 cloudflared가 WordPress로 넘기는 주소는 http://wordpress:80이다. 이 내부 HTTP 주소를 보고 공유기 포트가 열린 것으로 오해하면 안 된다.

HSTS는 이번 단계에서 바로 켜지 않는다. HSTS를 활성화한 뒤 HTTPS나 Cloudflare 설정을 끄면 설정된 기간 동안 방문자가 사이트에 접속하지 못할 수 있다. 관리자 로그인, www 리디렉션, 공개 글, 이미지, 광고·분석 스크립트를 충분히 검증한 뒤 짧은 Max Age부터 별도로 적용한다.

12. Mac에서 공개·보호 경로를 한 번에 확인하기

로그인 쿠키의 영향을 받지 않는 명령행에서도 응답을 확인한다.

[Mac에서 실행]

read -rp "사이트 주소(예: https://example.com): " access_site_url
access_site_url="${access_site_url%/}"

access_check() {
  local request_path=$1
  local request_method=$2
  local response_headers
  local http_status
  local access_result

  if [[ "$request_method" == "POST" ]]; then
    response_headers="$(
      curl -sS \
        --request POST \
        --data '' \
        --dump-header - \
        --output /dev/null \
        "${access_site_url}${request_path}"
    )"
  else
    response_headers="$(
      curl -sS \
        --dump-header - \
        --output /dev/null \
        "${access_site_url}${request_path}"
    )"
  fi

  http_status="$(
    printf '%s\n' "$response_headers" |
      awk '/^HTTP/{status=$2} END{print status}'
  )"

  if printf '%s\n' "$response_headers" |
    grep -qi '^location: .*cloudflareaccess\.com'; then
    access_result=ACCESS_LOGIN
  else
    access_result=NO_ACCESS_REDIRECT
  fi

  printf '%-32s status=%s result=%s\n' \
    "$request_path" \
    "$http_status" \
    "$access_result"
}

access_check / GET
access_check /robots.txt GET
access_check /wp-login.php GET
access_check /wp-admin/ GET
access_check /wp-admin/admin-ajax.php POST

unset access_site_url
unset -f access_check

이 명령은 리디렉션 주소 전체를 출력하지 않고 ACCESS_LOGIN 또는 NO_ACCESS_REDIRECT로만 분류한다. 현재 확인에서는 공개 페이지가 NO_ACCESS_REDIRECT, /wp-login.php와 /wp-admin/, /wp-admin/admin-ajax.php가 ACCESS_LOGIN이었다. 공개 기능 때문에 정확한 Bypass를 만든 경우에만 마지막 결과가 NO_ACCESS_REDIRECT여야 하며, 실제 기능 동작도 함께 확인한다.

Ubuntu에서도 웹 포트가 계속 비공개인지 확인한다.

[SSH로 Ubuntu에서 실행]

PUBLISHED_PORTS="$(
  for CONTAINER_ID in $(
    sudo docker compose \
      --env-file /opt/ttakdap/.env \
      -f /opt/ttakdap/compose.yaml \
      --profile tunnel \
      ps -q db wordpress cloudflared
  ); do
    sudo docker port "$CONTAINER_ID"
  done
)"

if [[ -z "$PUBLISHED_PORTS" ]]; then
  echo "호스트 공개 포트 없음"
else
  printf '확인 필요한 포트 매핑:\n%s\n' "$PUBLISHED_PORTS"
fi

unset PUBLISHED_PORTS CONTAINER_ID

정상 결과는 여전히 호스트 공개 포트 없음이다. Tunnel은 Ubuntu에서 외부로 연결하므로 Docker ports:나 공유기 포트포워딩이 필요하지 않다.

관리자 표시 없이 캡처한 딱답 공개 홈페이지

그림 3. 2026년 9월 1일 관리자 표시를 제외하고 다시 캡처한 실제 공개 홈페이지. 일반 방문자는 별도의 Access 인증 없이 이 화면에 들어올 수 있다.

문제가 생겼을 때 확인할 순서

한 번에 DNS, Tunnel, Docker, Access를 모두 바꾸면 원인을 찾기 어려워진다. 앞 단계에서 확인한 사실은 유지하고 다음 연결만 본다.

Cloudflare Tunnel 문제를 세 구간으로 나누어 확인하는 순서

그림 4. Tunnel이 연결되지 않은 문제, WordPress까지 가지 못하는 문제, Access 정책 문제를 서로 다른 구간으로 나눠 확인한다.

Tunnel이 Inactive 또는 Down이다

Cloudflare와 connector 사이 문제다. 아직 WordPress 설정을 바꾸지 않는다.

  1. cloudflared 컨테이너가 반복 재시작하는지 확인한다.
  2. 토큰 파일 UID/GID가 65532:65532, mode가 400인지 확인한다.
  3. Cloudflare에서 복사한 토큰이 현재 Tunnel의 토큰인지 확인한다.
  4. 토큰을 회전했다면 파일도 새 토큰으로 바꾸고 컨테이너를 다시 만든다.
  5. Ubuntu에서 Cloudflare로 나가는 TCP·UDP 7844가 차단됐는지 확인한다.

[SSH로 Ubuntu에서 실행]

sudo stat \
  -c 'uid=%u gid=%g mode=%a bytes=%s %n' \
  /opt/ttakdap/secrets/cloudflare_tunnel_token.txt

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  --profile tunnel \
  logs --tail 100 cloudflared 2>&1 |
sed -E \
  's/eyJ[A-Za-z0-9._=-]+/[TUNNEL_TOKEN_REDACTED]/g'

Cloudflare 오류 1033은 Cloudflare가 정상 connector를 찾지 못하는 구간을 먼저 봐야 한다.

Tunnel은 Healthy인데 502가 나온다

Cloudflare와 connector 연결은 됐지만 connector가 WordPress에 도달하지 못한 상태다.

  1. Published application이 http://wordpress:80인지 확인한다.
  2. localhost, VM IP, https://wordpress로 적지 않았는지 확인한다.
  3. WordPress와 cloudflared가 모두 edge 네트워크에 연결됐는지 확인한다.
  4. WordPress가 healthy인지 확인한다.

[SSH로 Ubuntu에서 실행]

sudo docker inspect \
  --format '{{.Name}} → {{range $name, $_ := .NetworkSettings.Networks}}{{$name}} {{end}}' \
  "$(sudo docker compose \
    --env-file /opt/ttakdap/.env \
    -f /opt/ttakdap/compose.yaml \
    --profile tunnel ps -q wordpress)" \
  "$(sudo docker compose \
    --env-file /opt/ttakdap/.env \
    -f /opt/ttakdap/compose.yaml \
    --profile tunnel ps -q cloudflared)"

두 컨테이너 결과에 같은 ttakdap_edge 네트워크가 보여야 한다. MariaDB는 ttakdap_db_internal에 있고 cloudflared와 네트워크를 공유하지 않는 것이 정상이다.

OTP 메일이 오지 않는다

먼저 Access Allow 정책의 이메일과 로그인 화면에 입력한 주소가 정확히 같은지 확인한다. 허용되지 않은 주소여도 화면에는 코드가 발송됐다고 보일 수 있다.

그 다음 One-time PIN identity provider 선택, 스팸함, 메일 보안 필터를 확인한다. 새 코드를 요청하면 이전 코드는 무효가 되고, 코드는 10분 후 만료된다.

공개 글까지 Access 로그인이 나온다

초기 설치용 전체 사이트 Access가 아직 남아 있는지 확인한다. 관리자 경로 규칙만 만든 것으로는 더 넓은 전체 사이트 규칙이 자동으로 없어지지 않는다.

새 시크릿 창으로 확인해야 한다. 기존 창은 Access 로그인 쿠키 때문에 공개·보호 범위를 잘못 판단할 수 있다.

관리자 화면은 되는데 공개 기능이 멈춘다

브라우저 개발자 도구나 플러그인 문서에서 요청 주소가 실제로 /wp-admin/admin-ajax.php인지 먼저 확인한다. 현재처럼 이 경로가 Access로 이동하고 공개 기능에 필요함이 확인됐다면, 정확한 hostname과 path의 별도 애플리케이션 및 Bypass 정책이 활성 상태인지 본다. 필요성이 확인되기 전에는 예외를 만들지 않고, 해결을 위해 /wp-admin/* 전체를 공개하지도 않는다.

HTTPS 리디렉션이 반복된다

2편의 .env에 SITE_URL=https://내도메인이 들어 있는지 확인한다. Cloudflare Always Use HTTPS를 사용한다면 Apache에 별도의 무조건 HTTP→HTTPS 규칙을 겹쳐 넣지 않는다.

[SSH로 Ubuntu에서 실행]

sudo sed -n 's/^SITE_URL=/SITE_URL=/p' /opt/ttakdap/.env

사이트 주소는 공개값이지만 도메인 철자가 맞는지 확인한다. DB 비밀번호와 Tunnel 토큰 파일은 출력하지 않는다.

문제가 생겼을 때 Tunnel만 되돌리는 방법

Cloudflare Published application을 제거하거나 비활성화해 새 외부 요청을 먼저 멈춘 뒤 connector만 내린다. WordPress와 MariaDB 볼륨은 건드리지 않는다.

[SSH로 Ubuntu에서 실행]

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  --profile tunnel \
  stop cloudflared

sudo docker compose \
  --env-file /opt/ttakdap/.env \
  -f /opt/ttakdap/compose.yaml \
  --profile tunnel \
  rm -f cloudflared

2편 Compose 백업이 있을 때만 원본을 복원한다.

[SSH로 Ubuntu에서 실행]

if sudo test -s /opt/ttakdap/compose.before-cloudflared.yaml; then
  sudo cp -a \
    /opt/ttakdap/compose.before-cloudflared.yaml \
    /opt/ttakdap/compose.yaml

  sudo docker compose \
    --env-file /opt/ttakdap/.env \
    -f /opt/ttakdap/compose.yaml \
    config --quiet &&
  echo "2편 Compose 복원 확인"

  sudo docker compose \
    --env-file /opt/ttakdap/.env \
    -f /opt/ttakdap/compose.yaml \
    up -d --wait db wordpress
else
  echo "Compose 백업을 찾지 못해 자동 복원하지 않았습니다."
fi

docker compose down -v나 docker system prune --volumes는 롤백 명령이 아니다. 실행하면 WordPress와 MariaDB 데이터 볼륨을 잃을 수 있으므로 사용하지 않는다.

Tunnel을 완전히 폐기한다면 Cloudflare에서 먼저 토큰을 회전하거나 Tunnel을 삭제한 뒤 로컬 토큰 파일을 별도로 정리한다. 단순 진단 중에는 토큰 파일부터 지우지 않는다.

3편 완료 점검표

  • ☐ 2편의 db와 wordpress가 먼저 healthy였다.
  • ☐ Tunnel 추가 전 Compose 원본과 VM 복구 지점을 만들었다.
  • ☐ 원격 관리 Named Tunnel을 만들었다.
  • ☐ Published application보다 전체 사이트 임시 Access를 먼저 만들었다.
  • ☐ Allow 정책에는 소유자의 정확한 이메일만 넣었다.
  • ☐ Tunnel 토큰을 명령줄, .env, Compose에 직접 적지 않았다.
  • ☐ 토큰 파일은 UID/GID 65532:65532, mode 0400이다.
  • ☐ Cloudflared는 edge에만 연결되고 MariaDB 내부 네트워크에는 연결되지 않았다.
  • ☐ Published application의 원본은 http://wordpress:80이다.
  • ☐ Tunnel 대시보드에서 Healthy를 확인했다.
  • ☐ Access를 통과한 뒤 WordPress 초기 설치와 관리자 로그인을 마쳤다.
  • ☐ /wp-login.php, /wp-admin, /wp-admin/*를 보호했다.
  • ☐ /wp-admin/admin-ajax.php는 보호 상태로 두었거나, 공개 기능에 필요함을 확인한 뒤 정확한 경로만 Bypass했다.
  • ☐ 전체 사이트 임시 Access를 제거한 뒤 공개 글이 인증 없이 열린다.
  • ☐ 새 시크릿 창에서 공개 페이지와 관리자 경로를 각각 확인했다.
  • ☐ www 리디렉션은 path와 query를 보존한다.
  • ☐ Always Use HTTPS를 켠 뒤 중복 리디렉션이 없는지 확인했다.
  • ☐ HSTS는 충분한 검증 뒤로 미뤘다.
  • ☐ 공유기·Mac·Ubuntu·Compose에 80·443 포트포워딩을 만들지 않았다.

다음 글에서 할 일

이제 WordPress는 외부에서 열리지만 운영 준비가 끝난 것은 아니다. 4편에서는 방문 요청에 의존하는 기본 WP-Cron을 systemd 타이머로 바꾸고, MariaDB 논리 덤프와 WordPress 파일을 함께 백업한다.

다음 글의 목표는 다음과 같다.

  1. 서버 시간과 systemd 타이머 상태 확인
  2. WP-Cron을 외부 요청과 분리
  3. MariaDB 일관성 덤프 만들기
  4. WordPress 파일과 설정 함께 보관하기
  5. 백업 체크섬 검증하기
  6. 로컬 보존 기간 적용하기
  7. rclone crypt로 암호화해 Cloudflare R2에 하루 최대 한 세트 전송하기
  8. 실제 파일을 다시 내려받아 복호화와 복원 재료 확인하기

다음 글: WP-Cron을 systemd로 바꾸고 Cloudflare R2에 백업하기

공식 참고 자료