Compare commits

84 Commits

Author SHA1 Message Date
kaffa
b1ba9f1d90 k3s-to-incus-migration: APISIX 서울 유지·Tofu+Ansible·kr1 GPU 확정 2026-06-01 13:25:42 +09:00
kaffa
a44ff12f15 infra/compute: K3s → Incus 이전 플랜 초안 2026-06-01 13:22:11 +09:00
kaffa
fbb3506643 history: Longhorn sftpgo corruption loop 해소 + snapshot 자동 retention 영구 수정
- sftpgo replica kr1 13분 주기 corruption loop → kr2 anti-affinity reschedule로 해소
- root cause: snapshot .checksum 누락 + chain meta 손상 (디스크 정상)
- 시스템 차원 발견: 906/906 snapshot이 recurring-job 라벨 누락
- snapshot 일괄 정리 906 → 194 (volume당 7~8개)
- RecurringJob 4개 spec.labels patch — 자동 retention 영구 수정
- 후속: ArgoCD/IaC 매니페스트 반영 필요
2026-06-01 11:38:19 +09:00
kaffa
156e1b2dce history: termix image.tag 명시 고정은 정책 위배 — :latest 복귀 위임 기록
- 사용자 정책: 컨테이너 이미지 tag는 :latest 유지가 디폴트
- 헤임달의 image.tag 고정 변경 및 본 history의 '교훈' 가이드 모두 정정
- 헤임달에 :latest 복귀 별도 위임
2026-05-29 19:56:38 +09:00
kaffa
26cd0ba2ba history: termix :latest → release-2.3.1 업그레이드 + image.tag 명시 고정
- chart 0.1.2 → 0.1.4, commit 5d3b027
- ImagePullBackOff 1차 실패 (release tag 이름 vs registry tag 이름 차이) 기록
- ArgoCD Synced/Healthy, pod Running, https://termix.inouter.com/ HTTP 200
2026-05-29 19:52:04 +09:00
kaffa
1a944bdd6b nas: r8152 v2.20.1-1 → v2.21.4-1 (LPM 재발 대응) + spk_su 설치 절차 기록
- 5/24 LPM disconnect 재발 → 새 카드 4일 만에 빈도 증가
- bb-qq r8152 패키지 업그레이드. 첫 install이 의도적으로 fail (spk_su 부재) → spk_su manual install 후 SPK 재install로 정상화
- 검증: status=running, driver v2.21.4 (2025-10-28), end-to-end JF (ping -M do -s 8972) 통과
- 효과(LPM 발화 빈도 감소)는 며칠 관찰 필요
- nas-storage.md에 5/24 재발 + 5/26 업그레이드 시점 추가
- history/2026-05-26 신규 — 설치 절차 함정(spk_su) 정리
2026-05-26 17:12:02 +09:00
kaffa
03ef787444 cf-bouncer: fall-mvp.com zone 제거 — netbis CF bouncer 21일 만에 동기화 재개
fall-mvp.com zone이 CF에서 삭제되어 bouncer가 2026-05-02부터 fatal 루프로 정지. config에서 2줄 제거 후 enable+start. LAPI pull 2026-05-23T04:56:28Z로 갱신 확인.
- infra/security/crowdsec-safeline.md: 적용 zone 6 → 5
- history/2026-05-23-cf-bouncer-fall-mvp-removal.md: 인시던트 기록
2026-05-23 14:00:25 +09:00
kaffa
ae8d8b7dfa history: kr1 k3s 12시간 stuck — etcd/cf-bouncer/longhorn 연쇄 인시던트 기록
2026-05-22 20:02 JST kr1 k3s.service가 activating 상태로 12시간 매달려 노드 NotReady → instance-manager 9d Terminating → 4 볼륨 degraded + apisix-etcd-0 raft 손상까지 연쇄. systemctl restart 한 번으로 4분 안에 회복. 재발 방지 룰 정리.
2026-05-23 10:41:58 +09:00
kaffa
5be51134c9 incus-hp1 storage-net 참여 사실 반영 + kr2 multus-shim ETXTBSY 인시던트 기록
- infra-hosts.md: hp1 행 갱신 (이전 "1GbE only" → 2.5G 192.168.205.227, MAC 20:e1:5d:6a:2b:2e, MTU 9000 JF OK), 2.5G 표 hp1 행 추가, NAS NIC 정보(USB cdc_ncm → r8152 RTL8157 chip rev 14 + MAC)로 정확화, 호스트 자원 표에 hp1 추가, SSH 정보 라인에 hp1 접근 한계 명시
- nas-storage.md: K3s 노드 표 3→4 노드로 갱신 (hp1 추가), nodeAffinity 설명 표현 갱신
- history/2026-05-20-kr2-multus-shim-etxtbsy.md: ETXTBSY 데드락 RCA 및 `rm /opt/cni/bin/multus-shim` 회피책 기록
2026-05-20 17:10:07 +09:00
kaffa
ba30121962 NAS eth2 USB 2.5GbE 카드 교체 (rev 17 → rev 14, USB 4-1 → 2-2)
- ethtool: 2500 Mb/s Full Duplex 정상 협상
- iperf v2 (MTU 9000): TX 2.26 Gbit/s, RX 2.45 Gbit/s (이론 91%/98%)
- DSM ifcfg가 슬롯 기반으로 eth2 자리 자동 매핑 → 이름/IP/MTU 승계
- watchdog 그대로 유효, 새 카드도 RTL8157 계열이므로 LPM 재발 모니터링 대상
2026-05-20 15:41:31 +09:00
kaffa
0223c832ab history: complete safeline chaos/detector-logs preemptive PVC replacement (2026-05-18) 2026-05-18 19:35:38 +09:00
kaffa
aec8891bac history: add 2026-05-18 detector PVC follow-up + remaining timebombs (chaos / detector-logs) 2026-05-18 17:54:15 +09:00
kappa
2763e33675 history: SafeLine RWX PVC ext4 호환성 인시던트 복구 기록 (2026-05-17) 2026-05-17 16:33:53 +09:00
kaffa
bc01809643 incus-kr2: K3s/reboot 동반 작업 전 iommu=pt 사전 체크 항목 추가 2026-05-14 16:57:09 +09:00
kaffa
f0de7a3c37 infra-hosts: frontmatter updated 누적 보존 (iommu=pt 기록 복원) 2026-05-14 12:28:12 +09:00
kaffa
821c5a6278 infra-hosts: K3s 4노드 v1.34.5 → v1.34.7+k3s1 patch 업그레이드 (2026-05-14) 2026-05-14 12:27:10 +09:00
heimdall
81e270a90f longhorn: snapshot-purge 임시 cron 회수 (1.11.2 fix 적용 후 의미 소멸) 2026-05-09 20:37:13 +09:00
heimdall
02a9b9dfd8 longhorn: 1.11.1 → 1.11.2 (snapshot warning #12856 fix) 2026-05-07 08:10:59 +09:00
kaffa
0a45e0536c AMD-Vi (IOMMU) Completion-Wait timeout 메커니즘 정본 신설
incus-kr2 freeze 사건 분석으로 확정된 IOMMU 부분 hang 메커니즘과
운영 규칙(`iommu=pt` 선제 적용)을 별도 reference 문서로 분리.
호스트 사연(history)과 메커니즘(reference)을 분리해 다른 AMD Ryzen
호스트 도입 시 재사용 가능한 정본으로 정리.

- infra/compute/amd-vi-iommu.md 신규 (메커니즘 + 차단 + 운영 규칙)
- compute _index.md, hosts/incus-kr2.md, history 문서에 링크
2026-05-05 11:47:31 +09:00
kaffa
35f1e16f09 incus-kr2 freeze 원인 = AMD-Vi (IOMMU) Completion-Wait timeout
호스트 약 2주 간격 freeze 재발 패턴 분석 결과 AMD Ryzen 6900HX의
IOMMU Completion-Wait queue stall이 근본 원인. GRUB cmdline에
`iommu=pt` 추가하여 IOMMU passthrough 모드로 차단.

- infra/compute/hosts/incus-kr2.md 신규 (호스트 정본)
- history/2026-05-04-amd-iommu-freeze.md 신규 (사건 기록)
- _index.md / infra-hosts.md 갱신
2026-05-05 07:18:28 +09:00
kaffa
e068f18955 dev: Karpathy LLM 코딩 4원칙 참고 문서 추가 2026-05-04 09:22:49 +09:00
heimdall
46419eb8a1 longhorn: snapshot-purge cron 워크어라운드 (v1.11.1 stuck snapshot) 2026-05-02 15:22:51 +09:00
kaffa
1b1c968826 infra/network/beryl-ax: zram swap (zstd) 섹션 추가
휴대용 라우터의 빠듯한 RAM(491MB, swap 없음) 대비 CPU가 95% idle인
환경적 특성에 맞춰 zram 압축 swap을 도입. lzo 기본을 zstd로
전환해 압축률 ~1.5배 향상. 설치/검증/UCI 영구화 절차 정리.
2026-05-02 08:06:22 +09:00
kaffa
ebac715db5 infra/network: Beryl AX (GL-MT3000) 휴대용 라우터 문서 추가
GL.iNet 4.8.1 기본 설정에서 fw3 tailscale0 zone에 masq 옵션이
누락되어 LAN→tailnet forward 시 SNAT가 안 되는 문제와, dnsmasq
ts.net split-forward 미설정으로 MagicDNS 동작 안 하는 문제를
정리. 영구 UCI 설정과 Mac 측 /etc/resolver/ts.net + search domain
보정까지 포함.
2026-05-01 19:37:20 +09:00
heimdall
3afb3c884d outline: history 갱신 — pgpool DML 라우팅도 동일 버그, haproxy-pg override 영구 유지 2026-04-29 15:44:44 +09:00
heimdall
23a91899e5 outline: 0.82.0 -> 1.7.0 메이저 업그레이드 (OIDC SameSite 쿠키 수정) 2026-04-29 15:30:25 +09:00
kaffa
b9300e6798 domain-boundaries: ironclad 자체 cycle 추가
ironclad 트래픽 흐름을 명시:
- CF + BunnyCDN 병행 entry (zone proxied 패턴별 분기)
- OpenWrt 라우터 (192.168.9.1) → MetalLB → K3s APISIX + SafeLine
- 1차 limit-req + 2차 chaitin-waf in-line gRPC
- 로그: VL → CrowdSec acquisition → 자작 4종 + hub 시나리오
- enforce layer: CF/BunnyCDN bouncer 모두 폐기 후 재구축 미정
  · CF bouncer: netbis-cf-firewall과 동일 패턴으로 kappa account용 추가 운영 후보
  · BunnyCDN bouncer: BlockedIps API 또는 Shield Access List(Advanced) — Ticket 388529 답변 후 결정

자산 매핑 표를 netbis/ironclad/bridge로 분리해 명시
추적 시 주의 항목 보강
2026-04-27 07:44:24 +09:00
kaffa
e68977a9d3 infra: 도메인 경계 정본 신규 (netbis vs ironclad cross-domain)
- infra/domain-boundaries.md 신규 — 두 영역 자산/계정/책임 분리 + cyclic 데이터 흐름 + 자산 매핑 + 추적 시 주의사항
- infra/_index.md에 도메인 경계 절 추가
- 기존 정본들(services/netbis, infra/security/crowdsec-safeline, services/bunnycdn-security 등)이 흩어져 갖던 cross-domain 정보의 단일 진입점

핵심 사실:
- netbis CF account != kappa CF account (별개)
- NPM 6대 origin은 netbis 자산, vl/CrowdSec/netbis-cf-firewall은 ironclad 인프라
- zlambda는 양 영역 다리 (Tailscale + Linode public IP 보유)
- netbis-cf-firewall은 ironclad 컨테이너 동거하지만 netbis CF API token으로 push
2026-04-27 07:38:47 +09:00
kaffa
d25dc3e52f obsidian 정합성 정정 — bouncer 단일화 잔존 stale 정리
- infra/compute/infra-hosts.md: jp1 default 20→19, cs-cf-worker-bouncer 컨테이너 라인에서 제거
- services/bunnycdn-security.md: Edge Script 64811 / bloom filter / 국가차단 / Turnstile inouter-bunny 폐기 반영. 현재 layer (Bunny Shield + Rate Limit + 대역폭 한도) 중심 재작성
- infra/network/apisix.md: Edge Script 64811 attach 라인 폐기 표시
- infra/security/cloudflare.md: Workers 인벤토리 + Worker 라우트 + CF proxy 패턴 + cfb-manager 절 모두 폐기 반영
- infra/security/crowdsec-safeline.md: cs-cf-worker-bouncer 운영 중 문장 폐기 표시
- ops-agents/overview.md: Syn 영역 정의에서 폐기 자산 명시
- history/_index.md: 누락된 2026-04-25-netbis-npm-vector-msg-rewrite, 2026-04-26-bouncer-consolidation 등록 + frontmatter updated
2026-04-26 10:33:48 +09:00
kaffa
560dde3f88 cloudflare: Turnstile 위젯 6개 + Edge Script 64811 정리 추기
- Turnstile 4개 (cs-cf-worker-bouncer-widget): 삭제
- Turnstile inouter (legacy orphan): 삭제
- Turnstile inouter-bunny-middleware: 삭제 (BunnyCDN 미들웨어 64811과 함께)
- Edge Script 64811: DELETE /compute/script/64811 HTTP 204
  - 4개 풀존 (iron-jp/iron-kr/iron-kr-waf/iron-git) MiddlewareScriptId null 자동 정리 확인
- 보류: Turnstile crowdsec-captcha (8 도메인 사용처 추적 미완)
- bouncer 단일화 history에 Syn 회신 + Turnstile/Edge Script 추가 정리 결과 추기
2026-04-26 10:27:02 +09:00
heimdall
53172c06e8 bouncer 단일화: netbis-cf-firewall만 유지, 나머지 3종(cs-cf-worker/apisix-waf/bunny-cdn) 폐기 2026-04-26 09:55:55 +09:00
heimdall
f733dd574a netbis: NPM Vector _msg 재합성 (proxy_v2 → nginx combined) — child-nginx-logs unparsed 84% 해결 2026-04-25 15:17:01 +09:00
kaffa
46cb3236d3 deprecate anomaly-detect (오탐 다수로 인스턴스까지 제거)
원인: Grok-4-fast agentic 분석기가 더미 IP(1.1.1.1, 1.2.3.4 시퀀스),
Cloudflare 엣지 IP(172.70.x), 자체 Linode IDC 대역(45.79.x)을
path-enumeration으로 오탐 ban. 같은 기간 hub 시나리오는 진짜 스캐너
1건(India SoloRDP)을 정확히 잡음.

작업:
- infra/platform/anomaly-detect.md → deprecated stub
- history/2026-04-25-anomaly-detect-removal.md 신규 (폐기 사유, 재가동 조건 정리)
- crowdsec-safeline.md acquisition 다이어그램에서 anomaly-detect 분기 제거
- infra/compute/infra-hosts.md hp2 default 5→4 갱신
- infra/platform/_index.md, history/_index.md 인덱스 갱신
- infra/security/vault.md apps 목록에서 항목 제거 (apps/anomaly-detect 경로는 비어있음, 유지)

보존:
- Vault secret/ai/openrouter (다른 서비스 공용 가능성)
- Gitea kaffa/anomaly-detect repo (재구축 reference)
2026-04-25 15:00:13 +09:00
kaffa
ffdabc994b crowdsec: profile current state, distributed DDoS scenario notes, bouncer action mapping
- profile.yaml is ban-only — captcha/throttle decision은 emit 안 됨
- Hub 분산 DDoS 시나리오 (http-ddos-by-ASN/cn) 도입 시 profile 분기 필수
- cs-cf-worker-bouncer: default_action captcha (Turnstile)
- netbis-cf-firewall: default_action managed_challenge (6 zone)
- 두 bouncer 모두 LAPI decision type 무관하게 캡챠 페이지로 응답
2026-04-25 12:59:49 +09:00
kaffa
e5c6b4deab netbis CF firewall bouncer 재구축 (origin filter) + VL acquisition 통합 + sigmatch v2.4
- crowdsec-safeline.md: VL → CrowdSec acquisition 3개(apisix/traefik/npm) → 1개 통합
  (victorialogs-nginx.yaml, query OR 결합). Netbis NPM CrowdSec 연동 활성.
  netbis-cf-firewall 재구축 섹션 추가.
- services/netbis.md: bouncer 폐기 → 재구축 정정. firewall_bouncer_token 정보 갱신.
  Worker bouncer는 트래픽 비례 비용으로 재구축 안 함 명시.
- history/2026-04-25-netbis-cf-firewall-rebuild.md: 오늘 작업 종합 (sigmatch v2.4
  MP 제거, VL 통합, CrowdSec 연동, CF Firewall bouncer 재구축, origin filter로
  10k 한도 회피).

origin filter [crowdsec, cscli] 적용으로 2026-04-23 폐기 사유였던 CF IP List
10k 한도 회피. Worker bouncer는 origin filter로 회피 불가 (트래픽 비례 비용)
이라 재구축 안 함.
2026-04-25 12:43:47 +09:00
kaffa
ae6237de21 netbis-sigmatch v2.4: A4 Matrix Profile 트리거 제거
18h DRY 가동 결과 99 distinct IP 차단 누적. attack_contributor 2,668건 중
97.5%가 MP only 시그널 발동. MP threshold p99 (1% 오탐) × 매 5분 사이클 평가
= 누적 폭주. attack_contributor의 단순 volume top이 webhook + heavy normal
user 항상 잡음.

LIVE였으면 베팅 콜백 4~5개 차단 + KR 활발 유저 + IPv6 모바일 240/4 가짜 IP
99개 차단 = 서비스 사고.

matrix_profile.py 모듈은 보존 (관찰/연구용). loop.py에서 호출만 제거.

폐기 사유: MP의 self-similar 시계열 가정이 daily seasonality 있는 트래픽과
안 맞음 + 단일 시그널이 contributor 단계로 무차별 차단 증폭
2026-04-25 10:17:24 +09:00
kaffa
e0787b38f3 netbis-sigmatch v2.3 Phase 11.5: CF rule 한도 초과 방지 (max_rules=2000 기본) 2026-04-24 15:04:11 +09:00
kaffa
d32121bfda netbis-sigmatch v2.3: CF Access Rules LIVE 연동 (Phase 11) 2026-04-24 14:58:28 +09:00
kaffa
beecbb3be4 netbis-sigmatch v2.2: Matrix Profile discord (stumpy) trigger 추가 2026-04-24 13:17:03 +09:00
kaffa
baa81a1955 netbis-sigmatch v2.1: CUSUM Page-Hinkley + global src IP entropy drop trigger 추가 2026-04-24 12:45:03 +09:00
kappa
2e9244377e sigmatch v2 재설계: 집계 기반 공격 모드 + 개별 극단 시그니처 + 롤링 baseline 자동 갱신 2026-04-24 12:21:46 +09:00
kaffa
027e3c2a45 sigmatch: Phase 5 완료 — state DB + 2트랙 탐지 (클러스터+단발) + 오탐방지 설계 명시 2026-04-24 10:13:05 +09:00
kaffa
61275e6e81 netbis: sigmatch 프로젝트 정본 추가 (VL 기반 자동 시그니처 + CF 차단, 개발 단계) 2026-04-24 08:55:26 +09:00
kaffa
102da9c2fe cloudflare: Pseudo IPv4 (Class E 240/4) 정리 — Netbis 관찰 기반
- infra/security/cloudflare.md: Pseudo IPv4 섹션 신규 (동작 원리·모드·함의·오해 주의)
- services/netbis.md: client_ip 의미에서 부정확한 '254.x 범위' → '240.0.0.0/4' 정정, CF docs 링크
- history: 2026-04-24 CF Pseudo IPv4 정체 규명 (CGNAT 오진 교훈 포함)
2026-04-24 08:11:44 +09:00
heimdall
2a8cf22e43 netbis: NPM client_ip 실 IP 추출 정비 (nginx real_ip_header + Vector VRL) 2026-04-23 16:06:37 +09:00
heimdall
461ee81839 netbis: NPM 6대 Vector→zlambda→VL 로그 수집 파이프라인 구축 2026-04-23 15:23:37 +09:00
heimdall
bf33c043f9 netbis: CF 바운서 전량 제거 (netbis-cf + netbis-cf-firewall) 2026-04-23 13:52:59 +09:00
heimdall
29c17065b2 netbis: crowdsec-cloudflare-bouncer (firewall rule) 추가 — worker bouncer와 병행 2026-04-23 13:27:23 +09:00
heimdall
738a60b093 longhorn: 1.8.2 -> 1.11.1 업그레이드 (2026-04-23) 2026-04-23 09:06:21 +09:00
kaffa
3dfceb81b7 ironclad production cutover (2026-04-21): apex → Worker 전환
products/ironclad-website.md: 배포 표에 라우팅 방식 컬럼 추가.
production만 zone route 방식(custom_domain 대신 zone_name + /*)을 써서
기존 APISIX A record를 건드리지 않고 Cloudflare 엣지에서 Worker가
매칭 요청을 가로채는 구조. 전제 조건(crowdsec bouncer wildcard가
apex를 가로채지 않도록 *.ironclad.it.com/* 수정) 명시.

history/2026-04-21: 3차에 걸친 cutover 경로(DNS 충돌 → route 전환 →
crowdsec wildcard dot 누락 수정) + 현재 prod 상태 + 후속 정리 항목.
2026-04-21 17:13:33 +09:00
kaffa
71aae4e374 products/ironclad-website: Next.js 16 + Claude Design 전환 반영
기존 Astro+Stitch 계획을 실제 구현(Next.js 16 App Router, opennextjs-cloudflare,
독자 Worker 3환경) 기준으로 갱신. 보안서비스 페이지 섹션별 콘텐츠 규칙 정리.
anvil-hosting(hosting.inouter.com)은 폐기된 실험으로 명시.

history/2026-04-21: Claude Design 도입 + /ko/security 본체 교체 경위 기록.
2026-04-21 15:48:53 +09:00
kaffa
08313cd4d4 infra/platform/ollama: add GPU and VRAM details
GTX 1080 Ti(11GB VRAM, CUDA 12.4) GPU 가속 확인. gemma4:e4b 로드 시
10GB 점유(89%), 동시 로드 한계·GPU 공유 제약·36 tok/s 실측치 추가.
infra-hosts.md §GPU 링크.
2026-04-21 12:29:11 +09:00
kaffa
6a54e8bbc3 add infra/platform/ollama.md
kr1 호스트의 Ollama 런타임 정본 기록. Tailscale 11434 엔드포인트,
qwen3:4b / gemma4:e4b 모델, 2026-04-21 tool-calling 검증 내역 포함.
2026-04-21 11:48:37 +09:00
heimdall
b0cef67afd history: 2026-04-21 outline APISIX route + SafeLine WAF
outline.inouter.com 을 Traefik 에서 APISIX 경유로 전환하고
chaitin-waf plugin 부착. CRD (ApisixTls + ApisixRoute,
ingressClassName=apisix) 로 관리. 정상 pass, SQLi 403 reject 검증.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:13:06 +09:00
kaffa
0fbce86cfc outline: route via APISIX (port 9443, ApisixRoute CRD), not Traefik
Switched Bunny iron-kr-nowaf origin from :443 (Traefik) to :9443 (APISIX,
no SafeLine plugin) since APISIX plugins are per-route. Used existing
:9443 path (juiceshop already there) instead of opening a new OpenWrt
port. Outline route managed via ApisixTls + ApisixRoute CRDs because
admin-API direct PUTs get swept by apisix-ingress-controller as orphans.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:08:16 +09:00
kaffa
a20c68e3a1 outline: split to iron-kr-nowaf pull zone, restore iron-kr WAF rules
Created new Bunny pull zone iron-kr-nowaf (ID 5720695) without Shield to
host outline.inouter.com exclusively. Uploaded *.inouter.com wildcard cert
from cert-manager since Bunny LE auto-provision kept returning invalid.
Restored 7 CRS rules (942100,932230/235/260/370/380,933160) on iron-kr
Shield so vault/n8n/telegram-webhook/jarvis regain protection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:32:11 +09:00
kaffa
782fff8fe9 k3s: document kr2 kubelet memory reserve as intentional OOM mitigation
Ties the existing /etc/rancher/k3s/config.yaml kubelet-arg (system-reserved=8Gi,
eviction-hard<2Gi) to the 2026-04-19 OOM freeze incident so it won't be
flagged as mystery asymmetry in future audits. Closes item 6 of 2026-04-20
K3s improvements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 07:44:09 +09:00
heimdall
f8c4274124 history: 2026-04-20 K3s 개선 6건 실행 리포트
Default SC 통일, safeline 볼륨 replica 3 통일, vector healthcheck
disable, hp1 rebalance 자연 진행 (3→10 replica), iSCSI 재시작은
helm-upgrade 주기 영향 추정·관찰 유지, kr2 system-reserved 10Gi
비대칭은 기록 완료·통일 실행은 호스트 접근 후 후속.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:54:18 +09:00
heimdall
0d1bf40cfe history: 2026-04-20 K3s 상세 점검 리포트
Outline 업로드가 BunnyCDN Shield 403 으로 상세 본문 차단되어
요청자(kappa) 열람 경로를 Obsidian history 로 대체. Outline
parent 요약 문서 ID: c1ec3f2c-0fa8-49f8-9d0b-3d619a0e4715.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:11:06 +09:00
kaffa
5d0632cb68 backup: install Velero v1.18 (chart 12.0.0) + daily-full schedule to R2 velero-backup 2026-04-20 10:12:21 +09:00
kaffa
a2884a60d8 backup: add k8s VolumeSnapshot API (snapshot-controller v8.5.0) as Velero prereq; fix democratic-csi restart loop 2026-04-20 09:06:48 +09:00
kaffa
7d58e159af metallb: raise controller mem limit 64Mi→256Mi (OOM fix) 2026-04-20 08:20:39 +09:00
kaffa
dc18ec8bac infra-hosts: Longhorn 자동 복구 설정 (node-down auto-delete + replica-auto-balance) 2026-04-19 15:05:00 +09:00
kaffa
620abeae79 infra-hosts: Descheduler 설치 기록 (kr2 OOM freeze 대응) 2026-04-19 14:36:03 +09:00
kaffa
fa9534c265 tasks: add Longhorn 1.8.2 → 1.11.1 upgrade plan
3 sequential minor hops required before Phase 3 storage-network migration.
Includes pre-flight checks, per-hop commands, validation checklist, rollback plan.
2026-04-19 13:44:57 +09:00
kaffa
afd2935ea4 infra/network: add multus.md (Multus CNI + storage-205 NAD)
Document Multus CNI + Whereabouts IPAM deployment on k3s, including:
- k3s path peculiarities (symlink-based multicall binary vs thick daemonset chroot)
- /opt/cni/bin real-directory workaround with all plugin binaries
- multus-shim/passthru symlinks to k3s CNI dir
- storage-205 NAD for Longhorn 2.5G storage network (ens2, MTU 9000, whereabouts .240-.254)
2026-04-19 13:39:34 +09:00
kaffa
354b2bb9d6 graphifyignore: exclude dev/ from knowledge graph 2026-04-19 12:17:30 +09:00
kappa
75090c8d19 bunnycdn-security: iron-jp 호스트네임 전부 제거 (anvil.it.com 계열 미사용) 2026-04-17 22:00:29 +09:00
kappa
dfc48ae606 bunnycdn-security: MidRate 규칙 전 풀존 삭제 (NAT 오탐 + 비브라우저 호환 문제) 2026-04-17 17:51:48 +09:00
kappa
8ccdc75f55 bunnycdn-security: iron-git rate limit 제거 (git 클라이언트 호환 불가) 2026-04-17 17:50:42 +09:00
kappa
cbe53f0c22 bunnycdn-security: MidRate 80/block → 120/challenge 변경 (NAT 환경 오탐 방지) 2026-04-17 17:41:13 +09:00
kappa
1ef3bedcb5 bunnycdn-security: Rate Limiting 규칙 2개 설정 (IPBurst + MidRate) 2026-04-17 10:21:14 +09:00
kappa
74b0bdaee6 bunnycdn-security: MonthlyBandwidthLimit 50→100GB 상향 2026-04-17 10:12:17 +09:00
kappa
38a1d8d9ad bunnycdn-security: MonthlyBandwidthLimit 초과 시 풀존 비활성화 동작 기록 (Ticket #386429) 2026-04-17 10:11:19 +09:00
kappa
78dca9185e bunnycdn-security: DDoS 과금 정책 공식 확인 결과 기록 (Ticket #386429) 2026-04-17 09:38:01 +09:00
kappa
57c028e0dc bunnycdn-security: Turnstile multi-domain 해결 + MonthlyBandwidthLimit 설정 2026-04-17 08:55:56 +09:00
kappa
1a79c6464a bunnycdn: WAF wafExecutionMode 0→1(Block) 전환 반영 + PATCH API 엔드포인트 수정 2026-04-17 08:49:36 +09:00
kappa
c1a9e84127 crowdsec: Vector _msg 표준 nginx combined 통일, 커스텀 파서 제거
- Vector transform에서 Traefik JSON → 표준 nginx combined _msg 변환
- APISIX 서울도 _msg 재구성 (비표준 → 표준 nginx combined)
- custom/apisix-logs 파서 제거, nginx-logs 하나로 통일
- CrowdSec VictoriaLogs Traefik acquisition type: nginx로 변경
2026-04-17 07:30:22 +09:00
kappa
e9ca6c7917 crowdsec: Discord 알림 설정 추가 (2026-04-17) 2026-04-17 07:12:17 +09:00
kappa
ba8b4b160e crowdsec: 화이트리스트/파서 설정 변경사항 반영 (2026-04-17)
- crowdsecurity/whitelists 파서 설치 (192.168.9.1 false positive 해결)
- custom/tailscale-whitelist 추가 (100.64.0.0/10)
- custom/apisix-logs 파서 추가 (서울 APISIX 비표준 nginx 포맷)
- crowdsecurity/traefik collection 설치
- 버전 v1.7.7 확인
2026-04-17 00:51:45 +09:00
heimdall
7395446478 docs: add _index.md MOC to all directories 2026-04-16 13:46:06 +09:00
heimdall
f0e51daafd refactor: organize infra/ into compute/network/security/data/platform 2026-04-16 13:43:36 +09:00
heimdall
66d2f51743 refactor: normalize infra wikilinks to bare names 2026-04-16 13:43:18 +09:00
heimdall
3f9727f3ac refactor: move nixos-manual to reference/ + graphifyignore 2026-04-16 13:43:03 +09:00
244 changed files with 6165 additions and 803 deletions

View File

@@ -2,3 +2,5 @@ graphify-out/
.graphify-cache/ .graphify-cache/
.obsidian/ .obsidian/
.trash/ .trash/
reference/nixos-manual/
dev/

17
dev/_index.md Normal file
View File

@@ -0,0 +1,17 @@
---
title: dev 인덱스
updated: 2026-04-16
tags: [moc, dev]
---
## Dev
| 문서 | 설명 |
|------|------|
| [[claude-agent-sdk]] | Claude Agent SDK와 Harness 개념 |
| [[claude-code-setup]] | Claude Code 설정 및 인스트럭션 |
| [[dev-environment]] | 개발 환경 및 도구 (Chrome CDP 등) |
| [[go-vibe-coding]] | Go 바이브코딩 가이드 |
| [[karpathy-coding-principles]] | Karpathy LLM 코딩 4원칙 (외부 코딩 프로젝트 참고용) |
| [[obsidian-schema]] | Obsidian Frontmatter 스키마 (Bases/Dataview 쿼리용) |
| [[workstations]] | 개인 워크스테이션 / 모바일 디바이스 인벤토리 |

View File

@@ -0,0 +1,93 @@
---
title: Karpathy 코딩 4원칙 (LLM 코딩 참고용)
updated: 2026-05-04
tags: [dev, llm, coding, reference]
---
## 출처
- 레포: https://github.com/forrestchang/andrej-karpathy-skills
- Andrej Karpathy의 LLM 코딩 관찰을 4원칙으로 정리한 가이드
- 본 문서는 코딩 작업 시 참고용. CLAUDE.md에는 적용하지 않음 (이유는 맨 아래 참조)
## 문제 정의
LLM이 코딩할 때 자주 하는 4가지 실수:
1. 잘못된 가정을 silent하게 진행
2. 과도하게 추상화/일반화
3. 요청 범위 밖까지 손대기
4. 명확한 성공 기준 없이 "고쳤다" 선언
## 4원칙
### 1. Think Before Coding (생각 먼저, 코드 나중)
> "Don't assume. Don't hide confusion. Surface tradeoffs."
- **가정을 명시적으로 표면화**. silent하게 결정 내리지 말 것
- 모호한 부분이 있으면 여러 해석 제시 후 선택 요청
- 혼란스러우면 멈춰서 물어볼 것
**예시**: "유저 데이터 export 기능 추가해줘" 라는 요청에
- 전체 vs 필터링?
- 다운로드 파일 vs 백그라운드 잡 vs API 엔드포인트?
- 어떤 필드?
- 보통 몇 명 규모?
→ 위를 모두 추측하지 말고 물어볼 것.
### 2. Simplicity First (단순함 먼저)
> "Minimum code that solves the problem. Nothing speculative."
- 요청한 것만 구현. 미요청 기능 추가 금지
- 1회성 추상화 금지 (use case 1개에 strategy pattern + abstract base class 금지)
- speculative error handling 금지
- 벤치마크: "시니어가 이걸 over-engineered라고 할까?"
**예시**: 할인 계산 함수 하나 → 그냥 함수 하나로. 추상 베이스 클래스나 strategy pattern은 진짜 다양성이 생긴 후에.
### 3. Surgical Changes (외과적 변경)
> "Touch only what you must. Clean up only your own mess."
- 필요한 것만 건드릴 것
- 기존 스타일 보존 (quote 스타일, 타입힌트 스타일 등)
- 무관한 리팩토링 금지
- import/변수 정리는 **본인이 만든 미사용분만**. 기존에 있던 미사용은 손대지 말 것
**예시**: "빈 이메일에서 크래시 나는 거 고쳐줘" 라는 버그 픽스에서
- 이메일 validation 로직 전체 개선 ❌
- quote 스타일 통일 ❌
- 타입힌트 추가 ❌
- 그 버그만 고치기 ✅
### 4. Goal-Driven Execution (목표 기반 실행)
> "Define success criteria. Loop until verified."
- 시작 전에 검증 가능한 성공 기준 정의
- 가능하면 테스트 먼저 작성 → 통과시키는 방향으로 구현
- 복잡한 작업은 단계별 검증 체크포인트 둘 것
- "고쳤다"가 아니라 "검증했다"가 종료 조건
**예시**: "인증 시스템 고쳐줘"
- ❌ 막연히 코드 수정 후 "고쳤습니다"
- ✅ 1) 재현 테스트 작성 → 2) 버그 재현 확인 → 3) 픽스 → 4) 테스트 통과 → 5) 회귀 없음 확인
## 핵심 한 줄
> "Good code is code that solves today's problem simply, not tomorrow's problem prematurely."
## kappa 환경에 적용 안 한 이유 (참고)
CLAUDE.md에 4원칙을 직접 박지 않은 이유:
| 원칙 | 현재 상태 |
|------|----------|
| 1. Think Before Coding | `feedback_act_dont_ask`, `feedback_less_confirmation`**정반대 방침** (확인 질문 최소화). 인프라 운영 위주라 추론·실행 비용이 작은 컨텍스트 |
| 2. Simplicity First | Claude Code 시스템 프롬프트에 "Don't add features...beyond what the task requires" 동일 문구 이미 존재 |
| 3. Surgical Changes | 시스템 프롬프트의 "Avoid backwards-compatibility hacks", "Don't add error handling for scenarios that can't happen"로 커버 |
| 4. Goal-Driven Execution | `verify-before-done` 스킬이 종료 직전 검증을 강제 (시점이 다름 — Karpathy는 시작 전 정의) |
**다른 코딩 프로젝트(특히 외부 협업 / 신규 코드베이스)에서는 4원칙이 그대로 유효**하므로 그때 이 문서 참고할 것.

View File

@@ -0,0 +1,42 @@
---
title: MetalLB controller OOMKilled 수정 (limit 상향)
date: 2026-04-20
tags: [history, metallb, k3s, oom]
---
## 현상
`metallb-controller-685b4dc4d8-jzxwj` 파드가 15시간 동안 5회 재시작.
`describe`로 확인한 이전 종료 상태가 `OOMKilled / Exit 137`.
## 근본원인
- controller limit: **64Mi** (helm 기본값)
- 실사용: **62Mi** (평상시만으로도 이미 한계)
- cert-rotation · webhook 서버 · reconcile 시 메모리 spike → OOM
metallb v0.15는 cert-controller와 webhook server가 controller 프로세스 안에서 실행되어 기본 64Mi로는 부족. speaker는 이미 256Mi/128Mi였는데 controller만 1/4 수준이었음.
## 조치
`helm upgrade --reuse-values`로 controller 리소스 상향:
```bash
helm upgrade metallb metallb/metallb -n metallb-system --reuse-values \
--set controller.resources.limits.memory=256Mi \
--set controller.resources.requests.memory=128Mi
```
결과:
- revision 3으로 배포
- 새 파드 `metallb-controller-6c846cbc7-gpz7q` Running
- 적용된 resources: `limits.memory=256Mi, requests.memory=128Mi`
- 재시작 0회로 안정
## 교훈
helm 기본 resources가 운영 환경에서 부족한 경우가 흔함. 설치 시점에 반드시 기본값 리뷰 필요. 이번 외에도 chart 기본값으로 영구화가 안 돼 있던 건들은 [[2026-04-19-k3s-resource-requests|history]] 참고.
## 정본 링크
- [[metallb|infra/network/metallb]]

View File

@@ -0,0 +1,118 @@
---
title: snapshot-controller 도입 + democratic-csi 재시작 루프 해소 (Velero 선결)
date: 2026-04-20
tags: [history, k3s, csi, snapshot, velero, democratic-csi]
---
## 현상
democratic-csi controller(`synology-iscsi-democratic-csi-controller`)가 14일 동안 26회 재시작. 이전 종료 로그:
```
failed to list *v1.VolumeSnapshotContent: the server could not find
the requested resource (get volumesnapshotcontents.snapshot.storage.k8s.io)
→ Unexpected EOF during watch stream
→ failed to renew lease
→ F stopped leading
```
## 근본원인
- controller 파드의 `external-snapshotter` sidecar(v8.2.1)가 쿠버네티스 표준 `snapshot.storage.k8s.io` CRD를 watch 시도
- 클러스터에 해당 CRD가 설치되어 있지 않음 (Longhorn 자체 `snapshots.longhorn.io`, k3s `etcdsnapshotfiles.k3s.cattle.io`만 존재)
- 404 에러 루프 + API watch stream 끊길 때 leader lease 갱신 실패 → 전체 sidecar exit
## 결정: 표준 API 도입 + sidecar 정리 병행 (옵션 A + B)
단순 수정이면 sidecar 비활성화만으로 충분. 그러나 "k8s 표준 백업 도구(Velero) 도입" 계획이 잡혔으므로 표준 CRD 설치도 함께 진행.
### 옵션 분석
| 방안 | 변경 범위 | Velero 호환 | 채택 |
|-----|----------|-----------|------|
| A. snapshot-controller 설치 | 클러스터 전역 | ✅ | ✅ |
| B. democratic-csi sidecar 비활성 | controller 1개 | - | ✅ |
| 현상 유지 | - | ❌ | - |
Synology iSCSI PVC = 0개 (StorageClass만 있고 실사용 워크로드 없음). 현재 Synology snapshot 기능 불필요 → B 병행해도 기능 손실 없음.
## 조치
### 1. snapshot CRDs 설치 (v8.5.0)
```bash
for f in snapshot.storage.k8s.io_volumesnapshotclasses.yaml \
snapshot.storage.k8s.io_volumesnapshotcontents.yaml \
snapshot.storage.k8s.io_volumesnapshots.yaml; do
kubectl apply -f \
"https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.5.0/client/config/crd/$f"
done
```
### 2. snapshot-controller Deployment (kube-system)
```bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.5.0/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.5.0/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
```
결과: `snapshot-controller` Deployment(replicas=2) 가동, 파드 2개 Running.
### 3. Longhorn VolumeSnapshotClass
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: longhorn-snapshot
labels:
velero.io/csi-volumesnapshot-class: "true"
driver: driver.longhorn.io
deletionPolicy: Delete
parameters:
type: snap
```
`velero.io/csi-volumesnapshot-class=true` 라벨로 Velero가 자동 인식. 현재는 `type: snap`(Longhorn 내부 스냅샷). Velero는 VolumeSnapshot 메타를 오브젝트 스토리지에 저장하고 데이터는 이 스냅샷으로 관리. 장기 보관용 `type: bak`(R2 영속) 클래스는 Velero 실사용 단계에서 재검토.
### 4. democratic-csi external-snapshotter sidecar 제거
democratic-csi helm chart가 로컬에 없어 repo 추가 후 upgrade:
```bash
helm repo add democratic-csi https://democratic-csi.github.io/charts/
helm repo update
helm upgrade synology-iscsi democratic-csi/democratic-csi \
-n democratic-csi --version 0.15.1 --reuse-values \
--set controller.externalSnapshotter.enabled=false
```
결과:
- controller 파드 sidecar 6/6 → **5/5**
- 새 파드 `synology-iscsi-democratic-csi-controller-ccb945d6b-94fph` Running
- 재시작 0회
## 검증
| 확인 항목 | 결과 |
|----------|------|
| snapshot.storage.k8s.io CRDs | 3종 Established |
| snapshot-controller Deployment | 2/2 Running (kube-system) |
| VolumeSnapshotClass `longhorn-snapshot` | 생성, Velero 라벨 부여 |
| democratic-csi controller sidecars | 5개 (external-snapshotter 빠짐) |
| 재시작 루프 | 해소 |
## 다음 단계 (Phase 2)
Velero 설치:
- Backup Storage Location: R2 (기존 `longhorn-backup` 재활용 또는 신설 `velero-backup`) — 결정 필요
- Volume Snapshot Location: CSI → `longhorn-snapshot` VolumeSnapshotClass
- 스코프: 네임스페이스 단위, 전체 k8s 리소스 + PV 스냅샷 동시 캡처
- 기존 파이프라인 정합:
- Longhorn BackupTarget(R2 longhorn-backup) 유지 → 볼륨 단위 복구용
- Velero → 네임스페이스/앱 단위 전체 복구용
- 중복이지만 목적이 다름. 두 개 공존
## 정본 링크
- [[backup|infra/data/backup.md]] (K8s 표준 VolumeSnapshot API 섹션 추가)

View File

@@ -0,0 +1,159 @@
---
title: Velero 도입 (K8s 표준 백업 도구)
date: 2026-04-20
tags: [history, velero, backup, k3s, csi]
---
## 배경
Longhorn BackupTarget은 볼륨 단위 복구에 강하지만 네임스페이스 · 앱 단위 전체 복원(k8s 리소스 정의 + PV 데이터 동시 캡처)을 지원하지 않음. 표준 도구 필요성 논의 후 Velero 도입 결정. 선결 조건 snapshot-controller + VolumeSnapshot CRDs는 [[2026-04-20-snapshot-controller-velero-prep|prep]]에서 완료됨.
## 결정 사항
| 항목 | 값 | 근거 |
|------|-----|------|
| 백업 대상 스토리지 | R2 신설 버킷 `velero-backup` | 기존 `longhorn-backup`과 역할 혼동 방지. 버킷 IAM 정책 분리 여지 |
| 리전 | ENAM (R2 기본 자동 배정) | APAC 지정 API 옵션 없음. 배치 백업이라 레이턴시 영향 미미 |
| Credentials | 기존 R2 키 재사용 | Vault `secret/cloud/cloudflare/r2` · 현재 R2 계정 단일 |
| Volume snapshot | CSI (`longhorn-snapshot` VolumeSnapshotClass, type=snap) | Longhorn 네이티브 스냅샷 사용. 속도 빠름 |
| 스케줄 | 일 1회 UTC 16:00 (KST 01:00) | Longhorn 6h critical-backup과 겹치지 않음 |
| TTL | 720h (30일) | 월 단위 보관. 장기 보관은 별도 수동 백업 또는 TTL 연장 |
| 스코프 | 전체 네임스페이스 + cluster-scoped, 단 인프라 NS 제외 | 초기 도입은 포괄적으로. 추후 좁히는 게 쉬움 |
## 설치
### 1. Helm 저장소 · 네임스페이스
```bash
helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts
helm repo update vmware-tanzu
kubectl create namespace velero
```
### 2. R2 credentials 시크릿
```bash
printf '[default]\naws_access_key_id=<KEY>\naws_secret_access_key=<SECRET>\n' \
| kubectl -n velero create secret generic velero-cloud-credentials \
--from-file=cloud=/dev/stdin
```
키는 Vault `secret/cloud/cloudflare/r2`에 보관.
### 3. values.yaml
```yaml
initContainers:
- name: velero-plugin-for-aws
image: velero/velero-plugin-for-aws:v1.14.0
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /target
name: plugins
credentials:
useSecret: true
existingSecret: velero-cloud-credentials
configuration:
features: EnableCSI
defaultBackupTTL: 720h
defaultVolumesToFsBackup: false
backupStorageLocation:
- name: default
provider: aws
bucket: velero-backup
default: true
config:
region: us-east-1 # R2 무시하지만 필수 필드
s3ForcePathStyle: "true"
s3Url: https://d8e5997eb4040f8b489f09095c0f623c.r2.cloudflarestorage.com
checksumAlgorithm: "" # R2 호환성 (AWS SDK checksum 비활성)
volumeSnapshotLocation:
- name: default
provider: csi
snapshotsEnabled: true
deployNodeAgent: false # restic 파일 단위 백업은 현재 불필요
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
```
```bash
helm install velero vmware-tanzu/velero -n velero --version 12.0.0 \
-f /tmp/velero-values.yaml
```
### 4. 검증 (테스트 백업)
`outline` 네임스페이스(Longhorn 5Gi PVC 포함)로 테스트:
```
Phase: Completed
CSI Snapshots:
outline/outline-data:
Snapshot Content Name: snapcontent-297415e0-...
CSI Driver: driver.longhorn.io
Snapshot Size (bytes): 5368709120
Result: succeeded
```
### 5. 정기 스케줄
```yaml
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: daily-full
namespace: velero
spec:
schedule: "0 16 * * *"
template:
ttl: 720h
includedNamespaces: ["*"]
excludedNamespaces:
- kube-system
- kube-public
- kube-node-lease
- velero
- longhorn-system
- democratic-csi
- metallb-system
- cert-manager
- monitoring
- logging
- external-secrets
- nfs-provisioner
- rabbitmq-system
snapshotVolumes: true
storageLocation: default
volumeSnapshotLocations: [default]
defaultVolumesToFsBackup: false
```
## 주의/함정
- `config.checksumAlgorithm: ""` 없으면 R2에서 AWS SDK의 integrity check로 업로드 실패 가능. Velero 공식 issue에서 권장됨.
- `region: us-east-1`은 R2가 무시하지만 SDK 검증 때문에 필수.
- VolumeSnapshot/VolumeSnapshotContent CRD는 스냅샷 완료 후 Velero가 즉시 GC. `kubectl get volumesnapshot -A`에 남지 않는 건 정상.
- velero CLI 설치는 로컬 Mac에서 `brew install velero`. 서버 작업에는 kubectl + Backup CRD로도 가능.
## 다음 과제
- R2 `velero-backup` 버킷 전용 Scoped Token 발급 (현재는 전 버킷 공통 키 재사용) — 보안 강화
- 복원 리허설: 월 1회 임의 네임스페이스를 다른 클러스터 또는 격리된 네임스페이스로 restore 테스트
- Longhorn `type: bak` VolumeSnapshotClass 추가 검토 (R2 영속 저장 필요 시)
- Velero 알림 통합: backup 실패 시 Discord 또는 email (velero webhook/plugin)
## 정본 링크
- [[backup|infra/data/backup.md]] (Velero 섹션)
- [[2026-04-20-snapshot-controller-velero-prep|prep history]]

View File

@@ -0,0 +1,76 @@
---
title: 2026-04-21 Ironclad production cutover — apex 도메인 Worker 전환
updated: 2026-04-21
tags: [history, ironclad, website, cloudflare, production, crowdsec]
---
## 요약
`ironclad.it.com` (apex) production이 이전 경로(Cloudflare → APISIX → Caddy 컨테이너, Stamp/Flux volume_shared) 에서 **Cloudflare Worker `ironclad-site` (Next.js 16 + opennextjs-cloudflare)** 로 전환됨. 고객이 보는 화면이 6주 묵은 영문 "Ironclad Corp — Managed Security Hosting" 에서 최신 한국어 "Ironclad — 올어라운드 호스팅 & 보안서비스" 로 즉시 교체.
## 진행 경로
### 1차 시도 — `v2026.04.21` 태그 push (실패)
- `deploy-production.yml` 트리거, wrangler 빌드·업로드 성공
- DNS 등록 단계에서 Cloudflare API 실패:
```
code: 100117
Hostname 'ironclad.it.com' already has externally managed DNS records (A, CNAME).
Either delete them, try a different hostname, or use the option
'override_existing_dns_record' to override.
```
- 원인: 기존 `ironclad.it.com A 172.233.93.180 (Proxied)` 레코드가 먼저 있어 `custom_domain: true` 등록 거부
### 2차 시도 — custom_domain 제거하고 zone route로 전환 (성공)
- `wrangler.jsonc` production env routes 변경:
- Before: `{ pattern: "ironclad.it.com", custom_domain: true }`
- After: `{ pattern: "ironclad.it.com/*", zone_name: "ironclad.it.com" }`
- 로컬에서 `CLOUDFLARE_API_TOKEN=... pnpm deploy:production` 실행 → Worker 업로드 + route 등록 성공
- 결과: Cloudflare에 `ironclad.it.com/* → ironclad-site` route binding 생성됨
### 3차 보정 — crowdsec bouncer wildcard route 버그 수정 (결정타)
- 배포 후에도 prod 응답이 여전히 구 HTML. 원인 조사 결과 Cloudflare Workers Routes 상태:
```
www.ironclad.it.com/* -> ironclad-site
ironclad.it.com/* -> ironclad-site
*ironclad.it.com/* -> crowdsec-cloudflare-worker-bouncer ← 문제
```
- `*ironclad.it.com/*` 패턴이 의도는 "subdomain wildcard" 였으나 **dot 누락**으로 apex까지 포괄(prefix wildcard로 해석되어 `ironclad.it.com/*` 도 매칭). crowdsec bouncer Worker가 먼저 잡아서 origin(APISIX → Caddy)로 pass-through.
- Cloudflare API로 zone route `c148ae77ebb74f20b61ecd71d0e86b77`를 `*.ironclad.it.com/*`로 변경:
```bash
curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE/workers/routes/$ROUTE_ID" \
-d '{"pattern":"*.ironclad.it.com/*","script":"crowdsec-cloudflare-worker-bouncer"}'
```
- 수정 직후 `ironclad.it.com` 응답에 `x-opennext: 1` + `x-powered-by: Next.js` 확인 → Worker가 정상 서빙
## 현재 prod 상태 (2026-04-21 post-cutover)
- `https://ironclad.it.com/` → Cloudflare → `ironclad-site` Worker (Next.js 16)
- title: `Ironclad — 올어라운드 호스팅 & 보안서비스 | Ironclad`
- `x-opennext: 1`, `x-powered-by: Next.js`
- DNS A record `172.233.93.180`은 건드리지 않음 (롤백 준비)
- apex 도메인의 bot 차단은 Next.js Worker 내부의 CrowdSec middleware (KV binding `CROWDSECCFBOUNCERNS = 9af0d1c1c14a4bc1a3835c2a5b22fd7a`)가 담당. Edge Worker bouncer는 서브도메인(`*.ironclad.it.com`)만 커버.
## 후속 정리 필요
| 항목 | 담당 | 우선순위 |
|------|------|----------|
| APISIX 라우트 `ironclad.it.com` 제거 (volume_shared 모드) | 씬 | 낮음 (트래픽 안 감, 남아있어도 무해) |
| Stamp-Flux 파이프라인에서 ironclad.it.com 배포 경로 deprecate | 헤임달 | 중 (혼선 방지) |
| `openclaw/openclaw-agents.md` volume_shared 모드 ironclad 관련 기록 정리 | 헤임달 | 중 |
| Cloudflare DNS A record 변경 (172.233.93.180 → Cloudflare 기본) | kappa | 낮음 (zone route 방식이라 origin 무관) |
| `v2026.04.21` 태그는 `deploy-production.yml`에서 실패로 남음 → 재태깅 불필요 (로컬에서 직접 배포 완료) | — | — |
## 커밋
- `hosting/ironclad`:
- `v2026.04.21` (태그, f736487) — Header dark nav 승격 시점
- `6f2e2cc` fix(deploy): custom_domain → zone route로 전환 (DNS 충돌 회피)
- Cloudflare API 변경: zone route `c148ae77ebb74f20b61ecd71d0e86b77` pattern 업데이트 (wrangler 소스 외)
## 학습 포인트
- **apex 도메인에 `custom_domain: true`를 쓰려면 기존 DNS 레코드가 없어야 한다**. Stamp/Flux 같이 기존 정적 배포 경로가 있는 도메인은 `zone_name` 기반 route 방식이 자연스럽다.
- **Cloudflare Worker Routes wildcard의 dot 누락은 apex까지 포괄**. `*example.com/*` vs `*.example.com/*`는 전혀 다른 패턴. `*example.com/*`은 prefix wildcard로 apex 포함.
- 기존 wildcard route가 apex로 해석되면 "내가 새로 만든 route가 우선" 될 거라 기대하기 어렵다. Cloudflare는 specificity tie-breaker가 있지만 명확히 의도한 대로 동작하지 않음. 가장 안전한 건 **wildcard pattern이 apex를 포함하지 않게 수정**.
- `x-opennext: 1` 헤더는 OpenNext가 서빙하고 있음을 확인하는 가장 빠른 시그널.

View File

@@ -0,0 +1,72 @@
---
title: 2026-04-21 Ironclad 홈페이지 재구성 (Claude Design 도입)
updated: 2026-04-21
tags: [history, ironclad, website, claude-design]
---
## 요약
Ironclad 홈페이지(ironclad.it.com)의 보안서비스 페이지(`/ko/security`)를 Claude Design 기반으로 전면 재구성. Obsidian 정본을 Astro+Stitch 계획에서 실제 구현(Next.js 16 + Claude Design)으로 동기화.
## 배경
- 기존 정본 `products/ironclad-website.md`는 "Astro + Google Stitch + Tailwind + cf-multisite 배포" 계획을 담고 있었지만, 실제 배포는 이미 **Next.js 16 + opennextjs-cloudflare + 독자 Worker**로 진행된 상태였음 (hosting/ironclad Gitea repo, 2026-04-02 시작)
- `kaffa/anvil-hosting`(hosting.inouter.com)은 이전 실험으로 폐기된 상태지만 정본에 반영 안 됨
- Cloudflare Workers에 `ironclad-site`(prod) / `ironclad-site-staging` / `ironclad-site-preview` 3환경 존재, R2 캐시 3버킷 공존
- 정본과 실제 코드 간 갭이 커서 이번 재구성과 함께 정본 업데이트
## 작업 내용
### Claude Design 세팅
1. claude.ai/design 에서 **Ironclad Design System** 생성 (project id `a17ceb77-c652-44dd-ad39-71f9afd98074`)
- Company blurb에 staging 톤, 카피 규칙, 내부 벤더 은닉 규칙 전부 주입
- 라이트 + 민트 그린 팔레트, Urbanist/Nunito Sans/JetBrains Mono 자동 설정
2. 보안서비스 페이지 프로젝트 생성 (`ironclad-security-v2`, id `6a3e45a2-459f-4301-baac-57098282105f`)
3. 프롬프트 투입 후 11개 컴포넌트(Nav/Hero/Defense 2/Value 2/Console 2/Pricing 3) 생성
4. "Export as standalone HTML" → 단일 HTML 번들 수령
### staging 배포 검증 (1차)
- `public/security-preview.html`에 standalone HTML 저장 → main push → Gitea Actions로 staging 자동 배포 성공 (`https://staging.ironclad.it.com/security-preview`, 2분 소요)
- 디자인 일관성 확인 후 본체 교체 결정
### 본체 교체
- JSX 6개 컴포넌트 파일을 `src/components/security-v2/*.tsx`로 변환 (`curl` 다운로드 + sed/perl 일괄 수정)
- `const { useState } = React``import { useState } from 'react'`
- `window.__resources` fallback 제거 → `/assets/xxx.svg`로 직접 경로
- 모듈 스코프 `window.X = X` 할당 제거
- Tailwind 엄격 타입체크 회피를 위해 `// @ts-nocheck` 추가
- `src/app/[locale]/security/page.tsx` 재작성 (Layout Header·Footer 유지, Nav 컴포넌트는 중복이라 제외)
- 2개 SVG를 `public/assets/` 배치
### 트러블슈팅
- 1차 배포 후 500 에러 → `next dev` 로 재현 → `React.useState`가 import 없이 쓰여서 `ReferenceError: React is not defined` (Pricing.tsx SecurityFAQ)
- 해결: 해당 파일에 `import * as React from 'react'` 추가 → fix 커밋 후 재배포로 HTTP 200 확인
## 변경 파일
| 경로 | 변경 |
|------|------|
| `hosting/ironclad` `src/app/[locale]/security/page.tsx` | 기존 Container 기반 → security-v2 컴포넌트 조합 |
| `hosting/ironclad` `src/components/security-v2/` 신규 | Hero/Defense/Value/Console/Pricing (+Nav 미사용) 6 TSX |
| `hosting/ironclad` `public/assets/` 신규 | logo-white.svg, hero-shield.svg |
| `obsidian` `products/ironclad-website.md` | 기술 스택·배포·디자인 시스템 전면 갱신, anvil 폐기 기록 |
## 커밋 (hosting/ironclad)
- `b6dbf61` feat: add security-preview.html (Claude Design standalone)
- `c2093ce` feat(security): Claude Design 기반 /ko/security 본체 교체
- `5fcef59` fix(security-v2): add 'import * as React' to Pricing.tsx (ReferenceError on SSR)
## 학습 포인트
- Claude Design의 "Export as standalone HTML"은 파일 다운로드가 아니라 **Claude에게 번들링 작업을 지시**하는 액션. 결과물은 프로젝트 Design Files에 추가되고 signed URL로 fetch 가능.
- Claude Design이 만드는 JSX는 browser-only(babel standalone) 전제라 `React.xxx` 접근과 `window.Name = Name` 모듈 스코프 할당이 자연스러움. Next.js SSR로 이식 시 이 두 패턴을 일괄 정리해야 함.
- 라이트 테마 + 민트 그린 조합은 Stripe/Vercel류 다크 프리미엄 톤보다 한국 B2B SaaS 친화적. Obsidian 정본의 "다크 + 프리미엄" 전제가 실제 제품 방향과 어긋나 있었음 → 정본이 코드보다 뒤처져 있었던 사례.
- `hosting/ironclad` Gitea repo는 **private**이라 토큰 없이 curl하면 404. 자동화 탐색 시 Vault에서 토큰을 미리 받아야 전체 repo 목록 보임.
## 후속 과제
- `graphify update ~/obsidian` 실행하여 새 파일 인덱싱
- `/ko/security` 페이지 품질·성능 실측 (Lighthouse), 필요시 Claude Design Tweaks로 반복
- production 배포 전 추가 검증: `deploy-production.yml`은 수동 workflow_dispatch 트리거
- 향후 `/ko/pricing` 등 다른 페이지도 같은 Design System으로 리디자인 가능성 검토

View File

@@ -0,0 +1,81 @@
---
date: 2026-04-21
topic: Outline을 WAF 없는 전용 Bunny 풀존(iron-kr-nowaf)으로 분리
areas:
- infra/platform/outline
- services/bunnycdn-security
---
# 2026-04-21 Outline Bunny 풀존 분리
`outline.inouter.com`을 WAF 없는 신규 풀존 `iron-kr-nowaf`로 이전. iron-kr의 7개 CRS 룰 복원.
## 배경
2026-04-13 iron-kr Shield learning mode 종료 후 CRS 룰이 enforce로 전환되면서 Outline API JSON 요청에 다수 오탐 누적:
| 룰 | 오탐 경로 | 누적 |
|---|---|---|
| 942100 libinjection SQLi | `api/*.list`, `api/collections.list` | 9 |
| 932235 Unix CMD Injection | `api/documents.create` | 3 |
| 932370 Windows CMD Injection | `api/documents.create` | 2 |
| 930130 Restricted File Access | `api/*` | 2 |
| 932230 Unix CMD Injection (2-3 chars) | `api/documents.create` | 1 |
| 933160 PHP Injection | `api/documents.create` | 1 |
Outline API는 Bearer 토큰 강제 인증 — WAF가 추가 보호 거의 없고 오탐만 생성.
Basic plan 한계로 host-per-path 커스텀 룰 불가, 따라서 **전용 풀존 분리** 채택.
## 변경 내역
### Bunny
| 항목 | 이전 | 이후 |
|---|---|---|
| Pull Zone | iron-kr (ID 5555227) | **iron-kr-nowaf** (ID 5720695, 신규) |
| Shield | iron-kr Shield 101015 (CRS 활성) | Shield 없음 |
| 국가차단 | middleware 64811 | middleware 64811 (동일) |
| OriginUrl | https://220.120.65.245 (Traefik 직결) | **https://220.120.65.245:9443** (APISIX 경유) |
| Backend 경로 | OpenWrt :443 → HAProxy → Traefik 192.168.9.53:443 → outline IngressRoute | OpenWrt :9443 → HAProxy → APISIX 192.168.9.50:443 → ApisixRoute outline → outline svc |
| AddHostHeader | true | true |
| VerifyOriginSSL | false | false |
| EnableWebSockets | true | true |
| TLS 인증서 | LE 자동 | `*.inouter.com` wildcard (cert-manager 수동 업로드) |
### APISIX (서울 K3s)
CRD 기반으로 관리 (admin API 직접 PUT은 apisix-ingress-controller가 orphan으로 판단해 sweep함):
| 객체 | 종류 | 이름 | 비고 |
|---|---|---|---|
| SSL | `ApisixTls` | `outline-tls` | sni=`outline.inouter.com`, secret=`apisix/wildcard-inouter-tls`, ssl_id=`4e7704e0` |
| Route | `ApisixRoute` | `outline` | host=`outline.inouter.com`, upstream=`outline.outline.svc:80`, plugins 없음(SafeLine 미장착), route_id=`ce4d2d80` |
| Class | `ingressClassName: apisix` | | apisix-ingress-controller가 reconcile |
⚠️ **중요**: APISIX 객체를 admin API로 직접 PUT하면 `apisix-ingress-controller`가 ingressClassName 없는 orphan으로 판단해 자동 DELETE함. 반드시 ApisixTls/ApisixRoute CRD 사용.
### DNS
```
outline.inouter.com CNAME iron-kr.b-cdn.net → iron-kr-nowaf.b-cdn.net
```
Cloudflare zone `inouter.com` (id cd84743d9c61b97bada5ce903a29ae2b), record id cb8fd1cf67b872e52600e4f868a2a992.
### iron-kr Shield 복원
임시 적용했던 `wafDisabledRules: [942100, 932230, 932235, 932260, 932370, 932380, 933160]` 전부 해제 → vault/n8n/telegram-webhook/jarvis 호스트 보호 원상복구.
## 검증
- `list_collections` MCP 호출 200 OK (이전 942100 차단)
- `create_document` 성공 (doc id `4bfff4c7-2fd3-478e-af00-2e25301da781` YouTube 요약)
- TLS cert SAN: `DNS:inouter.com, DNS:*.inouter.com` (GTS WR1, 만료 2026-06-21)
- HTTP/2 200, `cdn-pullzone: 5720695` 응답 헤더 확인
## 미해결 / 후속
- **Bunny LE 자동 발급 실패** — `POST /pullzone/loadFreeCertificate?hostname=outline.inouter.com``The request is invalid` 반환 (원인 불명). 현재 *.inouter.com wildcard로 우회했지만 LE 자동 갱신 체계 구축하려면 재조사 필요. cert-manager 와일드카드 갱신이 2026-06-21 전에 Bunny에 재업로드 필요 — 자동화 스크립트 또는 cert-manager webhook 연동 검토
- Advanced plan 업그레이드 시 iron-kr에서 host-per-path 커스텀 룰로 통합 관리 가능 (현재는 풀존 2개 운영)
- 이번 이슈 원인 분석과 932380 n8n/vault 21건 실제 공격 건은 별도 Outline 문서로 보존: doc `ac429f1b-...`, doc `7ac9ec42-...`

View File

@@ -0,0 +1,113 @@
---
date: 2026-04-23
topic: Netbis CF 바운서 전량 제거
areas: [infra/security/crowdsec-safeline.md, services/netbis.md, infra/security/cloudflare.md]
tags: [crowdsec, cloudflare, netbis, removal, cost-control]
---
## 배경
같은 날 [[2026-04-23-netbis-firewall-bouncer-migration|Firewall Rule 바운서 병행 도입]] 직후 재평가 → **두 바운서 모두 제거 결정**.
### 재평가 핵심
- **Worker Bouncer (`netbis-cf`)**: KV.get 구조. 2026-04 청구서 $100 중 82%가 KV read 오버. Worker 실행 자체가 과금 유발
- **Firewall Rule Bouncer (`netbis-cf-firewall`)**: CF Rule List 용량 10,000 (Pro). LAPI 전체 decision ~29k 중 67% 초과분은 CF에 동기화 불가. 우선순위 선별이지만 실제 공격자 IP 누락 가능성 + CF API rate limit(10040) 만성
- **결론**: 두 방식 모두 현재 Netbis 트래픽 규모와 비용 모델에 적합하지 않음. 더 적절한 방어 수단 재설계 필요 (후속 과제)
## 제거 범위
### netbis-cf-firewall (Firewall Rule Bouncer)
| 리소스 | 상태 |
|---|---|
| systemd service `crowdsec-cloudflare-bouncer.service` | disable + stop |
| 패키지 `crowdsec-cloudflare-bouncer` (0.3.0) | apt purge |
| LAPI bouncer 엔트리 `netbis-cf-firewall` | `cscli bouncers delete` |
| CF Rule List `crowdsec_block` (id `b14745a3306a4f81b46d96593135f78b`, 10,000 items) | DELETE |
| CF Firewall Rules (6 zones) + companion filters | 각 zone DELETE |
| CF scoped API token `crowdsec-cloudflare-bouncer-netbis` (id `2196850b050493d01e0ab9ec0cde3e15`) | DELETE |
| Vault `secret/cloud/cloudflare-netbis` `api_token` 필드 | kappa 경유 제거 예정 |
### netbis-cf (Worker Bouncer)
| 리소스 | 상태 |
|---|---|
| systemd service `crowdsec-cloudflare-worker-bouncer.service` | disable + stop |
| 패키지 `crowdsec-cloudflare-worker-bouncer` (0.0.14) | apt purge |
| LAPI bouncer 엔트리 `netbis-cf` | `cscli bouncers delete` |
| CF Worker Script `crowdsec-cloudflare-worker-bouncer` | DELETE |
| CF KV Namespace `CROWDSECCFBOUNCERNS` (id `8cb07c1cb7784a4ca533f319bf66aa64`) | DELETE |
| CF Worker Routes (6 zones, pattern `*<zone>/*`) | 각 zone DELETE |
| CF Turnstile widgets (6개 `crowdsec-cloudflare-worker-bouncer-widget`) | DELETE |
| CF User API Token `crowdsec-cf-bouncer-netbis` (`cfut_239XYQ...cf7c816`, id `d06694e98a2e58bfe9e0c338711b3aba`) | DELETE |
### 컨테이너 자체
| 리소스 | 상태 |
|---|---|
| jp1 Incus 컨테이너 `netbis-cf-bouncer` (10.253.103.33) | `incus delete --force` |
| OpenTofu 정의 | 탐색 결과 없음 (heimdall `~/kaffa/ops-agents-tofu/`, kappa Mac 모두 미발견). destroy 대상 없음 |
### 6개 보호 zone (변경 영향)
fall-vip.com, fall-mvp.com, fall-vip7.com, psd777.com, rss-555.com, rss-7790.com — CrowdSec 기반 CF 엣지 차단 일시 해제. 다른 계층(SafeLine WAF, BunnyCDN middleware 등)이 있다면 그쪽은 무변경.
## 실행 순서
```
1. systemctl disable --now crowdsec-cloudflare-bouncer crowdsec-cloudflare-worker-bouncer (jp1:netbis-cf-bouncer)
2. cscli bouncers delete netbis-cf-firewall netbis-cf (jp1:crowdsec)
3. apt purge crowdsec-cloudflare-bouncer crowdsec-cloudflare-worker-bouncer (jp1:netbis-cf-bouncer)
4. CF API (cfut token): Worker Routes × 6 DELETE → Worker Script DELETE → KV Namespace DELETE → Turnstile widgets × 6 DELETE
5. CF API (global_api_key): Firewall Rules × 6 DELETE + Filters × 6 DELETE → IP List DELETE → scoped api_token DELETE → cfut User Token DELETE
6. 컨테이너 내부 잔존 config 백업·log shred → /etc/crowdsec 삭제 → systemctl reset-failed
7. incus delete --force jp1:netbis-cf-bouncer
8. tmux → kappa: Vault api_token 필드 제거 요청 (global_api_key/email/account_id 보존)
```
## 검증
제거 후 CF 상태 (global_api_key X-Auth 조회):
| 지표 | 값 |
|---|---|
| Netbis Workers Scripts | 0 |
| Netbis KV Namespaces | 0 |
| 6 zones Worker Routes | 0 각각 |
| 6 zones CrowdSec firewall rules | 0 각각 |
| `crowdsec*` IP Lists | 0 |
| `bouncer*` Turnstile widgets | 0 |
| scoped api_token verify | Invalid API Token |
| cfut User Token verify | Invalid API Token |
LAPI (jp1:crowdsec):
```
cscli bouncers list # netbis-cf, netbis-cf-firewall 둘 다 없음
```
Incus (jp1):
```
incus list --all-projects jp1: | grep netbis # (empty)
```
## 롤백 불가 영역
- 제거된 API 토큰 2개: 값 복구 불가. 필요시 global_api_key로 재발급
- 삭제된 IP List의 10,000 IP 항목: 재생성 시 LAPI에서 다시 sync (바운서 재설치 필요)
- 삭제된 Turnstile widgets: secret key 포함 완전 파기. 재설치 시 새 widget/secret 발급
- Worker Script/KV: 재배포로 재생성 가능
## 후속 과제
- [ ] **Vault `secret/cloud/cloudflare-netbis`의 `api_token` 필드 제거** — kappa 경유 (다른 필드 보존)
- [ ] Netbis 방어 수단 재설계: CF 비용 + 차단 범위 트레이드오프 분석 후 대체 구조 결정 (예: BunnyCDN Edge + SafeLine WAF 조합, CrowdSec → CF Account Rule Lists 단독, CF Custom Ruleset 등)
- [ ] 2026-05 CF 청구서에서 Netbis 요금 감소 확인 (Worker 호출 0 + KV read 0 예상)
## 관련 문서
- [[../infra/security/crowdsec-safeline|crowdsec-safeline]] (섹션 2개 제거됨)
- [[2026-04-23-netbis-firewall-bouncer-migration|이전 history: Firewall Bouncer 도입]] (같은 날, 역전)
- [[../services/netbis|netbis]] (방어 구조 재설계 대상)

View File

@@ -0,0 +1,143 @@
---
date: 2026-04-23
topic: Netbis Firewall Bouncer 도입 (Worker Bouncer 병행)
areas: [infra/security/crowdsec-safeline.md, services/netbis.md, infra/security/cloudflare.md]
tags: [crowdsec, cloudflare, bouncer, netbis, cost-control]
---
## 배경
Netbis 계정 2026-04 Cloudflare Workers 요금 $100 중 **82%가 KV read 비용 오버**. Worker Bouncer(`netbis-cf`) 구조는 요청이 CF에 도달 → Worker 실행 → KV.get으로 IP 확인 → 차단/통과 순서. 즉 Worker가 실행돼야 KV read가 발생. 악성 IP라 결국 차단해도 KV read 비용은 이미 청구됨.
기존 Bouncer는 제거하지 않고 **병행**. Worker Bouncer는 Turnstile/captcha 등 정교한 챌린지용 유지, Firewall Rule Bouncer는 엣지 즉시 차단으로 Worker 실행 자체를 회피해 비용 방어.
## 최종 구조
```
LAPI (jp1:crowdsec, 10.253.100.240:8080)
├─ netbis-cf → CF Worker + KV (기존, Turnstile/정교)
└─ netbis-cf-firewall → CF Rule List + Firewall Rule (신규, 엣지 차단)
```
CF 요청 평가 순서상 Firewall Rule → Worker. Firewall Rule에서 block되면 Worker는 실행되지 않아 KV read 없음.
## 실행 단계
### 1. 사전 조사 — CF 플랜 / IP List 용량
6개 zone 플랜 (scoped API 아님, global_api_key X-Auth 조회):
| Zone | Zone ID | Plan |
|---|---|---|
| fall-vip.com | 662312b0ca619d1d5c8f4c112150d749 | Pro |
| fall-mvp.com | 6c171579912a271c0fc89c8187493b0f | Free |
| fall-vip7.com | a8832b9d3b546f96505abeadea4750d1 | Free |
| psd777.com | a14533c2937b19e5b7ed19cbecd58679 | Pro |
| rss-555.com | 6d4b084940520c1f820927e5d8ade2c6 | Pro |
| rss-7790.com | d9db9e50e202339326498baa340a9d16 | Pro |
Free/Pro/Biz 공통 IP list 상한 = 10,000. `total_ip_list_capacity: 10000` 설정.
### 2. 컨테이너 패키지 설치
`jp1:netbis-cf-bouncer` (기존 Worker Bouncer 컨테이너 재사용, 서비스 분리).
```bash
incus exec jp1:netbis-cf-bouncer -- apt-get update
incus exec jp1:netbis-cf-bouncer -- apt-get install -y crowdsec-cloudflare-bouncer # v0.3.0
```
### 3. LAPI 바우언서 등록
```bash
incus exec jp1:crowdsec -- cscli bouncers add netbis-cf-firewall
# API key 출력을 즉시 설정 파일에 주입
```
### 4. CF scoped API token 발급
v0.3.0 바우언서는 `cloudflare.NewWithAPIToken(token)` **Bearer-only**. Global API Key는 X-Auth 헤더 전용이라 `token` 필드에 그대로 못 넣음 (Bearer auth에서 `Invalid request headers` 거절 확인). 해결: global_api_key 경유로 scoped API token 신규 발급.
| 항목 | 값 |
|---|---|
| Token 이름 | `crowdsec-cloudflare-bouncer-netbis` |
| Account 스코프 | Netbis (8fcf3c7876332aba33e974cbbfdad951) 전용, All zones |
| TTL | 무기한 |
| 권한 | Account Rule Lists Write + Account Settings Read + Zone Firewall Services Write + Zone WAF Write + Zone Read |
| 저장 | Vault `secret/cloud/cloudflare-netbis` `api_token` 필드 (기존 `global_api_key` 보존) |
발급 시 CF API permission_group IDs:
- `2edbf20661fd4661b0fe10e9e12f485c` Account Rule Lists Write
- `c1fde68c7bcc44588cbb6ddbc16d6480` Account Settings Read
- `43137f8d07884d3198dc0ee77ca6e79b` Firewall Services Write
- `fb6778dc191143babbfaa57993f1d275` Zone WAF Write
- `c8fed203ed3043cba015a93ad1616f1f` Zone Read
메모: `Account.Account Filter Lists:Edit` (user 스펙 명)은 현재 CF에서 `Account Rule Lists Write`로 이름 변경됨. 기능 동일. Rulesets API 경로 사용 시 403 발생하면 `Account Rulesets:Edit` (`56907406c3d548ed902070ec4df0e328`) 추가 후 재발급.
### 5. Config 파일 전송
`/etc/crowdsec/bouncers/crowdsec-cloudflare-bouncer.yaml` — 전체 파일 push (sed -i 금지, Incus 컨테이너 내 in-place 편집 피함):
```bash
incus exec jp1:netbis-cf-bouncer --mode=non-interactive -- \
bash -c 'cat > /etc/crowdsec/bouncers/crowdsec-cloudflare-bouncer.yaml && chmod 600 /etc/crowdsec/bouncers/crowdsec-cloudflare-bouncer.yaml' \
< /tmp/crowdsec-cloudflare-bouncer.yaml
```
주요 값:
- `crowdsec_lapi_url: http://10.253.100.240:8080`
- `crowdsec_lapi_key`: cscli bouncers add 출력
- `cloudflare_config.accounts[0].token`: 위에서 발급한 scoped token
- `ip_list_prefix: crowdsec`
- `default_action: block`, 모든 zone `actions: [block]` 단일
- `total_ip_list_capacity: 10000`
### 6. 서비스 기동 + CF 리소스 자동 생성
```bash
incus exec jp1:netbis-cf-bouncer -- systemctl enable --now crowdsec-cloudflare-bouncer
```
바우언서 부팅 직후:
- CF IP List `crowdsec` (ID `b14745a3306a4f81b46d96593135f78b`) 생성 — Account Rule List
- 6개 zone에 firewall rule 자동 생성 (ip.src in $crowdsec → block)
- LAPI 스트림 첫 풀: 28,885건 decision 수신, 10,000건 CF로 push
### 7. E2E 검증
- 추가 경로: `cscli decisions add --ip 203.0.113.99 --duration 2m --type ban` → LAPI 기록 확인 → 바우언서 다음 사이클에 처리. **단 LAPI 총량 > 10k 상황에서 신규 IP는 용량 우선순위 밀려 CF 반영 보장 안 됨**. 실제 테스트에서 `1 IPs won't be inserted/kept to avoid exceeding IP list limit` 경고 발생
- 삭제 경로: CF 리스트 내 IP (`1.14.7.100`) LAPI decision 삭제 → 다음 사이클 제거. CF API 레이트리밋(10040) 영향으로 즉시 반영 아닌 다음 rate-limit window 이후 반영
- 실제 push 성공 증거: 바우언서 로그 `added 10000 new IPs and deleted 0 IPs` — 초기 sync 정상
### 8. Obsidian 커밋 + graphify
- `infra/security/crowdsec-safeline.md``### netbis-cf-firewall` 서브섹션 추가, 바우언서 역할 분담 도식
- `history/2026-04-23-netbis-firewall-bouncer-migration.md` — 이 문서
- commit: `netbis: crowdsec-cloudflare-bouncer (firewall rule) 추가 — worker bouncer와 병행`
- `/graphify update ~/obsidian`
## 운영 관찰 사항
### IP List 용량 제약
LAPI 전체 decision 수가 ~29k, CF Pro 제한 10k. 바우언서는 10k 초과분을 우선순위 선별해 push하지 않음. 결과:
- **오래된/높은 우선순위 decision은 CF 반영** (일관성 있는 차단)
- **신규 short-TTL decision은 CF 반영 불확실** (커뮤니티 CAPI 결정 다수 + 큰 TTL 누적 영향)
- 운영상 Worker Bouncer 경로가 여전히 필요한 이유 — Firewall Rule로 못 잡는 부분은 Worker Bouncer가 KV 조회로 커버
### CF API 레이트리밋 (10040)
바우언서 `update_frequency: 30s`에서 매 사이클 신규 IP 1~수건 push 시도. CF Rule List items API 제한(분당 N건)에 금방 도달. 로그에 `you have been ratelimited` 출력, 다음 사이클에서 자동 재시도.
### 향후 개선 후보
- CrowdSec LAPI `only_include_decisions_from` 필터로 CAPI 커뮤니티 feed 제외 → 차단 quality 향상하고 CF 10k 용량 효율화
- `include_scenarios_containing: ["http"]` 등 시나리오 필터로 포커스
- Enterprise zone 업그레이드 시 `total_ip_list_capacity: 50000` 가능
## 영향 및 롤백
- Worker Bouncer `netbis-cf` 무변경 — 병행 운영, 기존 Turnstile/captcha 동작 그대로
- 롤백: `systemctl stop crowdsec-cloudflare-bouncer && cscli bouncers delete netbis-cf-firewall` + CF IP List/Firewall Rule 수동 삭제 (Rule List `crowdsec` + 6 zone firewall rules)
- CF 요금 방어 효과는 다음 월 청구서에서 확인 가능

View File

@@ -0,0 +1,140 @@
---
date: 2026-04-23
topic: Netbis NPM 로그 파이프라인 — client_ip 실 IP 추출 정비
areas: [services/netbis.md, infra/security/crowdsec-safeline.md]
tags: [netbis, nginx, vector, victorialogs, real-ip, cloudflare]
---
## 배경
[[2026-04-23-netbis-npm-vl-collection|오전 구축한 NPM→zlambda→VL 파이프라인]] 의 VL 샘플을 확인한 결과 client IP 필드가 **CF edge IP** 또는 **CF Pseudo IPv4(254.x)** 로 찍혀 AI 분석·rate 집계·LAPI ban 후보 선정에 사용 불가. 실 IP 추출로 정비.
샘플 (정비 전):
```
[Client 254.152.57.112] [Real 172.64.213.21] [XFF 254.152.57.112]
Client = 254.x (CF Pseudo 또는 CF edge) — 익명화/CF 노드
```
## 현황 조사
`docker exec <npm_container> nginx -T | grep -E "log_format|real_ip_header"`:
| Host | log_format (before) | real_ip_header (before) | 비고 |
|---|---|---|---|
| npm-1 | `[Client $remote_addr]` 만 | X-Real-IP | Rewrite 무효 (CF는 X-Real-IP 안 보냄) |
| npm-2 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 — psd777 트래픽 Client=CF edge |
| npm-3 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 |
| npm-4 | `[Client $remote_addr] [Real $realip_remote_addr] [XFF $http_x_forwarded_for]` | **CF-Connecting-IP** | 이미 정상. 이 포맷을 기준으로 통일 |
| npm-5 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 |
| npm-6 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 (lepresidente image) |
`set_real_ip_from`: 6대 전부 Cloudflare + CloudFront + Docker 사설 대역 포함 (총 258 entries). **trusted proxy 쪽은 문제 없음**. 문제는 `real_ip_header` 가 X-Real-IP (CF 미매칭) 이라는 점 + log_format 에 Real/XFF 필드가 빠져있다는 점.
## 수정 — nginx (npm-1/2/3/5/6 5대)
npm-4 기준으로 `/etc/nginx/nginx.conf` 패치 + `nginx -s reload`. 패치 로직 (idempotent sed):
```bash
# log_format proxy/standard에 Real+XFF 삽입
sed -i 's|\[Client \$remote_addr\] \[Length|\[Client \$remote_addr\] \[Real \$realip_remote_addr\] \[XFF \$http_x_forwarded_for\] \[Length|g'
# real_ip_header 교체
sed -i 's|real_ip_header X-Real-IP;|real_ip_header CF-Connecting-IP;|g'
```
실행: `docker cp` out → sed → `docker cp` in → `docker exec nginx -t && nginx -s reload`.
**주의**: NPM 컨테이너의 `/etc/nginx/nginx.conf` 는 이미지 파일 레이어에 있어 컨테이너 재생성 시 원복됨. **영속화 필요 시** docker-compose `volumes:` 에 custom nginx.conf 를 host bind mount 로 추가해야 함. 본 작업은 runtime-only 패치 (재부팅/재생성 후 재적용 필요).
재실행 안전 — 패치 스크립트는 `[Real $realip_remote_addr]` 마커 체크로 중복 실행 방지.
## 수정 — Vector VRL (6대 전부)
`/etc/vector/vector.yaml``parse_npm_access` transform 재작성. 핵심:
1. **정규식 4단 fallback**: `proxy_v2` (Real+XFF 포함) → `proxy_v1` (legacy) → `standard_v2``standard_v1``raw`
2. **필드 추출**: `remote_addr`(raw Client), `cf_edge_ip`(Real = `$realip_remote_addr`, TCP peer), `xff_chain`(XFF 원본)
3. **client_ip 유도**: nginx real_ip 재작성 결과인 Client 값을 CF IP 대역(15개 CIDR)과 대조. 대역 밖이면 `client_ip = remote_addr`, 대역 안이면 `client_ip = null` + `client_ip_is_cf_edge=true` + `client_ip_source="cf_edge_rewrite_failed"` (rewrite 실패 케이스 명시)
4. CF Pseudo IPv4 `254.0.0.0/8` 은 CF CIDR 리스트에 **포함 안 함** — IPv6 방문자에 대한 CF 매핑 ID로 동작하므로 식별자로 유효
CF IPv4 CIDR 리스트 (VRL 내 inline):
```
173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22,
141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20,
197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13,
104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22
```
배포: 6대 전부 `vector.yaml` 재작성(전체 파일 푸시), `systemctl kill -s SIGKILL vector && reset-failed && start` (기존 inflight retry 회피).
## 검증
LogsQL (VL):
```
service:npm log_format:proxy_v2 _time:3m # 총 1000+ 건
service:npm log_format:proxy_v2 client_ip_is_cf_edge:true _time:3m # 0 건
```
샘플 (npm-2 psd777.com, 정비 후):
```json
{
"host": "npm-2",
"domain": "psd777.com",
"remote_addr": "182.208.67.53",
"client_ip": "182.208.67.53",
"cf_edge_ip": "172.70.123.209",
"xff_chain": "182.208.67.53",
"client_ip_is_cf_edge": "false",
"client_ip_source": "remote_addr",
"log_format": "proxy_v2"
}
```
상위 client_ip (5분 윈도우):
```
77 106.101.11.143 (KR)
62 106.101.2.46 (KR)
44 247.138.125.110 (CF Pseudo, IPv6)
42 251.48.101.40 (CF Pseudo)
37 254.205.147.150 (CF Pseudo)
36 182.225.199.226 (KR)
35 247.242.90.88 (CF Pseudo)
34 251.166.27.129 (CF Pseudo)
33 250.160.168.101 (CF Pseudo)
30 121.131.161.96 (KR)
```
전부 **실 클라이언트** (KR IPv4 또는 CF Pseudo). CF edge (172.64.x, 162.158.x, …) 하나도 없음.
호스트별 proxy_v2 수신량(3분 창):
```
npm-1: 0 (shared/no traffic)
npm-2: 317 (psd777 실트래픽)
npm-3: 510 (rss-555/7790)
npm-4: 562 (fall-vip/mvp/vip7)
npm-5: 0
npm-6: 0
```
## 운영 주의
- **NPM 컨테이너 재생성 시 nginx 패치 소실** — `docker-compose down && up` 후에는 log_format + real_ip_header 원복. 영속 대응: docker-compose에 nginx.conf custom mount 추가 (후속 과제)
- **Vector VRL에서 `ip_cidr_contains` 호출** — VRL 0.12+ 에서 제공. Vector 0.55 기준 사용 가능
- **`client_ip_is_cf_edge=true` 모니터링**: 이 플래그가 진실이면 nginx real_ip 재작성 실패 의미. 플래그 상승 추이 감지 시 원인 조사 (CF IP 대역 추가 / `real_ip_recursive` off / CF-Connecting-IP 헤더 누락 등)
- **Pseudo IPv4 254/247/250/251.x**: CF Enterprise 없는 플랜에서 IPv6 방문자 → IPv4 역양식. 동일 방문자에 대해 stateful하므로 rate limit/ban 대상으로 사용 가능
- **CF Pseudo 차단 시 부수효과**: CF Pseudo IP 는 같은 CF 지역 방문자 여럿이 공유할 수 있음 (매핑 해시에 따라). 차단 결정 시 XFF chain 교차 확인 권장
## 후속 과제
- [ ] NPM docker-compose에 custom nginx.conf bind mount 추가 (재생성 영속화)
- [ ] `client_ip_is_cf_edge=true` 알람 — VictoriaMetrics LogsQL alert (임계: 시간당 10건)
- [ ] CrowdSec scenarios/parser에 `client_ip` 필드 매핑 — VL `service:npm` acquisition 추가 시 IP 기반 bouncer 판단 가능
- [ ] `xff_chain` 다중 IP 케이스 (여러 proxy 경유) 전용 파싱 — 현재는 문자열 그대로 저장
## 롤백
```bash
# nginx original:
# sed -i '[Real $realip_remote_addr] [XFF $http_x_forwarded_for] ' 제거 + real_ip_header CF-Connecting-IP → X-Real-IP
# Vector: vector.yaml에서 client_ip 관련 블록 제거, parse_regex 를 v1 만 사용
```

View File

@@ -0,0 +1,154 @@
---
date: 2026-04-23
topic: Netbis NPM 6대 → VictoriaLogs 로그 수집 파이프라인 구축 (zlambda Vector 중계)
areas: [services/netbis.md, infra/platform/victorialogs.md, infra/security/crowdsec-safeline.md]
tags: [netbis, vector, victorialogs, npm, zlambda, observability, nixos]
---
## 배경 / 결정
Netbis 오리진 6대(NPM, Linode Tokyo public)의 nginx access/error log를 사내 VictoriaLogs(`vl.inouter.com`)로 수집. 목적: 향후 CrowdSec 파싱/anomaly-detect 연동 + 요청 패턴 모니터링.
**핵심 제약**: `vl.inouter.com`의 public DNS가 LAN IP(192.168.9.53)으로만 해석되어 public NPM들이 직접 도달 불가. 해결로 zlambda(Tailscale 100.78.51.18 / public 139.162.71.52) 를 **Vector HTTP 중계** 로 투입.
선택지 검토:
- (A) 6대에 Tailscale 설치 — 방침상 탈락 (설치 불가)
- (B) zlambda Vector 중계 — **선택** (기존 NixOS 플레이크에 모듈 추가)
- (C) VL public 엔드포인트 노출 — 공격면 확대 우려 탈락
## 최종 구조
```
┌────────── public internet ──────────┐ ┌── tailnet ──┐
NPM-1..6 (Linode Tokyo) │ │ │
Vector 0.55 (host, file source) │ │ │
http sink POST:9999 (basic auth) ├─► zlambda Vector-relay 0.45 ─► vl.inouter.com
│ HTTP server bearer=basic │ (K3s Traefik → vlogs svc)
│ ES bulk sink │
└─────────────────────────────┘
```
- **NPM Vector**: 호스트-레벨. `/etc/vector/vector.yaml` (mode 600, bearer 평문), `vector.service`
- **중계**: zlambda `vector-relay` 컨테이너 (NixOS oci-container, docker network `vector-net`)
- **VL ingest**: `https://vl.inouter.com/insert/elasticsearch` (Tailscale로 LAN 도달, Traefik TLS)
## 구현
### 1. zlambda vector.nix 모듈
`~/nixos-infra/vector.nix` 신규 작성. 요점:
- `virtualisation.oci-containers.containers.vector-relay``docker.io/timberio/vector:0.45.0-debian`, `--network=vector-net`, `ports=[9999:9999]`
- `systemd.services.vector-relay-render-config` oneshot — `/var/lib/vector-relay/vector.yaml` 템플릿 렌더 (env interpolation `${VECTOR_BEARER_TOKEN}`)
- `systemd.services.vector-relay-env` oneshot — agenix 복호 결과(`/run/agenix/vector-bearer-token`) 를 `/run/vector-relay/env` 로 이동 후 container에 `environmentFiles` 주입
- `systemd.services.init-vector-net` oneshot — docker network 생성
- `age.secrets.vector-bearer-token.file = ./secrets/vector-bearer-token.age` (상대경로, pure eval 호환)
Vector 컨테이너 config:
```yaml
sources.http_npm: { type: http_server, address: 0.0.0.0:9999, encoding: ndjson,
auth: { strategy: basic, username: npm-relay, password: "${VECTOR_BEARER_TOKEN}" } }
transforms.tag_relay: { type: remap, source: | .relay = "zlambda" }
sinks.vlogs: { type: elasticsearch, endpoints: [https://vl.inouter.com/insert/elasticsearch],
mode: bulk, healthcheck.enabled: false, query._stream_fields: [host, service, log_type] }
```
### 2. agenix bearer token
```bash
# 64자 URL-safe base64 난수 생성 후 age 로 2 recipient 암호화
age -r "<kaffa user ed25519>" -r "<zlambda host ed25519>" \
-o secrets/vector-bearer-token.age < bearer.secret
```
`secrets/secrets.nix``"vector-bearer-token.age".publicKeys = allUsers ++ allHosts;` 추가.
### 3. flake/config 배선
- `configuration.nix` `imports``./vector.nix` 추가
- `configuration.nix` `users.users.root.openssh.authorizedKeys.keys`**heimdall `ops-agents@kaffa` ed25519 공개키** 추가 (runtime 임시 등록을 flake 영속화)
- `git add vector.nix secrets/vector-bearer-token.age secrets/secrets.nix configuration.nix`
- `nixos-rebuild switch --flake .#zlambda`
### 4. Linode 방화벽 (zlambda id 691875)
```
allow-npm-relay-9999:
protocol TCP, ports 9999, action ACCEPT
addresses.ipv4: [172.104.100.11/32, 139.162.114.197/32, 139.162.73.17/32,
139.162.73.240/32, 172.104.70.137/32, 172.105.226.218/32]
```
기존 rules(SSH, CF HTTP/HTTPS, Tailscale UDP 41641, ICMP) 보존. inbound_policy=DROP 유지.
### 5. NPM 6대 Vector 설치
`setup.vector.dev` (공식 sh 스크립트)로 `/usr/local/bin/vector` 0.55 설치. systemd unit 생성 후 enable+start.
각 호스트 `/etc/vector/vector.yaml` (mode 600):
- **sources.npm_access / npm_error**: file tail
- NPM-1..5: `/root/data/logs/proxy-host-*_access.log`
- NPM-6: `/home/kaffa/npm/data/log/...` (다른 install)
- **transforms.parse_npm_access / parse_npm_error**: remap (VRL)
- NPM proxy log_format 정규식 파싱 → ip/method/path/status/bytes/domain/upstream/UA/referer 구조화
- 실패 시 NPM standard log_format 재시도, 그래도 실패하면 `log_format="raw"`
- 공통 필드: `.service="npm"`, `.host="<label>"`, `.zone="<served zones csv>"`, `.log_type=access|error`
- 파일명에서 `proxy_host_id` 추출
- **sinks.relay**: `type: http`, `uri: http://139.162.71.52:9999/`, `auth.strategy: basic (user=npm-relay, password=<bearer>)`, `encoding.codec: json`, `framing.method: newline_delimited`, `batch: {max_events:100, timeout_secs:5}`, `healthcheck.enabled: false`
### 6. 호스트별 라벨
| Host | Public IP | 로그 경로 | `zone` 라벨 |
|---|---|---|---|
| npm-1 | 172.104.100.11 | /root/data/logs | shared |
| npm-2 | 139.162.114.197 | /root/data/logs | psd777.com |
| npm-3 | 139.162.73.17 | /root/data/logs | rss-555.com,rss-7790.com |
| npm-4 | 139.162.73.240 | /root/data/logs | fall-vip.com,fall-mvp.com,fall-vip7.com |
| npm-5 | 172.104.70.137 | /root/data/logs | shared |
| npm-6 | 172.105.226.218 | /home/kaffa/npm/data/log | shared |
## 검증
각 NPM에서 합성 로그 주입 후 `https://vl.inouter.com/select/logsql/query?query=service:npm host:"<host>"` 로 확인:
```
npm-1 rows>=9 (probe + 실 요청)
npm-2 rows=1000 (cap, 실 트래픽 다수)
npm-3 rows=1000
npm-4 rows=1000
npm-5 rows>=1 (probe, 조용한 호스트)
npm-6 rows>=1 (probe)
```
VL 문서에서 확인된 필드 샘플(npm-1 sythetic):
```json
{"host":"npm-1","service":"npm","zone":"shared","log_type":"access","log_format":"proxy","relay":"zlambda",
"domain":"h.test","ip":"127.0.0.1","method":"GET","path":"/debug-test","status":"200",
"upstream_cache_status":"-","upstream_status":"200","user_agent":"heimdall-debug-v","referer":"-","bytes":"0",
"proxy_host_id":null,"file":"/root/data/logs/fallback_access.log"}
```
## 운영 주의
- **NPM 측 bearer 평문**: `/etc/vector/vector.yaml` 내부. 호스트 compromised 시 노출 → zlambda 방화벽 IP allowlist + basic auth 2중 방어. 노출 탐지 시 agenix 재발급 + 재배포
- **zlambda 측 호환성**: `vector 0.45` 의 http_server source는 `auth.strategy: basic` 만 지원. `bearer` strategy 미지원이라 basic + fixed user(`npm-relay`) 로 구현. 기능적 차이 없음
- **Vector http sink 그레이스풀 종료 지연**: 오래된 elasticsearch sink(이전 config)를 `systemctl restart vector` 할 때 inflight retry로 최대 60초 대기. 빠른 재기동 필요 시 `systemctl kill -s SIGKILL vector && systemctl reset-failed vector && systemctl start vector`
- **VL endpoint TLS**: `http://vl.inouter.com/insert/elasticsearch` 는 404 반환(Traefik HTTPS-only routing). 반드시 `https://`
- **checkpoint 위치**: `/var/lib/vector/npm_{access,error}/checkpoints.json`. 로그 재수집이 필요하면 해당 파일 삭제 후 vector 재시작
## 영향 및 롤백
- NPM 부하: Vector agent ~20MB RSS, negligible
- zlambda 부하: 중계 컨테이너 ~30MB, 6 NPM 집계 트래픽 ≪ 기존 APISIX DR
- VL 스토리지: index `npm-netbis`, 초당 수십 건 수준 예상
- 롤백:
1. 각 NPM `systemctl disable --now vector` + `apt purge` (or `/usr/local/bin/vector` 삭제 + unit 제거)
2. zlambda `configuration.nix` imports 에서 `./vector.nix` 제거 + `nixos-rebuild switch`
3. Linode 방화벽 `allow-npm-relay-9999` rule 삭제
4. agenix `secrets/vector-bearer-token.age` 제거
## 관련 문서
- [[../services/netbis|netbis]] — NPM 구성, 라우트, CrowdSec 연동
- [[../infra/platform/victorialogs|victorialogs]] — VL 구조 + LogsQL
- [[../infra/security/crowdsec-safeline|crowdsec-safeline]] — 기존 로그 파이프라인(APISIX, SafeLine)

View File

@@ -0,0 +1,109 @@
---
title: CF Pseudo IPv4 (Class E 240/4) 정체 규명
updated: 2026-04-24
tags: [cloudflare, netbis, log-analysis, incident]
---
## 배경
Netbis NPM → zlambda → VictoriaLogs 로그 파이프라인이 2026-04-23 구축 + 실 client IP 추출 VRL transform 정비 완료 직후, VL top client_ip 집계에서 **240.0.0.0/4 (클래스 E, "Reserved for Future Use") 대역이 전체 트래픽의 45%**를 차지하는 현상 관찰.
## 초기 오진
- 처음에는 "한국 모바일 통신사의 CGNAT 240/4 활용" 으로 추정하고 답변
- 근거로 UA 95%+ 모바일 + KR 앱 브랜드(KakaoTalk/Daum/NAVER)
- **이 추정은 틀렸음.** 공식 문서·RFC·APNIC/IETF 검색 결과 KR 통신사가 240/4를 CGNAT에 사용한다는 기록 없음
## 실제 원인 — CF Pseudo IPv4
Cloudflare 공식 [Pseudo IPv4 문서](https://developers.cloudflare.com/network/pseudo-ipv4/) 및 [Support 문서](https://support.cloudflare.com/hc/en-us/articles/229666767) 확인:
> "Cloudflare uses Class E IP space (240.0.0.0 - 255.255.255.255) for these addresses. The Pseudo IPv4 service uses a hashing algorithm applied to top 64 bits of the connecting IPv6 address"
> "overwrite the existing Cf-Connecting-IP and X-Forwarded-For headers with a Pseudo IPv4 address"
즉:
```
IPv6 클라이언트 → CF 엣지
→ IPv6 상위 64비트 MD5 해시 → 240/4 매핑
→ CF-Connecting-IP 및 X-Forwarded-For 헤더 값 overwrite
→ NPM의 real_ip_header CF-Connecting-IP 설정 → $remote_addr에 240/4 기록
→ Vector VRL → VL의 client_ip 필드에 240/4 저장
```
Netbis 조건과 완벽 일치:
- 백엔드 Apache 2.4.38 + PHP 5.6.40 (IPv6 지원 미흡한 레거시)
- 모바일 트래픽 비율 높음 (모바일 = IPv6 비율 높음)
- CF 대시보드에서 Pseudo IPv4 Overwrite headers 모드 활성 상태
## 검증 증거
로그 필드 샘플 5건 공통 패턴:
```
client_ip = 240/4 범위 값
xff_chain = 동일 값 (CF가 두 헤더 모두 overwrite)
cf_edge_ip = 172.70.x 등 CF 대역
client_ip_source = remote_addr
```
UA 분포 (1h, 240/4 IP 한정):
```
Android (Chrome): 15,469 (67%)
iPhone (Safari): 4,694 (20%)
Daum/Kakao 앱: 1,512
KakaoTalk: 962
NAVER: 573
Windows: 591 (2.6%)
Mac: 3 (0.01%)
```
데스크톱 2.6%뿐 — 모바일 IPv6 비율 높음과 부합.
## 함의
### 차단 영향 범위
- 240/4 IP 하나 = **단일 IPv6 /64 prefix 전체**의 해시 결과
- 한 가정 IPv6 `/64`는 수십~수백 디바이스·수년 SLAAC 주소 포함
- 따라서 **240/4 IP ban = 한 가정/기업의 모든 IPv6 디바이스 장기간 차단 위험**
- CGNAT(수천 유저)보다는 좁지만 단일 IP로 간주하면 오탐 증폭
### 봇 탐지 관점
- 역설적으로 현재 Overwrite 모드가 IPv6 봇넷 탐지에 유리
- IPv6 프라이버시 확장으로 수천 주소 돌리는 봇도 같은 /64면 같은 240/4 하나로 collapse
- 진짜 IPv6를 받았다면 매번 다른 주소라 탐지 훨씬 어려웠을 것
- 단 /64 차단 = 해당 prefix의 정상 유저 동반 차단 → ban duration을 짧게(10~30분) 유지 권장
### 시나리오/ML feature
- 240/4 IP를 "가상 식별자"로 그대로 사용 가능 (결정적 해시이므로)
- CGNAT 플래그로 분류 금지 — 잘못된 분류로 처리 로직 왜곡됨
- 필요 시 Pseudo IPv4 모드를 "Add header"로 전환해 실 IPv6 확보 가능 (백엔드 IPv6 처리 여부 선행 확인 필수)
## 대안 경로 (추후 결정사항)
| 조치 | 효과 | 리스크 |
|------|------|-------|
| 현상 유지 (Overwrite) | 레거시 백엔드 호환 유지 | 240/4 해석·ban 설계에 주의 필요 |
| Add header 모드 전환 | CF-Connecting-IP에 실 IPv6, 별도 헤더에 240/4 | PHP 5.6 IPv6 주소 문자열 처리 검증 필요 |
| 백엔드 IPv6 업그레이드 | 근본 해결 | 애플리케이션 호환 작업 대규모 |
## 정본 반영
- `infra/security/cloudflare.md``Pseudo IPv4 (Class E 240/4)` 섹션 신규
- `services/netbis.md` — client_ip 의미 설명 정정 (254.x → 240/4, 메커니즘 링크)
## 참고 자료
- [CF Pseudo IPv4 공식 문서](https://developers.cloudflare.com/network/pseudo-ipv4/)
- [CF Support 문서](https://support.cloudflare.com/hc/en-us/articles/229666767)
- [APNIC Blog: Looking for 240/4 addresses](https://blog.apnic.net/2024/09/10/looking-for-240-4-addresses/)
- [benjojo: Class E Addresses in the Real World](https://blog.benjojo.co.uk/post/class-e-addresses-in-the-real-world)
- [IETF draft-schoen-intarea-unicast-240](https://datatracker.ietf.org/doc/draft-schoen-intarea-unicast-240/)
## 오진 교훈
- UA 분포만으로 "CGNAT"로 단정 금지 — Pseudo IPv4가 모바일 IPv6 해시 결과이므로 같은 시그널이 나옴
- 예약 대역 IP 관찰 시 먼저 CF 엣지 기능(Pseudo IPv4, IP anonymization 등) 가능성부터 확인
- 공식 벤더 문서 확인이 UA·트래픽 패턴 추론보다 선행되어야 함

View File

@@ -0,0 +1,62 @@
---
title: anomaly-detect 폐기 (오탐 다수)
updated: 2026-04-25
tags: [security, crowdsec, anomaly-detect, deprecation]
---
# anomaly-detect 인스턴스 완전 제거 (2026-04-25)
## 결정
[[../infra/platform/anomaly-detect|anomaly-detect]] (Grok-4-fast agentic 분석기)를 컨테이너 포함 완전 제거.
## 폐기 사유
CrowdSec active decision 점검 중 anomaly-detect가 발급한 ban이 사실상 모두 오탐임을 확인.
### 오탐 사례 (2026-04-19 ~ 2026-04-24 alerts metrics 기준)
| ban된 IP | 시나리오 | 의심 사유 |
|---|---|---|
| `1.1.1.1`, `2.2.2.2`, `3.3.3.3`, `4.4.4.4`, `5.5.5.5` | path-enumeration | 명백한 시퀀스/예시 IP. 1.1.1.1은 Cloudflare DNS |
| `1.2.3.4`, `5.6.7.8`, `9.10.11.12`, `13.14.15.16` | path-enumeration | 튜토리얼/예제용 더미 IP 패턴 |
| `172.70.242.122` | path-enumeration | **Cloudflare 엣지 IP 대역** (172.70.0.0/16). real IP 추출 실패 의심 |
| `45.79.164.218`, `45.79.207.123`, `45.79.218.123/124`, `45.79.245.123` | path-enumeration | **Linode 도쿄 IDC 대역** — Netbis NPM 오리진과 같은 IDC. 자체 인프라 가능성 |
| `203.133.168.226/227/228` | path-enumeration | 동일 ASN 연속 IP — NAT 통합 환경과 공격 구분 실패 |
같은 기간 hub 시나리오는 단 1건(`103.215.74.213`, India SoloRDP)만 잡았고 그것이 진짜 스캐너였음 (`http-probing` + `http-sensitive-files` + `http-crawl-non_statics` 동시 매칭).
### 추정 원인
1. **Grok-4-fast의 LogSQL 결과 해석 오류**: APISIX access log에 합성/health-check/tutorial 트래픽이 섞여있고, agent가 이를 "여러 IP가 같은 path 열거"로 분류
2. **real IP 추출 검증 부재**: `172.70.x.x` ban이 통과한 건 Cloudflare 엣지 IP를 client_ip로 받았다는 의미. APISIX `real_ip_from`에 CF 엣지 대역이 빠진 가능성
3. **자체 인프라 whitelist 부재**: Linode IDC `45.79.x` 대역이 화이트리스트에 없음
## 작업 내역 (헤임달 위임)
| 단계 | 명령 | 위치 |
|---|---|---|
| 1 | `systemctl stop anomaly-detect.timer anomaly-detect.service` + `disable` | incus-hp2 / `anomaly-detect` 컨테이너 |
| 2 | `cscli decisions delete --origin crowdsec` (활성 2건 해제: `45.94.31.74`, `45.76.123.45`) | jp1 incus / `crowdsec` 컨테이너 |
| 3 | `incus stop anomaly-detect; incus delete anomaly-detect` | incus-hp2 |
| 4 | `cscli machines delete <anomaly-detect watcher>` | jp1 incus / `crowdsec` 컨테이너 |
## 보존 항목
- **Vault `secret/ai/openrouter`**: OpenRouter API 키. 다른 서비스가 공용으로 사용할 가능성이 있어 삭제하지 않고 보존.
- **Gitea `kaffa/anomaly-detect` private repo**: 코드 reference로 보존 (재구축 시 참고).
## 향후 재가동 조건
재도입 시 다음 항목 선행 필수:
1. APISIX access log 합성/health-check 트래픽 사전 필터링 (Vector transform 단계)
2. APISIX `real_ip_from`에 Cloudflare 엣지 대역 (`173.245.48.0/20`, `103.21.244.0/22`, `103.22.200.0/22`, `103.31.4.0/22`, `141.101.64.0/18`, `108.162.192.0/18`, `190.93.240.0/20`, `188.114.96.0/20`, `197.234.240.0/22`, `198.41.128.0/17`, `162.158.0.0/15`, `104.16.0.0/13`, `104.24.0.0/14`, `172.64.0.0/13`, `131.0.72.0/22`) 추가 검증
3. Linode 도쿄 IDC 대역 (45.79.x 등 자체 인프라 IDC) whitelist 등록
4. dry_run으로 최소 1주 운영 후 임계값 조정
## 참고
- 정본 stub: [[../infra/platform/anomaly-detect]] (deprecated marker만 남김)
- 이전 설계 반복 이력: [[2026-04-08-anomaly-detect-iterations]]
- CrowdSec LAPI 정본: [[../infra/security/crowdsec-safeline]]

View File

@@ -0,0 +1,90 @@
---
title: Netbis CF Firewall Bouncer 재구축 + sigmatch v2.4 + VL acquisition 통합
updated: 2026-04-25
tags: [netbis, crowdsec, cloudflare, sigmatch, vector, history]
---
## 배경
2026-04-23 [[2026-04-23-netbis-bouncer-removal|netbis bouncer 전량 폐기]] 이후 netbis CF 측엔 enforcement 컴포넌트가 비어 있었음. 그동안 [[../projects/netbis-sigmatch|netbis-sigmatch]]가 자체 detect + 자체 enforcement (CF Account-level IP Access Rules 직접 호출) 모델로 차단을 시도했으나, 다음 문제들로 LIVE 전환 보류:
1. **2026-04-24 webhook callback 오탐**: 베팅 프로바이더 webhook (`/vinus/cback?bet_<hash>`)이 path entropy 기반 scanner_shape에 매칭. 어제 fix 적용 (path normalize, query string 제거)
2. **2026-04-25 attack_contributor 누적 오탐**: A4 Matrix Profile trigger의 1% false positive가 단순 volume top 차단 단계에서 증폭되어 18시간 동안 99 distinct IP 누적 차단 (webhook 4~5개 + KR 일반 유저 + IPv6 모바일 240/4 가짜 IP). LIVE였으면 베팅 콜백 중단 사고
이로 인해 sigmatch 자체 detect 모델 한계 확인 → CrowdSec 시나리오 기반 공식 도구로 enforcement 이전 결정.
## 변경 요약
### 1. sigmatch v2.4 — A4 Matrix Profile 트리거 제거
- `loop.py`에서 `compute_mp_trigger` 호출 제거. `mp_seg` 로그 포맷 제거. `--mp-*` argparse 옵션은 deprecated 표기로 호환만 유지
- `matrix_profile.py` 모듈은 보존 (관찰/연구용 수동 호출 가능)
- 활성 103건 sqlite 정리 후 재시작
- 사유: MP self-similar 시계열 가정이 daily seasonality 트래픽과 안 맞고, 단일 시그널이 attack_contributor 단계의 단순 volume top 차단으로 증폭되는 위험 패턴
상세: [[../projects/netbis-sigmatch|projects/netbis-sigmatch.md]]
### 2. VL → CrowdSec acquisition 통합
기존 분리:
- `victorialogs-apisix.yaml`
- `victorialogs-traefik.yaml`
신규 통합 (단일 파일):
- `/etc/crowdsec/acquis.d/victorialogs-nginx.yaml`
- query: `(program:apisix AND log_type:access) OR program:traefik OR (program:npm AND log_type:access)`
- labels: `type: nginx`
- 이전 파일 3개는 `.bak`로 보존 (롤백 가능)
같은 VL endpoint에 같은 label, query만 OR 결합 가능했음. connection 1개 + 메트릭 단일 source.
### 3. Netbis NPM 로그 → CrowdSec 신규 연동
- 기존: NPM → Vector → zlambda → VL (수집만)
- 신규: 위 통합 acquisition에 합류 → nginx-logs parser → 시나리오 → LAPI decision
- nginx-logs parser는 NPM proxy format 호환 (cscli explain 결과 s01-parse 정상, 시나리오 매칭됨)
### 4. CF Firewall Rule Bouncer 재구축
| 항목 | 값 |
|---|---|
| 패키지 | `crowdsec-cloudflare-bouncer 0.3.0` (apt) |
| 컨테이너 | jp1 incus `crowdsec` (LAPI 동거) |
| LAPI bouncer 이름 | `cs-cloudflare-bouncer-1777082222` |
| origin filter | `[crowdsec, cscli]` |
| API token | Vault `secret/cloud/cloudflare-netbis.firewall_bouncer_token` (Bearer cfut_…). global_api_key는 사용 불가 (`6003 Invalid request headers`) |
| Token 권한 | Account Firewall Access Rules Write, Account Rule Lists Write, 6 zone Firewall Services Write |
| CF 리소스 | IP List `crowdsec_managed_challenge`(`f728ad9d…`) + 6 zone Firewall Rule (managed_challenge) |
### 5. 폐기 사유 회피 검증
2026-04-23 폐기 사유였던 **CF IP List 10k 한도**가 origin filter로 회피됨:
| origin | LAPI 24h 누적 | CF List 푸시 |
|---|---|---|
| CAPI (커뮤니티) | 30k+ | ❌ 제외 |
| lists (tor 등) | 2,195 | ❌ 제외 |
| crowdsec (로컬) | 30+ | ✅ 푸시 |
| cscli (수동) | 0 | ✅ 푸시 |
→ CF IP List에 한도 (10k) 이내로만 push. 향후 로컬 시나리오 폭주에 대비 sigmatch처럼 max_rules 안전장치 검토 가치 있음.
Worker bouncer는 트래픽 비례 비용 ($14~50/월 추정)이라 origin filter로 회피 불가 → **재구축 안 함**.
## 영향 받은 정본
- [[../projects/netbis-sigmatch|projects/netbis-sigmatch.md]] — v2.4
- [[../infra/security/crowdsec-safeline|infra/security/crowdsec-safeline.md]] — VL 통합 acquisition + netbis-cf-firewall 섹션
- [[../services/netbis|services/netbis.md]] — bouncer 재구축 정정, API token 정보
## 미해결 / 다음 결정
- **sigmatch 역할 재정의**: 자체 detect + enforcement → CrowdSec이 이걸 담당하므로 sigmatch는 (a) 폐기, (b) 관측/대시보드 전용, (c) 분포 anomaly 보조 (CrowdSec 시나리오로 못 잡는 영역) 중 결정 필요
- **분산 DDoS 시나리오 미설치**: `http-ddos-by-asn`, `http-ddos-by-country` 같은 AS/Country 단위 시나리오 미설치. 필요 시 추가
- **bouncer rename**: `cs-cloudflare-bouncer-1777082222``netbis-cf-firewall` 같은 의미 있는 이름. cscli 또는 config 수정으로 가능
## 운영 검증 시점 (작업 직후)
- LAPI bouncer list: `cs-cloudflare-bouncer-1777082222` ✔️ valid
- CF IP List `crowdsec_managed_challenge` count: 1 (origin filter 정상 — 30k+ CAPI 제외 확인)
- 6 zone Firewall Rule 모두 `managed_challenge` 액션으로 생성됨

View File

@@ -0,0 +1,119 @@
---
date: 2026-04-25
topic: NPM Vector `_msg` 재합성 — proxy_v2 → 표준 nginx combined
areas: [services/netbis.md, infra/security/crowdsec-safeline.md, infra/platform/victorialogs.md]
tags: [netbis, vector, vrl, crowdsec, nginx-logs, observability]
---
## 배경
[[2026-04-23-netbis-npm-vl-collection|NPM → zlambda → VL 파이프라인]] + [[2026-04-23-netbis-npm-client-ip-fix|client_ip 정비]] 완료 후 CrowdSec 파서 통과율 확인 결과 `child-crowdsec/nginx-logs` unparsed 84%. 원인: NPM `proxy_v2` raw 포맷(`[Client x] [Real y] [XFF z] ...`)이 hub 파서의 `NGINXACCESS` grok과 매칭되지 않음. cscli explain으로 100% 실패 확인.
이전 정본에 `nginx-logs parser가 NPM proxy format도 호환 처리 (lenient)` 라고 잘못 기록되어 있던 부분 정정 — 호환 안 됨, Vector 측에서 표준 포맷으로 재합성하는 방식 채택.
## 결정
NPM Vector `parse_npm_access` remap 끝부분에 `_msg` 재작성 블록 추가. 이미 추출된 메타필드를 활용해 표준 nginx combined 포맷을 합성하여 `.message` 에 덮어쓰기. 원본은 `.original_message` 에 백업.
대안 비교:
- (A) **Vector 측 재합성** ← 선택. 모든 다운스트림(VL, CrowdSec, anomaly-detect 등)에 표준 포맷 일괄 공급
- (B) CrowdSec 측 custom 파서 작성 — child-nginx-logs를 NPM proxy_v2용으로 분기. 분기 추가시마다 유지보수 부담
- (C) NPM nginx 측 log_format을 표준 combined로 변경 — Real/XFF 메타정보 손실, IP 추적 불가
## 변환 로직
```vrl
lf = .log_format
if lf != "raw" {
ip_v = .client_ip
if ip_v == null { ip_v = .remote_addr }
ip_s = to_string(ip_v) ?? "-"
if ip_s == "" { ip_s = "-" }
m_s = to_string(.method) ?? "-"
...
ts_s = to_string(.log_time) ?? "" # NPM $time_local
if ts_s == "" {
ts_v = .timestamp # fallback: Vector ingest time
if is_nullish(ts_v) { ts_v = now() }
fmt, fmt_err = format_timestamp(ts_v, "%d/%b/%Y:%H:%M:%S %z")
if fmt_err == null { ts_s = fmt } else { ts_s = format_timestamp!(now(), "%d/%b/%Y:%H:%M:%S %z") }
}
.original_message = .message
.message = ip_s + " - - [" + ts_s + "] \"" + m_s + " " + p_s + " HTTP/1.1\" " + st_s + " " + b_s + " \"" + r_s + "\" \"" + u_s + "\""
}
```
각 parse 분기에서 `.log_time = m.ts` 도 추가해 NPM 의 `$time_local` 보존 (재합성 시 `[$time_local]` 자리에 사용).
raw 포맷(파싱 실패)은 재작성 안 함 — `_msg` 그대로 둬서 디버깅 단서 보존.
## Vector 검증 시 함정
1. **YAML 주석 내 `$variable` 표기** → Vector env-var interpolation 트리거 → 검증 실패. `$$variable` 로 escape 필요. `$time_local`, `$body_bytes_sent` 등 nginx 변수 표기 모두 영향
2. **infallible 표현식에 `??` 사용** → VRL 컴파일 에러 (`E651: unnecessary error coalescing operation`). 예: `to_string(.log_format)` 가 모든 분기에서 string으로 설정되었음을 컴파일러가 추론 → `??` 가 dead code → 직접 `.log_format` 참조로 변경
## npm-1 단독 검증
합성 라인 주입:
```
[25/Apr/2026:06:10:56 +0000] - 200 200 - GET http heimdall.test "/transform-verify" [Client 203.0.113.55] [Real 172.71.24.1] [XFF 203.0.113.55] [Length 1234] [Gzip 1.5] [Sent-to 42.0.0.1] "heimdall-transform-test/1.0" "https://heimdall.test/ref"
```
VL 도달 후 `_msg`:
```
203.0.113.55 - - [25/Apr/2026:06:10:56 +0000] "GET /transform-verify HTTP/1.1" 200 1234 "https://heimdall.test/ref" "heimdall-transform-test/1.0"
```
`cscli explain --type nginx` (jp1:crowdsec):
```
├ s01-parse
| └ 🟢 crowdsecurity/nginx-logs (+23 ~2)
├ s02-enrich
| ├ 🟢 crowdsecurity/dateparse-enrich (+2 ~2)
| ├ 🟢 crowdsecurity/geoip-enrich (+9)
| ├ 🟢 crowdsecurity/http-logs (+7)
├-------- parser success 🟢
├ Scenarios
├ 🟢 crowdsecurity/http-crawl-non_statics
├ 🟢 custom/apisix-high-rate-per-ip
└ 🟢 custom/apisix-single-path-flood
```
s01-parse 통과 + 3개 시나리오 매칭.
## 6대 롤링 배포
npm-1 → 5대(npm-2..6) 순차. 각 호스트 `vector validate` 통과 후 `systemctl kill -SIGKILL && reset-failed && start`.
VL 샘플 확인 (host별):
| Host | 샘플 _msg (앞 150자) |
|---|---|
| npm-1 | `203.0.113.55 - - [25/Apr/2026:06:10:56 +0000] "GET /transform-verify HTTP/1.1" 200 1234 ...` |
| npm-2 | `255.95.21.9 - - [25/Apr/2026:06:13:12 +0000] "POST /main/betIt HTTP/1.1" 200 56 ...` |
| npm-3 | `170.187.231.160 - - [25/Apr/2026:06:13:12 +0000] "POST /vinus/cback?bet_..." 200 85 "-" "-"` |
| npm-4 | `254.5.9.221 - - [25/Apr/2026:06:13:14 +0000] "POST /wel/login.html HTTP/1.1" 200 90 "https://fall-vip.com/wel.html" ...` |
전부 표준 nginx combined 포맷.
## 30분 후 검증
baseline 캡처 (06:13:40Z):
```
victorialogs source: 977.24k read / 514.72k parsed / 462.52k unparsed (47.3% unparsed)
child-crowdsec/nginx-logs: 3.29M hits / 514.72k parsed / 2.78M unparsed (84.5% unparsed)
```
30분 후 cscli metrics 재캡처 → Δ window 비율 계산 → Discord webhook 푸시 (백그라운드 스크립트 예약).
## 운영 주의
- **`.original_message` 보존** — 디버깅·재처리 용도. 재합성 결과가 이상하면 original 로 진단 가능
- **로그 회전 안전성** — VRL 변경은 새 라인부터 적용. 기존 unparsed 누적은 그대로 (cscli metrics는 cumulative). delta 비교에서만 효과 보임
- **Vector restart 전략** — 기존 inflight ES sink 때문에 graceful shutdown 60s 대기. 빠른 전환 필요 시 SIGKILL → reset-failed → start
- **추가 영향** — VL 인덱스에 `_msg` 가 표준 포맷으로 들어가서 LogsQL 쿼리 패턴이 단순해짐. 동시에 `.original_message` 가 별도 필드로 추가되어 retention 비용 약간 증가 (~5-10%)
## 후속
- [ ] 30분 metrics 비교 결과 (백그라운드 스크립트 자동 푸시)
- [ ] 안정화 후 CrowdSec acquisition에 VL `service:npm log_type:access` 추가 → APISIX 파이프라인과 동일하게 시나리오 매칭 → bouncer 결정

View File

@@ -0,0 +1,124 @@
---
date: 2026-04-26
topic: CrowdSec bouncer 단일화 — netbis-cf-firewall만 유지, 나머지 3종 완전 제거
areas: [infra/security/crowdsec-safeline.md, infra/security/cloudflare.md, services/netbis.md]
tags: [crowdsec, bouncer, cloudflare, bunnycdn, apisix, simplification]
---
## 결정
CrowdSec LAPI 등록 4종 bouncer 중 `cs-cloudflare-bouncer-1777082222` (netbis-cf-firewall) 만 유지하고 나머지 3종 완전 제거.
| 보존 | 폐기 |
|---|---|
| `cs-cloudflare-bouncer-1777082222` (netbis-cf-firewall, 2026-04-25 재구축) | `cs-cf-worker-bouncer` (kappa CF Worker + KV + Turnstile×4) |
| | `apisix-waf-bouncer` (APISIX plugin 0.1) |
| | `bunny-cdn-bouncer` (BunnyCDN Edge Script 64811 bloom filter) |
이유 (kappa 지시):
- 운영 단순화 — 4종 bouncer 동시 관리 부담
- Worker bouncer 비용 + Turnstile 위젯 168h rotation 부담
- BunnyCDN Edge bloom filter 갱신 cron 의존성
- APISIX plugin 0.1 (오래된 패키지)
- netbis-cf-firewall은 origin filter `[crowdsec, cscli]` + Free CF 영역으로 비용 0, 단순
## 영향 (실행 전 사용자 확인 불필요로 진행)
- **enforce 사라진 영역**:
- kappa zone (keepanker.cv, actions.it.com, ironclad.it.com, servidor.it.com) CrowdSec ban → CF에서 관통
- BunnyCDN 풀존 iron-jp(5555247) / iron-kr(5555227) edge bouncer 차단 → 풀존 도착 트래픽이 origin까지 전달
- APISIX 인스턴스 chaitin-waf 보완용 IP ban (어느 인스턴스인지 Syn 확인) → SafeLine WAF만 유지
- **유지되는 enforce**:
- SafeLine WAF (chaitin-waf 플러그인, K3s safeline ns)
- APISIX limit-req
- netbis-cf-firewall (Netbis 6 zone CF Firewall Rule managed_challenge)
- BunnyCDN WAF (OWASP CRS) — bouncer와 별개
## 작업 분담 + 진행
### Heimdall 직접
| 단계 | 결과 |
|---|---|
| `incus stop+delete jp1:cs-cf-worker-bouncer` | ok, 컨테이너 사라짐 확인 |
| `jp1:infra-tool` cron `/etc/cron.d/crowdsec-bunny-bouncer` 삭제 | ok |
| `jp1:infra-tool` `/opt/crowdsec-bouncer/` 디렉토리 + bouncer.py 삭제 | ok |
| `pkill -9 -f bouncer.py` (잔존 프로세스) | ok |
| `cscli bouncers delete cs-cf-worker-bouncer` | deleted successfully |
| `cscli bouncers delete apisix-waf-bouncer` | deleted successfully |
| `cscli bouncers delete bunny-cdn-bouncer` | deleted successfully |
| LAPI 최종 상태 | `cs-cloudflare-bouncer-1777082222` 단일 |
### Syn 위임 (peer 통신, 동시 진행)
heimdall→syn tmux 메시지로 3건 위임:
1. **CF Worker (kappa account)** 정리 — Worker 스크립트 + KV namespace + Turnstile 위젯 4개 (`crowdsec-cloudflare-worker-bouncer-widget`, 적용 zone keepanker/actions/ironclad/servidor) 모두 삭제
2. **BunnyCDN Edge Script 64811** 정리 — iron-jp(5555247) + iron-kr(5555227) 두 풀존에서 `MiddlewareScriptId: 64811` 해제 + 스크립트 자체 삭제
3. **APISIX crowdsec-bouncer 플러그인** 비활성화 — apisix-osaka 추정 (헤임달 검증: K3s 서울 미사용, zlambda 미사용). plugin_metadata/crowdsec-bouncer 삭제 + 사용하는 route/global_rule plugins.crowdsec-bouncer 모두 제거 + config.yaml plugins 목록에서 crowdsec-bouncer 제거 (선택)
> **APISIX는 본래 Syn 영역**(CLAUDE.md 스코프) 이지만 kappa task 에서는 Heimdall 직접으로 명시되어 있었음. 실제로 apisix-osaka admin 권한이 Heimdall ops-agents 키로 없음 → Syn 위임이 자연스러움. 이 경계 deviation 은 작업 효율 우선으로 합리적 판단.
## Vault 시크릿 처리
- `secret/infra/crowdsec-bunny-bouncer`**보존** (별도 폐기 결정)
- `cs-cf-worker-bouncer` 관련 토큰 — **보존** (별도 폐기 결정)
- LAPI api-key 3종 — `cscli bouncers delete` 시점에 자동 무효화
## 검증
```bash
# LAPI bouncers list
incus exec jp1:crowdsec -- cscli bouncers list
# → cs-cloudflare-bouncer-1777082222 단일
# infra-tool cron 사라짐 확인
incus exec jp1:infra-tool -- ls /etc/cron.d/ | grep -i crowdsec
# → (empty)
# cs-cf-worker-bouncer 컨테이너 사라짐
incus list jp1: --all-projects | grep cs-cf-worker-bouncer
# → (empty)
```
### Syn 회신 (DONE 2026-04-26)
| 영역 | 결과 |
|---|---|
| CF Worker 스크립트 + KV namespace | 삭제 완료 |
| **CF Turnstile 위젯 4개** (`crowdsec-cloudflare-worker-bouncer-widget`) | Syn API 권한 부족으로 보류 → kappa가 `global_api_key`로 직접 삭제 (sitekey: `…N-HtUDFAaJ`/`…RrhJUQKfIF`/`…r02LA19njc7`/`…6l0x5reO6ZQ`) |
| BunnyCDN MiddlewareScriptId iron-jp/iron-kr 해제 | 완료 |
| BunnyCDN Edge Script 64811 자체 | Syn 보고 시 orphan 상태 → kappa가 `DELETE /compute/script/64811` HTTP 204로 삭제. iron-jp(5555247)/iron-kr(5555227)/iron-kr-waf(5555224)/iron-git(5584382) 모두 `MiddlewareScriptId: null` 자동 정리 확인 |
| APISIX(osaka) `crowdsec-bouncer` 플러그인 | global_rules + route `tg-webhook` + config.yaml에서 제거 + 재시작 완료 |
### Turnstile 위젯 추가 정리 (kappa, 2026-04-26)
bouncer 단일화 후속으로 같은 날 추가 폐기:
| Sitekey | Name | 폐기 사유 |
|---|---|---|
| `0x4AAAAAACbmaudAjITah7y7` | `inouter` | 이름·도메인 불일치 legacy orphan |
| `0x4AAAAAAC3otPWhldI96Aks` | `inouter-bunny-middleware` | BunnyCDN 미들웨어 64811 폐기와 함께 사용처 사라짐 |
CF Turnstile에 남은 위젯: `crowdsec-captcha` (`0x4AAAAAABvmO8BKc1ss5d-S`, 8 도메인). bouncer 외 사용처(charon.my / n8n.my / subin.my 등) 추적 미완 → 폐기 보류.
## 롤백
각 bouncer 재구성 절차 필요 시 이전 history 참조:
- cs-cf-worker-bouncer: 정본 `gitea.inouter.com/kaffa/k8s` `configs/crowdsec/crowdsec-cloudflare-worker-bouncer.yaml`
- bunny-cdn-bouncer: `gitea.inouter.com/kaffa/crowdsec-bunny-bouncer` (소스 보존)
- apisix-waf-bouncer: `crowdsec-bouncer.lua` 파일 + APISIX config.yaml plugins 등록 + plugin_metadata 설정
## 후속 과제
- [ ] Vault `secret/infra/crowdsec-bunny-bouncer` + cs-cf-worker-bouncer 관련 토큰 폐기 결정 (kappa)
- [ ] Vault `secret/cloud/cloudflare/turnstile-inouter-bunny` 폐기 (위젯 삭제됨)
- [ ] CrowdSec scenarios 다시 검토 — 일반 enforce 경로가 사라졌으니 시나리오 그룹화/조용화 가능
- [ ] kappa zone에 대한 대체 보호 검토 (필요 시 BunnyCDN WAF 강화 / CF Pro 플랜 활용 등)
- [ ] `crowdsec-captcha` Turnstile 위젯 8 도메인 사용처 추적 (charon.my / n8n.my / subin.my 등) → 폐기 가능 여부 결정
- [x] ~~Syn 회신 후 본 history에 CF Worker / KV / Turnstile / BunnyCDN Edge / APISIX plugin 정리 결과 추기~~ — 위 "Syn 회신" 절
## 관련 history
- [[2026-04-23-netbis-bouncer-removal|2026-04-23 Netbis 바운서 전량 제거]] (1차)
- [[2026-04-25-netbis-cf-firewall-rebuild|2026-04-25 netbis-cf-firewall 재구축]] (origin filter 적용)

View File

@@ -0,0 +1,113 @@
---
date: 2026-04-29
topic: Outline 0.82.0 → 1.7.0 메이저 업그레이드 (OIDC SameSite 쿠키 수정)
areas: [infra/platform/outline.md, infra/data/postgresql-ha.md]
tags: [outline, k3s, upgrade, oidc, postgresql, pgpool, helm-charts, argocd]
---
## 배경
Outline 0.82.0 의 OIDC 로그인이 외부에서 `State not return in OAuth flow` 에러로 실패. 진단 결과(2026-04-29 오전):
- 환경변수 / Gitea OAuth 앱 / Redis / 시계 모두 정상
- 외부 응답에 Set-Cookie 헤더 부재 — 브라우저의 bounce-tracking protection이 SameSite 미지정 쿠키를 strip
- 같은 결함이 GitHub Issue [outline/outline#11933](https://github.com/outline/outline/issues/11933) 에 보고. 1.7.0에서 csrfToken 쿠키에 `samesite=lax` 명시 추가하여 수정
## 결정
helm chart `gitea.inouter.com/kaffa/helm-charts``charts/outline/values.yaml` 에 image tag `0.82.0 → 1.7.0` 변경 후 ArgoCD 자동 sync.
## 사전 백업
| 백업 항목 | 위치 |
|---|---|
| Longhorn PVC snapshot | `pre-upgrade-outline-1-7-0` (volume `pvc-c4bb2746-79f4-41ea-85fc-765cfc473b15`, ready=true) |
| K8s deploy/secret/PVC | `~/outline-upgrade-backup/{deploy,outline-secrets,pvc,argocd-app}.yaml` |
| Outline DB pg_dump | `~/outline-upgrade-backup/outline-pre-1.7.0.dump` (2.1MB, custom format, Patroni leader postgres-2 = kr1 10.100.3.185) |
## 실행
1. helm-charts 레포 clone (Gitea PAT) → `charts/outline/values.yaml` 의 image tag 0.82.0 → 1.7.0 변경 → commit `outline: 0.82.0 -> 1.7.0 (OIDC SameSite fix)` push
2. ArgoCD App `outline` sync 트리거 → Deployment image 갱신
## 부수 이슈 — pgpool DDL/DML 라우팅
### 1차: 마이그레이션 ALTER TABLE 실패
신규 1.7.0 pod 기동 시 마이그레이션 `20250207120103-add-collectionId-to-subscriptions.js``cannot execute ALTER TABLE in a read-only transaction` 으로 실패. 누적 5회 CrashLoopBackOff.
### 2차: 운영 후 apiKeys UPDATE 실패
마이그레이션 완료 후 deploy env override 제거 + 운영 DATABASE_URL = `pgpool:9999` 로 복귀했는데, API 요청마다 발생하는 `UPDATE "apiKeys" SET "lastActiveAt"=$1 WHERE "id" = $2` 가 동일 사유로 실패 (PG error 25006). pgpool 이 DDL뿐 아니라 DML UPDATE 도 replica 로 라우팅.
### 원인
- DATABASE_URL 이 `pgpool.db.svc.cluster.local:9999` 사용 → pgpool이 ALTER TABLE 및 UPDATE 를 stream replication 모드에서 replica로 라우팅
- Patroni 자체는 healthy (Leader postgres-2 running, replicas streaming, lag=0)
- pgpool config 에서 write를 primary로 강제 라우팅하는 설정이 미적용으로 보임
### 해결 (영구 override)
ArgoCD selfHeal=false 유지 + deployment env 에 직접 DATABASE_URL override 적용 (`haproxy-pg.db.svc.cluster.local:5432` — Patroni leader 전용 라우팅, K3s db ns LoadBalancer):
```yaml
env:
- name: DATABASE_URL
value: postgresql://outline:outline@haproxy-pg.db.svc.cluster.local:5432/outline
- (기존 env: DEFAULT_LANGUAGE/ENABLE_UPDATES/FILE_STORAGE/.../URL ...)
```
마이그레이션 정상 적용:
- `20250207120103-add-collectionId-to-subscriptions.js` (up) 완료
- 누적된 0.82.0 → 1.7.0 마이그레이션 모두 적용
- "Listening on http://localhost:3000 / https://outline.inouter.com" 출력
운영 후 apiKeys UPDATE 에러도 해소됨.
> 외부 secret(`outline-secrets`)은 ESO가 1h 주기로 Vault `secret/apps/outline` 동기화. ESO 사이클이 patch를 1h 안에 revert하므로 secret 직접 수정은 무의미 — deploy env override 가 정답.
> ArgoCD selfHeal 은 영구 false 유지 — 활성화하면 chart values 의 DATABASE_URL 이 patch 를 덮어 다시 깨짐. 후속으로 chart values 자체에 haproxy-pg 명시 또는 pgpool config 수정 필요.
### pgpool DDL 라우팅 후속 과제
- pgpool config `disable_load_balance_on_write: 'transaction'` 또는 `dml_adaptive_object_relationship_list` 검토
- 또는 Outline 같은 schema 변경 잦은 앱은 처음부터 haproxy-pg 라우팅 사용 (chart values 에 DATABASE_URL 명시 검토)
## 검증
| 항목 | 결과 |
|---|---|
| Pod 상태 | 1/1 Running, restarts=1 (초기 liveness probe 한 번 실패 후 안정화) |
| Container image | `outlinewiki/outline:1.7.0` |
| 외부 https://outline.inouter.com/healthz | HTTP 200 |
| /api/auth.config (POST) | `{"data":{"name":"Outline","providers":[{"id":"oidc","name":"Gitea","authUrl":"/auth/oidc"}]}}` ✓ Gitea OIDC provider 등장 |
| Pod-internal /auth/oidc Set-Cookie | **2개 쿠키, csrfToken 에 `samesite=lax` 명시** + state 쿠키. 0.82.0 대비 csrfToken 쿠키 신규 추가됨 (issue #11933 fix) |
샘플 (pod-internal):
```
set-cookie: csrfToken=…; path=/; domain=localhost; samesite=lax
set-cookie: state=localhost|…|web||; path=/; expires=…; domain=localhost; httponly
```
브라우저는 cross-site OAuth redirect 후 csrfToken 쿠키를 동봉 → state mismatch 해소.
> 외부 (BunnyCDN/APISIX 경유) 응답에는 여전히 Set-Cookie 가 보이지 않음. 별개 이슈 (CDN 또는 APISIX route 가 Set-Cookie 를 strip 하는 가능성). 추후 디버깅 — 본 업그레이드 범위 외.
## 운영 메모
- Outline 1.7.0 의 `/api/auth.config`**POST** 만 받음 (0.82.0 GET → 1.7.0 POST). 외부 모니터링 스크립트가 GET 사용 중이면 404 받게 됨 — 갱신 필요
- 1.7.0 부터 `Restricting process count to 1 due to use of collaborative service without REDIS_COLLABORATION_URL` 로그 — collaboration service가 단일 process로 동작. multi-replica 확장 시 `REDIS_COLLABORATION_URL` env 필요
- helm chart 자체는 thin wrapper (deployment + ingressroute + service) — Outline secrets/config 대부분 outside chart (ESO + Vault + APISIX route)
## 롤백 절차
```bash
# helm-charts 레포에서 tag 되돌리기
sed -i 's|tag: "1.7.0"|tag: "0.82.0"|' charts/outline/values.yaml
git commit -am "outline: rollback 1.7.0 -> 0.82.0"
git push
# ArgoCD 자동 sync 후 pod 재기동. PVC 데이터는 그대로 유지됨
# DB schema 변경은 forward-only — 0.82.0 으로 내릴 시 신규 컬럼/테이블이 무시됨 (오류 없음)
```
PVC snapshot `pre-upgrade-outline-1-7-0` 으로 데이터 복원 가능 (1.7.0 schema 적용 전 상태). 단 schema 다운그레이드는 위험 — 가능하면 forward-only.

View File

@@ -0,0 +1,130 @@
---
date: 2026-05-02
topic: Longhorn stuck snapshot cleanup workaround
areas: [infra/platform/longhorn]
---
# 2026-05-02 / Longhorn snapshot-purge cron 도입
## 배경
Longhorn v1.11.1 에서 instance-manager 재시작(2026-05-02 09:00 UTC 즈음) 후 12개 snapshot CR 이 다음 패턴으로 stuck:
- `status.markRemoved = true`
- `status.readyToUse = false`
- `status.ownerID = ""`
- `metadata.finalizers = ["longhorn.io"]`
ownerID 가 비어 longhorn-manager controller 가 이 CR 들을 reconcile 하지 않음. 수동 `kubectl delete` / finalizer patch 는 admission webhook(`admission.go:115`) 이 즉시 finalizer 를 다시 추가해 무력화.
근본 원인은 v1.11.1 의 ownerId-loss 회귀로 추정. 1.11.2 / 1.12 에서 fix 예상. 임시로 snapshotPurge API 를 cron 으로 주기 호출.
## stuck 12개 식별
```
pvc-0440758f-f056-46d0-9733-dbb77f2e9101 (snapshot 1e632239-...)
pvc-15af4f6d-6129-4858-ae51-a3aa3546c4c2 (snapshot d95f165a-...)
pvc-384dd143-05b6-4cd6-a0dd-3edf5dca3acc (snapshot a8f0cbfd-...)
pvc-3c39ef90-d8ea-402d-8363-772ddbaf56a2 (snapshot 8f80ed9d-...)
pvc-411b3be5-ce8f-4039-a73e-d9b1e61a07de (snapshot 545f2ca5-...)
pvc-4d4cb56b-7be8-43ac-80be-5367cacd625a (snapshot 26dbf46f-...)
pvc-573db32c-f3fa-494e-8e1f-9f899938ec40 (snapshot 300158d8-...)
pvc-8534d9f3-91b9-49c3-9acd-804581c51893 (snapshot ef714f28-...)
pvc-8f9bfed6-236d-49d3-94d9-41880c351059 (snapshot b700b4d7-...)
pvc-bc38d860-0bc5-4b49-ab19-2586e0f78515 (snapshot 2581dac8-...)
pvc-c702c67d-7c5d-47d5-8814-b13fff45cbe7 (snapshot d43c685b-...)
pvc-f4b8b3b4-1a51-45a7-a460-422f9fca023f (snapshot cd9c36a3-...)
```
12 snapshot ↔ 12 volume (1:1).
## 도입 단계
### Chart 작성 + push
`gitea.inouter.com/kaffa/helm-charts` · `charts/longhorn-snapshot-purge/` 생성.
- `Chart.yaml`: name longhorn-snapshot-purge, version 0.1.0
- `values.yaml`: schedule `*/30 * * * *`, image `alpine/k8s:1.32.1`, longhornFrontendUrl http://longhorn-frontend.longhorn-system.svc.cluster.local
- `templates/serviceaccount.yaml`: SA `longhorn-snapshot-purge`
- `templates/role.yaml`: Role + RoleBinding (snapshots.longhorn.io get/list, ns-scoped)
- `templates/cronjob.yaml`: CronJob with kubectl + curl shell script
commit: `0edf886 Add longhorn-snapshot-purge chart for stuck snapshot cleanup`
fix commit: `d75c9a6 longhorn-snapshot-purge: query in-namespace to match Role scope` (`-A``-n longhorn-system` — RBAC scope mismatch 수정)
### ArgoCD Application 생성
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: longhorn-snapshot-purge
namespace: argocd
spec:
destination: { namespace: longhorn-system, server: https://kubernetes.default.svc }
project: default
source:
path: charts/longhorn-snapshot-purge
repoURL: https://gitea.inouter.com/kaffa/helm-charts.git
targetRevision: HEAD
syncPolicy:
automated: { prune: true, selfHeal: true }
```
`Synced/Healthy` 도달.
### 수동 트리거 검증
```
kubectl -n longhorn-system create job --from=cronjob/longhorn-snapshot-purge \
longhorn-snapshot-purge-manual-2
[2026-05-02T06:14:55Z] longhorn-snapshot-purge starting
stuck snapshot owner volumes (deduped): 12
ok pvc-0440758f-... (HTTP 200)
ok pvc-15af4f6d-... (HTTP 200)
ok pvc-384dd143-... (HTTP 200)
ok pvc-3c39ef90-... (HTTP 200)
ok pvc-411b3be5-... (HTTP 200)
ok pvc-4d4cb56b-... (HTTP 200)
ok pvc-573db32c-... (HTTP 200)
ok pvc-8534d9f3-... (HTTP 200)
ok pvc-8f9bfed6-... (HTTP 200)
ok pvc-bc38d860-... (HTTP 200)
ok pvc-c702c67d-... (HTTP 200)
ok pvc-f4b8b3b4-... (HTTP 200)
[2026-05-02T06:15:03Z] longhorn-snapshot-purge done (rc=0)
```
snapshotPurge 12/12 HTTP 200. job 8 초 내 완료 (rc=0).
### snapshotPurge 한계 확인
`POST snapshotList` 로 첫 stuck volume 의 engine snapshot 상태 검사:
```json
{"data":[
{"id":"volume-head", "parent":"1e632239-...", "removed":false},
{"id":"1e632239-...", "parent":"", "removed":true, "size":"51265536"}
]}
```
- `1e632239-...` 가 chain 의 유일 snapshot, volume-head 의 직속 parent
- `removed=true` 마킹은 됐으나 **.img 파일은 미정리** (chain merge 대상 없음)
- snapshotPurge 는 chain 중간의 redundant snapshot 만 합치므로 leaf snapshot 은 정리 불가
- 따라서 stuck snapshot CR 12 → 12 그대로 (5분 대기 후 재확인)
### 결론
- cron 은 사용자 요건 (3) "snapshotPurge API 매 30분 자동 호출" 을 정확히 수행
- engine 의 chain redundancy 가 발생하면 정리 — 향후 같은 볼륨에 추가 snapshot 이 쌓일 때 효과
- 단일 leaf stuck snapshot 들은 v1.11.2 / v1.12 업그레이드로 manager 가 reconcile 회복할 때까지 그대로 잔존
- 디스크 사용량은 12 × ~50MiB ≈ 600MiB 수준 — 운영 임팩트 무시 가능
- 향후 새 stuck 이 생기더라도 cron 이 자동 처리
## 후속
- v1.11.2 또는 v1.12 출시 → 표준 minor 순차 업그레이드 절차로 진행
- 업그레이드 완료 후 `kubectl -n argocd delete application longhorn-snapshot-purge` 로 cron 회수 (chart 디렉토리는 repo 에 보존)
- 만약 disk 회수가 시급한 stuck 이 발생하면 ownerID 강제 부여 patch + delete 흐름 도입 검토 — admission webhook 우회 필요해 별도 작업

View File

@@ -0,0 +1,90 @@
---
date: 2026-05-04
topic: incus-kr2 AMD-Vi (IOMMU) Completion-Wait timeout 호스트 부분 hang
areas:
- infra/compute/hosts/incus-kr2.md
- infra/compute/infra-hosts.md
tags: [history, incident, kr2, amd, iommu, freeze]
---
## 배경
incus-kr2가 약 2주 간격으로 호스트 freeze 재발. 이전 두 사건([[2026-04-04-usb-25g-hang]] USB autosuspend, 2026-04-19 OOM)은 각각 `usbcore.autosuspend=-1``kubelet system-reserved=8Gi`로 막은 상태였으나 또 다른 원인으로 hang. Tapo 스마트플러그를 kr2에 연결하여 강제 리부팅 가능하도록 사전 대비된 상태.
## 인시던트
**시각**: 2026-05-04 12:36 KST (호스트 시계 PDT로 잘못 설정되어 있어 `May 03 20:36` 기록 — KST 12:36)
**복구**: 2026-05-04 12:50 KST 사용자가 Tapo로 강제 리부팅
**다운타임**: 약 2시간 13분 (10:37 NotReady → 12:50 reboot)
**영향**: leewell.com 포함 inbest 7 컨테이너 응답 일부만 동작 (LXC 커널 namespace로 nginx는 살아 외부에 nginx 404 반환), K3s 워크로드 일부 Terminating (argocd redis, db pgpool, external-secrets, longhorn csi)
## 증상 vs 가설 부정 매트릭스
| 가설 | 증거 | 결론 |
|------|------|------|
| NIC (USB r8152 autosuspend) | carrier=1, 다른 노드에서 wget 응답 정상, 평소 트래픽 14~70 B/s | ❌ |
| NFS hard mount D-state | `procs_blocked=0` 8:43 KST까지 | ❌ |
| OOM | MemoryPressure=False, MemAvailable 23 GiB | ❌ |
| 디스크 hardware | SMART Critical=0, Errors=0, Percentage Used 1% | ❌ |
| **AMD IOMMU Completion-Wait** | dmesg `AMD-Vi: Completion-Wait loop timed out` (12:36, 12:46 x2) → 그 직후 hang | ✅ |
## 진단 핵심
- ICMP/메모리 매핑된 프로세스(node-exporter, nginx)는 응답 유지
- containerd, incus 데몬, sshd 새 세션 fork만 동시에 hang
- kubelet 마지막 이벤트: `failed to read podLogsDirectory "/var/log/pods": readdirent: input/output error` (91회 반복)
- node-exporter의 `up=1`은 끊긴 적 없음 — 헤임달 1차 진단 "kr2 호스트 자체 다운 2일 2시간"은 시간대/로그 해석 오류, 실제는 당일 약 2시간 13분
- 이 패턴은 IOMMU가 stall되어 DMA 통하는 디바이스(NVMe, USB 컨트롤러)가 응답 못 하지만 커널 자체와 메모리 매핑된 프로세스는 동작하는 부분 hang의 전형
## 직접 검증 데이터 (메트릭)
**freeze 직전 (08:31~08:44 KST 호스트 시계 기준 — 실제 KST는 +9h가 아니라 호스트 시계가 PDT라서 변환 필요. K3s NotReady transition 기준 10:37:43 KST)**:
- load1 = 2.22 (정상)
- MemAvailable = 23.13 GiB
- `procs_blocked` = 0
- `nvme0n1` io_time fraction = 2%, write 34 IOPS
- NVMe 온도 41~42 °C
→ 점진적 자원 고갈 흔적 0. "갑자기 정지" 패턴 = 펌웨어/IOMMU level hang.
## 재부팅 후 검증
```
[ 0.641598] iommu: Default domain type: Passthrough (set via kernel command line)
```
이전 부팅에서 부팅 후 24분 만에 첫 Completion-Wait timeout 발생, 이후 누적되며 hang. `iommu=pt` 적용 부팅 후 동일 시점 검증 필요.
## 조치
GRUB cmdline에 `iommu=pt` 추가:
```bash
sudo cp /etc/default/grub /etc/default/grub.bak.20260504-iommu
sudo sed -i -E 's/^(GRUB_CMDLINE_LINUX_DEFAULT="[^"]*)(")/\1 iommu=pt\2/' /etc/default/grub
sudo update-grub
sudo systemctl reboot
```
**적용 후 cmdline**:
```
ro usbcore.autosuspend=-1 quiet iommu=pt
```
부수 조치:
- 호스트 시간대 `America/Los_Angeles``Asia/Seoul` 수정 (`timedatectl set-timezone Asia/Seoul`). 이 잘못된 timezone이 헤임달 1차 진단의 "2일 2시간" 오판 원인.
## 교훈
- AMD Ryzen 미니 PC 베어메탈은 IOMMU passthrough 기본 적용 권장. 직전 부팅 24분 만에 첫 timeout이 떴다는 건 시간 문제로 hang 보장된 상태.
- IOMMU hang은 OOM/디스크/NIC와 다르게 메트릭에 점진적 흔적 안 남고 갑자기 정지. `node_procs_blocked` 0으로 끝나도 안전 신호 아님.
- `dmesg | grep AMD-Vi` 가 새 호스트 도입 시 표준 점검 항목.
- 호스트 timezone 통일 — 한국 인프라는 `Asia/Seoul`로 강제. 잘못된 timezone은 인시던트 분석 시 시간대 변환 오류로 잘못된 결론 유도.
- **헤임달 1차 진단 "kr2 호스트 자체 다운"은 표면 ssh/incus refused만 보고 결론. 메트릭 시계열·외부 응답까지 교차 검증 안 함.** 다음에 비슷한 진단 시 헤임달이 node-exporter `up`, 외부 nginx 응답 등을 같이 보도록 운영 규칙 보강 검토.
## 관련 문서
- [[amd-vi-iommu]] — AMD-Vi 부분 hang 메커니즘 정본 (왜 일부 프로세스만 죽는가, `iommu=pt`가 왜 해결하는가)
- [[infra/compute/hosts/incus-kr2|incus-kr2]]
- [[2026-04-04-usb-25g-hang]] — 직전 freeze (USB)
- [[infra/compute/infra-hosts]]

View File

@@ -0,0 +1,91 @@
---
date: 2026-05-07
topic: Longhorn 1.11.1 → 1.11.2 patch upgrade
areas: [infra/platform/longhorn]
---
# 2026-05-07 / Longhorn 1.11.1 → 1.11.2 업그레이드
## 동기
v1.11.2 GA (2026-05-05 06:11 UTC, [release notes](https://github.com/longhorn/longhorn/releases/tag/v1.11.2)) 백포트 [#12856](https://github.com/longhorn/longhorn/issues/12856) 적용 — `snapshot becomes not ready to use` Warning 회귀 fix. 2026-05-02 도입한 임시 cron `longhorn-snapshot-purge` 가 릴리스 직전까지 운영 안전망 역할을 했고, 이번 업그레이드로 정식 fix 적용.
## 사전 점검
- 4 노드 Ready (kr2 도 회복 — 5월 4일 leewell 인시던트 이후 정상 복구)
- 25 volumes attached/healthy
- backup target available, last sync 5분 전 (강제 backup 트리거 불필요)
- helm rev 5, chart longhorn-1.11.1, app v1.11.1
- helm chart 1.11.2 가용
- breaking change 없음 (release notes 검토)
## 업그레이드 단계
### 1) helm upgrade
```
helm upgrade longhorn longhorn/longhorn -n longhorn-system \
--version 1.11.2 --reset-then-reuse-values
→ NAMESPACE: longhorn-system STATUS: deployed REVISION: 6
```
values 그대로 (defaultSettings: nodeDownPodDeletionPolicy / nodeDrainPolicy / replicaAutoBalance).
### 2) longhorn-manager DS 롤링
```
Waiting for daemon set "longhorn-manager" rollout to finish: 2 of 4 updated pods are available...
Waiting for daemon set "longhorn-manager" rollout to finish: 3 of 4 updated pods are available...
daemon set "longhorn-manager" successfully rolled out
```
4/4 pod 2 컨테이너 모두 Running, 25초 내 완료.
### 3) 신규 engine image 배포
```
ei-75a03ec3 deployed longhorn-engine:v1.11.1 refcount=125
ei-c9fa6d45 deploying longhorn-engine:v1.11.2 refcount=0
```
70초 후 신규 EI 4/4 deployed.
### 4) 모든 볼륨 live engine upgrade
```bash
NEW_IMG="docker.io/longhornio/longhorn-engine:v1.11.2"
for v in $(kubectl -n longhorn-system get volumes.longhorn.io --no-headers -o custom-columns=:.metadata.name); do
kubectl -n longhorn-system patch volume.longhorn.io $v --type=merge \
-p "{\"spec\":{\"image\":\"$NEW_IMG\"}}"
done
```
25 볼륨 모두 patched. 약 30초 내 전수 `status.currentImage` = `v1.11.2`, 25 볼륨 attached/healthy 유지.
## 사후 관측 (35분)
업그레이드 직후 baseline 시각 (`2026-05-06T22:37:42Z`) 이후의 Warning 만 카운트:
```
T+1m ~ T+30m warn_after_baseline=0 stuck_snap_CR=0 vol_unhealthy=0
T+22m (08:00 KST = 23:00 UTC) — critical-snapshot RecurringJob 정시 사이클 통과
```
- **stuck CR 즉시 0** — 업그레이드 직후 manager ownership reset 흐름 회복으로 12개 CR 자연 cleanup
- **신규 Warning 0건** — 23:00 UTC RecurringJob 사이클에서도 발생 안 함
- **볼륨 헬스 100% 유지** — engine upgrade 동안 단 한 건도 unhealthy 진입 없음
#12856 fix 가 즉시 효과 발휘.
## 후속
1. **임시 cron 회수 작업** (별건). ArgoCD `longhorn-snapshot-purge` Application 삭제, helm-charts repo 의 chart 디렉토리는 재현 시 재활용 위해 보존.
2. 구 EI `ei-75a03ec3` (v1.11.1) refcount 0 확인 후 manager 자동 정리 (기본 timeout ~10분).
3. 다음 출시 (v1.11.3 또는 v1.12.x) 까지 별다른 작업 없음. 이번 fix 가 backport 라 v1.12 에도 동일 fix 포함될 것.
## 산출물
- helm rev 5 → 6
- chart longhorn-1.11.1 → longhorn-1.11.2
- engine image v1.11.1 → v1.11.2 (25/25 볼륨)
- Outline `heimdall/2026-05-07 longhorn 1.11.2 업그레이드` 참조

View File

@@ -0,0 +1,66 @@
---
date: 2026-05-09
topic: Longhorn snapshot-purge 임시 cron 회수
areas: [infra/platform/longhorn]
---
# 2026-05-09 / longhorn-snapshot-purge cron 회수
## 동기
2026-05-02 도입한 임시 cron `longhorn-snapshot-purge` (v1.11.1 snapshot 회귀 [#12856](https://github.com/longhorn/longhorn/issues/12856) 우회용) 가 2026-05-07 v1.11.2 업그레이드 (Outline `a0a5ad90-68ed-4939-bbc1-1e20cd25f874`) 로 fix 적용되어 의미 사라짐. 이틀 안정 운영 후 회수.
## 사전 상태
- ArgoCD Application `longhorn-snapshot-purge` Synced/Healthy, `automated.prune=true`
- CronJob `longhorn-system/longhorn-snapshot-purge` 마지막 실행 26초 전
- ns Warning 0건
- stuck snapshot CR 0개
- helm-charts repo 의 `charts/longhorn-snapshot-purge/` 디렉토리 그대로
## 회수 단계
### ArgoCD Application 삭제 (cascade prune)
```bash
kubectl -n argocd patch application longhorn-snapshot-purge --type=merge \
-p '{"metadata":{"finalizers":["resources-finalizer.argocd.argoproj.io"]}}'
kubectl -n argocd delete application longhorn-snapshot-purge --wait=false
```
`resources-finalizer.argocd.argoproj.io` 추가로 cascade prune 트리거. ArgoCD 가 `automated.prune=true` 이므로 deployed 리소스 자동 정리.
### prune 완료 대기
CronJob `longhorn-snapshot-purge` 사라질 때까지 폴링. 시작 후 약 수십 초 내 prune 완료.
### chart 디렉토리 보존
`gitea.inouter.com/kaffa/helm-charts` repo 의 `charts/longhorn-snapshot-purge/` 는 그대로 남김. 향후 동일 회귀 발생 시 재활용 가능.
## 사후 검증
```
$ kubectl -n longhorn-system get cronjob,sa,role,rolebinding | grep snapshot-purge
(no match)
$ kubectl -n argocd get application longhorn-snapshot-purge
Error: NotFound
$ ls /tmp/helm-charts/charts/longhorn-snapshot-purge/
Chart.yaml templates values.yaml ← 보존됨
```
회수 후 5분 폴링 (1분 간격, 5회):
```
T+1m ~ T+5m warn_after_baseline=0 stuck_CR=0
```
회귀 신호 없음.
## 후속
- 다음 v1.11.x / v1.12 업그레이드 시 이 임시 cron chart 는 무관 — 정본 helm release 만 다룸
- 만약 v1.11.x 후속 패치에서 동일 회귀 재현 시 chart 디렉토리 그대로 ArgoCD Application 만 재배포로 복구 가능
- 본 회수로 longhorn-system 의 ArgoCD App 의존이 다시 0 (longhorn 본체는 helm 직접 관리 그대로 유지)

View File

@@ -0,0 +1,169 @@
---
date: 2026-05-17
topic: SafeLine RWX PVC ext4 호환성 인시던트 — Longhorn v1.11.2 strict fsck 거부
areas: [infra/platform/longhorn, infra/security/crowdsec-safeline]
---
# 2026-05-17 / SafeLine RWX PVC 손상 복구 — Longhorn 1.11.2 share-manager strict fsck
## 증상
`/sc:test` 클러스터 상태 점검 중 발견:
- `safeline-mgt` Pod: `CrashLoopBackOff` (restart 1008회, 3일 15시간)
- `safeline-luigi` Pod: `Init:0/1` (43시간 wait, mgt:1443 폴링)
- 그 외 SafeLine pods (chaos/database/detector/fvm) 정상
mgt 로그:
```
panic: failed to init models: init empty license error: database disk image is malformed (11)
/work/initialize/pg_init.go:82
SELECT * FROM `options` WHERE key = "license"
```
표면적으로는 SQLite SQLITE_CORRUPT(11). 실제 SafeLine 메인 DB(PostgreSQL `safeline-database-0`)는 정상.
## 근본 원인
mgt를 scale 0으로 내리고 디버그 pod로 PVC mount 시도하니 `FailedAttachVolume "Waiting for volume share to be available"` — Longhorn share-manager pod가 1초만에 Failed로 죽고 2분 backoff 사이클 반복. share-manager 로그 캡처:
```
/dev/longhorn/pvc-8534d9f3-...: Resize inode not valid.
UNEXPECTED INCONSISTENCY; RUN fsck MANUALLY.
(i.e., without -a or -p options)
fatal: 'fsck' found errors on device but could not correct them
```
**ext4 superblock metadata 손상** (`Resize inode not valid`)으로 share-manager가 자동 모드 fsck (`-a`/`-p`)로 거부 → NFS export 못함 → 모든 PVC mount 실패.
[[2026-05-07-longhorn-1-11-2-upgrade]] 업그레이드에서 share-manager 이미지가 v1.11.2로 바뀌면서 fsck strict 모드로 동작. 옛 fs metadata와 호환성 깨진 것이 주범으로 판단. 실제 데이터는 정상일 가능성 매우 큼 (수동 `fsck -y` 한 번이면 풀림).
## 영향 받은 PVC
같은 시점에 만들어진 `incus-hp2` 노드의 RWX PVC 3개 모두 동일 패턴:
| PVC | 옛 PV | 상태 |
|---|---|---|
| `safeline/safeline-mgt` | `pvc-8534d9f3-91b9-49c3-9acd-804581c51893` | Released (Retain 보존) |
| `safeline/safeline-tengine-logs` | `pvc-15af4f6d-6129-4858-ae51-a3aa3546c4c2` | Released (Retain 보존) |
| `safeline/safeline-luigi` | `pvc-3c39ef90-d8ea-402d-8363-772ddbaf56a2` | Released (Retain 보존) |
같은 노드의 다른 RWX 4개 (chaos/detector/detector-logs/8f9bfed6)는 mount 유지 중이라 영향 없음 — 다음 재기동 시 동일 에러 가능성.
## 시도한 조치
1. **Longhorn snapshot revert (2026-05-13 16:03, `52eb920e`, 손상 직전 시점)** — 실패. blockdev revert로 ext4 metadata도 같이 복원되어 동일 fsck 에러 재현.
2. **PVC 폐기 + 동일 이름 새 PVC 생성** — 성공. 새 mkfs.ext4로 만들어진 fs는 strict fsck 통과.
## 복구 절차 (재사용 가능)
```sh
NS=safeline
NAME=safeline-mgt # 손상 PVC 이름
SIZE=1Gi # 동일 사이즈
# 1. 워크로드 내림
kubectl -n $NS scale deploy $NAME --replicas=0
# 2. PV reclaim Retain으로 변경 (포렌식용 보존)
PV=$(kubectl -n $NS get pvc $NAME -o jsonpath='{.spec.volumeName}')
kubectl patch pv $PV -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
# 3. PVC 삭제 (PV는 Released 상태로 살아남음)
kubectl -n $NS delete pvc $NAME
# 4. 동일 이름 새 PVC 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: $NAME
namespace: $NS
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: $SIZE
storageClassName: longhorn
volumeMode: Filesystem
EOF
# 5. 워크로드 올림
kubectl -n $NS scale deploy $NAME --replicas=1
```
## 데이터 영향
- **mgt**: 라이선스/룰/설정 초기화 → SafeLine UI 첫 로그인 + 라이선스 재입력 필요. WAF 검출 룰 정본은 PG에 있으므로 검출 자체는 그대로 동작.
- **tengine-logs**: nginx access log 손실 (가치 없음).
- **luigi**: 검출 캐시 손실, 정본은 PG.
## 옛 PV 데이터 복구 가능성
3개 옛 PV는 reclaim Retain으로 `Released` 상태 보존 중. 향후 시간 있을 때 `fsck.ext4 -y /dev/longhorn/pvc-...` 로 메타 복구 후 디버그 pod에 mount해서 데이터 추출 시도 가능. 특히 mgt 라이선스 파일(`/app/data/license`) 회수 가능성.
## Longhorn HTTP API 작업 노트
snapshot revert 시도 시 사용. port-forward는 longhorn-manager 포트 9500이 0.0.0.0이 아닌 다른 인터페이스에 bind되어 macOS에서 직접 forward 실패 → in-cluster pod에서 호출 필요.
```sh
# in-cluster curl pod
kubectl -n longhorn-system run lh-curl --image=curlimages/curl:8.10.1 \
--command -- sleep 7200
# Volume detach → maintenance attach → snapshot revert
VOL=pvc-XXX
LH=http://longhorn-backend:9500
kubectl -n longhorn-system exec lh-curl -- sh -c "
curl -X POST $LH/v1/volumes/$VOL?action=detach -d '{}'
curl -X POST $LH/v1/volumes/$VOL?action=attach \
-d '{\"hostId\":\"<node>\",\"disableFrontend\":true,\"attachedBy\":\"manual\"}'
curl -X POST $LH/v1/volumes/$VOL?action=snapshotRevert \
-d '{\"name\":\"snapshot-<uuid>\"}'
curl -X POST $LH/v1/volumes/$VOL?action=detach -d '{}'
"
```
## 잠재 위험 (TODO)
- 같은 노드/같은 시점 mkfs된 다른 RWX 볼륨도 share-manager 재기동 시 동일 에러 가능. 사전 점검 가치 있음 — 한 번씩 detach/reattach 사이클 돌려 fail하는 PVC 식별 후 미리 교체.
- 또는 share-manager의 fsck 옵션을 `-y`로 patch (Longhorn 자체 코드 수정 필요, upstream 이슈로 보고 권장).
## 2026-05-18 후속 — detector PVC 폭발 (예측 적중)
어제 명시한 4개 위험 PVC 중 `safeline/safeline-detector` (옛 PV `pvc-8f9bfed6`)가 **2026-05-17 23:12 KST부터 6시간 이상 VolumeAttachment finalizer hang** (`Waiting for volume share to be available`) 후 **2026-05-18 14:18 KST 동일 절차로 복구**됨. 신규 PV `pvc-d2654f70-01e2-4a40-8336-1b56ddacb2cb` 바인딩, detector 파드 정상 기동(rskynet hyperscan 47 패턴 로드, NFSv4.1 클라이언트 정상 등록). 데이터 영향 없음(detector PVC는 룰셋/캐시, 이미지에 포함).
복구 actor 미상 — helm/Argo 변경 없음(generation/RS 변동 없음), K3s audit log 부재로 PVC 삭제 호출자 식별 불가. csi-plugin이 attach 강제 시도까지는 자동, PVC delete/recreate는 사람 또는 다른 자동화로 추정.
### 남은 시간폭탄
| PVC | PV | 노드 | mkfs 시점 | 위험 |
|---|---|---|---|---|
| `safeline/safeline-chaos` | `pvc-0440758f-f056-46d0-9733-dbb77f2e9101` | incus-hp2 | 55d (2026-03-24) | 다음 share-manager 재기동 시 동일 ext4 fsck 거부 예상 |
| `safeline/safeline-detector-logs` | `pvc-384dd143-05b6-4cd6-a0dd-3edf5dca3acc` | incus-hp2 | 55d (2026-03-24) | 동일 |
어제 위험 명단의 `detector``8f9bfed6`은 동일 PVC(`safeline-detector`)에 옛 PV였고 이번에 해소됨. 남은 둘은 어제 절차로 사전 교체 가능 — 데이터 가치 낮음(chaos = 검출 룰 캐시 / detector-logs = nginx access 로그).
### 학습
- v1.11.2 share-manager strict fsck는 옛 ext4 metadata와 호환 안 됨 — 같은 시점 mkfs된 RWX 볼륨은 **share-manager 재기동을 절대 트리거하지 말거나** 사전에 강제 교체해야 함.
- 같은 노드 RWX share-manager 6개 집중 위험은 별건 — incus-hp2 share-manager 이슈 시 SafeLine 전체 영향.
### 사전 교체 — chaos / detector-logs (2026-05-18 18:55 KST)
남은 시간폭탄 2개를 표준 절차로 사전 교체. SafeLine 6 파드 모두 Running, share-manager 6/6 Running 회복.
| PVC | 옛 PV (Released, Retain 보존) | 새 PV | 영향 워크로드 |
|---|---|---|---|
| `safeline/safeline-chaos` | `pvc-0440758f-f056-46d0-9733-dbb77f2e9101` | `pvc-623baf8f-78a6-4fa1-a946-57f191d0fbb9` | `deploy/safeline-chaos` (1~2분 중단) |
| `safeline/safeline-detector-logs` | `pvc-384dd143-05b6-4cd6-a0dd-3edf5dca3acc` | `pvc-e5f7459d-99ce-4404-a435-270aec44599b` | `deploy/safeline-detector` (1~2분 중단) |
`safeline-detector-logs` PVC는 detector 본체 deployment가 마운트하므로 detector pod도 같이 토글된 점 주의. 신규 detector pod `safeline-detector-566c8fb8fd-n8zbl`에서 rskynet 정상 init, `signal: killed`는 init wait 한 줄(무해 패턴).
incus-hp2 RWX share-manager 6개 전수가 신규 fs로 교체 완료되어 **이번 라운드의 fsck 시간폭탄은 해소**. 다음 위험은 다른 시점 mkfs된 볼륨이 등장할 때까지 잠재.
## 관련
- [[../infra/platform/longhorn]] — Longhorn 플랫폼 정본
- [[../infra/security/crowdsec-safeline]] — SafeLine ↔ CrowdSec 연동
- [[2026-05-07-longhorn-1-11-2-upgrade]] — 직전 업그레이드

View File

@@ -0,0 +1,85 @@
---
date: 2026-05-20
topic: kr2 multus-shim ETXTBSY 데드락 — k3s 재시작 후 CNI 복구 불가
areas: [infra/compute/infra-hosts, infra/platform]
---
# 2026-05-20 / kr2 multus-shim ETXTBSY 데드락
## 증상
`/sc:test` 와 무관, NAS NFS 점검 중 발견. kr2 노드는 K8s `Ready` 인데 노드 위 Pod 8개가 stuck:
- `Unknown`: traefik, kube-multus-ds, kube-system/descheduler, vector, longhorn-manager / longhorn-csi-plugin / engine-image
- `ContainerCreating` 장시간 (1629h): critical-backup, critical-snapshot, descheduler
`kubelet` 에러:
```
plugin type="multus-shim" name="multus-cni-network" failed (add):
CmdAdd (shim): timed out waiting for the condition
```
## 데드락 구조
1. 2026-05-19 11:11:59 KST kr2 `k3s.service` 재시작 직후 `kube-multus-ds` Pod이 init 단계에서 깨끗하게 복구 안 됨.
2. `install-multus-binary` initContainer 가 `cp``/host/opt/cni/bin/multus-shim` 갱신 시도 → `cp: cannot create regular file '/host/opt/cni/bin/multus-shim': Text file busy` (**ETXTBSY**).
3. ETXTBSY 원인: kubelet 이 stuck Pod sandbox 재생성 시도 → `/opt/cni/bin/multus-shim` 을 자식 프로세스로 spawn → multus daemon socket 부재로 RPC hang → shim 프로세스가 inode 잡은 채로 매달림.
4. shim 누적 (관찰 시 8 → 14 → 매달림 지속). cp 는 영원히 fail. multus daemon 영원히 안 뜸. **완전한 데드락.**
5. `pkill -9 multus-shim`, `fuser -k` 모두 즉시 새 shim 이 spawn 되어 효과 없음. `systemctl restart k3s` 도 마찬가지 — 재시작 후 kubelet 이 sandbox cleanup retry 하면서 shim 다시 누적.
## 해결 키
```bash
sudo rm /opt/cni/bin/multus-shim
```
`unlink(2)` 는 매달린 프로세스의 fd 가 잡고 있는 inode 를 보존한 채 path 만 제거. 다음 `install-multus-binary` retry 의 `cp`**fresh inode** 로 새 파일 생성 → ETXTBSY 회피 → multus daemon 정상 기동 → 매달린 shim 들이 응답 받고 자연 종료.
## 복구 절차 (실제 수행)
```bash
# 1. stuck pod force delete (kubelet retry 루프 끊기)
kubectl delete pod --force --grace-period=0 -n kube-system descheduler-* traefik-*
kubectl delete pod --force --grace-period=0 -n logging vector-*
kubectl delete pod --force --grace-period=0 -n longhorn-system \
critical-{backup,snapshot}-* engine-image-* longhorn-{csi-plugin,manager}-*
# 2. kr2 호스트에서 multus-shim 파일 unlink (핵심)
ssh kaffa@kr2 'sudo rm /opt/cni/bin/multus-shim'
# 3. multus DS pod 재생성 → backoff 카운터 리셋
kubectl delete pod -n kube-system kube-multus-ds-<crashing-pod> --force --grace-period=0
```
3분 내 모든 stuck Pod 정상화. 잔여 shim 0개 자동 정리. instance-manager 등 cascade로 띄워지는 Pod도 자동 복구.
## 검증
```
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system kube-multus-ds-9lxxn 1/1 Running 0 29s
kube-system traefik-kjts5 1/1 Running 0 3m10s
kube-system descheduler-... 0/1 Completed 0 3m10s
logging vector-rc6ph 1/1 Running 0 3m9s
longhorn-system engine-image-... 1/1 Running 0 3m9s
longhorn-system instance-manager-... 1/1 Running 0 43s
longhorn-system longhorn-csi-plugin-* 3/3 Running 0 3m9s
longhorn-system longhorn-manager-* 2/2 Running 0 3m9s
longhorn-system critical-snapshot-* 0/1 Completed 0 3m8s
```
전 클러스터 비정상 Pod 0개. 4 노드 모두 Ready.
## 교훈
- **ETXTBSY 데드락 패턴**: CNI plugin daemon이 정상 socket을 listen하지 않으면 kubelet retry가 shim을 누적 spawn → 바이너리 갱신 불가. 같은 패턴은 calico, cilium 등 다른 CNI 에서도 발생 가능.
- **`rm` 으로 unlink → fresh inode** 가 표준 회피책. `pkill` / `fuser -k` / 데몬 재시작 모두 retry 루프 못 끊음.
- **재발 방지**: kr2 k3s 재시작 시 `kube-multus-ds` 가 다시 init 단계에서 깨질 수 있음. 재시작 직후 60초 안에 multus DS Pod 상태 확인하고 backoff 회수가 빠르게 늘면 즉시 위 절차 적용.
- 발견 경위: NAS NFS 연결 점검 중 `kubectl get pods -A` 부수 확인에서 우연히 노출. NFS 점검과 인과 무관.
## 관련 문서
- [[infra-hosts]] — K3s 4 노드 구성 (kr1, kr2, hp1, hp2)
- [[longhorn]] — instance-manager / engine-image / manager / csi-plugin DS 구조
- multus CNI 공식: <https://github.com/k8snetworkplumbingwg/multus-cni>

View File

@@ -0,0 +1,61 @@
---
date: 2026-05-20
topic: NAS eth2 USB 2.5GbE 어댑터 물리 교체 — RTL8157 chip rev 17 → rev 14
areas: [infra/data/nas-storage]
---
# 2026-05-20 / NAS eth2 USB 2.5GbE 카드 물리 교체
## 배경
[[nas-storage#NAS eth2 USB NIC watchdog (2026-04-14)]] 에서 옛 USB NIC(RTL8157 chip rev 17, USB 4-1 포트)이 `-71 EPROTO` LPM exit latency 이슈로 간헐적 disconnect 발생. cron watchdog로 자동 복구하고 있었으나 카드 자체 고장 가능성이 누적되어 동일 RTL8157 계열 새 어댑터로 교체.
## 교체 정보
| 항목 | 옛 카드 | 새 카드 |
|------|--------|--------|
| USB 포트 | 4-1 | 2-2 |
| chip rev (r8152) | 17 | 14 |
| MAC | (이전 기록 없음) | `c8:4d:44:27:a9:63` |
| 인터페이스 이름 | eth2 | **eth2 (그대로)** |
| 드라이버 | r8152 v2.20.1 (2025/05/13) | r8152 v2.20.1 (동일) |
DSM의 ifcfg-eth2 가 슬롯 기반(eth0/eth1 은 온보드 1G 슬레이브, 추가 NIC 은 eth2)으로 이름을 매핑하므로 USB 포트가 4-1 → 2-2 로 바뀌어도 인터페이스 이름과 IP/MTU 설정은 그대로 승계.
## dmesg 시계열
```
[45424] r8152 4-1:1.0 eth2: v2.20.1 (2025/05/13) # 옛 카드 활성 (rev 17)
[45441] r8152 4-1:1.0 eth2: Stop submitting intr, status -71 # 옛 카드 사망
[139755] r8152 2-2:1.0 eth2: v2.20.1 (2025/05/13) # 새 카드 인식 (rev 14, USB 2-2)
[139815] r8152 2-2:1.0 eth2: carrier on # 링크 업
```
dmesg 끊김 약 94000초(≈26시간) 구간에 물리 교체 수행. DSM 자동 ifcfg 적용으로 OS 레벨 추가 작업 불필요.
## 검증 (2026-05-20)
ethtool eth2:
- Speed: 2500 Mb/s, Duplex: Full, Link detected: yes
- Auto-negotiation: on
- bus-info: `usb-0000:00:14.0-2`
iperf v2, 10초, MTU 9000 end-to-end (mss=8948 확인):
| 방향 | 처리량 | 이론(2.5Gbit/s) 대비 |
|------|--------|---------------------|
| kr1 → NAS (TX) | 2.26 Gbit/s | 91% |
| NAS → kr1 (RX) | 2.45 Gbit/s | 98% |
| kr1 → NAS, 4 병렬 | 2.27 Gbit/s SUM | 단일로 이미 라인레이트 |
iperf v2 사용 이유: NAS 패키지에 `iperf` 만 있고 `iperf3` 없음. kr1 호스트에는 `iperf3` 만 있어 임시로 `apt install iperf` (Debian 13, `iperf 2.2.1+dfsg-1`) 설치. 측정 후 서버 프로세스만 정리(패키지는 유지).
## watchdog 영향
`/usr/local/bin/eth2-watchdog.sh` (cron 1분 주기) 는 인터페이스 이름(eth2) 기반으로 동작하므로 카드 교체 후에도 그대로 유효. 새 카드의 LPM 동작 패턴(chip rev 14)이 옛 카드와 다를 가능성 있으므로 향후 watchdog 발화 빈도 변화 모니터링 가치 있음.
## 관련 문서
- [[nas-storage]] — NAS NFS/iSCSI StorageClass 정본 (eth2 정보 본 교체로 갱신)
- [[2026-04-14-nas-eth2-watchdog|2026-04-14 / NAS eth2 watchdog 구축]] (Outline `93baf66b-f003-47a9-9d14-7a66e3dbfde0`)
- 옛 카드 RCA: Outline `db923170-8c16-459d-82ce-46fdc1f0f0d0` (2026-04-14)

View File

@@ -0,0 +1,44 @@
---
date: 2026-05-23
topic: cf-bouncer config에서 fall-mvp.com zone 제거 — netbis CF bouncer 21일 만에 동기화 재개
areas: [infra/security/crowdsec-safeline]
tags: [history, cloudflare, crowdsec, bouncer, netbis]
---
# 2026-05-23 / cf-bouncer fall-mvp.com 제거 + 동기화 재개
## 배경
- `cs-cloudflare-bouncer-1777082222` (= [[../infra/security/crowdsec-safeline#netbis-cf-firewall-cloudflare-firewall-rule-bouncer-재구축-2026-04-25|netbis-cf-firewall]]) 가 2026-05-02 00:21:58Z 정지 후 21일째 down 상태였음 — kappa가 [[2026-05-23-kr1-k3s-stuck-cascade|kr1 k3s stuck 점검]] 흐름에서 발견.
- 정지 원인 추적 결과 root cause는 단순 `disabled` 아니라 **fall-mvp.com zone (`6c171579912a271c0fc89c8187493b0f`)이 CF에서 삭제됨** → bouncer가 시작 시 `account ... doesn't have access to zone ...` fatal exit → 무한 재시작 루프 → 누군가 `systemctl stop && disable`로 끄고 잊은 채로 21일 방치.
## 조치
1. Vault `secret/cloud/cloudflare-netbis` 의 firewall_bouncer_token으로 CF API 직접 확인:
- zone `6c171579912a271c0fc89c8187493b0f``code 1001 "Invalid zone identifier"` (zone 자체 존재하지 않음)
- netbis account의 남은 zone = 5개 (fall-vip, fall-vip7, psd777, rss-555, rss-7790) 모두 active
2. `/etc/crowdsec/bouncers/crowdsec-cloudflare-bouncer.yaml` 백업 후 fall-mvp 항목 2줄 제거 (actions/zone_id 쌍)
3. `systemctl enable --now crowdsec-cloudflare-bouncer.service`
4. LAPI pull 갱신 확인:
- 이전: `Last API pull: 2026-05-02T00:21:55Z`
- 이후: `Last API pull: 2026-05-23T04:56:28Z`
## 변경 후 상태
| 항목 | 값 |
|---|---|
| 적용 zone | 5개 (fall-mvp 제거됨) |
| service | `active (running)` since 2026-05-23 04:55:58 UTC |
| LAPI bouncer | `cs-cloudflare-bouncer-1777082222` Valid, 30s 폴링 정상 |
| config | `/etc/crowdsec/bouncers/crowdsec-cloudflare-bouncer.yaml` (백업: `.bak.20260523`) |
## 미해결
- **fall-mvp.com zone이 언제/왜 CF에서 삭제됐는지 미상.** netbis 측 의도된 폐기일 가능성 — 별도 확인 필요.
- bouncer 무한 재시작 루프가 production silently로 21일 방치된 건 모니터링 공백 — `systemctl is-active crowdsec-cloudflare-bouncer.service` 알람 추가 검토.
## 참조
- [[../infra/security/crowdsec-safeline|crowdsec-safeline 정본]] — netbis-cf-firewall 섹션 zone 6→5 반영 완료
- [[2026-04-25-netbis-cf-firewall-rebuild|2026-04-25 netbis-cf-firewall 재구축]] — 6 zone으로 처음 구축한 시점
- [[2026-05-23-kr1-k3s-stuck-cascade|2026-05-23 kr1 k3s stuck]] — 이 발견의 동행 작업

View File

@@ -0,0 +1,111 @@
---
date: 2026-05-23
topic: kr1 k3s.service 12시간 stuck — apisix-etcd raft 손상, cf-bouncer down, Longhorn degraded까지 연쇄
areas: [infra/compute/hosts/incus-kr1, infra/data/postgresql-ha, infra/platform/longhorn, infra/security/crowdsec-safeline]
tags: [history, incident, k3s, longhorn, etcd, patroni]
---
# 2026-05-23 / kr1 k3s 12시간 stuck — 다단 연쇄
## 배경
2026-05-22 20:02 JST 어떤 트리거로 kr1의 `k3s.service`가 재시작 시도 → kine connect 단계에서 process가 stuck → 12시간 동안 `Active: activating (start)` 상태. 이 한 사건이 다음 세 가지로 연쇄 폭발:
1. **apisix-etcd-0 raft 로그 손상** (kr1 위의 instance-manager가 Terminating으로 매달리면서 etcd replica write가 끊김) — 2026-05-22 23:00경 발견, 별도 복구
2. **Cloudflare bouncer 동기화 정지** — 사실은 2026-05-02부터 별건이었지만 이번 점검 중 함께 노출
3. **Longhorn 4개 볼륨 degraded + 1개 unknown** — kr1의 instance-manager 9일째 Terminating으로 매달림
체감상 별건처럼 보였던 세 인시던트가 사실 같은 뿌리(kr1 NotReady)였다.
## 증상
- `kubectl get node incus-kr1`**NotReady**, condition `NodeStatusUnknown` "Kubelet stopped posting node status"
- `systemctl status k3s.service` on kr1 → `Active: activating (start) since Fri 2026-05-22 20:02:20 JST; 12h ago`, Main process alive but never reaching ready
- kr1 호스트 자체는 **살아 있음** (uptime 20d, ssh/Tailscale 정상, load 0.3, 디스크 23% / 메모리 8/62GB)
- journal 반복 에러:
```
k3s[*]: time="..." level=error msg="Failed to ping database connection:
failed to connect to user=kine database=kine:
10.100.2.5:5432: ValidateConnect failed: read only connection
10.100.3.185:5432: ValidateConnect failed: read only connection
10.100.1.83:5432: ValidateConnect failed: read only connection"
```
- 9일째 Terminating으로 매달린 pod 다수 (`instance-manager-d1d06d9c...`, `longhorn-ui`, `vmalert`, `rabbitmq-server-0`, `searxng`)
## 진단 — 가설 → 검증
### 가설 1: Patroni Leader 부재 (k3s 로그 그대로)
**검증**: `patronictl list` from 임의 patroni node → `postgres-2` (kr1 컨테이너, 10.100.3.185) = **Leader, TL 18, lag 0**. 다른 두 노드 streaming healthy. **Patroni는 완전 정상.** k3s 로그의 "read only connection" 메시지는 pgx 드라이버의 `target_session_attrs=read-write` 검증 실패 일반화 메시지로, 진짜 원인은 process 자체가 stuck이라 connection state가 stale.
### 가설 2: Tailscale 네트워크 단절
**검증**: kr1 호스트에서 세 postgres 컨테이너 IP(10.100.2.5/10.100.3.185/10.100.1.83) 모두 ping ok, TCP 5432 도달 ok, Tailscale status 정상. **네트워크는 정상.**
### 가설 3: postgres-2 데이터 손상 (dmesg sdo I/O error)
**검증**: postgres-2 컨테이너 root device는 incus default pool (nvme0n1p2 위). sdo는 별도 디바이스 — Longhorn iSCSI 가상 디바이스가 한때 mount되었다 detach된 흔적. **postgres 데이터는 안전.** dmesg의 `comm: postgres`는 죽은 K8s pod의 stale mount에서 나온 거였음.
### 가설 4: kr1 호스트 디스크 손상
**검증**: `smartctl -H /dev/nvme0n1`**PASSED.** 호스트 디스크 정상.
### 결론: k3s process가 첫 시작 시점에 stuck했고 systemd가 그 상태로 12시간 retry. 새 process fork만 하면 풀림.
## 복구 절차 (≈ 4분)
```bash
# 1. kr1에서 k3s.service 재시작
ssh incus-kr1 'sudo systemctl restart k3s.service'
# stop-sigterm phase에 90초 정도 걸리고 (containerd-shim cleanup), 새 process가 fresh connect → active
# 2. kr1 Ready 확인
kubectl get node incus-kr1 # → Ready
# 3. 9d 매달린 Terminating pod 5개 force delete
kubectl delete pod -n longhorn-system instance-manager-d1d06d9c2833120691e8806c13559fc3 --force --grace-period=0
kubectl delete pod -n longhorn-system longhorn-ui-765d5c5b6c-68wd4 --force --grace-period=0
kubectl delete pod -n monitoring vmalert-vm-stack-victoria-metrics-k8s-stack-759d7b8595-qbb5x --force --grace-period=0
kubectl delete pod -n mq rabbitmq-server-0 --force --grace-period=0
kubectl delete pod -n searxng searxng-67c6c9d46d-75qgn --force --grace-period=0
# 4. Longhorn은 자동 회복 (kr1 노드가 Ready로 돌아오면 instance-manager 자동 재생성, degraded 볼륨은 hp1/hp2의 정상 replica로부터 자동 rebuild)
```
## 검증
```
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
incus-hp1 Ready <none> 36d v1.34.7+k3s1
incus-hp2 Ready <none> 59d v1.34.7+k3s1
incus-kr1 Ready control-plane 59d v1.34.7+k3s1
incus-kr2 Ready control-plane 60d v1.34.7+k3s1
$ Longhorn 볼륨 robustness
26 healthy, 0 degraded # 4 → 2 → 0 degraded, ≈ 4분
```
Patroni stable, ArgoCD controller 1/1, K8s 전체 비정상 pod 0.
## 함께 처리한 별건 (같은 점검 흐름에서 드러남)
| 별건 | 상태 | 진짜 원인 |
|---|---|---|
| `apisix-etcd-0` raft 손상 (2026-05-22 23:00) | 복구 완료 (별도 procedure: member remove → PVC wipe → re-add) | kr1 instance-manager가 Terminating으로 매달리면서 etcd replica write 끊김 |
| `cs-cloudflare-bouncer` 5/2부터 down | 발견만, 미복구 (`fall-mvp.com` zone이 CF에서 삭제됨, config 정리 필요) | kr1 사건과 무관, 별건 |
| Longhorn 4 볼륨 degraded + 1 unknown | k3s restart 후 자동 회복 (4분) | kr1 노드가 NotReady라 그 위 replica 사라진 결과 |
## 교훈
- **`Active: activating` 12시간은 디버깅 우선순위 최상위.** 단순 stop+start로 풀리는 경우가 압도적이고, 그 사이 노드의 모든 pod가 stale로 변해 다른 시스템에 cascading damage를 줄 수 있음.
- **K3s/Longhorn은 그 위에 올라간 워크로드(여기서는 etcd, ArgoCD, Patroni 등)와 결합도가 매우 높음.** 노드 1개의 stuck process가 분산 시스템 전체의 정합성을 깨뜨릴 수 있다.
- **kr1 k3s가 처음에 왜 멈췄는지는 미확인.** Patroni가 일시적으로 leader가 없던 시점에 kine connect 실패 → graceful retry 못하고 process 자체가 stuck 상태로 진입한 가설이 가장 가능성 높음. 재현 못 함.
- **재발 방지**: kr1 NotReady 알림을 받으면 (1) systemctl status k3s.service의 ActiveState 확인 → (2) `activating`이면 즉시 restart, 다른 가설 점검 전 먼저. activating이 5분 넘으면 100% 비정상.
- **Patroni leader가 stuck 노드 위에 있을 때의 위험**: postgres-2(kr1)가 leader인 상태에서 kr1이 NotReady가 되면 k3s kine이 leader에 connect는 되지만 process 자체가 stuck. Patroni가 leader를 다른 노드로 옮기지 못해서(컨테이너 자체는 정상이라 demote 트리거 없음) 결국 stuck 노드의 process 재시작이 유일한 해결.
## 참조
- [[../infra/compute/hosts/incus-kr1|incus-kr1 호스트]]
- [[../infra/data/postgresql-ha|Patroni PostgreSQL HA]]
- [[../infra/platform/longhorn|Longhorn]]
- [[2026-04-04-usb-25g-hang|2026-04-04 kr1 USB hang]] — 다른 패턴이지만 kr1 관련
- [[2026-05-20-kr2-multus-shim-etxtbsy|2026-05-20 kr2 multus-shim 데드락]] — 비슷하게 K3s 재시작 후 회복 못 한 케이스 (다른 원인)

View File

@@ -0,0 +1,109 @@
---
date: 2026-05-26
topic: NAS r8152 드라이버 v2.20.1-1 → v2.21.4-1 업그레이드 — LPM 재발 대응 + spk_su 설치 절차 함정
areas: [infra/data/nas-storage]
tags: [history, nas, r8152, synology, usb, driver, lpm]
---
# 2026-05-26 / NAS r8152 v2.21.4-1 업그레이드
## 배경
5/20 NAS eth2 카드 물리 교체(rev 17 → 14) 후 4일 만에 LPM disconnect 재발 ([[../infra/data/nas-storage#nas-eth2-usb-nic-watchdog-2026-04-14|nas-storage]] 참조). 시간당 ~1회 빈도. watchdog가 자동 복구해 서비스 영향 0이지만 빈도가 높아 드라이버 업그레이드로 LPM 처리 개선 시도.
| 항목 | Before | After |
|---|---|---|
| 드라이버 | r8152 v2.20.1 (2025-05-13) | **r8152 v2.21.4 (2025-10-28)** |
| Synology 패키지 | r8152-braswell-2.20.1-1 | r8152-braswell-2.21.4-1_7.3 |
| 출처 | bb-qq/r8152 (Synology community) | 동일 |
## 설치 절차의 함정 — spk_su
bb-qq 패키지는 DSM 7 release에서 **첫 install이 의도적으로 실패**한다. postinst 스크립트가 `/opt/sbin/spk_su` (setuid root) 호출을 시도하는데, 이건 패키지가 자체 번들로 제공하면서 수동 install을 요구하는 부트스트랩 도구. README 명시:
> The installation will fail the first time. After that, run the following command from the SSH terminal:
> ```
> sudo install -m 4755 -o root -D /var/packages/r8152/target/r8152/spk_su /opt/sbin/spk_su
> ```
즉 표준 절차:
1. SPK install → postinst가 spk_su 부재로 fail → 패키지 broken 상태
2. SPK 내부의 spk_su를 `/opt/sbin/`으로 manual 설치
3. SPK 재install → 이번엔 postinst 성공 → 패키지 정상화
**처음에 우리가 spk_su 단계를 모르고 진행해서** 시간이 추가로 들었음. 첫 install 직후 eth2가 사라지고(모듈 unload), broken 상태에서 start도 불가. 임시 복구는 `insmod /volume1/@appstore/r8152/r8152/r8152.ko` 직접 호출 + `ifconfig eth2 ... up`로 가능.
## 실제 수행한 복구 + 정상화 순서
```bash
# 1. 첫 install 실패 후 긴급 복구
sudo insmod /volume1/@appstore/r8152/r8152/r8152.ko
sudo ifconfig eth2 192.168.205.100 netmask 255.255.255.0 mtu 9000 up
# 2. spk_su 설치 (bb-qq README)
sudo install -m 4755 -o root -D /var/packages/r8152/target/r8152/spk_su /opt/sbin/spk_su
# 3. SPK 재install
curl -sSL -o /tmp/r8152.spk 'https://github.com/bb-qq/r8152/releases/download/2.21.4-1/r8152-braswell-2.21.4-1_7.3.spk'
sudo rm -f /var/packages/r8152/startFailed /var/packages/r8152/installing
sudo /usr/syno/bin/synopkg install /tmp/r8152.spk # 모든 script code=0 SUCCESS
# 4. 우리가 수동 insmod한 모듈을 unload해야 패키지 start의 insmod가 충돌 안 함
sudo rmmod r8152
sudo /usr/syno/bin/synopkg start r8152 # status=running
# 5. eth2 MTU 9000 재설정 (패키지 start는 DSM ifcfg-eth2 기준이라 MTU 1500으로 올라옴)
sudo ifconfig eth2 mtu 9000
```
## 검증
```
$ sudo /usr/syno/bin/synopkg status r8152
status: running
$ ethtool -i eth2
driver: r8152
version: v2.21.4 (2025/10/28)
$ ifconfig eth2 | head -4
eth2 UP BROADCAST RUNNING MULTICAST MTU:9000
inet addr:192.168.205.100 ...
$ sudo ping -c 2 -M do -s 8972 -I eth2 192.168.205.135
2 packets transmitted, 2 received, 0% packet loss
rtt avg = 0.458 ms # end-to-end JF 통과
```
## 새 드라이버에서 변경된 점
bb-qq 2.21.4-1 release notes:
- Fix transmit queue timeout (PR #609)
- Realtek base code 2.21.4 업데이트
- `tx-copybreak` ethtool 옵션 추가
- TX flow-control 개선
- 마이크로코드 minor 업데이트
LPM 관련 fix는 명시 안 됨. 다만 base code 갱신에 LPM 처리 변화가 포함됐을 가능성. **효과 검증은 LPM 발화 빈도로 며칠 관찰 필요.**
## 한계
- r8152 모듈 파라미터는 DSM 3.10 커널이 sysfs `/sys/module/r8152/parameters/`에 노출하지 않음 → LPM 옵션 modparam으로 제어 불가
- DSM의 `usbcore.autosuspend` 모듈 옵션도 커널 컴파일 옵션이라 비활성. `/usr/local/etc/rc.d/usb-no-suspend.sh` 스타트업 스크립트는 sysfs `power/control`, `power/autosuspend` 파일 부재로 실효 없음 (확인 2026-05-26)
- 즉 LPM 제어는 드라이버 코드 내부에서만 가능하며, 호스트 측 회피 수단이 거의 봉쇄됨
## 교훈 + 후속 작업
- **bb-qq r8152 SPK는 첫 install fail이 정상**. spk_su manual install이 표준 절차. 다음 업그레이드 시 시간 낭비 없도록 기억.
- LPM 발화 빈도를 며칠(2~3일) 관찰. 줄지 않으면 다음 옵션:
- 다른 칩셋 USB NIC (ASIX AX88179)로 교체
- Thunderbolt/PCIe 2.5G 어댑터로 전환 (DS916+ 확장 슬롯 검토)
- spk_su `/opt/sbin/spk_su`는 영구 설치됨 — 다음 r8152 SPK 업그레이드부터는 첫 install이 바로 성공할 것
## 참조
- [[../infra/data/nas-storage|nas-storage 정본]]
- [[2026-04-14-nas-eth2-watchdog|watchdog 구축 — 2026-04-14]] (간접)
- [[2026-05-20-nas-eth2-replacement|카드 물리 교체 — 2026-05-20]]
- bb-qq 패키지: <https://github.com/bb-qq/r8152>
- 2.21.4-1 release: <https://github.com/bb-qq/r8152/releases/tag/2.21.4-1>

View File

@@ -0,0 +1,50 @@
---
date: 2026-05-29
topic: termix `:latest``release-2.3.1` 업그레이드 + image.tag 명시 고정
areas: [services, infra/k3s]
tags: [history, termix, k3s, argocd, helm, image-tag]
---
# 2026-05-29 / termix release-2.3.1 업그레이드
## 변경 요약
| 항목 | Before | After |
|---|---|---|
| Image tag | `:latest` (digest `577c0e`, 3d8h 전 pull) | **`release-2.3.1`** (digest `8db77c`) |
| Chart 버전 | 0.1.2 | **0.1.4** |
| `image.tag` 관리 | 미명시 (`:latest` 부유) | **chart values에 명시 고정** |
| ArgoCD 상태 | Synced/Healthy (구버전) | Synced/Healthy (신버전) |
| Pod | Running RESTARTS=0 | Running RESTARTS=0 |
| 헬스체크 | — | https://termix.inouter.com/ HTTP 200 |
배포 chart commit: `5d3b027`.
## 트러블슈팅 — ImagePullBackOff (1차 실패)
처음 시도 시 `image.tag: release-2.3.1-tag`로 작성 → `ImagePullBackOff`.
원인: GitHub release tag 이름과 Docker registry tag 이름이 다름.
- GitHub release: `release-2.3.1-tag`
- Docker registry tag: `release-2.3.1` (suffix `-tag` 없음)
수정: chart values의 `image.tag`에서 `-tag` suffix 제거 → 정상 pull.
## 정책 노트 — image.tag 명시 고정 ❌
이번에 chart values에 `image.tag: release-2.3.1`을 명시 고정했지만, **사용자 정책상 잘못된 변경**이다.
- 디폴트는 `:latest` 유지. 업그레이드는 pod 재시작 한 번으로 충분.
- 명시 고정은 (a) chart 매번 수정 + (b) tag 이름 매칭 (이번 `-tag` suffix처럼) 함정 + (c) 자동 갱신 차단 → 비용만 늘림.
- 사용자 정책: 명시 고정은 "이 버전이어야만 하는 명확한 이유"가 있을 때만. 단순 업그레이드 작업에서 임의로 도입하지 말 것.
- 처리: 헤임달에 `image.tag` 줄 제거 + `:latest` 복귀 작업 위임 (2026-05-29 동일자).
수행자 노트: kappa가 헤임달 변경 + 이 history의 초안 가이드("좋은 변경") 모두 사용자 정책 위배로 정정. 향후 헤임달 위임 시 본 정책을 명시 전달.
## 검증
- `kubectl get pod -n termix`: Running, RESTARTS=0
- ArgoCD: Synced, Healthy
- `curl -sI https://termix.inouter.com/`: HTTP/2 200
수행: [[../ops-agents/heimdall/_index|헤임달]] (소요 ~6분)

View File

@@ -0,0 +1,124 @@
---
date: 2026-06-01
topic: Longhorn sftpgo replica corruption loop 해소 + 시스템 차원 snapshot 자동 retention 영구 수정 (RecurringJob 라벨 누락)
areas: [infra/k3s, infra/storage/longhorn]
tags: [history, longhorn, k3s, sftpgo, snapshot, recurring-job, retention]
---
# 2026-06-01 / Longhorn snapshot retention 시스템 차원 수정
## 시작 — sftpgo replica corruption loop
Longhorn UI에 "Warning". 진단 결과 `sftpgo/sftpgo` PVC(1Gi, 실사용 50MB)의 kr1 replica가 13분 주기로 `Detected corrupted replica tcp://10.42.1.189:...` 마킹 → rebuild → 또 마킹 무한 반복.
| 항목 | 값 |
|---|---|
| 영향 PVC | `sftpgo/sftpgo` |
| Replica 분포 | kr1(❌) / hp1(✅) / hp2(✅) — 3-way |
| Corruption 마킹 출처 | longhorn-manager engine controller |
| 발생 간격 | ~13분 (rebuild 끝나자마자 또 fault) |
| 영향 워크로드 | sftpgo (실서비스 영향 0, 자동 rebuild로 데이터 0 손실) |
## 진단 — kr1 디스크는 정상
| 점검 | 결과 |
|---|---|
| kr1 dmesg (err/warn) | I/O 에러 0, NVMe 에러 0, ext4 에러 0. systemd-sysv 잡소리만 |
| kr1 디스크 용량 | nvme0n1 28% (247G/937G), `/var/lib/longhorn` 114G — 여유 충분 |
| kr1 uptime | 30일, 최근 재기동 없음 |
| kr1 위 다른 replica 18개 | 전부 running — 디스크 자체 정상 |
| iSCSI 5/27 SCSI reset 1회 | 무관 |
**하드웨어 이슈 아님**. Longhorn chain 레벨의 메타데이터 손상 의심.
## Root cause 추적
instance-manager-kr2 (rebuild 시) 로그에서 핵심 단서:
```
level=error msg="Failed to get recorded metadata for file volume-snap-snapshot-...img"
error="failed to open checksum file: ...img.checksum: no such file or directory"
```
= snapshot의 `.checksum` 파일이 source 측에 누락. snapshot-data-integrity(`fast-check`)가 checksum 기반으로 비교하는데 일부 snapshot에 checksum 부재 → integrity check fail → "corrupted" 마킹 → rebuild → 같은 chain을 source에서 받음 → 또 fail → 루프.
sftpgo만의 특별한 원인은 없음 (다른 25개 volume도 더 많은 snapshot 보유, 같은 환경). **우연한 chain 메타 손상이 first cause, integrity check가 루프 증폭**.
## 해소 절차
1. Volume의 orphan `recurring-job-group.longhorn.io/default=enabled` 라벨 제거 (정의 안 된 group)
2. kr1 replica `r-750f60ae` 명시적 삭제
3. → Longhorn anti-affinity가 kr1을 회피해 **kr2에 새 replica `r-4cee3e8e` 생성**
4. fresh chain을 hp1에서 sync → 39 snapshot 모두 새로 받음
5. Volume robustness: degraded → healthy, RW 3-replica 안정화
6. 2분 post-healthy 관찰 — **재발 0**
## 시스템 차원 이슈 발견 — snapshot 자동 retention 깨짐
별건 점검에서 전체 906개 snapshot 모두 `recurring-job.longhorn.io/<job-name>` 라벨이 **없음**. 1시간 전 만든 신규 snapshot도 동일.
| 항목 | 데이터 |
|---|---|
| 전체 snapshot | 906개 |
| `recurring-job.longhorn.io/*` 라벨 보유 | **0개 (100% 누락)** |
| Longhorn 버전 | v1.11.2 |
| RecurringJob spec.labels | **`{}` (4개 모두 비어있음)** ← 원인 |
Longhorn RecurringJob retention 메커니즘:
- RecurringJob이 만든 snapshot에 자기 라벨(`recurring-job.longhorn.io/<job-name>=enabled`) 부착
- retention 시 그 라벨로 자기 소속 snapshot 식별 → retain 개수 외 삭제
`spec.labels`가 비어 있으면 snapshot에 라벨 안 붙음 → retention 동작 불가 → **모든 RecurringJob snapshot이 무한 누적**. 알려진 동작 (명시 안 하면 자동 안 붙임).
## 정리 — snapshot 906 → 194
각 volume에서 volume-head + 최신 7개 chain만 유지하고 나머지 일괄 delete (`kubectl delete snapshot.longhorn.io`). Longhorn finalizer가 chain coalesce 자동 처리.
| 지표 | Before | After |
|---|---|---|
| 전체 snapshot | 906 | **194** |
| volume당 평균 | ~35 | **7~8** |
| Volume robustness | 26 healthy | 26 healthy (변동 0) |
| 데이터 영향 | — | 0 (backup은 S3에 별도 보관) |
## 영구 수정 — RecurringJob 라벨 정책 patch
4개 RecurringJob에 자기 라벨을 `spec.labels`로 명시:
```bash
for j in critical-snapshot critical-backup standard-snapshot standard-backup; do
kubectl -n longhorn-system patch recurringjob.longhorn.io $j --type=merge \
-p "{\"spec\":{\"labels\":{\"recurring-job.longhorn.io/$j\":\"enabled\"}}}"
done
```
다음 cron 실행부터 새 snapshot에 라벨 붙음 → retention 자동 동작 시작.
| Job | spec.labels (After) |
|---|---|
| critical-snapshot | `recurring-job.longhorn.io/critical-snapshot: enabled` |
| critical-backup | `recurring-job.longhorn.io/critical-backup: enabled` |
| standard-snapshot | `recurring-job.longhorn.io/standard-snapshot: enabled` |
| standard-backup | `recurring-job.longhorn.io/standard-backup: enabled` |
## 검증 (예정)
다음 critical-snapshot 실행 (매시간 정각) 후:
```bash
kubectl -n longhorn-system get snapshots.longhorn.io --sort-by=.metadata.creationTimestamp -o json \
| jq -r '.items[-3:] | .[] | "\(.metadata.creationTimestamp) \(.metadata.name) \(.metadata.labels)"'
```
→ 새 snapshot에 `recurring-job.longhorn.io/critical-snapshot=enabled` 있으면 OK.
## 잔여 사항 / 후속
- RecurringJob 4개 spec.labels 수동 patch만 적용. **ArgoCD/IaC 매니페스트에도 같은 변경 반영 필요** (다음 sync에서 빈 spec.labels로 되돌아갈 위험)
- snapshot `.checksum` 파일 누락 패턴은 Longhorn 알려진 이슈일 가능성 — GitHub issue 검색 후 v1.11.3+ 업그레이드 시 해소되는지 확인 필요
- 같은 corruption loop가 다른 volume에서 재발 시 동일 절차 (anti-affinity 활용한 노드 회피 rebuild) 적용 가능
## 운영 메모
- 로컬 Mac Tailscale 데몬이 작업 중 멈춤(`Tailscale is stopped.`, 버전 mismatch 1.98.1 vs 1.96.5 의심). LAN 192.168.9.214 직접 접근으로 우회.
- 작업 중 임시 kubectl wrapper `/tmp/kk`: `kubectl --server=https://192.168.9.214:6443 --insecure-skip-tls-verify=true` (Tailscale 복구 후 폐기)
- 헤임달(10.100.3.108)은 작업 초반 진단만 위임, snapshot 정리는 kappa 직접 수행 (헤임달은 ops-agents 키가 kr1 sshd에 미등록이라 SMART 등 직접 진단 불가)
수행: kappa (직접) + [[../ops-agents/heimdall/_index|헤임달]] (초기 카테고리 진단)

38
history/_index.md Normal file
View File

@@ -0,0 +1,38 @@
---
title: history 인덱스
updated: 2026-05-04
tags: [moc, history]
---
## History
변경 이력, 인시던트 대응, 마이그레이션 과정 기록. 정본 문서와 분리하여 날짜별로 관리.
| 문서 | 설명 |
|------|------|
| [[2026-03-15-apisix-git-push-500]] | APISIX git push 500 에러 + http-logger 401 해결 |
| [[2026-03-24-k3s-postgresql-migration]] | K3s PostgreSQL 백엔드 이전 (etcd → Supabase → Patroni) |
| [[2026-03-25-apisix-to-traefik-routing]] | 메인 HTTP 라우팅 APISIX → Traefik 전환 |
| [[2026-03-31-netbis-ddos-attack]] | Netbis 도메인 대규모 봇 공격 및 대응 |
| [[2026-03-various-gitea-changes]] | Gitea 관련 이전/분리/도메인 변경 (3월) |
| [[2026-04-04-usb-25g-hang]] | USB 2.5GbE 어댑터 절전 hang + NFS hard mount D-state 장애 |
| [[2026-04-05-supabase-to-patroni]] | K3s 데이터스토어 Supabase → Patroni PostgreSQL HA 이전 |
| [[2026-04-06-apisix-etcd-consolidation]] | APISIX etcd 통합/분리 과정 |
| [[2026-04-08-anomaly-detect-iterations]] | anomaly-detect 3차 재설계 (gemma → cohort → agentic Grok-4) |
| [[2026-04-08-patroni-failover-incident]] | Patroni failover 사고 (pgcat/nocodb/outline read-only) |
| [[2026-04-08-zlambda-nixos-migration]] | zlambda Debian → NixOS 전환 |
| [[2026-04-09-ops-agents-setup]] | Ops Agents (Heimdall/Syn) 구축 |
| [[2026-04-10-edge-cleanup]] | Edge layer cleanup (BunnyCDN + Cloudflare 전수 감사) |
| [[2026-04-15-apisix-http-logger-removal]] | APISIX → CrowdSec http-logger 레거시 경로 제거 |
| [[2026-04-15-longhorn-backup-label-typo]] | Longhorn recurring job 라벨 오타로 백업 미동작 |
| [[2026-04-15-pgcat-ha-promotion]] | pgcat HA 승격 (Step 0) |
| [[2026-04-15-vault-mcp-duplicate-investigation]] | vault-mcp-server 중복 배포 의혹 조사 |
| [[2026-04-16-kine-multihost-migration]] | kine pgx multi-host 직결 (HAProxy 의존 제거) |
| [[2026-04-16-pgcat-patroni-tcp-keepalive]] | pgcat + Patroni TCP keepalive 적용 |
| [[2026-04-16-pgpool-full-migration]] | pgpool-II 전면 전환 + pgcat 퇴역 |
| [[2026-04-16-pgpool-n8n-poc]] | pgpool-II PoC (n8n 전용 전환) |
| [[2026-04-25-anomaly-detect-removal]] | anomaly-detect 폐기 (오탐 다수, 컨테이너 포함 완전 제거) |
| [[2026-04-25-netbis-npm-vector-msg-rewrite]] | NPM Vector `_msg` 재합성 (proxy_v2 → nginx combined) — child-nginx-logs unparsed 84% 해결 |
| [[2026-04-26-bouncer-consolidation]] | CrowdSec bouncer 단일화 (netbis-cf-firewall만 유지, 나머지 3종 + Edge Script 64811 + Turnstile 위젯 6개 폐기) |
| [[2026-05-04-amd-iommu-freeze]] | incus-kr2 AMD-Vi (IOMMU) Completion-Wait timeout 호스트 부분 hang — `iommu=pt` 적용 |
| [[2026-05-17-safeline-pvc-fsck-incompat]] | SafeLine RWX PVC (mgt/luigi/tengine-logs) ext4 `Resize inode not valid` — Longhorn v1.11.2 strict fsck 거부, PVC 재생성으로 복구 |

23
infra/_index.md Normal file
View File

@@ -0,0 +1,23 @@
---
title: infra 인덱스
updated: 2026-04-27
tags: [moc, infra]
---
## Infra
인프라 전반을 다루는 최상위 카테고리. 5개 서브카테고리로 구성.
| 카테고리 | 설명 |
|----------|------|
| [[infra/compute/_index\|compute]] | 서버, 호스트, 컨테이너, VM, K3s 노드 |
| [[infra/network/_index\|network]] | 라우팅, 인그레스, 로드밸런서, SMTP, SSH 프록시 |
| [[infra/security/_index\|security]] | 인증서, 시크릿, WAF, 접근 관리 |
| [[infra/data/_index\|data]] | 데이터베이스, 스토리지, 백업 |
| [[infra/platform/_index\|platform]] | 플랫폼 서비스, CI/CD, 모니터링, 위키 |
## 도메인 경계 (cross-cutting)
| 문서 | 설명 |
|------|------|
| [[domain-boundaries]] | netbis 영역 vs ironclad 영역 — 자산/계정/책임 분리 + cross-domain 데이터 흐름 |

View File

@@ -1,171 +0,0 @@
---
title: anomaly-detect (VictoriaLogs + ollama 기반 이상 트래픽 감지)
updated: 2026-04-08 agentic 재설계
tags: [security, crowdsec, victorialogs, ollama, gemma, anomaly]
---
> 코드: `gitea.inouter.com/kaffa/anomaly-detect` (private)
> 아키텍처: OpenRouter agentic (Grok-4-fast) + VictoriaLogs tool + CrowdSec LAPI tool
# anomaly-detect
## 아키텍처
OpenRouter `x-ai/grok-4-fast`에 tool 2개(logsql_query, ban_ips)만 노출하는 agentic 구조. fallback 모델 `qwen/qwen3-235b-a22b-2507`.
설계 반복 및 모델 벤치마크 이력: [[../history/2026-04-08-anomaly-detect-iterations|history]]
### 새 아키텍처
```
systemd timer (5분)
analyzer.py (Python oneshot)
├─ OpenRouter → x-ai/grok-4-fast (tools=[logsql_query, ban_ips])
├─ agent loop (max 10턴)
│ ├─ tool: logsql_query(query, start, limit) — VictoriaLogs 자유 조회
│ │ ※ 응답에서 사설망/Tailscale/RFC5737 IP 자동 제거 (서버측 guardrail)
│ └─ tool: ban_ips(ips, reason, scenario) — CrowdSec LAPI batch POST
│ ※ dedup 24h, MAX_BAN_PER_CYCLE cap, 사설망 거부
├─ DRY_RUN=1 (기본): ban_ips가 "would ban" 로그만 찍고 실제 POST 안 함
└─ dedup.json 갱신
```
### 환경변수
| 변수 | 기본값 | 비고 |
|------|--------|------|
| `OPENROUTER_API_KEY` | (from `/etc/anomaly-detect/openrouter.env`) | Vault `secret/ai/openrouter` |
| `OR_MODEL` | `x-ai/grok-4-fast` | 주 모델 |
| `OR_FALLBACK_MODEL` | `qwen/qwen3-235b-a22b-2507` | OR 장애 시 재시도 |
| `WINDOW_MIN` | 5 | 조사 윈도우 |
| `MAX_TURNS` | 10 | agent loop 상한 |
| `MAX_BAN_PER_CYCLE` | 2000 | 한 사이클 ban 상한 (대규모 DDoS 대비) |
| `LAPI_BAN_CHUNK` | 500 | LAPI POST를 500건씩 쪼개 발송 (부분 실패 허용) |
| `BAN_DURATION` | 4h | |
| `DRY_RUN` | **1** | ⚠ 초기 안전장치 |
### 서버측 guardrail (중요)
LLM은 지시만 받고 강제할 수 없다. 따라서 `logsql_query``ban_ips` 두 tool 모두 **파이썬 코드 레벨에서** 다음을 enforce:
- `is_skippable_ip()`: `ipaddress.is_private` + Tailscale 100.64/10 + RFC5737 TEST-NET-2/3 포함
- Python 3.9의 `ipaddress.ip_address('203.0.113.42').is_private == True` — 문서 IP도 자동 차단됨 (2026-04-08 벤치마크에서 확인)
LLM 프롬프트가 무시되어도 실수로 사설망이 ban되지 않음.
### Vault 위치
- OpenRouter key: `secret/ai/openrouter` (`API_KEY` 키)
- 컨테이너 배포본: `/etc/anomaly-detect/openrouter.env` (mode 600, systemd `EnvironmentFile=`)
### 운영 중 주의사항
- **injected 로그 잔재**: E2E 테스트 중 주입한 270 rows가 vlogs retention 기간 동안 남음. `sim_e2e` 마커 필드로 식별 가능. 다음 cycle에서 다시 탐지될 수 있으나 dedup 24h으로 재ban 차단됨.
- **재테스트 시**: 테스트 후 반드시 `cscli decisions delete -s anomaly-detect/<scenario>` + `/var/lib/anomaly-detect/dedup.json` 업데이트 (해당 IP 추가하거나 리셋)
### 비용 모니터링
매 사이클 journalctl에 한 줄 요약 출력 (`48eb489` 이후):
```
cycle usage: turns=5 prompt=9142 completion=2670 total=11812 cost=$0.001866
```
집계 명령:
```bash
# 오늘 누적
ssh incus-hp2 "incus exec anomaly-detect -- bash -c '
journalctl -u anomaly-detect.service --since today --no-pager |
grep -oE \"cost=\\\$[0-9.]+\" |
awk -F\\\$ \"{sum+=\\\$2} END {printf \\\"today: \\\$%.6f (%d cycles)\\n\\\", sum, NR}\"
'"
# 사이클별 상세
ssh incus-hp2 "incus exec anomaly-detect -- journalctl -u anomaly-detect.service --since today --no-pager | grep 'cycle usage'"
```
**OpenRouter `/activity` API의 지연** (~2-3주)으로 실시간 비용 조회는 여기(journalctl) 경로를 써야 한다. dashboard는 https://openrouter.ai/activity 에서 UI 확인 가능 (더 빠름). management(provisioning) 키는 Vault `secret/ai/openrouter``PROVISIONING_KEY` 에 저장됨.
**월 비용 예상**: 평시 사이클당 ~$0.001-0.002, 5분 주기 × 288/일 × 30일 = **~$10-15/월**. DDoS 발생 시 사이클당 $0.01 수준 (massive_ddos 시나리오 기준)까지 튈 수 있으나 일시적.
[[crowdsec-safeline#~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)|폐기된 ddos-detect]] 후속. [[victorialogs|VictoriaLogs]]에 적재된 서울+오사카 APISIX access log를 5분마다 분석하여 봇/공격성 IP를 [[crowdsec-safeline|CrowdSec]]에 자동 ban으로 등록한다.
## 위치 / 사양
| 항목 | 값 |
|------|-----|
| 호스트 | incus-hp2 |
| 컨테이너 | `anomaly-detect` (default 프로젝트, Debian 13 trixie) |
| IP | 10.100.2.164 |
| 사양 | 1 vCPU, 512MB RAM, 5GB |
| 설치 경로 | `/opt/anomaly-detect/{venv,analyzer.py}`, `/etc/anomaly-detect/lapi.yaml`, `/etc/anomaly-detect/openrouter.env`, `/var/lib/anomaly-detect/dedup.json` |
| systemd | `anomaly-detect.service` (oneshot) + `anomaly-detect.timer` (`OnCalendar=*:0/5`, `Persistent=true`, `RandomizedDelaySec=20`) |
## 데이터 흐름
```
[5분 주기 systemd timer]
analyzer.py
├─ 1) https://vl.inouter.com — LogsQL: program:apisix log_type:access 지난 5분
├─ 2) per-IP 통계 게이트 (count/4xx/5xx/499/distinct paths)
├─ 3) 후보 N개 (default max 5)
├─ 4) 각 후보 → http://100.87.221.126:11434/api/generate (kaffa-macmini ollama)
│ 모델: gemma4:e4b (Q4_K_M, 8.0B), format=json
├─ 5) verdict=yes → CrowdSec LAPI alert POST
│ http://10.253.100.240:8080/v1/alerts
│ profiles.yaml의 default_ip_remediation이 자동 ban 생성
└─ 6) dedup.json에 처리 IP + 타임스탬프 기록 (24h 내 재처리 안 함)
```
## CrowdSec LAPI 등록
`anomaly-detect`라는 watcher machine을 jp1 crowdsec에 등록하고, credentials를 컨테이너 안 `/etc/anomaly-detect/lapi.yaml`에 저장:
```yaml
url: http://10.253.100.240:8080
login: anomaly-detect
password: <vault: secret/apps/anomaly-detect>
```
> [!warning] cscli machines add 함정
> `cscli machines add NAME --auto`는 default로 `/etc/crowdsec/local_api_credentials.yaml`을 덮어씀 — 이건 jp1 crowdsec **daemon 자체의 LAPI 클라이언트 설정**이라 덮어쓰면 daemon이 새 password로 LAPI 인증을 시도하면서 동기화가 깨짐. 반드시 `--file <별도 경로>` 옵션을 줘야 한다. 만약 실수로 덮어썼다면 `cscli machines add <default-machine-name> --auto --force --file /etc/crowdsec/local_api_credentials.yaml`로 default machine 새 credentials 발급 후 `systemctl reload crowdsec`로 복구.
## 운영 명령
```bash
# 컨테이너 진입
ssh incus-hp2 'incus exec anomaly-detect -- bash'
# 수동 1회 실행
incus exec anomaly-detect -- /opt/anomaly-detect/venv/bin/python /opt/anomaly-detect/analyzer.py
# 상태
incus exec anomaly-detect -- systemctl status anomaly-detect.timer
incus exec anomaly-detect -- journalctl -u anomaly-detect.service --since "30 minutes ago"
# dedup 초기화 (모든 IP 재분석 허용)
incus exec anomaly-detect -- sh -c 'echo "{}" > /var/lib/anomaly-detect/dedup.json'
# 게이트/모델/주기 변경 → /etc/systemd/system/anomaly-detect.service의 Environment=
```
## 검증 (최초 배포)
- `cscli machines list``anomaly-detect` 등록 확인
- `curl https://vl.inouter.com/select/logsql/query?query=program:apisix\&limit=1` 200 OK (컨테이너 내부)
- `curl http://100.87.221.126:11434/api/tags``gemma4:e4b` 노출
- `curl http://10.253.100.240:8080/v1/decisions` → 403 (인증 필요, 네트워크 OK)
- 더미 IP `198.51.100.99`로 alert POST → 201 + decision 등록 → cleanup 확인 (smoke test)
## 향후 작업
- [ ] **[Medium]** Discord webhook 알림 추가 (`secret/apps/discord` Vault에서 가져오기) + systemd `OnFailure=` drop-in
- [ ] **[Low]** CrowdSec alert `origin``"crowdsec"``"anomaly-detect"`로 태깅
전임자 (`ddos-detect`, 1/2차 구현) 폐기 이력: [[../history/2026-04-08-anomaly-detect-iterations|history]]

15
infra/compute/_index.md Normal file
View File

@@ -0,0 +1,15 @@
---
title: compute 인덱스
updated: 2026-05-05
tags: [moc, compute]
---
## Compute
| 문서 | 설명 |
|------|------|
| [[infra-hosts]] | 인프라 호스트 및 네트워크 (SSH 접속, Incus, Tailscale) |
| [[amd-vi-iommu]] | AMD-Vi (IOMMU) Completion-Wait timeout 부분 hang 메커니즘과 차단 (`iommu=pt`) |
| [[k3s-migration]] | K3s PostgreSQL 백엔드 이전 기록 |
| [[k3s-to-incus-migration]] | K3s → Incus 워크로드 이전 플랜 (2026-06 결정) |
| [[zlambda]] | zlambda Linode 도쿄 NixOS VM |

View File

@@ -0,0 +1,132 @@
---
title: AMD-Vi (IOMMU) Completion-Wait timeout — 부분 hang 메커니즘과 차단
updated: 2026-05-05
tags: [infra, kernel, iommu, amd, freeze, troubleshooting]
---
## 개요
AMD Ryzen 호스트에서 발생하는 IOMMU(AMD-Vi) Completion-Wait timeout으로 인한 호스트 부분 hang 패턴 정본. 처음 만난 사례는 [[2026-05-04-amd-iommu-freeze]] (incus-kr2, Ryzen 9 6900HX). AMD Ryzen 베어메탈을 신규 도입할 때마다 선제 차단(`iommu=pt`)할 것.
## IOMMU(AMD-Vi)가 하는 일
PCIe/USB 디바이스가 메모리에 DMA 할 때 **"디바이스가 보는 주소(IOVA) → 실제 물리 메모리(PA)"** 변환 수행.
- 보안: 디바이스가 임의 메모리 접근 못 하게 격리 (e.g. 악성 USB가 RAM 덤프 못 함)
- 가상화: VFIO passthrough, SR-IOV에 필수
- AMD에서는 `AMD-Vi`, Intel에서는 `Intel VT-d`라 부름. 동일 개념
## 정상 동작
```
1. OS가 페이지 매핑을 바꿀 때마다 IOMMU에 invalidation 명령 전송
→ IOMMU 내부 캐시(IOTLB)를 비우라고
2. 명령은 IOMMU의 Command Queue에 쌓임
3. 끝에 Completion-Wait 명령으로 "여기까지 다 처리됐는지 알려줘" sync
4. OS는 이 응답을 기다림 (1초 내)
```
## 펌웨어 버그 발현 시 흐름
1. 어떤 조건에서 펌웨어/HW가 Completion-Wait 응답을 못 보냄
2. Linux가 1초 후 타임아웃 → `dmesg``AMD-Vi: Completion-Wait loop timed out` 출력
3. **타임아웃이 나면 큐 상태가 inconsistent해져 다음 invalidation도 같은 큐에서 stuck**
4. 새 IO나 새 매핑이 필요한 모든 작업이 IOMMU 응답 대기로 hang
5. 누적될수록 hang하는 작업 범위가 넓어져 결국 호스트 사용자공간 대부분이 부분 hang
## 부분 hang의 특이 패턴 — "왜 일부만 죽는가"
| 영향 받는 작업 | 영향 안 받는 작업 |
|---|---|
| 새 fork (페이지 매핑) | 이미 메모리에 매핑된 프로세스 (예: nginx, node-exporter) |
| 디스크 쓰기 (NVMe DMA 셋업) | ICMP (커널 raw socket, 이미 셋업된 NIC ring buffer) |
| 새 컨테이너 시작 | LXC 컨테이너 내부에서 이미 떠 있는 서비스 |
| sshd login (utmp/journal 쓰기) | 이미 열려있는 SSH 세션 |
| containerd / incus 데몬의 신규 작업 | 라우팅·NAT처럼 커널이 자체 처리하는 트래픽 |
→ 외부에서 보면 "ICMP 응답 OK, nginx 응답 OK, 그런데 SSH banner 직후 끊기고 K3s NotReady"라는 미스터리한 부분 hang. 동시에 OOM/디스크/NIC가 모두 멀쩡 (메트릭상 정상). [[2026-05-04-amd-iommu-freeze]] 참조.
## 영향 받는 하드웨어 / 트리거 조건
- **AMD Ryzen 6000 시리즈 (Rembrandt, Zen 3+)** — 알려진 펌웨어 이슈, 다수 보고 (Linux 메일링리스트, AMD 포럼)
- **AMD Ryzen Mobile / 미니 PC** — BIOS AGESA 업데이트가 늦거나 안 되는 환경에서 더 자주 발현
- 일부 Intel 플랫폼(VT-d)에서도 비슷한 패턴 보고 있으나 본 정본은 AMD 한정
트리거되는 일반 조건:
- DMA 자주 쓰는 디바이스 (NVMe, USB 컨트롤러, 10G NIC) 다수 동시 동작
- VFIO/가상화 + LXC/Docker 같은 컨테이너 동시 동작
- 부팅 후 일정 시간 (사례에서는 24분) 후 첫 timeout, 이후 누적
## 진단 신호
```bash
# 1차 확인 — Completion-Wait timeout 발생 여부
sudo dmesg -T | grep -iE "AMD-Vi.*Completion-Wait|AMD-Vi.*timed out"
# 부분 hang 의심 시 부수 증거
ssh <host> 'echo ok' # banner 직후 끊기면 의심
kubectl get nodes # NotReady, container runtime is down
node_procs_blocked # 0이어도 안전 신호 아님 (IOMMU hang은 D-state 안 거침)
node_memory_* # 정상이어도 안전 신호 아님
```
**핵심 함정**: `node_procs_blocked = 0`, `MemoryPressure = False`, `DiskPressure = False` 다 정상이어도 hang 가능. IOMMU layer는 OS가 노출하는 압박 메트릭에 안 잡힘. **메트릭 정상 + 외부 ICMP 응답 + 새 SSH 거부 조합이면 IOMMU 의심.**
## 차단 옵션
### 1. `iommu=pt` (권장 — Passthrough 모드)
GRUB cmdline에 추가:
```bash
sudo cp /etc/default/grub /etc/default/grub.bak.$(date +%Y%m%d)-iommu
sudo sed -i -E 's/^(GRUB_CMDLINE_LINUX_DEFAULT="[^"]*)(")/\1 iommu=pt\2/' /etc/default/grub
sudo update-grub
sudo systemctl reboot
```
검증:
```bash
cat /proc/cmdline | grep iommu=pt
sudo dmesg | grep "iommu: Default domain type"
# 기대: "iommu: Default domain type: Passthrough (set via kernel command line)"
```
**왜 해결되는가**: `pt` 모드는 **호스트 자신의 DMA에 대해 IOMMU 변환을 1:1 mapping으로 고정** → invalidation 거의 안 씀 → Completion-Wait 자체가 발생하지 않음 → 펌웨어 버그 트리거 안 됨. VFIO/SR-IOV 가상화는 여전히 IOMMU 사용 가능 (격리 유지). 트레이드오프는 호스트 자체 DMA 격리가 약해지는 것 — 베어메탈 + 알려진 디바이스 환경에서는 보통 수용 가능한 비용.
### 2. `amd_iommu=off` (차선)
IOMMU 자체를 비활성. VFIO passthrough 못 함, SEV/Confidential Computing 못 함. 가상화 안 쓰는 호스트면 가능하나 기본 권장 X.
### 3. BIOS AGESA 업데이트 (장기 대책)
펌웨어 fix가 들어간 AGESA 마이크로코드로 업데이트. Mini PC 제조사가 BIOS를 늦게 내거나 안 내면 불가능. 가능할 때 적용.
### 4. 커널 업그레이드
Linux 6.13+에서 일부 우회 패치 머지된 사례 있음. 효과는 트리거 조건에 따라 다름. `iommu=pt`로 먼저 차단하고 부수적으로 검토.
## 운영 규칙 (kappa 인프라)
1. **AMD Ryzen 베어메탈 신규 도입 시 처음부터 GRUB cmdline에 `iommu=pt` 박을 것.** 사고 경험한 다음 적용은 늦음 — 첫 freeze까지 평균 2주 간격.
2. 호스트 문서(`infra/compute/hosts/<name>.md`)의 GRUB 섹션에 `iommu=pt` 표시.
3. 기존 호스트 점검: `ssh <host> 'cat /proc/cmdline'` 해서 `iommu=pt` 없으면 다음 정기 재부팅 시 추가.
4. 시간대 `Asia/Seoul` 강제. 잘못된 timezone(예: PDT)은 dmesg 분석 시 시간 변환 오류로 잘못된 결론 유도 ([[2026-05-04-amd-iommu-freeze]] 헤임달 1차 진단 오판 사례).
5. AMD Ryzen 호스트는 OOB 전원 제어(Tapo 또는 IPMI) 필수. 펌웨어 hang은 OS shutdown으로 못 풀고 강제 전원 차단만 가능.
## 현재 적용 호스트 상태
| 호스트 | CPU | iommu=pt | 비고 |
|---|---|---|---|
| [[incus-kr2]] | AMD Ryzen 9 6900HX | ✓ (2026-05-04) | 본 사건 진원지 |
| [[incus-kr1]] | unknown | — | 점검 필요 |
| [[incus-hp1]] | Intel Xeon E5-2670 | N/A | Intel, AMD-Vi 무관 |
| [[incus-hp2]] | Intel Xeon E5-2670 | N/A | Intel, AMD-Vi 무관 |
## 참조
- [[2026-05-04-amd-iommu-freeze]] — 진단 + 조치 사연
- [[infra-hosts]] — 인프라 호스트 정본
- Linux kernel: `drivers/iommu/amd/iommu.c``__domain_flush_pages`, `iommu_completion_wait`
- AMD I/O Virtualization Technology Specification (IOMMU)

View File

@@ -55,4 +55,4 @@ Incus + K3s 워커 호스트 (서울존). HP ProLiant DL360p Gen8 베어메탈.
- hp2와 동일 Gen8 스펙이나 2.5G LAN 미탑재 (1G only) - hp2와 동일 Gen8 스펙이나 2.5G LAN 미탑재 (1G only)
- Longhorn replica 참여 시 nvme에 별도 마운트 필요 - Longhorn replica 참여 시 nvme에 별도 마운트 필요
상세 인프라 컨텍스트: [[../infra-hosts]] 상세 인프라 컨텍스트: [[infra-hosts]]

View File

@@ -38,6 +38,6 @@ Incus + K3s 워커 호스트 (서울존). 홈랩 bare-metal 서버. Incus 프로
## 상세 ## 상세
- Incus 컨테이너이므로 커널은 호스트 bare-metal과 공유 (커널 업데이트는 호스트에서) - Incus 컨테이너이므로 커널은 호스트 bare-metal과 공유 (커널 업데이트는 호스트에서)
- 2026-04-13 drain+reboot 테스트 완료 → [[../../tasks|tasks.md]] 참조 - 2026-04-13 drain+reboot 테스트 완료 → [[tasks|tasks.md]] 참조
상세 인프라 컨텍스트: [[../infra-hosts]] 상세 인프라 컨텍스트: [[infra-hosts]]

View File

@@ -42,4 +42,4 @@ Incus + K3s control-plane 호스트 (서울존). GPU 워크로드 가능(GTX 108
- heimdall ops 에이전트는 이 호스트의 incus 컨테이너에 상주 - heimdall ops 에이전트는 이 호스트의 incus 컨테이너에 상주
- K3s kine 엔드포인트는 OpenWrt HAProxy 192.168.9.1:5432 → Patroni leader 자동 감지 - K3s kine 엔드포인트는 OpenWrt HAProxy 192.168.9.1:5432 → Patroni leader 자동 감지
상세 인프라 컨텍스트: [[../infra-hosts]] 상세 인프라 컨텍스트: [[infra-hosts]]

View File

@@ -0,0 +1,86 @@
---
title: incus-kr2
updated:
- 2026-05-05 AMD-Vi IOMMU Completion-Wait timeout 차단 (`iommu=pt`) 검증 완료
- 2026-05-14 K3s/reboot 동반 작업 사전 체크 항목 추가 (iommu=pt 활성 확인)
tags: [infra, host, incus, k3s, seoul, amd, iommu]
type: host
host_kind: server
location: seoul
provider: self-hosted
status: active
ssh_host: incus-kr2
tailscale_ip: 100.119.109.41
lan_ip: 192.168.9.135
lan_ip_2_5g: 192.168.205.135
os: Debian 13 (trixie)
kernel: 6.12.85+deb13-amd64
cpu_model: AMD Ryzen 9 6900HX
cpu_cores: 16
ram_gb: 30
k3s_role: control-plane
critical: true
monthly_cost_usd: 0
chassis: Bosgame EffiZen Series (mini PC)
---
## 역할
Incus + K3s control-plane 호스트 (서울존). Mini PC 폼팩터, AMD Ryzen 9 6900HX. K3s control-plane 2 중 하나, Incus 프로젝트 `default` + `inbest`. inbest 프로젝트가 leewell.com 등 7 컨테이너(cloudflared/nginx/php5/php8/mariadb10/phpmyadmin/sftp) 호스팅 — kr2 단일 장애 시 leewell.com 포함 inbest 서비스 전체 다운.
## 네트워크
- 1G LAN `192.168.9.135` (eno1) — Tailscale 진입
- 2.5G LAN `192.168.205.135` (USB r8152, `enx803f5dd34c9f`) — 백업/대용량 전송용 (실 사용량 14~70 B/s 수준으로 거의 idle, [[infra-hosts]] 참조)
- Tailscale `100.119.109.41`
## 전원/원격제어
- Tapo 스마트플러그 연결 → 강제 리부팅 가능 (호스트 hang 시 유일한 OOB 수단)
- iLO/IPMI 미장착 (mini PC 폼팩터)
## 알려진 hang 패턴 — AMD-Vi (IOMMU) Completion-Wait Timeout
호스트 freeze 반복의 근본 원인. 메커니즘 정본은 [[amd-vi-iommu]], 사건 사연은 [[2026-05-04-amd-iommu-freeze]].
요약: AMD Ryzen 6900HX IOMMU의 Completion-Wait queue가 stall되면서 DMA 의존 디바이스(NVMe, USB 컨트롤러 등)가 멈춤 → containerd/incus/sshd login만 죽고 ICMP/이미 매핑된 프로세스(nginx, node-exporter)는 살아남는 부분 hang. 조치: GRUB cmdline `iommu=pt` 추가 (2026-05-04 적용, 17시간 timeout 0건 검증).
**과거 freeze 이력 비교**:
| 일자 | 원인 | 조치 |
|------|------|------|
| 2026-04-04 | USB 2.5GbE r8152 autosuspend hang ([[2026-04-04-usb-25g-hang]]) | `usbcore.autosuspend=-1` + udev rule |
| 2026-04-19 | OOM freeze (RAM 30 GiB 과적) | `kubelet system-reserved=8Gi` |
| 2026-05-04 | AMD-Vi IOMMU Completion-Wait timeout | `iommu=pt` |
## kr2 전용 K3s 설정
- `kubelet system-reserved=memory=8Gi`, `eviction-hard=memory.available<2Gi` (2026-04-19 OOM 대응, RAM 30 GiB로 K3s + Incus 공존)
- 변경 금지 — 제거 시 OOM 재현 위험
## GRUB cmdline
```
ro usbcore.autosuspend=-1 quiet iommu=pt
```
- `usbcore.autosuspend=-1` — USB r8152 절전 hang 차단 (2026-04-04)
- `iommu=pt` — AMD-Vi Completion-Wait timeout 차단 (2026-05-04)
### 사전 체크 — reboot/워크로드 재배치 동반 작업 전 필수
K3s 업그레이드, OS apt + reboot, 커널 업그레이드, dist-upgrade, grub 패키지 업그레이드, 호스트 reboot 등 **GRUB cmdline 또는 워크로드 부하 패턴이 영향받을 수 있는 작업** 직전에 다음 두 줄로 iommu=pt 활성 여부 확인:
```bash
ssh kaffa@incus-kr2 'cat /proc/cmdline; grep GRUB_CMDLINE /etc/default/grub'
```
- `/proc/cmdline``iommu=pt` 있어야 현재 부팅에서 활성
- `GRUB_CMDLINE_LINUX_DEFAULT``iommu=pt` 있어야 다음 reboot에서도 적용
- 둘 중 하나라도 빠지면 작업 중단하고 GRUB 복구 후 reboot 사이클 별건 처리
이유: AMD-Vi freeze는 K3s+Incus 워크로드 부하 패턴에서 발생. K3s drain/uncordon으로 pod 재배치 시 freeze 위험 윈도우. dist-upgrade·grub 패키지 prompt 또는 `/etc/default/grub` 수동 편집으로 iommu=pt가 빠지면 다음 reboot에서 freeze 재발.
## 디스크
- `/dev/nvme0n1` — root + 데이터, ext4 937 GiB (사용 18%)
- SMART 깨끗 (Critical Warning 0, Media Errors 0, Percentage Used 1%)

View File

@@ -41,7 +41,7 @@ NixOS 베이스 호스트 (도쿄, Linode nano). 이전 `sandbox-tokyo` 장비
## 용도 검토 ## 용도 검토
- K3s worker 편입 후보 (taint geo=tokyo) — [[../k3s-migration|검토 중]] - K3s worker 편입 후보 (taint geo=tokyo) — [[k3s-migration|검토 중]]
- 도쿄 POP 엣지 워크로드 용도 - 도쿄 POP 엣지 워크로드 용도
상세 인프라 컨텍스트: [[../infra-hosts]] 상세 인프라 컨텍스트: [[infra-hosts]]

View File

@@ -1,12 +1,15 @@
--- ---
title: 인프라 호스트 및 네트워크 title: 인프라 호스트 및 네트워크
updated: 2026-04-09 정합성 점검 — Incus 3호스트 라이브 전수, ArgoCD/Helm/Vault/etcd/anomaly-detect drift 정리 updated:
- 2026-05-04 incus-kr2 GRUB `iommu=pt` 적용 (AMD-Vi Completion-Wait timeout 차단)
- 2026-05-14 K3s 4노드 patch 업그레이드 v1.34.5+k3s1 → v1.34.7+k3s1 (containerd 2.1.5 → 2.2.3)
- 2026-05-20 incus-hp1 storage-net 2.5G 참여 사실 확인 (192.168.205.227, MAC 20:e1:5d:6a:2b:2e, MTU 9000 JF OK) — 이전 표기 "1GbE only" 수정
tags: [infra, network, kr-zone, openwrt] tags: [infra, network, kr-zone, openwrt]
--- ---
## SSH 접속 정보 ## SSH 접속 정보
인프라 호스트 SSH 접속 정보: incus-jp1 (공인 42.125.196.116, Tailscale 100.109.123.1), incus-kr1 (공인 220.120.65.245, Tailscale 100.84.111.28), incus-hp2 (Tailscale 100.100.52.34), osaka(gw) (ssh root@100.108.39.107, 공인 172.233.93.180), Synology NAS (SSH 불가, Tailscale 100.126.100.82) 인프라 호스트 SSH 접속 정보: incus-jp1 (공인 42.125.196.116, Tailscale 100.109.123.1), incus-kr1 (공인 220.120.65.245, Tailscale 100.84.111.28), incus-hp1 (LAN 192.168.9.227 only, **Tailscale 미설치 + kappa 키 미배포 → 호스트 직접 SSH 불가**, K3s API 경유만 가능), incus-hp2 (Tailscale 100.100.52.34), osaka(gw) (ssh root@100.108.39.107, 공인 172.233.93.180), Synology NAS (SSH 불가, Tailscale 100.126.100.82)
## 서버 상세 ## 서버 상세
@@ -15,8 +18,8 @@ tags: [infra, network, kr-zone, openwrt]
| apisix-osaka | 100.108.39.107 | [[apisix]] API Gateway (오사카) | SSH 직접 접속 | | apisix-osaka | 100.108.39.107 | [[apisix]] API Gateway (오사카) | SSH 직접 접속 |
| incus-jp1 | 100.109.123.1 | Incus 호스트 (도쿄) | agents, db, default, monitoring 프로젝트 | | incus-jp1 | 100.109.123.1 | Incus 호스트 (도쿄) | agents, db, default, monitoring 프로젝트 |
| incus-kr1 | 100.84.111.28 | Incus+K3s 호스트 (서울) | GTX 1080 Ti, K3s control-plane (LAN 192.168.9.214), default 프로젝트 | | incus-kr1 | 100.84.111.28 | Incus+K3s 호스트 (서울) | GTX 1080 Ti, K3s control-plane (LAN 192.168.9.214), default 프로젝트 |
| incus-kr2 | 100.119.109.41 | Incus+K3s 호스트 (서울) | K3s control-plane (LAN 192.168.9.135), default, inbest 프로젝트 | | incus-kr2 | 100.119.109.41 | [[incus-kr2\|Incus+K3s 호스트 (서울)]] | **AMD Ryzen 9 6900HX, 30 GiB RAM, Bosgame mini PC**, K3s control-plane (LAN 192.168.9.135), default, inbest 프로젝트, GRUB `iommu=pt` (AMD-Vi hang 차단, [[2026-05-04-amd-iommu-freeze]]), Tapo 스마트플러그 OOB |
| incus-hp1 | — | Incus+K3s 호스트 (서울) | **HP ProLiant DL360p Gen8** 베어메탈, Xeon E5-2670 32코어, 188GB RAM, K3s worker/k3s-agent (LAN 192.168.9.227), 1GbE only (2.5G 미탑재), Tailscale 미설치, default 프로젝트, 2026-04-16 신규 | | incus-hp1 | — | Incus+K3s 호스트 (서울) | **HP ProLiant DL360p Gen8** 베어메탈, Xeon E5-2670 32코어, 188GB RAM, Debian 13 trixie kernel 6.12.85, K3s worker/k3s-agent (LAN 192.168.9.227), **storage-net 192.168.205.227 (MAC 20:e1:5d:6a:2b:2e, MTU 9000 JF 검증 2026-05-20)**, Tailscale 미설치 + kappa SSH 키 미배포 (호스트 직접 접근 불가, K3s API 경유만), 2026-04-16 신규 |
| incus-hp2 | 100.100.52.34 | Incus+K3s 호스트 (서울) | **HP ProLiant DL360p Gen8** 베어메탈, Xeon E5-2670 32코어, 188GB RAM, 커널 6.12.74+deb13+1 (2026-04-14 업데이트), K3s worker/k3s-agent (LAN 192.168.9.134), default, inbest 프로젝트 | | incus-hp2 | 100.100.52.34 | Incus+K3s 호스트 (서울) | **HP ProLiant DL360p Gen8** 베어메탈, Xeon E5-2670 32코어, 188GB RAM, 커널 6.12.74+deb13+1 (2026-04-14 업데이트), K3s worker/k3s-agent (LAN 192.168.9.134), default, inbest 프로젝트 |
| openwrt-gw | 100.66.60.66 | **OpenWrt 라우터 (서울, critical)** | HAProxy: 80/443 → MetalLB Traefik(192.168.9.53:80/443), 9080/9443 → MetalLB APISIX(192.168.9.50:80/443), **5432 → Patroni PostgreSQL Leader (K3s kine 데이터스토어 진입점, [[postgresql-ha]] 참조)**. 이 노드 다운 시 K3s API/HTTP 진입 모두 중단 | | openwrt-gw | 100.66.60.66 | **OpenWrt 라우터 (서울, critical)** | HAProxy: 80/443 → MetalLB Traefik(192.168.9.53:80/443), 9080/9443 → MetalLB APISIX(192.168.9.50:80/443), **5432 → Patroni PostgreSQL Leader (K3s kine 데이터스토어 진입점, [[postgresql-ha]] 참조)**. 이 노드 다운 시 K3s API/HTTP 진입 모두 중단 |
| zlambda (구 sandbox-tokyo) | 100.78.51.18 | [[zlambda|NixOS 베이스 호스트]] (도쿄, Linode `zlambda`) | NixOS 25.05 (Warbler), 공인 139.162.71.52, sshd+tailscale+docker, 2026-04-08 Debian→NixOS 전환 (이전 APISIX/etcd/microsocks/tlsproxy/vault-prod/wg-easy 모두 제거됨), Linode 프로필 kernel=`linode/direct-disk`, BBR+fq+sysctl 튜닝, configuration: Gitea [`kaffa/nixos-infra`](https://gitea.inouter.com/kaffa/nixos-infra) (kaffa-macmini `~/nixos-infra/`, zlambda `/root/nixos-infra/`) | | zlambda (구 sandbox-tokyo) | 100.78.51.18 | [[zlambda|NixOS 베이스 호스트]] (도쿄, Linode `zlambda`) | NixOS 25.05 (Warbler), 공인 139.162.71.52, sshd+tailscale+docker, 2026-04-08 Debian→NixOS 전환 (이전 APISIX/etcd/microsocks/tlsproxy/vault-prod/wg-easy 모두 제거됨), Linode 프로필 kernel=`linode/direct-disk`, BBR+fq+sysctl 튜닝, configuration: Gitea [`kaffa/nixos-infra`](https://gitea.inouter.com/kaffa/nixos-infra) (kaffa-macmini `~/nixos-infra/`, zlambda `/root/nixos-infra/`) |
@@ -25,7 +28,42 @@ tags: [infra, network, kr-zone, openwrt]
## 서울 K3s 클러스터 ## 서울 K3s 클러스터
서울존 4대(kr1, kr2, hp1, hp2)를 K3s v1.34.5+k3s1 클러스터로 구성. **kr1/kr2는 control-plane, hp1/hp2는 worker(k3s-agent)**. 서울존 4대(kr1, kr2, hp1, hp2)를 K3s v1.34.7+k3s1 클러스터로 구성 (2026-05-14 patch 업그레이드, containerd 2.2.3-k3s1). **kr1/kr2는 control-plane, hp1/hp2는 worker(k3s-agent)**.
### Descheduler (2026-04-19 설치)
CronJob `kube-system/descheduler`, 30분 주기, helm `descheduler/descheduler` v0.35.1.
- **LowNodeUtilization**: 메모리/CPU 30% 미만 노드 → 70% 초과 노드에서 pod evict하여 분산
- **RemoveDuplicates**: 같은 Deployment pod이 한 노드에 몰리면 분산
- **RemovePodsHavingTooManyRestarts**: 재시작 10회 초과 pod 정리
- evict 제외: kube-system, longhorn-system
- 배경: 2026-04-19 kr2(30GB) OOM freeze — K3s pod 33개 + Incus 9개 = 42 워크로드 과적, 커널 freeze 후 물리 재부팅
### kr2 kubelet memory reserve (2026-04-19 OOM 대응)
kr2만 `/etc/rancher/k3s/config.yaml` 에 명시 설정:
```yaml
kubelet-arg:
- "system-reserved=memory=8Gi"
- "eviction-hard=memory.available<2Gi"
```
- capacity 32Gi → allocatable 21.6Gi (약 10Gi system reserve)
- 배경: 위 2026-04-19 OOM freeze 재발 방지. kr2는 K3s control-plane + Incus (default 2 + inbest 7) 동시 호스팅으로 RAM 30GiB 중 non-K3s 예약 필요
- 타 노드(kr1 62GiB / hp1·hp2 188GiB)는 reserve 없음 — kr2 전용 조치
- 변경 금지 — 제거 시 OOM 재현 위험. 미래 kr2 RAM 증설 시 reserve 크기 축소 재검토 가능
### Longhorn 자동 복구 설정 (2026-04-19)
| 설정 | 값 | 효과 |
|------|-----|------|
| `node-drain-policy` | `always-allow` | 노드 drain 시 볼륨 강제 detach (레플리카 있으면 안전) |
| `node-down-pod-deletion-policy` | `delete-both-statefulset-and-deployment-pod` | 노드 다운 시 StatefulSet+Deployment pod 자동 삭제 → 다른 노드에서 재생성 |
| `auto-salvage` | `true` | faulted 볼륨 자동 복구 시도 |
| `replica-auto-balance` | `best-effort` | 새 노드 추가 시 레플리카 자동 분산 |
이전 값: node-drain-policy=`block-if-contains-last-replica`, node-down-pod-deletion-policy=`do-nothing`, replica-auto-balance=`disabled`. 변경 사유: kr2 다운 시 볼륨 detach 불가 → Multi-Attach 에러로 pod 재스케줄 실패, 수동 VolumeAttachment 삭제 필요했음
| 노드 | LAN IP | OS | | 노드 | LAN IP | OS |
|------|--------|----| |------|--------|----|
@@ -96,7 +134,7 @@ HP ProLiant DL360p Gen8 iLO 4 관리 포트. MAC 기반 고정 할당 (uci `dhcp
| argocd | argocd | argo-cd-9.4.16 | v3.3.5 | | argocd | argocd | argo-cd-9.4.16 | v3.3.5 |
| cert-manager | cert-manager | cert-manager-v1.20.0 | v1.20.0 | | cert-manager | cert-manager | cert-manager-v1.20.0 | v1.20.0 |
| gitea | gitea | gitea-12.5.0 | 1.25.4 | | gitea | gitea | gitea-12.5.0 | 1.25.4 |
| longhorn | longhorn-system | longhorn-1.8.2 | v1.8.2 | | longhorn | longhorn-system | longhorn-1.11.1 | v1.11.1 |
| metallb | metallb-system | metallb-0.15.3 | v0.15.3 | | metallb | metallb-system | metallb-0.15.3 | v0.15.3 |
| n8n | n8n | n8n-2.0.1 | 1.122.4 | | n8n | n8n | n8n-2.0.1 | 1.122.4 |
| nfs-provisioner | nfs-provisioner | nfs-subdir-external-provisioner-4.0.18 | 4.0.2 | | nfs-provisioner | nfs-provisioner | nfs-subdir-external-provisioner-4.0.18 | 4.0.2 |
@@ -144,7 +182,7 @@ db (proxysql, pgcat), kroki, mq (RabbitmqCluster CR), openmemory (mcp/ui/qdrant)
| cert-manager | K3s 클러스터 (cert-manager ns) | kubectl | | cert-manager | K3s 클러스터 (cert-manager ns) | kubectl |
| [[gitea]] | K3s 클러스터 (gitea ns): gitea, postgresql, valkey | kubectl, gitea.inouter.com | | [[gitea]] | K3s 클러스터 (gitea ns): gitea, postgresql, valkey | kubectl, gitea.inouter.com |
| Kroki | K3s 클러스터 (kroki ns) | kubectl | | Kroki | K3s 클러스터 (kroki ns) | kubectl |
| Longhorn | K3s 클러스터 (longhorn-system ns): 분산 스토리지 v1.8.2 | kubectl | | [[../platform/longhorn]] | K3s 클러스터 (longhorn-system ns): 분산 스토리지 v1.11.1 | kubectl |
| n8n | K3s 클러스터 (n8n ns, Helm) | kubectl | | n8n | K3s 클러스터 (n8n ns, Helm) | kubectl |
| NocoDB | K3s 클러스터 (tools ns, kubectl 직접) | kubectl, nocodb.inouter.com | | NocoDB | K3s 클러스터 (tools ns, kubectl 직접) | kubectl, nocodb.inouter.com |
| OpenMemory | K3s 클러스터 (openmemory ns): mcp, ui, qdrant | kubectl | | OpenMemory | K3s 클러스터 (openmemory ns): mcp, ui, qdrant | kubectl |
@@ -164,7 +202,6 @@ db (proxysql, pgcat), kroki, mq (RabbitmqCluster CR), openmemory (mcp/ui/qdrant)
| searxng | K3s 클러스터 (searxng ns) | kubectl | | searxng | K3s 클러스터 (searxng ns) | kubectl |
| juice-shop | K3s 클러스터 (juiceshop ns, 별도 jp1 default 프로젝트에도 컨테이너 존재) | kubectl | | juice-shop | K3s 클러스터 (juiceshop ns, 별도 jp1 default 프로젝트에도 컨테이너 존재) | kubectl |
| Teleport / sftpgo / sshpiper / searxng / Outline | K3s 클러스터 (각자 전용 ns) | kubectl | | Teleport / sftpgo / sshpiper / searxng / Outline | K3s 클러스터 (각자 전용 ns) | kubectl |
| anomaly-detect | hp2 incus default 프로젝트 컨테이너 `anomaly-detect` (10.100.2.164, 2026-04-08~) | incus exec |
| Prometheus, Grafana (인프라 metric 백업) | jp1 monitoring 프로젝트 (Grafana 10.253.103.199, Prometheus 10.253.100.193) — 일상 운영은 K3s VictoriaMetrics 스택 사용 | incus exec --project monitoring | | Prometheus, Grafana (인프라 metric 백업) | jp1 monitoring 프로젝트 (Grafana 10.253.103.199, Prometheus 10.253.100.193) — 일상 운영은 K3s VictoriaMetrics 스택 사용 | incus exec --project monitoring |
| DB 서버 (jp1 db 프로젝트) | jp1 db 프로젝트: etcd-1 | incus exec --project db | | DB 서버 (jp1 db 프로젝트) | jp1 db 프로젝트: etcd-1 | incus exec --project db |
| DB (분산 mariadb/postgres) | mariadb-1/postgres-1 (hp2), mariadb-2/postgres-2 (kr1), mariadb-3/postgres-3 (kr2) — 각 서울 노드 default 프로젝트 | incus exec | | DB (분산 mariadb/postgres) | mariadb-1/postgres-1 (hp2), mariadb-2/postgres-2 (kr1), mariadb-3/postgres-3 (kr2) — 각 서울 노드 default 프로젝트 | incus exec |
@@ -185,18 +222,18 @@ db (proxysql, pgcat), kroki, mq (RabbitmqCluster CR), openmemory (mcp/ui/qdrant)
|--------|---------|------------|------| |--------|---------|------------|------|
| jp1 | agents | 15 | 모두 RUNNING | | jp1 | agents | 15 | 모두 RUNNING |
| jp1 | db | 1 | etcd-1 | | jp1 | db | 1 | etcd-1 |
| jp1 | default | 20 | RUNNING 18 + STOPPED 2 (baserow, iac-route) | | jp1 | default | 19 | RUNNING 17 + STOPPED 2 (baserow, iac-route) — cs-cf-worker-bouncer 폐기 2026-04-26 |
| jp1 | monitoring | 2 | grafana, prometheus | | jp1 | monitoring | 2 | grafana, prometheus |
| kr1 | default | 3 | brokkr, mariadb-2, postgres-2 | | kr1 | default | 3 | brokkr, mariadb-2, postgres-2 |
| kr1 | ops | 1 | heimdall (tofu 관리) | | kr1 | ops | 1 | heimdall (tofu 관리) |
| kr2 | default | 2 | mariadb-3, postgres-3 | | kr2 | default | 2 | mariadb-3, postgres-3 |
| kr2 | inbest | 7 | cloudflared 포함 | | kr2 | inbest | 7 | cloudflared 포함 |
| hp2 | default | 5 | anomaly-detect 포함 | | hp2 | default | 4 | jarvis, mariadb-1, postgres-1, trader (anomaly-detect 폐기 2026-04-25) |
| hp2 | inbest | 0 | 프로젝트만 존재, 인스턴스 없음 | | hp2 | inbest | 0 | 프로젝트만 존재, 인스턴스 없음 |
스토리지 풀: jp1=`btrfs-pool` (btrfs, /dev/sda1 268G/109G used), kr1=`default` (dir, NVMe 937G/115G used), kr2=`default` (dir, NVMe 937G/115G used), hp2=`default` (btrfs, `/dev/mapper/pve-root` 126G/63G used — Proxmox 잔재 LVM 레이아웃). 스토리지 풀: jp1=`btrfs-pool` (btrfs, /dev/sda1 268G/109G used), kr1=`default` (dir, NVMe 937G/115G used), kr2=`default` (dir, NVMe 937G/115G used), hp2=`default` (btrfs, `/dev/mapper/pve-root` 126G/63G used — Proxmox 잔재 LVM 레이아웃).
호스트 자원: jp1=Xeon E5-2670 32core/31GiB, kr1=28core/62GiB+GTX 1080 Ti, kr2=Ryzen 9 6900HX 16core/30GiB, hp2=Xeon E5-2670 32core/188GiB. 호스트 자원: jp1=Xeon E5-2670 32core/31GiB, kr1=28core/62GiB+GTX 1080 Ti, kr2=Ryzen 9 6900HX 16core/30GiB, hp1=Xeon E5-2670 32core/188GiB (DL360p Gen8 추정, hp2와 동일 스펙), hp2=Xeon E5-2670 32core/188GiB. hp1 incus 사용 여부 미확인 (호스트 SSH 불가).
### jp1 컨테이너 ### jp1 컨테이너
@@ -204,8 +241,8 @@ db (proxysql, pgcat), kroki, mq (RabbitmqCluster CR), openmemory (mcp/ui/qdrant)
**db 프로젝트** (1): etcd-1 (10.253.102.11) **db 프로젝트** (1): etcd-1 (10.253.102.11)
**default 프로젝트** (20): **default 프로젝트** (19):
- RUNNING 컨테이너: crowdsec (10.253.100.240), cs-cf-worker-bouncer (10.253.100.131), dev-web (10.253.102.169), **etcd** (10.253.101.233, 2026-04-06 신규), hey (10.253.103.80), infra-tool (10.253.100.183), **netbis-cf-bouncer** (10.253.103.33, 2026-04-03 신규), pricing-api (10.253.102.142), **socks5-proxy** (10.253.102.12, 2026-03-21 신규), ssh-test (10.253.102.82), sshpiper (10.253.100.34), telegram-web-client (10.253.102.124), tor-server (10.253.101.178), **vault** (10.253.101.58, 2026-03-24 신규 — K3s 이전 후 인프라 정본), voice-api (10.253.101.7) - RUNNING 컨테이너: crowdsec (10.253.100.240), dev-web (10.253.102.169), **etcd** (10.253.101.233, 2026-04-06 신규), hey (10.253.103.80), infra-tool (10.253.100.183), **netbis-cf-bouncer** (10.253.103.33, 2026-04-03 신규), pricing-api (10.253.102.142), **socks5-proxy** (10.253.102.12, 2026-03-21 신규), ssh-test (10.253.102.82), sshpiper (10.253.100.34), telegram-web-client (10.253.102.124), tor-server (10.253.101.178), **vault** (10.253.101.58, 2026-03-24 신규 — K3s 이전 후 인프라 정본), voice-api (10.253.101.7) _(cs-cf-worker-bouncer는 2026-04-26 bouncer 단일화로 폐기)_
- RUNNING VM: gitea-runner (10.253.103.203, +docker0), juice-shop (10.253.100.202, +docker0), k8s (10.253.103.124, +flannel/cni0) - RUNNING VM: gitea-runner (10.253.103.203, +docker0), juice-shop (10.253.100.202, +docker0), k8s (10.253.103.124, +flannel/cni0)
- STOPPED: baserow (CONTAINER APP, 2026/01/05 마지막 시작), iac-route (CONTAINER, 2026/03/04) - STOPPED: baserow (CONTAINER APP, 2026/01/05 마지막 시작), iac-route (CONTAINER, 2026/03/04)
@@ -226,7 +263,7 @@ db (proxysql, pgcat), kroki, mq (RabbitmqCluster CR), openmemory (mcp/ui/qdrant)
### hp2 컨테이너 ### hp2 컨테이너
**default 프로젝트** (5): **anomaly-detect** (10.100.2.164, 2026-04-08 신규), jarvis (10.100.2.162), mariadb-1 (10.100.2.234), postgres-1 (10.100.2.5), trader (10.100.2.9) **default 프로젝트** (4): jarvis (10.100.2.162), mariadb-1 (10.100.2.234), postgres-1 (10.100.2.5), trader (10.100.2.9). _anomaly-detect (10.100.2.164)는 2026-04-25 폐기 — [[../../history/2026-04-25-anomaly-detect-removal|history]]_
**inbest 프로젝트** (0): 프로젝트만 존재, 인스턴스 없음 (default profile만 사용 중). kr2 inbest와 페어 구성을 고려해 만들어둔 빈 프로젝트로 보임. **inbest 프로젝트** (0): 프로젝트만 존재, 인스턴스 없음 (default profile만 사용 중). kr2 inbest와 페어 구성을 고려해 만들어둔 빈 프로젝트로 보임.
@@ -266,7 +303,7 @@ Docker: `--runtime=nvidia` 또는 `--gpus all`로 GPU 사용. Podman: CDI 방식
└── OpenWrt 라우터 (공인 IP: 220.120.65.245, 내부: 192.168.9.1) └── OpenWrt 라우터 (공인 IP: 220.120.65.245, 내부: 192.168.9.1)
├── incus-kr1 (192.168.9.214) ← K3s control-plane ├── incus-kr1 (192.168.9.214) ← K3s control-plane
├── incus-kr2 (192.168.9.135, br-uplink 고정) ← K3s control-plane ├── incus-kr2 (192.168.9.135, br-uplink 고정) ← K3s control-plane
├── incus-hp1 (192.168.9.227, 1GbE) ← K3s worker (k3s-agent) ├── incus-hp1 (192.168.9.227, 2.5G storage-net 192.168.205.227) ← K3s worker (k3s-agent)
└── incus-hp2 (192.168.9.134) ← K3s worker (k3s-agent) └── incus-hp2 (192.168.9.134) ← K3s worker (k3s-agent)
외부 트래픽 흐름 (TCP): 외부 트래픽 흐름 (TCP):
@@ -309,8 +346,9 @@ Docker: `--runtime=nvidia` 또는 `--gpus all`로 GPU 사용. Podman: CDI 방식
|--------|-----|-----|---------|-----| |--------|-----|-----|---------|-----|
| kr1 | enp7s0 (Realtek RTL8125 PCIe) | b8:fb:b3:c6:07:cf | 192.168.205.214 | 9000 | | kr1 | enp7s0 (Realtek RTL8125 PCIe) | b8:fb:b3:c6:07:cf | 192.168.205.214 | 9000 |
| kr2 | enx803f5dd34c9f (USB r8152) | 80:3f:5d:d3:4c:9f | 192.168.205.135 | 9000 | | kr2 | enx803f5dd34c9f (USB r8152) | 80:3f:5d:d3:4c:9f | 192.168.205.135 | 9000 |
| hp1 | (NIC명 미확인, 호스트 SSH 불가) | 20:e1:5d:6a:2b:2e | 192.168.205.227 | 9000 (JF OK 2026-05-20) |
| hp2 | ens2 (Realtek RTL8125 PCIe) | b8:fb:b3:c6:06:f0 | 192.168.205.134 | 9000 | | hp2 | ens2 (Realtek RTL8125 PCIe) | b8:fb:b3:c6:06:f0 | 192.168.205.134 | 9000 |
| NAS | USB cdc_ncm | - | 192.168.205.100 | 9000 | | NAS | eth2 (USB r8152, RTL8157 chip rev 14) | c8:4d:44:27:a9:63 | 192.168.205.100 | 9000 |
마지막 옥텟은 1G LAN(192.168.9.x)과 동일하게 통일. 마지막 옥텟은 1G LAN(192.168.9.x)과 동일하게 통일.
@@ -319,7 +357,7 @@ Docker: `--runtime=nvidia` 또는 `--gpus all`로 GPU 사용. Podman: CDI 방식
- kr2: `/etc/systemd/network/30-usb-2g5.network` - kr2: `/etc/systemd/network/30-usb-2g5.network`
- hp2: `/etc/systemd/network/30-ens2.network` - hp2: `/etc/systemd/network/30-ens2.network`
USB autosuspend/NFS hang 인시던트 이력: [[../history/2026-04-04-usb-25g-hang|2026-04-04 USB 2.5G hang]] USB autosuspend/NFS hang 인시던트 이력: [[2026-04-04-usb-25g-hang|2026-04-04 USB 2.5G hang]]
안정성 대책: 안정성 대책:
- kr2: GRUB `usbcore.autosuspend=-1`, udev rule `99-usb-ethernet.rules` (scatter-gather off) - kr2: GRUB `usbcore.autosuspend=-1`, udev rule `99-usb-ethernet.rules` (scatter-gather off)

View File

@@ -7,7 +7,7 @@ tags: [k3s, migration, postgresql, supabase]
이 문서는 과거 마이그레이션 작업 기록입니다. 전체 내용은 history 파일로 이동했습니다. 이 문서는 과거 마이그레이션 작업 기록입니다. 전체 내용은 history 파일로 이동했습니다.
- [[../history/2026-03-24-k3s-postgresql-migration|2026-03-24 K3s PostgreSQL 이전]] - [[2026-03-24-k3s-postgresql-migration|2026-03-24 K3s PostgreSQL 이전]]
- [[../history/2026-04-05-supabase-to-patroni|2026-04-05 Supabase → Patroni 이전]] - [[2026-04-05-supabase-to-patroni|2026-04-05 Supabase → Patroni 이전]]
현재 클러스터 구성은 [[infra-hosts]] 참조. 현재 클러스터 구성은 [[infra-hosts]] 참조.

View File

@@ -0,0 +1,250 @@
---
title: K3s → Incus 이전 플랜
updated:
- 2026-06-01 초안 작성 (kappa 결정 — K3s 운영부담 감축 위해 Incus 전환)
- 2026-06-01 미결정 사항 확정 — APISIX 서울 유지 / 프로비저닝 OpenTofu+Ansible / kr1 GTX 1080 Ti 유지
tags: [infra, compute, k3s, incus, migration, plan]
status: draft
---
## 배경
K3s 운영 부담이 누적되어 Incus 전환 결정. 주요 통증:
- Longhorn 리밸런싱·drain hang·VolumeAttachment 수동 정리
- kine → HAProxy → Patroni 다단 의존, 장애 cascade ([[../../history/2026-05-23-kr1-k3s-stuck-cascade]])
- kr2 RAM 30GiB에서 K3s + Incus 동시 호스팅 → OOM freeze ([[../../history/2026-05-04-amd-iommu-freeze]] 포함)
- multus·MetalLB·descheduler 튜닝, kube-system CRD/operator 다수 (cert-manager, ArgoCD 등) 유지비
- helm/ArgoCD GitOps 학습·디버깅 비용
반대로 Incus는 이미 정착(Vault, Patroni, ops 에이전트 모두 incus), OpenTofu 모듈 보유, OCI 이미지 1급 지원.
## 목표
- 서울 K3s 클러스터(kr1·kr2·hp1·hp2) 워크로드를 incus 컨테이너/VM으로 이전
- K3s 자체 폐기 (kine·Patroni 의존 종료, Longhorn 폐기)
- 운영 면적 축소: GitOps·CRD·CNI·CSI·CRDS 레이어 제거
## 비목표
- jp1 Incus 호스트 그대로 유지 (도쿄, Vault·agents 정본)
- 서울 4대 Incus 클러스터링은 별도 결정 ([[infra-hosts]] 참조). 이전은 호스트 분리 상태로 진행
- Outline/Slack 등 외부 SaaS 이전 (해당 없음)
## 디폴트 결정 — 대체 패턴
플랜 단계에서 확정. 변경 시 본 문서 history에 명시.
| 레이어 | K3s 현행 | Incus 대체 | 근거 |
|--------|---------|-----------|------|
| 진입점/라우팅 | MetalLB + Traefik + APISIX | OpenWrt HAProxy → APISIX 서울 (incus 컨테이너) → 컨테이너 IP | APISIX 서울은 유지 (정책·플러그인·SafeLine 통합 가치). MetalLB·Traefik만 폐기. APISIX는 K3s StatefulSet에서 incus 단일 컨테이너 + sqlite/etcd 백엔드로 축소 |
| TLS 인증서 | cert-manager + Let's Encrypt | acme.sh + Cloudflare DNS-01, HAProxy/APISIX/SafeLine reload | CF global key 이미 운영. cert-manager CRD 학습 비용 회피 |
| 스토리지 | Longhorn (분산 블록) | Synology NAS NFS/iSCSI 직접 마운트 + 호스트 btrfs 로컬 | 분산 복제는 앱 레벨 HA(Patroni)로 한정. NAS는 democratic-csi 없이 직접 |
| 프로비저닝 | ArgoCD + Helm (Gitea) | **OpenTofu + Ansible** (ops-agents-tofu 패턴 확장 + playbook 표준화) | Tofu = 인프라(컨테이너 생성·디스크·네트워크 device), Ansible = 컨테이너 안 OS/앱 설정. 기존 헤임달/씬 Tofu 패턴 위에 Ansible 추가. ArgoCD GitOps는 Gitea 저장소 + Tofu Cloud 또는 atlantis로 PR 기반 plan/apply |
| 시크릿 | K8s Secret + Vault Agent | Vault API 직접 (envconsul/sidecar/init) | Vault는 이전 없음 |
| 로깅 | VictoriaLogs + vector daemonset | VictoriaLogs 컨테이너 + vector systemd unit on host | 컨테이너 수집은 console.log tail |
| 메트릭 | VictoriaMetrics + node-exporter daemonset | VictoriaMetrics 컨테이너 + node-exporter systemd on host | 동일 패턴 |
| 메시지 큐 | RabbitMQ operator + cluster | RabbitMQ 단일 incus 컨테이너 (현 사용량 단일로 충분) | operator 폐기, 필요 시 페어 |
| WAF | SafeLine in K3s (6 컴포넌트) | SafeLine docker-compose → incus 단일 컨테이너 묶음 | 공식 docker-compose 그대로, K3s 분산 가치 적음 |
| HA 정책 | Pod 자동 재스케줄 | 디폴트 active-passive (호스트 다운 시 분 단위 다운타임 수용) | Patroni·Vault만 자체 HA. 나머지는 분 단위 다운 허용 |
확정 사항 (2026-06-01):
- **APISIX 서울 유지**: K3s StatefulSet → incus 단일 컨테이너로 축소 이전. etcd 3-replica 폐기, sqlite 또는 단일 etcd. ApisixRoute CRD는 Admin API + Tofu provider 또는 dashboard로 대체
- **OpenTofu + Ansible 조합**: Tofu가 incus 컨테이너·디스크·네트워크 device 생성, Ansible이 컨테이너 안 OS 설정·앱 배포. 시범 단계부터 두 도구 동시 적용
- **kr1 GTX 1080 Ti 유지**: Incus GPU 컨테이너로 GPU 워크로드 계속. K3s 종료 후에도 docker-gpu/podman-gpu 이미지 활용
## 배치 정책
| 호스트 | 권장 워크로드 | 사유 |
|--------|-------------|------|
| kr1 (62GiB + GTX 1080 Ti) | GPU 워크로드 (docker-gpu/podman-gpu 이미지 기반), brokkr(현행), postgres-2 replica | GPU 자원 유지. control-plane 부담 제거 후 GPU 활용도 확대 가능 |
| kr2 (30GiB) | inbest 7컨테이너 유지, mariadb-3/postgres-3 유지, **신규 무거운 워크로드 금지** | RAM 작음. K3s 폐기 후에도 OOM 위험 |
| hp1 (188GiB) | 메인 호스트 — Gitea, Outline, OpenMemory, SafeLine, VictoriaLogs/Metrics | RAM 여유, 베어메탈 |
| hp2 (188GiB) | hp1 짝꿍 — jarvis, trader 유지 + 분산 부담 분배, mariadb-1/postgres-1 유지 | RAM 여유, Tailscale 가입 |
## Phase 0 — 대체 패턴 인프라 셋업
K3s 워크로드 이전 전 인프라 토대 구축. 약 1~2주.
- [ ] **OpenTofu 컨테이너 모듈** 작성 (`kaffa/ops-agents-tofu``modules/incus-service/` 추가): Incus 컨테이너 + 디스크 device + proxy device + cloud-init 표준화
- [ ] **Ansible 표준 playbook 셋업**: `kaffa/ansible-services` 신규 저장소 — inventory를 Tofu output으로 자동 생성, 공통 role(`base`, `vector-agent`, `node-exporter`, `tailscale`, `app-runtime` 등) 작성. Vault lookup plugin으로 시크릿 주입
- [ ] **Tofu ↔ Ansible 연결 패턴 확정**: Tofu apply → Ansible playbook 자동 실행 (`null_resource` + `local-exec` 또는 별도 CI 단계). 적용 순서·재실행 멱등성 검증
- [ ] **공통 베이스 이미지** (Debian 13 + vector + node-exporter + tailscale 선택적): incus image build, 캐시 push. Ansible playbook으로 이미지 빌드 자체도 코드화
- [ ] **acme.sh 인증서 발급기**: hp1에 incus 컨테이너 `acme` 신설, Cloudflare DNS-01 + Vault에서 CF global key 조회. hook으로 HAProxy / APISIX / SafeLine reload
- [ ] **HAProxy 확장**: OpenWrt의 `/etc/haproxy/haproxy.cfg`에 신규 backend 추가 검증 (현재 Traefik VIP 192.168.9.53 백엔드 → APISIX 서울 incus 컨테이너 IP 또는 직접 컨테이너 IP로 점진 전환)
- [ ] **APISIX 서울 incus 컨테이너 준비**: hp1 또는 hp2에 신규 APISIX 컨테이너 생성, sqlite/etcd 백엔드 결정, 현행 K3s APISIX config·플러그인·SafeLine 통합 설정 export → 신규 컨테이너로 import. dual-run으로 검증 (Phase 4까지 K3s APISIX 병행)
- [ ] **NAS 마운트 표준**: Synology NFS export 디렉터리 구조 결정 (`/volume1/incus-data/<service>/`), btrfs subvol 옵션, idmap 정책
- [ ] **VictoriaLogs/Metrics incus 이전 사전 작업**: jp1 monitoring 프로젝트 또는 hp1에 신규 컨테이너 — K3s VictoriaMetrics 데이터 마이그레이션 dry-run
- [ ] **롤백 스냅샷 정책**: `incus snapshot` 자동화 — 이전 후 일정 기간 보관
성공 기준: Phase 1 시범 서비스를 Tofu apply → 컨테이너 RUN → HAProxy 라우팅 → 인증서 발급까지 무인 실행
## Phase 1 — 시범 이전 (1~2주)
패턴 검증용. 다운타임 영향 작은 서비스만.
| 순서 | 서비스 | 출처 ns | 목표 호스트 | 메모 |
|------|--------|--------|-----------|------|
| 1 | kroki | kroki | hp1 | stateless, 다이어그램 렌더링. 의존성 없음 |
| 2 | searxng | searxng | hp1 | stateless 검색 프록시 |
| 3 | juice-shop | juiceshop | hp2 | 테스트용, jp1 default에도 컨테이너 존재 → 통합 검토 |
검증 항목:
- OpenTofu 모듈로 컨테이너 생성·재생성 멱등성
- **Ansible playbook으로 컨테이너 안 설정·앱 배포 멱등성** (재실행 시 변경 없음)
- **Tofu output → Ansible inventory 자동 연결 동작**
- OCI 이미지 자동 업데이트 패턴 (blue-green vs in-place restart)
- HAProxy → APISIX 서울 (또는 컨테이너 IP 직접) 라우팅 + SafeLine 통과 (필요 시)
- acme.sh 인증서 발급·갱신
- vector → VictoriaLogs 수집 (K3s 잔존 + 신규 incus 동시 수집)
성공 기준: 3 서비스 정상 가동 + DNS 전환 후 1주 무이슈
## Phase 2 — Stateless·단일 컨테이너 이전 (2~3주)
| 순서 | 서비스 | 출처 ns | 목표 호스트 | 메모 |
|------|--------|--------|-----------|------|
| 1 | smtp-relay | mail | hp1 | ArgoCD App, 단순 |
| 2 | BunnyCDN MCP | mcp | hp1 | ArgoCD App |
| 3 | cfb-manager | tools | hp1 | ArgoCD `cf-bouncer-manager` |
| 4 | Namecheap API | api | hp1 | ArgoCD |
| 5 | Vultr API | api | hp1 | ArgoCD |
| 6 | NocoDB | tools | hp1 | Patroni 사용 — DB 연결만 유지 |
| 7 | n8n | n8n | hp1 | Helm. Postgres 연결만 유지. workflow 데이터 백업 후 마이그레이션 |
| 8 | sftpgo | sftpgo | hp2 | SFTP 22 노출 — HAProxy TCP 라우팅 |
| 9 | sshpiper | sshpiper | hp2 | jp1에 sshpiper 컨테이너도 있음 — 역할 분리 확인 후 |
| 10 | teleport | teleport | hp1 | auth + proxy 2 컴포넌트, 단일 incus 컨테이너로 통합 가능 |
각 항목 작업:
1. OpenTofu apply로 컨테이너 생성
2. 데이터 이전 (PVC → NAS 또는 컨테이너 disk)
3. K3s 측에서 동일 DNS로 dual-run, healthcheck
4. HAProxy backend 전환 (K3s VIP → incus 컨테이너 IP)
5. 1주 모니터 후 K3s 측 helm uninstall / kubectl delete ns
6. ArgoCD Application 삭제
## Phase 3 — 멀티 컴포넌트 이전 (2~3주)
| 순서 | 서비스 | 출처 ns | 목표 호스트 | 구성 |
|------|--------|--------|-----------|------|
| 1 | Gitea | gitea | hp1 | gitea + valkey 두 컨테이너. PostgreSQL은 Patroni 그대로. valkey는 컨테이너 동거 또는 별도 |
| 2 | Outline | outline | hp1 | outline + redis 두 컨테이너. PostgreSQL은 Patroni |
| 3 | OpenMemory | openmemory | hp1 | mcp + ui + qdrant 세 컨테이너. qdrant 데이터는 NAS NFS |
| 4 | SafeLine WAF | safeline | hp1 | 공식 docker-compose 기반, 단일 incus 컨테이너 안에서 compose 실행 (예외적 사용) 또는 multi-container profile |
| 5 | RabbitMQ | mq | hp1 | 단일 컨테이너로 충분. operator·cluster 폐기 |
| 6 | PgCat/ProxySQL | db | hp1 또는 jp1 db 프로젝트 | Patroni pooler 위치 재검토 |
작업 패턴 (Phase 2와 동일하나 데이터 마이그레이션 비중 큼):
- Postgres 의존: 이전 중 connection string만 incus 컨테이너 IP로 전환
- Redis/valkey/qdrant: 데이터 export → import 또는 NAS 볼륨 통째로 이동
- Gitea: LFS 데이터 NAS 이전 + clone/push 무중단 테스트
## Phase 4 — 게이트웨이 정리 (1~2주)
- [ ] **APISIX 서울 incus 컨테이너 전면 전환**: Phase 0에서 준비한 incus APISIX 컨테이너로 트래픽 전환. HAProxy backend를 K3s APISIX VIP(192.168.9.50) → incus APISIX 컨테이너 IP로 변경. DNS TTL + 1주 모니터 후 K3s apisix ns 삭제
- [ ] **Traefik 폐기**: 모든 HTTPRoute/IngressRoute → HAProxy backend(또는 APISIX 라우트)로 이전 완료 확인. K3s Traefik helm uninstall
- [ ] **MetalLB 폐기**: LoadBalancer Service 없음 확인 후 helm uninstall
- [ ] **cert-manager 폐기**: 모든 Certificate CR 무사용 확인 (APISIX·HAProxy·SafeLine 인증서 모두 acme.sh로 대체됨) 후 helm uninstall
## Phase 5 — K3s 인프라 폐기 (1~2주)
- [ ] **Longhorn 데이터 마이그레이션 완료 확인**: 모든 PVC가 NAS NFS 또는 호스트 디스크로 이전됨. `kubectl get pv` 비어있음
- [ ] **Longhorn helm uninstall + longhorn-system ns 삭제**
- [ ] **ArgoCD helm uninstall + argocd ns 삭제**: GitOps 저장소(`kaffa/k3s-charts` 등) 아카이브
- [ ] **VictoriaLogs/Metrics 데이터 incus로 이전**: jp1 monitoring 또는 hp1 신규 컨테이너에서 단일 인스턴스 운영
- [ ] **kube-system 잔여 정리**: descheduler, multus, nfs-provisioner 등
- [ ] **Patroni → kine 의존 제거**: K3s 자체 종료 직전 단계. Patroni는 그대로 유지 (다른 워크로드가 사용)
## Phase 6 — K3s 종료 (1주)
- [ ] kr1·kr2 control-plane drain + `k3s-uninstall.sh`
- [ ] hp1·hp2 agent drain + `k3s-agent-uninstall.sh`
- [ ] OpenWrt HAProxy에서 K3s 관련 backend 전체 삭제
- [ ] DNS — `k3s.inouter.com`, `*.inouter.com` 매핑 정리
- [ ] [[infra-hosts]] 업데이트: K3s 섹션 → history로 이동, 호스트 역할 재정의
- [ ] Obsidian K3s 관련 정본 문서 → `history/2026-XX-XX-k3s-decommission.md`로 이전
## 서비스 매핑 요약
| 서비스 | 현재 (K3s) | 이전 후 (Incus) | Phase |
|--------|-----------|----------------|-------|
| kroki | kroki ns | hp1 컨테이너 `kroki` | 1 |
| searxng | searxng ns | hp1 컨테이너 `searxng` | 1 |
| juice-shop | juiceshop ns | hp2 컨테이너 (jp1과 통합 검토) | 1 |
| smtp-relay | mail ns | hp1 컨테이너 `smtp-relay` | 2 |
| BunnyCDN MCP | mcp ns | hp1 컨테이너 `bunny-mcp` | 2 |
| cfb-manager | tools ns | hp1 컨테이너 `cfb-manager` | 2 |
| Namecheap/Vultr API | api ns | hp1 컨테이너 `dns-api` (통합 또는 분리) | 2 |
| NocoDB | tools ns | hp1 컨테이너 `nocodb` | 2 |
| n8n | n8n ns | hp1 컨테이너 `n8n` | 2 |
| sftpgo | sftpgo ns | hp2 컨테이너 `sftpgo` | 2 |
| sshpiper | sshpiper ns | hp2 컨테이너 `sshpiper-seoul` (jp1과 역할 분리) | 2 |
| Teleport | teleport ns | hp1 컨테이너 `teleport` (auth+proxy 통합) | 2 |
| Gitea | gitea ns | hp1 컨테이너 `gitea` + `gitea-valkey` | 3 |
| Outline | outline ns | hp1 컨테이너 `outline` + `outline-redis` | 3 |
| OpenMemory | openmemory ns | hp1 컨테이너 `openmemory-mcp` + `openmemory-ui` + `openmemory-qdrant` | 3 |
| SafeLine WAF | safeline ns | hp1 컨테이너 `safeline` (compose 내부) | 3 |
| RabbitMQ | mq ns | hp1 컨테이너 `rabbitmq` | 3 |
| PgCat/ProxySQL | db ns | hp1 또는 jp1 db 프로젝트 | 3 |
| VictoriaLogs/Metrics | logging/monitoring ns | hp1 컨테이너 또는 jp1 monitoring | 5 |
| APISIX 서울 | apisix ns (Deployment x2 + ingress-controller + etcd 3/3) | hp1 또는 hp2 단일 컨테이너 `apisix-seoul` (sqlite/단일 etcd 백엔드) | 0 준비 / 4 전환 |
| Traefik | kube-system | **폐기** | 4 |
| MetalLB | metallb-system | **폐기** | 4 |
| cert-manager | cert-manager | **폐기** (acme.sh로 대체) | 4 |
| Longhorn | longhorn-system | **폐기** (NAS NFS로 대체) | 5 |
| ArgoCD | argocd | **폐기** (OpenTofu로 대체) | 5 |
## 롤백 절차
Phase별 롤백 가능. 단 Phase 5 (Longhorn 폐기) 이후 K3s 복귀는 비현실적 — 그 이전 단계는 데이터 손실 없이 복귀 가능.
각 서비스 단위 롤백:
1. HAProxy backend를 K3s VIP로 되돌림
2. Incus 컨테이너 stop (삭제 금지 — `incus snapshot`으로 백업 보존)
3. K3s 측 Deployment/StatefulSet replica 복원
4. DNS TTL 안에 트래픽 복귀
전체 롤백 트리거:
- 이전 후 1주 내 다운타임 > 1시간 누적
- 데이터 무결성 사고
- 운영 부담이 예상보다 큼 (incus 자체 OOM, idmap 깨짐 반복 등)
## 위험요소 / 트레이드오프
| 위험 | 완화 |
|------|------|
| HA 손실 (호스트 다운 = 서비스 다운) | 디폴트 수용. Patroni·Vault·DNS만 자체 HA. 핵심 외 분 단위 다운타임 허용 |
| 이전 중 K3s + Incus 이중 운영 부담 | Phase 시간 단축, 시범에서 패턴 확립 후 가속 |
| ArgoCD GitOps 시각화 손실 | OpenTofu state + Tofu Cloud 또는 atlantis로 PR 기반 plan/apply. Ansible은 별도 CI 실행 로그 |
| APISIX 서울 단일 컨테이너 — etcd 3-replica 폐기 후 가용성 저하 | sqlite 백엔드 시 단일 컨테이너 다운 = 라우팅 중단. NAS 백업 + 빠른 재기동 패턴. 필요 시 hp1·hp2 active-passive 페어 |
| Tofu + Ansible 이중 도구 학습·운영 부담 | Phase 0에서 패턴 표준화·문서화. Phase 1 시범으로 마찰 조기 발견 |
| OCI 이미지 자동 업데이트 부재 | blue-green 패턴 표준화, renovate-bot으로 tag PR 자동 생성 |
| Longhorn 폐기 후 백업 전략 | NAS snapshot + R2 sync 기존 파이프라인 ([[backup]]) 활용 |
| kr2 OOM 재발 (Incus 단독에서도) | inbest 7컨테이너 + DB 2 + 신규 워크로드 금지. 가벼운 것만 |
| SafeLine WAF Phase 3 단일 컨테이너 통합 — chaitin-waf 트래픽 처리 성능 | 시범에서 부하 테스트, 필요 시 hp1·hp2 페어 active-passive |
## 일정 추정 (의지적, 실제는 더 길어질 수 있음)
| Phase | 기간 | 누적 |
|-------|------|------|
| Phase 0 (인프라 토대) | 2주 | 2주 |
| Phase 1 (시범) | 2주 | 4주 |
| Phase 2 (단일 컨테이너) | 3주 | 7주 |
| Phase 3 (멀티 컴포넌트) | 3주 | 10주 |
| Phase 4 (게이트웨이) | 1주 | 11주 |
| Phase 5 (인프라 폐기) | 2주 | 13주 |
| Phase 6 (K3s 종료) | 1주 | 14주 |
전체 약 3.5개월. 실제 진행은 다른 작업·인시던트로 6개월~1년 범위 예상.
## 관련 문서
- [[infra-hosts]] — 현재 K3s/Incus 호스트 및 워크로드 정본
- [[../platform/longhorn]] — Longhorn 현행 운영
- [[../network/apisix]] — APISIX 서울/오사카 라우팅
- [[../network/metallb]] — MetalLB IP 풀
- [[../data/postgresql-ha]] — Patroni HA (그대로 유지)
- [[../security/vault]] — Vault (그대로 유지, jp1)
- [[../data/k3s-backup]] — 현행 백업 파이프라인
- [[../../history/2026-05-23-kr1-k3s-stuck-cascade]] — 이전 결정 동기 중 하나
- [[../../history/2026-05-04-amd-iommu-freeze]] — kr2 freeze 이력

View File

@@ -9,7 +9,7 @@ tags: [nixos, linode, zlambda, infra]
Linode Tokyo VM (라벨 `zlambda`, id 47271589). NixOS 25.05 (Warbler), nixos-anywhere로 설치. 옛 이름 `sandbox-tokyo`는 Mac ssh config alias로만 남아 있음. Linode Tokyo VM (라벨 `zlambda`, id 47271589). NixOS 25.05 (Warbler), nixos-anywhere로 설치. 옛 이름 `sandbox-tokyo`는 Mac ssh config alias로만 남아 있음.
NixOS 전환 이력: [[../history/2026-04-08-zlambda-nixos-migration|history]] NixOS 전환 이력: [[2026-04-08-zlambda-nixos-migration|history]]
## 접속 ## 접속

View File

@@ -1,303 +0,0 @@
---
title: CrowdSec 및 SafeLine WAF
updated: 2026-04-10
---
## CrowdSec LAPI
| 항목 | 값 |
|------|-----|
| 위치 | jp1 Incus `crowdsec` 컨테이너 |
| LAPI | `http://10.253.100.240:8080` |
| 관리 | `ssh incus-jp1 "incus exec crowdsec -- cscli ..."` |
## 로그 수집 (Acquisition)
### Traefik → CrowdSec (Vector)
```
Traefik DaemonSet (stdout JSON accessLog)
→ Vector Agent DaemonSet (K3s logging ns, kubernetes_logs source)
→ VRL transform (access log만 필터, non-JSON abort)
→ HTTP sink (배치 50건, 5초)
→ CrowdSec HTTP acquisition (:8086/traefik-logs)
→ crowdsecurity/traefik-logs 파서
```
| 항목 | 값 |
|------|-----|
| Vector | Helm `vector/vector` 0.51.0, Agent DaemonSet (3노드) |
| Values | `~/k8s/vector/values.yaml` |
| CrowdSec 포트 | 8086 |
| 인증 | `Authorization: traefik-crowdsec-log-2024` |
| 파서 | `crowdsecurity/traefik-logs` (Hub, JSON 모드) |
### APISIX → VictoriaLogs → CrowdSec (서울+오사카 통합)
```
서울 APISIX (K3s) stdout → Vector DaemonSet → VictoriaLogs (ES bulk API)
오사카 APISIX (Docker) stdout → Vector (Docker) → VictoriaLogs (ES bulk API)
→ CrowdSec victorialogs acquisition (tail 모드, 실시간)
→ custom/apisix-json-logs 파서
+ anomaly-detect (5분 폴링, AI 분석)
```
| 항목 | 값 |
|------|-----|
| VictoriaLogs | `vl.inouter.com` (K3s logging ns, Traefik IngressRoute) |
| CrowdSec acquisition | `/etc/crowdsec/acquis.d/victorialogs-apisix.yaml` (`source: victorialogs`, `mode: tail`, `query: program:apisix log_type:access`) |
| 서울 Vector | K3s DaemonSet (Helm `vector/vector`), `parse_apisix` transform → `vlogs` ES sink |
| 오사카 Vector | Docker `timberio/vector:0.45.0-debian`, `/etc/vector/vector.yaml`, `docker_logs` source → `parse_apisix``vlogs` ES sink. `location: osaka` 필드 추가 |
| 파서 | `custom/apisix-json-logs` (로컬) |
### APISIX → log-collector → CrowdSec (sandbox-tokyo)
```
sandbox-tokyo APISIX http-logger
→ log-collector (jp1 crowdsec 컨테이너, :8087)
→ SQLite (/var/lib/log-collector/requests.db)
→ CrowdSec HTTP acquisition (:8085/apisix-logs) 포워딩
```
| 항목 | 값 |
|------|-----|
| log-collector | Go HTTP 서버, `/usr/local/bin/log-collector` (jp1 crowdsec 컨테이너) |
| 소스 | jp1 `~/log-collector/main.go` |
| 수신 포트 | 8087 |
| 인증 | `Authorization: apisix-crowdsec-log-2024` |
| SQLite | `/var/lib/log-collector/requests.db` |
| 기능 | APISIX 로그 수신 → SQLite 저장 → CrowdSec 포워딩, 타임스탬프 밀리초 보정 |
### SafeLine → CrowdSec (실시간, PG LISTEN/NOTIFY)
```
SafeLine WAF 차단 → mgt_detect_log_basic INSERT
→ PG 트리거 (notify_detect_log) → pg_notify('safeline_detect', JSON)
→ safeline-listener (kr2, Go) → detail 테이블 enrichment (x-real-ip, user-agent)
→ CrowdSec HTTP acquisition (:8088/safeline-logs)
→ custom/safeline-http-logs 파서
→ custom/safeline-waf-blocked 시나리오 (trigger, 즉시 밴)
```
| 항목 | 값 |
|------|-----|
| safeline-listener | Go, `/usr/local/bin/safeline-listener` (kr2) |
| 소스 | kr2 `~/safeline-listener/main.go` |
| systemd | `safeline-listener.service` (kr2) |
| PG DSN | SafeLine CE DB (10.43.8.243:5432, safeline-ce) |
| CrowdSec 포트 | 8088 |
| 인증 | `Authorization: safeline-crowdsec-2026` |
| acquis 설정 | `/etc/crowdsec/acquis.d/safeline-http.yaml` |
| 파서 | `custom/safeline-http-logs` (`/etc/crowdsec/parsers/s01-parse/safeline-http-logs.yaml`) |
| 시나리오 | `custom/safeline-waf-blocked` (trigger 타입, 즉시 밴) |
| enrichment | req_header에서 `x-real-ip`, `user-agent` 추출, detail 테이블에서 method/payload 조회 |
PG 트리거 (SafeLine CE DB에 설치):
```sql
CREATE OR REPLACE FUNCTION notify_detect_log() RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('safeline_detect', json_build_object(
'id', NEW.id, 'ts', NEW.created_at,
'src_ip', NEW.src_ip, 'host', NEW.host,
'url_path', NEW.url_path, 'attack_type', NEW.attack_type
)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_detect_log
AFTER INSERT ON mgt_detect_log_basic
FOR EACH ROW EXECUTE FUNCTION notify_detect_log();
```
### ~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)
> [!warning] 폐기됨
> 2026-04-08 분석기 서비스 정지 + 삭제. "이 방식으로는 안 될 것 같다"는 판단 (60초 폴링 + Claude CLI 동기 호출 구조의 한계). 새로운 AI 분석 아키텍처는 [[victorialogs|VictoriaLogs]] 기반으로 재설계 예정.
>
> **제거된 항목**: `ddos-detect.service` (systemd), `/var/lib/log-collector/ddos-detect/` Go 바이너리 + 소스, `ddos-detect.sh`, `extract_behavior.py`, `ddos-logs/` (분석 결과 markdown). `requests.db`는 별개의 `log-collector` 데몬이 사용 중이라 보존. Gitea repo `kaffa/ddos-detect`는 보존 (코드 reference).
이전 동작 (참고):
| 항목 | 값 |
|------|-----|
| 위치 | jp1 crowdsec 컨테이너 `/var/lib/log-collector/ddos-detect/` |
| Gitea | `gitea.inouter.com/kaffa/ddos-detect` |
| 기능 | SQLite 폴링 → IP별 행위 패턴 추출 → Claude AI 분석 → cscli 자동 밴 |
| 설정 | `config.yaml` (poll 60s, Claude sonnet, ban 4h, max_workers 3) |
### 리얼 IP 처리
| 경로 | 설정 | source IP 추출 |
|------|------|---------------|
| BunnyCDN → HAProxy → **Traefik** | `forwardedHeaders.trustedIPs: 127/8, 10/8, 172.16/12, 192.168/16` + `externalTrafficPolicy: Local` | `ClientHost` (X-Forwarded-For에서 추출) |
| BunnyCDN → HAProxy → **APISIX** | `real_ip_header: X-Forwarded-For` + `real_ip_from: 127/8, 10.42/16, 10.43/16, 192.168.9/24, 100.64/10` | `client_ip` (nginx real_ip 모듈) |
Traefik values: `~/k8s/traefik/values.yaml`
## 시나리오
| 유형 | 시나리오 | 출처 |
|------|---------|------|
| HTTP | http-probing, http-crawl-non_statics, http-sensitive-files, http-backdoors-attempts, http-sqli-probing, http-xss-probing 등 | Hub |
| SafeLine | custom/safeline-waf-blocked (trigger), custom/safeline-waf-repeated (3+/5min) | 로컬 |
| **APISIX** (2026-04-08 신규) | custom/apisix-high-rate-per-ip, custom/apisix-499-burst, custom/apisix-single-path-flood, custom/apisix-5xx-burst | 로컬 |
| CAPI | http:dos, http:scan, ssh:bruteforce (커뮤니티) | 자동 |
### APISIX 시나리오 (4종, 2026-04-08 작성)
ddos-detect AI 분석기 폐기 후 deterministic 패턴 매칭으로 대체. 모두 leaky bucket, ban 4h. **K3s 서울 APISIX 트래픽만 보고 판단** (osaka/zlambda는 http-logger 송신 안 함).
| 시나리오 | 매개변수 | 의도 | filter |
|---|---|---|---|
| `apisix-high-rate-per-ip` | capacity 1000, leakspeed 100ms (≈ sustained 10 req/s, burst 1000) | 일반 HTTP flood. APISIX 자체 limit-req(20 req/s)에 안 걸리는 sustained 패턴 | `log_type=http_access-log`, groupby `source_ip` |
| `apisix-499-burst` | capacity 30, leakspeed 2s | connection exhaustion / Slowloris (클라이언트 강제 끊김) | `http_status == "499"` |
| `apisix-single-path-flood` | capacity 50, leakspeed 600ms | 동일 path 반복 (CAPTCHA 우회, login bf, 동일 자원 폭격) | groupby `source_ip + ":" + http_path` |
| `apisix-5xx-burst` | capacity 20, leakspeed 3s | 백엔드 부하 유발성 공격, 취약점 스캔 | `http_status startsWith "5"` |
⚠️ **운영 주의**:
- `5xx-burst`: 백엔드 장애 시 false positive 가능. 운영 알람과 분리해서 검토.
- `499-burst`: 모바일 클라이언트 끊김으로 정상 발생할 수 있어 30 임계값을 며칠 관찰 후 조정 권장.
- `high-rate-per-ip`: APISIX `limit-req` 글로벌 룰(20 req/s burst 10)을 통과한 트래픽만 도달 → 1차 통과 후 sustained pattern을 잡는 2차 layer.
- 현재 모두 `remediation: true` (즉시 ban). dry-run으로 시작 안 함 — false positive 발생 시 임계값 또는 ban duration 조정.
과거 인시던트 및 변경 이력은 `history/` 참조. 예: `history/2026-04-10-edge-cleanup.md` (cf-audit-cleanup-2 3-incident chain, Turnstile sitekey 교체, 미들웨어 64811 `/__captcha/verify` 버그 수정 등), [[../history/2026-04-15-apisix-http-logger-removal|2026-04-15 APISIX http-logger 레거시 제거]].
### 발견 사항: K3s APISIX 글로벌 limit-req
```json
{
"key_type": "var",
"key": "remote_addr",
"rate": 20,
"burst": 10,
"rejected_code": 429
}
```
`/apisix/global_rules/limit-req` (etcd). 모든 라우트에 적용. CrowdSec 시나리오는 이 1차 차단을 통과한 트래픽만 본다는 점을 고려해서 임계값 설계.
## Bouncer
### cs-cf-worker-bouncer (Cloudflare Worker)
| 항목 | 값 |
|------|-----|
| 위치 | jp1 Incus `cs-cf-worker-bouncer` 컨테이너 |
| 설정 | `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` |
| 정본 | `gitea.inouter.com/kaffa/k8s``configs/crowdsec/crowdsec-cloudflare-worker-bouncer.yaml` |
| 동기화 | 10초 (decision stream polling) |
| 방식 | LAPI → bouncer → Cloudflare Worker KV (bloom filter) → Worker에서 차단/captcha |
| 보호 zone | keepanker.cv, actions.it.com, ironclad.it.com, servidor.it.com |
| 비보호 zone | inouter.com, anvil.it.com — DNS `proxied: false` 라 CF 엣지를 거치지 않아 bouncer 라우트가 enforce 되지 않음. 보호는 BunnyCDN 미들웨어 64811 단독 |
| Turnstile 위젯 | 4개 bouncer-managed (`crowdsec-cloudflare-worker-bouncer-widget` 이름, 168h rotation). BunnyCDN 미들웨어용 `inouter-bunny-middleware` 는 별도 수동 관리 — [[cloudflare#Turnstile 위젯]] |
| yaml writer | `cfb-manager` 단일 (K8s `default/cfb-manager` REST API). 과거 `/etc/cron.d/cf-zone-sync` + `auto-add-cf-zones.sh` 자동 쓰기는 coordination 문제로 제거됨 — 신규 zone 추가는 `cfb-manager POST /domains` 또는 수동 yaml edit + systemctl restart |
#### Decision 수동 관리
bouncer의 `lapi_key`**읽기 전용**. decision 추가/삭제는 `crowdsec` 컨테이너에서 `cscli`로:
```bash
# ban 추가
ssh incus-jp1 "incus exec crowdsec -- cscli decisions add --ip 1.2.3.4 --duration 2m --reason 'manual ban' --type ban"
# 확인
ssh incus-jp1 "incus exec crowdsec -- cscli decisions list --ip 1.2.3.4"
# 삭제
ssh incus-jp1 "incus exec crowdsec -- cscli decisions delete --ip 1.2.3.4"
```
bouncer 컨테이너에서 `curl POST /v1/decisions`로 직접 넣으면 등록 안 됨 — API 키 권한 제한.
#### 설정 변경 시 주의
**`sed -i` 사용 금지** — Incus 컨테이너 내에서 `sed -i`로 수정 시 이전 내용이 파일 끝에 남아 YAML 깨짐. 반드시 전체 파일 덮어쓰기:
```bash
cat updated-config.yaml | ssh incus-jp1 "incus file push - cs-cf-worker-bouncer/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml"
ssh incus-jp1 "incus exec cs-cf-worker-bouncer -- systemctl restart crowdsec-cloudflare-worker-bouncer"
```
### netbis-cf (Cloudflare Worker — Netbis 계정)
Kappa 계정용 `cs-cf-worker-bouncer`와 별도 컨테이너로 분리 운영. 상세: [[netbis]]
| 항목 | 값 |
|------|-----|
| 위치 | jp1 Incus `netbis-cf-bouncer` 컨테이너 (10.253.103.33) |
| 바운서 이름 | netbis-cf |
| 설정 | `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` |
| 동기화 | 10초 |
| CF 계정 | Netbis (netbis@netbis.io, Account ID 8fcf3c7876332aba33e974cbbfdad951) |
| 보호 zone | fall-vip.com, fall-mvp.com, fall-vip7.com, psd777.com, rss-555.com, rss-7790.com |
| Turnstile | 6개 zone managed 모드, 168시간 secret key 로테이션 |
| 로그 소스 | sandbox-tokyo APISIX → CrowdSec http-logger (8085/apisix-logs) |
### bunny-cdn-bouncer (BunnyCDN Edge Script)
| 항목 | 값 |
|------|-----|
| 동기화 | jp1 `infra-tool` 컨테이너, `/opt/crowdsec-bouncer/bouncer.py` |
| 크론 | 3분 delta sync, 매시 full sync |
| 방식 | LAPI → Bloom filter → BunnyCDN Edge Script 임베딩 → 퍼블리시 |
| Edge Script | ID 64811 (`crowdsec-bouncer-middleware`), FNV-1a bloom filter |
| 차단 시 | Cloudflare Turnstile CAPTCHA + LibSQL verified IP 캐싱(4h) |
| 적용 풀존 | iron-jp (5555247), iron-kr (5555227) — 두 풀존 모두 `MiddlewareScriptId: 64811` |
| API 키 | Vault `secret/infra/crowdsec-bunny-bouncer` |
| 소스 | `~/crowdsec-bunny-bouncer/` |
## 보안 구조
```
클라이언트 → BunnyCDN Edge Script (CrowdSec bouncer, 0차)
→ BunnyCDN WAF (OWASP CRS, 1차)
→ Traefik / APISIX + SafeLine WAF (2차)
→ CrowdSec 로그 분석 (3차) → bouncer 피드백 루프
```
### 0차: BunnyCDN Edge Script (CrowdSec bouncer)
- Bloom filter로 악성 IP 즉시 차단
- false positive 시 Turnstile CAPTCHA로 구제
### 1차: BunnyCDN WAF (OWASP CRS)
- CDN 에지에서 오리진 도달 전 차단
- 차단: SQLi, XSS, CMDi, SSRF, Shellshock, Log4j
- 비활성화: DATA LEAKAGES SQL (id=911) — NocoDB 오탐 방지
- 통과: Request Smuggling, NoSQLi, 경로 스캔
### 2차: SafeLine WAF (chaitin-waf)
- APISIX plugin_metadata → detector `10.43.253.244:8000`
- SafeLine v9.3.2, K3s safeline ns
- 관리: safeline.inouter.com, API 헤더 `X-SLCE-API-TOKEN`
- 토큰: Vault `secret/infra/safeline`
- tengine 미사용, APISIX 직접 연동
### 3차: CrowdSec 로그 분석
- Traefik / APISIX 로그: Vector → VictoriaLogs(`vl.inouter.com`) → CrowdSec `victorialogs` acquisition (tail 모드)
- SafeLine 차단: PG NOTIFY → safeline-listener → CrowdSec HTTP acquisition(`:8088`)
- sandbox-tokyo APISIX: http-logger → log-collector(`:8087`) → CrowdSec
- HTTP 시나리오 매칭 → decision → bouncer 피드백
## iron-kr-waf BunnyCDN Pull Zone (구 waf-kr)
APISIX 외부 인입 경로 (juiceshop SafeLine 통합 테스트 용도). APISIX 자체는 독립 LoadBalancer gateway (MetalLB 192.168.9.50) 이지 "SafeLine 전용" 이 아님 — 현재 1 route 가 SafeLine 용일 뿐.
| 항목 | 값 |
|------|-----|
| Zone ID | 5555224 |
| Name | iron-kr-waf |
| Origin | https://220.120.65.245:9443 |
| 경로 | 인터넷 → BunnyCDN(iron-kr-waf) → OpenWrt HAProxy(:9443) → MetalLB 192.168.9.50:443 → APISIX(K3s 서울) → chaitin-waf → SafeLine detector → K3s svc |
등록 호스트: `iron-kr-waf.b-cdn.net`, `juiceshop.keepanker.cv` (WAF 테스트용). ApisixRoute `juiceshop` (chaitin-waf block) → juiceshop:3000.
> 옛 메모의 `waf-kr (5554681)` 는 더 이상 존재하지 않음 — `iron-kr-waf (5555224)` 로 통합·재명명됨.
## 참고
- BunnyCDN WAF 차단 시 오리진에 로그 안 옴 → CrowdSec에 미수신
- OpenWrt CrowdSec firewall bouncer는 DNAT 구조라 리얼 IP 매칭 불가
- chaitin-waf 플러그인은 `plugin_attr`이 아닌 **`plugin_metadata`(etcd)**에서 detector 노드를 읽음
- Incus 컨테이너에서 `sed -i`로 설정 수정 시 파일 손상 주의 (전체 파일 push 사용)

16
infra/data/_index.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: data 인덱스
updated: 2026-04-16
tags: [moc, data]
---
## Data
| 문서 | 설명 |
|------|------|
| [[backup]] | 백업 파이프라인 (Longhorn PVC, R2) |
| [[k3s-backup]] | K3s 백업 파이프라인 (Longhorn + Synology 2레이어) |
| [[nas-storage]] | NAS StorageClass (NFS + iSCSI, Synology) |
| [[postgresql-ha]] | PostgreSQL HA (Patroni + etcd 3노드) |
| [[sftpgo]] | SFTPGo SFTP/SSH 서버 |
| [[storage-plan]] | 스토리지 기획 (NVMe NAS + iSCSI) |

View File

@@ -1,9 +1,58 @@
--- ---
title: 백업 파이프라인 title: 백업 파이프라인
updated: 2026-04-15 updated: 2026-04-20
tags: [infra, backup] tags: [infra, backup]
--- ---
## Velero (K8s 표준 백업 도구)
네임스페이스 · 앱 단위 전체 복원을 위한 표준 백업 도구. Longhorn BackupTarget(볼륨 단위)과 공존 — 목적이 다름.
| 항목 | 값 |
|------|-----|
| Namespace | velero |
| Chart | vmware-tanzu/velero `12.0.0` (app `v1.18.0`) |
| Plugin | `velero/velero-plugin-for-aws:v1.14.0` (initContainer) |
| Features | `EnableCSI` |
| BackupStorageLocation | `default` → R2 버킷 `velero-backup` (ENAM), endpoint `d8e5997eb4040f8b489f09095c0f623c.r2.cloudflarestorage.com` |
| VolumeSnapshotLocation | `default` (provider=csi) |
| VolumeSnapshotClass | `longhorn-snapshot` (driver.longhorn.io, type=snap) |
| Credentials 시크릿 | `velero-cloud-credentials` (namespace: velero). Vault `secret/cloud/cloudflare/r2` 재사용 (기존 `longhorn-backup-r2`와 동일 키) |
| 클라이언트 | `brew install velero` (macOS) |
### 백업 스케줄 `daily-full`
| 항목 | 값 |
|------|-----|
| Cron | `0 16 * * *` UTC (KST 01:00) |
| TTL | 720h (30일) |
| 스코프 | 전체 네임스페이스 (`*`) + cluster-scoped |
| 제외 네임스페이스 | kube-system, kube-public, kube-node-lease, velero, longhorn-system, democratic-csi, metallb-system, cert-manager, monitoring, logging, external-secrets, nfs-provisioner, rabbitmq-system |
| 볼륨 | CSI 스냅샷 (Longhorn 네이티브 스냅샷으로 생성) |
기존 Longhorn BackupTarget은 유지 (볼륨 단위 장애 복구용). Velero는 네임스페이스 단위 전체 복구용.
### 운영 명령
```bash
velero backup get -n velero
velero backup describe <name> -n velero --details
velero backup logs <name> -n velero
velero backup create adhoc-<name> --include-namespaces <ns> --snapshot-volumes -n velero --wait
velero schedule get -n velero
velero restore create --from-backup <name> -n velero
```
### snapshot-controller (선결 인프라)
| 항목 | 값 |
|------|-----|
| snapshot-controller | v8.5.0, Deployment (kube-system, replicas=2) |
| CRDs | snapshot.storage.k8s.io/v1 — VolumeSnapshot, VolumeSnapshotContent, VolumeSnapshotClass |
도입 이력: [[2026-04-20-snapshot-controller-velero-prep|prep]] · [[2026-04-20-velero-install|install]]
## Longhorn PVC 백업 (K3s) ## Longhorn PVC 백업 (K3s)
BackupTarget `default` → R2 버킷 `longhorn-backup` (시크릿 `longhorn-backup-r2`). RecurringJob 4종 (critical-snapshot 매시 UTC, critical-backup 6h UTC, standard-snapshot `0 18 * * *` UTC = KST 03:00, standard-backup `0 19 * * *` UTC = KST 04:00). BackupTarget `default` → R2 버킷 `longhorn-backup` (시크릿 `longhorn-backup-r2`). RecurringJob 4종 (critical-snapshot 매시 UTC, critical-backup 6h UTC, standard-snapshot `0 18 * * *` UTC = KST 03:00, standard-backup `0 19 * * *` UTC = KST 04:00).
@@ -22,7 +71,7 @@ kubectl -n longhorn-system get volumes.longhorn.io \
-l recurring-job-group.longhorn.io/critical=enabled -l recurring-job-group.longhorn.io/critical=enabled
``` ```
라벨 기록이 꼬였을 때의 대응은 [[../history/2026-04-15-longhorn-backup-label-typo|history]] 참고. 라벨 기록이 꼬였을 때의 대응은 [[2026-04-15-longhorn-backup-label-typo|history]] 참고.
## incus 백업 (inbest 데이터) ## incus 백업 (inbest 데이터)

View File

@@ -1,6 +1,8 @@
--- ---
title: NAS StorageClass (NFS + iSCSI) title: NAS StorageClass (NFS + iSCSI)
updated: 2026-04-14 문서 정확성 보정 (hp2 연결 상태, nodeAffinity, iSCSI on-demand 동작) updated:
- 2026-05-20 eth2 USB 2.5GbE 카드 교체 반영 (rev 17 → rev 14, USB 4-1 → 2-2, 라인레이트 재검증)
- 2026-05-20 K3s 노드 표 4 노드로 갱신 (hp1 storage-net 192.168.205.227 참여 확인)
tags: [infra, k3s, storage, nfs, iscsi, synology] tags: [infra, k3s, storage, nfs, iscsi, synology]
--- ---
@@ -14,17 +16,18 @@ Synology NAS(DS916+)를 K3s NFS/iSCSI StorageClass로 사용. Longhorn(로컬
|------|-----| |------|-----|
| 모델 | Synology DS916+ (Braswell, x86_64, Linux 3.10.108) | | 모델 | Synology DS916+ (Braswell, x86_64, Linux 3.10.108) |
| bond0 (eth0+eth1, 1G×2) | **192.168.9.100** — 관리/SMB/DSM 웹 | | bond0 (eth0+eth1, 1G×2) | **192.168.9.100** — 관리/SMB/DSM 웹 |
| eth2 (USB 2.5G, RTL8157, r8152) | **192.168.205.100** — NFS/iSCSI 데이터 플레인, MTU 9000 | | eth2 (USB 2.5G, RTL8157, r8152) | **192.168.205.100** — NFS/iSCSI 데이터 플레인, MTU 9000. 현재 카드: chip rev 14, MAC `c8:4d:44:27:a9:63`, USB 포트 2-2 ([[2026-05-20-nas-eth2-replacement\|2026-05-20 교체]]) |
| 디스크 | 11TB (사용 2%) | | 디스크 | 11TB (사용 2%) |
| NFS export | `/volume1/k3s-nfs` | | NFS export | `/volume1/k3s-nfs` |
| NFS 옵션 | `rw,async,no_wdelay,crossmnt,no_root_squash,insecure_locks,sec=sys,anonuid=1025,anongid=100` | | NFS 옵션 | `rw,async,no_wdelay,crossmnt,no_root_squash,insecure_locks,sec=sys,anonuid=1025,anongid=100` |
K3s 노드는 모두 192.168.205.0/24 서브넷에 참여하고 있음 (2026-04-14 검증): K3s 노드는 4 노드 모두 192.168.205.0/24 서브넷에 참여하고 있음 (kr1/kr2/hp2 2026-04-14 검증, hp1 2026-05-20 확인):
| 노드 | 출구 NIC | src IP | | 노드 | 출구 NIC | src IP |
|------|---------|--------| |------|---------|--------|
| incus-kr1 | enp7s0 (PCIe 2.5G) | 192.168.205.214 | | incus-kr1 | enp7s0 (PCIe 2.5G) | 192.168.205.214 |
| incus-kr2 | enx803f5dd34c9f (USB 2.5G) | 192.168.205.135 | | incus-kr2 | enx803f5dd34c9f (USB 2.5G) | 192.168.205.135 |
| incus-hp1 | NIC명 미확인 (호스트 SSH 불가, MAC 20:e1:5d:6a:2b:2e) | 192.168.205.227 |
| incus-hp2 | ens2 | 192.168.205.134 | | incus-hp2 | ens2 | 192.168.205.134 |
## K3s 설정 ## K3s 설정
@@ -37,7 +40,7 @@ K3s 노드는 모두 192.168.205.0/24 서브넷에 참여하고 있음 (2026-04-
| NFS 경로 | `/volume1/k3s-nfs` | | NFS 경로 | `/volume1/k3s-nfs` |
| 마운트 옵션 | `soft,timeo=50,retrans=3` | | 마운트 옵션 | `soft,timeo=50,retrans=3` |
| archiveOnDelete | true (PVC 삭제 시 데이터 archived- 접두사로 보존) | | archiveOnDelete | true (PVC 삭제 시 데이터 archived- 접두사로 보존) |
| nodeAffinity | **없음 (제약 없이 배포)** — 2026-04-14 기준 Deployment에 nodeSelector/affinity 미설정. 3 노드(kr1/kr2/hp2) 전부 192.168.205.0/24 도달 가능해 스케줄 자유 | | nodeAffinity | **없음 (제약 없이 배포)** — 2026-04-14 기준 Deployment에 nodeSelector/affinity 미설정. 4 노드(kr1/kr2/hp1/hp2) 전부 192.168.205.0/24 도달 가능해 스케줄 자유 |
### Helm 설치 명령 ### Helm 설치 명령
@@ -100,7 +103,7 @@ spec:
- **hard**: NAS 끊기면 무한 대기 → 서버 먹통 - **hard**: NAS 끊기면 무한 대기 → 서버 먹통
- **soft**: 타임아웃 후 에러 반환 → 서버 생존 - **soft**: 타임아웃 후 에러 반환 → 서버 생존
모든 NFS 마운트는 `soft,timeo=50,retrans=3` 필수. 인시던트 이력: [[../history/2026-04-04-usb-25g-hang|history]] 모든 NFS 마운트는 `soft,timeo=50,retrans=3` 필수. 인시던트 이력: [[2026-04-04-usb-25g-hang|history]]
## iSCSI StorageClass (democratic-csi) ## iSCSI StorageClass (democratic-csi)
@@ -219,6 +222,9 @@ NAS의 eth2(USB 2.5G, RTL8157)가 USB 3.0 LPM exit latency 미보고 이슈로
- 2026-04-14 강제 down 테스트: 1분 내 watchdog 실행 → `ip link up` → DSM ifcfg 자동 IP 할당 → 정상 복귀 (MTU 9000 유지) - 2026-04-14 강제 down 테스트: 1분 내 watchdog 실행 → `ip link up` → DSM ifcfg 자동 IP 할당 → 정상 복귀 (MTU 9000 유지)
- Outline 상세 기록: `2026-04-14 / NAS eth2 watchdog 구축 / kappa` (id `93baf66b-f003-47a9-9d14-7a66e3dbfde0`) - Outline 상세 기록: `2026-04-14 / NAS eth2 watchdog 구축 / kappa` (id `93baf66b-f003-47a9-9d14-7a66e3dbfde0`)
- RCA 문서: `2026-04-14 / NAS eth2 USB NIC 링크 드롭 원인 조사 / kappa` (id `db923170-8c16-459d-82ce-46fdc1f0f0d0`) - RCA 문서: `2026-04-14 / NAS eth2 USB NIC 링크 드롭 원인 조사 / kappa` (id `db923170-8c16-459d-82ce-46fdc1f0f0d0`)
- 2026-05-20 카드 물리 교체 (chip rev 17 → 14, USB 4-1 → 2-2). 인터페이스 이름/IP/MTU 모두 승계, watchdog 그대로 유효. iperf v2 검증 결과 TX 2.26 Gbit/s, RX 2.45 Gbit/s (이론 91%/98%) — [[2026-05-20-nas-eth2-replacement|history]]. 새 카드도 동일 RTL8157 계열이라 LPM 이슈 재발 가능, watchdog 발화 빈도 모니터링 대상
- 2026-05-24 LPM 재발 시작 — eth2 down/up + MTU drop이 시간당 ~1회 발생. 새 카드 4일 만에 재발. watchdog 자동 복구로 서비스 영향 0.
- 2026-05-26 NAS r8152 드라이버 v2.20.1-1 → **v2.21.4-1 업그레이드** (bb-qq Synology 패키지 `r8152-braswell-2.21.4-1_7.3.spk`). 효과는 LPM 발화 빈도로 며칠 관찰 필요. **설치 절차 주의**: 첫 install은 의도적으로 fail (postinst가 spk_su 필요). 패키지에 포함된 spk_su를 `/opt/sbin/`으로 manual install 후 SPK 재install 해야 정상화 — [[../../history/2026-05-26-nas-r8152-upgrade-2.21.4|history]].
### kr2 USB NIC 드라이버 (r8152 DKMS) ### kr2 USB NIC 드라이버 (r8152 DKMS)

View File

@@ -98,7 +98,7 @@ datastore-endpoint: "postgres://kine:kine@10.100.2.5:5432,10.100.3.185:5432,10.1
### 검증 (2026-04-16) ### 검증 (2026-04-16)
kr2 → kr1 순서로 rolling restart. switchover (postgres-1→postgres-3, TL12→13) 시 kine API 다운타임 **<1초** (t+2ms 에 정상 응답). [[../history/2026-04-16-kine-multihost-migration|history]] kr2 → kr1 순서로 rolling restart. switchover (postgres-1→postgres-3, TL12→13) 시 kine API 다운타임 **<1초** (t+2ms 에 정상 응답). [[2026-04-16-kine-multihost-migration|history]]
### OpenWrt HAProxy :5432 프론트엔드 ### OpenWrt HAProxy :5432 프론트엔드
@@ -112,7 +112,7 @@ kine + pgpool 전환 완료로 HAProxy postgres 프론트엔드는 **더 이상
NocoDB/n8n/Outline → pgpool.db.svc.cluster.local:9999 → Patroni 3노드 직결 NocoDB/n8n/Outline → pgpool.db.svc.cluster.local:9999 → Patroni 3노드 직결
``` ```
이전 pgcat+HAProxy 구조는 2026-04-16 폐기. [[../history/2026-04-16-pgpool-full-migration|history]] 이전 pgcat+HAProxy 구조는 2026-04-16 폐기. [[2026-04-16-pgpool-full-migration|history]]
### pgpool 구성 ### pgpool 구성
@@ -174,7 +174,7 @@ pgpool 은 자체 연결 관리가 있어 클라이언트 좀비 문제 없지
이전 pgcat 대비: 동일 시나리오에서 n8n 1038건 / NocoDB 13건 / pod restart 필요 이전 pgcat 대비: 동일 시나리오에서 n8n 1038건 / NocoDB 13건 / pod restart 필요
마이그레이션 이력: [[../history/2026-04-16-pgpool-n8n-poc|PoC]] · [[../history/2026-04-16-pgpool-full-migration|전면 전환]] 마이그레이션 이력: [[2026-04-16-pgpool-n8n-poc|PoC]] · [[2026-04-16-pgpool-full-migration|전면 전환]]
## APISIX etcd 사용 현황 ## APISIX etcd 사용 현황
@@ -184,7 +184,7 @@ pgpool 은 자체 연결 관리가 있어 클라이언트 좀비 문제 없지
| sandbox-tokyo | (미가동) | `/apisix/tokyo` | 2026-04-08 NixOS 전환으로 APISIX 자체 폐기, etcd 데이터만 보존 | | sandbox-tokyo | (미가동) | `/apisix/tokyo` | 2026-04-08 NixOS 전환으로 APISIX 자체 폐기, etcd 데이터만 보존 |
| 서울 K3s | **K3s 내부 apisix-etcd StatefulSet** (apisix.apisix.svc:2379) | `/apisix` | 2026-04-08 외부 통합에서 K3s 내부로 복귀 | | 서울 K3s | **K3s 내부 apisix-etcd StatefulSet** (apisix.apisix.svc:2379) | `/apisix` | 2026-04-08 외부 통합에서 K3s 내부로 복귀 |
APISIX etcd 통합/분리 이력: [[../history/2026-04-06-apisix-etcd-consolidation|history]]. 현재 외부 통합 etcd는 **Patroni DCS + osaka APISIX 전용**. APISIX etcd 통합/분리 이력: [[2026-04-06-apisix-etcd-consolidation|history]]. 현재 외부 통합 etcd는 **Patroni DCS + osaka APISIX 전용**.
## 관련 문서 ## 관련 문서

216
infra/domain-boundaries.md Normal file
View File

@@ -0,0 +1,216 @@
---
title: 도메인 경계 — netbis vs ironclad (cross-domain 운영)
updated: 2026-04-27 ironclad 자체 cycle 추가 (CF + BunnyCDN 병행, OpenWrt → K3s APISIX+SafeLine, enforce layer 재구축 미정)
tags: [infra, domain, netbis, ironclad, zlambda, moc]
---
# 도메인 경계와 cross-domain 운영
ironclad 자체 인프라가 netbis의 보안 운영을 위탁받아 처리하는 구조. 두 영역의 자산/계정은 별개지만, **데이터는 netbis에서 발생 → ironclad가 처리 → 결정은 netbis CF에 다시 적용**되는 cyclic cross-domain 패턴.
이 정본은 그 경계와 흐름을 한 자리에 정리해서 다른 정본들이 흩어져 갖고 있는 책임/계정 정보의 단일 진입점 역할을 한다.
## 두 영역의 자산
| 영역 | CF account | 인프라 자산 | 책임 |
|---|---|---|---|
| **netbis** | netbis CF account (별도, ID `8fcf3c7876332aba33e974cbbfdad951`) | netbis CF zone 6개 (`fall-vip` / `fall-mvp` / `fall-vip7` / `psd777` / `rss-555` / `rss-7790`) + NPM-1..6 (Linode Tokyo public 6대 호스트) | 사용자 트래픽 종착점 (origin) — 외부 노출 도메인은 netbis 자산 |
| **ironclad** | kappa CF account (`kappa@inouter.com`) | jp1/kr1/kr2/hp2 incus, K3s 서울/오사카, vl.inouter.com (VL), CrowdSec LAPI (jp1), netbis-cf-firewall, kappa CF zone (`keepanker.cv` / `actions.it.com` / `ironclad.it.com` / `servidor.it.com` / `inouter.com` / `anvil.it.com` 등) | 인프라 본체 + 분석 layer + 자체 워크로드 (Outline / Gitea / Vault / 등) |
| **zlambda** | _(중립)_ | NixOS Tokyo 호스트, **Tailscale + Linode public IP 둘 다 보유** | 양 영역의 다리 — VL이 LAN-only(192.168.9.53)라 public NPM이 도달 못 함. HTTP relay로 투입 |
## 데이터 흐름 1 — netbis cycle (cross-domain cyclic)
```
[사용자 요청]
[netbis CF zone (netbis 영역)]
- netbis CF account의 6 zone (fall-vip 등)
- netbis-cf-firewall이 IP List `crowdsec_managed_challenge` + Firewall Rule 적용
- hit IP는 managed_challenge (CF 캡챠), miss는 통과
▼ (CF 통과)
[NPM-1..6 origin (netbis 영역, Linode Tokyo public)]
- nginx-proxy-manager proxy_v2 포맷 access log 기록
- 응답을 사용자에 전달
│ access log (Vector 0.55 file→http)
[zlambda Vector-relay (다리)]
- Vector 0.45 http_server→elasticsearch
- .relay = "zlambda" 태그 추가
▼ (HTTP relay)
[vl.inouter.com (ironclad 영역, K3s logging ns)]
- VictoriaLogs `npm-netbis` 인덱스에 적재
- K3s 클러스터 LAN-only 자산
▼ (CrowdSec acquisition tail query)
[CrowdSec LAPI (ironclad 영역, jp1 incus crowdsec 컨테이너)]
- victorialogs source가 query OR로 NPM/APISIX/Traefik 통합 입수
- NPM 라인은 Vector remap이 nginx combined 포맷으로 재합성한 _msg를 대상으로 grok
- 시나리오 매칭 → LAPI decision (origin: crowdsec, scope: Ip)
▼ (cscli decisions stream polling, 10s)
[netbis-cf-firewall (ironclad 영역, jp1 crowdsec 컨테이너에 동거)]
- 같은 컨테이너에 LAPI + bouncer 동거지만 bouncer는 netbis CF API token으로 동작
- origin filter `[crowdsec, cscli]` 적용 — CAPI/lists 30k+ 무시 (CF List 10k 한도 회피)
▼ (Cloudflare API: netbis CF account)
[netbis CF IP List + Firewall Rule (netbis 영역)]
- IP List `crowdsec_managed_challenge` 갱신
- 6 zone × Firewall Rule managed_challenge 액션 자동 동기화
▲ (다음 요청에 반영)
[사용자 요청 ← 차단/캡챠]
```
## 데이터 흐름 2 — ironclad cycle (자체 워크로드)
netbis와 달리 ironclad는 **CDN을 CF + BunnyCDN 병행**으로 운영한다. 두 엣지가 동일한 origin(K3s 서울 클러스터)으로 수렴하고, 분석/결정/enforce는 모두 ironclad 인프라 내부 cycle.
```
[사용자 요청]
├──────────────┬──────────────┐
▼ ▼ │
[Cloudflare] [BunnyCDN 풀존] │ 병행 — proxied 패턴 별로 분기
(proxy=on) (iron-kr/iron-jp) │
kappa zone inouter.com 등 │
│ │ │
│ ※ enforce layer 현재 상태:
│ - CF bouncer (cs-cf-worker-bouncer) → 폐기 2026-04-26
│ - BunnyCDN bouncer (Edge Script 64811) → 폐기 2026-04-26
│ - 양쪽 모두 IP-list 기반 enforce 부재 (재구축 미완)
└──────┬───────┘
[OpenWrt 라우터 (kr1 LAN, 192.168.9.1)]
- HAProxy frontend (KR 진입점)
- TCP 로드밸런싱 → MetalLB L2 (192.168.9.50/51) → K3s
▼ (MetalLB LB → K3s svc)
[K3s 서울 — APISIX (서울 ns) + SafeLine WAF (safeline ns)]
- 인입: APISIX가 ApisixRoute / Gateway API 처리
- WAF: APISIX `chaitin-waf` plugin이 SafeLine detector를 in-line gRPC 호출 (10.43.253.244:8000)
- 1차 차단 layer: APISIX 글로벌 limit-req (rate 20, burst 10)
- 2차 차단 layer: SafeLine WAF (OWASP CRS + 커스텀 룰)
- origin 워크로드 (Outline / Gitea / Vault / 기타) 호출
│ access log (Vector DaemonSet)
[vl.inouter.com (K3s logging ns)]
- APISIX/Traefik/SafeLine 로그 적재
- netbis NPM 로그와 같은 VL이지만 다른 인덱스/program 라벨
▼ (CrowdSec acquisition tail query, 통합 query OR)
[CrowdSec LAPI (jp1 incus crowdsec 컨테이너)]
- 시나리오: hub HTTP CVE 시나리오 + 자작 APISIX 4종 (high-rate-per-ip / 499-burst / single-path-flood / 5xx-burst) + safeline-waf-blocked
- 시나리오 매칭 → LAPI decision (origin: crowdsec)
├──────────────┬──────────────┐
▼ ▼ │
[CF bouncer] [BunnyCDN bouncer] │ enforce 분기 (양쪽 모두 재구축 필요)
(재구축 미정) (재구축 미정) │
│ │ │
▼ ▼ │
[kappa CF zone] [BunnyCDN 풀존] │
enforce enforce │
▲ (다음 요청에 반영)
[사용자 요청 ← 차단]
```
### enforce layer 재구축 계획 (미정 / 진행 중)
| 분기 | 현재 상태 | 후보 |
|---|---|---|
| Cloudflare (kappa CF zone) | bouncer 부재 | netbis-cf-firewall과 같은 패턴(`crowdsec-cloudflare-bouncer` apt 패키지)으로 별도 인스턴스 또는 동일 인스턴스에 kappa CF token 추가 운영 |
| BunnyCDN (iron-kr/iron-jp 등) | bouncer 부재 | Pull Zone `BlockedIps` 배열 push 또는 Shield Access List (Advanced 플랜) — 한도/방식은 [BunnyCDN 지원 Ticket 388529](https://dash.bunny.net/support/ticket/388529) 답변 후 결정 |
| OpenWrt 라우터 | DNAT 통과 layer (decision enforce 안 함) | 검토 필요 — DNAT 구조라 IP 기반 차단 적용 가능 여부 별건 |
## 자산 → 도메인 매핑 (주요 노드)
### netbis 자산
| 노드 / 자산 | 비고 |
|---|---|
| netbis CF account (`8fcf3c…`) | 별도 계정 |
| netbis CF zone × 6 (`fall-vip` / `fall-mvp` / `fall-vip7` / `psd777` / `rss-555` / `rss-7790`) | 사용자 노출 도메인 |
| NPM-1..6 (Linode Tokyo public) | nginx-proxy-manager origin 6대 |
### ironclad 자산
| 노드 / 자산 | 비고 |
|---|---|
| kappa CF account (`d8e5997e…`) | ironclad 자체 운영 |
| kappa CF zone (proxied=on: `keepanker.cv` / `actions.it.com` / `ironclad.it.com` / `servidor.it.com`) | CF 엣지 진입 |
| kappa CF zone (proxied=off: `inouter.com` / `anvil.it.com`) | CF 엣지 미경유 — BunnyCDN 단독 |
| BunnyCDN 풀존 (`iron-kr` 5555227 / `iron-jp` 5555247 / `iron-kr-waf` 5555224 / `iron-git` 5584382 등) | BunnyCDN 엣지 진입 |
| OpenWrt 라우터 (kr1 LAN `192.168.9.1`) | KR 진입점 + HAProxy TCP LB |
| MetalLB L2 (`192.168.9.50/51` 등) | OpenWrt → K3s svc bridge |
| K3s 서울 (4노드 클러스터) | 메인 워크로드 클러스터 |
| K3s `apisix` ns (서울 APISIX) | 인그레스 게이트웨이 |
| K3s `safeline` ns (SafeLine detector `10.43.253.244:8000`) | WAF 처리 (chaitin-waf in-line gRPC) |
| K3s 워크로드 ns (Outline / Gitea / Vault / n8n / 등) | 사용자 호출 종착점 |
| vl.inouter.com (K3s `logging` ns) | LAN-only, public 도달 불가 |
| jp1 incus `crowdsec` 컨테이너 | LAPI + netbis-cf-firewall 동거 |
### bridge 자산
| 노드 / 자산 | 비고 |
|---|---|
| zlambda (NixOS Tokyo, `139.162.71.52` + Tailscale `100.108.39.107`) | Vector relay 전용. netbis NPM → ironclad VL 도달용 다리 |
| Vault `secret/cloud/cloudflare-netbis` | netbis CF API token. ironclad Vault에 저장하지만 netbis 권한만 보유 |
| netbis-cf-firewall (`cs-cloudflare-bouncer-1777082222`) | jp1 ironclad 컨테이너에 동거하지만 netbis CF에 push (ironclad 인프라가 netbis 운영 위탁) |
## cross-domain 운영 패턴
### 패턴 1: 데이터 위탁 (netbis → ironclad)
netbis가 NPM 로그를 ironclad VL로 보낸다. 이유:
- netbis는 자체 로그 분석 인프라 없음
- ironclad가 CrowdSec/VictoriaLogs/시나리오 운영 노하우 보유
- public NPM → LAN VL 도달 불가 → zlambda relay 필수
### 패턴 2: 결정 위탁 (ironclad → netbis CF)
ironclad CrowdSec이 netbis CF에 ban 결정을 push한다. 이유:
- ironclad가 분석 결과 보유 (시나리오 매칭, IP-based decision)
- netbis CF account에 직접 enforce해야 사용자 트래픽 차단 가능
- 별도 CF account 토큰을 ironclad Vault에 저장하고 ironclad 컨테이너에서 사용
### 패턴 3: 동거 (jp1 crowdsec 컨테이너)
한 컨테이너에 ironclad LAPI + netbis 전용 bouncer가 동거. 분리 안 한 이유:
- bouncer가 LAPI를 같은 컨테이너 localhost로 호출 → 네트워크 hop 0
- 각자 다른 CF account 토큰 사용이라 사실상 격리됨
-`cscli bouncers list` / `cscli decisions list` 같은 운영 시점에는 ironclad 운영자가 netbis bouncer 상태도 같이 본다 (의도된 합쳐진 운영 시점)
## 권한·시크릿 분리
| 시크릿 | 위치 | 용도 |
|---|---|---|
| netbis CF API token (`firewall_bouncer_token`) | Vault `secret/cloud/cloudflare-netbis` | netbis CF Account Firewall Access Rules Write + Account Rule Lists Write + Zone Firewall Services Write |
| netbis CF zone 자체 관리 토큰 | Vault `secret/cloud/cloudflare-netbis` | DNS / zone 설정 (별도) |
| kappa CF API key (kappa account) | Vault `secret/cloud/cloudflare` | ironclad zone 전체 (`global_api_key` 사용) |
| zlambda Vector relay basic auth | Vault `secret/cloud/vector-relay-netbis` | NPM-1..6 → zlambda HTTP 인증 |
netbis CF token은 **ironclad 인프라가 netbis CF에 푸시하기 위한 위탁 권한**이지 ironclad 자산 token 아님. 운영자(kappa)가 양쪽 자산 모두 관리하지만, 토큰 자체는 영역별로 분리.
## 추적 시 주의 (graphify / LLM 추론용)
- **CrowdSec 시나리오 매칭은 ironclad VL 데이터 위에서 일어남**. netbis IP가 어떤 zone을 노렸는지는 VL `npm-netbis` 인덱스 host/path 필드 backward query 필요. ironclad 자체 트래픽은 program별(`apisix` / `traefik`) 라벨로 분리됨
- **netbis-cf-firewall이 push하는 IP List는 netbis CF account에만 적용** — kappa CF zone에는 영향 없음
- **kappa zone과 BunnyCDN 풀존은 IP-list 기반 enforce layer 부재** (2026-04-26 cs-cf-worker-bouncer + bunny-cdn-bouncer 폐기 후 재구축 미완) — CrowdSec 결정이 발급되어도 ironclad 트래픽 entry 단계에서 enforce되지 못함. SafeLine WAF / APISIX limit-req는 in-cluster에서 작동 (1차/2차 layer 유지)
- **APISIX는 ironclad 자산만** — netbis NPM 트래픽은 APISIX를 통과하지 않음. APISIX limit-req / chaitin-waf / 자작 4종 시나리오는 ironclad 자체 워크로드 트래픽에만 적용
- **ironclad CDN은 CF + BunnyCDN 병행** — 같은 origin으로 수렴하지만 entry는 두 갈래. zone 별 proxied on/off로 어느 entry를 거치는지 결정 (`inouter.com` / `anvil.it.com`은 BunnyCDN 단독, 나머지 kappa zone은 CF)
- **OpenWrt 라우터(`192.168.9.1`)는 양 CDN 엣지에서 들어오는 ironclad 트래픽의 KR 진입점** — DNAT/HAProxy 통과 layer라 CrowdSec decision enforce 안 함 (별건 검토 필요)
## 관련 정본
- [[../services/netbis|netbis 정본]] — netbis 도메인/zone/NPM 호스트 상세
- [[../services/bunnycdn-security|BunnyCDN 보안]] — ironclad 자체 풀존 (iron-jp/iron-kr 등) 보안 layer
- [[security/crowdsec-safeline|CrowdSec / SafeLine]] — CrowdSec acquisition / 시나리오 / netbis-cf-firewall
- [[security/cloudflare|Cloudflare (kappa account)]] — ironclad CF zone / Workers / Turnstile
- [[compute/zlambda|zlambda]] — 다리 호스트 상세
- [[compute/infra-hosts|인프라 호스트 인덱스]] — incus / K3s 호스트 전반
- [[../history/2026-04-23-netbis-npm-vl-collection|history: NPM → VL 수집 도입]]
- [[../history/2026-04-23-netbis-firewall-bouncer-migration|history: netbis-cf-firewall 마이그레이션]]
- [[../history/2026-04-25-netbis-cf-firewall-rebuild|history: netbis-cf-firewall 재구축]]
- [[../history/2026-04-25-netbis-npm-vector-msg-rewrite|history: NPM Vector _msg 재합성]]
- [[../history/2026-04-26-bouncer-consolidation|history: bouncer 단일화]]

19
infra/network/_index.md Normal file
View File

@@ -0,0 +1,19 @@
---
title: network 인덱스
updated: 2026-04-19
tags: [moc, network]
---
## Network
| 문서 | 설명 |
|------|------|
| [[apisix]] | APISIX 설정 및 운영 |
| [[apisix-manual]] | APISIX 운영 매뉴얼 |
| [[gateway-api]] | K3s Gateway API (Traefik 메인 라우팅) |
| [[k3s-ingress-architecture]] | K3s 인그레스 아키텍처 (Traefik + APISIX 병렬) |
| [[metallb]] | MetalLB K3s LoadBalancer |
| [[multus]] | Multus CNI + NAD (Pod 2차 인터페이스, storage-205 NAD) |
| [[openwrt]] | OpenWrt 라우터 (서울, HAProxy/nftables) |
| [[smtp-relay]] | smtp-relay K3s 내부 SMTP 게이트웨이 (Mailgun) |
| [[sshpiper]] | sshpiper SSH 리버스 프록시 |

View File

@@ -125,7 +125,7 @@ config:
ingressClass: apisix ingressClass: apisix
``` ```
라우팅 전환/복구 이력: [[../history/2026-03-25-apisix-to-traefik-routing|history]] 라우팅 전환/복구 이력: [[2026-03-25-apisix-to-traefik-routing|history]]
ApisixRoute 예시 (라우트별 chaitin-waf): ApisixRoute 예시 (라우트별 chaitin-waf):
```yaml ```yaml
@@ -188,7 +188,7 @@ etcd:
- helm upgrade 전 release values nesting 점검 필수: `apisix.admin.allow.ipList`(O) vs `admin.allow.ipList`(X) 등. 잘못된 nesting은 차트가 조용히 무시함. - helm upgrade 전 release values nesting 점검 필수: `apisix.admin.allow.ipList`(O) vs `admin.allow.ipList`(X) 등. 잘못된 nesting은 차트가 조용히 무시함.
- ingress controller는 자동으로 모든 K8s CRD 객체를 etcd에 재push (rollout restart로 즉시 동기화) - ingress controller는 자동으로 모든 K8s CRD 객체를 etcd에 재push (rollout restart로 즉시 동기화)
etcd 통합/분리 이력: [[../history/2026-04-06-apisix-etcd-consolidation|history]] etcd 통합/분리 이력: [[2026-04-06-apisix-etcd-consolidation|history]]
### BunnyCDN Pull Zone 매핑 (2026-04-09 API 실측) ### BunnyCDN Pull Zone 매핑 (2026-04-09 API 실측)
@@ -203,8 +203,8 @@ etcd 통합/분리 이력: [[../history/2026-04-06-apisix-etcd-consolidation|his
참고: 참고:
- iron-kr / iron-jp 모두 `IgnoreQueryStrings: false` (통일) - iron-kr / iron-jp 모두 `IgnoreQueryStrings: false` (통일)
- iron-kr / iron-jp `BlockNoneReferrer: true`, iron-git 는 `false` (git smart HTTP 호환) - iron-kr / iron-jp `BlockNoneReferrer: true`, iron-git 는 `false` (git smart HTTP 호환)
- 모든 zone `EdgeRules: []` (Edge Rules 미사용, 모든 분기는 미들웨어 64811 안) - 모든 zone `EdgeRules: []` (Edge Rules 미사용)
- Edge Script 64811 `crowdsec-bouncer-middleware` 는 **iron-jp + iron-kr 두 풀존**에만 attach. iron-git 은 의도적 우회 (git pack 바이너리 보호 불가). - ~~Edge Script 64811 `crowdsec-bouncer-middleware` attach~~: **2026-04-26 bouncer 단일화로 폐기.** 전 풀존 `MiddlewareScriptId: null` ([[../../history/2026-04-26-bouncer-consolidation|history]])
- gitea.inouter.com 은 더 이상 iron-kr 가 아니라 iron-git 풀존 소속 (분리됨) - gitea.inouter.com 은 더 이상 iron-kr 가 아니라 iron-git 풀존 소속 (분리됨)
### DNS 참고 ### DNS 참고
@@ -256,7 +256,7 @@ BunnyCDN WAF가 NocoDB JS를 오탐 차단하여 CDN 우회 처리.
시나리오 매칭으로 반복 공격자 탐지. 시나리오 매칭으로 반복 공격자 탐지.
과거 트러블슈팅 이력: [[../history/2026-03-15-apisix-git-push-500|2026-03-15 git push 500 에러 + http-logger 401]] 과거 트러블슈팅 이력: [[2026-03-15-apisix-git-push-500|2026-03-15 git push 500 에러 + http-logger 401]]
## jarvis.inouter.com 라우트 ## jarvis.inouter.com 라우트

133
infra/network/beryl-ax.md Normal file
View File

@@ -0,0 +1,133 @@
---
title: Beryl AX (GL-MT3000) 휴대용 라우터
updated: 2026-05-02
tags: [openwrt, glinet, tailscale, router, portable]
---
## 호스트 정보
| 항목 | 값 |
|------|-----|
| 모델 | GL.iNet GL-MT3000 (Beryl AX) |
| Board | `glinet,mt3000-snand` (mediatek/mt7981) |
| 펌웨어 | GL.iNet 4.8.1 (OpenWrt 21.02-SNAPSHOT base, kernel 5.4.211) |
| LAN | 192.168.8.0/24 (gateway 192.168.8.1) |
| WAN | 상위 공유기 뒤 더블 NAT (예: TP-Link 192.168.1.1 뒤 192.168.1.198) |
| Web UI | http://192.168.8.1 (GL.iNet, nginx 80/443) · http://192.168.8.1:8080 (LuCI/uhttpd) |
| SSH | `root@192.168.8.1` |
용도: 출장/이동용 휴대 Wi-Fi 6 라우터. LAN을 만들고 [[tailscale]] subnet router로 동작시켜 노트북이 라우터에 붙기만 하면 tailnet에 접근 가능.
## Tailscale
| 항목 | 값 |
|------|-----|
| Tailnet | `otter-buri.ts.net` |
| Hostname | `gl-mt3000` |
| Tailscale IP | 100.98.170.28 |
| AdvertiseRoutes | `192.168.1.0/24, 192.168.8.0/24` |
| RouteAll (accept-routes) | true |
| 버전 | tailscale 1.80.3 (펌웨어 내장) |
`tailscaled --port 41641 --state /etc/tailscale/tailscaled.state`. tailnet admin console에서 advertise route 둘 다 approve 필요.
### LAN → tailnet forward SNAT (필수 설정)
GL.iNet 4.8.1 기본 fw3 zone 정의에는 `tailscale0``masq` 옵션이 없다. 그러면 LAN(192.168.8.0/24) → tailscale0 forward 트래픽이 SNAT되지 않아 source가 사설 IP(192.168.8.x) 그대로 tailnet에 나가고, peer가 응답 경로를 못 찾아 일부 노드와 통신이 끊긴다.
**증상**: LAN 클라이언트에서 `ping 100.x.x.x`가 timeout, **라우터 자체에서는 동일 peer 정상 통신**. ICMP/TCP 모두 같은 패턴. peer가 라우터의 advertise route(192.168.8.0/24)를 OS routing table에 가지고 있는 경우(예: 같은 사용자의 다른 macOS)만 응답 가능.
**영구 적용**:
```sh
uci set firewall.tailscale0.masq='1'
uci add_list firewall.tailscale0.masq_src='192.168.8.0/24'
uci commit firewall
fw3 reload
```
**검증**:
```sh
iptables -t nat -L zone_tailscale0_postrouting -n -v
# MASQUERADE ... 192.168.8.0/24 룰이 박혀 있어야 함
```
### MagicDNS split-forward
기본 설정에서 `tailscale prefs``CorpDNS: false`이고 dnsmasq에도 ts.net forward 룰이 없어 LAN 클라이언트가 MagicDNS를 못 쓴다. ts.net 도메인만 100.100.100.100(Tailscale 내부 DNS proxy)로 보내는 split 방식이 GL.iNet의 기존 dnsmasq를 안 건드리고 가장 안전.
**영구 적용**:
```sh
uci add_list dhcp.@dnsmasq[0].server='/ts.net/100.100.100.100'
uci commit dhcp
/etc/init.d/dnsmasq restart
```
**검증**:
```sh
nslookup incus-jp1.otter-buri.ts.net 127.0.0.1
# → 100.109.123.1
```
100.100.100.100은 short name(`incus-jp1`)에 응답하지 않는다 — FQDN만 처리. short name은 클라이언트의 search domain으로 보완.
## Mac 클라이언트 측 설정
Mac이 DNS를 hardcoded(예: 1.1.1.1, 8.8.8.8)로 쓰고 있으면 라우터 dnsmasq를 거치지 않아 ts.net 풀이가 안 된다. 정책을 바꾸지 않고 ts.net 도메인만 라우터로 보내려면:
```sh
sudo mkdir -p /etc/resolver
echo "nameserver 192.168.8.1" | sudo tee /etc/resolver/ts.net
```
`/etc/resolver/<domain>`은 macOS 시스템 resolver(getaddrinfo)에서만 적용된다. **`nslookup`/`dig`은 이 설정을 우회**하므로 검증할 때 부적합. `dscacheutil -q host -a name <fqdn>` 또는 실제 애플리케이션(`nc`, `ssh`, `ping`) 사용.
short name(`ssh kaffa@incus-kr1`)도 풀리려면 search domain이 라우터 tailnet과 일치해야 한다. macOS Tailscale 클라이언트가 stopped여도 이전 가입했던 tailnet의 search domain(`taila*****.ts.net`)이 잔재로 남아 있을 수 있다:
```sh
sudo networksetup -setsearchdomains Wi-Fi otter-buri.ts.net
```
이 설정은 macOS 차원에서 고정되어 다른 Wi-Fi에 옮겨도 유지된다(다른 네트워크의 DHCP search domain은 무시됨).
## 메모리 / zram swap
총 RAM 491MB(스펙 512MB - firmware reserve)에 swap 없는 환경. tailscaled + nginx + GL.iNet UI/cloud agent + dnsmasq 등으로 base 약 285MB 점유, 휴대용 라우터 치고 빠듯. CPU는 Cortex-A53 2-core인데 평소 95% idle이라 자원이 남으므로 zram(압축 RAM swap)으로 실효 가용 메모리를 늘리는 게 합리적.
**설치**:
```sh
opkg install kmod-zram zram-swap
/etc/init.d/zram enable
/etc/init.d/zram start
```
기본 동작: RAM의 약 50%(여기선 ~239MB)를 zram swap으로 잡음. UCI `system.@system[0].zram_size_mb`로 수동 지정 가능, 미설정 시 init script가 `MemTotal/2048` MB로 자동 계산.
**압축 알고리즘 zstd로 전환** (lzo 대비 압축률 ~1.5배, CPU 약간 더 사용 — 95% idle이라 무난):
```sh
uci set system.@system[0].zram_comp_algo='zstd'
uci commit system
/etc/init.d/zram restart
```
지원 알고리즘: `lzo`, `lzo-rle`, `lz4`, `zstd`. 활성 알고리즘은 `cat /sys/block/zram0/comp_algorithm`에서 대괄호로 표시됨 (`[zstd]`).
**검증**:
```sh
cat /proc/swaps # /dev/zram0 244MB
free -m # Swap 244732
cat /sys/block/zram0/comp_algorithm
```
USB/microSD 기반 swap은 eMMC 쓰기 수명과 latency 문제로 비추천 — zram이 디스크 쓰기 없이 RAM만 활용하므로 임베디드 라우터에 적합.
## 운영 주의
- **Beryl LAN을 벗어나면 ts.net 풀이가 timeout** (Mac이 192.168.8.1을 못 찾음). 외부 네트워크에선 Mac Tailscale을 다시 켜야 한다 — `/etc/resolver/ts.net`은 라우터 도달 가능성을 전제로 함
- **펌웨어 업그레이드 시 "Keep Settings" ON 필수**. 위 fw3 masq, dnsmasq split-forward 모두 UCI 설정이라 keep settings로 보존됨
- 더블 NAT 환경에서 NAT punching이 일부 peer만 성공할 수 있다(예: 한국 인프라끼리 punch 실패, 일본 인프라는 direct 성공). 나머지는 DERP relay로 자동 폴백되므로 통신 자체엔 문제 없음 — **단, 위 SNAT 설정이 있어야 LAN 트래픽이 정상 흐름**
- LuCI(:8080)와 GL.iNet UI(:80/443)가 동시 운영. 펌웨어 업그레이드는 GL.iNet UI의 Online Upgrade가 가장 단순
## 관련 문서
- [[infra-hosts]] — 서버 목록
- [[openwrt]] — 서울 OpenWrt 라우터 (별도 거점, Beryl과 다름)

View File

@@ -8,7 +8,7 @@ tags: [k3s, traefik, gateway-api]
K3s 메인 라우팅을 Traefik이 담당. APISIX는 독립 LoadBalancer(MetalLB VIP 192.168.9.50)로 병렬 운영. K3s 메인 라우팅을 Traefik이 담당. APISIX는 독립 LoadBalancer(MetalLB VIP 192.168.9.50)로 병렬 운영.
전환 이력: [[../history/2026-03-25-apisix-to-traefik-routing|history]] 전환 이력: [[2026-03-25-apisix-to-traefik-routing|history]]
## Traefik 배포 ## Traefik 배포

View File

@@ -1,6 +1,6 @@
--- ---
title: MetalLB (K3s LoadBalancer) title: MetalLB (K3s LoadBalancer)
updated: 2026-03-26 updated: 2026-04-20
tags: [infra, k3s, metallb, networking] tags: [infra, k3s, metallb, networking]
--- ---
@@ -21,6 +21,15 @@ K3s 내장 ServiceLB(Klipper)는 비활성화 (`--disable servicelb`, kr2/kr1 co
| Speaker | DaemonSet (노드당 1개, 3개) | | Speaker | DaemonSet (노드당 1개, 3개) |
| Controller | Deployment (1개) | | Controller | Deployment (1개) |
## 리소스 설정
| 컴포넌트 | requests mem | limits mem |
|---------|-------------|-----------|
| controller | 128Mi | 256Mi |
| speaker | 128Mi | 256Mi |
controller는 cert-rotation · webhook 서버 때문에 기본 limit(64Mi)으로는 OOM 발생. speaker와 동일 수준으로 맞춤. 이력: [[2026-04-20-metallb-controller-oom-fix|history]]
## IP 할당 현황 ## IP 할당 현황
| IP | Service | Namespace | Port | | IP | Service | Namespace | Port |
@@ -65,4 +74,4 @@ kubectl get l2advertisement -n metallb-system # L2 광고 확인
kubectl get svc --all-namespaces -o wide | grep LoadBalancer # LB 서비스 목록 kubectl get svc --all-namespaces -o wide | grep LoadBalancer # LB 서비스 목록
``` ```
NodePort → LoadBalancer 이전 이력: [[../history/2026-03-24-k3s-postgresql-migration|history]] (Phase 5: MetalLB 도입) NodePort → LoadBalancer 이전 이력: [[2026-03-24-k3s-postgresql-migration|history]] (Phase 5: MetalLB 도입)

199
infra/network/multus.md Normal file
View File

@@ -0,0 +1,199 @@
---
title: Multus CNI + NetworkAttachmentDefinition
updated: 2026-04-19
tags: [infra, k3s, multus, cni, networking, longhorn]
---
## 개요
K3s 클러스터에 **2차 네트워크 인터페이스**를 Pod에 붙여줄 수 있게 해주는 meta-CNI. Flannel(기본 eth0)은 그대로 두고 `k8s.v1.cni.cncf.io/networks` 어노테이션으로 추가 인터페이스(`net1`, `net2`, ...) 연결.
**주 용도**: Longhorn storage network 분리 (192.168.205.0/24 2.5G 전용망). [[longhorn-storage-network]]도 참고.
## 아키텍처
```
Pod
├── eth0 (10.42.x.x, Flannel, 1GbE via br-uplink)
└── net1 (192.168.205.240-254, macvlan via ens2, 2.5GbE, MTU 9000)
```
- **Multus CNI (thick daemonset)**: Pod 생성 시 Flannel 기본 네트워크에 추가로 NAD 기반 네트워크를 붙임
- **Whereabouts**: 범위 기반 IPAM (클러스터 전역 IP 충돌 방지, CRD 기반 추적)
- **macvlan CNI**: Pod에 ens2 부모 인터페이스의 MAC 기반 가상 인터페이스 생성
## K3s 경로 특이점
K3s CNI 구조가 표준 `/opt/cni/bin`과 다르고 대부분 **심볼릭 링크가 multicall 바이너리를 가리키는 형태**라 Multus thick chroot과 충돌함. 아래 방식으로 해결:
| 경로 | 역할 |
|------|------|
| `/var/lib/rancher/k3s/agent/etc/cni/net.d/` | k3s CNI conf dir (kubelet이 여기서 NetConf 읽음) |
| `/var/lib/rancher/k3s/data/cni/` | k3s CNI bin dir (flannel/bridge/host-local이 multicall `cni` 바이너리 심볼릭) |
| `/var/lib/rancher/k3s/data/current/bin/cni` | 실제 k3s multicall CNI 바이너리 (이름에 따라 flannel/bridge/호출 분기) |
| **`/opt/cni/bin/`** (신규, 실제 디렉토리) | **Multus/Whereabouts가 사용**하는 CNI 바이너리 저장소. 모두 real binary |
**`/opt/cni/bin/` 내용** (각 노드 kr1/kr2/hp1/hp2):
- `containernetworking/plugins v1.6.2` 풀 셋: `bandwidth`, `bridge`, `dhcp`, `dummy`, `firewall`, `host-device`, `host-local`, `ipvlan`, `loopback`, `macvlan`, `portmap`, `ptp`, `sbr`, `static`, `tap`, `tuning`, `vlan`, `vrf`
- `flannel` ← k3s multicall 바이너리 복사본 (`cp /var/lib/rancher/k3s/data/current/bin/cni /opt/cni/bin/flannel`)
- `multus-shim`, `passthru` ← Multus DS init container가 배포
- `whereabouts` ← Whereabouts DS가 배포
## 심볼릭 링크 (노드별)
K3s 기본 CNI dir과 표준 경로 브리지:
```sh
# kubelet이 k3s CNI dir에서 multus-shim 찾을 수 있게
/var/lib/rancher/k3s/data/cni/multus-shim → /opt/cni/bin/multus-shim
/var/lib/rancher/k3s/data/cni/passthru → /opt/cni/bin/passthru
# whereabouts 바이너리가 /etc/cni/net.d/whereabouts.d 기본 경로를 기대함
/etc/cni/net.d/whereabouts.d → /var/lib/rancher/k3s/agent/etc/cni/net.d/whereabouts.d
```
## 배포 정보
| 컴포넌트 | Kind | 위치 |
|---------|------|------|
| `kube-multus-ds` | DaemonSet | kube-system |
| `whereabouts` | DaemonSet | kube-system |
| `NetworkAttachmentDefinition` CRD | CRD | cluster-scope |
| `ippools`, `overlappingrangeipreservations`, `nodeslicepools` CRDs | CRD | cluster-scope (whereabouts.cni.cncf.io) |
**버전**:
- Multus CNI: `v4.2.2` (thick daemonset)
- Whereabouts: `v0.9.1`
- containernetworking/plugins: `v1.6.2`
**매니페스트 커스터마이징**: 상류 manifest에서 hostPath 2개만 k3s 경로로 sed 치환:
```sh
sed -e 's|path: /etc/cni/net.d$|path: /var/lib/rancher/k3s/agent/etc/cni/net.d|g' \
-e 's|path: /etc/cni/multus/net.d$|path: /var/lib/rancher/k3s/agent/etc/cni/multus/net.d|g' \
multus-daemonset-thick.yml > multus-k3s.yml
```
Whereabouts도 동일(conf dir만 패치, `/opt/cni/bin`은 실제 디렉토리라 default 유지).
## NetworkAttachmentDefinition
### `longhorn-system/storage-205`
Longhorn 전용 2.5G 스토리지 네트워크.
```yaml
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: storage-205
namespace: longhorn-system
spec:
config: |
{
"cniVersion": "0.3.1",
"type": "macvlan",
"master": "ens2",
"mode": "bridge",
"mtu": 9000,
"ipam": {
"type": "whereabouts",
"range": "192.168.205.0/24",
"range_start": "192.168.205.240",
"range_end": "192.168.205.254"
}
}
```
| 항목 | 값 |
|------|-----|
| 부모 인터페이스 | `ens2` (각 노드 2.5G NIC, 192.168.205.x/24) |
| 모드 | `bridge` (macvlan) |
| MTU | 9000 (jumbo frames) |
| IP 풀 | 192.168.205.240 - 192.168.205.254 (15개) |
| IPAM | whereabouts (클러스터 전역 IP 중복 체크) |
**192.168.205.0/24 점유 현황** (이 NAD 도입 시점):
- `.100` NAS (Synology) — 2.5G 스토리지 세그먼트
- `.134` hp2 / `.135` kr2 / `.214` kr1 / `.227` hp1 — 각 노드 ens2
- `.240 ~ .254` NAD 풀 (Longhorn instance-manager 등)
## 사용 방법
Pod 정의에 어노테이션 추가:
```yaml
metadata:
annotations:
k8s.v1.cni.cncf.io/networks: storage-205
# 다른 네임스페이스면: k8s.v1.cni.cncf.io/networks: longhorn-system/storage-205
```
Pod 안에 `net1` 인터페이스가 whereabouts가 할당한 IP(`.240-.254`)로 붙음. `eth0`(Flannel)은 그대로.
## 업그레이드 시 주의
K3s 업그레이드 시 `/var/lib/rancher/k3s/data/cni/` 심볼릭이 재생성될 수 있음. 아래 심볼릭 링크가 살아있는지 확인:
```sh
ls -la /var/lib/rancher/k3s/data/cni/multus-shim
ls -la /var/lib/rancher/k3s/data/cni/passthru
```
끊어졌으면 재생성:
```sh
sudo ln -sfn /opt/cni/bin/multus-shim /var/lib/rancher/k3s/data/cni/multus-shim
sudo ln -sfn /opt/cni/bin/passthru /var/lib/rancher/k3s/data/cni/passthru
```
`/opt/cni/bin/`는 k3s와 무관한 경로라 업그레이드 영향 없음.
## 트러블슈팅
### `failed to find plugin "X" in path [/opt/cni/bin]`
Multus thick의 chroot가 `/hostroot`로 들어간 뒤 `/opt/cni/bin/X` 찾음. 원인:
- `/opt/cni/bin`이 심볼릭이면 대상 디렉토리의 binary가 pod에 마운트 안 돼 실행 불가
- **해결**: `/opt/cni/bin`을 real directory로 만들고 모든 플러그인을 real binary로 배치 (이 구성이 그렇게 해결됨)
### `config file not found` (macvlan + whereabouts)
whereabouts 바이너리가 `/host/etc/cni/net.d/whereabouts.d/whereabouts.conf`를 찾지만 k3s는 conf를 `/var/lib/rancher/k3s/agent/etc/cni/net.d/`에 둠.
- **해결**: `/etc/cni/net.d/whereabouts.d` 심볼릭 링크 생성
### Multus DS pod 로그 확인
```sh
sudo kubectl -n kube-system logs -l app=multus --tail=50
```
### 테스트 Pod 예시
```yaml
apiVersion: v1
kind: Pod
metadata:
name: nad-test
namespace: longhorn-system
annotations:
k8s.v1.cni.cncf.io/networks: storage-205
spec:
containers:
- name: probe
image: alpine:3.20
command: ["sh","-c","sleep 600"]
```
```sh
sudo kubectl -n longhorn-system exec nad-test -- ip -br addr
# eth0=10.42.x.x, net1=192.168.205.24X (whereabouts 할당)
```
## 관련 문서
- [[openwrt]] — 192.168.9.0/24 라우터
- [[metallb]] — k3s LoadBalancer (별개, service IP용)
- [[infra-hosts]] — 각 노드 ens2 IP 현황

20
infra/platform/_index.md Normal file
View File

@@ -0,0 +1,20 @@
---
title: platform 인덱스
updated: 2026-04-25
tags: [moc, platform]
---
## Platform
| 문서 | 설명 |
|------|------|
| ~~[[anomaly-detect]]~~ | 이상 트래픽 감지 — **폐기 2026-04-25** ([[../../history/2026-04-25-anomaly-detect-removal|history]]) |
| [[brokkr]] | Brokkr 홈페이지 제작 에이전트 |
| [[helm-charts]] | Helm 차트 관리 체계 (ArgoCD GitOps) |
| [[infra-forge]] | Forge 인프라 에이전트 (지식 베이스 + LLM 보고) |
| [[infra-tofu]] | 인프라 프로비저닝 (OpenTofu) |
| [[kaniko]] | Kaniko K8s 네이티브 컨테이너 이미지 빌드 |
| [[longhorn]] | Longhorn 분산 블록 스토리지 (K3s) |
| [[ollama]] | Ollama LLM 로컬 추론 엔드포인트 (kr1 호스트) |
| [[outline]] | Outline Wiki (팀 문서 관리) |
| [[victorialogs]] | VictoriaLogs K3s 로그 저장소 |

View File

@@ -0,0 +1,29 @@
---
title: anomaly-detect (폐기 2026-04-25)
updated: 2026-04-25
tags: [security, crowdsec, deprecated]
status: deprecated
---
> [!warning] 폐기됨 (2026-04-25)
> Grok-4-fast agentic 분석기가 더미 IP(1.1.1.1, 1.2.3.4 시퀀스), Cloudflare 엣지 IP(172.70.x), 자체 Linode IDC 대역(45.79.x)을 path-enumeration으로 오탐 ban하는 사례 다수 확인.
> 컨테이너 포함 완전 제거. 폐기 경과 및 재가동 조건: [[../../history/2026-04-25-anomaly-detect-removal|history]]
## 보존된 자산
| 항목 | 위치 | 비고 |
|---|---|---|
| 코드 | `gitea.inouter.com/kaffa/anomaly-detect` (private) | 재구축 reference |
| Vault | `secret/ai/openrouter` (OpenRouter API key) | 다른 서비스 공용 가능성 — 보존 |
| 설계 이력 | [[../../history/2026-04-08-anomaly-detect-iterations]] | 1차 stats → 2차 cohort → 3차 agentic 반복 |
## 제거된 자산
- incus-hp2 `anomaly-detect` 컨테이너 (10.100.2.164)
- systemd `anomaly-detect.timer` / `anomaly-detect.service`
- CrowdSec LAPI watcher 등록
- 활성 decision 2건 (`45.94.31.74`, `45.76.123.45`)
## CrowdSec 시나리오 잔재
`anomaly-detect/path-enumeration`, `anomaly-detect/brute-force` 시나리오 이름은 본인이 발급한 alert에만 등장. CrowdSec Hub 표준 시나리오가 아니라 LAPI 측에서 별도 정리 불필요.

152
infra/platform/longhorn.md Normal file
View File

@@ -0,0 +1,152 @@
---
title: Longhorn 분산 블록 스토리지
updated: 2026-05-09
tags: [infra, platform, longhorn, storage, k3s]
---
## 개요
Longhorn은 K3s 클러스터의 **분산 블록 스토리지**. 각 노드 로컬 NVMe에 replica 분산 저장, CSI 드라이버로 PV/PVC 공급. 기본 StorageClass `longhorn` (default), 정적 복구용 `longhorn-static`.
| 항목 | 값 |
|---|---|
| Helm release | `longhorn` (ns `longhorn-system`) |
| Chart / App | `longhorn-1.11.2` / `v1.11.2` |
| 노드 | incus-kr1, incus-kr2, incus-hp1, incus-hp2 (4) |
| Data engine | v1 (v2 미사용) |
| Default data path | `/var/lib/longhorn/` |
| Default replica count | 2 (`{"v1":"2","v2":"2"}`) |
| Default SC | `longhorn` — WaitForFirstConsumer 아님, Immediate / ReclaimPolicy Delete / ExpansionAllowed true |
| 정적 복구 SC | `longhorn-static` (Volume CR 경유 복구 시) |
| Backup target | `s3://longhorn-backup@auto/` (Cloudflare R2) — [[../data/k3s-backup|k3s-backup]] 참조 |
## 컴포넌트 (helm rev 6 기준)
| 컴포넌트 | 종류 | 이미지 |
|---|---|---|
| longhorn-manager | DaemonSet | `longhorn-manager:v1.11.2` + `longhorn-share-manager:v1.11.2` |
| longhorn-csi-plugin | DaemonSet | `csi-node-driver-registrar:v2.16.0`, `livenessprobe:v2.18.0`, `longhorn-manager:v1.11.2` |
| longhorn-driver-deployer | Deployment (1) | `longhorn-manager:v1.11.2` |
| longhorn-ui | Deployment (2) | `longhorn-ui:v1.11.2` |
| csi-attacher / provisioner / resizer / snapshotter | Deployment (3 replicas each) | `csi-attacher:v4.x`, `csi-provisioner:v5.x`, `csi-resizer:v1.x`, `csi-snapshotter:v8.x` |
| engine-image | DaemonSet (per-version) | `longhorn-engine:v1.11.2` (ei-c9fa6d45). 신규 EI 적용 후 모든 25 볼륨 live engine upgrade 완료, 구 EI `ei-75a03ec3` (v1.11.1) 는 refcount 0 으로 자동 정리 대기 |
| instance-manager | per-node per-version | `longhorn-instance-manager:v1.11.2` |
## Helm values (커스텀)
```yaml
defaultSettings:
nodeDownPodDeletionPolicy: delete-both-statefulset-and-deployment-pod
nodeDrainPolicy: always-allow
replicaAutoBalance: best-effort
```
- `nodeDownPodDeletionPolicy`: 노드 다운 시 StatefulSet / Deployment 파드 양쪽 모두 강제 삭제하여 PV 재attach 허용
- `nodeDrainPolicy: always-allow`: `kubectl drain` 시 볼륨 attach 여부 무관하게 허용
- `replicaAutoBalance: best-effort`: 노드 간 replica 분포 자동 밸런싱 시도
## UI
- https://longhorn.inouter.com — Traefik IngressRoute 경유
- 백업 탭에서 R2 저장된 backup volume / snapshot 시각 확인
## 볼륨 운영
- 기본 PVC 생성: StorageClass `longhorn` (default)
- 백업/복구·라벨 기반 recurring job: [[../data/k3s-backup|k3s-backup]]
- 복구 시 주의: `longhorn.io/fromBackup` annotation은 Longhorn v1.8+ CSI에서 무시됨 → Volume CR 직접 생성 후 `longhorn-static` SC PV 만드는 절차. 상세 [[../data/k3s-backup|k3s-backup]]
- 주기 백업 라벨 키는 **대시 포함**: `recurring-job-group.longhorn.io/<group>=enabled` (과거 오타 이슈 [[../../history/2026-04-15-longhorn-backup-label-typo|history]])
## Stuck snapshot 임시 cron (v1.11.1 워크어라운드 — 2026-05-09 회수 완료)
> **상태 (2026-05-09): 회수 완료**. ArgoCD Application `longhorn-snapshot-purge` 삭제 (cascade prune 으로 CronJob / SA / Role / RoleBinding 자동 정리). helm-charts repo 의 chart 디렉토리는 재현 시 재활용 위해 **보존**. 회수 5분 사후 관측 신규 Warning / stuck CR 0건.
(과거 기록) v1.11.1 instance-manager 재시작 후 일부 snapshot CR 이 `status.markRemoved=true && status.readyToUse=false && status.ownerID=""` 상태로 멈췄다. ownerId 가 비어 longhorn-manager 가 reconcile 못 했고, 수동 `kubectl delete` / finalizer patch 시 admission webhook 이 finalizer 를 즉시 재추가해 정리 불가였다.
근본 fix 는 **v1.11.2 백포트 [#12856](https://github.com/longhorn/longhorn/issues/12856)** (2026-05-07 적용). 그 전까지 임시로 snapshotPurge API 를 cron 으로 자동 호출해 engine 단 chain cleanup 을 주기 트리거했다.
### 구성
| 항목 | 값 |
|---|---|
| Helm chart | `gitea.inouter.com/kaffa/helm-charts` · `charts/longhorn-snapshot-purge` |
| ArgoCD App | `argocd/longhorn-snapshot-purge` (auto-sync, prune, selfHeal) |
| CronJob | `longhorn-system/longhorn-snapshot-purge` |
| Schedule | `*/30 * * * *` |
| ServiceAccount | `longhorn-snapshot-purge` (ns Role: `snapshots.longhorn.io` get/list) |
| Image | `alpine/k8s:1.32.1` (kubectl + curl + jq 포함) |
### 동작
1. namespace 내 `snapshots.longhorn.io` 전수 list
2. `status.markRemoved=true` 이면서 `status.readyToUse=false` 인 CR 추출 → `spec.volume` 중복 제거
3. 각 volume 에 `POST http://longhorn-frontend.longhorn-system.svc/v1/volumes/{name}?action=snapshotPurge` 호출
4. HTTP 200 / 비-200 별 카운트, 비-200 있으면 job exit 1
### 한계 / 운영 주의
- snapshotPurge 는 chain 의 redundant snapshot 만 정리. **stuck snapshot 이 그 볼륨의 유일 snapshot 이고 volume-head 의 직속 parent 라면 .img 파일이 정리되지 않는다** (engine snapshotList 에서 `removed=true` 만 마킹 유지).
- 따라서 cron 은 디스크 재확보의 best-effort. CR 자체 cleanup 은 보장 안 됨 — 그건 v1.11.2 / v1.12 업그레이드로 해결.
- 임시 cron 이라 **업그레이드 직후 ArgoCD App 삭제로 회수**할 것 (chart 디렉토리는 repo 에 보존하되 Application 만 prune).
### 운영 명령
```bash
# 수동 트리거 (다음 30분 boundary 기다리지 않을 때)
kubectl -n longhorn-system create job --from=cronjob/longhorn-snapshot-purge longhorn-snapshot-purge-manual-$(date +%s)
# 마지막 실행 로그
kubectl -n longhorn-system logs -l job-name --tail=50 --selector='batch.kubernetes.io/job-name'
# stuck snapshot 현재 카운트
kubectl get snapshots.longhorn.io -A -o json | jq '[.items[] | select(.status.markRemoved == true and .status.readyToUse == false)] | length'
```
상세 도입 기록: [[../../history/2026-05-02-longhorn-snapshot-purge-cron|2026-05-02 도입]] / [[../../history/2026-05-07-longhorn-1-11-2-upgrade|2026-05-07 fix 적용]] / [[../../history/2026-05-09-longhorn-snapshot-purge-cron-removal|2026-05-09 회수]]
## 업그레이드 절차 (표준)
minor skip 금지 — 한 단계씩 순차. 각 단계 공통:
1. Pre-check: 볼륨 healthy/attached, backup target available, ongoing snapshot 없음, 4노드 Ready
2. `helm upgrade longhorn longhorn/longhorn -n longhorn-system --version <x.y.z> --reset-then-reuse-values`
3. `longhorn-manager` DS 롤링 대기
4. 신규 `engine-image` DS 4/4 state=deployed 확인
5. 모든 볼륨 `spec.image` 를 신규 engine image 로 일괄 패치 (live engine upgrade, v1 data engine)
6. `status.currentImage` 전수 확인
7. 구 engine-image CR refcount=0 확인 — Longhorn 자동 정리 대상
구 engine-image CR 과 구 instance-manager pod 는 refcount 0 확인 후 manager가 자동 정리 (기본 timeout ~10분). 수동 삭제 불필요.
## 최근 버전 변경
### 2026-05-07: 1.11.1 → 1.11.2 (patch, snapshot warning 회귀 fix)
| 단계 | Helm rev | Chart | 결과 |
|---|---|---|---|
| 1.11.1 → 1.11.2 | 6 | longhorn-1.11.2 | 25/25 볼륨 healthy |
- 백포트 [#12856](https://github.com/longhorn/longhorn/issues/12856) 적용 — `snapshot becomes not ready to use` Warning 사이클 해소
- 업그레이드 직후 stuck CR 12개 자연 cleanup (manager ownership reset 흐름 회복)
- 35분 관측 동안 신규 Warning 0건, 23:00 UTC `critical-snapshot` RecurringJob 사이클 통과
- 신규 EI `ei-c9fa6d45` (v1.11.2), 25 볼륨 모두 live engine upgrade 완료
- 상세: [[../../history/2026-05-07-longhorn-1-11-2-upgrade|history]]
### 2026-04-23: 1.8.2 → 1.11.1 (3단계 minor 순차)
| 단계 | Helm rev | Chart | 결과 |
|---|---|---|---|
| 1.8.2 → 1.9.2 | 3 | longhorn-1.9.2 | 27/27 볼륨 healthy |
| 1.9.2 → 1.10.2 | 4 | longhorn-1.10.2 | 27/27 볼륨 healthy |
| 1.10.2 → 1.11.1 | 5 | longhorn-1.11.1 | 27/27 볼륨 healthy |
- 전 단계 live engine upgrade (v1 data engine만 사용 중이라 가능)
- 전 단계 무중단 — attached 25 + detached 2(safeline) 볼륨 전수 신규 엔진 이미지로 이행
- 각 단계 breaking change 없음 (공식 릴리스 노트 기준)
- 상세 로그·검증 스냅샷: Outline `heimdall/2026-04-23 Longhorn 업그레이드` (id `750faea0-6720-4e27-b219-0942247d53aa`)
## 관련 문서
- [[../data/k3s-backup|k3s-backup]] — Longhorn → R2 백업 구성, 라벨 기반 recurring job, 복구 절차
- [[../data/storage-plan|storage-plan]] — 스토리지 전략 전반
- [[../compute/infra-hosts|infra-hosts]] — Helm 릴리스 전수 인벤토리

87
infra/platform/ollama.md Normal file
View File

@@ -0,0 +1,87 @@
---
title: Ollama — LLM 로컬 추론 엔드포인트
updated: 2026-04-21
tags: [infra, llm, ollama, inference, gpu]
---
## 개요
kr1 호스트에 직접 설치된 Ollama 런타임. 컨테이너 아닌 **호스트 systemd service**. **GTX 1080 Ti GPU 가속** (CPU 추론 아님). tailnet 전체에서 단일 엔드포인트로 공유.
| 항목 | 값 |
|------|-----|
| 위치 | incus-kr1 호스트 (컨테이너 아님) |
| 서비스 | `ollama.service` (systemd, User=ollama, Restart=always) |
| 버전 | 0.20.2 |
| 바이너리 | `/usr/local/bin/ollama` |
| GPU | NVIDIA GeForce GTX 1080 Ti (VRAM 11GB, CUDA 12.4, 드라이버 550.163.01) — [[infra-hosts#GPU]] 참조 |
| 엔드포인트 | `http://100.84.111.28:11434` (Tailscale IP, tailnet 공유) |
| 바인딩 | `0.0.0.0:11434` (`OLLAMA_HOST=0.0.0.0`) |
| 인증 | 없음 (tailnet 내부 전용, WAN 노출 아님) |
| 모델 저장소 | `/usr/share/ollama/.ollama` (12GB 사용) |
| Keep-alive | 무제한 (`OLLAMA_KEEP_ALIVE=-1`) — 모델 언로드 안 함 |
## 설치된 모델
| 모델 | 크기 | Capability | 용도 |
|------|------|------------|------|
| `qwen3:4b-instruct-2507-q4_K_M` | 2.5 GB | tools | 경량 tool-calling, 빠른 응답 |
| `gemma4:e4b` | 9.6 GB | tools, thinking, vision, audio | 멀티모달 + tool-calling, 에이전트 기본값 |
Capability는 `/api/show` 응답의 `capabilities` 필드로 확인.
## GPU 자원
- gemma4:e4b (9.6 GB 모델) 로드 시 VRAM **10,062 MiB / 11,264 MiB (89%)** 점유.
- `OLLAMA_KEEP_ALIVE=-1` 때문에 한 번 로드되면 메모리 상주 — 자동 언로드 없음.
- **모델 동시 로드 한계**: gemma4:e4b 로드 상태에서 qwen3:4b를 추가 로드하려면 VRAM 부족 → 하나를 수동 언로드해야 함 (`/api/generate` 호출 시 `keep_alive: 0`).
- 현재 GPU 공유 사용 프로세스는 Ollama 단독. K3s나 Incus 컨테이너가 GPU를 쓰지 않음.
- 관측 처리 속도: gemma4:e4b tool-calling 루프에서 약 **36 tok/s** (2026-04-21 실측).
## 접근 방법
### 모델 목록
```bash
curl -sS http://100.84.111.28:11434/api/tags | jq
```
### Tool-calling chat (OpenAI-style)
```bash
curl -sS http://100.84.111.28:11434/api/chat -d '{
"model": "gemma4:e4b",
"messages": [{"role":"user","content":"..."}],
"tools": [ { "type":"function", "function": {...} } ],
"stream": false,
"options": {"temperature": 0.2, "num_ctx": 8192}
}'
```
### 모델 pull (필요 시)
ollama 사용자만 가능. kr1 호스트에서:
```bash
sudo -u ollama ollama pull <model>
```
## 검증된 용도
| 일시 | 용도 | 결과 |
|------|------|------|
| 2026-04-21 | `agent.py` stdio 에이전트 루프 (gemma4:e4b, tools 2개, Obsidian 탐색) | 6턴 만에 최종 답변 성공. tool calling·한국어 응답 정상. `~/experiments/ollama-agent/` |
## 보안
- **바인딩은 `0.0.0.0`이지만 노출 범위는 tailnet에 한정**. kr1 호스트의 외부 인터페이스(WAN)는 방화벽으로 차단. Tailscale을 통해서만 11434 포트 도달 가능.
- 인증·TLS 없음. 별도 프록시를 붙이지 않음 (tailnet 신뢰 경계로 충분).
- OpenClaw·ops-agent 등 다수 클라이언트가 공유할 경우 개별 API 키 구분 불가 → 사용처는 OpenMemory/Obsidian에 명시 기록.
## 운영 주의점
- **GPU를 다른 워크로드와 공유할 수 없음**: 현재 VRAM이 거의 풀 점유 상태라 GPU를 쓰는 다른 컨테이너(예: docker-gpu Incus 이미지) 스케줄링 시 충돌 가능. 추가 GPU 워크로드는 `OLLAMA_KEEP_ALIVE=0`으로 Ollama 모델을 먼저 언로드해야 함.
- kr1 호스트 자원(CPU/RAM)도 Ollama가 일부 점유. heimdall·brokkr·postgres-2·mariadb-2가 같은 호스트라는 점 유의.
- `OLLAMA_KEEP_ALIVE=-1`이라 한 번 로드된 모델은 OOM 전까지 상주. 모델 전환 시 이전 모델 수동 언로드 필요.
- 모델 저장소 `/usr/share/ollama/.ollama` 는 호스트 로컬 디스크. 백업 대상 아님 (모델은 재다운로드 가능).
## 참조
- [[infra-hosts]] — kr1 Tailscale IP 100.84.111.28, GTX 1080 Ti GPU 상세 (§GPU)
- [[../../openclaw/openclaw-ollama|openclaw-ollama]] — OpenClaw의 Ollama 통합 (remote baseUrl 설정 패턴 재사용 가능)

View File

@@ -1,6 +1,6 @@
--- ---
title: Outline Wiki title: Outline Wiki
updated: 2026-04-08 updated: 2026-04-29
tags: [k3s, wiki, outline] tags: [k3s, wiki, outline]
--- ---
@@ -12,7 +12,7 @@ Outline은 팀 위키/문서 관리 플랫폼. K3s 클러스터에 배포.
|------|-----| |------|-----|
| URL | https://outline.inouter.com | | URL | https://outline.inouter.com |
| 네임스페이스 | outline | | 네임스페이스 | outline |
| 이미지 | `outlinewiki/outline:0.82.0` | | 이미지 | `outlinewiki/outline:1.7.0` (2026-04-29 0.82.0 → 1.7.0 메이저 업그레이드, OIDC SameSite 쿠키 수정 — [[../../history/2026-04-29-outline-1-7-0-upgrade|history]]) |
| 인증 | Gitea OAuth2 (OIDC) | | 인증 | Gitea OAuth2 (OIDC) |
| 기본 언어 | 한국어 | | 기본 언어 | 한국어 |
@@ -24,10 +24,11 @@ Outline은 팀 위키/문서 관리 플랫폼. K3s 클러스터에 배포.
| Redis | outline-redis (outline 네임스페이스 내 전용) | | Redis | outline-redis (outline 네임스페이스 내 전용) |
| 파일 저장소 | 로컬 (Longhorn PVC 5Gi, `/var/lib/outline/data`) | | 파일 저장소 | 로컬 (Longhorn PVC 5Gi, `/var/lib/outline/data`) |
| TLS (Traefik) | wildcard-inouter-tls (*.inouter.com) | | TLS (Traefik) | wildcard-inouter-tls (*.inouter.com) |
| TLS (CDN) | Let's Encrypt via BunnyCDN | | TLS (CDN) | *.inouter.com wildcard (cert-manager, GTS WR1 발급) 수동 업로드 |
| CDN | BunnyCDN iron-kr 존 (ID 5555227, 쿠키 허용) | | CDN | BunnyCDN **iron-kr-nowaf** 존 (ID 5720695, WAF 없음, 쿠키 허용) — 2026-04-21 iron-kr에서 분리 이전 |
| DNS | outline.inouter.com CNAME → iron-kr.b-cdn.net (Cloudflare, proxied OFF) | | Bunny Origin | https://220.120.65.245:9443 → APISIX (Traefik 미경유) |
| Ingress | Traefik IngressRoute (CRD) | | DNS | outline.inouter.com CNAME → iron-kr-nowaf.b-cdn.net (Cloudflare, proxied OFF) |
| Ingress | **APISIX ApisixRoute `outline` (ssl_id 4e7704e0, route_id ce4d2d80)** — 2026-04-21 변경. Traefik IngressRoute는 롤백 대비 유지 중이지만 비활성 경로 |
## 인증 (Gitea OAuth2) ## 인증 (Gitea OAuth2)
@@ -56,7 +57,7 @@ Outline MCP 서버 도입 시 헤임달이 직접 문서 CRUD 가능.
- heimdall `~/.claude.json` `/root` 프로젝트 mcpServers에 `outline` 항목 추가 (stdio, `uvx mcp-outline`, `OUTLINE_API_KEY`는 Vault `secret/apps/outline` brokkr-api-key, `OUTLINE_API_URL=https://outline.inouter.com`) - heimdall `~/.claude.json` `/root` 프로젝트 mcpServers에 `outline` 항목 추가 (stdio, `uvx mcp-outline`, `OUTLINE_API_KEY`는 Vault `secret/apps/outline` brokkr-api-key, `OUTLINE_API_URL=https://outline.inouter.com`)
- 새 컬렉션 `agent-qna` (id `c3ab34ab-fae4-4642-8f4e-12728e293e1b`) — 에이전트 간 장문 Q&A 교환 공간 (kappa↔heimdall이 tmux로 짧게 질문 → 답변은 여기에 작성) - 새 컬렉션 `agent-qna` (id `c3ab34ab-fae4-4642-8f4e-12728e293e1b`) — 에이전트 간 장문 Q&A 교환 공간 (kappa↔heimdall이 tmux로 짧게 질문 → 답변은 여기에 작성)
DB endpoint는 OpenWrt HAProxy(`192.168.9.1:5432`) 경유. 과거 Patroni failover 사고 이력: [[../history/2026-04-08-patroni-failover-incident|history]] DB endpoint는 OpenWrt HAProxy(`192.168.9.1:5432`) 경유. 과거 Patroni failover 사고 이력: [[2026-04-08-patroni-failover-incident|history]]
## Discord 통지 파이프라인 ## Discord 통지 파이프라인

16
infra/security/_index.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: security 인덱스
updated: 2026-04-16
tags: [moc, security]
---
## Security
| 문서 | 설명 |
|------|------|
| [[cert-manager]] | cert-manager SSL 인증서 관리 |
| [[cloudflare]] | Cloudflare 서비스 (DNS, CDN) |
| [[crowdsec-safeline]] | CrowdSec 및 SafeLine WAF |
| [[external-secrets]] | External Secrets Operator (Vault 연동) |
| [[teleport]] | Teleport 접근 관리 (SSH/K8s/웹앱) |
| [[vault]] | Vault 시크릿 관리 (HA Raft 3노드) |

View File

@@ -1,6 +1,6 @@
--- ---
title: Cloudflare 서비스 title: Cloudflare 서비스
updated: 2026-04-10 updated: 2026-04-26 bouncer 단일화로 Turnstile 위젯 6개 + Worker/KV 정리 반영
tags: [infra, cloudflare, cdn, dns] tags: [infra, cloudflare, cdn, dns]
--- ---
@@ -27,6 +27,46 @@ Syn 이 엣지 관점에서 소유. 일반 DNS 관리 협업은 Heimdall.
⚠️ **Rate Limit, WAF 규칙, Firewall 설정은 API로 조회/변경 불가 — 대시보드에서만 관리 가능**. 토큰 스코프 확장이 필요하면 CF 대시보드 > My Profile > API Tokens에서 편집. ⚠️ **Rate Limit, WAF 규칙, Firewall 설정은 API로 조회/변경 불가 — 대시보드에서만 관리 가능**. 토큰 스코프 확장이 필요하면 CF 대시보드 > My Profile > API Tokens에서 편집.
## Pseudo IPv4 (Class E 240/4)
레거시 IPv4-only 백엔드를 위한 CF 기능. **Netbis zone에서 활성 상태로 관찰됨** (Apache 2.4.38 + PHP 5.6.40). Kappa 계정 zone에는 현재 기본 Off로 추정.
### 동작 원리
```
클라이언트 (IPv6) → CF 엣지
→ IPv6 주소 상위 64비트를 MD5 해시
→ 결과를 240.0.0.0/4 (240~255.x.x.x) 범위의 IPv4로 매핑
→ CF-Connecting-IP 및 X-Forwarded-For 헤더를 해시된 IPv4로 overwrite
→ 진짜 IPv6는 Cf-Connecting-IPv6 별도 헤더로 전달
```
### 모드 3종 (CF 대시보드 Network → Pseudo IPv4)
| 모드 | CF-Connecting-IP | 별도 헤더 |
|------|-----------------|----------|
| Off | 실 IPv6/IPv4 | - |
| Add header | 실 IP 그대로 | `Cf-Pseudo-IPv4`에 해시 |
| **Overwrite headers** | 해시(240/4) | `Cf-Connecting-IPv6`에 실 IPv6 |
### 관찰 시그널 (Netbis)
- VL `client_ip` 필드 중 240.0.0.0/4 대역 IP 등장 = IPv6 접속자
- UA 분포: 95%+ 모바일 (Android Chrome / iPhone Safari / KakaoTalk / Daum / NAVER) — KR 모바일 IPv6 보급률 높음
- 동일 IPv6 `/64` prefix → 항상 같은 240/4 IP로 매핑 (재현 가능한 식별자)
- **한 240/4 IP = 한 가정/ /64 단위 유저 집합** — ban 시 /64 전체가 차단됨
### 오해 주의
- 240/4는 KR 통신사 CGNAT 아님 (공식 문서 없음, 공개 라우팅도 안 됨)
- 실제로는 CF Pseudo IPv4 해시값 — "가짜 IP"
- CGNAT 오인하면 차단 효과 완전히 다르게 판단됨
### 관련 링크
- [CF Pseudo IPv4 docs](https://developers.cloudflare.com/network/pseudo-ipv4/)
- [CF Support: Pseudo IPv4 overview](https://support.cloudflare.com/hc/en-us/articles/229666767)
## Zone ## Zone
| Zone | Zone ID | Status | Plan | NS | DNS rec | 비고 | | Zone | Zone ID | Status | Plan | NS | DNS rec | 비고 |
@@ -119,7 +159,7 @@ DNS 레코드 없음. zone 만 등록. 워커 라우트 + Turnstile 위젯 + cer
| Worker | 라우트 부착 zones | 비고 | | Worker | 라우트 부착 zones | 비고 |
|---|---|---| |---|---|---|
| **crowdsec-cloudflare-worker-bouncer** | actions, ironclad, keepanker, servidor | CrowdSec CF bouncer 본체. cs-cf-worker-bouncer (jp1) 가 168h 마다 secret rotate. 정본 [[crowdsec-safeline]] | | ~~**crowdsec-cloudflare-worker-bouncer**~~ | _(폐기)_ | **2026-04-26 bouncer 단일화로 Worker 스크립트 + KV namespace + 라우트 4개 모두 삭제.** [[../../history/2026-04-26-bouncer-consolidation\|history]] |
| **ironclad-site** | ironclad.it.com (apex) | 정적 사이트, has_assets | | **ironclad-site** | ironclad.it.com (apex) | 정적 사이트, has_assets |
| **cf-multisite** | `*.actions.it.com/*` | 라우팅 워커 | | **cf-multisite** | `*.actions.it.com/*` | 라우팅 워커 |
| chat-worker | (없음) | workers.dev only | | chat-worker | (없음) | workers.dev only |
@@ -133,16 +173,15 @@ DNS 레코드 없음. zone 만 등록. 워커 라우트 + Turnstile 위젯 + cer
| Zone | Pattern | Script | | Zone | Pattern | Script |
|---|---|---| |---|---|---|
| actions.it.com | `*actions.it.com/*` | crowdsec-cloudflare-worker-bouncer |
| actions.it.com | `*.actions.it.com/*` | cf-multisite | | actions.it.com | `*.actions.it.com/*` | cf-multisite |
| actions.it.com | `vultr.actions.it.com/*` | null (orphan, 미정리) | | actions.it.com | `vultr.actions.it.com/*` | null (orphan, 미정리) |
| actions.it.com | `linode.actions.it.com/*` | null (orphan, 미정리) | | actions.it.com | `linode.actions.it.com/*` | null (orphan, 미정리) |
| ironclad.it.com | `ironclad.it.com/*` | ironclad-site | | ironclad.it.com | `ironclad.it.com/*` | ironclad-site |
| ironclad.it.com | `*ironclad.it.com/*` | crowdsec-cloudflare-worker-bouncer |
| keepanker.cv | `*keepanker.cv/*` | crowdsec-cloudflare-worker-bouncer |
| servidor.it.com | `*servidor.it.com/*` | crowdsec-cloudflare-worker-bouncer |
inouter.com / anvil.it.com 에는 Worker 라우트 없음. DNS proxied=false 라 CF 엣지를 거치지 않으므로 enforce 불가 — 이들 zone 의 엣지 보호는 BunnyCDN 미들웨어 64811 이 단독 책임. > [!note] cs-cf-worker-bouncer 라우트 4개 폐기 (2026-04-26)
> `*actions.it.com/*`, `*ironclad.it.com/*`, `*keepanker.cv/*`, `*servidor.it.com/*` Worker 라우트는 bouncer 단일화로 모두 제거. Worker 스크립트 + KV namespace + Turnstile 위젯 4개 함께 폐기. [[../../history/2026-04-26-bouncer-consolidation|history]]
inouter.com / anvil.it.com 에는 Worker 라우트 없음. DNS proxied=false 라 CF 엣지를 거치지 않으므로 enforce 불가. ~~BunnyCDN 미들웨어 64811 단독 책임~~ 도 2026-04-26 폐기 — 현재 이들 zone 엣지 IP 차단 layer 부재 (Bunny Shield WAF + Rate Limit + 풀존 BlockedIps 만 가능).
Worker Custom Domains (계정 레벨): 0건. Worker Custom Domains (계정 레벨): 0건.
@@ -150,56 +189,47 @@ Worker Custom Domains (계정 레벨): 0건.
| Sitekey | 이름 | mode | 도메인 | 역할 | | Sitekey | 이름 | mode | 도메인 | 역할 |
|---|---|---|---|---| |---|---|---|---|---|
| `0x4AAAAAABvmO8BKc1ss5d-S` | `crowdsec-captcha` | managed | actions / anvil / charon.my / ironclad / keepanker / n8n.my / servidor / subin.my | multi-domain 운영용 (8 도메인). 수동 관리 (bouncer 외) | | `0x4AAAAAABvmO8BKc1ss5d-S` | `crowdsec-captcha` | managed | actions / anvil / charon.my / ironclad / keepanker / n8n.my / servidor / subin.my | multi-domain 운영용 (8 도메인). 수동 관리. **bouncer 외 사용처 추적 미완 — 폐기 보류** |
| `0x4AAAAAACbmaudAjITah7y7` | `inouter` | managed | anvil.it.com | 이름·도메인 불일치 (legacy/orphan 후보, 결정 미정) |
| `0x4AAAAAAC3otPWhldI96Aks` | `inouter-bunny-middleware` | managed | inouter.com | BunnyCDN 미들웨어 64811 의 `TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` env baked-in. 이름이 bouncer auto 패턴과 달라 cs-cf-worker-bouncer 가 관리 대상 외. **수동 관리**, sitekey/secret 변경 시 미들웨어 64811 env 동시 갱신 필수 |
| `0x4AAAAAAC3nIMLBRKWfiY8A` | `crowdsec-cloudflare-worker-bouncer-widget` | managed | actions.it.com | cs-cf-worker-bouncer 자동 (168h rotation). 수동 편집 금지 |
| `0x4AAAAAAC3nIYV_A5OA0Xzv` | 〃 | managed | ironclad.it.com | 〃 |
| `0x4AAAAAAC3nHnAB6Q9dlvHM` | 〃 | managed | keepanker.cv | 〃 |
| `0x4AAAAAAC3nH0xXSU8kbwsn` | 〃 | managed | servidor.it.com | 〃 |
`inouter-bunny-middleware` 의 secret 은 Vault `secret/cloud/cloudflare/turnstile-inouter-bunny` (`sitekey`, `secret`, `name`). > [!note] bouncer 단일화로 위젯 6개 정리 (2026-04-26)
> - `crowdsec-cloudflare-worker-bouncer-widget` × 4 (actions.it.com / ironclad.it.com / keepanker.cv / servidor.it.com): cs-cf-worker-bouncer 폐기로 자동 사용처 사라짐 → 삭제
> - `inouter-bunny-middleware` (`0x4AAAAAAC3otPWhldI96Aks`): BunnyCDN 미들웨어 64811 폐기와 함께 삭제
> - `inouter` (`0x4AAAAAACbmaudAjITah7y7`): legacy orphan 정리 차원 삭제
> - 정리 경과: [[../../history/2026-04-26-bouncer-consolidation|history]]
### Turnstile 토큰 권한 Vault 시크릿 잔여 (위젯 삭제됐지만 시크릿은 보존 — 재가동 시 새 위젯 발급해서 덮어쓰기):
- `secret/cloud/cloudflare/turnstile-crowdsec-captcha` (현역 `crowdsec-captcha` 위젯용)
- `secret/cloud/cloudflare/turnstile-inouter-bunny` (삭제된 `inouter-bunny-middleware` legacy)
### Turnstile 토큰 권한 (현재)
- **Vault `secret/cloud/cloudflare.api_token`** (`pUIZdTV0…`): DNS R/W + Turnstile **read-only**. 위젯 생성/수정/삭제 불가 - **Vault `secret/cloud/cloudflare.api_token`** (`pUIZdTV0…`): DNS R/W + Turnstile **read-only**. 위젯 생성/수정/삭제 불가
- **jp1 bouncer 호스트의 `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` 내 `token: seUKZID4…`**: Turnstile **read+write**. bouncer 의 자체 인증 자산이며 Vault 에 복제되어 있지 않음 - **`global_api_key`** (Vault `secret/cloud/cloudflare.global_api_key`): 모든 권한. 위젯 삭제 등 write 작업은 이쪽 사용
- ~~jp1 bouncer 호스트의 yaml 내 `token: seUKZID4…`~~: bouncer 컨테이너 삭제와 함께 분실 (2026-04-26)
## 특이사항 ## 특이사항
- **`*.actions.it.com → actions.b-cdn.net`** — 와일드카드가 dead 풀존 가리킴. 일부 서브가 5xx 가능. 미정리 (영향 평가 후 별건) - **`*.actions.it.com → actions.b-cdn.net`** — 와일드카드가 dead 풀존 가리킴. 일부 서브가 5xx 가능. 미정리 (영향 평가 후 별건)
- **Worker routes `vultr.actions.it.com/*`, `linode.actions.it.com/*`**`script: null` orphan. 미정리 - **Worker routes `vultr.actions.it.com/*`, `linode.actions.it.com/*`**`script: null` orphan. 미정리
- **Turnstile `inouter` (`…CbmaudAjITah7y7`)** — 이름·도메인 불일치 legacy 후보. 미정리 - ~~**Turnstile `inouter` legacy**~~ — 2026-04-26 정리 완료
- **Turnstile `crowdsec-captcha` (8 도메인)**`charon.my`, `n8n.my`, `subin.my` 등 사용처 미확인 → 폐기 보류
### CF proxy on/off 패턴 ### CF proxy on/off 패턴
| Zone | Proxy | 보호 | | Zone | Proxy | 보호 |
|---|---|---| |---|---|---|
| inouter.com / anvil.it.com | off | BunnyCDN 미들웨어 64811 단독 | | inouter.com / anvil.it.com | off | _IP-list enforce 부재 (2026-04-26 BunnyCDN 미들웨어 64811 폐기 후). Bunny Shield WAF + Rate Limit + 풀존 BlockedIps 만 가능_ |
| actions.it.com / ironclad.it.com | on | CF Worker (bouncer + cf-multisite / ironclad-site) | | actions.it.com / ironclad.it.com | on | CF Worker (cf-multisite / ironclad-site). ~~bouncer~~ 폐기 2026-04-26 |
| keepanker.cv | on (Tunnel) | CF Tunnel + juiceshop 만 iron-kr-waf override | | keepanker.cv | on (Tunnel) | CF Tunnel + juiceshop 만 iron-kr-waf override. ~~bouncer~~ 폐기 2026-04-26 |
| servidor.it.com | on | ~~bouncer~~ 폐기 2026-04-26, 현재 별도 보호 layer 없음 |
## cfb-manager (CrowdSec CF Worker bouncer 관리 API) > [!warning] cs-cf-worker-bouncer + BunnyCDN 미들웨어 64811 폐기 (2026-04-26)
> kappa zone 4개 (keepanker / actions / ironclad / servidor) + inouter / anvil 에서 CrowdSec 결정 enforce layer가 사라짐. CrowdSec ban은 현재 `netbis-cf-firewall` (netbis 6 zone) 만 enforce. 자세한 영향: [[../../history/2026-04-26-bouncer-consolidation|history]]
| 항목 | 값 | ## ~~cfb-manager~~ (폐기 2026-04-26)
|---|---|
| 위치 | K3s `default/cfb-manager` (`10.43.68.76:8000`) |
| 구현 | Python FastAPI |
| 기능 | bouncer 보호 도메인 추가/삭제, decision 조회, CF zone 동기화, bouncer 재시작 트리거 |
| SSH 키 | K8s `default/cfb-ssh-key` (ed25519) |
```bash > [!warning] cs-cf-worker-bouncer 폐기와 함께 cfb-manager도 의미 상실
BASE=http://cfb-manager.default.svc.cluster.local:8000 > cs-cf-worker-bouncer 자체가 사라져 `bouncer_running` / `widget rotate` 등 cfb-manager의 핵심 기능이 동작하지 않음. K8s `default/cfb-manager` Deployment는 별도 정리 결정 필요 (보존 / 폐기). [[../../history/2026-04-26-bouncer-consolidation|history]]
curl $BASE/status # bouncer_running, pids, protected_domains
curl $BASE/domains # 보호 중인 zone 상세
curl $BASE/decisions # 현재 결정
curl -X POST $BASE/domains/<zone> # zone 추가
curl -X DELETE $BASE/domains/<zone> # zone 제거 (destructive — bouncer restart + widget rotate)
```
**중요**: `DELETE /domains/<zone>` 는 단순 config 항목 제거가 아니라 bouncer restart 를 트리거하며, restart 가 모든 zone 의 widget 을 force-rotate 하고 제거된 zone 의 widget 을 destroy 한다. 외부에서 baked-in 으로 사용 중인 sitekey 가 있다면 동시에 깨진다.
BunnyCDN 미들웨어와는 무관. BunnyCDN 미들웨어 64811 의 bloom filter 동기화는 jp1 `infra-tool` 컨테이너 `/opt/crowdsec-bouncer/bouncer.py` (3분 delta + 매시 full sync) 가 담당.
## 관련 문서 ## 관련 문서

View File

@@ -0,0 +1,387 @@
---
title: CrowdSec 및 SafeLine WAF
updated: 2026-04-25
---
## CrowdSec LAPI
| 항목 | 값 |
|------|-----|
| 위치 | jp1 Incus `crowdsec` 컨테이너 |
| LAPI | `http://10.253.100.240:8080` |
| 관리 | `ssh incus-jp1 "incus exec crowdsec -- cscli ..."` |
| 버전 | v1.7.7 (최신, 2026-04-17 확인) |
## 로그 수집 (Acquisition)
### Traefik → CrowdSec (Vector)
```
Traefik DaemonSet (stdout JSON accessLog)
→ Vector Agent DaemonSet (K3s logging ns, kubernetes_logs source)
→ VRL transform (access log만 필터, non-JSON abort)
→ HTTP sink (배치 50건, 5초)
→ CrowdSec HTTP acquisition (:8086/traefik-logs)
→ crowdsecurity/traefik-logs 파서
```
| 항목 | 값 |
|------|-----|
| Vector | Helm `vector/vector` 0.51.0, Agent DaemonSet (3노드) |
| Values | `~/k8s/vector/values.yaml` |
| CrowdSec 포트 | 8086 |
| 인증 | `Authorization: traefik-crowdsec-log-2024` |
| 파서 | `crowdsecurity/nginx-logs` (Hub, 표준 nginx combined). Vector에서 모든 로그를 표준 포맷으로 변환 후 VictoriaLogs 저장 |
### APISIX + Traefik + NPM → VictoriaLogs → CrowdSec (통합 acquisition, 2026-04-25)
```
서울 APISIX (K3s) stdout → Vector DaemonSet ─┐
오사카 APISIX (Docker) stdout → Vector ────────┤
K3s Traefik stdout → Vector DaemonSet ────────┤→ VictoriaLogs (ES bulk API)
Netbis NPM 6대 nginx file → Vector → zlambda─┘
CrowdSec victorialogs acquisition (tail, 실시간, 단일 query OR로 통합)
→ custom/apisix-json-logs 파서 + crowdsecurity/nginx-logs (NPM 호환)
```
> [!note] anomaly-detect 분기 제거 (2026-04-25)
> 기존 `+ anomaly-detect (5분 폴링, AI 분석)` 분기는 폐기. [[../platform/anomaly-detect|anomaly-detect]] 인스턴스 완전 제거. [[../../history/2026-04-25-anomaly-detect-removal|history]]
| 항목 | 값 |
|------|-----|
| VictoriaLogs | `vl.inouter.com` (K3s logging ns, Traefik IngressRoute) |
| 통합 acquisition | `/etc/crowdsec/acquis.d/victorialogs-nginx.yaml` (`source: victorialogs`, query: `(program:apisix AND log_type:access) OR program:traefik OR (program:npm AND log_type:access)`, `labels: type: nginx`) |
| 이전 분리 파일 | `victorialogs-apisix.yaml.bak`, `victorialogs-traefik.yaml.bak`, `victorialogs-npm-netbis.yaml.bak` (롤백용 보존) |
| 서울 Vector | K3s DaemonSet (Helm `vector/vector`), `parse_apisix` / `parse_traefik` transform → `vlogs` ES sink |
| 오사카 Vector | Docker `timberio/vector:0.45.0-debian`, `parse_apisix``vlogs`. `location: osaka` 필드 추가 |
| Netbis NPM 경로 | NPM 호스트 Vector → zlambda vector-relay → VL `npm-netbis` 인덱스. nginx-logs parser가 NPM proxy format 호환 (cscli explain 검증) |
| 파서 | `crowdsecurity/nginx-logs` (Vector가 표준 nginx combined로 변환). NPM은 raw proxy format도 grok으로 핵심 필드 추출 정상 |
### APISIX → log-collector → CrowdSec (sandbox-tokyo)
```
sandbox-tokyo APISIX http-logger
→ log-collector (jp1 crowdsec 컨테이너, :8087)
→ SQLite (/var/lib/log-collector/requests.db)
→ CrowdSec HTTP acquisition (:8085/apisix-logs) 포워딩
```
| 항목 | 값 |
|------|-----|
| log-collector | Go HTTP 서버, `/usr/local/bin/log-collector` (jp1 crowdsec 컨테이너) |
| 소스 | jp1 `~/log-collector/main.go` |
| 수신 포트 | 8087 |
| 인증 | `Authorization: apisix-crowdsec-log-2024` |
| SQLite | `/var/lib/log-collector/requests.db` |
| 기능 | APISIX 로그 수신 → SQLite 저장 → CrowdSec 포워딩, 타임스탬프 밀리초 보정 |
### Netbis NPM → zlambda → VictoriaLogs (6대 오리진, 2026-04-23 수집 / 2026-04-25 CrowdSec 연동)
```
NPM-1..6 (Linode Tokyo public) Vector 0.55 file→http
→ zlambda(Tailscale+public) Vector-relay 0.45 http_server→elasticsearch
→ vl.inouter.com (index `npm-netbis`)
→ CrowdSec victorialogs acquisition (위 통합 파일에 query OR로 합류, 2026-04-25 연동)
→ nginx-logs parser → 시나리오 매칭 → LAPI decision (origin: crowdsec)
```
Netbis 오리진(NPM) nginx access/error 로그 수집. VL 은 LAN-only(192.168.9.53) 이므로 public NPM이 도달할 수 없어 zlambda 를 HTTP 중계로 투입. 상세: [[../../services/netbis#로그-수집-vector-→-zlambda-→-victorialogs|netbis]], [[../../history/2026-04-23-netbis-npm-vl-collection|history]]
**2026-04-25 CrowdSec 연동 완료.** 통합 acquisition `/etc/crowdsec/acquis.d/victorialogs-nginx.yaml` 의 query에 `(program:npm AND log_type:access)` 추가. **단**: 초기 `cscli explain` 결과로 "nginx-logs parser가 NPM proxy format도 lenient 호환"이라 적었던 부분은 잘못된 검증이었음 — 실제로는 `child-crowdsec/nginx-logs` unparsed 84%로 grok 미매칭. 정정 사항은 아래 `_msg` 재합성 서브섹션 참조.
#### `_msg` 재합성 (proxy_v2 → nginx combined, 2026-04-25)
CrowdSec `crowdsecurity/nginx-logs` Hub 파서는 표준 `NGINXACCESS` grok 만 매칭한다. NPM 의 `proxy_v2` raw 포맷(`[Client x] [Real y] ...`)은 grok 미지원이라 `child-crowdsec/nginx-logs` unparsed 84% 발생. **해결**: NPM Vector remap에 `_msg` 재작성 블록 추가 — 이미 추출된 메타필드(client_ip/method/path/status/bytes/user_agent/referer/log_time)로 표준 nginx combined 포맷을 합성해 `.message` 에 덮어쓰기. 원본은 `.original_message` 에 보존. 검증: `cscli explain --type nginx` 결과 `s01-parse: crowdsecurity/nginx-logs (+23 ~2)` 통과 + 시나리오(`crowdsecurity/http-crawl-non_statics`, `custom/apisix-high-rate-per-ip`, `custom/apisix-single-path-flood`) 매칭 확인. 상세: [[../../history/2026-04-25-netbis-npm-vector-msg-rewrite|history]]
`_msg` 가 이제 표준 nginx combined 포맷이라 hub 파서 그대로 동작 — 통합 acquisition의 `(program:npm AND log_type:access)` 분기에서 LAPI decision까지 자연스럽게 흐름.
### SafeLine → CrowdSec (실시간, PG LISTEN/NOTIFY)
```
SafeLine WAF 차단 → mgt_detect_log_basic INSERT
→ PG 트리거 (notify_detect_log) → pg_notify('safeline_detect', JSON)
→ safeline-listener (kr2, Go) → detail 테이블 enrichment (x-real-ip, user-agent)
→ CrowdSec HTTP acquisition (:8088/safeline-logs)
→ custom/safeline-http-logs 파서
→ custom/safeline-waf-blocked 시나리오 (trigger, 즉시 밴)
```
| 항목 | 값 |
|------|-----|
| safeline-listener | Go, `/usr/local/bin/safeline-listener` (kr2) |
| 소스 | kr2 `~/safeline-listener/main.go` |
| systemd | `safeline-listener.service` (kr2) |
| PG DSN | SafeLine CE DB (10.43.8.243:5432, safeline-ce) |
| CrowdSec 포트 | 8088 |
| 인증 | `Authorization: safeline-crowdsec-2026` |
| acquis 설정 | `/etc/crowdsec/acquis.d/safeline-http.yaml` |
| 파서 | `custom/safeline-http-logs` (`/etc/crowdsec/parsers/s01-parse/safeline-http-logs.yaml`) |
| 시나리오 | `custom/safeline-waf-blocked` (trigger 타입, 즉시 밴) |
| enrichment | req_header에서 `x-real-ip`, `user-agent` 추출, detail 테이블에서 method/payload 조회 |
PG 트리거 (SafeLine CE DB에 설치):
```sql
CREATE OR REPLACE FUNCTION notify_detect_log() RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('safeline_detect', json_build_object(
'id', NEW.id, 'ts', NEW.created_at,
'src_ip', NEW.src_ip, 'host', NEW.host,
'url_path', NEW.url_path, 'attack_type', NEW.attack_type
)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_detect_log
AFTER INSERT ON mgt_detect_log_basic
FOR EACH ROW EXECUTE FUNCTION notify_detect_log();
```
### ~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)
> [!warning] 폐기됨
> 2026-04-08 분석기 서비스 정지 + 삭제. "이 방식으로는 안 될 것 같다"는 판단 (60초 폴링 + Claude CLI 동기 호출 구조의 한계). 새로운 AI 분석 아키텍처는 [[victorialogs|VictoriaLogs]] 기반으로 재설계 예정.
>
> **제거된 항목**: `ddos-detect.service` (systemd), `/var/lib/log-collector/ddos-detect/` Go 바이너리 + 소스, `ddos-detect.sh`, `extract_behavior.py`, `ddos-logs/` (분석 결과 markdown). `requests.db`는 별개의 `log-collector` 데몬이 사용 중이라 보존. Gitea repo `kaffa/ddos-detect`는 보존 (코드 reference).
이전 동작 (참고):
| 항목 | 값 |
|------|-----|
| 위치 | jp1 crowdsec 컨테이너 `/var/lib/log-collector/ddos-detect/` |
| Gitea | `gitea.inouter.com/kaffa/ddos-detect` |
| 기능 | SQLite 폴링 → IP별 행위 패턴 추출 → Claude AI 분석 → cscli 자동 밴 |
| 설정 | `config.yaml` (poll 60s, Claude sonnet, ban 4h, max_workers 3) |
### 리얼 IP 처리
| 경로 | 설정 | source IP 추출 |
|------|------|---------------|
| BunnyCDN → HAProxy → **Traefik** | `forwardedHeaders.trustedIPs: 127/8, 10/8, 172.16/12, 192.168/16` + `externalTrafficPolicy: Local` | `ClientHost` (X-Forwarded-For에서 추출) |
| BunnyCDN → HAProxy → **APISIX** | `real_ip_header: X-Forwarded-For` + `real_ip_from: 127/8, 10.42/16, 10.43/16, 192.168.9/24, 100.64/10` | `client_ip` (nginx real_ip 모듈) |
Traefik values: `~/k8s/traefik/values.yaml`
## 시나리오
| 유형 | 시나리오 | 출처 |
|------|---------|------|
| HTTP | http-probing, http-crawl-non_statics, http-sensitive-files, http-backdoors-attempts, http-sqli-probing, http-xss-probing 등 | Hub |
| SafeLine | custom/safeline-waf-blocked (trigger), custom/safeline-waf-repeated (3+/5min) | 로컬 |
| **APISIX** (2026-04-08 신규) | custom/apisix-high-rate-per-ip, custom/apisix-499-burst, custom/apisix-single-path-flood, custom/apisix-5xx-burst | 로컬 |
| CAPI | http:dos, http:scan, ssh:bruteforce (커뮤니티) | 자동 |
### APISIX 시나리오 (4종, 2026-04-08 작성)
ddos-detect AI 분석기 폐기 후 deterministic 패턴 매칭으로 대체. 모두 leaky bucket, ban 4h. **K3s 서울 APISIX 트래픽만 보고 판단** (osaka/zlambda는 http-logger 송신 안 함).
| 시나리오 | 매개변수 | 의도 | filter |
|---|---|---|---|
| `apisix-high-rate-per-ip` | capacity 1000, leakspeed 100ms (≈ sustained 10 req/s, burst 1000) | 일반 HTTP flood. APISIX 자체 limit-req(20 req/s)에 안 걸리는 sustained 패턴 | `log_type=http_access-log`, groupby `source_ip` |
| `apisix-499-burst` | capacity 30, leakspeed 2s | connection exhaustion / Slowloris (클라이언트 강제 끊김) | `http_status == "499"` |
| `apisix-single-path-flood` | capacity 50, leakspeed 600ms | 동일 path 반복 (CAPTCHA 우회, login bf, 동일 자원 폭격) | groupby `source_ip + ":" + http_path` |
| `apisix-5xx-burst` | capacity 20, leakspeed 3s | 백엔드 부하 유발성 공격, 취약점 스캔 | `http_status startsWith "5"` |
⚠️ **운영 주의**:
- `5xx-burst`: 백엔드 장애 시 false positive 가능. 운영 알람과 분리해서 검토.
- `499-burst`: 모바일 클라이언트 끊김으로 정상 발생할 수 있어 30 임계값을 며칠 관찰 후 조정 권장.
- `high-rate-per-ip`: APISIX `limit-req` 글로벌 룰(20 req/s burst 10)을 통과한 트래픽만 도달 → 1차 통과 후 sustained pattern을 잡는 2차 layer.
- 현재 모두 `remediation: true` (즉시 ban). dry-run으로 시작 안 함 — false positive 발생 시 임계값 또는 ban duration 조정.
### 분산 DDoS 시나리오 (Hub, 도입 검토)
per-IP 시나리오는 분산(여러 IP 합산) 패턴을 못 잡음. CrowdSec Hub에서 제공하는 공식 시나리오로 보완 가능.
| 시나리오 | groupby | distinct | capacity | leakspeed | 의도 |
|---|---|---|---|---|---|
| `crowdsecurity/http-ddos-by-ASN` | `evt.Meta.ASNNumber` | `evt.Meta.source_ip` | 20 | 10s | 동일 ASN에서 distinct IP 20개가 10초 안에 도달 |
| `crowdsecurity/http-ddos-by-cn` | `evt.Meta.IsoCode` | `evt.Meta.source_ip` | 50 | 30s | 동일 국가에서 distinct IP 50개가 30초 안에 도달 |
공식 필터에 false positive 회피책이 미리 깔려 있음:
- `http_status == '200'` → 백엔드 5xx 장애 시 자폭 차단
- `static_ressource == 'false'` → CDN miss/이미지 burst 제외
- `distinct: source_ip` → 단일 IP 반복이 아닌 IP 다양성으로 측정
CrowdSec 공식 가이드 명시 사항 ([blog](https://www.crowdsec.net/blog/mitigate-ddos-with-crowdsec)):
> "banning these given countries will mean false positives and collateral damage."
**분산 시나리오는 ban이 아니라 `captcha` decision으로 emit하라는 게 공식 권장.** Hub 페이지에도 dos 계열은 "proper testing is advised" 경고가 직접 표기됨 (예: `http-dos-bypass-cache`).
**도입 전 점검 필요 (2026-04-25 현재 상태)**:
- profile.yaml은 ban only — captcha decision을 emit하려면 분기 추가 필수 (아래 [#알림-notification] 참조).
- bouncer 측 액션 매핑은 이미 captcha 쪽이라 ban으로 emit해도 사용자 체감은 캡챠와 동일 (아래 bouncer 절 표 참조). 하지만 의미 일관성을 위해 profile에 captcha 분기 도입 권장.
과거 인시던트 및 변경 이력은 `history/` 참조. 예: `history/2026-04-10-edge-cleanup.md` (cf-audit-cleanup-2 3-incident chain, Turnstile sitekey 교체, 미들웨어 64811 `/__captcha/verify` 버그 수정 등), [[2026-04-15-apisix-http-logger-removal|2026-04-15 APISIX http-logger 레거시 제거]].
### 발견 사항: K3s APISIX 글로벌 limit-req
```json
{
"key_type": "var",
"key": "remote_addr",
"rate": 20,
"burst": 10,
"rejected_code": 429
}
```
`/apisix/global_rules/limit-req` (etcd). 모든 라우트에 적용. CrowdSec 시나리오는 이 1차 차단을 통과한 트래픽만 본다는 점을 고려해서 임계값 설계.
## Bouncer
> [!note] Bouncer 단일화 (2026-04-26)
> CrowdSec bouncer를 `netbis-cf-firewall` 단일로 통합. 아래 ~~strikethrough~~ 항목 3종 (cs-cf-worker-bouncer / apisix-waf-bouncer / bunny-cdn-bouncer) 모두 폐기. 사유: 운영 단순화 + 비용 구조 비효율 + Worker bouncer Turnstile 위젯 168h rotation 부담. SafeLine WAF / APISIX limit-req / netbis-cf-firewall 보호는 그대로 유지.
>
> **사라진 enforce 영역**: kappa zone(keepanker.cv, actions.it.com, ironclad.it.com, servidor.it.com) CrowdSec ban / BunnyCDN 풀존(iron-jp, iron-kr) 엣지 차단 / APISIX 인스턴스 chaitin-waf 보완용 IP ban.
>
> 상세: [[../../history/2026-04-26-bouncer-consolidation|history]]
### ~~cs-cf-worker-bouncer (Cloudflare Worker)~~ — 폐기 (2026-04-26)
[[../../history/2026-04-26-bouncer-consolidation|history]] 참조. jp1 incus `cs-cf-worker-bouncer` 컨테이너 + 설정 파일 + LAPI 등록 모두 제거. CF Worker 스크립트 / KV namespace / Turnstile 위젯 4개 (`crowdsec-cloudflare-worker-bouncer-widget`)는 Syn에 위임 정리.
> **Netbis CF 바운서 (2026-04-23 폐기 → 2026-04-25 Firewall Rule 단독 재구축)**
>
> 2026-04-23: Worker + Firewall Rule 두 종류 모두 폐기 (Worker/KV 비용 + CF IP List 10k 한도). [[../../history/2026-04-23-netbis-bouncer-removal|history]]
>
> 2026-04-25: **`netbis-cf-firewall` 만 재구축**. 폐기 사유였던 10k 한도는 **origin filter `[crowdsec, cscli]`** 적용으로 회피 (CAPI/lists 30k+ 무시, 로컬 시나리오 발동만 푸시). Worker bouncer는 트래픽 비례 비용 구조라 origin filter로 회피 불가, **재구축 안 함**.
>
> 자세한 배경 및 구성: [[../../history/2026-04-25-netbis-cf-firewall-rebuild|history]], [[../../services/netbis#cloudflare-firewall-bouncer-재구축-2026-04-25|netbis 정본]]
### netbis-cf-firewall (Cloudflare Firewall Rule Bouncer, 재구축 2026-04-25)
| 항목 | 값 |
|------|-----|
| 컨테이너 | jp1 incus `crowdsec` (LAPI와 같은 컨테이너에 동거) |
| 패키지 | `crowdsec-cloudflare-bouncer 0.3.0` (apt) |
| 동작 | LAPI 폴링(10s) → CF Account IP List + Zone Firewall Rule 갱신 |
| LAPI bouncer 이름 | `cs-cloudflare-bouncer-1777082222` (자동 생성, rename 보류) |
| **origin filter** | `only_include_decisions_from: [crowdsec, cscli]` (CAPI/lists 30k+ 무시 → CF List 10k 한도 회피) |
| **액션 매핑** | `default_action: managed_challenge` (모든 5 zone 동일). LAPI decision type 무관하게 CF managed_challenge 액션으로 적용 → 사용자는 CF 캡챠 페이지 통과 후 진입. 분산 DDoS 시나리오 오탐 시에도 ban이 아니라 캡챠 |
| CF 리소스 | IP List `crowdsec_managed_challenge` (`f728ad9d4653467396d32466902c9e52`), 5 zone × Firewall Rule (managed_challenge 액션) 자동 생성 |
| 적용 zone | fall-vip / fall-vip7 / psd777 / rss-555 / rss-7790 (Account ID `8fcf3c7876332aba33e974cbbfdad951`). _2026-05-23 fall-mvp.com zone이 CF에서 삭제됨에 따라 config에서 제거_ |
| API token | Vault `secret/cloud/cloudflare-netbis` (`firewall_bouncer_token`, `firewall_bouncer_token_id`). 권한: Account Firewall Access Rules Write + Account Rule Lists Write + Zone Firewall Services Write |
| 인증 방식 | Bearer (`Authorization: Bearer cfut_...`). global_api_key는 `Invalid request headers (6003)`**사용 불가** — 신규 token 발급 필수 |
| config | `/etc/crowdsec/bouncers/crowdsec-cloudflare-bouncer.yaml` |
| systemd | `crowdsec-cloudflare-bouncer.service` |
| 비용 | $0 (Free plan 영역, IP List + Firewall Rule). Worker bouncer 대비 비용 구조 우월 |
| 한계 | CF 공식 deprecation 표기 ("isn't actively supported anymore"). 동작은 정상이나 향후 호환성 변화 가능 |
~~cs-cf-worker-bouncer (kappa 계정용)는 별도 컨테이너에서 그대로 운영~~**2026-04-26 폐기**. 현재 LAPI 등록 bouncer는 본 항목(`cs-cloudflare-bouncer-1777082222`) 단 하나. [[../../history/2026-04-26-bouncer-consolidation|history]]
### ~~bunny-cdn-bouncer (BunnyCDN Edge Script)~~ — 폐기 (2026-04-26)
[[../../history/2026-04-26-bouncer-consolidation|history]] 참조. jp1 `infra-tool` 컨테이너 cron + `/opt/crowdsec-bouncer/bouncer.py` 제거. LAPI 등록 삭제. BunnyCDN Edge Script 64811 (`crowdsec-bouncer-middleware`) 두 풀존(iron-jp 5555247 / iron-kr 5555227) middleware 해제 + 스크립트 삭제는 Syn 위임. Vault `secret/infra/crowdsec-bunny-bouncer` 시크릿 보존(별도 폐기 결정).
### ~~apisix-waf-bouncer (APISIX plugin)~~ — 폐기 (2026-04-26)
`apisix-plugin-crowdsec-bouncer 0.1` — APISIX 인스턴스(가능성: apisix-osaka)의 chaitin-waf 보완용 IP ban 플러그인. LAPI 등록 삭제 완료. APISIX plugin 비활성화 + plugin_metadata/route 정리는 Syn 위임. K3s 서울 / zlambda APISIX 는 미사용 확인 완료. 상세: [[../../history/2026-04-26-bouncer-consolidation|history]]
## 보안 구조 (2026-04-26 bouncer 단일화 이후)
```
클라이언트 → BunnyCDN WAF (OWASP CRS, 1차) — 일반 도메인 경로
→ Traefik / APISIX + SafeLine WAF (2차)
→ CrowdSec 로그 분석 (3차) → netbis-cf-firewall (Netbis 5 zone CF Firewall Rule 피드백 루프)
netbis 전용 enforce: CF Account IP List + 5 zone Firewall Rule managed_challenge (netbis-cf-firewall)
일반 zone enforce 사라짐 — kappa zone CrowdSec ban 미적용. BunnyCDN Edge bouncer / Worker bouncer / APISIX bouncer 모두 폐기 (2026-04-26)
```
### 1차: BunnyCDN WAF (OWASP CRS)
- CDN 에지에서 오리진 도달 전 차단
- 차단: SQLi, XSS, CMDi, SSRF, Shellshock, Log4j
- 비활성화: DATA LEAKAGES SQL (id=911) — NocoDB 오탐 방지
- 통과: Request Smuggling, NoSQLi, 경로 스캔
### 2차: SafeLine WAF (chaitin-waf)
- APISIX plugin_metadata → detector `10.43.253.244:8000`
- SafeLine v9.3.2, K3s safeline ns
- 관리: safeline.inouter.com, API 헤더 `X-SLCE-API-TOKEN`
- 토큰: Vault `secret/infra/safeline`
- tengine 미사용, APISIX 직접 연동
### 3차: CrowdSec 로그 분석
- Traefik / APISIX 로그: Vector → VictoriaLogs(`vl.inouter.com`) → CrowdSec `victorialogs` acquisition (tail 모드)
- SafeLine 차단: PG NOTIFY → safeline-listener → CrowdSec HTTP acquisition(`:8088`)
- sandbox-tokyo APISIX: http-logger → log-collector(`:8087`) → CrowdSec
- HTTP 시나리오 매칭 → decision → bouncer 피드백
## iron-kr-waf BunnyCDN Pull Zone (구 waf-kr)
APISIX 외부 인입 경로 (juiceshop SafeLine 통합 테스트 용도). APISIX 자체는 독립 LoadBalancer gateway (MetalLB 192.168.9.50) 이지 "SafeLine 전용" 이 아님 — 현재 1 route 가 SafeLine 용일 뿐.
| 항목 | 값 |
|------|-----|
| Zone ID | 5555224 |
| Name | iron-kr-waf |
| Origin | https://220.120.65.245:9443 |
| 경로 | 인터넷 → BunnyCDN(iron-kr-waf) → OpenWrt HAProxy(:9443) → MetalLB 192.168.9.50:443 → APISIX(K3s 서울) → chaitin-waf → SafeLine detector → K3s svc |
등록 호스트: `iron-kr-waf.b-cdn.net`, `juiceshop.keepanker.cv` (WAF 테스트용). ApisixRoute `juiceshop` (chaitin-waf block) → juiceshop:3000.
> 옛 메모의 `waf-kr (5554681)` 는 더 이상 존재하지 않음 — `iron-kr-waf (5555224)` 로 통합·재명명됨.
## 화이트리스트
| 파서 | 대역 | 이유 |
|------|------|------|
| `crowdsecurity/whitelists` (Hub) | 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, ::1, 127.0.0.0/8 | RFC1918 사설 IP |
| `custom/tailscale-whitelist` (로컬) | 100.64.0.0/10 | Tailscale CGNAT |
2026-04-17 추가: 기존에 `crowdsecurity/whitelists` 파서가 미설치 상태여서 내부 IP(192.168.9.1 OpenWrt)가 반복 밴됨. 설치 후 해결.
## Profile (decision 발급)
`/etc/crowdsec/profiles.yaml` — 시나리오 매칭 시 LAPI에 emit할 decision type을 결정하는 단계.
```yaml
name: default_ip_remediation
filters: [Alert.Remediation == true && Alert.GetScope() == "Ip"]
decisions: [{type: ban, duration: 4h}]
notifications: [http_default]
on_success: break
---
name: default_range_remediation
filters: [Alert.Remediation == true && Alert.GetScope() == "Range"]
decisions: [{type: ban, duration: 4h}]
notifications: [http_default]
on_success: break
```
**현재 상태: ban-only**. captcha/throttle decision은 emit되지 않음. ASN/Country scope 핸들러도 없음 — 분산 DDoS 시나리오를 import하면 매칭은 되지만 어떤 profile에도 안 잡혀서 decision 자체가 발급 안 될 수 있음 (`on_success: break` 없는 fallthrough 동작 확인 필요).
분산 DDoS 시나리오 도입 시 **상단에 captcha 분기 추가 필수** (예시):
```yaml
name: distributed_ddos_captcha
filters:
- Alert.Remediation == true && Alert.GetScenario() matches "(?i)ddos-by-(asn|cn|country|range)"
decisions:
- type: captcha
duration: 1h
on_success: break
---
# 기존 default_ip_remediation, default_range_remediation
```
## 알림 (Notification)
| 항목 | 값 |
|------|-----|
| 타입 | HTTP (Discord Bot API) |
| 이름 | `http_default` |
| 채널 | Discord `#heimdall` (1488119168145555486) |
| 트리거 | remediation=true인 모든 decision (IP, Range) |
| 그룹 | 30초 대기, 5건 이상 즉시 |
| Bot 토큰 | Vault `secret/apps/discord``bot_token` |
| 설정 | `/etc/crowdsec/notifications/http.yaml` |
## 참고
- BunnyCDN WAF 차단 시 오리진에 로그 안 옴 → CrowdSec에 미수신
- OpenWrt CrowdSec firewall bouncer는 DNAT 구조라 리얼 IP 매칭 불가
- chaitin-waf 플러그인은 `plugin_attr`이 아닌 **`plugin_metadata`(etcd)**에서 detector 노드를 읽음
- Incus 컨테이너에서 `sed -i`로 설정 수정 시 파일 손상 주의 (전체 파일 push 사용)

View File

@@ -32,7 +32,7 @@ Vault root token은 만료 없음 (TTL: 0s)
| 카테고리 | 경로 | 내용 | | 카테고리 | 경로 | 내용 |
|----------| ai/ | brave, context7, deepseek, google/drive-mcp, openai, openrouter, pinecone, testsprite, vertex | |----------| ai/ | brave, context7, deepseek, google/drive-mcp, openai, openrouter, pinecone, testsprite, vertex |
| apps/ | anomaly-detect, cfb-manager, cf-multisite, discord, figma, gitea, gitea/registry, k3s, myapp, n8n, nocodb, ops-agents-ssh, outline, portainer, postgres, sftpgo, telegram-ai-support, trader, twilio, waf-saas | | apps/ | cfb-manager, cf-multisite, discord, figma, gitea, gitea/registry, k3s, myapp, n8n, nocodb, ops-agents-ssh, outline, portainer, postgres, sftpgo, telegram-ai-support, trader, twilio, waf-saas (anomaly-detect 폐기 2026-04-25 — apps 경로는 비어있음, 유지) |
| auth/ | api-keys/openai, api-keys/stripe, github/oauth-gitea, google/ca/external-account-key, google/ca/service-account, google/oauth-gitea | | auth/ | api-keys/openai, api-keys/stripe, github/oauth-gitea, google/ca/external-account-key, google/ca/service-account, google/oauth-gitea |
| cloud/ | alibaba, aws, backblaze, backblaze/restic, bunnycdn, cloudflare, cloudflare-netbis, cloudflare/r2, cloudflare/turnstile-crowdsec-captcha, cloudflare/turnstile-inouter-bunny, latitude, lightsail, linode, r2-gitea, r2-multisite, r2-sftpgo, supabase, vultr, zenlayer | | cloud/ | alibaba, aws, backblaze, backblaze/restic, bunnycdn, cloudflare, cloudflare-netbis, cloudflare/r2, cloudflare/turnstile-crowdsec-captcha, cloudflare/turnstile-inouter-bunny, latitude, lightsail, linode, r2-gitea, r2-multisite, r2-sftpgo, supabase, vultr, zenlayer |
| company/ | bank, info, ironclad, korbit, koreaexim, popbill | | company/ | bank, info, ironclad, korbit, koreaexim, popbill |

15
openclaw/_index.md Normal file
View File

@@ -0,0 +1,15 @@
---
title: openclaw 인덱스
updated: 2026-04-16
tags: [moc, openclaw]
---
## OpenClaw
| 문서 | 설명 |
|------|------|
| [[openclaw-agents]] | OpenClaw 에이전트 시스템 (DarkRouter 등) |
| [[openclaw-jarvis]] | OpenClaw Jarvis 서버 (hp2 컨테이너) |
| [[openclaw-manual]] | OpenClaw 매뉴얼 인덱스 |
| [[openclaw-ollama]] | OpenClaw + Ollama 로컬 LLM 통합 |
| [[openclaw-reference]] | OpenClaw 레퍼런스 매뉴얼 (통합본) |

11
ops-agents/_index.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: ops-agents 인덱스
updated: 2026-04-16
tags: [moc, ops-agents]
---
## Ops Agents
| 문서 | 설명 |
|------|------|
| [[overview]] | 내부 운영 에이전트 개요 (Heimdall, Syn 등 북유럽 신화 명명) |

View File

@@ -0,0 +1,335 @@
---
date: 2026-04-20
topic: K3s 상세 점검 (기본 점검 이후 심화)
areas:
- infra/k8s/overview
- infra/data/longhorn
- infra/platform/argocd
- infra/observability/vector
---
# 2026-04-20 K3s 상세 점검
수집 시점 **2026-04-20 19:10 KST**. K3s v1.34.5+k3s1, containerd 2.1.5, Longhorn 1.8.2. 요청자: kappa.
## 종합 판정
| 섹션 | 판정 | 핵심 |
|---|---|---|
| 1. 노드 | OK | 전 노드 req/lim 40% 이하 |
| 2. NS 리소스 | OK | apisix mem 1위 3456Mi |
| 3. 파드 이슈 | OK | CrashLoop 0 |
| 4. PV/PVC | OK | 26 PVC 모두 Bound |
| 5. Longhorn | OK | 25 볼륨 healthy, 스냅샷 97/97 |
| 6. ArgoCD 14앱 | OK | 14/14 Synced+Healthy |
| 7. cert-manager | OK | 최단 62일 |
| 8. 네트워크 | OK | metallb 14%, 라우팅 24개 |
| 9. Helm 28 releases | OK | 모두 deployed |
| 10. 오늘 변경 | OK | 4/4 반영 |
| 11. graphify | OK | 불일치 없음 |
---
## 1. 노드 상세 — OK
### Capacity / Allocatable / Kernel
| Node | Role | CPU cap | Mem cap | Mem alloc | Kernel | Ready since |
|---|---|---|---|---|---|---|
| incus-hp1 | worker | 32 | 198.0 Gi | 198.0 Gi | 6.12.74+deb13+1 | 2026-04-19 12:43 |
| incus-hp2 | worker | 32 | 198.0 Gi | 198.0 Gi | 6.12.74+deb13+1 | 2026-04-13 19:51 |
| incus-kr1 | control-plane | 28 | 65.7 Gi | 65.7 Gi | 6.12.74+deb13+1 | 2026-04-16 07:33 |
| incus-kr2 | control-plane | 16 | 32.0 Gi | 21.6 Gi | 6.12.74+deb13+1 | 2026-04-19 12:08 |
kr2 `alloc mem` 21.6Gi < capacity 32Gi → 10.4Gi system-reserved. 타 노드 reserve 없음 (의도성 확인 여지).
### Requests / Limits
| Node | CPU req / lim | Mem req / lim |
|---|---|---|
| incus-hp1 | 5 (15%) / 420m (1%) | 4680 Mi (2%) / 11236 Mi (5%) |
| incus-hp2 | 5.7 (17%) / 1.8 (5%) | 7593 Mi (3%) / 18584 Mi (9%) |
| incus-kr1 | 5.34 (19%) / 5.8 (20%) | 3477 Mi (5%) / 8058 Mi (12%) |
| incus-kr2 | 2.29 (14%) / 400m (2%) | 1654 Mi (7%) / 3514 Mi (16%) |
전 노드 req/lim 40% 이하. 성장 여유 충분.
### Pressure / PLEG
전 노드 MemoryPressure/DiskPressure/PIDPressure=False. `reason=PLEGUnhealthy` 이벤트 0건.
### Longhorn 디스크 사용률
| Node | Available | Maximum | Scheduled | 사용률 |
|---|---|---|---|---|
| incus-hp1 (nvme) | 909 Gi | 938 Gi | 65 Gi | 3.1% |
| incus-hp2 | 895 Gi | 938 Gi | 137 Gi | 4.6% |
| incus-kr1 | 798 Gi | 936 Gi | 137 Gi | 14.7% |
| incus-kr2 | 798 Gi | 936 Gi | 64 Gi | 14.8% |
`/var/lib/rancher` 측정 미수행 (heimdall 컨테이너에서 호스트 SSH 불가). 다음 점검에서 longhorn-manager daemonset exec 경로로 수집.
---
## 2. NS 리소스 상위 10 — OK
파드 단위 CPU/메모리 request 합계 기준.
| # | Namespace | Pods | CPU req (core) | Mem req (MiB) |
|---|---|---|---|---|
| 1 | apisix | 7 | 0.64 | 3 456 |
| 2 | monitoring | 10 | 0.27 | 2 162 |
| 3 | kube-system | 22 | 1.45 | 1 770 |
| 4 | teable | 2 | 0.15 | 832 |
| 5 | logging | 5 | 0.30 | 768 |
| 6 | open-webui | 1 | 0.10 | 768 |
| 7 | teleport | 3 | 0.60 | 768 |
| 8 | openmemory | 3 | 0.15 | 704 |
| 9 | metallb-system | 5 | 0.00 | 640 |
| 10 | argocd | 7 | 0.20 | 600 |
apisix 메모리 1위 (etcd 3-member + dashboard + ingress-controller).
---
## 3. 파드 레벨 이슈 — OK
| 지표 | 값 |
|---|---|
| CrashLoopBackOff | 0 |
| Pending | 0 |
| Unknown | 0 |
| Non-Running pods | 0 |
| 재시작 ≥1회 (lifetime) | 28 |
재시작 누적 상위 10 (lifetime, 최근 6h 아님):
| NS | Pod | Restarts |
|---|---|---|
| democratic-csi | synology-iscsi-node-hlsr2 | 8 |
| longhorn-system | longhorn-csi-plugin-cqndq | 6 |
| monitoring | vm-stack-vmop | 5 |
| democratic-csi | synology-iscsi-node-287g6 | 5 |
| longhorn-system | longhorn-manager-l5smc | 4 |
| longhorn-system | csi-resizer-qk99l | 4 |
| kube-system | kube-multus-ds-sk7hh | 4 |
| longhorn-system | longhorn-csi-plugin-dt5hg | 3 |
| longhorn-system | longhorn-csi-plugin-97ssq | 3 |
| longhorn-system | csi-provisioner-vfbj2 | 3 |
재시작은 누적. "최근 6h" 판정 위해서는 `lastTerminated` 시각 필요 (본 점검에서 미수집). CrashLoop 0 이라 영향 없음. **주의**: democratic-csi 8회는 iSCSI 노드 드라이버 특성상 높은 편 — 원인 확인 여지.
---
## 4. PV / PVC — OK
| StorageClass | PVC 수 | Bound | Pending | Provisioner |
|---|---|---|---|---|
| longhorn | 25 | 25 | 0 | driver.longhorn.io |
| nfs (legacy 1개) | 1 | 1 | 0 | cluster.local/...nfs-subdir-external-provisioner |
| synology-iscsi | 0 | — | — | org.democratic-csi.iscsi.synology (준비됨, 미사용) |
| local-path | 0 | — | — | rancher.io/local-path |
| **합계** | **26** | **26** | **0** | |
기본 SC 2개 공존 (`local-path`, `longhorn`). **주의**: 이중 default 는 권장되지 않음 — 정리 검토.
---
## 5. Longhorn 디테일 — OK
### 볼륨/Replica 요약
- 전체 볼륨: **25** (모두 attached+healthy)
- 전체 replica: **72** 개, 모두 running, failedAt="", rebuildRetryCount=0
- 배치: hp2 25, kr1 25, kr2 15, **hp1 3** (신규 노드, rebalance 미완)
### Replica 정책
| numberOfReplicas | 볼륨 수 |
|---|---|
| 3 | 18 |
| 2 | 7 (safeline 7개: chaos/tengine-logs/detector-logs/luigi/mgt/detector/database) |
**주의**: safeline 2-replica 정책 의도성 확인 필요. 2-replica 는 단일 노드 장애 시 복구 안전지대 없음.
### 최근 6h 스냅샷
| 구분 | 성공 | 실패 |
|---|---|---|
| critical-snapshot (hourly) | 91 | 0 |
| standard-snapshot (daily 18:00) | 6 | 0 |
| **합계** | **97** | **0** |
### RecurringJob
| Name | Schedule | Retain | Task |
|---|---|---|---|
| critical-snapshot | `0 * * * *` | 24 | snapshot |
| critical-backup | `0 */6 * * *` | 28 | backup |
| standard-snapshot | `0 18 * * *` | 7 | snapshot |
| standard-backup | `0 19 * * *` | 7 | backup |
graphify `K3s Backup Pipeline` / `Longhorn RecurringJob (4 jobs)` 노드와 일치.
---
## 6. ArgoCD 14 앱 — OK
| App | Sync | Health | Last Sync (UTC) | Revision |
|---|---|---|---|---|
| bunnycdn-mcp | Synced | Healthy | 2026-04-13 07:03 | f9054536 |
| cfb-manager | Synced | Healthy | 2026-04-13 07:30 | 07dd408c |
| juiceshop | Synced | Healthy | 2026-04-13 06:43 | 550488f8 |
| kroki | Synced | Healthy | 2026-04-13 06:43 | 550488f8 |
| namecheap-api | Synced | Healthy | 2026-04-13 06:43 | 550488f8 |
| nas-proxy | Synced | Healthy | 2026-04-13 06:43 | 550488f8 |
| openmemory | Synced | Healthy | 2026-04-19 05:56 | c572d356 |
| outline | Synced | Healthy | 2026-04-19 05:56 | c572d356 |
| pgpool | Synced | Healthy | 2026-04-16 07:39 | 0a94c94f |
| proxysql | Synced | Healthy | 2026-04-13 06:43 | 550488f8 |
| searxng | Synced | Healthy | 2026-04-13 06:43 | 550488f8 |
| smtp-relay | Synced | Healthy | 2026-04-13 07:30 | 07dd408c |
| vault-mcp | Synced | Healthy | 2026-04-13 06:53 | 0f5a662a |
| vultr-api | Synced | Healthy | 2026-04-13 06:43 | 550488f8 |
- 14/14 Synced + Healthy
- 전 앱 `operationState.phase=Succeeded`
- 가장 오래된 sync 2026-04-13 (7일 전) — auto-sync 하 드리프트 없음
---
## 7. cert-manager — OK
| Certificate | Domain | Ready | NotAfter (UTC) | 만료까지 (일) |
|---|---|---|---|---|
| wildcard-actions-it-com | *.actions.it.com | True | 2026-06-21 18:19 | 62 |
| wildcard-anvil-it-com | *.anvil.it.com | True | 2026-06-21 18:14 | 62 |
| wildcard-api-inouter | *.api.inouter.com | True | 2026-06-24 02:54 | 64 |
| wildcard-inouter | *.inouter.com | True | 2026-06-21 18:12 | 62 |
| wildcard-ironclad-it-com | *.ironclad.it.com | True | 2026-06-21 18:12 | 62 |
| wildcard-keepanker-cv | *.keepanker.cv | True | 2026-06-21 18:12 | 62 |
| wildcard-mcp-inouter | *.mcp.inouter.com | True | 2026-06-24 03:56 | 64 |
| wildcard-servidor-it-com | *.servidor.it.com | True | 2026-06-21 18:12 | 62 |
30일 이내 만료 0건. 최단 잔여 62일.
---
## 8. 네트워크 — OK
### MetalLB
- Pool: `default-pool` = `192.168.9.50-192.168.9.99` (50 IPs)
- 할당: 7 / 50 (14%)
| IP | Namespace | Service |
|---|---|---|
| 192.168.9.50 | apisix | apisix-gateway |
| 192.168.9.51 | sshpiper | sshpiper |
| 192.168.9.52 | teleport | teleport-cluster |
| 192.168.9.53 | kube-system | traefik |
| 192.168.9.54 | gitea | gitea-ssh |
| 192.168.9.55 | sftpgo | sftpgo |
| 192.168.9.56 | db | haproxy-pg |
### 라우팅 리소스
| 타입 | 개수 | 비고 |
|---|---|---|
| Traefik IngressRoute | 13 | argocd-server, bunnycdn-mcp-tls, longhorn-ui(+tls), nas-proxy-tls, open-webui-tls, outline, portainer-tls, teable-tls, vault-mcp(+hcv)-tls, vector, vlogs |
| Gateway API HTTPRoute | 11 | argocd, bunnycdn-mcp, gitea, grafana, kroki, n8n, nocodb, openmemory-mcp, safeline-mgt, searxng, sftpgo-web |
| APISIXRoute | 0 | APISIX 설치됨(helm)이나 CR 미사용 |
| Ingress | 0 | |
graphify `Traefik DaemonSet + Gateway API`, `APISIX→Traefik 메인 라우팅 전환` 기록과 정합.
---
## 9. Helm releases — OK
**28개** 릴리스, 모두 `deployed`.
| NS | Release | Chart | App Ver | Updated |
|---|---|---|---|---|
| apisix | apisix | apisix-2.13.0 | 3.15.0 | 2026-04-20 08:21 |
| apisix | apisix-ingress-controller | apisix-ingress-controller-1.1.2 | 2.0.1 | 2026-04-19 14:53 |
| argocd | argocd | argo-cd-9.4.16 | v3.3.5 | **2026-04-20 12:01** |
| cert-manager | cert-manager | cert-manager-v1.20.0 | v1.20.0 | 2026-04-19 14:54 |
| kube-system | descheduler | descheduler-0.35.1 | 0.35.1 | 2026-04-19 14:25 |
| external-secrets | external-secrets | external-secrets-2.3.0 | v2.3.0 | 2026-04-19 14:55 |
| gitea | gitea | gitea-12.5.0 | 1.25.4 | 2026-04-19 14:54 |
| db | haproxy-pg | haproxy-pg-0.1.0 | 3.1 | 2026-04-16 17:31 |
| longhorn-system | longhorn | longhorn-1.8.2 | v1.8.2 | 2026-04-19 15:05 |
| metallb-system | metallb | metallb-0.15.3 | v0.15.3 | 2026-04-20 08:14 |
| n8n | n8n | n8n-2.0.1 | 1.122.4 | 2026-03-25 10:15 |
| nfs-provisioner | nfs-provisioner | nfs-subdir-external-provisioner-4.0.18 | 4.0.2 | 2026-04-19 14:55 |
| tools | nocodb | nocodb-1.10.0 | 0.301.5 | 2026-04-13 15:29 |
| open-webui | open-webui | open-webui-13.3.1 | 0.8.12 | 2026-04-19 16:25 |
| db | pgcat | pgcat-0.1.0 | 0.2.5 | 2026-04-16 17:07 |
| portainer | portainer | portainer-239.1.0 | ce-latest-ee-2.39.1 | 2026-04-19 14:55 |
| kube-system | reflector | reflector-10.0.21 | 10.0.21 | 2026-04-19 14:55 |
| safeline | safeline | safeline-10.1.0 | 9.3.2 | 2026-03-23 20:12 |
| sftpgo | sftpgo | sftpgo-0.44.0 | 2.7.1 | 2026-03-27 16:13 |
| sshpiper | sshpiper | sshpiper-0.4.6 | v1.5.0 | 2026-04-19 14:55 |
| democratic-csi | synology-iscsi | democratic-csi-0.15.1 | 1.0 | 2026-04-20 08:54 |
| teable | teable | teable-0.1.0 | latest | 2026-04-19 14:57 |
| teleport | teleport-cluster | teleport-cluster-18.7.3 | 18.7.3 | 2026-04-13 15:14 |
| kube-system | traefik | traefik-39.0.6 | v3.6.11 | 2026-04-19 14:54 |
| logging | vector | vector-0.51.0 | 0.54.0-distroless-libc | **2026-04-20 12:04** |
| velero | velero | velero-12.0.0 | 1.18.0 | 2026-04-20 10:04 |
| logging | vlogs | victoria-logs-single-0.11.31 | v1.49.0 | 2026-04-08 20:22 |
| monitoring | vm-stack | victoria-metrics-k8s-stack-0.72.6 | v1.139.0 | 2026-04-19 14:54 |
outdated 판정 본 점검 미수행. 2주 이상 `Updated` 없는 릴리스: n8n(3-25), sftpgo(3-27), safeline(3-23). ArgoCD auto-sync 커버리지 여부 확인 여지.
---
## 10. 오늘(2026-04-20) 변경 반영 확인 — OK (4/4)
| 항목 | 기대 | 실제 | 판정 |
|---|---|---|---|
| argocd-application-controller memory limit | 1Gi | `1Gi` | ✅ |
| vector container memory limit | 512Mi | `512Mi` | ✅ |
| vector buffer `max_events` | 10000 | `10000` | ✅ |
| vector buffer `retry_max_duration_secs` | 300 | `300` | ✅ |
참고:
- vector DS 4개 파드 restart count **0**, 시작 시각 2026-04-20 12:04:22~30 UTC
- 재시작 시점(03:04 UTC)에 `vlogs` 싱크 Healthcheck 400 1회 — 이후 30분간 동일 에러 미발생, buffer retry 로 복구
- vector `retry_initial_backoff_secs: 2`
- argocd application-controller `requests` 는 384Mi 유지
---
## 11. graphify 크로스체크 — OK
| graphify 노드 | 라이브 상태 | 정합 |
|---|---|---|
| Longhorn v1.8.2 | helm `longhorn-1.8.2` | ✅ |
| Traefik DaemonSet + Gateway API | traefik v3.6.11 + 13 IngressRoute + 11 HTTPRoute | ✅ |
| MetalLB L2 도입 | metallb v0.15.3, pool 9.50-99 | ✅ |
| Longhorn RecurringJob (4 jobs: critical/standard) | 4 RecurringJob (snapshot×2 + backup×2) | ✅ |
| Vector Log Collector → VictoriaLogs Log Pipeline | vector→vlogs 구성 | ✅ |
| APISIX→Traefik 메인 라우팅 전환 (2026-03-25 history) | Traefik 중심 HTTPRoute, APISIX CR 0 | ✅ |
| K3s PostgreSQL 백엔드 이전 (2026-03-24 history) | hp2 합류·운영 (uptime 6d23h+) | ✅ |
graphify 기록과 라이브 상태 불일치 없음.
---
## 후속 권장
1. Heimdall: longhorn-manager daemonset 으로 호스트 `/var/lib/rancher` 사용률 측정 경로 마련.
2. Heimdall: safeline 7볼륨 replica=2 정책 의도 확인 → 백서 기준 확정.
3. Heimdall: hp1 신규 노드로 Longhorn replica rebalance (scheduled=65Gi 만).
4. Heimdall: vector → vlogs 초기 healthcheck 400 원인 조사.
5. Heimdall: default StorageClass 2개 중 하나로 통일 검토.
6. Heimdall: iSCSI democratic-csi node-plugin 8회 재시작 원인 (syslog/dmesg).
---
## 비고: 본 리포트 산출 경로
- 원래 Outline `heimdall` 컬렉션에 업로드 시도했으나 **BunnyCDN Shield 403** 로 상세 본문 차단 (요약 문서 ID `c1ec3f2c-0fa8-49f8-9d0b-3d619a0e4715` 만 생성 완료, 부모 아래 하위 섹션 생성 시 WAF 차단).
- Gitea 업스트림 504 로 `git push` 도 대기. 로컬에 파일 먼저 commit, push 는 gitea 회복 시 재시도.
- Syn 에게 Outline 업로드 경로 WAF 룰 확인 요청 대상 (본 점검 범위 외 follow-up).

View File

@@ -0,0 +1,214 @@
---
date: 2026-04-20
topic: K3s 개선 6건 (default SC / Longhorn rebalance / safeline replica / iSCSI 재시작 / vector healthcheck / kr2 reserved)
areas:
- infra/compute/infra-hosts
- infra/data/k3s-backup
- infra/observability/vector
---
# 2026-04-20 K3s 개선 6건
전날 `2026-04-20 / K3s 상세 점검` 에서 도출된 후속 작업 6건 실행.
## 1. Default StorageClass 통일 — OK
### 변경 전
```
local-path true
longhorn true
longhorn-static false
nfs false
synology-iscsi false
```
### 실행
```bash
kubectl patch sc local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
```
### 변경 후
```
local-path false
longhorn true ← 유일 default
longhorn-static false
nfs false
synology-iscsi false
```
판정: **OK**. 신규 PVC 는 명시적 storageClassName 없으면 `longhorn` 으로 바인딩.
---
## 2. hp1 Longhorn rebalance — OK
### 현상태 확인
- `replica-auto-balance` 세팅: 이미 `best-effort` (원래 설정됨, `infra/compute/infra-hosts.md:46` 기록 존재)
- 즉시 trigger 없이 신규 replica 배치 + 기존 볼륨 rebuild 시 자동 재분산
### 3번 작업 (safeline replica 2→3) 부수효과로 hp1 배치 대폭 증가
```
변경 전: hp1=3 hp2=25 kr1=25 kr2=15 (total 68)
변경 후: hp1=10 hp2=25 kr1=25 kr2=15 (total 75)
```
7개 신규 replica 가 전부 hp1 에 배치됨 — `best-effort` + 신규 노드 가중치. 추가 조치 불필요.
판정: **OK**. 기존 설정이 이미 최적, 신규 replica 분산 작동 확인.
---
## 3. Safeline 7 볼륨 replica 2→3 통일 — OK
### Obsidian 의도 조회
`infra/` 전반에 safeline volume `replica=2` 의도 기록 **없음**. `infra/network/k3s-ingress-architecture.md:47` 에 tengine replica 0, `infra/network/apisix.md:51` 에 APISIX deployment replica 2 (파드 replica, Longhorn 볼륨 아님) 기록만 확인. 정본 기본 정책은 **replica=3** (`infra/data/k3s-backup.md:154, 179` numberOfReplicas: 3).
→ 명시적 근거 없음 → 3으로 통일해 정책 일관성 확보.
### 실행
```bash
for vol in pvc-0440758f-...safeline-chaos \
pvc-15af4f6d-...safeline-tengine-logs \
pvc-384dd143-...safeline-detector-logs \
pvc-3c39ef90-...safeline-luigi \
pvc-7da9941f-...database-data-safeline-database-0 \
pvc-8534d9f3-...safeline-mgt \
pvc-8f9bfed6-...safeline-detector; do
kubectl -n longhorn-system patch volume.longhorn.io $vol --type=merge \
-p '{"spec":{"numberOfReplicas":3}}'
done
```
### 결과 (25초 이내 전 볼륨 healthy)
| Volume | PVC | numberOfReplicas | robustness |
|---|---|---|---|
| pvc-0440758f-... | safeline-chaos | 3 | healthy |
| pvc-15af4f6d-... | safeline-tengine-logs | 3 | healthy |
| pvc-384dd143-... | safeline-detector-logs | 3 | healthy |
| pvc-3c39ef90-... | safeline-luigi | 3 | healthy |
| pvc-7da9941f-... | database-data-safeline-database-0 | 3 | healthy |
| pvc-8534d9f3-... | safeline-mgt | 3 | healthy |
| pvc-8f9bfed6-... | safeline-detector | 3 | healthy |
판정: **OK**. 전체 25 Longhorn 볼륨 replica=3 통일 완료.
---
## 4. democratic-csi iSCSI node 재시작 원인 조사 — 관찰 유지
### 대상 파드별 상태
| Pod | Node | Restarts | Last State | Last Terminated |
|---|---|---|---|---|
| synology-iscsi-democratic-csi-node-hlsr2 | hp2 | 8 | Unknown (exitCode 255) | 2026-04-13 10:51 UTC |
| synology-iscsi-democratic-csi-node-287g6 | kr2 | 5 | Unknown (exitCode 255) | 2026-04-19 03:08 UTC |
| synology-iscsi-democratic-csi-node-mvtg7 | kr1 | 1 | - | - |
| synology-iscsi-democratic-csi-node-w9ccj | hp1 | 0 | - | - |
### 컨테이너별 종료 분석 (hlsr2, 8 restart)
```
csi-driver exitCode 255 finishedAt 2026-04-13T10:51:32Z
csi-proxy exitCode 255 finishedAt 2026-04-13T10:51:32Z
driver-registrar exitCode 255 finishedAt 2026-04-13T10:51:32Z
cleanup exitCode 255 finishedAt 2026-04-13T10:51:32Z
```
**4개 컨테이너가 동일 시각, 동일 exitCode 255 로 종료** → pod-level signal (kubelet kill, 일반적으로 helm upgrade / DaemonSet spec 변경 / node drain).
- `helm history synology-iscsi` 기준 최근 업데이트 2026-04-20 08:54 KST 이지만, 이전 업그레이드 주기와 일치하는 타임스탬프 다수.
- `kubectl logs --previous` 전수: 정상 Probe 주기 기록만, 에러/경고 없음.
- `kubectl get events -n democratic-csi`: 결과 없음.
- 현재 전 파드 Ready, 로그에 에러 없음, 신규 파드 (hp1 w9ccj) restart 0 으로 clean.
### 판정
**현재 문제 없음 (관찰 유지)**. "재시작 누적"은 helm chart DaemonSet 재생성으로 인한 pod 재시작 (node 는 계속 Ready 상태라 drain 아님) 이 누적된 것으로 추정. 실제 장애 없음. 추가 계측이 필요하면 DaemonSet 에 `preStop: sleep 5` + 구조화된 exit log 추가. 현재 사이클에서는 **조치 불필요**.
---
## 5. Vector → vlogs healthcheck 400 — OK
### 원인
`vector` sink type=`elasticsearch` 의 healthcheck (startup 시점 1회) 가 엔드포인트 base URL 에 대해 elasticsearch cluster 정보 쿼리를 보냄. vlogs 의 elasticsearch-compat 엔드포인트 (`/insert/elasticsearch/`) 는 `/` 또는 `/_cluster/health` 같은 ES 표준 경로에 **400 Bad Request** 로 응답 → vector 로그에 `Healthcheck failed. error=Unexpected status: 400 Bad Request` 기록. 실제 데이터 경로는 정상, buffer retry 300s 로 자동 복구.
### 조치
healthcheck disable — 데이터 경로에 영향 없고 시작 시점 false alarm 제거.
```bash
helm -n logging upgrade vector \
oci://ghcr.io/vectordotdev/helm-charts/vector --version 0.51.0 \
--reuse-values \
--set customConfig.sinks.vlogs.healthcheck.enabled=false
```
### 결과 검증
- ConfigMap `vector.yaml``vlogs.healthcheck.enabled: false` 반영 확인
- DS 4개 파드 전체 재생성 (age 37~46s), restart 0
- 신규 파드 로그: `vector::topology::builder: Healthcheck disabled.` 명시적 기록
- 이후 `kubectl logs ds/vector --since=5m` 에서 error/fail 패턴 0건
판정: **OK**. 재발 차단 완료.
---
## 6. kr2 system-reserved 10Gi 조사 — 기록 후 정책 통일 검토
### kubelet config 비교 (`/api/v1/nodes/<n>/proxy/configz`)
| Node | systemReserved | kubeReserved | evictionHard |
|---|---|---|---|
| incus-kr2 | **memory: 8Gi** | null | **memory.available: 2Gi** + imagefs/nodefs 5% |
| incus-kr1 | null | null | imagefs/nodefs 5% |
| incus-hp2 | null | null | imagefs/nodefs 5% |
| incus-hp1 | null | null | imagefs/nodefs 5% |
### 원인
- **kr2만** `systemReserved.memory=8Gi` + `evictionHard.memory.available=2Gi` 로 설정됨 (합 10Gi).
- 32Gi capacity - 10Gi reserve ≈ 21.6Gi allocatable 과 정확히 일치.
- k3s CLI `--kubelet-arg="system-reserved=memory=8Gi"` 또는 `/etc/rancher/k3s/config.yaml``kubelet-arg` 항목으로 설정된 것으로 추정 (호스트 SSH 접근 제한으로 본 세션에서 직접 확인 불가).
### Obsidian 정본 검토
`infra/compute/infra-hosts.md` 및 k3s 관련 문서 어디에도 kr2 memory reserve 의도 기록 **없음**.
### 판정
**비대칭 설정, 의도 불명**.
- kr2 capacity 32Gi (다른 노드 대비 하위), 실제 usage mem 16% → reserve 가 과도할 수 있음.
- 그러나 과거 OOM 이슈로 설정된 가능성도 배제 못함.
- **Heimdall: 다음 점검에서 kr2 호스트 SSH 또는 kappa 경유 `/etc/rancher/k3s/config.yaml` 확인 후 정책 결정** — 과한 reserve 해소하려면 해당 설정 제거 + systemd k3s 재시작 (node NotReady 순간 발생, Longhorn replica 영향 계산 후 실행).
- 현재 사이클에서는 설정 존재만 **`infra/compute/infra-hosts.md` 에 명시 기록**, 통일 실행은 후속.
---
## 종합
| # | 작업 | 결과 |
|---|---|---|
| 1 | Default SC local-path=false | ✅ 완료 |
| 2 | hp1 Longhorn rebalance | ✅ best-effort 자연 분산 (3→10 replica) |
| 3 | safeline 7볼륨 replica 3 | ✅ 전 볼륨 healthy |
| 4 | iSCSI 재시작 원인 | 🔵 helm-upgrade 주기 추정, 현재 정상, 관찰 유지 |
| 5 | vector vlogs healthcheck | ✅ disable, 재발 차단 |
| 6 | kr2 system-reserved | 📝 설정 기록 완료, 통일 실행 다음 사이클 |
5/6 즉시 조치 완료, 6번 1건은 호스트 접근 필요 작업으로 후속.
### 다음 점검 대상
1. kr2 `/etc/rancher/k3s/config.yaml``kubelet-arg` 확인 → system-reserved 의도 확정, 불필요시 제거.
2. democratic-csi DaemonSet `preStop` hook 추가 여부 검토 (정상 exit 로그화).
3. hp1 Longhorn replica 추가 분산 (현재 10, 이상적 balance 대비 여전히 부족).

View File

@@ -0,0 +1,55 @@
---
date: 2026-04-21
topic: outline APISIX 라우트 + SafeLine WAF 부착
areas:
- infra/network/apisix
- infra/network/k3s-ingress-architecture
- services/outline
---
# 2026-04-21 outline APISIX 라우트 + SafeLine WAF 부착
## 한 줄 요약
`outline.inouter.com` 을 Traefik 에서 APISIX 경유로 이전 (Bunny iron-kr-nowaf pull zone origin 전환 준비). TLS + route 를 `ApisixTls`/`ApisixRoute` CRD 로 등록 후 `chaitin-waf` plugin 부착해 SafeLine WAF 경유로 완성. 정상 요청 pass, SQLi 403 reject 검증 완료. Traefik IngressRoute 는 롤백 대비 유지.
## 타임라인 (UTC)
| 시각 | 이벤트 |
|---|---|
| 00:58 | Admin API 로 SSL `outline-inouter` 최초 생성 (status=null → SNI 매칭 실패) |
| 00:59 | SSL 재 PUT (`status:1` 추가) → TLS handshake 정상, `curl --resolve` 200 |
| 01:04 | apisix-ingress-controller full-sync 에 의해 SSL `outline-inouter` **purge** (orphan 판정) |
| 01:06 | `ApisixTls` + `ApisixRoute` CRD 생성 (`ingressClassName: apisix` 필수), synced → SSL `4e7704e0`, Route `ce4d2d80` |
| 01:11 | `chaitin-waf` plugin ApisixRoute 에 부착 |
| 01:12 | 검증 완료 (정상 200 pass, SQLi 403 reject) |
## 최종 리소스
| 종류 | 위치 | APISIX ID |
|---|---|---|
| ApisixTls | `apisix/outline-inouter` | ssl `4e7704e0` |
| ApisixRoute | `outline/outline-inouter` | route `ce4d2d80` (name `outline_outline-inouter_outline-rule`) |
| K8s secret | `apisix/wildcard-inouter-tls` | GTS WR1, *.inouter.com, NotAfter 2026-06-21 |
| Traefik IngressRoute | `outline/outline` | 유지 (롤백 대비) |
## 교훈 (이번 작업에서 새로 확인)
1. **APISIX 3.15 Admin API PUT 으로 SSL 만들 때 `status: 1` 명시 필수** — 생략 시 etcd 저장은 되지만 `status: null` 로 SNI 매칭 되지 않음.
2. **apisix-ingress-controller 2.0.1 은 IngressClass 기준 authoritative sync** — CR 로 정의되지 않은 Admin API 직접 객체는 purge. 지속 가능한 운영은 ApisixRoute/ApisixTls CRD 로만.
3. **CR 에 `ingressClassName: apisix` 필수** — 누락 시 controller 가 CR 소유하지 않아 status.conditions 비어있고 sync 되지 않음. 기존 `wildcard-keepanker-cv`/`juiceshop` CR 과 스키마 정렬.
4. **cert-manager 자동 갱신 경로**: ApisixTls 는 secret 참조 방식이므로 `wildcard-inouter-tls` 갱신 시 controller 가 자동 재 sync. 별도 스크립트/cron 불필요.
## 검증 요약
| 테스트 | 기대 | 결과 |
|---|---|---|
| `GET /` | 200 Outline SPA | 200, 4827 bytes, HTML |
| `GET /api/auth.info` | 404 (비로그인 시) | 404 Outline JSON (`x-apisix-chaitin-waf-action: pass`) |
| `GET /?id=1+OR+1=1` | 403 SafeLine block | 403 JSON `blocked by Chaitin SafeLine WAF` (`action: reject`, time 28ms) |
## 관련 문서
- 상세 리포트: Outline `heimdall` 컬렉션 `2026-04-21 / APISIX outline 라우트 + SSL 설정 / kappa` (id `eb5d683b-47bd-4932-a585-fb232ac05cba`)
- [[../../infra/network/apisix]]
- [[../../infra/network/k3s-ingress-architecture]]

View File

@@ -74,11 +74,11 @@ fingerprint: `SHA256:eBCIglGmK/FnDxJLqxT0CJvRGFEGaIKRWnZ3ZpTaugU`
- Obsidian 정본 전체 쓰기 권한 - Obsidian 정본 전체 쓰기 권한
**Syn 전담 (Heimdall 손대지 말 것)**: **Syn 전담 (Heimdall 손대지 말 것)**:
- BunnyCDN: 풀존, 엣지 스크립트(MiddlewareScriptId 64811 `crowdsec-bouncer-middleware` 포함), Shield/WAF, 캐시 정책 - BunnyCDN: 풀존, Shield/WAF, Rate Limit, MonthlyBandwidthLimit, 캐시 정책 (Edge Script 64811 `crowdsec-bouncer-middleware`는 2026-04-26 폐기)
- SafeLine WAF: K3s `safeline` ns, Traefik middleware, 탐지 규칙, APISIX 통합 - SafeLine WAF: K3s `safeline` ns, Traefik middleware, 탐지 규칙, APISIX 통합
- APISIX: 모든 인스턴스 (K3s 서울 / osaka-gw / sandbox-tokyo / zlambda), 라우트, 플러그인, Admin API - APISIX: 모든 인스턴스 (K3s 서울 / osaka-gw / sandbox-tokyo / zlambda), 라우트, 플러그인, Admin API
- Cloudflare 엣지 관련: Turnstile 위젯, cs-cf-worker-bouncer, cfb-manager - Cloudflare 엣지 관련: Turnstile 위젯, Worker 라우트 (`cs-cf-worker-bouncer` / `cfb-manager`는 2026-04-26 폐기)
- Obsidian 중 엣지 범위 파일 쓰기: `infra/apisix.md`, `infra/crowdsec-safeline.md` 의 엣지 섹션, `services/bunnycdn*.md` - Obsidian 중 엣지 범위 파일 쓰기: `infra/network/apisix.md`, `infra/security/crowdsec-safeline.md` 의 엣지 섹션, `services/bunnycdn*.md`
**공동/협업**: **공동/협업**:
- CrowdSec 시나리오: Heimdall 소유, Syn은 APISIX/SafeLine용 시나리오 파라미터 튜닝 제안만 - CrowdSec 시나리오: Heimdall 소유, Syn은 APISIX/SafeLine용 시나리오 파라미터 튜닝 제안만

15
products/_index.md Normal file
View File

@@ -0,0 +1,15 @@
---
title: products 인덱스
updated: 2026-04-16
tags: [moc, products]
---
## Products
| 문서 | 설명 |
|------|------|
| [[ironclad-corp]] | Ironclad Corp 회사 정보 |
| [[ironclad-hosting-products]] | Ironclad 호스팅 상품 구성 및 가격 정책 |
| [[ironclad-website]] | Ironclad 홈페이지 제작 계획 |
| [[irondesk]] | IronDesk AI 고객지원 에이전트 |
| [[irondesk-tax-legal]] | IronDesk 세무 및 법률 |

View File

@@ -1,62 +1,72 @@
--- ---
title: Ironclad 홈페이지 제작 계획 title: Ironclad 홈페이지
updated: 2026-03-28 updated: 2026-04-21
tags: [product, website, design] tags: [product, website, design]
--- ---
## 개요 ## 개요
ironclad.it.com 공식 홈페이지. 호스팅 상품 소개, 가격표, 회사 소개. ironclad.it.com 공식 홈페이지. 호스팅 상품 소개, 가격표, 회사 소개, 보안서비스 전용 랜딩 포함.
- **정본 코드**: [hosting/ironclad](https://gitea.inouter.com/hosting/ironclad) (Gitea, private)
- **이전 실험 폐기**: `kaffa/anvil-hosting` + `hosting.inouter.com`은 2026-03 중순까지 실험 후 폐기. 최종 브랜드는 **Ironclad**로 통합.
## 기술 스택 ## 기술 스택
| 항목 | 선택 | 이유 | | 항목 | 선택 | 이유 |
|------|------|------| |------|------|------|
| 프레임워크 | **Astro** | 콘텐츠 사이트 최적, Cloudflare 인수로 Workers/R2 궁합 최고, Next.js 대비 2-3배 빠름 | | 프레임워크 | **Next.js 16.2.3** (App Router) | Cloudflare Workers 완전 지원(`@opennextjs/cloudflare`), 동적 라우트 + 정적 생성 혼용, 다국어 `[locale]` 세그먼트 |
| 디자인 | **Google Stitch** | AI 기반 UI 디자인, Tailwind 코드 export, 무료 (Google Labs) | | 디자인 도구 | **Claude Design** + 자체 Design System | 디자인 시스템 사전 설정 → 일관 톤 자동 유지. 영상 리뷰·실사용 검증 완료 (2026-04-21) |
| 스타일 | Tailwind CSS | Stitch export 호환, 유틸리티 기반 | | 스타일 | **Tailwind CSS v4** (`@tailwindcss/postcss`) | Next.js 16 공식 권장, OKLCH 네이티브 |
| 배포 | cf-multisite | Gitea push → R2 → Workers → ironclad.it.com | | 배포 | **opennextjs-cloudflare + Wrangler** | Worker + R2 캐시, `pnpm deploy:{staging,preview,production}` 일원화 |
| 다국어 | 한/영/일 | Astro i18n 지원 | | 다국어 | `[locale]` App Router 세그먼트 (현재 `ko`만 활성) | `SUPPORTED_LOCALES = ['ko']` — 영/일 추가 시 배열만 확장 |
| 패키지 매니저 | pnpm 10 | lockfile 재현성 |
## 디자인 도구: Google Stitch ## 배포 아키텍처
- URL: https://stitch.withgoogle.com/ ```
- Gemini 2.5 기반 AI UI 디자인 개발 → hosting/ironclad repo push (main 브랜치)
- 텍스트 프롬프트 → 고품질 UI 생성 → Gitea Actions (.gitea/workflows/deploy-{staging,preview,production}.yml)
- Vibe Design: 분위기/느낌 기반 디자인 (예: "Stripe처럼 프리미엄하고 미니멀하게") → pnpm install → opennextjs-cloudflare build → wrangler deploy --env=<env>
- 이미지 업로드: 참고 사이트 스크린샷 → 유사 디자인 생성 → Worker ironclad-site-{staging|preview|} + R2 ironclad-cache-{staging|preview|prod}
- Export: HTML/CSS, Tailwind, Vue, Angular, Flutter, SwiftUI (7개 프레임워크) ```
| 환경 | Worker | R2 버킷 | 도메인 | 배포 트리거 | 라우팅 방식 |
|------|--------|---------|--------|------------|------------|
| production | `ironclad-site` | `ironclad-cache-prod` | `ironclad.it.com` · `www.ironclad.it.com` | `v*` 태그 push (`deploy-production.yml`) | **zone route** (`pattern: ironclad.it.com/*, zone_name: ironclad.it.com`) — 기존 A record(APISIX 172.233.93.180) 유지, Worker가 엣지에서 가로챔 |
| staging | `ironclad-site-staging` | `ironclad-cache-staging` | `staging.ironclad.it.com` | main push 자동 (`deploy-staging.yml`) | `custom_domain: true` |
| preview | `ironclad-site-preview` | `ironclad-cache-preview` | `preview.ironclad.it.com` | manual workflow_dispatch (`deploy-preview.yml`) | `custom_domain: true` |
**왜 production만 zone route?** apex 도메인(`ironclad.it.com`)은 이미 APISIX origin을 가리키는 A record가 등록되어 있어 wrangler의 `custom_domain` 등록이 실패(`code: 100117`). `custom_domain` 대신 zone route를 쓰면 기존 DNS 레코드를 건드리지 않고 Cloudflare 엣지에서 Worker가 매칭된 요청만 처리. 롤백 시 DNS 수동 복구 불필요.
**전제 조건**: Cloudflare zone에 등록된 Worker route 중 apex를 가로채는 wildcard가 없어야 한다. [[crowdsec-safeline|crowdsec-cloudflare-worker-bouncer]]의 zone route가 `*ironclad.it.com/*`로 오설정되어 있었고 이걸 `*.ironclad.it.com/*`로 수정해야 apex가 `ironclad-site` Worker로 직행 (2026-04-21 조치).
- 공통 KV: `CROWDSECCFBOUNCERNS` (id `9af0d1c1c14a4bc1a3835c2a5b22fd7a`) — CrowdSec bouncer 미들웨어 상태 공유
- 배포 후 smoke test: `curl -skL -w '%{http_code}' https://<domain>/` 5xx면 실패
## 디자인 시스템 (Claude Design 관리)
Claude Design의 Design System 기능으로 관리. 프로젝트 ID `a17ceb77-c652-44dd-ad39-71f9afd98074` ("Ironclad Design System").
- **팔레트**: 라이트 베이스(#ffffff / #f8fafc) + 민트·에메랄드 그린 액센트 (oklch emerald 계열). 헤더만 다크 네이비 고정.
- **타이포**: Urbanist (display), Nunito Sans (body), JetBrains Mono (코드·수치). 모두 Google Fonts, `next/font/google` 주입.
- **아이콘**: Lucide 라인 아이콘 (민트 둥근 사각형 배경 + 녹색 outline).
- **모션**: 부드러운 페이드 업, 플로팅 배지 `fbA`/`fbB` keyframe, 네비 scroll elevation.
- **전체 인상**: Vercel/Linear/Resend 스타일 라이트 B2B SaaS.
## 페이지 구성 ## 페이지 구성
| 페이지 | 내용 | | 경로 | 내용 | 구현 위치 |
|--------|------| |------|------|-----------|
| 히어로/랜딩 | 핵심 메시지, CTA | | `/ko` | 히어로 + 상품 카테고리 4개 + "Ironclad만의 7가지 이유" + 가격 + 푸터 | `src/app/[locale]/page.tsx` |
| 상품 소개 | VPS, 베어메탈, IKE, 보안서비스 카테고리 | | `/ko/security` | **보안서비스 전용 랜딩** — Hero / 다계층 방어 체계 / DDoS 표 / 6개 장점 카드 / 글로벌 엣지 / 대시보드 프리뷰 / 온보딩 5단계 / 가격 / FAQ / SLA 푸터 | `src/app/[locale]/security/page.tsx` + `src/components/security-v2/*.tsx` (Hero·Defense·Value·Console·Pricing) |
| 가격표 | 리전별/스펙별 가격, 비교표 ([[ironclad-hosting-products]]) | | `/ko/pricing` | 리전별 가격, 비교표 ([[ironclad-hosting-products]]) | `src/app/[locale]/pricing/page.tsx` |
| 보안서비스 | 다계층 방어 체계 (CDN + WAF + 위협 인텔리전스) | | `/ko/cases` | 사례 | `src/app/[locale]/cases/page.tsx` |
| 회사 소개 | Ironclad Corp 정보 ([[ironclad-corp]]) | | `/ko/about` · `/ko/contact` · `/ko/demo` · `/ko/docs/quickstart` · `/ko/legal/*` · `/ko/sla` · `/ko/privacy` · `/ko/terms` | 회사 소개, 문의, 무료 체험, 문서, 법률/약관, SLA | `src/app/[locale]/...` |
| 문의 | 연락처, 상담 신청 |
## 배포 경로 ## 보안서비스 페이지 콘텐츠 규칙
``` 주력 상품 보안서비스 페이지는 공정거래 규제 준수 + 내부 기술 스택 은닉이 엄격하게 적용된다.
Stitch 디자인 → Tailwind 코드 export
→ Astro 프로젝트에 통합
→ hosting/ironclad repo (Gitea)
→ Gitea Actions (Astro build + rclone R2)
→ ironclad.it.com (Workers 서빙)
```
## 참고 디자인
- Stripe (프리미엄, 미니멀)
- Vercel (개발자 친화적)
- Vultr / Linode (호스팅 가격표)
## 개선사항: 보안서비스 페이지 콘텐츠 강화
주력 상품인 보안서비스의 홍보 콘텐츠가 부족하므로 아래 내용으로 보안서비스 페이지를 충실하게 구성할 것.
### 핵심 원칙 ### 핵심 원칙
- 내부 기술 스택명(BunnyCDN, SafeLine, CrowdSec) 노출 금지 - 내부 기술 스택명(BunnyCDN, SafeLine, CrowdSec) 노출 금지
@@ -64,128 +74,68 @@ Stitch 디자인 → Tailwind 코드 export
- 아이언클래드 자체 브랜딩으로 포장 - 아이언클래드 자체 브랜딩으로 포장
- 과대광고/부당비교 금지 — 공정거래위원회 부당광고 규제 준수 - 과대광고/부당비교 금지 — 공정거래위원회 부당광고 규제 준수
### 페이지 구성안 ### 다계층 방어 체계 표현
#### 1. 다계층(Multi-Layer) 방어 체계 (메인 섹션) 위협 인텔리전스 → 엣지 WAF → ML 심층 WAF → 로그 분석이 하나의 **순환 피드백 루프**로 동작하는 구조. 고객에게는 시각적 다이어그램으로 루프를 **방향성 화살표 포함**하여 표시.
위협 인텔리전스 → 엣지 WAF → 심층 WAF → 로그 분석이 하나의 피드백 루프로 동작하는 구조. 고객에게는 시각적 다이어그램으로 순환 구조를 보여줄 것.
| 계층 | 고객 표현 | 내부 기술 | 설명 | | 계층 | 고객 표현 | 내부 기술 | 설명 |
|------|----------|----------|------| |------|----------|----------|------|
| 엣지 차단 | 글로벌 위협 인텔리전스 | CrowdSec bloom filter | 글로벌 커뮤니티 기반 크라우드소싱 악성 IP DB로 엣지에서 즉시 차단, 엣지 차단 목록 3분 주기 갱신, 오탐 시 CAPTCHA 구제 | | 엣지 차단 | 글로벌 위협 인텔리전스 | CrowdSec bloom filter | 글로벌 커뮤니티 기반 악성 IP DB로 엣지에서 즉시 차단, 3분 주기 갱신, 오탐 시 CAPTCHA 구제 |
| 1차 WAF | 엣지 웹 방화벽 (WAF) | BunnyCDN WAF (OWASP CRS) | CDN 엣지에서 SQLi, XSS, RCE, SSRF, Log4j, Shellshock 등 OWASP Top 10 차단 | | 1차 WAF | 엣지 웹 방화벽 (WAF) | BunnyCDN WAF (OWASP CRS) | OWASP Top 10 (SQLi, XSS, RCE, SSRF, Log4j, Shellshock) 차단 |
| 2차 WAF | 머신러닝 기반 이상 탐지 방화벽 | SafeLine WAF | ML 기반 트래픽/요청 본문 심층 분석, 룰 기반 WAF가 놓치는 알려지지 않은 변형 공격에 대한 추가 탐지 계층 | | 2차 WAF | 머신러닝 기반 이상 탐지 방화벽 | SafeLine WAF | ML 기반 트래픽/요청 본문 심층 분석, 룰 기반이 놓치는 변형 공격 탐지 |
| 로그 분석 | 실시간 로그 분석 & 자동 차단 | CrowdSec 시나리오 | HTTP 프로빙/크롤링/백도어 탐지 → 위협 인텔리전스 DB에 자동 피드백 루프 | | 로그 분석 | 실시간 로그 분석 & 자동 차단 | CrowdSec 시나리오 | HTTP 프로빙/크롤링/백도어 탐지 → 위협 인텔리전스 DB에 자동 피드백 |
> 주의: "4중 보안" 숫자 마케팅 금지. 엣지 차단과 로그 분석은 동일 시스템(CrowdSec)의 입출력이므로 독립 계층으로 세지 않는다. "다계층 방어 체계" 또는 "3단계 WAF + 위협 인텔리전스 피드백 루프"로 표현. > **금지 표현**: "4중 보안", "5중 방어" 같은 숫자 마케팅. 엣지 차단과 로그 분석은 동일 시스템(CrowdSec)의 입출력이므로 독립 계층으로 세지 않는다. "다계층 방어 체계" 또는 "3단계 WAF + 위협 인텔리전스 피드백 루프"로 표현.
#### 2. DDoS 방어 — L3~L7 전 계층 보호 (차별화 핵심) ### DDoS 방어 — L3~L7 전 계층
대부분의 호스팅은 L3/L4만 방어. 아이언클래드는 L7까지 방어하는 것이 차별점.
| 계층 | 공격 유형 | 방어 방식 | | 계층 | 공격 유형 | 방어 방식 |
|------|----------|----------| |------|----------|----------|
| L3/L4 | UDP Flood, SYN Flood, 볼류메트릭 | 글로벌 엣지 네트워크 기반 대규모 DDoS 흡수 | | L3/L4 | UDP Flood, SYN Flood, 볼류메트릭 | 글로벌 엣지 네트워크 흡수 |
| L7 | HTTP Flood, Slowloris, 봇 공격 | 행동 분석 + Proof-of-Work 챌린지 | | L7 | HTTP Flood, Slowloris, 봇 공격 | 행동 분석 + Proof-of-Work 챌린지 |
| L7 | SQLi, XSS, RCE | 엣지 WAF + ML 심층 방화벽 이중 차단 | | L7 | SQLi, XSS, RCE | 엣지 WAF + ML 심층 방화벽 이중 차단 |
| L7 | 크롤링, 프로빙, 백도어 시도 | 실시간 로그 분석 → 자동 차단 | | L7 | 크롤링, 프로빙, 백도어 시도 | 실시간 로그 분석 → 자동 차단 |
> 주의: "200+ Tbps"는 CDN 네트워크 전체 총합 용량이지 고객별 할당이 아님. 수치 사용 시 "글로벌 CDN 네트워크 총 200+ Tbps 용량 기반"으로 명시하거나, 수치 없이 "글로벌 엣지 네트워크"로 표현. > "200+ Tbps"는 CDN 네트워크 총합 용량이지 고객별 할당이 아님. 수치 사용 시 "글로벌 CDN 네트워크 총 200+ Tbps 용량 기반" 명시하거나 수치 없이 "글로벌 엣지 네트워크"로.
#### 3. 아이언클래드 보안의 장점 (비교 대체) ### 아이언클래드 보안의 장점 (경쟁사 비교 대체)
> 주의: 경쟁사 비교표 사용 금지. "일반 CDN/WAF"와의 부당비교는 공정거래위원회 규제 대상. Cloudflare Free도 L7 DDoS 방어를 제공하고 주요 WAF 벤더는 자체 위협 인텔리전스를 보유하므로 "없음"이라는 표현은 사실과 다름. > 경쟁사 비교표 금지. "일반 CDN/WAF" 부당비교는 공정거래위원회 규제 대상.
아이언클래드 보안의 장점만 나열하는 방식으로 구성:
- L3~L7 전 계층 DDoS 방어 - L3~L7 전 계층 DDoS 방어
- 엣지 WAF + ML 심층 방화벽 이중 구조 - 엣지 WAF + ML 심층 방화벽 이중 구조
- 글로벌 크라우드소싱 위협 인텔리전스 + 자동 피드백 루프 - 글로벌 크라우드소싱 위협 인텔리전스 + 자동 피드백 루프
- DNS 연결만으로 간편 적용 - DNS 연결만으로 간편 적용
- 보안 올인원 패키지: CDN + WAF + SSL + 위협 인텔리전스 (서버 비용 별도) - 보안 올인원 패키지: CDN + WAF + SSL + 위협 인텔리전스 (서버 비용 별도)
#### 4. 글로벌 CDN ### 가격
- 119개 PoP, 자동 SSL 발급/갱신, 사이트당 월 1TB 포함 - 99 크레딧 / 월 / 사이트 (1 크레딧 = $1, 원화 환산 병기)
#### 5. 보안 대시보드
- 차단된 공격 건수/유형 통계
- 정상 vs 악성 트래픽 비율
- 실시간 알림 설정
- WAF 로그 조회
- 로그 보관 기간 명시 필요
#### 6. 가격
- 99크레딧/월/사이트 (1크레딧 = $1 환산 비율 명시 필요)
- 포함: CDN + WAF + SSL + 위협 인텔리전스 + 보안 대시보드 - 포함: CDN + WAF + SSL + 위협 인텔리전스 + 보안 대시보드
- 서버 비용 별도 - 서버 비용 별도
- 트래픽 초과 정책 명시 필요: 1TB 초과 시 요금/처리 방식, DDoS 트래픽 포함 여부 - 트래픽 초과 티어: 1~5TB $0.05/GB, 5~20TB $0.04/GB, 20TB+ 협의
#### 7. 온보딩 프로세스 ### 온보딩 5단계
사이트에 단계별 도식으로 표시: 서비스 신청 → DNS 변경 (CNAME) → SSL 자동 발급 → WAF 자동 활성화 → 대시보드 접속
1. 서비스 신청
2. DNS 변경 (CNAME 레코드 추가)
3. SSL 인증서 자동 발급
4. WAF 자동 활성화
5. 보안 대시보드 접속
> "원클릭 적용" 표현 금지. DNS 변경은 고객 측 작업 필요. "DNS 연결만으로 간편 적용"으로 표현. > "원클릭 적용" 금지 — DNS 변경은 고객 측 작업. "DNS 연결만으로 간편 적용"으로.
#### 8. SLA 및 면책 조항 (반드시 포함) ### SLA 및 면책 (반드시 포함)
- CDN/WAF 서비스 가용성 99.9%
- 미달성 시 서비스 크레딧 보상 (10/25/50% 티어)
- 오탐 대응: 영업일 기준 1시간 이내 1차 응답, 24시간 이내 해결/우회
- 데이터 처리: 요청 로그 30일 후 자동 삭제, DPA 검토 가능
- 면책: 100% 차단 비보장, 고객 애플리케이션 취약점 제외, DDoS 용량 초과 한계
**SLA (별도 페이지 또는 섹션)** ### FAQ 필수 항목
- CDN/WAF 서비스 가용성 보장 (예: 99.9%) 오탐 대응 / WebSocket 지원 / 1TB 초과 처리 / DDoS 트래픽 포함 여부 / 기존 CDN 마이그레이션 / 커스텀 WAF 룰 / 사고 알림 (최소 6개).
- DDoS 방어 범위 및 한계 명시
- 오탐(False Positive) 대응 시간
- 보안 사고 알림 및 대응 절차
**면책 조항**
- "본 서비스는 모든 공격의 100% 차단을 보장하지 않습니다"
- 고객 측 애플리케이션 취약점에 대한 책임 범위
- DDoS 방어 용량 초과 시의 한계
- 데이터 유출 발생 시 책임 소재
**데이터 처리**
- CDN 경유 트래픽 데이터(요청 로그, IP) 처리 방침
- 개인정보보호법 관련 데이터 처리 위탁 계약(DPA) 검토 필요
- GDPR 대응 (해외 고객 대상 시)
#### 9. 지원 범위 및 제한 사항
- 지원 프로토콜: HTTP/HTTPS, WebSocket 지원 여부
- 파일 업로드/요청 본문 크기 제한
- API 트래픽 지원 여부 (REST, GraphQL 등)
- 특정 CMS/프레임워크 호환성
- 커스텀 WAF 룰 설정 가능 여부
#### 10. 컴플라이언스
- ISMS 인증 보유/준비 여부
- 개인정보보호법 준수
- 일본: ISMAP 등
- 글로벌: SOC 2, ISO 27001 등
- 없는 경우 "보안 수준 설명" 또는 "준비 중" 명시
#### 11. FAQ
- 오탐으로 정상 사용자가 차단되면 어떻게 되나요?
- WebSocket을 사용하는 사이트에도 적용 가능한가요?
- 1TB 트래픽 초과 시 어떻게 되나요?
- DDoS 공격 시 발생하는 트래픽도 1TB에 포함되나요?
- 기존 CDN에서 마이그레이션하려면 어떻게 하나요?
- 커스텀 WAF 룰을 설정할 수 있나요?
- 보안 사고 발생 시 어떻게 알려주나요?
#### 12. 향후 추가 콘텐츠 (블로그/SNS)
- 공격 시나리오별 방어 사례 (DDoS, SQLi, Bot 등)
- 실제 차단 실적 데이터 ("월 평균 X건 공격 차단")
- 보안 대시보드 스크린샷/데모
- 산업별 사용 사례 (이커머스, SaaS, 미디어 등)
- 무료 보안 진단 기능 (전환율 향상용)
### 타깃 고객 ### 타깃 고객
- 1차: 별도 보안 인력이 없는 중소규모 웹사이트 운영자 - 1차: 별도 보안 인력이 없는 중소규모 웹사이트 운영자
- 2차: 기술 지식이 중간 수준인 의사결정자 - 2차: 기술 지식이 중간 수준인 의사결정자
- 향후: 엔터프라이즈 대응 시 전용 IP, 커스텀 룰, 전담 지원 등 상위 플랜 필요 - 향후 엔터프라이즈: 전용 IP, 커스텀 룰, 전담 지원 플랜 필요
- 페이지 구성: 비기술 의사결정자용(이점 중심) + 기술 상세 링크(아키텍처) 분리 권장
## 관련 문서 ## 관련 문서
- [[ironclad-corp]] — 회사 정보, 요금 플랜 - [[ironclad-corp]] — 회사 정보, 요금 플랜
- [[ironclad-hosting-products]] — 전체 상품 구성, 가격표 - [[ironclad-hosting-products]] — 전체 상품 구성, 가격표
- [[cf-multisite]] — 배포 플랫폼 - [[cf-multisite]] — 별도 멀티테넌트 플랫폼 (`*.actions.it.com`). Ironclad 본체와 **무관** — Ironclad는 독자 Worker 사용.

215
projects/netbis-sigmatch.md Normal file
View File

@@ -0,0 +1,215 @@
---
title: Netbis Sigmatch — VL 기반 자동 공격 탐지 + CF 차단
updated: 2026-04-24
tags: [netbis, security, ai-defense, wip]
---
## 개요
Netbis NPM 로그(VictoriaLogs)를 실시간 분석해 **사람 개입 없이 공격을 자동 탐지**하고 CF IP Access Rules에 challenge/block 반영. LAPI·CrowdSec·LLM·사전 정의 룰 모두 사용 안 함.
- **저장소**: https://gitea.inouter.com/kaffa/netbis-sigmatch
- **로컬 개발**: `~/netbis-sigmatch/` (Mac)
- **배포 예정**: jp1 Incus `ai-sigmatch` 컨테이너 (systemd timer 1분)
- **책임자**: kappa 직접 개발 (Heimdall 위임 X, 개발 단계)
## 핵심 설계 (v2.4, 2026-04-25 A4 Matrix Profile 트리거 제거)
### 탐지 트리거 (OR 조건, 하나라도 충족 시 attack mode ON)
**A1. Static threshold** (급격한 볼륨 이상)
```
현재 5분 총 req > baseline_시간대(UTC hour).p95 × 2.0
OR > baseline_시간대.max × 1.5
```
**A2. CUSUM Page-Hinkley** (점진적 ramp-up 공격)
```
g_t = max(0, g_{t-1} + (x_t - μ_hour - δ)) , δ = μ × 0.3
g_t > μ × 2.0 → trigger → reset g=0
```
- μ는 시간대별 롤링 mean. static threshold를 못 넘게 살살 올리는 low-rate DDoS 커버
- 전역 g_t 1개 유지 (hour 경계 reset 문제 회피)
**A3. Global src IP entropy drop** (소수 IP 집중 공격)
```
현재 5분 트래픽의 Shannon(Counter(client_ip)) < baseline_entropy_p10(시간대별)
```
- entropy 계산은 `min_entropy_baseline_samples=20` 확보 후부터
- `uniq_ips < entropy_min_uniq_ips=30` 이면 판정 스킵 (트래픽 자체가 너무 적을 때)
**A4. Matrix Profile discord** ~~(v2.2~v2.3)~~ — **v2.4에서 제거** (사유 아래 참조)
→ attack mode ON: top N contributor IP 일괄 challenge
**B. 개별 극단 (항상 동작)**
```
Scanner-shape: uniq_paths/reqs ≥ 0.8 AND path_entropy ≥ 7 AND reqs ≥ 300
(수백 경로를 각 1회씩 = 취약점 스캐너)
Raw extreme: reqs ≥ 1500 / 5min
(폴링 유저 상한의 약 7배)
```
### 롤링 baseline (자동 갱신)
- 매 사이클 현재 윈도우 샘플을 `baseline_samples`에 누적 (total_reqs + src_ip_entropy + uniq_ips)
- 시간대(hour_utc)별 mean/p95/max/entropy_p10/entropy_p50을 최근 7일 롤링 기준으로 실시간 계산
- **공격 판정된 윈도우 샘플은 baseline에 안 들어감** (baseline 오염 방지)
- 매 60 사이클마다 7일 초과 샘플 자동 prune
- 초기 seed는 `baseline_aggregate.py`로 1회 수집, 롤링 12+ 샘플 쌓이면 seed 대체
## 오탐 방지 특성
| 상황 | 동작 |
|------|------|
| 평상시 정상 트래픽 | 액션 0 (heavy user·폴링 유저 다 통과) |
| 평상시 섞인 취약점 스캐너 | 개별 극단 트리거 (확정 공격) |
| DDoS·대규모 공격 (급격) | static threshold (p95×2.0 / max×1.5) 발동 |
| Low-rate / 점진 ramp-up DDoS | CUSUM Page-Hinkley 누적으로 발동 |
| 소수 IP 집중 공격 (같은 볼륨) | global src IP entropy drop으로 발동 |
| 지속되는 비정형 shape (평소 한 번도 안 본 패턴) | Matrix Profile discord로 발동 |
| 가짜 공격 (baseline 경계선) | samples ≥ 10 확보 후에만 판정 (초기 1~2일은 판정 유예) |
**사람이 쓴 공격 지식 없음**:
- 특정 경로·ASN·UA 리스트 없음
- scanner-shape는 행동 통계 (uniq_paths/reqs 비율, entropy)
- attack mode는 트래픽 볼륨 이상 — 자기 과거와의 편차만 봄
## 조치 레벨
- **challenge** (Cloudflare `managed_challenge`, CAPTCHA, 내부 TTL 30분): 정상 유저는 한 번 풀고 통과
- **block** (CF `block`, 내부 TTL 24시간): challenge 통과 후에도 같은 IP가 5+ 사이클 연속 공격 시
- 내부 TTL이 지나면 loop이 CF rule을 DELETE (CF Access Rules 자체 TTL 없음, sigmatch가 관리)
## CF 연동 (Phase 11, v2.3)
### 엔드포인트
- Account-level: `POST /accounts/{id}/firewall/access_rules/rules` (6개 zone 동시 적용)
- 인증: `X-Auth-Email` + `X-Auth-Key` (global_api_key). API token은 Rulesets/Firewall 403
- Vault: `secret/cloud/cloudflare-netbis` (`account_id`, `email`, `global_api_key`)
### 중복 호출 최소화
상태 변화 있을 때만 CF API 호출:
- `없음 → challenge/block`: `create_rule``rule_id` 저장
- `challenge → block`: `update_rule (PATCH mode)` (같은 rule_id 유지)
- `같은 action 지속`: **skip-same** (CF 호출 없음, sqlite state만 갱신하여 TTL 연장)
- `expire_actions`: 내부 TTL 만료 시 `delete_rule` + state clear
### Rate Limit 대응
- `min_interval_sec=0.3` (sleep), 429 시 exponential backoff
- CF API 계정 제한 1200 req/5min — 지속 공격 IP 10개 × 사이클당 skip-same 로직이라 여유
### Rule 개수 상한 (Phase 11.5)
- CF 공식: account-level IP Access Rules 최대 50,000개/account
- sigmatch 기본 상한: `--cf-max-rules 2000` (보수적)
- startup 시 `list_sigmatch_rules` 호출로 현재 카운트 초기화, create/delete 시 in-memory 갱신
- 한도 도달 시 `CFLimitExceededError``apply_cf`에서 catch → `cf_op=limit-exceeded:N/MAX`, cf_rule_id=None으로 저장. 다음 사이클 expire로 slot 생기면 자동 재시도
### Rule 태깅
notes에 `netbis-sigmatch` prefix. startup 시 `list_sigmatch_rules()`로 우리가 만든 rule만 필터 캐시.
### CLI 유틸 (`cf_client.py`)
```bash
# env: CF_ACCOUNT_ID, CF_EMAIL, CF_GLOBAL_API_KEY
uv run python cf_client.py list # 현재 sigmatch rules 확인
uv run python cf_client.py create --ip X --mode managed_challenge --note "..."
uv run python cf_client.py delete --rule-id XXX
uv run python cf_client.py purge [--yes] # sigmatch prefix 전체 삭제
```
## 개발 단계
- [x] Phase 1: feature 추출 (`fetch_features.py`)
- [x] Phase 2: 24h retrospective baseline (`collect_baseline.py`)
- [x] Phase 3-5: (폐기) IsolationForest+DBSCAN+persistence 기반
- [x] Phase 6 (v2): 집계 기반 공격 모드 + 개별 극단 시그니처
- [x] Phase 7: 롤링 baseline 자동 갱신
- [x] Phase 8 (v2.1): CUSUM Page-Hinkley + global src IP entropy drop
- [x] Phase 9 (v2.2): Matrix Profile discord (stumpy.stump) 추가
- [x] Phase 10: bootstrap_baseline.py — 과거 24h seed로 모든 hour samples_ok 즉시 충족
- [x] Phase 11 (v2.3): CF Access Rules 호출 (managed_challenge/block) — DRY 검증 단계
- [x] **Phase 11.6 (v2.4): A4 Matrix Profile 트리거 제거** ← 현재 (false positive 누적 사고 회피)
- [ ] Phase 11 LIVE 전환: `--live` 플래그로 실 운영
- [ ] Phase 12: jp1 Incus 배포 (systemd timer)
## 파라미터 (사람 조정 가능)
| 파라미터 | 기본값 | 의미 |
|---------|-------|------|
| `attack_p95_multiplier` | 2.0 | 현재 req가 시간대 p95의 몇 배면 attack mode |
| `attack_max_multiplier` | 1.5 | 또는 max의 몇 배면 |
| `attack_top_n` | 20 | attack 시 challenge할 상위 IP 수 |
| `attack_contributor_min_reqs` | 200 | top IP 중 이 이상인 것만 |
| `cusum_drift_pct` | 0.3 | CUSUM 허용 드리프트 δ = μ × pct |
| `cusum_threshold_mult` | 2.0 | CUSUM 트리거 h = μ × mult |
| `min_entropy_baseline_samples` | 20 | entropy baseline 샘플 부족 시 판정 스킵 |
| `entropy_min_uniq_ips` | 30 | uniq IP 부족 시 entropy 판정 스킵 |
| `mp_subseq_len` | 12 | Matrix Profile subsequence length (5분×12=1h) |
| `mp_threshold_pct` | 0.99 | 과거 MP 분포의 이 percentile 초과 시 trigger |
| `scanner_uniq_ratio` | 0.8 | uniq_paths/reqs 임계 |
| `scanner_min_entropy` | 7.0 | path entropy 임계 (per-IP) |
| `scanner_min_reqs` | 300 | 스캐너 최소 요청 수 |
| `extreme_reqs` | 1500 | 단일 IP 극단 rate 임계 |
| `persistence_for_block` | 5 | challenge → block 승급 사이클 |
| `challenge_ttl_sec` | 1800 | 30분 |
| `block_ttl_sec` | 86400 | 24시간 |
| `baseline_rolling_days` | 7 | 롤링 윈도우 |
| `min_baseline_samples` | 10 | baseline 샘플 부족 시 판정 유예 |
## 검증 결과 (simulate.py)
- 평상시 24h 데이터: static threshold 0회 발동, **CUSUM hour별 μ 적용 시 0회 오탐**
- 마지막 5개 윈도우에 1.5x→3.5x 점진 ramp 주입: CUSUM 3회 발동 (idx 198/199/200 연속)
- CUSUM 유닛 테스트: 평상시 g=0 유지, ramp-up 시 g 누적 → h 초과 시 trigger + reset 확인
- **Matrix Profile**: 평상시 201 윈도우에서 p99 초과 ~1%. 단기 3-point spike는 MP 안 튀고 (CUSUM 담당), **1시간 sustained max×3 형상은 MP=3.46 > p99=3.19로 trigger** 확인. 역할 상보적
- 배포 후 cycle 101에서 `mp=1.87/thr=2.88` 안정 관찰 (현재 패턴이 과거와 유사 → trigger X)
## 폐기된 이전 설계
- ~~IsolationForest + DBSCAN per-IP anomaly~~ → 정상 폴링 유저를 outlier로 잡아 오탐 위험
- ~~Persistence 단독 트리거~~ → 페이지 오래 열어둔 유저 6 사이클 지속 시 오탐
- ~~사전 정의 hard rule (R1~R6)~~ → 공격 패턴 종속, 자동 시그니처 생성 취지 어긋남
- ~~Matrix Profile discord (attack 트리거 용도, v2.2~v2.3)~~ → **v2.4 제거**. 사유:
- threshold p99 = 정의상 1% false positive. 매 5분 사이클 평가 → 하루 ~14회 발동
- MP의 self-similar 시계열 가정이 우리 트래픽과 안 맞음. m=12 (1시간) 윈도우는
daily seasonality(낮/저녁/새벽 패턴 변동)보다 짧아 자연스러운 시간대 변동을 discord로 잡음
- 단일 시그널이 attack_mode 트리거가 되어 attack_contributor에서 reqs 순 top 20을
무차별 차단 → 18시간 동안 99 distinct IP 차단 누적 (webhook 4~5개 + KR 활발 유저
+ IPv6 모바일 240/4 가짜 IP). LIVE였으면 베팅 콜백 중단 사고
- matrix_profile.py 모듈은 보존 (관찰/연구용 수동 호출 가능). attack 트리거에서만 제거
## 폐기 후보 (검토 후 불채택)
- **Prophet / LSTM / Bi-LSTM** — 라벨 없음, 학습·추론 무거움, jp1 작은 컨테이너 오버킬
- **CatBoost / Random Forest 지도학습** — 라벨 필요
- **per-IP Isolation Forest / OC-SVM** — 정상 폴링 유저 오탐 (이미 폐기 이유 동일)
향후 데이터 2주+ 쌓이면 검토 예정:
- **Holt-Winters** — level+trend+seasonality. 현재 static p95×mult 대체 가능
- **River HalfSpaceTrees** — streaming Isolation Forest, 다변량 윈도우 feature 학습-예측 동시
- **ADWIN (concept drift)** — baseline 자동 window 조정 (서비스 성장 대응)
## 파일 구조
```
~/netbis-sigmatch/
├── fetch_features.py — feature 추출 (단발 조회)
├── collect_baseline.py — retrospective seed baseline 수집
├── baseline_aggregate.py — 시간대별 seed 통계 수집 (1회성)
├── inspect_baseline.py — baseline DB 탐색
├── state.py — state DB (ip_state + cf_rule_id, baseline_samples, cusum_state 등)
├── matrix_profile.py — stumpy MP discord 판정 (v2.2)
├── cf_client.py — CF Access Rules API 래퍼 + CLI 유틸 (v2.3)
├── bootstrap_baseline.py — 과거 N시간 seed 주입 (cold start 해소)
├── loop.py — 메인 실시간 루프 (--live로 CF 호출)
├── simulate.py — 과거 데이터로 로직 검증
├── baseline.db — seed snapshot 24h
├── state.db — 운영 상태 + 롤링 baseline
└── logs/ — 사이클 로그
```
## 연관 정본
- [[../services/netbis]] — NPM → VL 파이프라인
- [[../infra/security/cloudflare#Pseudo IPv4 (Class E 240/4)]] — 240/4 대역 해석
- [[../history/2026-04-24-cf-pseudo-ipv4-discovery]] — CF Pseudo IPv4 규명

13
reference/_index.md Normal file
View File

@@ -0,0 +1,13 @@
---
title: reference 인덱스
updated: 2026-04-16
tags: [moc, reference]
---
## Reference
외부 문서 레퍼런스 모음.
| 문서 | 설명 |
|------|------|
| [[nixos-manual/_index\|nixos-manual]] | NixOS 공식 매뉴얼 레퍼런스 |

View File

@@ -13,7 +13,7 @@ NixOS 25.11 stable("Xantusia") 매뉴얼의 CommonMark 소스. nixpkgs `nixos-25
- **온라인 렌더링**: https://nixos.org/manual/nixos/stable/ - **온라인 렌더링**: https://nixos.org/manual/nixos/stable/
- **상위**: https://github.com/NixOS/nixpkgs/tree/nixos-25.11/nixos/doc/manual - **상위**: https://github.com/NixOS/nixpkgs/tree/nixos-25.11/nixos/doc/manual
- **가져온 날짜**: 2026-04-08 - **가져온 날짜**: 2026-04-08
- **로컬 위치**: `~/obsidian/dev/nixos-manual/` - **로컬 위치**: `~/obsidian/reference/nixos-manual/`
## 구조 ## 구조
@@ -47,8 +47,8 @@ cd /tmp && rm -rf nixpkgs-manual-fetch
git clone --depth=1 --filter=blob:none --sparse \ git clone --depth=1 --filter=blob:none --sparse \
--branch nixos-26.05 https://github.com/NixOS/nixpkgs.git nixpkgs-manual-fetch --branch nixos-26.05 https://github.com/NixOS/nixpkgs.git nixpkgs-manual-fetch
cd nixpkgs-manual-fetch && git sparse-checkout set nixos/doc/manual cd nixpkgs-manual-fetch && git sparse-checkout set nixos/doc/manual
rm -rf ~/obsidian/dev/nixos-manual/{administration,configuration,development,installation,release-notes,*.md,*.nix,*.json} rm -rf ~/obsidian/reference/nixos-manual/{administration,configuration,development,installation,release-notes,*.md,*.nix,*.json}
cp -R /tmp/nixpkgs-manual-fetch/nixos/doc/manual/. ~/obsidian/dev/nixos-manual/ cp -R /tmp/nixpkgs-manual-fetch/nixos/doc/manual/. ~/obsidian/reference/nixos-manual/
# _index.md(이 파일)는 보존 # _index.md(이 파일)는 보존
``` ```

Some files were not shown because too many files have changed in this diff Show More