[카테고리:] 셀프호스팅 구축기

  • 포트를 하나도 안 열고 — Tailscale로 내 서버에 어디서나 안전 접속

    포트를 하나도 안 열고 — Tailscale로 내 서버에 어디서나 안전 접속

    포트를 하나도 안 열고 — Tailscale로 내 서버에 어디서나 안전 접속

    셀프호스팅 구축기 16편. 9편이 방문자를 블로그로 들이는 길(Cloudflare Tunnel)이었다면, 이번엔 나만 내 서버 안으로 들어가는 길이다.

    짝이 되는 편: 9편 — 보이지 않는 다리 (Cloudflare Tunnel)


    TL;DR (한 화면 요약)

    • 문제: 집 서버에 밖에서, 혹은 다른 방 노트북에서 원격 데스크톱·SSH로 들어가고 싶다. 그런데 포트를 열면 위험하다.
    • 옛 방식: 공유기에 원격 데스크톱 포트를 개방 → 전 세계 봇이 24시간 무차별 로그인 공격.
    • 새 방식: Tailscale — 내 기기들(서버·노트북·폰)을 암호화된 사설망 하나로 묶는다. 공유기 구멍 0개.
    • 안전한 이유: 인터넷에 포트를 하나도 노출 안 함 + 기기 간 종단간 암호화 + 로그인된 내 기기만 입장.
    • 도구: 각 기기에 Tailscale 앱 설치 + 같은 계정으로 로그인. 5분.
    • 비용: 0원 (개인 사용은 기기 100대까지 무료).

    1. 풀고 싶은 문제 — 내 서버에 어떻게 들어가나

    집에 작은 서버를 한 대 띄워두면 곧 이런 게 필요해진다. 밖에서, 혹은 같은 집 다른 방에서 그 서버에 원격 데스크톱(화면 그대로 보기) 이나 SSH(터미널) 로 들어가고 싶다.

    가장 흔한 옛날 방법은 공유기에 포트를 여는 것(포트포워딩)이다. 그런데 이게 골치다.

    골치
    무차별 공격 원격 데스크톱 포트를 인터넷에 열면, 켜자마자 전 세계 봇이 비밀번호를 찍어본다
    비밀번호 한 겹 봇이 뚫으면 서버 전체가 넘어간다. 로그인 하나가 유일한 방어선
    위치 노출 공인 IP만 알면 대략적인 동네가 추정된다
    갱신 골치 통신사가 공인 IP를 바꾸면 접속 주소도 바뀐다

    원격 데스크톱을 인터넷에 직접 여는 건 보안적으로 거의 자살에 가깝다. (앞서 9편에서 방문자용 트래픽을 다뤘다면, 이건 관리자인 나만 쓰는 통로라 더 민감하다.)


    2. 다른 발상 — 포트를 열지 말고, 기기끼리 사설망

    발상의 전환은 이렇다. 외부에서 서버로 들어오는 문을 열지 말자. 대신 내 기기들끼리만 통하는 암호화된 사설망을 따로 만들자.

    이걸 해주는 게 Tailscale이다. 핵심 엔진은 WireGuard(현대적이고 빠른 암호화 터널 기술) 이고, Tailscale은 그 WireGuard 터널들을 설정 없이 자동으로 깔아주는 앱이다.

    설치하면 내 기기들이 한 사설망(테일넷)에 묶이고, 각 기기는 100.x.x.x 형태의 사설 주소를 받는다. 이 주소는 인터넷엔 안 보이고, 내 기기끼리만 통한다. 멀리 떨어져 있어도 같은 방 랜선처럼 연결된다.


    3. 어떻게 안전한가 — 소개팅과 통화

    Tailscale의 가장 영리한 설계는 조정(control)과 데이터(data)를 분리한 것이다. 소개팅 주선자에 빗대면 쉽다.

    • 조정 서버 = 소개팅 주선자: Tailscale 본사 서버가 기기들을 인증하고 서로를 소개시킨다. “이 둘은 같은 주인 거니까 연결해도 돼” 하고 공개키를 나눠준다. 하지만 실제 대화 내용은 절대 듣지 않는다.
    • 데이터 = 실제 통화: 원격 데스크톱 화면 같은 진짜 트래픽은 기기끼리 직접(P2P), WireGuard로 종단간 암호화되어 흐른다. 본사 서버를 거치지 않는다.

    여기서 나오는 세 겹의 보안:

    보안 장치 내용
    포트 0개 노출 서버의 원격 데스크톱 포트가 인터넷엔 존재조차 안 보인다. 스캐너가 찾을 표적이 없다
    종단간 암호화 기기 간 모든 통신을 WireGuard가 암호화. 중간에서 가로채도 못 읽는다
    신분 기반 입장 공유 비밀번호가 아니라, 내 계정으로 로그인한 기기만 사설망에 들어온다. 잃어버린 기기는 콘솔에서 즉시 차단

    개인키(각 기기의 비밀 열쇠)는 그 기기를 절대 떠나지 않는다. 본사 서버엔 공개키만 올라가므로, 설령 본사가 털려도 내 트래픽은 못 푼다.


    4. 트래픽 흐름 — 옛 방식과 비교

    

    옛 방식은 공유기에 구멍을 뚫어 누구나 두드릴 수 있게 한다. 새 방식은 내 기기끼리 암호화 터널을 직접 잇고, 외부엔 아무 문도 열지 않는다.


    5. Cloudflare Tunnel과 뭐가 다른가 — 공개용 vs 나만의

    9편의 Cloudflare Tunnel과 헷갈리기 쉽다. 둘은 경쟁이 아니라 역할이 다른 보완재다.

    Cloudflare Tunnel (9편) Tailscale (이번 편)
    목적 방문자에게 블로그를 공개 만 서버에 비공개 접속
    대상 전 세계 누구나 (https) 내 로그인된 기기만
    쓰는 곳 블로그·웹사이트 공개 원격 데스크톱·SSH·관리
    비유 호텔 프론트데스크(손님 안내) 직원만 아는 뒷통로

    블로그는 Cloudflare Tunnel로 세상에 열고, 그 서버를 관리할 땐 Tailscale로 나만 들어간다. 한 서버에 둘을 같이 써도 전혀 충돌하지 않는다.


    6. 쓰는 법 — 5분

    개념은 단순하다. 접속하려는 모든 기기에 앱을 깔고, 같은 계정으로 로그인하면 끝.

    1. 서버(예: 리눅스 미니PC)에 Tailscale 설치 → 로그인
    2. 내 노트북·폰에도 Tailscale 앱 설치 → 같은 계정 로그인
    3. 이제 모든 기기가 한 사설망에 묶이고, 각자 100.x.x.x 주소를 받는다
    4. 원격 데스크톱·SSH는 그 서버의 Tailscale 주소로 접속한다

    MagicDNS를 켜면 주소 대신 기기 이름으로도 접속된다 (예: myserver → 그 기기). 이 사설 주소는 한 번 정해지면 안 바뀌어서, 즐겨찾기처럼 계속 쓸 수 있다.


    7. 함정 — 우리가 실제로 만난 것

    함정 1. 내부망 IP로 접속하면 막힌다

    같은 집 안 기기는 두 개의 주소를 가진다 — 공유기가 준 내부망 IP(192.168.x.x)와 Tailscale이 준 사설 주소(100.x.x.x). 서버 방화벽을 ‘안전하게’ 잠가서 Tailscale 통로만 허용해두면, 내부망 IP로 접속할 땐 방화벽이 막아버린다.

    증상이 고약하다. ping도 되고 SSH(22)도 되는데 원격 데스크톱만 안 된다 — 방화벽이 그 포트만 막고 있어서다. 같은 집이라도 Tailscale 주소로 접속해야 한다. 이걸 모르고 “집 안에서도 안 되네?” 하며 한참 헤맸다. (저장된 접속 주소가 내부망 IP로 되어 있던 게 범인이었다.)

    함정 2. 양쪽 다 켜져 있어야 한다

    Tailscale은 양쪽 기기에 다 떠 있어야 연결된다. 노트북에서 Tailscale이 로그아웃돼 있으면 당연히 사설망에 없어서 접속이 안 된다. 보통 부팅 시 자동 실행되지만, 안 될 땐 이걸 먼저 의심한다.

    함정 3. 오래 안 켠 기기는 오프라인으로 빠진다

    한동안 안 켠 폰 같은 기기는 콘솔에서 “오프라인”으로 표시된다. 다시 켜고 로그인하면 복귀한다. 기기 목록을 가끔 정리해주면 좋다.


    8. 검증

    연결됐는지 확인하는 가장 빠른 방법은 두 가지다 (양쪽 기기에서 Tailscale을 켠 상태에서).

    ① 서버의 사설 주소가 살아있나ping <서버의 Tailscale 주소> 로 응답을 본다.

    ② 목표 포트가 실제로 도달되나 — 윈도우라면 PowerShell에서:

    Test-NetConnection -ComputerName <서버 Tailscale 주소> -Port <포트>
    

    TcpTestSucceeded : True가 뜨면 사설망 터널이 정상이다. 이제 그 주소로 원격 데스크톱·SSH를 붙이면 된다.


    FAQ

    Q. Cloudflare Tunnel이랑 같이 써도 되나?
    된다. 역할이 다르다. 블로그는 Cloudflare Tunnel로 공개하고, 서버 관리(원격 데스크톱·SSH)는 Tailscale로 한다. 충돌 없다.

    Q. 무료 한도는?
    개인 플랜으로 기기 100대·사용자 3명까지 무료다. 집 서버 몇 대 묶는 용도엔 평생 무료로 충분하다.

    Q. Tailscale 회사가 내 트래픽을 보나?
    못 본다. 본사 서버는 소개만 하고, 실제 데이터는 기기끼리 직접 암호화로 흐른다. (직접 연결이 막힌 까다로운 망에서는 암호화된 중계 서버를 거치는데, 그때도 양 끝에서만 복호화되어 중계 서버는 내용을 못 본다.)

    Q. 전기·인터넷이 끊기면?
    서버가 꺼지면 사설망에서 빠진다. 다시 켜지면 자동 복귀한다. 사설망 자체는 내 계정에 묶여 있어 그대로 유지된다.

    Q. 회사에서도 쓰나?
    쓴다. “제로 트러스트(아무것도 기본 신뢰하지 않음)” 사내망 접속 방식으로 기업에서도 널리 쓰인다. 개인 홈랩에 같은 원리를 공짜로 적용하는 셈이다.


    한 줄 정리

    집 서버에 어디서나 들어가려면, 공유기에 포트를 열어 봇 공격을 자초하는 옛 방식 대신, 내 기기들만 묶는 암호화 사설망을 만드는 Tailscale이 깔끔하다. WireGuard로 종단간 암호화하고, 인터넷엔 포트를 하나도 안 연다. 설치는 각 기기에 앱 깔고 같은 계정 로그인, 5분.


    참고

  • 탭이 안 되는 링크 — 텔레그램에서 옵시디언 노트 열기

    탭이 안 되는 링크 — 텔레그램에서 옵시디언 노트 열기

    셀프호스팅 구축기 15편. 구축기를 마쳤어도 막상 쓰다 보면 후속 문제가 나온다 — 13편에서 동기화한 노트, 그 링크가 폰에서 탭이 안 됐다.

    ← 이전 편: 13편 — 내 노트는 내 서버에 (Obsidian LiveSync)

    요약

    • 옵시디언 노트는 obsidian://open?vault=...&file=... 형태의 고유 링크를 가진다
    • 이 링크를 텔레그램으로 보내면 파란 링크가 아니라 회색 죽은 글씨 — 탭이 안 된다
    • 원인: 채팅앱은 http/https만 자동으로 링크화한다. obsidian:// 같은 커스텀 스킴은 무시
    • 해결: https 주소 한 단을 끼운다 — https로 받아서 브라우저가 obsidian://로 튕기는 “셔틀” 페이지
    • 그 셔틀을 집 서버 대신 Cloudflare Worker(서버리스)에 올렸다 — 집 컴퓨터가 꺼져도 동작, 비용 0원

    1. 증상 — 회색으로 죽은 링크

    옵시디언은 노트마다 고유 링크를 준다. 노트를 우클릭해 “obsidian URL 복사”를 누르면 obsidian://open?vault=내볼트&file=폴더/노트 같은 주소가 잡힌다. 이걸 텔레그램으로 보내면 파란 글씨가 될 줄 알았는데, 회색 평문으로 떴다. 눌러도 아무 일도 안 일어난다. 링크가 아니라 그냥 글자였던 것이다.

    2. 원인 — 채팅앱은 http/https만 링크로 만든다

    채팅앱은 메시지 글자를 훑어 URL을 찾으면 자동으로 탭 가능한 링크로 바꾼다. 단 이 자동 변환은 http://·https://(그리고 텔레그램 자체의 tg://)에만 적용된다. obsidian://·notion://·slack:// 같은 앱 전용 커스텀 스킴은 변환 대상이 아니라 평문으로 남는다. 텔레그램 iOS 저장소에도 “커스텀 URI 스킴 링크는 클릭이 안 된다”는 같은 이슈가 올라와 있다.

    버튼이나 마크다운 링크로 우회하면 되지 않냐 — 그것도 안 된다. 봇이 메시지에 붙이는 인라인 버튼·링크 역시 http/https/tg만 허용한다. 커스텀 스킴은 거부된다.

    3. 정석 패턴 — https “셔틀” 한 단 끼우기

    커스텀 스킴이 직접 못 가니, 중간에 https를 한 번 거치게 한다.

    1. 텔레그램에는 https 링크를 보낸다 → 파란 링크가 된다
    2. 탭하면 브라우저가 그 https 페이지를 연다
    3. 그 페이지의 자바스크립트가 obsidian://로 튕긴다 → 옵시디언이 열린다

    이 패턴은 새로운 게 아니다. obsid.net이라는 공개 무료 서비스가 정확히 이 일만 한다 — vaultfile을 받아 브라우저에서 obsidian://로 넘겨주는 정적 페이지다. 그대로 써도 된다.

    4. 그런데 왜 직접 만들었나

    obsid.net이 있는데 굳이 직접 만든 이유는 이 시리즈의 일관된 이유와 같다.

    • 제3자를 안 거친다 — 내 볼트 이름과 노트 경로가 남의 서버(로깅을 안 한다 해도)를 지나가지 않는다
    • 내 도메인o.내도메인 한 줄, 링크가 내 것
    • 내가 페이지를 통제 — 자동 이동이 막힐 때 누를 버튼, 어떤 노트인지 보여주는 파일명을 직접 넣을 수 있다 (7절 함정 때문에 중요하다)

    처음엔 이미 띄워 둔 다른 앱의 정적 폴더에 페이지를 하나 얹었다. 작동은 했지만, 알림용 리다이렉트를 상관없는 앱 안에 끼워 넣은 꼴이라 경계가 더러워졌다. 리다이렉트는 어느 앱에도 속하지 않는 범용 유틸이다. 그래서 떼어내 독립시켰다.

    5. 어디에 둘까 — 집 서버 vs 엣지

    독립시킨다면 둘 중 하나다.

    집 서버에 작은 웹서버 Cloudflare Worker(엣지)
    동작 조건 집 컴퓨터가 켜져 있어야 집 컴퓨터 꺼져도 동작
    인증서 직접 발급·갱신 자동
    비용 전기 무료(하루 10만 요청)
    종속 없음 Cloudflare

    리다이렉트는 1초짜리 정적 응답이라 집 서버를 굳이 깨워 둘 이유가 없다. Cloudflare Worker를 골랐다 — 코드 한 조각을 클라우드 엣지에 올리면 서버를 직접 굴리지 않고도 응답한다(이걸 서버리스라 한다). 6편에서 산 도메인이 이미 Cloudflare에 있으니 서브도메인 하나 붙이면 끝이다.

    6. 작동 — https가 받아서 obsidian://로 튕긴다

    Worker가 하는 일은 짧다. ?f=노트경로를 받아, 그걸 obsidian:// 링크로 바꾼 HTML 한 장을 돌려준다.

    export default {
      async fetch(request) {
        const url = new URL(request.url);
        const file = url.searchParams.get('f') || '';
        const vault = url.searchParams.get('v') || 'MyVault';
        const uri = 'obsidian://open?vault=' +
          encodeURIComponent(vault) + '&file=' + encodeURIComponent(file);
        const html = '<!doctype html><meta charset=utf-8>'
          + '<p>' + file + '</p>'
          + '<a href="' + uri + '">노트 열기</a>'
          + '<script>setTimeout(function(){location.href=' + JSON.stringify(uri) + '},350)</' + 'script>';
        return new Response(html, {
          headers: { 'content-type': 'text/html; charset=utf-8' }
        });
      }
    }
    

    텔레그램에는 https://o.내도메인/?f=노트경로만 보낸다. 이건 https라 파란 링크가 되고, 탭하면 위 페이지가 떠서 옵시디언으로 넘긴다.

    7. 함정 셋

    ① 자동 이동이 막힌다. location.href = obsidian://...로 자동 전환을 걸어도, 모바일 브라우저(특히 안드로이드 크롬·채팅앱 내장 브라우저)는 사용자가 직접 누르지 않은 커스텀 스킴 이동을 보안상 막는 경우가 있다(“Navigation is blocked”). 그래서 자동 이동에만 기대면 안 되고, 사용자가 탭할 “노트 열기” 버튼을 반드시 같이 둔다. 자동 전용 페이지가 가끔 안 먹는 이유가 이거다.

    ② 볼트 이름이 기기마다 다르다. obsidian://open?vault=X의 X는 그 기기에 등록된 볼트 표시 이름과 정확히 같아야 한다. PC에선 열리는데 폰에선 “노트 없음”이 뜨면 십중팔구 볼트 이름이 다른 것이다. 옵시디언 포럼에도 “안드로이드에서 앱은 열리는데 노트는 안 열린다”는 같은 사례가 있다. 링크에 &v=폰볼트이름을 맞춰 주면 된다.

    ③ 동기화가 먼저다. 링크는 노트를 여는 거지 만들어 주는 게 아니다. 그 노트가 폰에 동기화돼 있어야(13편 LiveSync) 열린다.

    8. 비용

    • Cloudflare Worker: 0원 (무료 플랜 하루 10만 요청)
    • 도메인: 이미 보유(6편), 서브도메인 추가비 없음
    • 집 서버 부담: 없음(엣지가 응답)

    한 줄 정리

    채팅앱은 http/https만 링크로 만든다. obsidian:// 같은 앱 링크는 https 셔틀 한 단을 끼워야 탭이 된다. 그 셔틀을 Cloudflare Worker에 올리면 집 서버와 무관하게 공짜로 돈다. 단 자동 이동은 막힐 수 있으니 버튼을 같이 두고, 볼트 이름을 기기에 맞춰라.

  • 손 안 가는 서버 — cron으로 굴러가는 작은 자동화들

    손 안 가는 서버 — cron으로 굴러가는 작은 자동화들

    손 안 가는 서버 — cron으로 굴러가는 작은 자동화들

    셀프호스팅 구축기 14편(마지막). 백업도(12편) 동기화도(13편) 결국 “정해진 시각에 알아서” 돌아야 한다. 그 엔진이 cron이다.

    ← 이전 편: 13편 — 내 노트는 내 서버에 (Obsidian LiveSync)

    요약

    • cron(크론)은 정해진 시각에 명령을 돌리는 리눅스 기본 스케줄러 — 따로 설치할 것 없다
    • 우리 서버가 매일 알아서 하는 일: 백업 · DB 덤프 · 캐시 정리 · 헬스체크 · 요약 생성
    • crontab 한 줄 = 분·시·일·월·요일 + 실행할 명령
    • 함정 1: cron엔 PATH가 거의 없다 → 절대경로 안 쓰면 “command not found”
    • 함정 2: 조용한 실패 — 로그로 안 남기면, 안 돈 걸 며칠 뒤에야 안다

    1. cron이 뭔가 — 설치 없는 알람시계

    cron은 리눅스에 기본으로 들어 있는 알람시계다. “매일 새벽 4시 반에 이 스크립트 실행”처럼 시각과 명령을 등록해 두면, 컴퓨터가 켜져 있는 한 알아서 돌린다. 셀프호스팅에서 반복되는 잡일은 거의 다 cron에 맡길 수 있다.

    2. crontab 한 줄 읽는 법

    등록 목록을 crontab이라 부른다. 한 줄은 다섯 칸의 시각 + 명령으로 이루어진다.

    의미 범위
    1 0–59
    2 0–23
    3 1–31
    4 1–12
    5 요일 0–6 (0=일)

    *는 “매번”이라는 뜻이다. 그래서 30 4 * * *매일 4시 30분이 된다.

    30 4 * * *   /home/user/bin/db-dump.sh    >> /var/log/db-dump.log 2>&1
    0  5 * * *   /home/user/bin/backup.sh      >> /var/log/backup.log 2>&1
    

    3. 우리 서버가 매일 하는 일

    작은 cron들이 쌓여 “손 안 가는 서버”를 만든다.

    • DB 덤프 — 데이터베이스를 안전한 파일로 (백업보다 먼저)
    • 파일 백업 — NAS로 전송 (12편)
    • 캐시 정리 — 쌓인 임시 파일·만료 캐시 비우기
    • 헬스체크 — 서비스가 살아 있는지 확인, 죽었으면 알림
    • 요약 생성 — 어제 방문자·에러 한 장으로 정리

    4. 초보가 꼭 밟는 함정 3개

    ① PATH가 없다. 터미널에서 잘 되던 명령이 cron에선 “command not found”로 죽는다. cron은 최소한의 환경에서 돌아 PATH가 거의 비어 있기 때문이다. 해결: 명령을 절대경로로 쓰거나(/usr/bin/python3), 스크립트 맨 위에 PATH를 직접 지정한다.

    ② 조용한 실패. cron은 에러가 나도 화면에 안 띄운다. 기본적으로 메일로만 보내는데 아무도 안 본다. 그래서 위 예시처럼 >> 로그파일 2>&1로 출력과 에러를 파일에 남겨야, 안 돌았을 때 추적이 된다.

    ③ 시간대. 서버 시계가 UTC인지 한국 시간(KST)인지 확인하자. 30 4 * * *로 “새벽 4시 반”을 노렸는데 서버가 UTC면 한국 시각으로는 오후 1시 반에 돈다.

    5. 작은 자동화의 복리

    cron 하나하나는 사소하다. 백업 한 줄, 캐시 정리 한 줄. 그런데 이게 쌓이면 내가 매일 손대지 않아도 서버가 스스로를 돌보는 상태가 된다. 셀프호스팅의 목표는 결국 이거다 — 만들어 놓고, 잊어버려도 굴러가는 것.

    이 시리즈도 여기서 한 바퀴를 돈다. 빈 컴퓨터 한 대에서 시작해(1편), 블로그를 올리고, 도메인을 붙이고, 막고, 백업하고, 이제 자동으로 굴러가게 했다. 다음은 직접 한 대 세워 보는 일만 남았다.


    한 줄 정리

    cron은 “정해진 시각에 알아서”의 엔진이다. 한 줄에 분·시·일·월·요일 + 명령. 단 세 가지만 조심하자 — 절대경로로 쓰고, 로그를 남기고, 시간대를 확인할 것. 작은 자동화가 쌓이면 서버는 손이 안 가게 된다.

  • 내 노트는 내 서버에 — Obsidian LiveSync + CouchDB 셀프호스팅

    내 노트는 내 서버에 — Obsidian LiveSync + CouchDB 셀프호스팅

    내 노트는 내 서버에 — Obsidian LiveSync + CouchDB 셀프호스팅

    셀프호스팅 구축기 13편. 블로그를 내 서버에 올렸으면, 메모도 내 서버에 둘 수 있다.

    ← 이전 편: 12편 — 새벽마다 NAS로 (tar+ssh 백업)
    → 다음 편: 14편 — 손 안 가는 서버 (cron 자동화)

    요약

    • Obsidian 공식 싱크는 월정액 — 노트를 내 서버 데이터베이스에 직접 동기화하면 비용 0원 + 데이터 주권
    • 방법: Self-hosted LiveSync 플러그인 ↔ CouchDB(도커 컨테이너) 실시간 양방향
    • CouchDB는 반드시 127.0.0.1에만 묶는다 — 도커가 자동으로 0.0.0.0에 여는 함정을 조심
    • 내 기기들만 닿게: 사설 네트워크(VPN 메시) 안에서만 접근, 인터넷에는 비공개
    • 삽질 일지: ERR_SSL_PROTOCOL_ERROR의 범인은 인증서가 아니라 “뒤에 서버가 안 떠 있던 것”

    1. 왜 공식 싱크 대신 셀프호스트인가

    Obsidian(옵시디언)은 노트 앱이고, 여러 기기에서 같은 노트를 보려면 동기화가 필요하다. 공식 Sync는 잘 되지만 월정액이다. 그리고 내 메모가 남의 서버를 거친다.

    이미 블로그용 서버가 굴러가고 있으니, 노트 동기화도 거기 얹으면 된다.

    항목 공식 Sync Self-hosted LiveSync
    비용 월정액 0원 (내 서버)
    데이터 위치 회사 서버 내 서버
    설정 난이도 켜면 끝 직접 구성 필요
    동기화 빠름 실시간 양방향

    대가는 “직접 구성”이다. 한 번 세우면 그 뒤로는 공식만큼 매끄럽다.

    2. 구조 — 플러그인 ↔ CouchDB

    CouchDB(카우치DB)는 변경분을 실시간으로 주고받는 데 강한 데이터베이스다. LiveSync 플러그인이 노트가 바뀔 때마다 CouchDB로 보내고, 다른 기기는 그걸 당겨 온다.

    flowchart LR
        PC[노트북 Obsidian] -->|변경분| DB[(CouchDB / 내 서버 도커)]
        Phone[폰 Obsidian] -->|변경분| DB
        DB -->|동기화| PC
        DB -->|동기화| Phone
    

    3. 절대 0.0.0.0에 열지 마라

    여기가 가장 중요하다. CouchDB를 도커로 그냥 띄우면 포트가 0.0.0.0(모든 네트워크 인터페이스)에 열린다. 이러면 같은 와이파이의 다른 기기, 심지어 잘못 설정된 공유기 너머까지 내 데이터베이스가 노출된다.

    ports:
      - "5984:5984"             # 위험: 0.0.0.0, 모든 인터페이스에 노출
      - "127.0.0.1:5984:5984"   # 안전: 내 컴퓨터 자신만 접근
    

    127.0.0.1에만 묶으면 서버 바깥에서는 보이지 않는다. 그럼 폰은 어떻게 접속하냐고? 사설 네트워크(VPN 메시) 안에서만 닿게 한다. 내 기기들끼리만 연결된 가상 LAN을 만들고, CouchDB는 그 안에서만 https로 노출한다. 인터넷 전체에는 끝까지 비공개다.

    4. 삽질 일지 — ERR_SSL_PROTOCOL_ERROR

    설정을 마치고 https 주소로 접속했더니 브라우저가 ERR_SSL_PROTOCOL_ERROR를 뱉었다. 당연히 인증서 문제인 줄 알고 인증서만 한참 들여다봤다. 아니었다.

    진짜 원인은 단순했다. CouchDB 컨테이너 자체가 안 떠 있었다. https 주소(앞단)는 만들어 놨는데 그 뒤에서 받아 줄 서버가 없으니, TLS 연결이 끝까지 맺어지지 못하고 브라우저가 “SSL 오류”로 보고한 것이다.

    교훈: “SSL 에러”가 항상 인증서 문제는 아니다. 앞단(주소·인증서)과 뒷단(실제 서버)을 나눠서, 뒷단이 살아 있는지부터 확인하자. curl로 백엔드 포트를 직접 찔러 보면 5초 만에 갈린다.


    한 줄 정리

    노트 동기화도 내 서버로 가져올 수 있다 — LiveSync 플러그인 + CouchDB면 구독료 0원. 단, CouchDB는 127.0.0.1에만 묶고 사설 네트워크로만 접근. 그리고 ERR_SSL_PROTOCOL_ERROR를 만나면 인증서보다 뒷단 서버가 살아 있는지를 먼저 의심하자.

  • 새벽마다 NAS로 — tar+ssh 자동 백업 (왜 rsync를 안 썼나)

    새벽마다 NAS로 — tar+ssh 자동 백업 (왜 rsync를 안 썼나)

    새벽마다 NAS로 — tar+ssh 자동 백업 (왜 rsync를 안 썼나)

    셀프호스팅 구축기 12편. 서버는 지켰다(11편). 이제 서버가 죽어도 데이터는 살리는 법.

    ← 이전 편: 11편 — 공짜로 워드프레스 지키기 (Wordfence)
    → 다음 편: 13편 — 내 노트는 내 서버에 (Obsidian LiveSync)

    요약

    • 백업 없는 셀프호스팅은 시한폭탄 — 디스크 하나 죽으면 사진·글·DB가 한 번에 사라진다
    • 매일 새벽 cron이 중요 폴더를 tar로 묶어 ssh로 NAS에 던진다
    • rsync(전용 데몬·포트 873)를 일부러 안 썼다 — SSH는 이미 열려 있으니 추가로 열 포트가 0
    • DB 덤프가 끝난 뒤 파일 백업이 돌아야 그 덤프까지 한 묶음에 들어간다 — 순서가 정합성을 만든다
    • “백업이 돌았다”와 “백업이 성공했다”는 다르다 — 0바이트 파일은 실패로 처리

    1. 백업 없는 셀프호스팅 = 시한폭탄

    내 컴퓨터에 블로그를 올린다는 건, 클라우드가 대신 해주던 백업도 내 책임이 된다는 뜻이다. SSD 하나가 수명을 다하거나, 명령 하나 잘못 치면, 몇 달치 글과 사진과 데이터베이스가 한순간에 날아간다.

    규칙은 단순하다. 백업은 다른 물리적 장치에, 자동으로, 매일. 손으로 하는 백업은 반드시 잊어버린다.

    2. 왜 rsync가 아니라 tar + ssh인가

    백업이라면 보통 rsync를 떠올린다. 빠르고 증분 동기화도 된다. 그런데 NAS(여기선 시놀로지)에서 rsync를 제대로 쓰려면 rsync 전용 서비스(데몬, 포트 873)를 켜야 한다. 포트를 하나 더 여는 것이다.

    항목 rsync 데몬 tar + ssh
    추가로 열 포트 873 필요 0 (이미 쓰는 SSH 재사용)
    증분 전송 가능 매번 전체
    설정 복잡도 서비스·권한 설정 명령 한 줄
    노출 면적 늘어남 그대로

    우리 데이터는 작아서 매일 통째로 묶어도 부담이 없다. 그렇다면 포트를 더 여는 대가를 치를 이유가 없다. 노출 면적을 줄이는 쪽이 셀프호스팅에선 거의 항상 옳다.

    3. 한 줄짜리 백업

    tar czf - /중요/데이터/폴더 | ssh -p <포트> [email protected] "cat > /backup/site-$(date +%F).tgz"
    

    tar czf -는 폴더를 묶어 화면(stdout)으로 흘려보내고, 그걸 ssh가 받아 NAS에서 파일로 떨군다. 중간에 임시 파일을 만들지 않으니 디스크도 덜 쓴다. 파일명에 날짜($(date +%F))가 들어가 매일 다른 이름으로 쌓인다.

    4. 순서가 정합성을 만든다 — DB 먼저

    데이터베이스는 그냥 폴더를 복사하면 안 된다. 쓰는 도중에 복사하면 반쯤 쓰다 만 상태가 백업될 수 있다. 그래서 먼저 DB를 안전하게 덤프(dump) 파일로 뽑고, 그 덤프가 끝난 뒤에 파일 백업이 돌아야 한다.

    flowchart LR
        A[04:30 DB 덤프] --> B[덤프 파일 생성]
        B --> C[05:00 파일 백업
    덤프 포함해서 묶음] C --> D[NAS 전송]

    시간차를 두는 이유가 이것이다. 두 cron의 순서가 곧 백업의 정합성이다.

    5. “성공”을 검증하라

    가장 흔한 착각: cron이 시간 맞춰 실행됐으면 백업이 된 줄 안다. 하지만 실행 ≠ 성공이다. 디스크가 꽉 찼거나 NAS가 잠깐 죽었으면, 0바이트짜리 빈 파일이 남는다.

    • 백업이 끝나면 만들어진 파일 크기를 확인한다
    • 일정 크기 미만이면 실패로 간주하고 알림을 보낸다
    • 오래된 백업은 자동 삭제(rotate)해서 디스크가 차지 않게 한다

    이 세 줄을 추가하는 순간, 백업은 “있는 줄 알았는데 비어 있던” 함정에서 벗어난다.


    한 줄 정리

    백업은 다른 장치에·자동으로·매일. tar czf - … | ssh면 포트 하나 더 안 열고 NAS에 던질 수 있다. DB 덤프 먼저, 그다음 파일 백업. 그리고 크기를 확인해 “성공”을 검증하자 — 비어 있는 백업은 백업이 아니다.

  • 공짜로 워드프레스 지키기 — Wordfence 무료판과 유료 함정

    공짜로 워드프레스 지키기 — Wordfence 무료판과 유료 함정

    공짜로 워드프레스 지키기 — Wordfence 무료판과 유료 함정

    셀프호스팅 구축기 11편. 봇은 이미 8편에서 왔다. 이번엔 현관에 상주 경비를 세운다.

    ← 이전 편: 10편 — 블로그에 뭘 쓰면 안 되나 (개인정보 검열)
    → 다음 편: 12편 — 새벽마다 NAS로 (tar+ssh 백업)

    요약

    • 워드프레스는 전 세계 웹의 40%가 넘게 쓴다 — 그래서 자동 공격 봇의 1순위 표적
    • Wordfence 무료판이면 방화벽(WAF) · 2단계 인증(2FA) · 멀웨어 스캔이 전부 된다, 비용 $0
    • 함정: 설치하면 화면이 무료를 숨기고 유료($149/년)로 유도한다 — 무료 키는 작은 밑줄 링크에 숨어 있다
    • 무료와 유료의 진짜 차이는 “방화벽 룰을 30일 늦게 받는다”뿐 — 개인 블로그엔 무방
    • 확장 방화벽은 PHP보다 앞에 서야 진짜 막는다 — .htaccess 한 줄, 켜기 전 백업 필수

    1. 워드프레스는 왜 표적인가

    전 세계 웹사이트의 40%가 넘게 워드프레스로 돈다. 점유율이 높다는 건 공격자 입장에서 취약점 하나로 수천만 사이트를 노릴 수 있다는 뜻이다.

    8편에서 봤듯, 도메인을 공개하면 24시간 안에 로그인 페이지로 무차별 대입(brute force)이 들어온다. 사람이 아니라 자동 봇이다. 플러그인·테마의 알려진 구멍도 끊임없이 긁어 간다. 그래서 워드프레스는 “띄웠으면 바로 경비를 세워야 하는” 시스템이다.

    2. Wordfence 무료판이 해주는 것

    기능 무료판 설명
    방화벽(WAF) O 알려진 공격 패턴을 들어오는 길목에서 차단
    2단계 인증(2FA) O 비번이 털려도 한 겹 더 — 무차별 대입 1차 방어
    멀웨어 스캐너 O 변조된 파일·심어진 백도어 탐지
    실시간 악성 IP 차단 30일 지연 유료는 즉시, 무료는 30일 뒤 수신

    개인 블로그가 진짜로 필요한 건 위 셋(방화벽·2FA·스캔)이고, 전부 무료다. 실시간 IP 목록은 대형 표적 사이트에나 의미 있는 차이다.

    3. 무료 키 찾기 — 스토어의 유료 함정

    설치하면 “라이선스를 받으세요” 화면이 뜨고, 누르면 워드펜스 스토어로 보낸다. 여기가 헷갈린다.

    • 크고 파란 버튼 = “real-time protection” = 유료(연 $149). 장바구니에 자동으로 두 개가 담겨 더 비싸 보이기도 한다.
    • 무료 키는 그 아래 작은 밑줄 링크에 숨어 있다: I'm OK waiting 30 days for protection updates(30일 늦게 받아도 괜찮아요).
    • 이 링크를 눌러야 이메일로 무료 라이선스 키가 온다. 플러그인에 붙여넣으면 끝. 비용 $0.

    결제 화면처럼 보이지만, 결제하지 않아도 된다. “30일 기다려도 괜찮다”는 문장 하나를 찾는 게 전부다.

    4. 진짜 방어는 PHP보다 앞에 — 확장 방화벽

    기본 상태의 방화벽은 워드프레스(PHP) 안에서 돈다. 즉 공격 요청이 이미 PHP까지 들어온 다음에 검사한다. 한 발 늦다.

    “방화벽 최적화(Optimize)”를 켜면, PHP가 실행되기 전에 먼저 검사하도록 웹서버 설정(.htaccess)에 한 줄이 추가된다.

    php_value auto_prepend_file '/var/www/html/wordfence-waf.php'
    
    flowchart LR
        V[방문자/봇] --> W[확장 방화벽
    PHP보다 먼저] W -->|정상| P[PHP / 워드프레스] W -->|공격| X[차단]

    ⚠️ 켜기 전에 반드시 .htaccess를 백업하자. 서버 환경에 따라 이 한 줄이 사이트를 500 에러로 만들 수 있는데, 그때 복구는 백업한 .htaccess를 되돌리는 것이다. 그리고 처음엔 “학습 모드”로 하루이틀 정상 트래픽을 익히게 둔 뒤 자동으로 보호 모드로 전환된다.

    5. 로그인 자체를 숨기기 (보너스)

    /wp-login.php는 전 세계가 아는 주소다. 별도 플러그인으로 로그인 주소를 다른 경로로 숨기면, 봇이 두드릴 문 자체가 사라진다. 어디로 숨겼는지는 당연히 공개하지 않는다 — 그게 핵심이다.

    여기에 2FA를 켜고 “이 기기 7일 기억” 옵션을 쓰면, 보안은 챙기면서 매번 코드를 입력하는 번거로움은 던다.


    한 줄 정리

    무료 Wordfence면 개인 블로그 방어는 충분하다. 유료 유도 화면에서 “30일 기다려도 괜찮다”는 작은 링크만 찾으면 $0. 확장 방화벽은 켜기 전 .htaccess 백업, 로그인 주소는 숨기고 2FA. 돈이 아니라 클릭 몇 번의 문제다.

  • 블로그에 뭘 쓰면 안 되나 — 셀프호스팅 글의 개인정보 검열

    블로그에 뭘 쓰면 안 되나 — 셀프호스팅 글의 개인정보 검열

    블로그에 뭘 쓰면 안 되나 — 셀프호스팅 글의 개인정보 검열

    셀프호스팅 구축기 10편. 서버를 막는 것만큼, 그 서버 이야기를 쓴 글을 막는 것도 중요하다.

    ← 이전 편: 9편 — 보이지 않는 다리: Cloudflare Tunnel
    → 다음 편: 11편 — 공짜로 워드프레스 지키기 (Wordfence)

    요약

    • 셀프호스팅 글은 본질적으로 내 서버 구조를 공개한다 — 튜토리얼 가치와 정찰 정보 사이의 줄타기
    • 내가 실제로 흘린 것: 리눅스 계정명, 미니PC 내부 IP, 개인 이메일 (비번·토큰은 다행히 안 샘)
    • 가려야 할 5가지: 시크릿 · 개인정보 · 내부 네트워크 · 파일 경로 · frontmatter
    • 시크릿은 한 번 새면 교체밖에 답이 없다 — 검색엔진이 이미 긁어간 뒤다
    • 발행 전 grep 한 줄이면 기계가 대신 훑어준다

    1. 내가 실제로 흘린 것

    9편까지 신나게 써놓고 다시 보니, 글 곳곳에 이런 게 박혀 있었다.

    • /home/내계정/... — 리눅스 계정명이 경로에 그대로. SSH 무차별 대입(brute force)의 표적이 된다.
    • http://192.168.x.x:3001 — 미니PC 내부 IP. 내 홈 네트워크 구조를 알려주는 셈.
    • 도메인 설명하다 슬쩍 들어간 개인 Gmail 주소 한 줄.

    비밀번호·토큰은 운 좋게 변수 placeholder나 <openssl rand -hex 24> 같은 예시값으로 써둬서 안 샜다. 하지만 위 셋은 진짜였다.

    문제는 공개 블로그가 영구적이라는 점이다. 검색엔진이 인덱싱하고 캐시가 남는다. 지운다고 없던 일이 되지 않는다. 그래서 시크릿이 한 번 샜다면, 지우는 게 아니라 즉시 재발급해야 한다.


    2. 가려야 할 5가지

    분류 예시 왜 위험한가
    ① 시크릿 API 키·토큰·비번·개인키 계정·서버 탈취
    ② 개인정보 계정명·이메일·실명 신원·표적·스팸
    ③ 내부 네트워크 사설 IP·내부 호스트명·NAS 주소 침입 후 횡단 정찰
    ④ 파일 경로 /home/내계정/… 계정명·구조 노출
    ⑤ frontmatter 글 맨 위 메타데이터 발행 때 통째로 새기 쉬움

    ⑤번이 의외다. 마크다운으로 쓰면 글 맨 위에 ---로 둘러싼 메타데이터(frontmatter, 작성자·날짜·파일명 등)가 있는데, 발행 변환이 한 번 어긋나면 이게 본문에 코드블록으로 통째 노출된다. 실제로 내 글 하나가 그렇게 frontmatter를 다 드러내고 있었다.


    3. 지우지 말고 일반화하라

    핵심은 삭제가 아니라 치환이다. 튜토리얼 가치는 살리되 내 진짜 정보만 가린다.

    실제 발행본
    /home/내계정 /home/user
    내부 IP 192.168.1.50 192.168.0.x 또는 <server-ip>
    개인 이메일 [email protected]
    비번·토큰 PASSWORD 변수, <your-token>

    <openssl rand -hex 24>처럼 여기에 직접 생성한 값을 넣으세요라고 명시하면, 읽는 사람도 따라 하기 더 좋다. 일반화가 오히려 더 친절한 튜토리얼이 되는 셈이다.


    4. 발행 전 자동 검사 (grep 한 줄)

    눈으로 매번 잡으면 결국 샌다. 발행 전에 기계로 훑는다.

    grep -rEn "/home/[a-z]+|[0-9.]+|@(gmail|naver)\.com|_TOKEN=|_KEY=|PASSWORD=" 내-글-폴더/

    계정 경로·IP·개인 메일·시크릿 흔적을 한 번에 잡는다. 결과가 0줄이어야 발행. 변수·꺾쇠 플레이스홀더만 남으면 통과다.

    발행한 뒤엔 실제 공개된 페이지도 한 번 더 본다. 캐시 때문에 옛 화면이 남아 있을 수 있어서다.

    curl -s https://내도메인/글-주소/ | grep -E "/home/|192.168.|@gmail"

    5. 더 큰 질문 — 애초에 공개할까?

    개인정보를 다 가려도 남는 문제가 있다. 글 자체가 내 인프라 지도라는 것.

    나는 글로 쓰지 않는 비공개 운영 도구를 따로 돌린다. 키와 IP를 아무리 가려도 이런 구조의 관리 도구가 여기 있다는 사실 자체가 정찰 정보이기 때문이다.

    그래서 순서가 중요하다. 이 시리즈를 공개할까?를 먼저 정하고, 공개하기로 한 것만 앞의 검열을 거친다. 가릴 수 있는 정보와, 애초에 쓰면 안 되는 주제는 다른 층위의 문제다.


    한 줄 정리

    서버를 막는 P0가 있다면, 글을 막는 P0도 있다. 계정명·내부 IP·개인 메일·시크릿·frontmatter 다섯 가지를 플레이스홀더로 일반화하고, 발행 전 grep 한 줄로 검사하자. 시크릿은 새면 교체뿐이니, 처음부터 안 쓰는 게 가장 싸다.

  • 이름이 어렵다 — 사이트명 정하기

    이름이 어렵다 — 사이트명 정하기

    셀프호스팅 구축기 7편. 도메인보다 사이트명이 더 어렵다.

    ← 이전 편: 6편 — 도메인 사기: Cloudflare Registrar


    요약

    • 처음 이름 “잼잼의 항해일기” → 사흘 만에 “별고래”로 교체
    • 도메인(sticknstone.org)과 브랜드명(별고래)이 달라도 OK. About 페이지에서 풀면 됨
    • 영문 부제 “Star Whale” 병기 → 글로벌 검색 친화
    • 1인 블로그 네이밍 5기준: 차별·메타포 깊이·기억성·도메인 충돌·영어 발음

    1. “잼잼의 항해일기”의 문제

    처음 정한 이름. jamjam과 “항해일기”의 조합. 사흘 동안 글 위치 정하면서 5가지가 거슬렸다.

    거슬린 것이유
    너무 일상적“공부·트레이딩·자동화” 같은 객관 콘텐츠와 톤 충돌
    닉네임 노출jamjam이라는 닉이 사이트 정체성보다 본인 정체성
    모방 가능“OO의 항해일기” 같은 블로그 너무 많음. 검색 노출 약함
    영문 변환 어려움“jamjam’s voyage diary” — 이상함
    시야 좁음“일기”는 개인 일기장. 외부 독자가 보기엔 너무 사적

    2. 새 이름 후보들

    새 사이트명 후보 5개를 비교했다.

    이름의미차별메타포기억영문
    잼잼의 항해일기내 일기★★★★★
    firewhale불 + 고래 (강렬함 + 깊이)★★★★★★★★★★★★
    별고래별 + 고래 (탐험·깊이)★★★★★★★★★★★★★★★★
    한 평짜리 서재1인 작업실★★★★★★★★★
    항해사의 자료실정보 모음소★★★★★★

    별고래가 차별·메타포에서 가장 앞섬. 영문은 약하지만 부제 “Star Whale”로 보완 가능.


    3. 별고래의 메타포 깊이

    
    “별고래는 별을 향해 가는 깊은 항해” — 톤·콘텐츠 방향·블로그 정체성을 한 문장에 담음. 내 학습 노트는 “차분히 멀리” 가는 게 핵심이라 fit.

    별은 방향, 고래는 깊이를 뜻한다. 빠르게 소비되고 사라지는 정보 대신, 한 주제를 오래 파고들어 멀리 가겠다는 태도다. 트레이딩·학습·자동화처럼 길게 누적해야 의미가 생기는 주제와 잘 맞물린다. 이름 하나에 그 방향을 담아두면, 글을 쓸 때마다 “이게 별고래다운가?”라는 편집 기준이 생긴다.


    4. firewhale 탈락 — SEO 충돌

    “불 + 고래” = 불의 강렬함 + 고래의 깊이. 좋아 보이지만 검색해보니:

    점유자무엇
    firewhale.ioiOS 앱 개발사 (음악 앱 다수)
    firewhale.orgNotable Publications
    firewhalemusic.com음악 아티스트
    Firewhale (FIREW)암호화폐 토큰
    GitHub FireWhale개인 계정


    신규 사이트가 firewhale로 시작하면 영원히 묻힘.

    별고래는 검색해보면 거의 깨끗하다.
    SEO·GEO 노출 측면에서 유리하다 판단했다


    5. 영문 부제 “Star Whale”

    한국어 메인 + 영문 부제. WordPress 설정:

    Site Title:    별고래
    Tagline:       Star Whale — Self-hosted notes on learning, trading, automation
    

    장점:
    – 한국 검색: “별고래” 깨끗하게 1위
    – 영문 검색: “Star Whale” 부분이 영문 노출
    – 명함·SNS 자기소개: “별고래 (Star Whale)” 자연스러움
    – 글로벌 확장 시 부담 X

    Starwhale 한 단어는 starwhale.ai라는 ML 회사 있음. 나는 Star Whale 두 단어로 분리하면 무관 (브랜드 인접성만 약간).


    6. 도메인과 사이트명 불일치 — 괜찮은가

    도메인사이트명사례
    google.comGoogle일치
    meta.comMeta일치
    medium.comMedium일치
    stripe.comStripe일치
    sticknstone.org별고래불일치

    일반론은 일치해야하나 나는 의도적 분리했다.

    왜 분리해도 괜찮은가:
    – About 페이지에서 한 줄로 풀면 자연스러움
    – “Stick & Stone = 옛 항해사가 별을 가리키던 돌·막대 기구” → “별고래 = 그 별을 향한 깊은 항해” → 스토리텔링 자산
    – URL을 외우게 하려는 게 아니라 검색해서 들어오게 하는 사이트라 도메인-브랜드 일치는 부차

    기업 사이트라면 일치가 맞다. 개인 학습 노트 블로그는 다르다.


    7. 변경 작업 — wp-cli 한 줄

    wp option update blogname "별고래" --allow-root
    wp option update blogdescription "Star Whale — Self-hosted notes on learning, trading, automation" --allow-root
    

    이게 끝. 추가로 갱신해야 하는 것:
    llms.txt 한 줄 (# 별고래 (Star Whale))
    – About 페이지 (스토리텔링)
    – OG 태그 (Rank Math 자동)
    – 메일 발신자 이름 (UpdraftPlus 알림 등, 자동 따라옴)


    FAQ

    Q. 이름 한 번 정하면 못 바꾸나?
    바꿀 수 있음. 단 일찍 바꿀수록 비용 0. 발행 글 100편 후 바꾸면 외부 링크·소셜·검색 인덱스 전부 손봐야 함.

    Q. 영문 부제는 필수?
    한국어만으로도 가능. 단 영문 부제 = 글로벌 검색 친화 + 명함 자연스러움. 5분 작업 대비 이득 크다.

    Q. “별고래” 같은 합성 한국어가 외국인에게 어렵지 않나?
    어려움. 그래서 영문 부제. 내 메인 독자는 한국인이라 우선순위는 한국어.

    Q. 트레이드마크 등록해야 하나?
    1인 블로그라면 불필요. 외부 사용자가 별고래 이름으로 상품·서비스 만들 가능성은 없다 봐야한다. 콘텐츠 누적 + 수익화 시점에 재검토.

    Q. 사이트명 결정 못해서 발행 미루고 있는데?
    임시명으로 발행 시작. 콘텐츠가 우선. 이름은 콘텐츠 5편 정도 쓰면 자연스럽게 굳어짐.


    다음 편 예고

    8편 — 24시간 안에 봇이 온다: 도메인 공개 전 보안 P0. xmlrpc 차단 + wp-login 슬러그 변경 = 25분 작업으로 brute force 99% 차단.


    한 줄 정리

    사이트명은 도메인보다 어렵다. 생각보다 예민한 주제.
    별고래는 차별·메타포·검색 노출 5기준에서 다른 것보다 유리하기에 채택했다. 한국어 메인 + 영문 부제(Star Whale) + About 페이지 스토리텔링으로 도메인 불일치 보완하려한다.

  • 24시간 안에 봇이 온다 — 도메인 공개 전 보안 P0

    24시간 안에 봇이 온다 — 도메인 공개 전 보안 P0

    24시간 안에 봇이 온다 — 도메인 공개 전 보안 P0

    셀프호스팅 구축기 8편. 도메인 띄우기 전에 반드시 끝내야 하는 25분.


    TL;DR

    • WordPress 도메인 공개 = 24시간 안에 brute force 봇이 옴. 통계적 사실
    • 25분 작업으로 99% 차단: ① 사용자명 admin 회피 ② xmlrpc 차단 ③ wp-login 슬러그 변경
    • 이 셋이 보안 P0. 도메인 띄우기 전에 끝낸다
    • Wordfence·2FA는 P1 (도메인 띄운 후 추가)

    1. 첫 24시간의 진실

    WordPress 사이트가 인터넷에 노출되면 첫 24시간 안에:

    공격 빈도 대상
    /wp-login.php brute force 시간당 수천 회 관리자 비번
    /xmlrpc.php pingback amp 끊임없음 DDoS 증폭
    알려진 플러그인 취약점 스캔 끊임없음 옛 버전 플러그인
    SQL injection 시도 자동 봇 ?id=1 OR 1=1 URL
    User enum (?author=1) 자동 봇 사용자명 알아내기

    이걸 막을 게 아니라 표면 자체를 없애는 게 P0.


    2. P0 — 보안 3종 셋업

    

    3. ① 사용자명 admin 회피

    WordPress 설치 마법사가 첫 사용자 만들 때 admin이라고 입력하지 마세요. 봇 brute force 1순위 타겟.

    좋은 예: jamjam, sailor904, 본인 닉
    나쁜 예: admin, administrator, root, wpadmin
    

    이미 admin으로 만들었으면 wp-cli로 새 사용자 생성 후 admin 권한 박탈 또는 삭제:

    wp user create newname [email protected] --role=administrator --user_pass=$(openssl rand -base64 24) --allow-root
    wp user delete admin --reassign=newname --allow-root
    

    4. ② xmlrpc.php 차단 — mu-plugin

    /xmlrpc.php는 WordPress 옛 API. 거의 안 쓰임 (Jetpack 사용자만). 봇이 brute force amplification (한 번 요청으로 비번 1000개 시도)에 악용.

    /var/www/html/wp-content/mu-plugins/disable-xmlrpc.php:

    <?php
    /**
     * Plugin Name: Disable XML-RPC
     * Description: Block xmlrpc.php to reduce attack surface.
     */
    if (!defined('ABSPATH')) exit;
    
    add_filter('xmlrpc_enabled', '__return_false');
    add_filter('wp_headers', function ($headers) {
        unset($headers['X-Pingback']);
        return $headers;
    });
    add_filter('xmlrpc_methods', function ($methods) {
        unset($methods['pingback.ping']);
        unset($methods['pingback.extensions.getPingbacks']);
        return $methods;
    });
    add_action('init', function () {
        $uri = $_SERVER['REQUEST_URI'] ?? '';
        if (stripos($uri, '/xmlrpc.php') !== false) {
            status_header(403);
            exit('xmlrpc disabled');
        }
    });
    

    검증:

    curl -I http://localhost:8090/xmlrpc.php
    # HTTP/1.1 403  ← 성공
    

    5. ③ wp-login 슬러그 변경

    /wp-login.php는 모든 WordPress 사이트가 똑같이 갖는 로그인 URL. 봇이 처음 들어와서 자동으로 칠 URL. 이걸 다른 슬러그로 숨김.

    플러그인 wps-hide-login 설치:

    wp plugin install wps-hide-login --activate --allow-root
    wp option update whl_page starport --allow-root
    

    starport는 사장님이 정한 비밀 슬러그. 추측 어려운 단어 + 본인 기억하기 쉬운 거. 예: byeolgorae-gate, secret-door-2026, starport.

    검증:

    curl -I -L http://localhost:8090/wp-login.php
    # 301 redirect → 가짜 URL (봇 헛걸음)
    
    curl -I -L http://localhost:8090/wp-admin/
    # 404 (로그아웃 상태에선 admin 페이지 자체가 없는 척)
    
    curl -I -L http://localhost:8090/starport/
    # 200 (사장님만 아는 로그인 페이지)
    

    사장님이 슬러그 잊으면 본인도 못 들어옴. 즐겨찾기·메모 필수.


    6. 결과 — 봇 입장에서

    공격 차단 후 응답
    GET /wp-login.php 301 → 가짜 URL → 봇이 거기서 시도 → 영원히 실패
    POST /xmlrpc.php 403 즉시 차단. amplification 불가
    GET /wp-admin/ 404. 관리 페이지 자체가 없는 척
    사용자명 brute force admin 시도 → 우리 사이트엔 admin 없음
    ?author=1 user enum Rank Math가 자동 차단 (Disable Author Archives)

    봇이 들이는 자원 = 사장님 사이트에서 얻는 게 0.


    7. P0 vs P1 vs P2 — 우선순위

    단계 작업 시점
    P0 (필수, 도메인 공개 전) 사용자명·xmlrpc·wp-login 슬러그 이번 25분
    P1 (도메인 띄운 직후) Wordfence 위자드 + 2FA 사장님 GUI 작업
    P2 (월 1회) 플러그인 보안 업데이트 + WPA(취약점 스캔) 운영 단계

    P0 안 끝내고 도메인 띄우면 — 봇이 24시간 안에 와서 brute force 시작. 셋업 안 됐어도 어차피 못 뚫지만, 로그에 brute force 시도가 쌓이고 미니PC CPU 잡아먹음. 미리 막는 게 정신 위생.


    8. 함정

    함정 1. mu-plugin 자동 활성

    mu-plugins는 일반 플러그인 위에서 강제 실행 (mu = Must Use). 따라서 xmlrpc 차단처럼 절대 비활성되면 안 되는 보안 정책은 mu-plugin이 맞는 곳.

    함정 2. WPS Hide Login slug 잊기

    whl_page 옵션을 DB에 직접 박아두기 때문에, 사장님이 슬러그 잊으면 본인도 못 들어옴. 복구 방법: wp option get whl_page --allow-root (미니PC SSH로) 또는 mu-plugin 임시 비활성.

    함정 3. 인증 후 자동 우회

    WPS Hide Login은 슬러그를 모르는 사람만 차단. 로그인된 세션은 그대로 /wp-admin/ 사용. 사장님이 한 번 로그인하면 그 이후 30분 동안 admin 페이지 자유.

    함정 4. 로그아웃 시 새 슬러그로 진입

    사장님이 로그아웃하면 다음 로그인은 /starport로. 즐겨찾기 갱신 필수.


    FAQ

    Q. 이미 도메인 공개해버렸는데 P0 안 했으면?
    지금 즉시 cloudflared 끄거나 Tailscale Funnel 해제 → P0 셋업 → 다시 공개. 5분.

    Q. Wordfence 위자드(P1)는 왜 P0 아닌가?
    P0는 표면 자체를 없애는 것. Wordfence는 표면이 있는 상태에서 공격을 탐지·차단. 보완재.

    Q. 슬러그를 /admin 으로 바꿔도 되나?
    /admin도 봇이 시도하는 흔한 슬러그. 추천 X. 본인만 떠올릴 수 있는 + 추측 어려운 단어.

    Q. 2FA 없어도 P0면 충분한가?
    충분 + 충분. 2FA는 비번 노출됐을 때 마지막 방패. P0 + 2FA = 사실상 무적.

    Q. xmlrpc 차단하면 Jetpack 못 쓰나?
    Jetpack 일부 기능 X. Jetpack 자체가 없으면 무관. 우리 별고래는 Jetpack 안 씀.


    다음 편 예고

    9편 — 보이지 않는 다리: Cloudflare Tunnel. 드디어 도메인을 미니PC에 연결. 공유기 포트 안 열고, 미니PC IP 안 들키고, WAF·DDoS·CDN까지 무료로.


    한 줄 정리

    WordPress 도메인 공개 = 24시간 안에 봇이 옴. 25분 P0 셋업(사용자명·xmlrpc·wp-login 슬러그)으로 99% 차단. 도메인 띄우기 전에 끝낸다.

  • 보이지 않는 다리 — Cloudflare Tunnel로 우리집 블로그 외부 노출하기

    보이지 않는 다리 — Cloudflare Tunnel로 우리집 블로그 외부 노출하기

    보이지 않는 다리 — Cloudflare Tunnel로 우리집 블로그 외부 노출하기

    셀프호스팅 구축기 9편. 도메인 산 다음, 우리집 컴퓨터까지 트래픽을 어떻게 끌어오나.

    ← 이전 편: 8편 — 24시간 안에 봇이 온다 (보안 P0)
    → 다음 편: 10편 — 블로그에 뭘 쓰면 안 되나 (개인정보 검열)

    TL;DR (한 화면 요약)

    • 문제: 우리집 미니PC에 WordPress를 띄웠는데, 산 도메인(sticknstone.org)을 외부에서 어떻게 연결하나
    • 옛 방식: 공유기 포트 열기 → 공인 IP DNS 등록 → SSL 갱신. 5가지 골치
    • 새 방식: Cloudflare Tunnel — 미니PC가 Cloudflare에 outbound로 다리를 미리 놓는다. 공유기 구멍 X, IP 노출 X
    • 도구: cloudflared 데몬을 Docker로 가동. 5분
    • 공짜 보너스: WAF·DDoS·CDN·SSL·트래픽 통계 전부 무료
    • 비용: 0원 (도메인 갱신비 연 $10 별도)

    1. 풀고 싶은 문제

    블로그(WordPress)는 우리집 미니PC에 띄워뒀다. 도메인(sticknstone.org)도 샀다. 그런데 이 둘이 어떻게 만나야 하나?

    평범한 길 (포트포워딩) 5가지 골치

    골치
    공유기 구멍 443번 포트 외부에 열면 평생 봇 공격
    공유 IP 통신사가 매번 바꿈. 자동 갱신 스크립트 필요
    우리집 위치 노출 IP만 알면 대략적 동네 추정 가능
    SSL 갱신 매 90일마다 Let’s Encrypt 직접
    트래픽 폭주 DDoS 맞으면 우리집 인터넷 마비

    2. 다른 길: 다리를 놓는다

    발상의 전환: 외부에서 우리집으로 들어오는 연결을 받지 말자. 대신 우리집에서 외부 어딘가로 나가는 연결을 미리 만들어두자.

    flowchart LR
        subgraph oldway["옛 방식 — 포트포워딩"]
            A1[방문자] --> A2[공유기 포트 443 오픈]
            A2 --> A3[미니PC 공개 IP]
            A3 --> A4[WordPress]
            A5[해커] -.공격.-> A2
        end
        subgraph newway["새 방식 — Cloudflare Tunnel"]
            B1[방문자] --> B2[Cloudflare 서울 DC]
            B2 -. 미리 열린 outbound 채널 .-> B3[미니PC cloudflared]
            B3 --> B4[WordPress]
            B5[해커] -.차단.-> B2
        end
        style A5 fill:#fdd,stroke:#a00
        style B5 fill:#dfd,stroke:#0a0
        style A3 fill:#fdd
        style B3 fill:#dfd

    핵심: 미니PC가 먼저 손을 뻗는다. 외부 → 우리집 흐름이 아니라, 우리집 → Cloudflare로 outbound 연결을 늘 열어둔다. 방문자 요청은 Cloudflare가 이 채널로 미니PC에 던진다.

    호텔 비유

    호텔 손님(미니PC)이 친구를 만나고 싶을 때.

    • 평범한 길: 호텔 입구에 자기 방 번호를 크게 써붙임. 친구는 그 번호로 들어옴. 동시에 도둑도 옴.
    • 다리의 길: 호텔 프론트데스크(Cloudflare)에 미리 메모를 남긴다. 내 친구 찾는 사람 있으면 객실로 연결해달라고. 친구는 이름만 대고, 프론트데스크가 안내한다. 방 번호는 외부 노출 X. 의심스러운 사람은 1차로 거른다.

    3. 트래픽 흐름 — 방문자가 도메인 칠 때

    sequenceDiagram
        participant V as 방문자 브라우저
        participant CF as Cloudflare 서울 DC
        participant CD as cloudflared (미니PC)
        participant WP as WordPress 컨테이너
        V->>CF: GET https://sticknstone.org
        Note over CF: WAF·DDoS 1차 필터
        CF->>CD: 미리 열린 outbound 채널로 forward
        CD->>WP: http://localhost:8090
        WP-->>CD: HTML 응답
        CD-->>CF: outbound 채널 회신
        CF-->>V: SSL 종단·CDN 캐시 + 응답

    4단계: 방문자 → Cloudflare(필터) → 다리 → 미니PC → 역순. 미니PC IP는 어디에도 안 등장.


    4. 다리지기: cloudflared

    이 outbound 채널을 우리집에서 유지하는 작은 데몬이 cloudflared — Cloudflare 공식 오픈소스.

    왜 Docker로 띄우나

    미니PC에 이미 Docker가 돌고 있다. 다른 컨테이너들과 같은 호텔에 묵는 게 자연스럽다.

    flowchart TB
        subgraph minipc["미니PC (Ubuntu 24.04)"]
            subgraph docker["Docker 호스트"]
                CD[cloudflared / network_mode host]
                WP[wordpress-wordpress-1 :8090]
                DB[(wordpress-db-1 MariaDB)]
                RD[(wordpress-redis-1 Object Cache)]
                UM[umami_umami-1 :3001]
                UDB[(umami_db-1 Postgres)]
            end
            WP --> DB
            WP --> RD
            UM --> UDB
        end
        CF[Cloudflare 네트워크] -. outbound .-> CD
        CD --> WP
        CD --> UM
        style CD fill:#ffe4b5,stroke:#d97706
        style CF fill:#dbeafe,stroke:#1d4ed8

    cloudflarednetwork_mode: host로 띄워서 호스트의 localhost:8090(WP)와 localhost:3001(Umami)에 접근하게 한다.

    설치 5분 가이드

    폴더 + 설정 파일:

    /home/user/cloudflared/
    ├── .env                # TUNNEL_TOKEN=eyJh... (Cloudflare 대시보드에서 받은 토큰)
    └── docker-compose.yml

    docker-compose.yml:

    services:
      cloudflared:
        image: cloudflare/cloudflared:latest
        container_name: cloudflared
        restart: unless-stopped
        network_mode: host
        command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
        env_file: .env

    가동 + 로그 검증:

    cd /home/user/cloudflared
    sudo docker compose up -d
    sudo docker logs cloudflared --tail 20

    성공 로그에 다음 4줄이 보인다 (Cloudflare 서울 데이터센터 4곳 동시 연결):

    Registered tunnel connection ... location=icn06 protocol=quic
    Registered tunnel connection ... location=icn01 protocol=quic
    Registered tunnel connection ... location=icn05 protocol=quic
    Registered tunnel connection ... location=icn06 protocol=quic

    미니PC는 4중 백업 채널로 Cloudflare에 연결된다. 한 채널이 죽어도 나머지 3개가 살아있다.


    5. 토큰은 위험하다 — 어디 두나

    cloudflared가 Cloudflare에 자기를 증명할 때 쓰는 토큰. 발급 시 한 번 보여주고 끝.

    토큰 다루기 원칙

    • .env 파일에 평문 저장. 권한 chmod 600 (소유자만 읽기·쓰기)
    • 채팅·노트·git 커밋에 절대 평문 X
    • 토큰 노출 의심 시 즉시 Cloudflare 대시보드에서 revoke + 재발급

    정확한 명령

    sudo chown user:user /home/user/cloudflared/.env
    sudo chmod 600 /home/user/cloudflared/.env

    6. 공짜로 따라오는 것

    Cloudflare 무료 플랜으로 이 다리만 놓아도 따라오는 보너스:

    보너스 설명 평소 가격대
    WAF (웹 방화벽) SQL injection·XSS 알려진 공격 자동 차단 월 $20~
    DDoS 보호 트래픽 폭주 자동 흡수, 무제한 월 $200~
    CDN 정적 파일 전 세계 캐싱. 외국 방문자도 빠르게 월 $10~
    SSL 자동 Let’s Encrypt 갱신 신경 0 월 $5~
    트래픽 통계 Cloudflare 대시보드에서 그래프로 (분석 도구)

    총합 비용: 0원. 도메인 갱신비 연 $10만.


    7. 함정 — 우리가 실제로 만난 것

    함정 1. Operating System 선택 헷갈림

    Cloudflare 대시보드의 cloudflared 설치 명령 화면에서 OS를 골라야 한다. 처음에는 자기 윈도우 PC가 OS인 줄 알고 Windows를 골랐다. 틀렸다. cloudflared가 설치되는 곳은 미니PC(Ubuntu)지 내 윈도우가 아니다. Docker를 골랐어야 한다.

    함정 2. 토큰을 LLM 채팅에 그대로 붙여넣음

    복사한 토큰을 LLM 채팅에 보내면 그 채팅 내용은 외부 서버에 저장된다. 토큰은 카드 비밀번호급 민감 정보. 작업 끝나면 즉시 Cloudflare 대시보드에서 토큰 회수 권장.

    함정 3. cloudflared가 어떻게 다른 서비스에 forward하나

    cloudflared 컨테이너만 띄우면 Cloudflare와 연결은 되지만, 들어온 요청을 어디로 보낼지는 별도 설정이다. Public Hostname 설정이라고 한다. 다음 편(10편)에서 다룬다.


    8. 검증

    가동 직후 Cloudflare 대시보드의 Tunnel 페이지 하단 Connection Status를 본다.

    Before:  No connection detected yet
    After:   Connected (체크)

    이 한 줄이 바뀌면 다리가 놓인 것이다.


    FAQ

    Q. Tailscale Funnel과 무엇이 다른가?
    둘 다 NAT 우회 + 미니PC IP 비공개라는 점은 같다. Cloudflare Tunnel은 WAF·DDoS·CDN·자기 도메인 사용까지 무료로 묶어준다. Tailscale Funnel은 전용 도메인 강제 + 보호막 없음. 블로그용은 Cloudflare가 우위.

    Q. 미니PC가 꺼지면 어떻게 되나?
    다리가 끊긴다. 방문자는 Cloudflare에서 521(Web server is down) 에러를 본다. 미니PC가 다시 켜지면 cloudflared가 자동으로 다리를 재연결한다 (Docker restart: unless-stopped).

    Q. Cloudflare가 트래픽을 보나? 개인정보 우려?
    Cloudflare는 SSL을 자기가 종단한다 = 평문 트래픽을 본다. 신뢰 모델은 Cloudflare는 내 편이라는 가정. 신뢰 못 하면 적합 X (대안: 자체 reverse proxy + Let’s Encrypt + dynamic DNS).

    Q. 무료 플랜 한계는?
    대역폭 무제한, 사이트 수 무제한, Tunnel 수 무제한. 단 Cloudflare Workers·R2 같은 부가 서비스에 별도 한도. 블로그 1개 운영엔 무료 플랜이 평생 충분.

    Q. 도메인이 .org가 아니어도 되나?
    무관. .com / .net / .io / .dev 모두 동일하게 작동. 단 도메인이 Cloudflare DNS를 쓰는 게 가장 쉬움 (자동 DNS 등록).


    다음 편 예고

    10편에서는 Public Hostname을 설정해 sticknstone.org → WordPress로 매핑하고, 글에 무엇을 쓰면 안 되는지(개인정보 검열)를 다룬다.


    한 줄 정리

    집에 있는 컴퓨터에 블로그를 띄우려면 공유기에 구멍 뚫고 IP 노출하는 옛 방식 대신, 컴퓨터에서 Cloudflare로 outbound 채널을 미리 열어두는 Cloudflare Tunnel 방식이 깔끔하다. 그 채널을 유지하는 작은 데몬이 cloudflared. Docker로 띄우면 5분.


    참고