obsidian: 정본 문서에서 히스토리/인시던트 분리 완료

15개 정본 문서에서 날짜별 변경이력, 인시던트 기록, 폐기된 구현 상세를
history/ 디렉토리로 분리. 정본은 현재 상태만 기술하는 백서 형태로 정리.
각 정본에 history 위키링크 추가.

분리된 history 파일 12건:
- apisix git push 500, k3s postgresql migration, apisix→traefik 전환
- netbis DDoS 공격, gitea 이전/분리, usb 2.5g hang + NFS hard mount
- supabase→patroni, apisix etcd 통합/분리, anomaly-detect 재설계
- patroni failover incident, zlambda nixos migration, ops-agents setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kappa
2026-04-10 12:09:21 +09:00
parent 72750cfc9d
commit 2356b86d36
27 changed files with 554 additions and 514 deletions

View File

@@ -0,0 +1,36 @@
---
date: 2026-03-15
topic: APISIX git push 500 에러 + http-logger 401 에러 해결
areas:
- infra/apisix.md
- services/gitea.md
tags: [history, incident, apisix, gitea, safeline, crowdsec]
---
## git push 500 에러
### 증상
`gitea.inouter.com`으로 git push 시 BunnyCDN에서 403 반환 (오리진 500)
### 원인 1 — client_body_temp 퍼미션
nginx가 큰 POST body(git pack ~20KB)를 `/usr/local/apisix/client_body_temp/`에 쓸 때 퍼미션 거부. 디렉토리 소유자 `nobody` vs APISIX `apisix` 사용자.
수정: `chown apisix:apisix /usr/local/apisix/client_body_temp`
### 원인 2 — chaitin-waf 글로벌 적용
`chaitin-waf`가 global_rules에 있어서 git pack 바이너리를 SafeLine detector가 파싱 실패 → `mode: block`이면 500 반환.
수정: global_rules에서 `chaitin-waf` 제거, 각 라우트에 개별 적용. gitea 라우트는 git 경로 WAF 제외.
### 교훈
- SafeLine WAF는 바이너리 프로토콜을 처리할 수 없음 → 해당 경로 WAF 제외 필요
- global_rules WAF는 예외 처리가 어려움 → 라우트별 개별 적용이 유연함
## http-logger 401 에러
서울 APISIX http-logger가 CrowdSec으로 로그 전송 시 401. `headers.Authorization``auth_header` 필드로 수정.
## 참조
- `infra/apisix.md` — 현재 APISIX 설정
- `infra/crowdsec-safeline.md` — CrowdSec 로그 연동

View File

@@ -0,0 +1,72 @@
---
date: 2026-03-24
topic: K3s PostgreSQL 백엔드 이전 (외부 etcd → Supabase → Patroni)
areas:
- infra/k3s-migration.md
- infra/infra-hosts.md
- infra/postgresql-ha.md
- infra/metallb.md
- infra/gateway-api.md
tags: [history, k3s, migration, postgresql, supabase, metallb]
---
K3s 클러스터를 기존 외부 etcd 백엔드에서 Supabase PostgreSQL(kine)로 전환한 프로젝트. 2026-03-24~26 수행.
## 배경
기존 K3s 클러스터는 hp2(server), kr1(server), kr2(분리됨) 3노드에 외부 etcd(Incus 컨테이너 3개, `http://192.168.9.214:2379`, `http://192.168.9.135:2379`, `http://192.168.9.134:2379`)를 데이터스토어로 사용. 새 클러스터로 전환.
## 변경 사항
### Phase 0: 인프라 구성 (2026-03-24~25)
- kr2 첫 server 구성, Supabase PostgreSQL 연결
- cert-manager + 와일드카드 인증서 6개 + Reflector
- Traefik DaemonSet (hostPort 80/443) + Gateway API
- APISIX replica 1 (SafeLine WAF 테스트 라우트 1건, Ingress Controller 제거)
- HAProxy 80/443 → Traefik hostPort 복원
- Longhorn v1.8.2
### Phase 1~2: kr1 합류 + 서비스 이전 (2026-03-24~25)
- kr1 server로 합류 → 2-server HA 확보
- 전 서비스 이전 완료 (NocoDB, Gitea, n8n, ArgoCD, Grafana, SearXNG, SafeLine 등)
### Phase 3: hp2 합류 (2026-03-25)
- hp2 **agent**로 합류 (server 3대 → server 2 + agent 1 구성)
- Supabase 커넥션 절약 (agent는 DB 커넥션 불필요)
### Vault jp1 이전 (2026-03-24)
- jp1 Incus default 프로젝트, `vault` 컨테이너 (Debian 13)
- Vault v1.21.4 (raft 스토리지, 스냅샷 복원)
- vault-mcp-server v0.2.0 (Go 바이너리, systemd, port 8080)
- K3s vault 네임스페이스 → ExternalName 서비스로 전환
### Phase 4: 정리 (2026-03-25)
- 기존 etcd Incus 컨테이너 폐기 완료
- 기존 K3s server.bak 삭제 완료
### Phase 5: MetalLB 도입 + 네임스페이스 정리 (2026-03-26)
- MetalLB L2 도입 (192.168.9.50-59), K3s ServiceLB 비활성화
- NodePort 전면 제거 → LoadBalancer 전환:
- traefik: hostPort 80/443 → LoadBalancer 192.168.9.53
- apisix-gateway: NodePort 30233/31137 → LoadBalancer 192.168.9.50
- sshpiper: NodePort 31840 → LoadBalancer 192.168.9.51
- teleport-cluster: ClusterIP → LoadBalancer 192.168.9.52
- argocd-server: NodePort 30080/30443 → ClusterIP (Traefik Ingress)
- ironclad/anvil NodePort → 삭제 (오사카에서 서빙)
- HAProxy 백엔드: 3노드 roundrobin → MetalLB IP 단일 엔드포인트
- k3s.inouter.com DNS: 3노드 A 레코드 → 192.168.9.53 단일
- sshpiper 설치, Teleport 설치, api/mcp 네임스페이스 신설
- ironclad/anvil 네임스페이스 삭제
## 영향
새 클러스터 end state:
- 컨트롤 플레인: kr1 + kr2, 워커: hp2
- 데이터스토어: Supabase PostgreSQL(kine), Session mode pooler 5432
- 이후 2026-04-05에 Supabase → 로컬 Patroni PostgreSQL HA로 재이전
## 참조
- `infra/infra-hosts.md` — 현재 서버/클러스터 구성
- `infra/postgresql-ha.md` — Patroni HA 구성 (Supabase 후속)
- `infra/metallb.md` — MetalLB 설정

View File

@@ -0,0 +1,36 @@
---
date: 2026-03-25
topic: 메인 HTTP 라우팅 APISIX → Traefik 전환
areas:
- infra/gateway-api.md
- infra/apisix.md
tags: [history, traefik, apisix, gateway-api, routing]
---
메인 HTTP 라우팅을 APISIX에서 Traefik으로 전환한 과정.
## 배경
### 전환 이력
- 2026-03-21: K3s Ingress → Gateway API 전환 (기존 클러스터, Traefik v3.6.9)
- 2026-03-24: 새 클러스터(kr2)에서 APISIX + Ingress Controller 2.0으로 구성
- 2026-03-25: 메인 HTTP 라우팅을 Traefik으로 교체
### APISIX 이전 사유
- Ingress Controller 2.0 초기 시도에서 GatewayProxy 모드 + ApisixRoute CRD 연동 실패 (helm values v1.x 형식 불일치)
- Gateway API HTTPRoute에 플러그인 개별 적용 방법 없음 → ApisixRoute CRD 필요
- global_rules를 Ingress Controller가 덮어쓰려는 충돌 발생
## 변경 사항
- Traefik DaemonSet + MetalLB LoadBalancer 192.168.9.53으로 메인 라우팅 담당
- APISIX는 독립 LoadBalancer(192.168.9.50)로 유지, route 축소 (juiceshop 1건)
### 2026-03-22 장애
CoreDNS NodeHosts에 `gitea.inouter.com → 10.43.205.207`(stale ClusterIP)이 남아 ArgoCD 전체 Gitea 싱크 실패. CoreDNS rewrite 방식으로 교체하여 해결.
## 참조
- `infra/gateway-api.md` — Traefik Gateway API 현재 구성
- `infra/apisix.md` — APISIX 구성

View File

@@ -0,0 +1,29 @@
---
date: 2026-03-31
topic: Netbis 도메인 대규모 봇 공격 및 대응
areas:
- services/netbis.md
tags: [history, incident, ddos, netbis, cloudflare]
---
## 인시던트
2026-03-31 ~ 04-01 일본 IP에서 집중된 L7 DDoS 공격.
| 도메인 | 3/31 요청 | 4/1 요청 |
|--------|----------|---------|
| rss-555.com | 3050만 (threats 1700만) | 3000만 (threats 2300만) |
| fall-vip.com | 2560만 (threats 1160만) | 1540만 (threats 1000만) |
| fall-mvp.com | 정상 | 738만 (threats 340만) |
정상 트래픽 일 130~180만 대비 30배 이상 폭주. JP 99%.
## 대응
- Rate Limiting 600 → 120/분으로 강화 (2026-04-05)
- Super Bot Fight Mode 설정 (2026-04-03, Pro zone 4개)
- NPM-4 추가 커널/Nginx 튜닝 (2026-04-05)
## 참조
- `services/netbis.md` — 현재 Netbis 보안 설정

View File

@@ -0,0 +1,29 @@
---
date: 2026-03
topic: Gitea 관련 이전/분리/도메인 변경 (3월)
areas:
- services/gitea.md
tags: [history, gitea, migration, bunnycdn]
---
2026년 3월 중 Gitea 관련 주요 변경 사항 모음.
## Synology → K3s 이전 (2026-03-15)
Synology NAS(192.168.9.100, SQLite)에서 K3s(PostgreSQL)로 이전 완료. Synology 패키지 중지됨 (데이터 보존 중).
## BunnyCDN Pull Zone 분리 (2026-03-27)
Gitea를 iron-kr에서 **iron-git** (ID 5584382)으로 분리. iron-kr의 `BlockNoneReferrer: true`가 git 클라이언트(Referrer 없음)를 차단하여 git push/pull 403 에러 발생. iron-git은 `BlockNoneReferrer: false`로 설정.
## 도메인 이전 (2026-03-28)
기존 `gitea.anvil.it.com``gitea.inouter.com` 완전 교체. 변경 범위:
- DNS, BunnyCDN Free SSL, K8s HTTPRoute, Helm values, CoreDNS 헤어핀 rewrite
- ArgoCD 4개 앱 repoURL + 8개 repo secret
- Gitea Runner, Mailgun credential, 4개 repo CI workflow
- 컨테이너 이미지 경로 5개 Deployment, 로컬 git remote 6개, Obsidian 문서 7개 등
## 참조
- `services/gitea.md` — 현재 Gitea 구성

View File

@@ -0,0 +1,32 @@
---
date: 2026-04-04
topic: USB 2.5GbE 어댑터 절전 hang + NFS hard mount D-state 장애
areas:
- infra/infra-hosts.md
- infra/nas-storage.md
tags: [history, incident, network, nfs, usb]
---
## 배경
USB 2.5GbE 어댑터(r8152/cdc_ncm)가 Linux USB autosuspend에 의해 절전 모드 진입 후 드라이버 hang 발생.
## 인시던트
kr2에서 NFS hard mount가 죽은 2.5G IP로 D-state 누적되어 로드 2000+ 장애. 서버 먹통.
## 복구
- USB unbind/bind로 즉시 복구
- kr2: GRUB `usbcore.autosuspend=-1`, udev rule `99-usb-ethernet.rules` (scatter-gather off)
- NAS: `/usr/local/etc/rc.d/usb-no-suspend.sh` 스타트업 스크립트
## 교훈
- NFS hard mount는 NAS 끊기면 무한 대기 → 서버 먹통
- 모든 NFS 마운트는 `soft,timeo=50,retrans=3` 필수
## 참조
- `infra/infra-hosts.md` — 2.5G LAN 구성
- `infra/nas-storage.md` — NFS 마운트 옵션

View File

@@ -0,0 +1,32 @@
---
date: 2026-04-05
topic: K3s 데이터스토어 Supabase → 로컬 Patroni PostgreSQL HA 이전
areas:
- infra/postgresql-ha.md
- infra/infra-hosts.md
tags: [history, k3s, postgresql, patroni, supabase, migration]
---
K3s kine 데이터스토어를 Supabase Free tier에서 로컬 Patroni PostgreSQL 3노드 HA 클러스터로 이전.
## 배경
Supabase Free tier max_connections=60 제한, 싱가포르 리전 ~70ms 레이턴시. 로컬 PostgreSQL HA로 전환하여 성능/안정성 개선.
## 변경 사항
- Patroni 3노드 구성: postgres-1(hp2), postgres-2(kr1), postgres-3(kr2)
- etcd DCS 클러스터: etcd-nas(NAS Docker), etcd-hp2(hp2 Incus), etcd-jp1(jp1 Incus)
- OpenWrt HAProxy에 PostgreSQL frontend/backend 추가 (Patroni REST API `/primary`로 Leader 자동 감지)
- K3s config: `datastore-endpoint: "postgres://kine:kine@192.168.9.1:5432/kine"`
- hp2가 control-plane에서 worker(k3s-agent)로 전환
## 영향 / 검증
- K3s API 정상 동작 확인
- failover 시 HAProxy가 ~3초 내 새 Leader 감지
## 참조
- `infra/postgresql-ha.md` — 현재 Patroni HA 구성
- `history/2026-03-24-k3s-postgresql-migration.md` — 이전 단계 (외부 etcd → Supabase)

View File

