- apisix.md: real_ip_from 0.0.0.0/0, real_ip_header X-Forwarded-For, access_log_format에 xff/xrip 추가. helm values 미반영, cm 직접 patch 패턴 - victorialogs.md: 진짜 IP 기반 LogsQL 예시, parse_apisix 정규식과 APISIX log format이 짝이라는 주의
306 lines
16 KiB
Markdown
306 lines
16 KiB
Markdown
---
|
||
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 인스턴스. kr1의 Docker APISIX는 2026-03-15 제거 완료.
|
||
|
||
### 서울 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 포트 우회)
|
||
- 2026-03-17 AWS EC2에서 Lightsail nano($5/월)로 이전
|
||
- **주의**: config.yaml의 stream_proxy.tcp에 privileged 포트(1-1023)를 넣으면 비특권 컨테이너에서 bind 실패로 크래시. 2026-03-27 포트 22 추가로 장애 발생, 제거하여 복구
|
||
|
||
#### 포트 포워딩 (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(inouter, ID 5316471) → 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 네임스페이스) — SafeLine WAF 전용
|
||
```
|
||
외부 → OpenWrt HAProxy(:9080/:9443) → MetalLB 192.168.9.50(80/443) → APISIX(replica 2) → K3s 서비스
|
||
```
|
||
- 용도: SafeLine WAF 전용 리버스 프록시 (메인 라우팅은 Traefik으로 이전)
|
||
- 클러스터: K3s 새 클러스터 (Supabase PostgreSQL 백엔드, kr2+kr1+hp2)
|
||
- 배포: K3s apisix 네임스페이스, **Deployment replica 2** (2026-04-04 HA 업그레이드)
|
||
- 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가 관리.
|
||
- 서비스: 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).
|
||
- 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 메인 라우팅 역할을 Traefik으로 이전, APISIX는 SafeLine WAF 전용으로 축소
|
||
- 2026-04-08 ApisixRoute CRD 사용을 위해 ingress controller 복구 + K3s 내부 etcd 복귀
|
||
|
||
#### 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 그룹으로 처리.
|
||
|
||
#### 이전 사유 (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 복구 (2026-04-08)
|
||
|
||
`apisix-ingress-controller` Helm release는 살아있었으나 Deployment가 수동 삭제된 상태였음. helm values를 chart 1.1.2 (controller v2.0.1) 스키마에 맞게 재작성 후 `helm upgrade`로 복구.
|
||
|
||
값 수정 핵심:
|
||
```yaml
|
||
gatewayProxy:
|
||
createDefault: true # GatewayProxy CR 자동 생성
|
||
provider:
|
||
type: ControlPlane
|
||
controlPlane:
|
||
service:
|
||
name: apisix-admin
|
||
port: 9180
|
||
auth:
|
||
type: AdminKey
|
||
adminKey:
|
||
value: "edd1c9f034335f136f87ad84b625c8f1"
|
||
config:
|
||
disableGatewayAPI: false # Gateway API + ApisixRoute 양쪽 다 지원
|
||
kubernetes:
|
||
ingressClass: apisix
|
||
```
|
||
|
||
검증 결과: GatewayProxy 모드에서도 ApisixRoute CRD(v2)는 정상 동작함. 옛 메모의 "GatewayProxy 모드에서 ApisixRoute CRD 미지원"은 틀렸음 — 잘못된 helm values 때문이었음. ApisixRoute에 `ingressClassName: apisix`만 명시하면 controller가 자동으로 admin API에 push.
|
||
|
||
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.
|
||
|
||
#### K3s 내부 etcd 복귀 (2026-04-08)
|
||
|
||
기존: 외부 통합 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 관리
|
||
|
||
helm values 핵심:
|
||
```yaml
|
||
etcd:
|
||
enabled: true
|
||
replicaCount: 3
|
||
persistence:
|
||
enabled: true
|
||
size: 5Gi
|
||
```
|
||
|
||
업그레이드 시 주의:
|
||
- 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 명령 참고)
|
||
|
||
### BunnyCDN Pull Zone 매핑
|
||
|
||
| Zone | ID | Origin | 방향 | 주요 Hostnames |
|
||
|---|---|---|---|---|
|
||
| iron-jp | 5555247 | 172.233.93.180 | → 오사카 | inouter.com, n8n.inouter.com, tg.inouter.com, linode.actions.it.com |
|
||
| iron-kr | 5555227 | 220.120.65.245 | → 서울 (Traefik) | actions.it.com, gitea.inouter.com, n8n.inouter.com, jarvis.inouter.com, telegram-webhook.inouter.com, vault.inouter.com |
|
||
| iron-kr-waf | 5555224 | 220.120.65.245:9443 | → 서울 (APISIX WAF) | juiceshop.keepanker.cv |
|
||
|
||
참고: iron-kr zone은 `DisableCookies: false` (쿠키 허용). Gitea 웹 로그인 세션에 필요.
|
||
Edge Script: iron-jp, iron-kr에 CrowdSec bouncer middleware (ID 64811) 연결.
|
||
|
||
### 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 우회 처리 (2026-03-15).
|
||
|
||
## CrowdSec 로그 연동
|
||
|
||
오사카/서울 양쪽 APISIX → CrowdSec (10.253.100.240:8085) http-logger로 전송. 서울은 global_rule, 오사카는 글로벌 적용.
|
||
|
||
인증: `auth_header: apisix-crowdsec-log-2024` (주의: `headers` 필드가 아님)
|
||
|
||
커스텀 파서: custom/apisix-json-logs (403 응답만 필터)
|
||
|
||
시나리오 매칭으로 반복 공격자 탐지.
|
||
|
||
## 트러블슈팅 기록
|
||
|
||
### 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 → 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.
|