Files
obsidian/infra/apisix.md
kappa fb5a34ebcc edge gateway: "APISIX = SafeLine WAF 전용" 표현 정정
kappa 피드백 + kubectl 실측 기반 정정. 이전 기록의 "SafeLine WAF 전용 리버스
프록시" 또는 "SafeLine WAF 전용" 표현은 오해의 소지가 있음.

실측 사실 (2026-04-10 kubectl get svc + ApisixRoute + HTTPRoute):
- APISIX (svc apisix-gateway): MetalLB VIP 192.168.9.50, gateway 80/443 +
  admin 9180. 독립 LoadBalancer. Deployment replica 2.
  ApisixRoute 1건: juiceshop/juiceshop → juiceshop.keepanker.cv → juiceshop
  svc (chaitin-waf plugin 으로 SafeLine 통합 테스트)
- Traefik (svc traefik): MetalLB VIP 192.168.9.53, 80/443.
  HTTPRoute 14건 (argocd/gitea/grafana/n8n/nocodb/sftpgo/openmemory/searxng/
  kroki/safeline-mgt/vault-ui + api/namecheap + api/vultr + bunnycdn-mcp)
  + 5 legacy IngressRoute (vlogs/outline/vault-mcp/vault-mcp-http/bunnycdn-mcp)

→ **두 gateway 는 동등한 병렬 독립 LoadBalancer**. APISIX 는 Traefik 뒤의
리버스 프록시가 아니라 자체 MetalLB VIP 를 가진 별개 외부 인입 채널.
"SafeLine WAF 전용" 이 아니라, 2026-03-25 메인 라우팅이 Traefik 으로 이전된
이후 현재 APISIX route 가 SafeLine 테스트용 1건만 남은 상태일 뿐 — 범용
gateway 로 언제든 새 route 추가 가능.

정정 대상:
- infra/apisix.md — 서울 섹션 헤더/용도/축소 문구
- infra/infra-hosts.md — 게이트웨이 한 줄 요약
- infra/k3s-migration.md — 게이트웨이 열 + Phase 0 + Namespace 표
- infra/gateway-api.md — 전환 이력 2026-03-25 줄
- infra/crowdsec-safeline.md — waf-kr BunnyCDN Pull Zone 섹션 머리말

부수적으로 2026-04-10 에 발견된 heimdall kubectl 부재 문제 해결
(kubectl + helm + kubeconfig 복원) + tofu cloud-init 자동화 추가 —
commit bd5e4cb (ops-agents-tofu).
2026-04-10 00:36:29 +09:00

18 KiB
Raw Blame History

title, updated
title updated
APISIX 설정 및 운영 2026-04-04

아키텍처

서울 K3s 클러스터 구성: 고객 도메인 → [cloudflare] → OpenWrt HAProxy (80/443) → Traefik MetalLB 192.168.9.53 → K3s 서비스 → pods

오사카 구성: 고객 도메인 → [cloudflare]crowdsec-safeline → 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(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 (2026-04-04 HA 업그레이드)
  • APISIX: 3.15.0-ubuntu
  • SafeLine WAF 연동: plugin_attr.chaitin-wafsafeline-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 메인 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로 관리)

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):

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="..." 디버그 필드 추가.

적용 절차:

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에 구조화 필드가 정상 추출됨. 현재 정규식은 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로 복구.

값 수정 핵심:

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):

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 핵심:

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 매핑 (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 IgnoreQueryStrings: true / 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-middlewareiron-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 우회 처리 (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.