@@ -0,0 +1,33 @@
---
date: 2026-04-06
topic: APISIX etcd 통합/분리 과정 (K3s 내부 → 외부 통합 → K3s 내부 복귀)
areas:
- infra/apisix.md
- infra/postgresql-ha.md
tags: [history, apisix, etcd, k3s]
---
서울 K3s APISIX의 etcd 백엔드를 K3s 내부 StatefulSet에서 외부 통합 etcd로 이전했다가, 다시 K3s 내부로 복귀한 과정.
## 변경 사항
### 2026-04-06: K3s 내부 → 외부 통합 etcd
- K3s 내부 apisix-etcd StatefulSet 삭제
- 외부 통합 etcd(192.168.9.100, 10.100.2.214, 10.253.101.233)로 이전, prefix `/apisix/seoul`
- 의도: 통합 운영 + 컴포넌트 수 절감
### 2026-04-08: 외부 통합 → K3s 내부 복귀
- Patroni DCS와 같은 etcd 클러스터 공유 시 장애 전파 위험(Patroni 이슈 → APISIX 라우팅 영향)
- K3s 내부 `apisix-etcd` StatefulSet 3 replicas 복구 (Bitnami etcd, Longhorn PVC 5Gi x 3)
- 외부 통합 etcd의 `/apisix/seoul/*` 20개 키 삭제
- ApisixRoute CRD 사용을 위해 ingress controller도 복구
## 교훈
- Patroni DCS와 APISIX etcd는 장애 격리를 위해 분리하는 것이 안전
- helm 한 곳에서 etcd + apisix + ingress controller 관리가 운영 일관성 확보
## 참조
- `infra/apisix.md` — 현재 APISIX 구성
- `infra/postgresql-ha.md` — etcd 사용 현황

View File

@@ -0,0 +1,63 @@
---
date: 2026-04-08
topic: anomaly-detect 3차 재설계 과정 (gemma → cohort → agentic Grok-4)
areas:
- infra/anomaly-detect.md
- infra/crowdsec-safeline.md
tags: [history, anomaly-detect, crowdsec, ai, grok]
---
anomaly-detect 시스템의 3번의 설계 반복과 최종 agentic 구조 확정 과정.
## 1차 구현 (2026-04-08 초반) — gemma4:e4b + stats 파이프라인
per-IP 통계 게이트(count/4xx/5xx/499/distinct paths) → 후보 N개 → ollama gemma4:e4b(Q4_K_M, 8.0B) yes/no 분류. Python이 축을 정의하고 LLM은 판정만 수행하는 구조.
## 2차 구현 (같은 날) — cohort 탐지 추가
`_cohort_path_candidates`, `_cohort_ua_candidates` 등 집단 축 탐지 로직 추가. 여전히 "Python이 탐지, LLM이 분류"라는 본질적 한계 동일.
## 코드 리뷰 수정 2회
### 초기 리뷰 (커밋 d5310f0)
- LAPI POST 실패 시 dedup 선기록 버그 수정
- XFF CSV 파싱 버그 수정 (`extract_client_ip()` 헬퍼)
- 사설망/Tailscale 필터 개선 (`ipaddress.ip_address().is_private`)
- dedup.json 원자적 쓰기 (`tempfile` + `os.replace`)
### 2차 리뷰 (커밋 b0e3c68) — High 5건 / Medium 4건
- H1: `events_count` 프로토콜 오용 → sample 10건을 events에 풀어서 넣기
- H2: `limit=20000` raw 로그 pull → `| stats by (remote_addr)` 서버측 집계
- H4: prompt injection via path → `json.dumps` + 경고 삽입
- H5: `num_predict=80` 한국어 truncation → 256
- M1~M4, M7: start_at/stop_at, 파일 핸들 누수, ratio에 499 포함, set cap, housekeeping
### 남은 설계 이슈
- H3: 분산 봇넷 대비 게이트 사각지대 (per-IP 게이트의 한계)
## 3차 재설계 — agentic 구조 (최종)
사용자 원래 의도 "시계열 DB를 AI에 연결하면 AI가 알아서 찾는다"에 부합하도록 전면 재설계. OpenRouter `x-ai/grok-4-fast`에 tool 2개(logsql_query, ban_ips)만 노출하는 agentic 구조.
### 모델 벤치마크 결과
| 모델 | 턴 | 비용 | 결과 |
|------|-----|------|------|
| x-ai/grok-4-fast | 4 | $0.0036 | 정답 |
| qwen/qwen3-235b-a22b-2507 | 7 | $0.0012 | 정답 |
| google/gemini-2.0-flash-001 | 10(max) | $0.0026 | 결론 없음 |
| deepseek/deepseek-chat-v3.1 | 10(max) | $0.0127 | 결론 없음 |
### E2E 검증
- VictoriaLogs에 270 rows 주입, Grok-4-fast 5턴에 31개 공격 IP 정확 식별, 정상 IP 0건 ban
- DRY_RUN=0 활성화, 실운영 개시
## 전임자 폐기
- `ddos-detect` (Go, jp1 crowdsec 컨테이너, 60s 폴링, Claude CLI sonnet 호출) — 60s 폴링 + 동기 Claude CLI 구조 한계로 폐기
- 제거 항목: `ddos-detect.service`, Go 바이너리+소스, `ddos-detect.sh`, `extract_behavior.py`, `ddos-logs/`
## 참조
- `infra/anomaly-detect.md` — 현재 agentic 구조
- Gitea: `kaffa/anomaly-detect`, `kaffa/ddos-detect`(보존)

View File

@@ -0,0 +1,39 @@
---
date: 2026-04-08
topic: Patroni failover 사고 — pgcat/nocodb/outline read-only 에러
areas:
- infra/postgresql-ha.md
- infra/outline.md
tags: [history, incident, patroni, postgresql, pgcat, nocodb, outline]
---
Patroni failover 발생 후 pgcat, NocoDB, Outline이 구 primary IP를 hardcoded 참조하여 read-only 에러 발생.
## 배경
pgcat의 각 pool `shards.0.servers`에 Patroni 노드 IP(10.100.2.5 등)가 직접 박혀 있었음. Outline의 `DATABASE_URL` Secret도 노드 IP 직결.
## 인시던트
Patroni failover 발생 → postgres-1(10.100.2.5)이 replica로 강등 → pgcat와 Outline이 옛 primary를 계속 가리킴:
- NocoDB: 마이그레이션 시 `cannot execute UPDATE in a read-only transaction` → CrashLoopBackOff ~4시간
- n8n: 마이그레이션 없어 표면화되지 않았으나 동일 잠재 문제
- Outline: API 500 반환 (`apiKeys.lastActiveAt` UPDATE 실패 → 인증 자체 깨짐)
## 복구
### pgcat
`db/pgcat-config` ConfigMap의 모든 pool servers를 HAProxy 단일 백엔드(`192.168.9.1:5432`)로 변경.
### Outline
`outline-secrets` Secret의 `DATABASE_URL``postgresql://outline:outline@192.168.9.1:5432/outline`로 변경 + rollout restart.
## 교훈
- Patroni 사용 애플리케이션의 DB endpoint는 항상 OpenWrt HAProxy(`192.168.9.1:5432`) 또는 pgcat 경유. 노드 IP 직접 박지 말 것.
- 변경 시 `kubectl get secret -A -o json | jq` 검수로 나머지 Patroni 사용자도 일괄 확인
## 참조
- `infra/postgresql-ha.md` — pgcat 단일 백엔드 구조
- `infra/outline.md` — Outline 구성

View File

@@ -0,0 +1,42 @@
---
date: 2026-04-08
topic: zlambda (구 sandbox-tokyo) Debian → NixOS 전환
areas:
- infra/zlambda.md
- services/netbis.md
tags: [history, zlambda, nixos, linode, migration]
---
Linode Tokyo VM `sandbox-tokyo`를 Debian 12에서 NixOS 25.05로 교체하고 호스트명을 `zlambda`로 통일.
## 배경
누군가 nixos-anywhere를 시도하다가 14시간째 nixos-installer에 멈춰 있었음. 이전 Debian 디스크는 wipe되어 원본 데이터 모두 손실.
## 설치 과정
1. 첫 시도 실패: sda(512MB)/sdb(50GB) 순서 뒤바뀜 + 1.9GB RAM에 swap 없이 nixos-install → OOM-lock
2. 회복: Linode `POST /linode/instances/{id}/rebuild`로 Debian 12 클린 설치 → 디스크 순서 정상화
3. nixos-anywhere 실행: disko + grub `mirroredBoots` 중복 오류 → `boot.loader.grub.devices`를 빼고 disko 자동 설정 사용
4. 부팅 안 됨: Linode kernel `linode/grub2`가 NixOS grub.cfg 인식 못함 → LISH 콘솔에서 확인
5. 해결: Configuration profile kernel을 `linode/direct-disk`로 변경 → 정상 부팅
6. Tailscale: 옛 device(100.79.87.48) 삭제, 새 device 가입, 이름 회수
## 후속 변경 (같은 날)
- Gitea 리포지토리 `kaffa/nixos-infra` (private) 생성, deploy key 등록
- 호스트명 `sandbox-tokyo``zlambda` 통일 (NixOS, kernel, Tailscale)
- macbookair ed25519 키 영구 등록
- 커널/sysctl 튜닝 (BBR, conntrack, inotify 등)
- APISIX + etcd를 NixOS oci-containers로 재선언하여 기동
## 제거된 서비스
sandbox-tokyo에서 기존 운영하던 서비스들이 NixOS 전환으로 제거:
- vault-prod, wg-easy, nginx-tcp-proxy, microsocks(socks5-v4), tlsproxy, Caddy
- APISIX/etcd는 NixOS oci-containers로 재가동
## 참조
- `infra/zlambda.md` — 현재 구성
- `services/netbis.md` — Netbis DR APISIX

View File

@@ -0,0 +1,32 @@
---
date: 2026-04-09
topic: Ops Agents (Heimdall/Syn) 구축 및 heimdall tofu 재생성
areas:
- ops-agents/overview.md
- infra/infra-hosts.md
tags: [history, ops-agents, heimdall, syn, tofu]
---
내부 운영 에이전트 시스템 구축 과정.
## Heimdall 재생성 (2026-04-09)
- 이전: 2026-03 수동 생성 (root 유저, kr1 default project, IP 10.100.3.92)
- 이후: 2026-04-09 tofu 재생성 (kaffa 유저, kr1 ops project, IP 10.100.3.108)
- OpenTofu 관리: `kaffa/ops-agents-tofu/heimdall` 모듈
- 메모리/CPU limit: 8GiB/4core
- 재생성 시 `~/.claude` 전체 백업/복원
## Syn 신규 생성 (2026-04-09)
- hp2 ops 프로젝트, IP 10.100.2.173
- OpenTofu 관리: `kaffa/ops-agents-tofu/syn` 모듈
- 엣지 레이어 전담 (BunnyCDN, SafeLine, APISIX, Cloudflare)
## Vault 접근 게이트웨이 프로토콜 도입 (2026-04-09)
에이전트가 Vault에 직접 접근할 수 없도록 kappa 게이트웨이 ASK 프로토콜 도입. `.mcp.json`에서 vault 항목 완전 제거.
## 참조
- `ops-agents/overview.md` — 현재 에이전트 구성

View File

