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 # 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`.
### 전환 이유 설계 반복 및 모델 벤치마크 이력: [[../history/2026-04-08-anomaly-detect-iterations|history]]
사용자 원래 의도는 "시계열 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.
### 새 아키텍처 ### 새 아키텍처
@@ -78,31 +58,6 @@ LLM 프롬프트가 무시되어도 실수로 사설망이 ban되지 않음.
- OpenRouter key: `secret/ai/openrouter` (`API_KEY` 키) - OpenRouter key: `secret/ai/openrouter` (`API_KEY` 키)
- 컨테이너 배포본: `/etc/anomaly-detect/openrouter.env` (mode 600, systemd `EnvironmentFile=`) - 컨테이너 배포본: `/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 함정 > [!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`로 복구. > `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) - `curl http://10.253.100.240:8080/v1/decisions` → 403 (인증 필요, 네트워크 OK)
- 더미 IP `198.51.100.99`로 alert POST → 201 + decision 등록 → cleanup 확인 (smoke test) - 더미 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 - [ ] **[Medium]** Discord webhook 알림 추가 (`secret/apps/discord` Vault에서 가져오기) + systemd `OnFailure=` drop-in
- [ ] **[Low]** CrowdSec alert `origin``"crowdsec"``"anomaly-detect"`로 태깅 - [ ] **[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(라우팅) → 고객 오리진 오사카 구성: 고객 도메인 → [[cloudflare]](DNS) → [[crowdsec-safeline|SafeLine WAF]] → APISIX(라우팅) → 고객 오리진
3개의 독립 APISIX 인스턴스. kr1의 Docker APISIX는 2026-03-15 제거 완료. 3개의 독립 APISIX 인스턴스.
### 서울 relay (relay4wd) ### 서울 relay (relay4wd)
- 용도: 포트 포워딩 게이트웨이 - 용도: 포트 포워딩 게이트웨이
@@ -22,8 +22,7 @@ updated: 2026-04-04
- 방화벽: 22/tcp + 2201-2299/tcp + 443/tcp 개방, SSH는 Tailscale 경유 포트 2222 - 방화벽: 22/tcp + 2201-2299/tcp + 443/tcp 개방, SSH는 Tailscale 경유 포트 2222
- 22 → iptables REDIRECT → 9022 (SFTPGo용, privileged 포트 우회) - 22 → iptables REDIRECT → 9022 (SFTPGo용, privileged 포트 우회)
- 443 → iptables REDIRECT → 8443 (Teleport용, privileged 포트 우회) - 443 → iptables REDIRECT → 8443 (Teleport용, privileged 포트 우회)
- 2026-03-17 AWS EC2에서 Lightsail nano($5/월)로 이전 - **주의**: config.yaml의 stream_proxy.tcp에 privileged 포트(1-1023)를 넣으면 비특권 컨테이너에서 bind 실패로 크래시
- **주의**: config.yaml의 stream_proxy.tcp에 privileged 포트(1-1023)를 넣으면 비특권 컨테이너에서 bind 실패로 크래시. 2026-03-27 포트 22 추가로 장애 발생, 제거하여 복구
#### 포트 포워딩 (stream_routes) #### 포트 포워딩 (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. - 용도: **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 새 클러스터 (Supabase PostgreSQL 백엔드, kr2+kr1+hp2)
- 배포: K3s apisix 네임스페이스, **Deployment replica 2** (2026-04-04 HA 업그레이드) - 배포: K3s apisix 네임스페이스, **Deployment replica 2**
- APISIX: 3.15.0-ubuntu - APISIX: 3.15.0-ubuntu
- SafeLine WAF 연동: `plugin_attr.chaitin-waf``safeline-detector:8000` (10.43.253.244) - 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) - 서비스: 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) - 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) - 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로 관리) #### 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 그룹으로 처리. ⚠️ **Vector parse_apisix 정규식과 짝**: APISIX log format을 변경할 때마다 `vector` helm values의 parse_apisix 정규식도 같이 업데이트해야 [[victorialogs|VictoriaLogs]]에 구조화 필드가 정상 추출됨. 현재 정규식은 `xff/xrip` 필드를 optional 그룹으로 처리.
#### 이전 사유 (2026-03-25) #### Ingress Controller 설정
- 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 복구 (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 ```yaml
gatewayProxy: gatewayProxy:
createDefault: true # GatewayProxy CR 자동 생성 createDefault: true
provider: provider:
type: ControlPlane type: ControlPlane
controlPlane: controlPlane:
@@ -129,12 +120,12 @@ gatewayProxy:
adminKey: adminKey:
value: "edd1c9f034335f136f87ad84b625c8f1" value: "edd1c9f034335f136f87ad84b625c8f1"
config: config:
disableGatewayAPI: false # Gateway API + ApisixRoute 양쪽 다 지원 disableGatewayAPI: false
kubernetes: kubernetes:
ingressClass: apisix 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): ApisixRoute 예시 (라우트별 chaitin-waf):
```yaml ```yaml
@@ -178,16 +169,9 @@ WAF가 문제 시 `plugins` 항목만 빼면 즉시 비활성화됨.
⚠️ **etcd 직접 PUT은 임시 디버깅 외에는 금지.** 1분 안에 삭제됨. 옛 운영 메모에 있는 `etcdctl put` 예제는 모두 deprecated. ⚠️ **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 x 3, ClusterIP `apisix-etcd.apisix.svc:2379`, prefix `/apisix`).
복귀 후: 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 관리
helm values 핵심: helm values 핵심:
```yaml ```yaml
@@ -200,10 +184,11 @@ etcd:
``` ```
업그레이드 시 주의: 업그레이드 시 주의:
- Bitnami etcd 차트의 pre-upgrade hook은 기존 etcd 멤버 list를 시도함. 멤버가 없는 상황에서는 무한 retry 실패 `helm upgrade --no-hooks` 사용 또는 helm rollback 후 재시도 - Bitnami etcd 차트의 pre-upgrade hook 실패 `helm upgrade --no-hooks` 사용
- 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은 차트가 조용히 무시함. - 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의 stale `/apisix/seoul/*` 키는 수동 삭제 (postgresql-ha.md의 etcd cleanup 명령 참고)
etcd 통합/분리 이력: [[../history/2026-04-06-apisix-etcd-consolidation|history]]
### BunnyCDN Pull Zone 매핑 (2026-04-09 API 실측) ### BunnyCDN Pull Zone 매핑 (2026-04-09 API 실측)
@@ -216,7 +201,7 @@ etcd:
| i-gate | 5557897 | 172.233.93.180 | (미사용 슬롯) | `i-gate.b-cdn.net` | | 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 호환) - iron-kr / iron-jp `BlockNoneReferrer: true`, iron-git 는 `false` (git smart HTTP 호환)
- 모든 zone `EdgeRules: []` (Edge Rules 미사용, 모든 분기는 미들웨어 64811 안) - 모든 zone `EdgeRules: []` (Edge Rules 미사용, 모든 분기는 미들웨어 64811 안)
- Edge Script 64811 `crowdsec-bouncer-middleware` 는 **iron-jp + iron-kr 두 풀존**에만 attach. iron-git 은 의도적 우회 (git pack 바이너리 보호 불가). - 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). 트래픽 흐름: 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 로그 연동 ## CrowdSec 로그 연동
@@ -271,36 +256,7 @@ BunnyCDN WAF가 NocoDB JS를 오탐 차단하여 CDN 우회 처리 (2026-03-15).
시나리오 매칭으로 반복 공격자 탐지. 시나리오 매칭으로 반복 공격자 탐지.
## 트러블슈팅 기록 과거 트러블슈팅 이력: [[../history/2026-03-15-apisix-git-push-500|2026-03-15 git push 500 에러 + http-logger 401]]
### 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"`
## jarvis.inouter.com 라우트 ## jarvis.inouter.com 라우트

View File

@@ -42,10 +42,9 @@ tags: [infra, backup]
### NAS 설정 메모 ### NAS 설정 메모
- Synology sudo PATH 문제: `/etc/sudoers.d/path``secure_path` 추가 (2026-03-17)
- NAS `/volume1/incus/inbest/` 소유자: `kaffa:users` (rsync 쓰기용) - NAS `/volume1/incus/inbest/` 소유자: `kaffa:users` (rsync 쓰기용)
- btrfs subvolume: `/volume1/incus` (ID 741) - btrfs subvolume: `/volume1/incus` (ID 741)
- 모든 백업 스크립트에 NAS 접근 불가 시 스킵/로컬 보관 로직 추가 (2026-04-05) - 모든 백업 스크립트에 NAS 접근 불가 시 스킵/로컬 보관 로직 포함
- NFS 마운트는 반드시 `soft,timeo=50,retrans=3` 사용 (hard 금지, [[nas-storage]] 참조) - 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 마운트) - **출력**: `/mnt/nas-backup/daily/nocodb_YYYYMMDD_HHMMSS.dump` (NAS NFS 마운트)
- **보관**: 30일 초과 자동 삭제 - **보관**: 30일 초과 자동 삭제
- **NAS 마운트**: 스크립트 내에서 `soft,timeo=50,retrans=3`으로 자동 마운트 - **NAS 마운트**: 스크립트 내에서 `soft,timeo=50,retrans=3`으로 자동 마운트
- **NAS 미접근 시**: 10초 타임아웃 후 스킵 (2026-04-05 추가) - **NAS 미접근 시**: 10초 타임아웃 후 스킵
## kine 백업 (Supabase PostgreSQL) ## kine 백업 (Supabase PostgreSQL)
@@ -107,9 +106,9 @@ gunzip kine-YYYYMMDD.sql.gz
psql "$DB_URL" < kine-YYYYMMDD.sql 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 컨테이너) ### 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)로 병렬 운영.
### 전환 이력 전환 이력: [[../history/2026-03-25-apisix-to-traefik-routing|history]]
- 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 추가 가능.
## Traefik 배포 (새 클러스터) ## Traefik 배포
- **DaemonSet** (kube-system 네임스페이스) - **DaemonSet** (kube-system 네임스페이스)
- LoadBalancer 192.168.9.53 (MetalLB, 이전 hostPort 80/443에서 전환) - LoadBalancer 192.168.9.53 (MetalLB)
- Gateway API provider 활성화 - Gateway API provider 활성화
- TLSStore CRD로 와일드카드 인증서 기본 로드 - TLSStore CRD로 와일드카드 인증서 기본 로드
- 와일드카드 인증서: *.inouter.com, *.inouter.com, *.actions.it.com, *.api.inouter.com, *.mcp.inouter.com - 와일드카드 인증서: *.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 엔트리 위험 | | NodeHosts (IP 직접 등록) | 단순 | ClusterIP 변경 시 수동 갱신 필요, stale 엔트리 위험 |
| **rewrite (현재)** | ClusterIP 자동 추종 | HTTPRoute 추가 시 rewrite 규칙도 추가 필요 | | **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 재시작: 새 HTTPRoute 추가 시 `coredns-custom` ConfigMap에 rewrite 규칙 추가 후 CoreDNS 재시작:

View File

@@ -24,7 +24,7 @@ tags: [infra, network, kr-zone, openwrt]
## 서울 K3s 클러스터 ## 서울 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 | | 노드 | 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, 서비스만 잔존) 주요 네임스페이스 (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 통합) 게이트웨이: 두 독립 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 - 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 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 - 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 서비스 - 일반: 외부 → 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 | | vlogs | logging | victoria-logs-single-0.11.31 | v1.49.0 |
| vm-stack | monitoring | victoria-metrics-k8s-stack-0.72.6 | v1.139.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 라이브) ### 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) | | smtp-relay | mail | gitea.inouter.com/kaffa/smtp-relay (path: k8s) |
| vultr-api | api | gitea.inouter.com/kaffa/vultr-api (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 미적용) ### 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) - 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)
> **vaultwarden 컨테이너 제거됨** (이전 정본에는 있었음). 별도 확인 필요.
>
> 신규 추가된 `vault`, `socks5-proxy`, `netbis-cf-bouncer`, `etcd`(default proj)는 Obsidian의 별도 운영 문서가 아직 없을 수 있음 — 필요 시 `infra/` 하위에 추가.
**monitoring 프로젝트** (2): grafana (10.253.103.199), prometheus (10.253.100.193) **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) **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)` 대체) **ops 프로젝트** (1): **heimdall** (10.100.3.108, tofu 관리 `kaffa/ops-agents-tofu/heimdall`)
> 이전 정본의 `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).
### kr2 컨테이너 ### kr2 컨테이너
**default 프로젝트** (2): mariadb-3 (10.100.1.162), **postgres-3** (10.100.1.83) **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) **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 컨테이너 ### 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 프로젝트** (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와 페어 구성을 고려해 만들어둔 빈 프로젝트로 보임. **inbest 프로젝트** (0): 프로젝트만 존재, 인스턴스 없음 (default profile만 사용 중). kr2 inbest와 페어 구성을 고려해 만들어둔 빈 프로젝트로 보임.
## GPU ## 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) - **K3s datastore**: Incus etcd 3노드 클러스터 (192.168.9.214, 192.168.9.135, 192.168.9.134)
- `/registry/` — K3s 클러스터 백엔드 스토어 - `/registry/` — K3s 클러스터 백엔드 스토어
- `/patroni/nocodb-cluster` — NocoDB PostgreSQL HA - `/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 간 오버레이 - **OVN 네트워크**: ovn1 (10.165.246.0/24) — hp2↔kr2 간 오버레이
- **CDN IP 필터**: BunnyCDN + Cloudflare IP만 80/443 허용, 그 외 WAN 차단 - **CDN IP 필터**: BunnyCDN + Cloudflare IP만 80/443 허용, 그 외 WAN 차단
- 스크립트: `/etc/cdn-filter-update.sh` - 스크립트: `/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` - kr2: `/etc/systemd/network/30-usb-2g5.network`
- hp2: `/etc/systemd/network/30-ens2.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) - kr2: GRUB `usbcore.autosuspend=-1`, udev rule `99-usb-ethernet.rules` (scatter-gather off)
- NAS: `/usr/local/etc/rc.d/usb-no-suspend.sh` 스타트업 스크립트 - 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 참조). 재기동 또는 폐기 결정 필요 | | 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 확인 후 폐기 또는 정식 등록 결정 필요 | | 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/` 하위에 분리. 서버가 아닌 kaffa 개인 디바이스(Mac mini / MacBook / iPhone)는 [[workstations]] (`dev/workstations.md`) 참조. 인프라 정본이 아닌 개발 환경 맥락이라 `dev/` 하위에 분리.

View File

@@ -5,134 +5,9 @@ status: 완료
tags: [k3s, migration, postgresql, supabase] tags: [k3s, migration, postgresql, supabase]
--- ---
## 개요 이 문서는 과거 마이그레이션 작업 기록입니다. 전체 내용은 history 파일로 이동했습니다.
기존 K3s 클러스터(외부 etcd)에서 Supabase PostgreSQL 백엔드로 전환하는 프로젝트. - [[../history/2026-03-24-k3s-postgresql-migration|2026-03-24 K3s PostgreSQL 이전]]
2026-03-24~25 완료. - [[../history/2026-04-05-supabase-to-patroni|2026-04-05 Supabase → Patroni 이전]]
## 기존 클러스터 (폐기됨) 현재 클러스터 구성은 [[infra-hosts]] 참조.
| 항목 | 값 |
|------|-----|
| 노드 | 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]] — 백업 체계

View File

@@ -7,7 +7,6 @@ tags: [infra, k3s, metallb, networking]
## 개요 ## 개요
K3s 클러스터에 LoadBalancer 타입 서비스를 제공하는 베어메탈 로드밸런서. K3s 클러스터에 LoadBalancer 타입 서비스를 제공하는 베어메탈 로드밸런서.
NodePort 난립 문제를 해결하기 위해 도입 (2026-03-26).
K3s 내장 ServiceLB(Klipper)는 비활성화 (`--disable servicelb`, kr2/kr1 config.yaml). 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 서비스 목록 kubectl get svc --all-namespaces -o wide | grep LoadBalancer # LB 서비스 목록
``` ```
## 이전 기록 (2026-03-26) NodePort → LoadBalancer 이전 이력: [[../history/2026-03-24-k3s-postgresql-migration|history]] (Phase 5: MetalLB 도입)
| 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 단일 엔드포인트로 변경.

View File

@@ -87,14 +87,12 @@ spec:
- `no_root_squash` 설정으로 root 컨테이너는 소유권 문제 없음 - `no_root_squash` 설정으로 root 컨테이너는 소유권 문제 없음
- 비-root 컨테이너는 Pod `securityContext.fsGroup`으로 제어 - 비-root 컨테이너는 Pod `securityContext.fsGroup`으로 제어
## NFS hard vs soft 교훈 (2026-04-04) ## NFS hard vs soft
kr2에서 NAS NFS가 `hard` 마운트 + NAS 연결 끊김으로 load 1959까지 폭주한 사건 발생. D-state 프로세스(mountpoint, NFS manager)가 커널 전체를 잠식.
- **hard**: NAS 끊기면 무한 대기 → 서버 먹통 - **hard**: NAS 끊기면 무한 대기 → 서버 먹통
- **soft**: 타임아웃 후 에러 반환 → 서버 생존 - **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) ## 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`) - 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로 짧게 질문 → 답변은 여기에 작성)
## 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 실패 → 인증 자체가 깨짐). ## Discord 통지 파이프라인
**복구**: `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 구축)
`agent-qna` 컬렉션에 새 문서가 만들어지면 heimdall Discord 채널(#heimdall, id `1488119168145555486`)에 자동 알림이 뜬다. 다른 컬렉션은 무시한다. `agent-qna` 컬렉션에 새 문서가 만들어지면 heimdall Discord 채널(#heimdall, id `1488119168145555486`)에 자동 알림이 뜬다. 다른 컬렉션은 무시한다.
@@ -134,7 +118,7 @@ Outline (documents.create webhook)
- 필터를 통과해야 하는데 0 items면 payload.event 또는 payload.model.collectionId 구조 확인 - 필터를 통과해야 하는데 0 items면 payload.event 또는 payload.model.collectionId 구조 확인
3. **n8n DB connection timeout** 3. **n8n DB connection timeout**
- n8n pod 재시작: `kubectl -n n8n delete pod -l app.kubernetes.io/name=n8n` - 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** 4. **Discord 401/403**
- bot token 재발급 필요 (`secret/apps/discord` 갱신, n8n Code 노드의 하드코딩도 함께 갱신) - bot token 재발급 필요 (`secret/apps/discord` 갱신, n8n Code 노드의 하드코딩도 함께 갱신)
- bot이 채널에 access 권한 있는지: `GET /channels/{channel_id}` 200이면 OK - 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)로 사용. PostgreSQL 3노드 HA 클러스터. Patroni가 자동 failover를 관리하고, etcd를 DCS(Distributed Consensus Store)로 사용.
K3s의 kine 데이터스토어로 사용 중. Supabase Free tier에서 로컬로 이전 완료 (2026-04-05). K3s의 kine 데이터스토어로 사용 중.
## PostgreSQL 클러스터 ## 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` - 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`) - 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` - Patroni etcd namespace: `/patroni`
- 2026-04-08: etcd-hp2(10.100.2.214)를 etcd-mbp로 교체. hp2 incus 컨테이너 삭제됨.
### etcd 확인 명령어 ### etcd 확인 명령어
@@ -79,7 +78,7 @@ incus exec postgres-1 -- etcdctl --endpoints=http://192.168.9.100:2379,http://10
|--------|------| |--------|------|
| `/patroni` | Patroni DCS (Leader election, 설정) | | `/patroni` | Patroni DCS (Leader election, 설정) |
| `/apisix/osaka` | APISIX 오사카 라우팅 설정 | | `/apisix/osaka` | APISIX 오사카 라우팅 설정 |
| `/apisix/tokyo` | APISIX sandbox-tokyo 라우팅 설정 (2026-04-08 NixOS 전환 후 미사용, 데이터 보존) | | `/apisix/tokyo` | APISIX sandbox-tokyo 라우팅 설정 (미사용, 데이터 보존) |
| `/apisix/seoul` | APISIX 서울 K3s 라우팅 설정 | | `/apisix/seoul` | APISIX 서울 K3s 라우팅 설정 |
## K3s kine 연결 ## 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 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 ```toml
[pools.nocodb.shards.0] [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 비활성). pgcat는 풀링 전용으로만 쓰고, leader 탐지는 OpenWrt HAProxy에 위임. `query_parser_enabled = false` 설정 (read/write splitting 비활성).
### 2026-04-08 사고 기록 Patroni failover 인시던트 이력: [[../history/2026-04-08-patroni-failover-incident|2026-04-08 pgcat/nocodb/outline read-only 사고]]
Patroni failover 발생 → pgcat가 옛 primary IP(`10.100.2.5`)를 hardcoded 참조 → nocodb 마이그레이션 시 `cannot execute UPDATE in a read-only transaction` 에러로 4시간 가량 CrashLoopBackOff. n8n은 마이그레이션이 없어서 표면화되지는 않았으나 동일한 잠재 문제 존재. 위의 단일 백엔드 구조로 변경하여 항구 해결.
## APISIX etcd 사용 현황 ## 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 데이터만 보존 | | 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 내부로 복귀 |
### 2026-04-06 → 2026-04-08 변경 이력 APISIX etcd 통합/분리 이력: [[../history/2026-04-06-apisix-etcd-consolidation|history]]. 현재 외부 통합 etcd는 **Patroni DCS + osaka APISIX 전용**.
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\"}"
```
## 관련 문서 ## 관련 문서

View File

@@ -25,7 +25,7 @@ Vault root token은 만료 없음 (TTL: 0s)
## 시크릿 구조 (KV v2) ## 시크릿 구조 (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] 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 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 참조) - [ ] [[vault]] SSH CA에 새 호스트키 등록 (vault.md 참조)
- [ ] 필요 시 [[netbis]] APISIX/etcd Docker compose 재배포 - [ ] 필요 시 [[netbis]] APISIX/etcd Docker compose 재배포
- [ ] 필요 시 [[searxng]]용 tlsproxy/microsocks 재배포 - [ ] 필요 시 [[searxng]]용 tlsproxy/microsocks 재배포

View File

@@ -4,8 +4,6 @@ updated: 2026-04-09
tags: [agent, ops, claude-code] 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)과도 무관. kappa가 혼자 쓰는 **내부 인프라·운영 자동화 Claude Code 에이전트** 집합. 고객 대상 OpenClaw 에이전트(jp1 `agents` 프로젝트, anvil/stamp/flux 등)와는 완전히 분리된 영역. 외부 클라우드 BM 프로비저닝(jp1 `infra-tool` 의 Tofu API)과도 무관.
@@ -15,8 +13,8 @@ kappa가 혼자 쓰는 **내부 인프라·운영 자동화 Claude Code 에이
| 이름 | 호스트 | Incus 프로젝트 | IP | 역할 | 비고 | | 이름 | 호스트 | Incus 프로젝트 | IP | 역할 | 비고 |
|------|--------|---------------|-----|------|------| |------|--------|---------------|-----|------|------|
| **[[heimdall]]** | kr1 | `ops` | 10.100.3.108 | 인프라 전반 (K3s, Incus, Longhorn, 스토리지, 네트워크, 일반 서비스) | 2026-04-09 tofu 재생성 (`kaffa/ops-agents-tofu/heimdall`) | | **[[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 엣지) | 2026-04-09 신규, tofu 관리 (`kaffa/ops-agents-tofu/syn`) | | **[[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 한 번에 여러 키를 동시 요청 가능 (같은 작업 범위 내) - ASK 한 번에 여러 키를 동시 요청 가능 (같은 작업 범위 내)
- 예외 없음 — bunnycdn/openmemory/nocodb 등 다른 MCP는 그대로 동작 - 예외 없음 — bunnycdn/openmemory/nocodb 등 다른 MCP는 그대로 동작
- 환경변수 잔존(`$VAULT_TOKEN`, `$VAULT_ADDR`) 신뢰 금지 - 환경변수 잔존(`$VAULT_TOKEN`, `$VAULT_ADDR`) 신뢰 금지
- 검증: 2026-04-09 Syn + Heimdall 양쪽에서 e2e 테스트 완료 (DENIED 경로 포함) - 검증: Syn + Heimdall 양쪽에서 e2e 테스트 완료 (DENIED 경로 포함)
**이점**: **이점**:
- 기술적 격리 (에이전트가 규칙을 잊거나 혼동해도 Vault 도달 불가) - 기술적 격리 (에이전트가 규칙을 잊거나 혼동해도 Vault 도달 불가)
@@ -118,8 +116,6 @@ fingerprint: `SHA256:eBCIglGmK/FnDxJLqxT0CJvRGFEGaIKRWnZ3ZpTaugU`
### Heimdall ### Heimdall
- OpenTofu 관리: [`kaffa/ops-agents-tofu/heimdall`](https://gitea.inouter.com/kaffa/ops-agents-tofu) (2026-04-09 재생성) - 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` - 변경 플로우: 로컬 clone → `heimdall/` 에서 `tofu plan``tofu apply`
- State: 로컬 `terraform.tfstate` (gitignore, 수동 백업) - State: 로컬 `terraform.tfstate` (gitignore, 수동 백업)
- Secrets (`terraform.tfvars`): Vault `secret/apps/gitea` 참조, 커밋 금지 - 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"` | | 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"` | | 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 워크스페이스 정본 #### 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/}` - `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>}` - `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 (정합성) - 직접 수정 + 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 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 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 - 7일 보존, Longhorn PVC 10Gi
- 매니페스트: `~/k8s/gitea/backup-cronjob.yaml` - 매니페스트: `~/k8s/gitea/backup-cronjob.yaml`
## 도메인 이전 (2026-03-28) 이전/분리/도메인 변경 이력: [[../history/2026-03-various-gitea-changes|history]]
기존 `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
## 트러블슈팅 ## 트러블슈팅
@@ -129,4 +111,3 @@ curl -s -X PATCH "https://gitea.inouter.com/api/v1/admin/users/kaffa" \
``` ```
- API 토큰, 비밀번호: Vault `secret/apps/gitea` - API 토큰, 비밀번호: Vault `secret/apps/gitea`
- 2026-03-17 웹 로그인 불가 → API로 비밀번호 리셋하여 해결

View File

@@ -11,7 +11,7 @@ Netbis 팀 도메인의 예비(DR) 리버스 프록시 서버. 평소에는 트
기존 Ironclad 인프라([[apisix]], [[crowdsec-safeline]])와는 별도 구성. 기존 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 보안 설정 ## Cloudflare 보안 설정
### Rate Limiting (2026-04-05 변경: 600→120/분) ### Rate Limiting (120/분)
| Zone | 제한 | 액션 | 차단시간 | | Zone | 제한 | 액션 | 차단시간 |
|------|------|------|---------| |------|------|------|---------|
@@ -154,7 +154,7 @@ APISIX global_rule로 모든 요청 로그를 CrowdSec(jp1)로 전송.
정상 사용자 IP당 ~10 req/분 기준, 12배 여유. 공격 IP(230+/분)는 확실히 차단. 정상 사용자 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)에 적용: Pro zone 4개(fall-vip.com, psd777.com, rss-555.com, rss-7790.com)에 적용:
- Definitely automated → **managed_challenge** - Definitely automated → **managed_challenge**
@@ -167,25 +167,12 @@ Free zone(fall-mvp.com, fall-vip7.com)은 미적용.
기본 활성화 상태 (Cloudflare managed ruleset). 감도는 기본값(Medium). 기본 활성화 상태 (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만. 일 평균 약 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 - TCP BBR, conntrack 262144, fin_timeout 10s, keepalive 300s, port range 1024-65535
- limits.conf nofile 655360 (Docker 컨테이너 반영은 compose ulimits 추가 필요, 서비스 중이라 미적용) - 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 - 커널: 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 - 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 사전 등록 완료 상태이므로 즉시 서비스 가능 2. APISIX 라우트/SSL 사전 등록 완료 상태이므로 즉시 서비스 가능
3. 전환 후 CrowdSec 로그 수신 및 바운서 차단 자동 동작 확인 3. 전환 후 CrowdSec 로그 수신 및 바운서 차단 자동 동작 확인
## 이전에 운영했던 서비스 (제거됨) 이전 서비스 제거 이력: [[../history/2026-04-08-zlambda-nixos-migration|history]]
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만 있으면 된다.
## 부트스트랩 체크리스트 (재가동 시) ## 부트스트랩 체크리스트 (재가동 시)