Archive horanglab 제작기: 블로그와 서버를 직접 만들며 겪은 일

옛 워드프레스 블로그를 정리하면서, 글을 쓰는 도구부터 서버까지 직접 만든 블로그로 옮겼습니다. 지금 보고 계신 이 사이트입니다. 만드는 과정에서 어떤 구조를 골랐고, 어떤 문제를 만났는지 정리했습니다.

보안을 위해 서버 IP, 포트 번호, 계정 이름, 비밀번호 같은 실제 값은 이 글에 적지 않았습니다. 어떤 문제가 있었고 어떻게 해결했는지만 다룹니다.

한눈에 보기

구분 사용 기술
공개 사이트 ASP.NET Core Razor Pages
관리 사이트 Blazor Server, MudBlazor
데이터베이스 PostgreSQL, EF Core
서버 Rocky Linux, nginx, systemd
앞단 Cloudflare 프록시, Cloudflare Access
저장소 RAID10 위의 LVM 볼륨

서버 사양

데스크톱 PC 한 대를 서버로 쓰고 있습니다.

항목 사양
운영체제 Rocky Linux 10.2
CPU AMD Ryzen 7 5700X (8코어 16스레드, 최대 약 4.6GHz)
메모리 16GB
메인보드 ASRock X570 Pro4

전체 구조

방문자 ── Cloudflare ── nginx ── 공개 사이트(읽기 전용) ──┐
                                                        ├── PostgreSQL · 미디어
관리자 ── Cloudflare Access ── nginx ── 관리 사이트 ───────┘

처음부터 공개 사이트와 관리 사이트를 나누는 것을 기준으로 잡았습니다. 공개 사이트에는 로그인도 쓰기 기능도 없습니다. 공격받을 수 있는 면을 줄이기 위해서입니다.

공개 사이트는 서버에서 HTML을 그려 보내는 방식을 골랐습니다. 링크 미리보기 봇이나 일부 검색 엔진은 JavaScript를 실행하지 않아서, 화면을 브라우저에서 그리는 방식은 맞지 않았습니다.

서버와 보안에서 겪은 문제

서버 IP로 직접 접속할 수 있었다

Cloudflare 뒤에 두었지만, 서버가 공인 IP를 직접 쓰고 있어서 IP만 알면 Cloudflare를 건너뛰고 웹 서버에 바로 접속할 수 있었습니다. 포트를 바꾸는 방법은 스캔으로 금방 찾을 수 있어 효과가 작습니다.

그래서 방화벽에서 웹 포트를 Cloudflare IP 대역에만 열었습니다. 대역은 바뀔 수 있으므로 주 1회 자동으로 갱신하고, 목록을 받지 못하면 기존 규칙을 유지하게 해서 갱신 오류로 사이트가 끊기지 않게 했습니다. 규칙을 바꿀 때는 새 허용 규칙을 먼저 넣고 기존 규칙을 지워 중단을 피했습니다.

방화벽을 닫았더니 http 접속이 523 오류

80번 포트를 닫자, http://로 들어온 요청을 Cloudflare가 서버로 전달하지 못해 523 오류가 났습니다. Cloudflare의 "항상 HTTPS 사용"을 켜서 앞단에서 https로 넘기게 해 해결했습니다. 서버의 80번은 계속 닫아 둘 수 있습니다.

데이터베이스 포트가 외부에 열려 있었다

예전 설정이 남아 있어 DB가 모든 주소에서 연결을 받고, 방화벽도 열려 있었습니다. 로그인은 거부되더라도 외부에서 DB에 닿을 수 있는 상태였습니다. DB가 서버 내부 주소만 듣게 바꾸고 방화벽 규칙도 지웠습니다. 다른 공인 IP에서 접속을 시도해 막히는 것까지 확인했습니다.

비밀번호 없이 들어갈 수 있는 규칙

PostgreSQL 접속 규칙에 비밀번호 없이 복제 접속을 허용하는 trust 규칙이 남아 있었습니다. 서버 안의 어떤 프로세스든 DB 전체를 복사할 수 있는 설정이었습니다. 삭제된 옛 DB용 외부 허용 규칙과 함께 지우고, 필요한 계정만 서버 내부에서 scram-sha-256 비밀번호로 접속하게 정리했습니다.

비밀번호를 다루는 규칙

  • 비밀번호는 스크립트나 명령줄에 넣지 않고 대화형으로 입력합니다. 파일이나 셸 기록에 남지 않게 하기 위해서입니다.
  • 앱의 접속 정보는 소유자만 읽을 수 있는 환경 파일에 두고, 저장소에는 넣지 않습니다.
  • 앱은 별도의 시스템 계정으로 실행하고 파일 시스템 쓰기도 제한합니다.

DB 계정을 역할별로 나누기

