Files
obsidian/infra/network/apisix.md

268 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: APISIX 설정 및 운영
updated: 2026-04-04
---
## 아키텍처
서울 K3s 클러스터 구성: 고객 도메인 → [[cloudflare]](DNS) → OpenWrt HAProxy (80/443) → Traefik MetalLB 192.168.9.53 → K3s 서비스 → pods
오사카 구성: 고객 도메인 → [[cloudflare]](DNS) → [[crowdsec-safeline|SafeLine WAF]] → APISIX(라우팅) → 고객 오리진
3개의 독립 APISIX 인스턴스.
### 서울 relay (relay4wd)
- 용도: 포트 포워딩 게이트웨이
- 호스트: relay4wd (AWS Lightsail, Tailscale 100.103.161.4, 공인 52.79.45.166, Debian 12), Docker APISIX 3.15.0, SSH: admin (포트 2222, Vault CA 인증서)
- Admin API: `http://100.103.161.4:9180` (Tailscale 100.64.0.0/10에서만 접근 가능)
- Admin Key: `edd1c9f034335f136f87ad84b625c8f1`
- etcd: incus-jp1 db 프로젝트 `etcd-1` (10.253.102.11:2379), prefix `/apisix-sandbox`
- 설정 파일: `/opt/apisix/` (config.yaml, docker-compose.yml)
- 모드: stream only (HTTP proxy 비활성화, 9080 미사용)
- 방화벽: 22/tcp + 2201-2299/tcp + 443/tcp 개방, SSH는 Tailscale 경유 포트 2222
- 22 → iptables REDIRECT → 9022 (SFTPGo용, privileged 포트 우회)
- 443 → iptables REDIRECT → 8443 (Teleport용, privileged 포트 우회)
- **주의**: config.yaml의 stream_proxy.tcp에 privileged 포트(1-1023)를 넣으면 비특권 컨테이너에서 bind 실패로 크래시
#### 포트 포워딩 (stream_routes)
| 포트 | 용도 | upstream | 비고 |
|------|------|----------|------|
| 9022 (외부 22) | SFTPGo SFTP | 192.168.9.55:22 | K3s MetalLB → SFTPGo, iptables 22→9022 리다이렉트 |
| 2201 | inbest SSH | 10.100.1.158:22 | inbest 전용 SSH 포트, OpenWrt Tailscale 광고 경유 |
| 2202 | Gitea SSH | 192.168.9.54:22 | K3s MetalLB → Gitea SSH |
| 8443 (외부 443) | Teleport | 192.168.9.52:443 | K3s MetalLB → Teleport proxy, iptables 443→8443 리다이렉트 |
### 오사카 (apisix-osaka)
```
BunnyCDN(iron-jp, ID 5555247) → apisix-osaka(172.233.93.180) → 백엔드
```
- 용도: Ironclad 인프라 서비스 (ironclad.it.com, n8n, twilio 등)
- Docker: APISIX 3.15.0 (waf-apisix) + etcd v3.5.11 (waf-etcd)
- 보안: SafeLine WAF + CrowdSec 연동
- upstream: incus-jp1 내부(10.253.x), K3s Traefik
### 서울 (K3s 새 클러스터, apisix 네임스페이스) — 독립 외부 인입 게이트웨이
```
외부 → OpenWrt HAProxy(:9080/:9443) → MetalLB 192.168.9.50(80/443) → APISIX(replica 2) → K3s 서비스
```
- 용도: **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**
- 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에 없음** — 라우트별 적용. 두 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`.
- 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)
#### plugin_metadata (GatewayProxy CR로 관리)
chaitin-waf 플러그인은 `plugin_attr`(config.yaml)이 아닌 **`plugin_metadata`(etcd)**에서 detector 노드를 읽음. 옛날에는 etcd에 직접 PUT했으나, 2026-04-08부터 ingress controller가 destructive sync로 미관리 객체를 1분마다 삭제하므로 **반드시 GatewayProxy CR의 `spec.pluginMetadata`로 선언**해야 함. helm values의 `gatewayProxy.pluginMetadata` 항목 참고.
#### 등록된 라우트 / TLS (ApisixRoute / ApisixTls CRD)
모든 라우트와 SSL은 K8s CRD로 선언됨. ingress controller가 watch하여 APISIX admin API로 push.
| Kind | Name | Namespace | 설명 |
|------|------|-----------|------|
| ApisixRoute | juiceshop | juiceshop | `juiceshop.keepanker.cv` → juiceshop:3000, plugins.chaitin-waf (block) |
| ApisixTls | wildcard-keepanker-cv | apisix | `*.keepanker.cv` + `keepanker.cv`, secret `wildcard-keepanker-cv-tls` (cert-manager 발급) |
⚠️ **etcd 직접 등록 금지** (1분 이내 controller가 삭제). 모든 신규 객체는 CRD로 선언해야 함.
#### real_ip 설정 (2026-04-08 patch 반영)
⚠️ **helm values로는 반영 안 됨** — APISIX helm chart의 nginx.http.realIpFrom/realIpHeader 옵션이 K3s에 배포된 차트(2.13.0) 구조에서 `nginx_config.http`로 전달이 안 됨. 그래서 `cm/apisix` 직접 patch가 필요하고, helm upgrade 시 reset됨.
현재 적용 값 (cm/apisix `config.yaml`):
```yaml
nginx_config:
http:
real_ip_header: "X-Forwarded-For"
real_ip_from:
- 0.0.0.0/0 # BunnyCDN/OpenWrt/K3s 모든 hop 신뢰 (Tailnet+LAN+CDN 경유)
real_ip_recursive: "on"
access_log_format: '$remote_addr - $remote_user [$time_local] $http_host \"$request\" $status $body_bytes_sent $request_time \"$http_referer\" \"$http_user_agent\" $upstream_addr $upstream_status $upstream_response_time \"$upstream_scheme://$upstream_host$upstream_uri\" xff=\"$http_x_forwarded_for\" xrip=\"$http_x_real_ip\"'
```
→ access log의 첫 필드 `$remote_addr`이 X-Forwarded-For에서 추출한 진짜 클라이언트 IP가 됨. 끝에 `xff="..." xrip="..."` 디버그 필드 추가.
적용 절차:
```bash
kubectl get cm apisix -n apisix -o jsonpath='{.data.config\.yaml}' > /tmp/apisix-config.yaml
# /tmp/apisix-config.yaml 편집 (real_ip_*, access_log_format)
kubectl create cm apisix --from-file=config.yaml=/tmp/apisix-config.yaml -n apisix --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deploy/apisix -n apisix
```
⚠️ **Vector parse_apisix 정규식과 짝**: APISIX log format을 변경할 때마다 `vector` helm values의 parse_apisix 정규식도 같이 업데이트해야 [[victorialogs|VictoriaLogs]]에 구조화 필드가 정상 추출됨. 현재 정규식은 `xff/xrip` 필드를 optional 그룹으로 처리.
#### Ingress Controller 설정
`apisix-ingress-controller` Helm release (chart 1.1.2, controller v2.0.1). GatewayProxy 모드에서 ApisixRoute CRD(v2)도 정상 동작. ApisixRoute에 `ingressClassName: apisix`만 명시하면 controller가 자동으로 admin API에 push.
helm values 핵심:
```yaml
gatewayProxy:
createDefault: true
provider:
type: ControlPlane
controlPlane:
service:
name: apisix-admin
port: 9180
auth:
type: AdminKey
adminKey:
value: "edd1c9f034335f136f87ad84b625c8f1"
config:
disableGatewayAPI: false
kubernetes:
ingressClass: apisix
```
라우팅 전환/복구 이력: [[2026-03-25-apisix-to-traefik-routing|history]]
ApisixRoute 예시 (라우트별 chaitin-waf):
```yaml
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: <name>
namespace: <ns>
spec:
ingressClassName: apisix
http:
- name: rule1
match:
hosts: [<host>]
paths: ["/*"]
backends:
- serviceName: <svc>
servicePort: <port>
plugins:
- name: chaitin-waf
enable: true
config:
mode: block
append_waf_resp_header: true
```
WAF가 문제 시 `plugins` 항목만 빼면 즉시 비활성화됨.
#### Destructive sync 주의사항 (v2.x controller)
새 controller는 **CRD/GatewayProxy로 선언되지 않은 모든 APISIX 객체를 1분마다 자동 삭제**함. 영향:
| 객체 | 관리 방법 |
|------|----------|
| routes | `ApisixRoute` CRD (또는 HTTPRoute) |
| upstreams | `ApisixUpstream` CRD |
| ssls | `ApisixTls` CRD |
| consumers | `ApisixConsumer` CRD |
| plugin_metadata | `GatewayProxy.spec.pluginMetadata` (helm values) |
| global_rules | `GatewayProxy.spec.plugins` (helm values) |
⚠️ **etcd 직접 PUT은 임시 디버깅 외에는 금지.** 1분 안에 삭제됨. 옛 운영 메모에 있는 `etcdctl put` 예제는 모두 deprecated.
#### etcd 설정
K3s 내부 `apisix-etcd` StatefulSet 3 replicas (Bitnami etcd, Longhorn PVC 5Gi x 3, ClusterIP `apisix-etcd.apisix.svc:2379`, prefix `/apisix`).
helm values 핵심:
```yaml
etcd:
enabled: true
replicaCount: 3
persistence:
enabled: true
size: 5Gi
```
업그레이드 시 주의:
- 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 통합/분리 이력: [[2026-04-06-apisix-etcd-consolidation|history]]
### BunnyCDN Pull Zone 매핑 (2026-04-09 API 실측)
| Zone | ID | Origin | 방향 | Hostnames (전수) |
|---|---|---|---|---|
| iron-kr | 5555227 | 220.120.65.245 | → 서울 (OpenWrt → Traefik) | `iron-kr.b-cdn.net`, actions.it.com, n8n.inouter.com, jarvis.inouter.com, telegram-webhook.inouter.com, vault.inouter.com, outline.inouter.com (사용자 6 + 시스템 1 = 7) |
| iron-jp | 5555247 | 172.233.93.180 | → 오사카 (osaka-gw APISIX) | `iron-jp.b-cdn.net`, anvil.it.com, n8n.anvil.it.com, tg.anvil.it.com, linode.actions.it.com (사용자 4 + 시스템 1 = 5) |
| iron-kr-waf | 5555224 | 220.120.65.245:9443 | → 서울 (HAProxy :9443 → K3s APISIX SafeLine WAF) | `iron-kr-waf.b-cdn.net`, juiceshop.keepanker.cv |
| iron-git | 5584382 | 220.120.65.245 | → 서울 (gitea, **미들웨어 미장착**) | `iron-git.b-cdn.net`, gitea.inouter.com |
| i-gate | 5557897 | 172.233.93.180 | (미사용 슬롯) | `i-gate.b-cdn.net` |
참고:
- 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 바이너리 보호 불가).
- gitea.inouter.com 은 더 이상 iron-kr 가 아니라 iron-git 풀존 소속 (분리됨)
### DNS 참고
- `*.inouter.com` 와일드카드 CNAME → `iron-jp.b-cdn.net` (오사카)
- iron-kr zone 호스트는 전용 CNAME 필수 (와일드카드 오버라이드)
- `gitea.inouter.com` CNAME → `iron-kr.b-cdn.net`
- `hcv.inouter.com` CNAME → `k3s.inouter.com` (LAN 직접, BunnyCDN 우회)
- `nocodb.inouter.com` CNAME → `k3s.inouter.com` (LAN 직접, BunnyCDN 우회)
- K3s CoreDNS: `gitea.inouter.com` → Traefik ClusterIP (10.43.205.207) 헤어핀 방지
## ironclad.it.com 라우트
ironclad.it.com Cloudflare DNS origin: 172.233.93.180 (osaka), zone ID: bc8761b398cc52cf731f804bd3cbf388. APISIX 라우트 ironclad-it-com → web 컨테이너 10.253.100.159:80. SSL: Google Trust Services wildcard cert (*.ironclad.it.com) in APISIX. → [[cert-manager]]
## SSL ID 규칙
APISIX SSL ID는 도메인 MD5 해시 앞 16자리
## 플러그인
APISIX 연동: ip-restriction + geoip-restriction 플러그인
## Twilio 라우트
APISIX 라우트 ID: twilio-jp-inouter-com → [[twilio]]
## Gitea POST 변환
[[gitea]]가 POST 미지원(AuthenticateNotImplemented, 404)하므로 APISIX에서 POST body 파라미터를 GET query string으로 변환
## hcv.inouter.com 라우트
APISIX 서울 라우트 hcv-inouter-com → K3s Traefik (192.168.9.134/214/135:443, roundrobin, scheme https). upstream ID: hcv-inouter-com. [[vault]] UI/API 서빙. DNS: k3s.inouter.com CNAME (LAN 직접 연결, BunnyCDN 우회). BunnyCDN pull zone actions (ID 5330178)에 hostname 등록. 오사카에서 서울로 이전 (2026-03-15).
## nocodb.inouter.com 라우트
트래픽 흐름: 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 우회 처리.
## CrowdSec 로그 연동
오사카/서울 양쪽 APISIX → CrowdSec (10.253.100.240:8085) http-logger로 전송. 서울은 global_rule, 오사카는 글로벌 적용.
인증: `auth_header: apisix-crowdsec-log-2024` (주의: `headers` 필드가 아님)
커스텀 파서: custom/apisix-json-logs (403 응답만 필터)
시나리오 매칭으로 반복 공격자 탐지.
과거 트러블슈팅 이력: [[2026-03-15-apisix-git-push-500|2026-03-15 git push 500 에러 + http-logger 401]]
## jarvis.inouter.com 라우트
jarvis.inouter.com → APISIX(오사카) → jarvis(10.100.2.162:18789). OpenClaw 게이트웨이 웹 UI 및 webhook 엔드포인트. enable_websocket: true.
## telegram-webhook.inouter.com 라우트
telegram-webhook.inouter.com → APISIX(오사카) → jarvis(10.100.2.162:8787). 텔레그램 봇 webhook 수신. Cloudflare proxied, *.inouter.com 와일드카드 SSL.