@@ -9,31 +9,11 @@ tags: [security, crowdsec, victorialogs, ollama, gemma, anomaly]
# anomaly-detect
## 3차 재설계 (2026-04-08, agentic)
## 아키텍처
기존 per-IP 게이트 + cohort 탐지 + gemma4:e4b classifier 구조를 **전면 폐기**하고, OpenRouter `x-ai/grok-4-fast`에 tool 2개만 노출하는 agentic 구조로 전환.
OpenRouter `x-ai/grok-4-fast`에 tool 2개(logsql_query, ban_ips)만 노출하는 agentic 구조. fallback 모델 `qwen/qwen3-235b-a22b-2507`.
### 전환 이유
사용자 원래 의도는 "시계열 DB를 AI에 연결해주면 AI가 알아서 공격을 찾는다"였는데, 1~2차 구현은 "Python이 축(path/UA/IP)을 미리 정의하고 LLM은 yes/no만" 파이프라인이라 원래 의도와 어긋났다. 새 공격 벡터가 등장할 때마다 코드에 축을 추가해야 하는 한계가 본질적이었다.
### OpenRouter 모델 벤치마크 (2026-04-08)
24시간 실트래픽에 대해 4개 모델을 tool-use agent로 돌린 결과:
| 모델 | 턴 | 지연 | in/out 토큰 | 비용 | 결과 |
|------|-----|------|-------------|------|------|
| x-ai/grok-4-fast | **4** | 17.5s | 12303/2166 | $0.0036 | ✅ 정답 (`211.211.28.97`) |
| qwen/qwen3-235b-a22b-2507 | 7 | 20.5s | 16623/522 | **$0.0012** | ✅ 정답 |
| google/gemini-2.0-flash-001 | 10(max) | 16.8s | 22981/886 | $0.0026 | ❌ 결론 없음 |
| deepseek/deepseek-chat-v3.1 | 10(max) | 95.7s | 78180/1316 | $0.0127 | ❌ 결론 없음 |
- **Grok-4-fast**: agentic 품질 최상 — 에러 path → 에러 IP → pivot → UA/admin 교차 확인 → 결론의 정석 흐름. LogsQL 문법 에러 0회.
- **Qwen3-235b-2507**: 정답 도달했지만 Grok보다 턴 수 많음. 비용은 가장 쌈.
- **Gemini 2.0 Flash**: `http_user_agent=~` (Prometheus 문법) 같은 잘못된 LogsQL을 반복, 24h 지시도 무시, 사설망 제외도 약함.
- **DeepSeek v3.1**: 추론은 좋지만 `sort by`, `path:~"..."` 같은 LogsQL 구문에 실패를 반복, MAX_TURNS 소진.
선택: **`x-ai/grok-4-fast`** 주 모델, **`qwen/qwen3-235b-a22b-2507`** fallback.
설계 반복 및 모델 벤치마크 이력: [[../history/2026-04-08-anomaly-detect-iterations|history]]
### 새 아키텍처
@@ -78,31 +58,6 @@ LLM 프롬프트가 무시되어도 실수로 사설망이 ban되지 않음.
- OpenRouter key: `secret/ai/openrouter` (`API_KEY` 키)
- 컨테이너 배포본: `/etc/anomaly-detect/openrouter.env` (mode 600, systemd `EnvironmentFile=`)
### 폐기된 구조
- 1차 구현 (2026-04-08 초반): gemma4:e4b + stats 파이프라인
- 2차 구현 (같은 날): cohort 탐지(`_cohort_path_candidates`, `_cohort_ua_candidates`) 추가
- 두 구현 모두 로직 자체가 "AI가 아닌 Python이 탐지"였다는 점에서 본질적 한계. 전면 폐기.
### 코드 커밋 해시
- `a702870` — agentic rewrite (OpenRouter + Grok-4-fast) 초기 구현
- `af2873d``simulate.py` mock 기반 smoke test (5/5 시나리오 PASS)
- `d7789ad``DRY_RUN=0` 활성화 (E2E 검증 후)
- `23c67bd` — 스케일 업 (`MAX_BAN_PER_CYCLE` 100→2000, LAPI chunk POST, exec_logsql 200KB, 2000 IP 시나리오 추가 — 6/6 PASS)
- `48eb489` — 사이클당 token/cost 누적 로깅 (`cycle usage: ... cost=$X`)
### E2E 검증 (2026-04-08)
`simulate.py` 로컬 mock 테스트 5개 시나리오 전원 PASS 후, 실 VictoriaLogs `/insert/jsonline`에 270 rows 주입해 end-to-end 검증:
- **시나리오**: sqlmap single IP (`91.92.93.100`, 60건) + distributed brute force (`91.92.94.10~39`, 150건) + 정상 노이즈 (`185.100.200.1~20`, 60건)
- **결과**: Grok-4-fast가 5턴에 **31개 공격 IP 정확 식별**, 정상 IP 0건 ban
- **지연**: ~18초 / **토큰**: 3939 prompt / 223 completion / **비용**: ~$0.001
- **LAPI**: POST 201 Created, `cscli decisions list -s anomaly-detect/distributed-wp-bruteforce` 에서 31 decisions 확인, scenario 단위 일괄 삭제 cleanup 정상
- **DRY_RUN=1 안전장치**: 첫 수동 실행은 DRY_RUN=1로 돌려 `"would ban 31 IPs"` 로그만 확인, 실제 LAPI 호출 없음
검증 완료 후 systemd unit에 `Environment=DRY_RUN=0` 추가, daemon-reload, 다음 5분 timer 사이클부터 실운영 개시.
### 운영 중 주의사항
@@ -178,48 +133,6 @@ 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`로 복구.
## ~~통계 게이트 (환경변수로 조정)~~ (폐기, 3차 재설계)
| 변수 | default | 의미 |
|------|---------|------|
| `WINDOW_MIN` | 5 | LogsQL 윈도우 (분) |
| `GATE_MIN_REQS` | 30 | 윈도우 내 최소 요청 수 |
| `GATE_MIN_4XX_RATIO` | 0.5 | 4xx 비율 (count >= MIN_REQS와 AND) |
| `GATE_MIN_5XX_COUNT` | 10 | 5xx 최소 발생 |
| `GATE_MIN_499_COUNT` | 15 | 499 최소 발생 |
| `GATE_MIN_DISTINCT_PATH` | 20 | 서로 다른 path 최소 |
| `MAX_CANDIDATES` | 5 | 한 사이클에 ollama로 분석할 IP 상한 |
| `BAN_DURATION` | 4h | profiles.yaml이 아닌 alert 자체에서 전달하는 ban 기간 |
| `DEDUP_HOURS` | 24 | 같은 IP 재분석 방지 윈도우 |
게이트 통과 조건 (OR):
1. count ≥ MIN_REQS AND 4xx 비율 ≥ MIN_4XX_RATIO
2. 5xx ≥ MIN_5XX_COUNT
3. 499 ≥ MIN_499_COUNT
4. distinct path ≥ MIN_DISTINCT_PATH
사설망 IP(10/8, 127/8, 192.168/16, 172.16/12)는 자동 제외.
## ~~ollama prompt~~ (폐기, 3차 재설계)
`format=json`으로 강제 + 명확한 schema:
```json
{"verdict": "yes" | "no", "reason": "<한 문장 한국어>"}
```
판단 근거는 system prompt에 명시 (반복 패턴, 머신 속도, 4xx/5xx 비율, path 열거, 알려진 스캐너 UA, 로그인 brute force, 비정상 rate). 정상 브라우저 패턴은 "no"로 분류.
## ~~CrowdSec 자동 ban~~ (폐기, 3차 재설계 — `ban_ips` tool로 대체)
profile 기반:
```
alert (machine=anomaly-detect, source.scope=Ip, remediation=true)
→ /etc/crowdsec/profiles.yaml의 default_ip_remediation에 매치
→ 자동으로 ban decision 4h 생성
→ bouncers (BunnyCDN, APISIX 서울/오사카, netbis-cf 등)가 다음 pull에 적용
```
## 운영 명령
@@ -248,48 +161,11 @@ incus exec anomaly-detect -- sh -c 'echo "{}" > /var/lib/anomaly-detect/dedup.js
- `curl http://10.253.100.240:8080/v1/decisions` → 403 (인증 필요, 네트워크 OK)
- 더미 IP `198.51.100.99`로 alert POST → 201 + decision 등록 → cleanup 확인 (smoke test)
> 아래는 1~2차 구현의 리뷰 이력이다. 3차 재설계에서 해당 코드는 전면 폐기됐다.
## 초기 리뷰 수정 (2026-04-08)
코드 리뷰 결과 다음 버그/개선을 반영:
1. **LAPI POST 실패 시 dedup 선기록 버그**: `dedup[ip] = now_ts`가 LAPI alert POST 이전에 설정되어, LAPI가 일시적으로 죽으면 해당 공격 IP가 24h 동안 재ban되지 않던 문제 수정. 성공/`no` 판정일 때만 dedup에 기록하고, LAPI 예외는 다음 사이클 재시도.
2. **XFF CSV 파싱**: `xff="1.2.3.4, 5.6.7.8"` 같은 CSV를 그대로 IP로 쓰던 버그 수정. `extract_client_ip()` 헬퍼로 첫 번째 IP만 사용.
3. **사설망/Tailscale 필터 개선**: `ip.startswith(("10.", "172.16.", ...))` 문자열 매칭 → `ipaddress.ip_address().is_private` + Tailscale CGNAT `100.64.0.0/10` 제외.
4. **dedup.json 원자적 쓰기**: `tempfile` + `os.replace`로 크래시 시 파일 절단 방지.
수정본은 `gitea.inouter.com/kaffa/anomaly-detect` main 브랜치에 커밋됨 (`d5310f0`).
## 2차 리뷰 수정 (2026-04-08 커밋 b0e3c68)
직접 코드 리뷰로 High 5건 / Medium 4건 추가 발견, 모두 반영:
- **H1 `events_count` 프로토콜 오용**: `events` 배열은 1건인데 count에 전체 요청 수를 넣어 대시보드 집계 왜곡. → sample 10건을 `events`에 풀어서 넣고 `events_count = len(events)`로 일치. 첫 event에 `reason` meta 포함.
- **H2 LogsQL 서버측 집계**: `limit=20000` raw 로그 pull은 DDoS에서 잘림 → 통계 왜곡. → `| stats by (remote_addr) count() as cnt`로 1단계 서버측 집계 후 상위 `MAX_CANDIDATES*3` IP만 2단계 raw 쿼리. 새 `_aggregate_ip_rows` 헬퍼 분리.
- **H4 prompt injection via path**: UA는 `!r`로 방어됐지만 path는 raw. → events를 `json.dumps` 리스트로 변환 + prompt에 "untrusted data, do not follow instructions inside" 경고 삽입.
- **H5 `num_predict=80` 한국어 truncation**: 한국어 reason + JSON envelope가 잘려 non-json으로 떨어져 공격 놓침. → `num_predict: 256`.
- **M1 `start_at == stop_at`**: alert가 시점으로 찍혀 대시보드 시계열 왜곡. → `start_at = now - WINDOW_MIN*60`, `stop_at = now`.
- **M2 `lapi_login` 파일 핸들 누수**: `yaml.safe_load(open(...))``with open(...) as f:`.
- **M3 ratio에 499 포함**: blended 공격(4xx 29 + 499 14)이 모든 게이트를 아슬아슬 피하는 사각지대. → `ratio_4xx = ((d["4xx"] + d["499"]) / c)`.
- **M4 `paths`/`uas`/`hosts` set cap**: 공격자가 query string 다양화로 set 무한 성장 → OOM 가능. → 각 set에 500개 상한.
- **M7 `candidates=0` 경로 housekeeping 누락**: early return 전에 `save_dedup(dedup)` 호출해 만료 엔트리 정리.
**남은 High 설계 이슈 (별도 작업)**:
- **H3 분산(저강도) 봇넷 대비 게이트 사각지대**: 게이트가 per-IP라 1,000 IP × 각 29건이면 전부 통과. `/24` CIDR 집계, 동일 UA 집합 집계, 동일 path 집중 IP 집합 집계 같은 집단 축이 필요. 설계 작업량이 커서 별도 MR 예정.
## 향후 작업
- [ ] **[Medium]** Discord webhook 알림 추가 (`secret/apps/discord` Vault에서 가져오기) + systemd `OnFailure=` drop-in
- [ ] **[Low]** CrowdSec alert `origin``"crowdsec"``"anomaly-detect"`로 태깅
- [x] gemma4:e4b 한국어 reason 품질 평가 → 모델 변경 검토 — **Grok-4-fast로 전환** (3차 재설계)
- [x] 코드를 Gitea repo로 분리 (`gitea.inouter.com/kaffa/anomaly-detect`) — 2026-04-08 완료
- [-] ~~ollama 장시간 장애 시 하드 게이트 fallback~~ — 폐기 (ollama 미사용)
- [-] ~~`LLM no 판정` IP의 dedup 짧게 (1h) 따로 관리~~ — 폐기 (cohort/classifier 구조 폐기)
- [-] ~~sample 10건을 "최근 10건"으로 변경~~ — 폐기 (sample 구조 자체 폐기)
- [-] ~~`scenario_hash` 고정 해시 지정~~ — 폐기 (scenario는 LLM이 ban_ips tool로 직접 지정)
## 폐기된 전임자
- [[crowdsec-safeline#~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)|ddos-detect]] (Go, jp1 crowdsec 컨테이너 안, 60s 폴링, Claude CLI sonnet 호출). 폐기 사유: 60s 폴링 + 동기 Claude CLI 구조 한계. 이번 anomaly-detect는 5분 주기 + 통계 게이트 + 로컬 LLM(ollama gemma4)으로 비용/지연 동시 개선.
전임자 (`ddos-detect`, 1/2차 구현) 폐기 이력: [[../history/2026-04-08-anomaly-detect-iterations|history]]

View File

@@ -9,7 +9,7 @@ updated: 2026-04-04
오사카 구성: 고객 도메인 → [[cloudflare]](DNS) → [[crowdsec-safeline|SafeLine WAF]] → APISIX(라우팅) → 고객 오리진
3개의 독립 APISIX 인스턴스. kr1의 Docker APISIX는 2026-03-15 제거 완료.
3개의 독립 APISIX 인스턴스.
### 서울 relay (relay4wd)
- 용도: 포트 포워딩 게이트웨이
@@ -22,8 +22,7 @@ updated: 2026-04-04
- 방화벽: 22/tcp + 2201-2299/tcp + 443/tcp 개방, SSH는 Tailscale 경유 포트 2222
- 22 → iptables REDIRECT → 9022 (SFTPGo용, privileged 포트 우회)
- 443 → iptables REDIRECT → 8443 (Teleport용, privileged 포트 우회)
- 2026-03-17 AWS EC2에서 Lightsail nano($5/월)로 이전
- **주의**: config.yaml의 stream_proxy.tcp에 privileged 포트(1-1023)를 넣으면 비특권 컨테이너에서 bind 실패로 크래시. 2026-03-27 포트 22 추가로 장애 발생, 제거하여 복구
- **주의**: config.yaml의 stream_proxy.tcp에 privileged 포트(1-1023)를 넣으면 비특권 컨테이너에서 bind 실패로 크래시
#### 포트 포워딩 (stream_routes)
@@ -49,16 +48,14 @@ BunnyCDN(iron-jp, ID 5555247) → apisix-osaka(172.233.93.180) → 백엔드
```
- 용도: **Traefik(VIP 192.168.9.53)과 동등한 병렬 독립 LoadBalancer gateway**. "SafeLine WAF 전용" 이 아니라, 2026-03-25 에 메인 HTTP 라우팅을 Traefik 으로 이전한 이후 현재 APISIX 에는 `juiceshop.keepanker.cv` 1건만 남아 있고 그것이 chaitin-waf 플러그인으로 SafeLine 통합 테스트 용도라 **결과적으로 현재만** SafeLine 테스트 전용처럼 보일 뿐 — 언제든 새 ApisixRoute 추가 가능한 범용 gateway.
- 클러스터: K3s 새 클러스터 (Supabase PostgreSQL 백엔드, kr2+kr1+hp2)
- 배포: K3s apisix 네임스페이스, **Deployment replica 2** (2026-04-04 HA 업그레이드)
- 배포: K3s apisix 네임스페이스, **Deployment replica 2**
- APISIX: 3.15.0-ubuntu
- SafeLine WAF 연동: `plugin_attr.chaitin-waf``safeline-detector:8000` (10.43.253.244)
- global_rules: `http-logger` (CrowdSec 로그 전송) + `limit-req` (rate 20, burst 10). **`chaitin-waf`는 global_rule에 없음** — 라우트별 적용 (2026-03-15 git push 500 사건 이후). 두 global_rule 모두 GatewayProxy CR(`spec.plugins`)로 선언되어 ingress controller가 관리.
- global_rules: `http-logger` (CrowdSec 로그 전송) + `limit-req` (rate 20, burst 10). **`chaitin-waf`는 global_rule에 없음** — 라우트별 적용. 두 global_rule 모두 GatewayProxy CR(`spec.plugins`)로 선언되어 ingress controller가 관리.
- 서비스: apisix-gateway LoadBalancer 192.168.9.50 (80→9080, 443→9443)
- etcd: **K3s 내부 apisix-etcd StatefulSet 3 replicas** (Bitnami etcd 차트, Longhorn PVC 5Gi × 3), prefix `/apisix`. ClusterIP `apisix-etcd.apisix.svc.cluster.local:2379`. 외부 통합 etcd로 잠시 이전했다가(2026-04-06) Patroni와의 장애 격리 + 네트워크 단순화를 위해 K3s 내부로 복귀(2026-04-08).
- etcd: **K3s 내부 apisix-etcd StatefulSet 3 replicas** (Bitnami etcd 차트, Longhorn PVC 5Gi × 3), prefix `/apisix`. ClusterIP `apisix-etcd.apisix.svc.cluster.local:2379`.
- Admin API: `apisix-admin` ClusterIP :9180 (`adminKey: edd1c9f034335f136f87ad84b625c8f1`). admin allow IPs: `127.0.0.1/24`, `10.42.0.0/16`(pod), `10.43.0.0/16`(svc), `192.168.9.0/24`(LAN), `100.64.0.0/10`(Tailscale)
- HAProxy: OpenWrt에서 :9080→192.168.9.50:80, :9443→192.168.9.50:443 (MetalLB)
- 2026-03-25 메인 HTTP 라우팅을 Traefik 으로 이전. APISIX route 축소되어 현재 juiceshop 1건만 (SafeLine chaitin-waf 통합 테스트). **APISIX 자체는 여전히 독립 외부 LoadBalancer gateway — MetalLB VIP 별도 유지**
- 2026-04-08 ApisixRoute CRD 사용을 위해 ingress controller 복구 + K3s 내부 etcd 복귀
#### plugin_metadata (GatewayProxy CR로 관리)
@@ -104,20 +101,14 @@ kubectl rollout restart deploy/apisix -n apisix
⚠️ **Vector parse_apisix 정규식과 짝**: APISIX log format을 변경할 때마다 `vector` helm values의 parse_apisix 정규식도 같이 업데이트해야 [[victorialogs|VictoriaLogs]]에 구조화 필드가 정상 추출됨. 현재 정규식은 `xff/xrip` 필드를 optional 그룹으로 처리.
#### 이전 사유 (2026-03-25)
- Ingress Controller 2.0 초기 시도에서 GatewayProxy 모드 + ApisixRoute CRD 연동 실패 (당시 helm values에 v1.x 형식의 `config.apisix.serviceName` 사용 → 차트 1.x 스키마와 불일치)
- Gateway API HTTPRoute에 플러그인 개별 적용 방법이 없음 → ApisixRoute CRD 필요
- global_rules를 Ingress Controller가 덮어쓰려는 충돌 발생
- → Traefik으로 메인 라우팅 교체, APISIX는 SafeLine WAF 연동 전용으로 유지
#### Ingress Controller 설정
#### Ingress Controller 복구 (2026-04-08)
`apisix-ingress-controller` Helm release (chart 1.1.2, controller v2.0.1). GatewayProxy 모드에서 ApisixRoute CRD(v2)도 정상 동작. ApisixRoute에 `ingressClassName: apisix`만 명시하면 controller가 자동으로 admin API에 push.
`apisix-ingress-controller` Helm release는 살아있었으나 Deployment가 수동 삭제된 상태였음. helm values를 chart 1.1.2 (controller v2.0.1) 스키마에 맞게 재작성 후 `helm upgrade`로 복구.
값 수정 핵심:
helm values 핵심:
```yaml
gatewayProxy:
createDefault: true # GatewayProxy CR 자동 생성
createDefault: true
provider:
type: ControlPlane
controlPlane:
@@ -129,12 +120,12 @@ gatewayProxy:
adminKey:
value: "edd1c9f034335f136f87ad84b625c8f1"
config:
disableGatewayAPI: false # Gateway API + ApisixRoute 양쪽 다 지원
disableGatewayAPI: false
kubernetes:
ingressClass: apisix
```
검증 결과: GatewayProxy 모드에서도 ApisixRoute CRD(v2)는 정상 동작함. 옛 메모의 "GatewayProxy 모드에서 ApisixRoute CRD 미지원"은 틀렸음 — 잘못된 helm values 때문이었음. ApisixRoute에 `ingressClassName: apisix`만 명시하면 controller가 자동으로 admin API에 push.
라우팅 전환/복구 이력: [[../history/2026-03-25-apisix-to-traefik-routing|history]]
ApisixRoute 예시 (라우트별 chaitin-waf):
```yaml
@@ -178,16 +169,9 @@ WAF가 문제 시 `plugins` 항목만 빼면 즉시 비활성화됨.
⚠️ **etcd 직접 PUT은 임시 디버깅 외에는 금지.** 1분 안에 삭제됨. 옛 운영 메모에 있는 `etcdctl put` 예제는 모두 deprecated.
#### K3s 내부 etcd 복귀 (2026-04-08)
#### etcd 설정
기존: 외부 통합 etcd 클러스터(192.168.9.100/10.100.2.214/10.253.101.233, prefix `/apisix/seoul`) — Patroni DCS와 etcd 공유.
복귀 후: K3s 내부 `apisix-etcd` StatefulSet 3 replicas (Bitnami etcd, Longhorn PVC 5Gi × 3, ClusterIP `apisix-etcd.apisix.svc:2379`, prefix `/apisix`).
이유:
- **장애 격리**: Patroni 이슈가 APISIX 라우팅에 영향 주지 않게
- **네트워크 단순화**: K3s 내부 통신만으로 충분
- **운영 일관성**: helm 한 곳에서 etcd + apisix + ingress controller 관리
K3s 내부 `apisix-etcd` StatefulSet 3 replicas (Bitnami etcd, Longhorn PVC 5Gi x 3, ClusterIP `apisix-etcd.apisix.svc:2379`, prefix `/apisix`).
helm values 핵심:
```yaml
@@ -200,10 +184,11 @@ etcd:
```
업그레이드 시 주의:
- Bitnami etcd 차트의 pre-upgrade hook은 기존 etcd 멤버 list를 시도함. 멤버가 없는 상황에서는 무한 retry 실패 `helm upgrade --no-hooks` 사용 또는 helm rollback 후 재시도
- helm upgrade 전 release values nesting 점검 필수: `apisix.admin.allow.ipList`(O) vs `admin.allow.ipList`(X), `apisix.nginx.http.realIpFrom`(O) vs `nginx.http.realIpFrom`(X), `service.http.containerPort`(O) vs `gateway.http.containerPort`(X). 잘못된 nesting은 차트가 조용히 무시함.
- 이전 후 ingress controller는 자동으로 모든 K8s CRD 객체를 etcd에 재push (rollout restart로 즉시 동기화 가능)
- 외부 통합 etcd의 stale `/apisix/seoul/*` 키는 수동 삭제 (postgresql-ha.md의 etcd cleanup 명령 참고)
- Bitnami etcd 차트의 pre-upgrade hook 실패 `helm upgrade --no-hooks` 사용
- helm upgrade 전 release values nesting 점검 필수: `apisix.admin.allow.ipList`(O) vs `admin.allow.ipList`(X). 잘못된 nesting은 차트가 조용히 무시함.
- ingress controller는 자동으로 모든 K8s CRD 객체를 etcd에 재push (rollout restart로 즉시 동기화)
etcd 통합/분리 이력: [[../history/2026-04-06-apisix-etcd-consolidation|history]]
### BunnyCDN Pull Zone 매핑 (2026-04-09 API 실측)
@@ -216,7 +201,7 @@ etcd:
| i-gate | 5557897 | 172.233.93.180 | (미사용 슬롯) | `i-gate.b-cdn.net` |
참고:
- iron-kr `IgnoreQueryStrings: true` / iron-jp `IgnoreQueryStrings: false` — 동일 미들웨어 풀존이지만 캐시 키 정책이 다름
- iron-kr / iron-jp 모두 `IgnoreQueryStrings: false` (통일)
- iron-kr / iron-jp `BlockNoneReferrer: true`, iron-git 는 `false` (git smart HTTP 호환)
- 모든 zone `EdgeRules: []` (Edge Rules 미사용, 모든 분기는 미들웨어 64811 안)
- Edge Script 64811 `crowdsec-bouncer-middleware` 는 **iron-jp + iron-kr 두 풀존**에만 attach. iron-git 은 의도적 우회 (git pack 바이너리 보호 불가).
@@ -259,7 +244,7 @@ APISIX 서울 라우트 hcv-inouter-com → K3s Traefik (192.168.9.134/214/135:4
트래픽 흐름: Cloudflare DNS (A 220.120.65.245, BunnyCDN 우회) → OpenWrt(:443) → hp2(:9443, incus proxy device) → APISIX(10.179.99.126, 라우트 nocodb/nocodb-nuxt) → K3s Traefik (192.168.9.134/214/135:443, roundrobin, scheme https) → nocodb svc:8080 (namespace tools).
BunnyCDN WAF가 NocoDB JS를 오탐 차단하여 CDN 우회 처리 (2026-03-15).
BunnyCDN WAF가 NocoDB JS를 오탐 차단하여 CDN 우회 처리.
## CrowdSec 로그 연동
@@ -271,36 +256,7 @@ BunnyCDN WAF가 NocoDB JS를 오탐 차단하여 CDN 우회 처리 (2026-03-15).
시나리오 매칭으로 반복 공격자 탐지.
## 트러블슈팅 기록
### git push 500 에러 (2026-03-15)
**증상**: `gitea.inouter.com`으로 git push 시 BunnyCDN에서 403 반환 (오리진 500)
**원인 1 — client_body_temp 퍼미션**:
- nginx가 큰 POST body(git pack ~20KB)를 `/usr/local/apisix/client_body_temp/`에 임시 파일로 쓸 때 퍼미션 거부
- 디렉토리 소유자가 `nobody`인데 APISIX는 `apisix` 사용자로 실행
- 작은 요청은 메모리 버퍼로 처리되어 정상, 큰 요청만 실패
- **수정**: `chown apisix:apisix /usr/local/apisix/client_body_temp`
**원인 2 — chaitin-waf 글로벌 적용**:
- `chaitin-waf`가 global_rules에 있어서 모든 요청에 WAF 검사 적용
- SafeLine detector가 git pack 바이너리를 파싱 실패 시 `mode: block`이면 500 반환 (fail-closed)
- 라우트 레벨에서 match 제외를 넣어도 global_rule이 먼저 적용되어 무시됨
- **수정**: global_rules에서 `chaitin-waf` 제거, 각 라우트에 개별 적용. gitea 라우트는 `match: [{"vars": [["uri", "!", "~*", "\\.git/"]]}]`로 git 경로 WAF 제외
**교훈**:
- SafeLine WAF는 바이너리 프로토콜(git pack 등)을 처리할 수 없음 → 해당 경로는 WAF에서 제외 필요
- global_rules의 WAF는 예외 처리가 어려움 → 라우트별 개별 적용이 유연함
- BunnyCDN `errorcode: 112`는 오리진 에러를 나타냄 → 실제 원인은 오리진 로그에서 확인
### http-logger 401 에러 (2026-03-15)
**증상**: 서울 APISIX http-logger가 CrowdSec으로 로그 전송 시 401 Unauthorized
**원인**: http-logger 플러그인은 Authorization 헤더를 `auth_header` 필드로 설정해야 하는데, `headers.Authorization`으로 설정되어 있었음 (무시됨)
**수정**: `"headers": {"Authorization": "..."}``"auth_header": "apisix-crowdsec-log-2024"`
과거 트러블슈팅 이력: [[../history/2026-03-15-apisix-git-push-500|2026-03-15 git push 500 에러 + http-logger 401]]
## jarvis.inouter.com 라우트

View File

@@ -42,10 +42,9 @@ tags: [infra, backup]
### NAS 설정 메모
- Synology sudo PATH 문제: `/etc/sudoers.d/path``secure_path` 추가 (2026-03-17)
- NAS `/volume1/incus/inbest/` 소유자: `kaffa:users` (rsync 쓰기용)
- btrfs subvolume: `/volume1/incus` (ID 741)
- 모든 백업 스크립트에 NAS 접근 불가 시 스킵/로컬 보관 로직 추가 (2026-04-05)
- 모든 백업 스크립트에 NAS 접근 불가 시 스킵/로컬 보관 로직 포함
- NFS 마운트는 반드시 `soft,timeo=50,retrans=3` 사용 (hard 금지, [[nas-storage]] 참조)
### 복구 시나리오
@@ -71,7 +70,7 @@ NocoDB가 사용하는 PostgreSQL(Incus 컨테이너) 백업. pg_dump → NAS.
- **출력**: `/mnt/nas-backup/daily/nocodb_YYYYMMDD_HHMMSS.dump` (NAS NFS 마운트)
- **보관**: 30일 초과 자동 삭제
- **NAS 마운트**: 스크립트 내에서 `soft,timeo=50,retrans=3`으로 자동 마운트
- **NAS 미접근 시**: 10초 타임아웃 후 스킵 (2026-04-05 추가)
- **NAS 미접근 시**: 10초 타임아웃 후 스킵
## kine 백업 (Supabase PostgreSQL)
@@ -107,9 +106,9 @@ gunzip kine-YYYYMMDD.sql.gz
psql "$DB_URL" < kine-YYYYMMDD.sql
```
## etcd 스냅샷 백업 (비활성, 2026-04-05)
## etcd 스냅샷 백업 (비활성)
~~K3s datastore인 외부 etcd 클러스터의 스냅샷 백업.~~ K3s가 kine(Supabase PostgreSQL)로 전환되어 etcd를 더 이상 사용하지 않음. etcd-backup.timer, etcd-backup-sync.timer 모두 비활성화됨. kine 백업이 대체.
K3s가 kine(PostgreSQL)로 전환되어 etcd 백업은 불필요. etcd-backup.timer, etcd-backup-sync.timer 비활성화됨. kine 백업이 대체.
### 1. etcd snapshot (hp2 etcd 컨테이너)

View File

@@ -6,18 +6,14 @@ tags: [k3s, traefik, gateway-api]
## 개요
K3s 메인 라우팅을 Traefik이 담당 (2026-03-25 APISIX에서 전환).
K3s 메인 라우팅을 Traefik이 담당. APISIX는 독립 LoadBalancer(MetalLB VIP 192.168.9.50)로 병렬 운영.
### 전환 이력
- 2026-03-21: K3s Ingress → Gateway API 전환 (기존 클러스터, Traefik v3.6.9)
- 2026-03-24: 새 클러스터(kr2)에서 APISIX + Ingress Controller 2.0으로 구성
- 2026-03-25: APISIX Ingress Controller의 Gateway API 플러그인 제한으로 **메인 HTTP 라우팅을 Traefik 으로 교체**
- APISIX 는 여전히 독립 LoadBalancer (MetalLB VIP 192.168.9.50) 로 운영 중, 다만 route 는 `juiceshop.keepanker.cv` 1건만 남아 SafeLine chaitin-waf 플러그인 통합 테스트 용도. Traefik 과 동등한 병렬 gateway 로 언제든 새 route 추가 가능.
전환 이력: [[../history/2026-03-25-apisix-to-traefik-routing|history]]
## Traefik 배포 (새 클러스터)
## Traefik 배포
- **DaemonSet** (kube-system 네임스페이스)
- LoadBalancer 192.168.9.53 (MetalLB, 이전 hostPort 80/443에서 전환)
- LoadBalancer 192.168.9.53 (MetalLB)
- Gateway API provider 활성화
- TLSStore CRD로 와일드카드 인증서 기본 로드
- 와일드카드 인증서: *.inouter.com, *.inouter.com, *.actions.it.com, *.api.inouter.com, *.mcp.inouter.com
@@ -103,10 +99,6 @@ rewrite name hcv.inouter.com traefik.kube-system.svc.cluster.local
| NodeHosts (IP 직접 등록) | 단순 | ClusterIP 변경 시 수동 갱신 필요, stale 엔트리 위험 |
| **rewrite (현재)** | ClusterIP 자동 추종 | HTTPRoute 추가 시 rewrite 규칙도 추가 필요 |
### 장애 사례 (2026-03-22)
NodeHosts에 `gitea.inouter.com → 10.43.205.207`(stale ClusterIP)이 남아있어서 ArgoCD 전체가 Gitea 싱크 실패(Unknown 상태). CoreDNS rewrite 방식으로 교체하여 해결.
### 유지보수
새 HTTPRoute 추가 시 `coredns-custom` ConfigMap에 rewrite 규칙 추가 후 CoreDNS 재시작:

View File

@@ -24,7 +24,7 @@ tags: [infra, network, kr-zone, openwrt]
## 서울 K3s 클러스터
서울존 3대(kr1, kr2, hp2)를 K3s v1.34.5+k3s1 클러스터로 구성. **kr1/kr2는 control-plane, hp2는 worker(k3s-agent)** — 2026-04-05 hp2가 control-plane에서 worker로 전환됨.
서울존 3대(kr1, kr2, hp2)를 K3s v1.34.5+k3s1 클러스터로 구성. **kr1/kr2는 control-plane, hp2는 worker(k3s-agent)**.
| 노드 | LAN IP | OS |
|------|--------|----|
@@ -34,14 +34,14 @@ tags: [infra, network, kr-zone, openwrt]
주요 네임스페이스 (2026-04-09 라이브): api, apisix, argocd, cert-manager, db, democratic-csi, gitea, juiceshop, kroki, kube-system, logging, longhorn-system, mail, mcp, metallb-system, monitoring, mq, n8n, nfs-provisioner, openmemory, outline, rabbitmq-system, safeline, searxng, sftpgo, sshpiper, teleport, test, tools, vault(빈 ns, 서비스만 잔존)
> anvil/ironclad ns는 삭제됨. system-upgrade ns도 현재 없음. vault ns는 빈 상태로 ClusterIP 서비스(`vault-external`)만 잔존 — 실제 Vault 서버는 jp1 incus 컨테이너로 이전됨 (아래 "서비스 위치" 참조).
> vault ns는 빈 상태로 ClusterIP 서비스(`vault-external`)만 잔존 — 실제 Vault 서버는 jp1 incus 컨테이너 (아래 "서비스 위치" 참조).
게이트웨이: 두 독립 LoadBalancer 병렬 운영 — Traefik (MetalLB VIP 192.168.9.53, 메인 라우팅 14 HTTPRoute + 5 legacy IngressRoute) + APISIX (MetalLB VIP 192.168.9.50, 2026-03-25 축소 이후 `juiceshop.keepanker.cv` 1 route · chaitin-waf SafeLine 통합)
- Traefik DaemonSet, MetalLB LoadBalancer 192.168.9.53 + Gateway API
- APISIX Deployment **replica 2**, MetalLB LoadBalancer 192.168.9.50, SafeLine WAF chaitin-waf 플러그인 연동, Admin API 수동 관리
- APISIX etcd: 통합 etcd 클러스터 사용 (K3s 내 StatefulSet 삭제, 2026-04-06). prefix `/apisix-seoul`
- APISIX etcd: K3s 내부 apisix-etcd StatefulSet 3 replicas, prefix `/apisix`
- CoreDNS hairpin rewrite: traefik.kube-system.svc.cluster.local
- K3s 데이터스토어: kine → HAProxy(192.168.9.1:5432) → Patroni PostgreSQL Leader 자동 감지 (Supabase에서 로컬 이전, 2026-04-05)
- K3s 데이터스토어: kine → HAProxy(192.168.9.1:5432) → Patroni PostgreSQL Leader 자동 감지
트래픽 흐름:
- 일반: 외부 → OpenWrt HAProxy(:80/:443) → MetalLB Traefik(192.168.9.53:80/443) → K3s 서비스
@@ -72,9 +72,9 @@ tags: [infra, network, kr-zone, openwrt]
| vlogs | logging | victoria-logs-single-0.11.31 | v1.49.0 |
| vm-stack | monitoring | victoria-metrics-k8s-stack-0.72.6 | v1.139.0 |
> **주의**: `nfs-provisioner` Helm 릴리스 status=`failed` (revision 2, 2026-04-05). 실제 파드는 정상 Running이라 동작은 영향 없음. `helm history` 확인 후 정리 필요.
> **주의**: `nfs-provisioner` Helm 릴리스 status=`failed` (revision 2). 실제 파드는 정상 Running. `helm history` 확인 후 정리 필요.
>
> Vault Helm 릴리스 제거됨. RabbitMQ Operator는 `rabbitmq-system` ns에서 kubectl apply 직배포 (Helm 미사용).
> RabbitMQ Operator는 `rabbitmq-system` ns에서 kubectl apply 직배포 (Helm 미사용).
### ArgoCD Applications (2026-04-09 라이브)
@@ -86,7 +86,6 @@ tags: [infra, network, kr-zone, openwrt]
| smtp-relay | mail | gitea.inouter.com/kaffa/smtp-relay (path: k8s) |
| vultr-api | api | gitea.inouter.com/kaffa/vultr-api (path: k8s) |
> 이전 등록되어 있던 anvil, ironclad, n8n, nocodb, pgcat, cloud-api-emulator는 모두 ArgoCD에서 제거됨. anvil/ironclad/cloud-api-emulator는 ns 자체가 삭제, n8n은 Helm 릴리스로 전환, nocodb/pgcat은 kubectl 직접 관리로 전환.
### kubectl 직접 관리 (Helm/ArgoCD 미적용)
@@ -172,9 +171,6 @@ db (proxysql, pgcat), kroki, mq (RabbitmqCluster CR), openmemory (mcp/ui/qdrant)
- 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)
> **vaultwarden 컨테이너 제거됨** (이전 정본에는 있었음). 별도 확인 필요.
>
> 신규 추가된 `vault`, `socks5-proxy`, `netbis-cf-bouncer`, `etcd`(default proj)는 Obsidian의 별도 운영 문서가 아직 없을 수 있음 — 필요 시 `infra/` 하위에 추가.
**monitoring 프로젝트** (2): grafana (10.253.103.199), prometheus (10.253.100.193)
@@ -182,26 +178,18 @@ db (proxysql, pgcat), kroki, mq (RabbitmqCluster CR), openmemory (mcp/ui/qdrant)
**default 프로젝트** (3): brokkr (10.100.3.54), mariadb-2 (10.100.3.64), **postgres-2** (10.100.3.185)
**ops 프로젝트** (1): **heimdall** (10.100.3.108, 2026-04-09 tofu 재생성 — 신규 프로젝트, 구 `default/heimdall (10.100.3.92)` 대체)
> 이전 정본의 `etcd (10.100.3.7)`는 라이브에 존재하지 않음. K3s kine이 Patroni로 이전된 시점(2026-04-05) 전후에 정리된 것으로 추정. **postgres-2**가 새로 추가됨 (Patroni HA 멤버 추정).
>
> 2026-04-09: heimdall을 `kaffa/ops-agents-tofu/heimdall` 모듈로 재생성. 유저 root→kaffa, 프로젝트 default→ops, 메모리/CPU limit 설정 (8GiB/4core). `syn` 과 동일 패턴. IP 변경 (10.100.3.92 → 10.100.3.108).
**ops 프로젝트** (1): **heimdall** (10.100.3.108, tofu 관리 `kaffa/ops-agents-tofu/heimdall`)
### kr2 컨테이너
**default 프로젝트** (2): mariadb-3 (10.100.1.162), **postgres-3** (10.100.1.83)
> 이전 정본의 `etcd (10.100.1.198)`, `cloudflared (10.100.1.95)`는 default에 없음. cloudflared는 **inbest 프로젝트로 이동**(아래). etcd는 제거됨. safeline VM 언급도 더 이상 유효하지 않음 (이미 삭제됨).
**inbest 프로젝트** (7): **cloudflared** (10.100.1.95), mariadb10 (10.100.1.148), nginx (10.100.1.121), php5 (10.100.1.174), php8 (10.100.1.3), phpmyadmin (10.100.1.60), sftp (10.100.1.158)
### 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)
> 이전 정본의 `etcd (10.100.2.11)`는 라이브에 없음. APISIX etcd가 K3s StatefulSet으로 통합된 시점(2026-04-06) 전후에 정리된 것으로 추정. **anomaly-detect**가 새로 추가됨.
**inbest 프로젝트** (0): 프로젝트만 존재, 인스턴스 없음 (default profile만 사용 중). kr2 inbest와 페어 구성을 고려해 만들어둔 빈 프로젝트로 보임.
## GPU
@@ -264,7 +252,7 @@ Docker: `--runtime=nvidia` 또는 `--gpus all`로 GPU 사용. Podman: CDI 방식
- **K3s datastore**: Incus etcd 3노드 클러스터 (192.168.9.214, 192.168.9.135, 192.168.9.134)
- `/registry/` — K3s 클러스터 백엔드 스토어
- `/patroni/nocodb-cluster` — NocoDB PostgreSQL HA
- **APISIX etcd** (K3s 내부): apisix-etcd StatefulSet **3 replicas** (ClusterIP 10.43.20.100:2379, prefix `/apisix`, 2026-04-04 HA 업그레이드)
- **APISIX etcd** (K3s 내부): apisix-etcd StatefulSet **3 replicas** (ClusterIP 10.43.20.100:2379, prefix `/apisix`)
- **OVN 네트워크**: ovn1 (10.165.246.0/24) — hp2↔kr2 간 오버레이
- **CDN IP 필터**: BunnyCDN + Cloudflare IP만 80/443 허용, 그 외 WAN 차단
- 스크립트: `/etc/cdn-filter-update.sh`
@@ -292,14 +280,9 @@ Docker: `--runtime=nvidia` 또는 `--gpus all`로 GPU 사용. Podman: CDI 방식
- kr2: `/etc/systemd/network/30-usb-2g5.network`
- hp2: `/etc/systemd/network/30-ens2.network`
2026-04-08 hp2 NIC 추가(Realtek RTL8125 PCIe) 및 2.5G LAN 4노드 완성. kr2의 IP는 .201에서 .135로 변경(1G LAN과 옥텟 통일).
USB autosuspend/NFS hang 인시던트 이력: [[../history/2026-04-04-usb-25g-hang|2026-04-04 USB 2.5G hang]]
#### USB 2.5G 안정성 이슈 (2026-04-04 해결)
USB 2.5GbE 어댑터(r8152/cdc_ncm)가 Linux USB autosuspend에 의해 절전 모드 진입 후 드라이버 hang 발생. kr2에서 NFS hard mount가 죽은 2.5G IP로 D-state 누적되어 로드 2000+ 장애.
**해결:**
- USB unbind/bind로 즉시 복구
안정성 대책:
- kr2: GRUB `usbcore.autosuspend=-1`, udev rule `99-usb-ethernet.rules` (scatter-gather off)
- NAS: `/usr/local/etc/rc.d/usb-no-suspend.sh` 스타트업 스크립트
@@ -345,6 +328,8 @@ Xray VLESS+XHTTP 스텔스 구성
| safeline-osaka | 100.100.212.6 | Linode VPS (오사카) | offline 49d (~2026-02-19) | SafeLine WAF 단독 호스트였으나 K3s `safeline` ns로 이전 후 미사용. 토큰/시크릿 미등록 (vault.md 참조). 재기동 또는 폐기 결정 필요 |
| china-ali | 100.67.31.11 | Aliyun VPS (중국) | offline 43d (~2026-02-25) | 용도 불명 — 과거 중국 리전 테스트용 추정. kappa 확인 후 폐기 또는 정식 등록 결정 필요 |
과거 마이그레이션 / 인시던트 이력은 `history/` 참조.
## 개인 워크스테이션
서버가 아닌 kaffa 개인 디바이스(Mac mini / MacBook / iPhone)는 [[workstations]] (`dev/workstations.md`) 참조. 인프라 정본이 아닌 개발 환경 맥락이라 `dev/` 하위에 분리.

View File

@@ -5,134 +5,9 @@ status: 완료
tags: [k3s, migration, postgresql, supabase]
---
## 개요
이 문서는 과거 마이그레이션 작업 기록입니다. 전체 내용은 history 파일로 이동했습니다.
기존 K3s 클러스터(외부 etcd)에서 Supabase PostgreSQL 백엔드로 전환하는 프로젝트.
2026-03-24~25 완료.
- [[../history/2026-03-24-k3s-postgresql-migration|2026-03-24 K3s PostgreSQL 이전]]
- [[../history/2026-04-05-supabase-to-patroni|2026-04-05 Supabase → Patroni 이전]]
## 기존 클러스터 (폐기됨)
| 항목 | 값 |
|------|-----|
| 노드 | hp2(server), kr1(server), kr2(분리됨) |
| 데이터스토어 | 외부 etcd (Incus 컨테이너 3개) |
| etcd 엔드포인트 | http://192.168.9.214:2379, http://192.168.9.135:2379, http://192.168.9.134:2379 |
## 현재 클러스터
| 항목 | 값 |
|------|-----|
| 컨트롤 플레인 | kr1 (192.168.9.214), kr2 (192.168.9.135) |
| 워커 (agent) | hp2 (192.168.9.134) |
| 데이터스토어 | Supabase PostgreSQL (싱가포르) |
| Pooler | Session mode, port 5432 |
| DB 호스트 | aws-1-ap-southeast-1.pooler.supabase.com |
| DB 유저 | postgres.bahmyfgldxmtgvsufmwf |
| 시크릿 | Vault `secret/cloud/supabase` |
| 커넥션 제한 | pool_max_conns=5&pool_min_conns=1 |
| max_connections | 60 (무료 티어) |
| 네트워크 | LAN 192.168.9.x, flannel vxlan |
| Pod CIDR | 10.42.0.0/16 |
| Service CIDR | 10.43.0.0/16 |
| tls-san | k3s.inouter.com, 192.168.9.135, 100.119.109.41 |
| 게이트웨이 | 두 독립 LoadBalancer 병렬: Traefik (DaemonSet, MetalLB 192.168.9.53, 메인 HTTPRoute 라우팅) + APISIX (Deployment replica 2, MetalLB 192.168.9.50, 현재 juiceshop 1 route · chaitin-waf SafeLine 통합) |
| LB | MetalLB L2 (192.168.9.50-59), K3s ServiceLB 비활성화 |
| 스토리지 | Longhorn v1.8.2 |
| 인증서 | cert-manager + Google Trust Services (와일드카드 8개, Reflector) |
| disable | traefik (Helm 별도 설치), servicelb (MetalLB 사용) |
## 장애 시나리오
| 장애 | 영향 | 복구 |
|------|------|------|
| server 1대 죽음 | 서비스 무중단, 나머지 server가 API 유지 | 자동 |
| agent 죽음 | Pod 재스케줄 | 자동 |
| server 2대 동시 | 기존 Pod 유지, 변경 불가 | 아무 노드에서 k3s server 시작 |
| Supabase DB 장애 | 기존 Pod 유지, 변경 불가 | Supabase 복구 시 자동 |
## Supabase 제약사항
- **Transaction mode pooler(6543) 사용 불가** — K3s Kine이 prepared statements 사용, transaction pooler와 충돌
- **Session mode pooler(5432) 필수** — pool_max_conns로 커넥션 수 제한
- 무료 티어 max_connections=60, Pro($25/월)도 Micro는 동일
- Small($65/월 총)로 올리면 90개
- 싱가포르 리전 → 서울에서 ~70ms 레이턴시 (허용 범위)
## 마이그레이션 이력
### Phase 0: 인프라 구성 ✅ (2026-03-24~25)
- kr2 첫 server 구성, Supabase PostgreSQL 연결
- cert-manager + 와일드카드 인증서 6개 + Reflector
- Traefik DaemonSet (hostPort 80/443) + Gateway API
- APISIX replica 1 (초기엔 SafeLine WAF 테스트 라우트 1건, Ingress Controller 제거). 2026-04-04 에 replica 2 HA 로 확장
- HAProxy 80/443 → Traefik hostPort 복원
- Longhorn v1.8.2
### Phase 1~2: kr1 합류 + 서비스 이전 ✅ (2026-03-24~25)
- kr1 server로 합류 → 2-server HA 확보
- 전 서비스 이전 완료 (NocoDB, Gitea, n8n, ArgoCD, Grafana, SearXNG, SafeLine 등)
### Phase 3: hp2 합류 ✅ (2026-03-25)
- hp2 **agent**로 합류 (server 3대 → server 2 + agent 1 구성으로 변경)
- Supabase 커넥션 절약 (agent는 DB 커넥션 불필요)
### Vault jp1 이전 ✅ (2026-03-24)
- jp1 Incus default 프로젝트, `vault` 컨테이너 (Debian 13)
- Vault v1.21.4 (raft 스토리지, 스냅샷 복원)
- vault-mcp-server v0.2.0 (Go 바이너리, systemd, port 8080)
- K3s vault 네임스페이스 → ExternalName 서비스로 전환
### Phase 4: 정리 ✅ (2026-03-25)
- 기존 etcd Incus 컨테이너 폐기 완료
- 기존 K3s server.bak 삭제 완료
### Phase 5: MetalLB 도입 + 네임스페이스 정리 ✅ (2026-03-26)
- MetalLB L2 도입 (192.168.9.50-59), K3s ServiceLB 비활성화
- NodePort 전면 제거 → LoadBalancer 전환 (APISIX .50, sshpiper .51, Teleport .52, Traefik .53)
- Traefik: hostPort 80/443 → LoadBalancer 192.168.9.53
- HAProxy 백엔드: 3노드 roundrobin → MetalLB IP 단일 엔드포인트
- k3s.inouter.com DNS: 3노드 A 레코드 → 192.168.9.53 단일
- sshpiper 설치 (SSH 리버스 프록시, Pipe CRD)
- Teleport 설치 + relay4wd APISIX stream 포워딩 (443→8443→192.168.9.52:443)
- ironclad/anvil 네임스페이스 삭제 (오사카에서 서빙)
- api 네임스페이스 신설 (namecheap-api, vultr-api, *.api.inouter.com)
- mcp 네임스페이스 신설 (bunnycdn-mcp, kaniko 빌드, bunny.mcp.inouter.com)
- 신규 인증서: *.api.inouter.com, *.mcp.inouter.com
## 현재 네임스페이스 구조
| Namespace | 서비스 | 비고 |
|-----------|--------|------|
| kube-system | Traefik (LB .53) | 메인 라우팅 |
| apisix | APISIX (LB .50) | 독립 외부 gateway (Traefik 과 병렬). 현재 route juiceshop 1건 (SafeLine 통합 테스트) |
| sshpiper | sshpiper (LB .51) | SSH 리버스 프록시 |
| teleport | Teleport (LB .52) | 접근 관리 |
| metallb-system | MetalLB | L2 LB |
| cert-manager | cert-manager | 인증서 (와일드카드 8개) |
| argocd | ArgoCD | GitOps |
| gitea | Gitea + PostgreSQL + Valkey | Git |
| api | namecheap-api, vultr-api | API 서비스 |
| mcp | bunnycdn-mcp | MCP 서버 |
| tools | cfb-manager, nocodb | 도구 |
| db | pgcat | PostgreSQL 커넥션 풀러 |
| mq | rabbitmq-server | 메시지 브로커 |
| rabbitmq-system | rabbitmq-cluster-operator | RabbitMQ Operator |
| juiceshop | juiceshop | OWASP WAF 테스트용 |
| monitoring | VictoriaMetrics + Grafana | 모니터링 |
| longhorn-system | Longhorn | 스토리지 |
| safeline | SafeLine WAF | 보안 |
| openmemory | OpenMemory MCP + Qdrant | 메모리 |
| vault | Vault (ExternalName → jp1) | 시크릿 |
| searxng | SearXNG | 검색 |
| n8n | n8n | 자동화 |
| kroki | Kroki | 다이어그램 |
## 관련 문서
- [[infra-hosts]] — 서버 목록
- [[metallb]] — MetalLB 설정 및 IP 할당
- [[teleport]] — Teleport 접근 관리
- [[sshpiper]] — SSH 리버스 프록시
- [[gateway-api]] — Traefik Gateway API
- [[apisix]] — APISIX 설정
- [[backup]] — 백업 체계
현재 클러스터 구성은 [[infra-hosts]] 참조.

View File

@@ -7,7 +7,6 @@ tags: [infra, k3s, metallb, networking]
## 개요
K3s 클러스터에 LoadBalancer 타입 서비스를 제공하는 베어메탈 로드밸런서.
NodePort 난립 문제를 해결하기 위해 도입 (2026-03-26).
K3s 내장 ServiceLB(Klipper)는 비활성화 (`--disable servicelb`, kr2/kr1 config.yaml).
## 배포 정보
@@ -66,16 +65,4 @@ kubectl get l2advertisement -n metallb-system # L2 광고 확인
kubectl get svc --all-namespaces -o wide | grep LoadBalancer # LB 서비스 목록
```
## 이전 기록 (2026-03-26)
| Service | Before | After |
|---------|--------|-------|
| traefik | hostPort 80/443 | LoadBalancer 192.168.9.53 |
| apisix-gateway | NodePort 30233/31137 | LoadBalancer 192.168.9.50 |
| sshpiper | NodePort 31840 | LoadBalancer 192.168.9.51 |
| teleport-cluster | ClusterIP | LoadBalancer 192.168.9.52 |
| argocd-server | NodePort 30080/30443 | ClusterIP (Traefik Ingress) |
| ironclad/* | NodePort | 삭제 (오사카에서 서빙) |
| anvil/* | NodePort | 삭제 |
HAProxy 백엔드: 3노드 roundrobin → MetalLB IP 단일 엔드포인트로 변경.
NodePort → LoadBalancer 이전 이력: [[../history/2026-03-24-k3s-postgresql-migration|history]] (Phase 5: MetalLB 도입)

View File

@@ -87,14 +87,12 @@ spec:
- `no_root_squash` 설정으로 root 컨테이너는 소유권 문제 없음
- 비-root 컨테이너는 Pod `securityContext.fsGroup`으로 제어
## NFS hard vs soft 교훈 (2026-04-04)
kr2에서 NAS NFS가 `hard` 마운트 + NAS 연결 끊김으로 load 1959까지 폭주한 사건 발생. D-state 프로세스(mountpoint, NFS manager)가 커널 전체를 잠식.
## NFS hard vs soft
- **hard**: NAS 끊기면 무한 대기 → 서버 먹통
- **soft**: 타임아웃 후 에러 반환 → 서버 생존
모든 NFS 마운트는 `soft,timeo=50,retrans=3` 필수.
모든 NFS 마운트는 `soft,timeo=50,retrans=3` 필수. 인시던트 이력: [[../history/2026-04-04-usb-25g-hang|history]]
## iSCSI StorageClass (democratic-csi)

View File

@@ -56,25 +56,9 @@ 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`)
- 새 컬렉션 `agent-qna` (id `c3ab34ab-fae4-4642-8f4e-12728e293e1b`) — 에이전트 간 장문 Q&A 교환 공간 (kappa↔heimdall이 tmux로 짧게 질문 → 답변은 여기에 작성)
## 2026-04-08 사고: DATABASE_URL이 Patroni replica 직결
DB endpoint는 OpenWrt HAProxy(`192.168.9.1:5432`) 경유. 과거 Patroni failover 사고 이력: [[../history/2026-04-08-patroni-failover-incident|history]]
`outline-secrets` Secret의 `DATABASE_URL`이 Patroni 노드 IP `10.100.2.5`(postgres-1)를 직접 가리키도록 설정되어 있었음. Patroni failover로 postgres-1이 replica로 강등되자 Outline의 모든 write 쿼리가 `cannot execute UPDATE in a read-only transaction` 에러로 실패. API는 500을 반환(`apiKeys.lastActiveAt` UPDATE 실패 → 인증 자체가 깨짐).
**복구**: `DATABASE_URL`을 OpenWrt HAProxy 경유로 변경.
```bash
kubectl patch secret outline-secrets -n outline --type=json \
-p='[{"op":"replace","path":"/data/DATABASE_URL","value":"<base64 of postgresql://outline:outline@192.168.9.1:5432/outline>"}]'
kubectl rollout restart deployment/outline -n outline
# RWO PVC 때문에 old pod를 수동 삭제해야 새 pod attach 가능
kubectl delete pod -n outline <old-pod>
```
**원인**: 이 Secret은 nocodb/n8n/pgcat 변경(2026-04-08 [[postgresql-ha#2026-04-08 사고 기록]])과 함께 일괄 마이그레이션되지 않았음. **나머지 Patroni 사용자도 동일 검증 필요**`kubectl get secret -A -o json | jq -r '.items[] | select(.data.DATABASE_URL) | "\(.metadata.namespace)/\(.metadata.name)"'`로 검수 권장.
**교훈**: Patroni 사용 애플리케이션의 DB endpoint는 항상 OpenWrt HAProxy(`192.168.9.1:5432`) 또는 pgcat(`db.svc.cluster.local:6432`)을 통해야 한다. 노드 IP 직접 박지 말 것.
## Discord 통지 파이프라인 (2026-04-08 구축)
## Discord 통지 파이프라인
`agent-qna` 컬렉션에 새 문서가 만들어지면 heimdall Discord 채널(#heimdall, id `1488119168145555486`)에 자동 알림이 뜬다. 다른 컬렉션은 무시한다.
@@ -134,7 +118,7 @@ Outline (documents.create webhook)
- 필터를 통과해야 하는데 0 items면 payload.event 또는 payload.model.collectionId 구조 확인
3. **n8n DB connection timeout**
- n8n pod 재시작: `kubectl -n n8n delete pod -l app.kubernetes.io/name=n8n`
- pgcat → 192.168.9.1:5432 (Patroni leader) 경로 확인. 2026-04-08에 stale connection으로 15h 멈춰있던 사례 있음
- pgcat → 192.168.9.1:5432 (Patroni leader) 경로 확인
4. **Discord 401/403**
- bot token 재발급 필요 (`secret/apps/discord` 갱신, n8n Code 노드의 하드코딩도 함께 갱신)
- bot이 채널에 access 권한 있는지: `GET /channels/{channel_id}` 200이면 OK

View File

@@ -8,7 +8,7 @@ tags: [infra, postgresql, patroni, etcd, ha]
PostgreSQL 3노드 HA 클러스터. Patroni가 자동 failover를 관리하고, etcd를 DCS(Distributed Consensus Store)로 사용.
K3s의 kine 데이터스토어로 사용 중. Supabase Free tier에서 로컬로 이전 완료 (2026-04-05).
K3s의 kine 데이터스토어로 사용 중.
## PostgreSQL 클러스터
@@ -61,7 +61,6 @@ incus exec postgres-1 -- /opt/patroni/bin/patronictl -c /etc/patroni.yml reinit
- jp1: openrc 서비스 (`/etc/init.d/etcd`), `command_background=true`
- mbp: docker container `etcd` (named volume `etcd-data`), client/peer 모두 호스트 `127.0.0.1`만 노출 → `socat`이 Tailscale IP `100.115.154.78``2379`/`2380`으로 forward (`~/Library/LaunchAgents/com.kaffa.etcd-socat{,-peer}.plist`)
- Patroni etcd namespace: `/patroni`
- 2026-04-08: etcd-hp2(10.100.2.214)를 etcd-mbp로 교체. hp2 incus 컨테이너 삭제됨.
### etcd 확인 명령어
@@ -79,7 +78,7 @@ incus exec postgres-1 -- etcdctl --endpoints=http://192.168.9.100:2379,http://10
|--------|------|
| `/patroni` | Patroni DCS (Leader election, 설정) |
| `/apisix/osaka` | APISIX 오사카 라우팅 설정 |
| `/apisix/tokyo` | APISIX sandbox-tokyo 라우팅 설정 (2026-04-08 NixOS 전환 후 미사용, 데이터 보존) |
| `/apisix/tokyo` | APISIX sandbox-tokyo 라우팅 설정 (미사용, 데이터 보존) |
| `/apisix/seoul` | APISIX 서울 K3s 라우팅 설정 |
## K3s kine 연결
@@ -122,7 +121,7 @@ NocoDB, n8n 등 K3s 내부 애플리케이션은 **pgcat**(연결 풀링)을 통
nocodb/n8n → pgcat (db.svc.cluster.local:6432) → HAProxy 192.168.9.1:5432 → Patroni Leader
```
`db/pgcat-config` ConfigMap의 각 풀의 `shards.0.servers`**HAProxy 단일 백엔드만 가리켜야 함** (2026-04-08 변경):
`db/pgcat-config` ConfigMap의 각 풀의 `shards.0.servers`**HAProxy 단일 백엔드만 가리켜야 함**:
```toml
[pools.nocodb.shards.0]
@@ -138,9 +137,7 @@ servers = [["192.168.9.1", 5432, "primary"]]
pgcat는 풀링 전용으로만 쓰고, leader 탐지는 OpenWrt HAProxy에 위임. `query_parser_enabled = false` 설정 (read/write splitting 비활성).
### 2026-04-08 사고 기록
Patroni failover 발생 → pgcat가 옛 primary IP(`10.100.2.5`)를 hardcoded 참조 → nocodb 마이그레이션 시 `cannot execute UPDATE in a read-only transaction` 에러로 4시간 가량 CrashLoopBackOff. n8n은 마이그레이션이 없어서 표면화되지는 않았으나 동일한 잠재 문제 존재. 위의 단일 백엔드 구조로 변경하여 항구 해결.
Patroni failover 인시던트 이력: [[../history/2026-04-08-patroni-failover-incident|2026-04-08 pgcat/nocodb/outline read-only 사고]]
## APISIX etcd 사용 현황
@@ -150,21 +147,7 @@ Patroni failover 발생 → pgcat가 옛 primary IP(`10.100.2.5`)를 hardcoded
| sandbox-tokyo | (미가동) | `/apisix/tokyo` | 2026-04-08 NixOS 전환으로 APISIX 자체 폐기, etcd 데이터만 보존 |
| 서울 K3s | **K3s 내부 apisix-etcd StatefulSet** (apisix.apisix.svc:2379) | `/apisix` | 2026-04-08 외부 통합에서 K3s 내부로 복귀 |
### 2026-04-06 → 2026-04-08 변경 이력
1. **2026-04-06**: 서울 K3s APISIX의 K3s 내부 apisix-etcd StatefulSet을 삭제하고 외부 통합 etcd로 이전 (`/apisix/seoul` prefix). 통합 운영 + 컴포넌트 수 절감 의도.
2. **2026-04-08**: 다시 K3s 내부로 복귀. Patroni DCS와 같은 etcd 클러스터를 공유할 때 장애 전파 위험(Patroni 이슈 → APISIX 라우팅 영향)이 비직관적이라 격리. 외부 통합 etcd의 `/apisix/seoul/*` 20개 키 삭제 완료. **현재 외부 통합 etcd는 Patroni DCS + osaka APISIX 전용.**
### Stale prefix 삭제 명령 (참고)
```bash
# HTTP API v3 사용 (heimdall에서 직접 호출)
KEY=$(echo -n "/apisix/seoul/" | base64)
RANGE_END=$(echo -n "/apisix/seoul0" | base64)
curl -s -X POST "http://192.168.9.100:2379/v3/kv/deleterange" \
-H "Content-Type: application/json" \
-d "{\"key\":\"$KEY\",\"range_end\":\"$RANGE_END\"}"
```
APISIX etcd 통합/분리 이력: [[../history/2026-04-06-apisix-etcd-consolidation|history]]. 현재 외부 통합 etcd는 **Patroni DCS + osaka APISIX 전용**.
## 관련 문서

View File

@@ -25,7 +25,7 @@ Vault root token은 만료 없음 (TTL: 0s)
## 시크릿 구조 (KV v2)
vault.inouter.com(Synology)에서 hcv.inouter.com(K3s)으로 이관 완료 (2026-03-12). 카테고리별 정리:
카테고리별 정리:
| 카테고리 | 경로 | 내용 |
|----------|------|------|

View File

@@ -5,13 +5,11 @@ aliases: [sandbox-tokyo, sandbox-tokyo-nixos]
tags: [nixos, linode, zlambda, infra]
---
> **이름 변경 안내**: 이 노드는 2026-04-08까지 `sandbox-tokyo`로 불렸으나 OS hostname / Tailscale machine name / git 레포가 모두 `zlambda`로 통일되었습니다. 옛 이름 `sandbox-tokyo`는 Mac ssh config 및 본 문서 alias로만 남아 있습니다.
## 개요
Linode Tokyo VM (라벨 `zlambda`, id 47271589)을 2026-04-08 Debian 12 → **NixOS 25.05 (Warbler)** 로 교체. nixos-anywhere로 무관리 원격 설치.
Linode Tokyo VM (라벨 `zlambda`, id 47271589). NixOS 25.05 (Warbler), nixos-anywhere로 설치. 옛 이름 `sandbox-tokyo`는 Mac ssh config alias로만 남아 있음.
이전에 운영하던 [[netbis]] DR APISIX, microsocks, tlsproxy, vault-prod, wg-easy 등은 제거됨. 이후 같은 날 **APISIX + etcd를 NixOS `virtualisation.oci-containers`로 재선언**하여 기동 완료 (`apisix.nix`). 라우트/SSL은 아직 비어 있음.
NixOS 전환 이력: [[../history/2026-04-08-zlambda-nixos-migration|history]]
## 접속
@@ -138,29 +136,8 @@ nix run github:nix-community/nixos-anywhere -- \
root@139.162.71.52
```
## 설치 과정 메모 (2026-04-08)
## 후속 작업
1. **상황**: 누군가 nixos-anywhere를 시도하다가 14시간째 nixos-installer에 멈춰 있었음. 이전 Debian 디스크는 wipe되어 있었고, 원본 데이터는 모두 손실.
2. **첫 시도 실패**: installer 환경에서 sda(512MB)/sdb(50GB) 순서가 뒤바뀜 + 1.9GB RAM에 swap 없이 nixos-install 실행 → OOM-lock으로 SSH banner도 응답 안 함.
3. **회복**: Linode `POST /linode/instances/{id}/rebuild`로 Debian 12 클린 설치 → 디스크 순서 정상화(sda=50G, sdb=512M).
4. **nixos-anywhere 실행**: `--build-on remote --generate-hardware-config`로 자동 진행. 첫 실행에서 disko + grub `mirroredBoots` 중복 오류 → `boot.loader.grub.devices`를 빼고 disko의 자동 설정을 사용하도록 수정 후 재실행 성공.
5. **부팅 안 됨**: Linode 프로필 kernel이 `linode/grub2`(Linode emulated GRUB)였는데, NixOS grub.cfg 경로를 인식 못해서 `grub>` 프롬프트에 멈춤. LISH 콘솔로 확인.
6. **해결**: Configuration profile kernel을 `linode/direct-disk`로 변경 후 reboot → 정상 부팅.
7. **Tailscale**: API key로 ephemeral auth key 발급(`POST /api/v2/tailnet/-/keys`) → `tailscale up --authkey`. 옛 device(`100.79.87.48`)는 offline이라 새 device가 `sandbox-tokyo-1`로 가입됨 → 옛 device 삭제(API DELETE) + rename으로 `sandbox-tokyo` 이름 회수.
## 후속 변경 (2026-04-08, 같은 날)
- **Gitea 리포지토리 [[kaffa/nixos-infra]]** (private) 생성 → `kaffa-macmini ~/nixos-infra/``zlambda /root/nixos-infra/` 양쪽에 clone. zlambda는 deploy key (read-only SSH) 사용.
- **macbookair ed25519 키**(`SHA256:kdYCep0k22+QxnOq...`)를 `users.users.root.openssh.authorizedKeys.keys`에 영구 등록 → macbookair에서 직접 `ssh root@zlambda` 가능.
- **호스트네임 `sandbox-tokyo``zlambda`**: NixOS configuration.nix의 `networking.hostName`, `/etc/hostname`, kernel hostname, Tailscale device name(API rename, deviceId `6511000756301111`) 모두 통일. macbookair `~/.ssh/config``Host zlambda sandbox-tokyo` alias 양쪽 유지.
- **커널 / sysctl 튜닝**: BBR + fq qdisc, conntrack/inotify/file 한도 증가, swappiness 등. 위 "활성화된 모듈/서비스" 섹션 참조.
## 알려진 후속 작업
- [x] Gitea 리포지토리로 푸시 → `kaffa/nixos-infra` (private)
- [x] zlambda에 deploy key 설정 (gitea repo deploy key id 1, fingerprint `SHA256:Amz8LUDKHU59qxyMS48hfoP+KxRE/o6CITfkXzkAFNU`)
- [x] macbookair ed25519 등록 + 호스트네임 zlambda 통일
- [x] 커널/sysctl 튜닝
- [ ] [[vault]] SSH CA에 새 호스트키 등록 (vault.md 참조)
- [ ] 필요 시 [[netbis]] APISIX/etcd Docker compose 재배포
- [ ] 필요 시 [[searxng]]용 tlsproxy/microsocks 재배포

View File

@@ -4,8 +4,6 @@ updated: 2026-04-09
tags: [agent, ops, claude-code]
---
<!-- 2026-04-09: heimdall tofu 재생성. 유저 root→kaffa, project default→ops, IP 10.100.3.92→10.100.3.108 -->
kappa가 혼자 쓰는 **내부 인프라·운영 자동화 Claude Code 에이전트** 집합. 고객 대상 OpenClaw 에이전트(jp1 `agents` 프로젝트, anvil/stamp/flux 등)와는 완전히 분리된 영역. 외부 클라우드 BM 프로비저닝(jp1 `infra-tool` 의 Tofu API)과도 무관.
@@ -15,8 +13,8 @@ kappa가 혼자 쓰는 **내부 인프라·운영 자동화 Claude Code 에이
| 이름 | 호스트 | Incus 프로젝트 | IP | 역할 | 비고 |
|------|--------|---------------|-----|------|------|
| **[[heimdall]]** | kr1 | `ops` | 10.100.3.108 | 인프라 전반 (K3s, Incus, Longhorn, 스토리지, 네트워크, 일반 서비스) | 2026-04-09 tofu 재생성 (`kaffa/ops-agents-tofu/heimdall`) |
| **[[syn]]** | hp2 | `ops` | 10.100.2.173 | 엣지 레이어 전담 (BunnyCDN, SafeLine WAF, APISIX, Cloudflare 엣지) | 2026-04-09 신규, tofu 관리 (`kaffa/ops-agents-tofu/syn`) |
| **[[heimdall]]** | kr1 | `ops` | 10.100.3.108 | 인프라 전반 (K3s, Incus, Longhorn, 스토리지, 네트워크, 일반 서비스) | tofu 관리 (`kaffa/ops-agents-tofu/heimdall`) |
| **[[syn]]** | hp2 | `ops` | 10.100.2.173 | 엣지 레이어 전담 (BunnyCDN, SafeLine WAF, APISIX, Cloudflare 엣지) | tofu 관리 (`kaffa/ops-agents-tofu/syn`) |
## 공통 원칙
@@ -102,7 +100,7 @@ fingerprint: `SHA256:eBCIglGmK/FnDxJLqxT0CJvRGFEGaIKRWnZ3ZpTaugU`
- ASK 한 번에 여러 키를 동시 요청 가능 (같은 작업 범위 내)
- 예외 없음 — bunnycdn/openmemory/nocodb 등 다른 MCP는 그대로 동작
- 환경변수 잔존(`$VAULT_TOKEN`, `$VAULT_ADDR`) 신뢰 금지
- 검증: 2026-04-09 Syn + Heimdall 양쪽에서 e2e 테스트 완료 (DENIED 경로 포함)
- 검증: Syn + Heimdall 양쪽에서 e2e 테스트 완료 (DENIED 경로 포함)
**이점**:
- 기술적 격리 (에이전트가 규칙을 잊거나 혼동해도 Vault 도달 불가)
@@ -118,8 +116,6 @@ fingerprint: `SHA256:eBCIglGmK/FnDxJLqxT0CJvRGFEGaIKRWnZ3ZpTaugU`
### Heimdall
- OpenTofu 관리: [`kaffa/ops-agents-tofu/heimdall`](https://gitea.inouter.com/kaffa/ops-agents-tofu) (2026-04-09 재생성)
- 이전: 2026-03 수동 생성 (root 유저, kr1 default project, IP 10.100.3.92) → 2026-04-09 tofu 재생성 (kaffa 유저, kr1 ops project, IP 10.100.3.108)
- 재생성 시 `~/.claude` (CLAUDE.md, credentials, settings, plugins) 전체 백업/복원
- 변경 플로우: 로컬 clone → `heimdall/` 에서 `tofu plan``tofu apply`
- State: 로컬 `terraform.tfstate` (gitignore, 수동 백업)
- Secrets (`terraform.tfvars`): Vault `secret/apps/gitea` 참조, 커밋 금지
@@ -162,7 +158,7 @@ fingerprint: `SHA256:eBCIglGmK/FnDxJLqxT0CJvRGFEGaIKRWnZ3ZpTaugU`
| heimdall | `heimdall-tmux.service` | `/home/kaffa/heimdall` | `tmux new-session -d -s heimdall "claude"` |
| syn | `syn-tmux.service` | `/home/kaffa/syn` | `tmux new-session -d -s syn "claude --dangerously-skip-permissions"` |
heimdall 은 `--dangerously-skip-permissions` 플래그를 **사용하지 않음**2026-04-09 신규 생성 시 first-run prompt 경쟁조건으로 기동 정지 문제가 있었고 `settings.json` allowlist 로 충분히 대체 가능. syn 은 수동 first-login 단계를 이미 통과한 상태라 플래그 유지.
heimdall 은 `--dangerously-skip-permissions` 플래그를 **사용하지 않음**`settings.json` allowlist 로 대체. syn 은 플래그 유지.
#### tofu repo 워크스페이스 정본
@@ -171,7 +167,7 @@ heimdall 은 `--dangerously-skip-permissions` 플래그를 **사용하지 않음
- `heimdall-workspace/{CLAUDE.md, mcp.json, runbooks/{k3s,incus,longhorn,network,patroni,storage}}``/home/kaffa/heimdall/{CLAUDE.md, .mcp.json, runbooks/}`
- `syn-workspace/{CLAUDE.md, mcp.json, runbooks/{bunnycdn,cloudflare,safeline,apisix}}``/home/kaffa/syn/{CLAUDE.md, .mcp.json, <런북들 top-level>}`
**자동 전개 (2026-04-09 도입)**: cloud-init runcmd 가 부팅 시 ops-agents-tofu repo 를 `git clone --depth=1` 로 가져와 위 매핑대로 `/home/kaffa/<agent>/` 에 복사 + clone 정리. 신규 컨테이너 provisioning 시 manual scp 단계 0.
**자동 전개**: cloud-init runcmd 가 부팅 시 ops-agents-tofu repo 를 `git clone --depth=1` 로 가져와 위 매핑대로 `/home/kaffa/<agent>/` 에 복사 + clone 정리. 신규 컨테이너 provisioning 시 manual scp 단계 0.
라이브 컨테이너에 워크스페이스 변경 반영 시:
- 직접 수정 + repo 에 commit/push (정합성)

View File

@@ -27,9 +27,6 @@ K3s 클러스터에서 Helm 차트(gitea/gitea 12.5.0)로 운영. 네임스페
helm upgrade gitea gitea/gitea -n gitea -f ~/k8s/gitea/values.yaml
```
### 이전 (Synology)
2026-03-15 Synology NAS(192.168.9.100, SQLite)에서 K3s(PostgreSQL)로 이전 완료. Synology 패키지 중지됨 (데이터 보존 중).
## 이미지 레지스트리
@@ -47,9 +44,6 @@ helm upgrade gitea gitea/gitea -n gitea -f ~/k8s/gitea/values.yaml
R2에 저장되는 데이터: packages, lfs, attachments, avatars, repo-avatars, repo-archive, actions_log, actions_artifacts
### BunnyCDN Pull Zone 분리 (2026-03-27)
Gitea는 iron-kr에서 **iron-git** (ID 5584382)으로 분리. 이유: iron-kr의 `BlockNoneReferrer: true`가 git 클라이언트(Referrer 없음)를 차단하여 git push/pull 403 에러 발생. iron-git은 `BlockNoneReferrer: false`로 설정.
## 컨테이너 레지스트리
@@ -103,19 +97,7 @@ K8s CronJob `gitea-backup` (매일 03:00 UTC):
- 7일 보존, Longhorn PVC 10Gi
- 매니페스트: `~/k8s/gitea/backup-cronjob.yaml`
## 도메인 이전 (2026-03-28)
기존 `gitea.anvil.it.com` 도메인을 `gitea.inouter.com`으로 완전 교체.
변경 항목:
- DNS: Cloudflare CNAME → iron-git.b-cdn.net + BunnyCDN Free SSL
- K8s: HTTPRoute, Helm values (DOMAIN, ROOT_URL), CoreDNS 헤어핀 rewrite
- ArgoCD: 4개 앱 repoURL + 8개 repo secret
- Gitea Runner: `.runner`, `.docker/config.json`, `/etc/hosts`
- Mailgun: 기존 `gitea@anvil.it.com``gitea@inouter.com` credential 교체
- 4개 repo CI workflow 이미지 경로 수정
- 컨테이너 이미지 경로: 5개 Deployment
- 로컬 git remote 6개, Obsidian 문서 7개, CLAUDE.md, Vault secret
이전/분리/도메인 변경 이력: [[../history/2026-03-various-gitea-changes|history]]
## 트러블슈팅
@@ -129,4 +111,3 @@ curl -s -X PATCH "https://gitea.inouter.com/api/v1/admin/users/kaffa" \
```
- API 토큰, 비밀번호: Vault `secret/apps/gitea`
- 2026-03-17 웹 로그인 불가 → API로 비밀번호 리셋하여 해결

View File

@@ -11,7 +11,7 @@ Netbis 팀 도메인의 예비(DR) 리버스 프록시 서버. 평소에는 트
기존 Ironclad 인프라([[apisix]], [[crowdsec-safeline]])와는 별도 구성.
> **2026-04-08 상태**: Debian 12 → **NixOS 25.05** 전환 후 APISIX/etcd NixOS `virtualisation.oci-containers`로 재선언하여 기동 완료. 설정은 [[zlambda|kaffa/nixos-infra]] flake의 `apisix.nix` 모듈에 있음. 컨테이너는 구동되지만 **라우트/SSL은 비어 있으므로 DR 역할 부트스트랩은 별도 작업** (acme.sh 재발급 → admin API로 라우트/업스트림/SSL 등록).
APISIX/etcd NixOS `virtualisation.oci-containers`로 기동 완료. 설정은 [[zlambda|kaffa/nixos-infra]] flake의 `apisix.nix` 모듈. **라우트/SSL은 비어 있으므로 DR 역할 부트스트랩은 별도 작업** 필요.
## 서버 정보
@@ -141,7 +141,7 @@ APISIX global_rule로 모든 요청 로그를 CrowdSec(jp1)로 전송.
## Cloudflare 보안 설정
### Rate Limiting (2026-04-05 변경: 600→120/분)
### Rate Limiting (120/분)
| Zone | 제한 | 액션 | 차단시간 |
|------|------|------|---------|
@@ -154,7 +154,7 @@ APISIX global_rule로 모든 요청 로그를 CrowdSec(jp1)로 전송.
정상 사용자 IP당 ~10 req/분 기준, 12배 여유. 공격 IP(230+/분)는 확실히 차단.
### Super Bot Fight Mode (2026-04-03 설정)
### Super Bot Fight Mode
Pro zone 4개(fall-vip.com, psd777.com, rss-555.com, rss-7790.com)에 적용:
- Definitely automated → **managed_challenge**
@@ -167,25 +167,12 @@ Free zone(fall-mvp.com, fall-vip7.com)은 미적용.
기본 활성화 상태 (Cloudflare managed ruleset). 감도는 기본값(Medium).
## 공격 이력
### 2026-03-31 ~ 04-01 대규모 봇 공격
| 도메인 | 3/31 요청 | 4/1 요청 | 출처 |
|--------|----------|---------|------|
| rss-555.com | 3050만 (threats 1700만) | 3000만 (threats 2300만) | JP 99% |
| fall-vip.com | 2560만 (threats 1160만) | 1540만 (threats 1000만) | JP 99% |
| fall-mvp.com | 정상 | 738만 (threats 340만) | JP |
- 일본 IP에서 집중된 L7 DDoS 공격
- Cloudflare가 threat으로 분류했으나 완전 차단하지 않음
- 정상 트래픽 일 130~180만 대비 30배 이상 폭주
- 대응: Rate Limiting + SBFM 사후 설정
### 정상 트래픽 기준 (30일 평균)
## 트래픽 기준
일 평균 약 140만 요청. 월 환산 약 4200만.
공격 이력: [[../history/2026-03-31-netbis-ddos-attack|2026-03-31 대규모 봇 공격]]
## 로그 분석
### 사용 가능
@@ -230,10 +217,10 @@ Workers Paid에 포함. CrowdSec Worker Bouncer 요청 로그를 R2에 저장
- TCP BBR, conntrack 262144, fin_timeout 10s, keepalive 300s, port range 1024-65535
- limits.conf nofile 655360 (Docker 컨테이너 반영은 compose ulimits 추가 필요, 서비스 중이라 미적용)
### NPM-4 추가 튜닝 (2026-04-05)
### NPM-4 추가 튜닝
- 커널: tcp_tw_reuse=1, rmem_max/wmem_max 16MB, tcp_max_tw_buckets 131072, tcp_max_orphans 32768
- Nginx: worker_connections 10240, proxy_buffers 16 32k, keepalive_requests 1000, open_file_cache
- real_ip_header: X-Real-IP → CF-Connecting-IP (컨테이너 내 sed, 재시작 시 초기화 주의)
- real_ip_header: CF-Connecting-IP (컨테이너 내 sed, 재시작 시 초기화 주의)
## 유사시 전환 절차
@@ -241,18 +228,7 @@ Workers Paid에 포함. CrowdSec Worker Bouncer 요청 로그를 R2에 저장
2. APISIX 라우트/SSL 사전 등록 완료 상태이므로 즉시 서비스 가능
3. 전환 후 CrowdSec 로그 수신 및 바운서 차단 자동 동작 확인
## 이전에 운영했던 서비스 (제거됨)
sandbox-tokyo에서 기존 운영하던 아래 서비스는 2026-04-03 중지, 2026-04-08 NixOS 전환으로 제거:
- ~~APISIX 3.15.0 + apisix-etcd (Docker Compose)~~ → **2026-04-08 NixOS oci-containers로 재가동**
- vault-prod (HashiCorp Vault)
- wg-easy (WireGuard VPN)
- nginx-tcp-proxy
- socks5-v4 (microsocks) — [[searxng]]가 사용 중이었음
- tlsproxy
- Caddy (systemd, disabled)
재구성 시 참고: NixOS 모듈로 선언하는 패턴은 `apisix.nix` 참조. 간단한 외부 이미지 컨테이너는 `virtualisation.oci-containers.containers.<name>` 블록 하나 + 필요 시 docker network 생성용 systemd oneshot만 있으면 된다.
이전 서비스 제거 이력: [[../history/2026-04-08-zlambda-nixos-migration|history]]
## 부트스트랩 체크리스트 (재가동 시)