소유자, 관리 사이트, 공개 사이트의 계정을 따로 만들었습니다. 공개 사이트 계정은 공개 글만 보여 주는 뷰만 읽을 수 있습니다. 공개 사이트가 뚫려도 초안을 읽거나 글을 고칠 수 없습니다. 구조를 바꾸는 권한은 소유자에게만 있고, 앱은 쓰지 않습니다.

관리 사이트는 두 겹으로

관리 사이트는 Cloudflare Access 뒤에 두어 허용된 이메일만 일회용 코드로 들어옵니다. 앱에서도 Cloudflare가 넘겨준 토큰의 서명과 대상, 이메일을 다시 확인합니다.

글 편집기

글을 Markdown 문자열 하나로 저장하다가, 블록 목록을 JSON으로 저장하는 방식으로 바꿨습니다. 편집기, 화면 출력, 검색이 같은 구조를 쓸 수 있기 때문입니다.

글, 코드, 수식, 이미지, 갤러리, 동영상, 파일, 인용, 콜아웃, 요약, 접기, 링크 카드, 다단, 여백까지 블록 14종을 만들었습니다. 자동 저장, 버전 기록, 휴지통, 예약 발행, 끌어서 옮기기도 넣었습니다. 반복되는 글 구성은 템플릿으로 저장해 꺼내 씁니다.

업로드가 왜 이렇게 느릴까

동영상을 올리면 진행 상황도 보이지 않고 매우 느렸습니다. 확인해 보니 한국에서 접속해도 Cloudflare의 미국 서부 거점을 거치고 있었고, Blazor 파일 입력은 작은 조각을 보낼 때마다 응답을 기다려서 왕복 시간에 그대로 묶였습니다.

브라우저가 파일을 8MB 조각으로 잘라 HTTP로 직접 보내게 바꿨습니다. 조각마다 진행률을 보여 주고, 끊기면 서버가 받은 위치부터 다시 보냅니다. 다 받은 뒤에만 제자리로 옮겨서 반쯤 쓴 파일이 남지 않습니다.

방문자 수는 IP 없이

방문자 수를 세려면 같은 사람을 구분해야 하지만, IP를 그대로 저장하고 싶지는 않았습니다. 날마다 바뀌는 비밀 값과 IP, 브라우저 정보를 합쳐 해시로 만들고, 그 기록도 이틀 뒤 지웁니다. 날짜별 합계만 남습니다. 기록은 DB 함수 하나로만 할 수 있게 권한을 묶었습니다.

워드프레스 글 옮기기

옛 서버의 데이터로 임시 데이터베이스를 띄워 글을 꺼냈습니다. 739편을 분석해 카테고리와 태그를 다시 나누고, 워드프레스 HTML을 블록으로 변환했습니다. 원래 공개였던 글만 발행하고, 비공개 글과 초안은 비공개로 두었습니다.

백준 풀이 글은 제목 형식이 글마다 달라서 스크립트로 같은 순서로 맞췄습니다. 문장과 코드가 빠지지 않았는지 글마다 변환 전후를 비교했습니다.

옮기고 나서 드러난 문제도 있었습니다.

  • 본문 이미지가 꺼진 옛 도메인을 가리켜 깨진 글이 있었습니다.
  • 제목의 굵게 표시가 ** 글자로 그대로 보이는 글이 있었습니다.
  • 글 제목에 <strong> 태그가 글자로 남은 글이 있었습니다.

디자인을 바꿨는데 홈만 그대로

관리 화면에서 디자인을 적용했는데 카테고리 페이지는 바뀌고 홈은 그대로였습니다. Cloudflare 캐시를 꺼도 같았습니다. 원인은 서버 안의 페이지 캐시였습니다. 디자인 값이 HTML에 함께 들어 있어서, 페이지마다 캐시가 만료되는 시점에 따라 일부만 바뀌어 보였습니다.

캐시 구분 값에 디자인을 넣어서, 디자인이 바뀌면 캐시가 남아 있어도 새로 그리게 고쳤습니다.

그 밖에 해결한 문제

문제 원인 해결
HTML의 한글이 숫자 문자 참조로 출력 Razor 기본 인코더가 한글을 인코딩 인코더 허용 범위를 전체로 변경
한글 제목의 앵커가 모두 section Markdig 기본 설정이 비ASCII 문자를 제거 GitHub 방식 앵커로 변경
검색어에 _가 있으면 결과 없음 ILIKE 이스케이프가 무효 이스케이프 문자를 명시
psql이 소켓을 찾지 못함 기본 포트를 바꿔 소켓 이름이 달라짐 접속할 때 포트를 지정
태그가 엉뚱한 글에 붙음 정규식 \b가 한글 조사를 경계로 보지 않음 영문자 경계를 직접 비교

마치며

작은 개인 블로그지만, 직접 만들어 보니 공개 범위와 권한을 어디서 나눌지부터 정해야 한다는 것을 배웠습니다. 앞으로 글을 쓰면서 불편한 부분을 계속 고쳐 나갈 생각입니다.