--- title: 통합 위키 포털 아키텍처 (Wiki Portal Architecture) date: 2026-06-29 type: ref domain: homelab tags: [portal, architecture, md-viz, homelab] --- **Date:** 2026-06-29 **호스트:** ubuntu (home server, 192.168.0.2) **도메인:** Infra / Networking / 운영 **티어:** PRIVATE (basic_auth) — 사내·홈랩 내부 사양 포함, 단 자격증명 값은 비포함 --- ## 1. 개요 통합 위키 포털은 여러 도메인의 markdown 노트를 **단일 Caddy 프런트도어(:8080)** 뒤에서 브라우저 열람용 HTML 아카이브로 묶어, 공개(open)·비공개(basic_auth)·파일허브(download)를 한 출처(same-origin)로 제공하는 시스템임. md 원본은 `md-viz` 파이프라인이 보존하고 같은 이름의 시각화 HTML을 생성하며, Cloudflare quick tunnel로 외부에 노출함. 목적은 ① 흩어진 작업 로그·도메인 노트를 한 곳에서 검색·열람, ② 공개해도 되는 자료(public)와 홈랩 내부 사양이 든 자료(private)를 **한 코드 경로로 분리 서빙**, ③ 사내 프록시 환경에서도 GET만으로 열람·다운로드가 되도록 설계하는 것임. --- ## 2. 전체 아키텍처 (ASCII 다이어그램) ``` +-----------+ +---------------------+ +------------------------------+ | Internet | ---> | Cloudflare | ---> | Caddy :8080 (kb-docs) | | browsers | | quick tunnel | | single front door, host net | +-----------+ | (kb-tunnel) | | root * /srv (ro bind) | | url changes/restart | +------------------------------+ +---------------------+ | | path routing +-----------------------------------+-----------------+-----------------+ | | | v v v +----------------------+ +----------------------+ +----------------------+ | handle / + /public | | handle /private* | | handle /files* | | portal + open docs | | basic_auth gate | | reverse_proxy | | no auth | | admin / bcrypt hash | | 127.0.0.1:8081 | | import archive | | import archive | | (dufs, ro, GET-only) | +----------------------+ +----------------------+ +----------------------+ | | | v v v +----------------------+ +----------------------+ +----------------------+ | /srv/public/* | | /srv/private/* | | kb-fileshare (dufs) | | _site.json open | | _site.json behind | | ~/fileshare -> /data | | sanitized archives | | auth (no meta leak) | | browse/zip/search | +----------------------+ +----------------------+ +----------------------+ LAN-only side door (NOT forwarded by the tunnel; tunnel only points at :8080): +----------------------+ +------------------------------+ | Home LAN / mac | ---> | kb-filemgr (filebrowser) | | 192.168.0.0/24 | | 192.168.0.2:8082 manage RW | +----------------------+ | upload / mkdir / move / del | +------------------------------+ ``` - 단일 프런트도어 원칙임. 외부로 나가는 길은 quick tunnel → Caddy `:8080` **하나**뿐이고, 그 안에서 `handle` 블록이 경로로 갈라짐. 따라서 외부에서 보이는 것은 Caddy가 허용한 경로뿐임. - `kb-filemgr`(:8082)는 LAN IP에만 바인딩되어 터널 경로 밖에 있음 → 외부에서 도달 불가, 집/사내 LAN에서만 관리용으로 접근함. --- ## 3. 3-tier 구조 + URL 맵 | 티어 | URL 패턴 | 접근성 | 내용 | |---|---|---|---| | Portal / Public | `/`, `/public//` | 공개 (no auth) | 살균 게이트 통과한 공개 노트. 포털 랜딩 + 도메인 카탈로그 | | Private | `/private//` | basic_auth (admin) | 작업 로그·홈랩 내부 사양. 메타데이터(`_site.json`)까지 auth 뒤 | | Files | `/files/` | 공개 GET (dufs, ro) | 첨부·바이너리 다운로드 허브 (사내 다운로드용) | **핵심 메커니즘 — basic_auth는 사내 프록시도 통과함.** private 티어를 막는 것은 HTTP Basic 인증임. 브라우저는 `GET /private/...` 요청에 `Authorization: Basic base64(admin:pass)` 헤더를 실어 보내고, Caddy가 헤더의 bcrypt 해시를 검증함. 이때 사용되는 메서드는 **표준 GET + 표준 헤더 하나**뿐임. 사내 프록시가 흔히 차단하는 것은 PUT/POST/PROPFIND 같은 쓰기 메서드나 비표준 인증 핸드셰이크인데, basic_auth는 그 어느 것에도 해당하지 않음. 즉 private이라도 사내망에서 **열람은 가능**함(자격증명만 있으면). 이 점이 "private = 사내 비공개"가 아니라 "private = 인증 게이트 + 검색 메타 차단"임을 의미함. 진짜로 사내에서 가리고 싶은 자료는 티어가 아니라 별도 격리가 필요함. --- ## 4. 컨테이너 & 포트 | 컨테이너 | 이미지 | 바인드/포트 | network_mode | 역할 | |---|---|---|---|---| | `kb-docs` | `caddy:latest` | `:8080` (host net) | host | 프런트도어. `~/knowledge-base -> /srv:ro`, Caddyfile ro 마운트. 라우팅·basic_auth·캐시헤더 | | `kb-tunnel` | `cloudflare/cloudflared:latest` | outbound 7844/443 | host | `tunnel --url http://localhost:8080` quick tunnel. 외부 노출. 재시작마다 URL 변경 | | `kb-fileshare` | `sigoden/dufs:latest` | `127.0.0.1:8081` | host | dufs read-only·no-auth. `~/fileshare -> /data`. browse/zip/search, GET만. Caddy `/files/`가 프록시 | | `kb-filemgr` | `filebrowser/filebrowser:latest` | `192.168.0.2:8082` | host | filebrowser 관리 UI. `~/fileshare -> /srv`. 업로드·폴더생성·이동·삭제. LAN 전용 | - 네 컨테이너 모두 `network_mode: host` + `restart: unless-stopped`임. host net을 쓰는 이유는 dufs를 `127.0.0.1:8081` 루프백에만 묶어 **터널에서 직접 못 닿게** 하기 위함임(Caddy 경유만 허용). - `kb-fileshare`와 `kb-filemgr`는 `user: "1000:1000"`(jinsoo)으로 돌아 호스트 파일 소유권과 일치시킴. --- ## 5. 파일허브 — 다운로드/관리 분리 (+왜) 파일허브는 **다운로드 경로(dufs)** 와 **관리 경로(filebrowser)** 를 의도적으로 두 컨테이너로 분리함. - 다운로드(dufs, `kb-fileshare`) — read-only·no-auth로 `/files/`에 공개. 모든 동작(목록·zip·검색·다운로드)이 평범한 **GET**임. 로그인 버튼도 업로드 버튼도 없어 혼동 여지 자체가 없음. 사내에서 첨부 받을 때 쓰는 경로임. - 관리(filebrowser, `kb-filemgr`) — 업로드·폴더 생성·이동·삭제·압축이 되는 진짜 파일 매니저. LAN IP `192.168.0.2:8082`에만 바인딩 → 터널로 안 나감. 집/사내 LAN의 mac에서만 접근해 쓰기 작업을 함. 쓴 파일은 `~/fileshare`에 떨어지고, read-only dufs가 그것을 다운로드용으로 서빙함. **분리 이유(메커니즘):** 사내 프록시는 보통 업로드(PUT/POST)와 비표준 인증을 차단함. 다운로드 경로를 GET-only·no-auth로 만들면 그 차단을 **구조적으로 우회**할 게 아니라 애초에 걸릴 게 없음. 반대로 쓰기 기능은 외부에 한 톨도 노출하지 않고 LAN에 가둠으로써, "외부=읽기 전용, 집=읽기+쓰기"라는 경계가 포트 바인딩 한 줄로 강제됨. 한 컨테이너에 인증 분기로 섞었다면 프록시 환경에서 다운로드가 같이 깨졌을 것임. --- ## 6. md-viz 파이프라인 마스터 자산은 전부 `~/.claude/skills/md-viz/`에 있음 — `scripts/`(md2viz·gen_index·gen_tree·gen_nav·gen_portal), `assets/viz.css`·`assets/viz.js`, `publish.sh`, `denylist.txt`. 산출물은 `~/knowledge-base///`에 떨어짐. ``` src.md --add--> markdown/.md --render(md2viz)--> .html | reindex(gen_index+gen_tree) --> index.html + files.html + catalog.json | sync-assets --> push viz.css/viz.js to KB + every archive | portal(gen_nav+gen_portal) --> _site.json x2 + /index.html ``` | 단계 | 명령 | 효과 | |---|---|---| | add | `publish.sh add / ` | 살균 게이트 → md 복사 → 1개 렌더 → reindex | | render | `publish.sh render ` | 아카이브 markdown 전체 재렌더(SVG 강화본 보존) | | reindex | `publish.sh reindex ` | index.html·files.html·catalog.json 재생성 | | sync-assets | `publish.sh sync-assets` | 마스터 viz.css/viz.js를 KB와 모든 아카이브 assets로 전파 | | portal | `publish.sh portal` | 두 티어 `_site.json` + 포털 `/index.html` 갱신 | - **단일 편집점.** CSS/JS는 마스터 한 곳(`skills/md-viz/assets/`)에서 고치고 `sync-assets`로 전 아카이브에 복사함 → 전역 restyle 1회로 전체 반영(재렌더 불필요). 인라인 금지·CDN 의존 0(폐쇄망 대비, mermaid 미사용). - 슬러그 문법은 `__-`임. `topic`은 `__` 앞, `type`은 `__` 뒤 첫 `-`까지(note·src·runbook·guide·report·ref·MOC). 이 문법으로 카탈로그 토픽 그룹핑이 결정됨. - 다이어그램은 손수 인라인 SVG임. `render`가 남긴 `needs-svg` placeholder를 사람이/에이전트가 SVG로 교체하면 이후 `rebuild`가 그 HTML을 **보존**함(`` 감지). --- ## 7. 보안 모델 3겹 경계로 동작함. - **public 살균 게이트(denylist).** `add`/`render`/`reindex`가 public 아카이브를 대상으로 하면 `denylist.txt` 정규식(예: `kakaopay`, `192\.168\.`, `10\.0\.0\.`, `1\.238\.`, `enp7s0`, `\bbr0\b`, `hostname ubuntu`)을 servable·코드 파일에 grep하여 **1건이라도 맞으면 exit 1로 차단**함. private 대상이면 같은 grep을 돌리되 **warn-only**로 통과시킴(private은 그 값들을 정당하게 포함할 수 있으므로). 문서 내 예시 IP는 RFC5737 `203.0.113.x` 같은 문서용 대역으로 redaction함. - **private basic_auth.** `/private/*`는 Caddy `basic_auth`(admin + bcrypt 해시) 뒤에 있음. - **메타유출 경계.** `/private/_site.json`(검색·nav용 도메인·문서 제목 메타)까지 `handle /private*` 안에 있어 **auth 뒤에서만** 서빙됨. 따라서 미인증 사용자는 private 도메인이 몇 개인지, 문서 제목이 무엇인지조차 못 봄. viz.js는 미인증 시 `/private/_site.json`에서 401을 받아 제목 없는 `🔒 Private — log in` 링크 하나만 그림. > [!key] 교훈 — denylist는 floor지 ceiling이 아님 > 살균 게이트는 **알려진 토큰의 자동 차단(floor)** 일 뿐, 누출이 없음을 보장하는 천장(ceiling)이 아님. denylist는 IP·hostname·`kakaopay` 같은 **문자열**만 잡음. 반면 VM 이름(`linux-lab` 류), 내부 프로젝트 코드네임, 토폴로지 설명 같은 **의미적(semantic) 누출**은 정규식에 안 걸림. 그래서 public 승격 시에는 게이트 통과 여부와 별개로 사람이 한 번 더 읽어야 함. 판단이 애매하거나 의미적 누출이 섞인 문서는 **public으로 올리지 말고 private에 유지**하는 것이 기본값임. --- ## 8. 캐싱 - origin(Caddy)이 모든 아카이브 응답에 `Cache-Control: no-cache`를 붙임. 이는 "캐시 저장은 해도 **매번 ETag로 재검증**하라"는 뜻임 → 변경 없으면 304, 바뀌면 새 본문. 과거 Caddy가 Cache-Control을 안 보내던 시절 브라우저와 Cloudflare quick-tunnel 엣지가 stale한 `viz.css`/`viz.js`/`index.html`을 물고 있어 "내 수정이 반영 안 됨" 증상이 났던 것을 이 헤더로 해결함. - **운영 함정.** Caddyfile의 헤더·`basic_auth` 변경은 `caddy reload`로 **반영되지 않는 경우**가 있음. 이때는 `docker restart kb-docs`로 컨테이너를 재가동해야 새 설정이 먹음. 따라서 인증/캐시 관련 변경 후에는 reload가 아니라 restart로 검증할 것. --- ## 9. 헤더 / UX - **포털 헤더** — `📁 Files`(→`/files/`), `🔒 Private`(→`/private/`, 미인증 시 로그인 링크), `🌓 테마`(라이트/다크 토글, localStorage `vizTheme2`) 제공. - **문서 헤더 런타임 주입** — 각 문서 페이지는 `fuseDocHeader()`로 헤더 chrome(`☰` 사이드바 토글, 문서 제목, `📄 MD 원본`, `🌓 테마`)을 런타임에 주입함. 손수 그린 인라인 SVG(``)는 건드리지 않고 보존함. - **좌측 글로벌 사이드바** — viz.js가 body를 `#page`로 감싸고 `#sitenav`를 앞에 붙여 2열 그리드를 만듦. `/public/_site.json`·`/private/_site.json`을 절대경로(same-origin)로 fetch해 도메인·토픽·문서 트리를 그리고, 검색 박스로 병합 문서 목록을 부분일치 검색함. 접기 상태는 localStorage `vizNav`에 영속함. --- ## 10. 경로 맵 | 경로 | 역할 | 비고 | |---|---|---| | `~/knowledge-base` | 웹 루트 = 컨테이너 `/srv:ro`. 생성물(HTML·_site.json·catalog) | Caddy가 read-only로 서빙 | | `~/docs-publish` | `docker-compose.yml`·`Caddyfile`·`.env` (운영 ops 파일) | 스택 정의·자격증명 | | `~/.claude/skills/md-viz` | 파이프라인 마스터(scripts·assets·publish.sh·denylist) | 단일 편집점 | | `~/fileshare` | 파일허브 데이터 | dufs(ro 다운로드) + filebrowser(rw 관리) 공유 마운트 | | `~/Documents/claude-logs` | 작업 로그 markdown | SMB로 mac에서 접근 | --- ## 11. 현재 콘텐츠 인벤토리 - **public** (7 도메인, 살균 게이트 적용): - `istio` (57 md) · `etcd` (6) · `k8s` (4) · `devtools` (3) · `networking` (2) · `kernel` (1) · `virsh` (1) - **private** (3 도메인, basic_auth): - `homelab` (21 md — 작업 로그·인프라 레퍼런스; 이 아키텍처 문서 + 도메인 활성화 런북 포함) · `k8s` (4) · `virsh` (2) > 카운트는 각 아카이브 `markdown/*.md` 기준임. public/private에 같은 도메인명(`k8s`·`virsh`)이 있으나 서로 다른 아카이브임(공개분 vs 내부분). --- ## 12. 자격증명 위치 - 모든 자격증명은 `~/docs-publish/.env`에 있음(**값은 본 문서에 비포함**). 변수명만: - `PRIVATE_USER` / `PRIVATE_PASS` — private 티어 basic_auth 계정. - `FILEMGR_USER` / `FILEMGR_PASS` — filebrowser(LAN 관리 UI) 로그인. filebrowser는 강도 검증이 있어 별도 값 사용. - `FILESHARE_PASS` — dufs가 현재 no-auth라 미사용(참고용 보존). - private의 실제 인증은 **bcrypt 해시**로 `Caddyfile`의 `basic_auth` 블록에 동기되어 있음(`.env`의 평문은 운영자 참조용, Caddy가 검증에 쓰는 것은 해시). 평문을 바꾸면 해시도 재생성해 Caddyfile에 반영해야 함. - filebrowser 계정은 LAN 전용 UI에만 쓰여 외부로 노출되지 않음. --- ## 13. 향후 (Phase 1) 현재는 quick tunnel이라 `kb-tunnel` 재시작마다 `*.trycloudflare.com` URL이 바뀜(북마크·고정 공유 불가). Phase 1 계획: - 본인 소유 **도메인** + Cloudflare **named tunnel**(토큰 기반)로 교체 → 리붓해도 주소 불변. - private 티어 앞단에 Cloudflare **Access**(SSO/이메일 정책)를 선택적으로 추가. > 상세 절차(도메인 구매·존 등록·named tunnel 생성·DNS CNAME·Caddy host-split·Access 정책)는 같은 아카이브의 **`infra__guide-domain-activation`** (도메인 활성화 런북)을 참조할 것. --- ## 14. 운영 절차 요약 ```bash # 1) 스택 재배포 (compose 변경 후) cd ~/docs-publish && docker compose up -d # 2) 인증/캐시 헤더 변경 시 — reload로 안 먹으면 restart docker restart kb-docs # 3) 문서 추가 (private/homelab 예) bash ~/.claude/skills/md-viz/publish.sh add private/homelab __- bash ~/.claude/skills/md-viz/publish.sh reindex private/homelab bash ~/.claude/skills/md-viz/publish.sh portal # _site.json x2 + /index.html 갱신 # 4) 검증 — 미인증 401, 인증 200 curl -sI http://localhost:8080/private/homelab/.html # -> 401 curl -sI -u "$PRIVATE_USER:$PRIVATE_PASS" http://localhost:8080/private/homelab/.html # -> 200 curl -sI http://localhost:8080/public/k8s/ # -> 200 (공개 무영향) ``` - 새 도메인 아카이브는 `publish.sh new [title]`로 스캐폴드 후 `add`함. - public 대상 작업은 살균 게이트가 누출 시 exit 1로 막으므로, 차단되면 본문을 스크럽하거나 private으로 옮길 것. --- ## 참조 - 운영 파일: `~/docs-publish/{docker-compose.yml,Caddyfile,.env}` - 파이프라인 마스터: `~/.claude/skills/md-viz/{publish.sh,denylist.txt,PORTAL-CONTRACT.md,SKILL.md}` - 도메인 승격 절차: 같은 아카이브 `infra__guide-domain-activation` (Cloudflare 도메인 활성화 런북)