refactor: organize infra/ into compute/network/security/data/platform
This commit is contained in:
193
infra/network/apisix-manual.md
Normal file
193
infra/network/apisix-manual.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: APISIX 운영 매뉴얼
|
||||
updated: 2026-03-15
|
||||
tags: [infra, apisix, manual]
|
||||
---
|
||||
|
||||
## 접속
|
||||
|
||||
### 서울 (incus-hp2)
|
||||
```bash
|
||||
# Admin API (LAN에서 직접 접근, incus proxy device)
|
||||
curl -s http://10.179.99.126:9180/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
|
||||
```
|
||||
|
||||
### 오사카 (apisix-osaka)
|
||||
```bash
|
||||
# Admin API (Tailscale 네트워크에서 직접 접근)
|
||||
curl -s http://100.108.39.107:9180/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
|
||||
```
|
||||
|
||||
## Admin API
|
||||
|
||||
### 라우트 조회/생성/삭제
|
||||
```bash
|
||||
KEY="X-API-KEY: edd1c9f034335f136f87ad84b625c8f1"
|
||||
|
||||
# 전체 조회
|
||||
curl -s http://127.0.0.1:9180/apisix/admin/routes -H "$KEY"
|
||||
|
||||
# 특정 라우트 조회
|
||||
curl -s http://127.0.0.1:9180/apisix/admin/routes/{id} -H "$KEY"
|
||||
|
||||
# 라우트 생성/수정
|
||||
curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/{id} -H "$KEY" \
|
||||
-H 'Content-Type: application/json' -d '{
|
||||
"uri": "/*",
|
||||
"hosts": ["example.com"],
|
||||
"upstream_id": "my-upstream"
|
||||
}'
|
||||
|
||||
# 라우트 삭제
|
||||
curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/{id} -H "$KEY"
|
||||
```
|
||||
|
||||
### Upstream 관리
|
||||
```bash
|
||||
# 전체 조회
|
||||
curl -s http://127.0.0.1:9180/apisix/admin/upstreams -H "$KEY"
|
||||
|
||||
# 생성/수정
|
||||
curl -X PUT http://127.0.0.1:9180/apisix/admin/upstreams/{id} -H "$KEY" \
|
||||
-H 'Content-Type: application/json' -d '{
|
||||
"name": "my-upstream",
|
||||
"type": "roundrobin",
|
||||
"scheme": "https",
|
||||
"pass_host": "pass",
|
||||
"nodes": [
|
||||
{"host": "192.168.9.134", "port": 443, "weight": 1},
|
||||
{"host": "192.168.9.214", "port": 443, "weight": 1}
|
||||
]
|
||||
}'
|
||||
|
||||
# 삭제
|
||||
curl -X DELETE http://127.0.0.1:9180/apisix/admin/upstreams/{id} -H "$KEY"
|
||||
```
|
||||
|
||||
### SSL 인증서 관리
|
||||
```bash
|
||||
# 전체 조회
|
||||
curl -s http://127.0.0.1:9180/apisix/admin/ssls -H "$KEY"
|
||||
|
||||
# 등록 (cert-manager에서 가져올 때)
|
||||
CERT=$(sudo k3s kubectl get secret wildcard-inouter-com-tls -n tools -o jsonpath='{.data.tls\.crt}' | base64 -d)
|
||||
SKEY=$(sudo k3s kubectl get secret wildcard-inouter-com-tls -n tools -o jsonpath='{.data.tls\.key}' | base64 -d)
|
||||
|
||||
curl -X PUT http://127.0.0.1:9180/apisix/admin/ssls/{id} -H "$KEY" \
|
||||
-H 'Content-Type: application/json' -d '{
|
||||
"snis": ["*.inouter.com", "inouter.com"],
|
||||
"cert": "'"$CERT"'",
|
||||
"key": "'"$SKEY"'"
|
||||
}'
|
||||
|
||||
# 삭제
|
||||
curl -X DELETE http://127.0.0.1:9180/apisix/admin/ssls/{id} -H "$KEY"
|
||||
```
|
||||
|
||||
## 로깅
|
||||
|
||||
### 현재 상태
|
||||
access.log/error.log가 `/dev/stdout`, `/dev/stderr`로 심링크 → 파일로 안 남음.
|
||||
|
||||
### file-logger 플러그인 (라우트별 파일 로깅)
|
||||
```bash
|
||||
# 특정 라우트에 file-logger 추가
|
||||
curl -X PATCH http://127.0.0.1:9180/apisix/admin/routes/{id} -H "$KEY" \
|
||||
-H 'Content-Type: application/json' -d '{
|
||||
"plugins": {
|
||||
"file-logger": {
|
||||
"path": "/tmp/apisix/logs/access.log"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 커스텀 로그 포맷 (글로벌)
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:9180/apisix/admin/plugin_metadata/file-logger -H "$KEY" -d '{
|
||||
"log_format": {
|
||||
"host": "$host",
|
||||
"@timestamp": "$time_iso8601",
|
||||
"client_ip": "$remote_addr",
|
||||
"method": "$request_method",
|
||||
"uri": "$request_uri",
|
||||
"status": "$status",
|
||||
"upstream_addr": "$upstream_addr"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### log-rotate 설정 (config.yaml)
|
||||
```yaml
|
||||
plugin_attr:
|
||||
log-rotate:
|
||||
interval: 3600
|
||||
max_kept: 168
|
||||
enable_compression: false
|
||||
```
|
||||
|
||||
### nginx access log 포맷 (config.yaml)
|
||||
현재 설정:
|
||||
```yaml
|
||||
nginx_config:
|
||||
http:
|
||||
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"
|
||||
access_log_format_escape: default
|
||||
```
|
||||
|
||||
## 주요 플러그인
|
||||
|
||||
| 플러그인 | 용도 |
|
||||
|---------|------|
|
||||
| chaitin-waf | SafeLine WAF 연동 |
|
||||
| ip-restriction | IP 차단/허용 |
|
||||
| cors | CORS 설정 |
|
||||
| real-ip | 실제 클라이언트 IP 추출 |
|
||||
| redirect | HTTP→HTTPS 리다이렉트 |
|
||||
| proxy-rewrite | URI/헤더 변환 |
|
||||
| file-logger | 파일 로깅 |
|
||||
| http-logger | HTTP 엔드포인트로 로그 전송 |
|
||||
| log-rotate | 로그 로테이션 |
|
||||
| jwt-auth | JWT 인증 |
|
||||
| basic-auth | Basic 인증 |
|
||||
| limit-req | 요청 제한 |
|
||||
| limit-count | 요청 횟수 제한 |
|
||||
|
||||
### 플러그인 활성화/비활성화
|
||||
```bash
|
||||
# 라우트에 플러그인 추가 (PATCH로 기존 설정 유지)
|
||||
curl -X PATCH http://127.0.0.1:9180/apisix/admin/routes/{id} -H "$KEY" \
|
||||
-H 'Content-Type: application/json' -d '{
|
||||
"plugins": {
|
||||
"ip-restriction": {
|
||||
"whitelist": ["192.168.9.0/24"]
|
||||
}
|
||||
}
|
||||
}'
|
||||
|
||||
# 플러그인 제거
|
||||
curl -X PATCH http://127.0.0.1:9180/apisix/admin/routes/{id} -H "$KEY" \
|
||||
-H 'Content-Type: application/json' -d '{
|
||||
"plugins": {
|
||||
"ip-restriction": null
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 설정 파일
|
||||
|
||||
- config.yaml: `/usr/local/apisix/conf/config.yaml`
|
||||
- 설정 변경 후: `apisix reload` (컨테이너 내부)
|
||||
|
||||
## SSL ID 규칙
|
||||
|
||||
도메인 MD5 해시 앞 16자리 사용.
|
||||
|
||||
## 참고
|
||||
|
||||
- [[apisix]] — 아키텍처 및 라우트 현황
|
||||
- [[cert-manager]] — 인증서 자동 갱신
|
||||
267
infra/network/apisix.md
Normal file
267
infra/network/apisix.md
Normal file
@@ -0,0 +1,267 @@
|
||||
---
|
||||
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.
|
||||
117
infra/network/gateway-api.md
Normal file
117
infra/network/gateway-api.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: K3s Gateway API — Traefik 메인 라우팅
|
||||
updated: 2026-03-25
|
||||
tags: [k3s, traefik, gateway-api]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
K3s 메인 라우팅을 Traefik이 담당. APISIX는 독립 LoadBalancer(MetalLB VIP 192.168.9.50)로 병렬 운영.
|
||||
|
||||
전환 이력: [[2026-03-25-apisix-to-traefik-routing|history]]
|
||||
|
||||
## Traefik 배포
|
||||
|
||||
- **DaemonSet** (kube-system 네임스페이스)
|
||||
- LoadBalancer 192.168.9.53 (MetalLB)
|
||||
- Gateway API provider 활성화
|
||||
- TLSStore CRD로 와일드카드 인증서 기본 로드
|
||||
- 와일드카드 인증서: *.inouter.com, *.inouter.com, *.actions.it.com, *.api.inouter.com, *.mcp.inouter.com
|
||||
|
||||
## Gateway
|
||||
|
||||
- GatewayClass: `traefik` (traefik.io/gateway-controller)
|
||||
- Gateway: `traefik-gateway` (kube-system)
|
||||
- Listeners: web (HTTP 80) + websecure (HTTPS 443)
|
||||
- TLS: TLSStore로 기본 인증서, Gateway listener에서 추가 인증서 참조
|
||||
- ReferenceGrant: cert-manager → kube-system (Secret 참조 허용)
|
||||
|
||||
## 서비스 상태 (2026-03-25 전환 완료)
|
||||
|
||||
| 서비스 | 도메인 | 상태 |
|
||||
|--------|--------|------|
|
||||
| NocoDB | nocodb.inouter.com | 200 ✅ |
|
||||
| Gitea | gitea.inouter.com | 200 ✅ |
|
||||
| n8n | n8n.inouter.com | 200 ✅ |
|
||||
| ArgoCD | argocd.inouter.com | 200 ✅ |
|
||||
| Grafana | grafana.inouter.com | 302 ✅ |
|
||||
| SearXNG | searxng.inouter.com | 200 ✅ |
|
||||
| SafeLine | safeline.inouter.com | 200 ✅ |
|
||||
| Namecheap API | namecheap.api.inouter.com | 403 ✅ |
|
||||
| Vultr API | vultr.api.inouter.com | 403 ✅ |
|
||||
| OpenMemory | mem0.inouter.com | MCP 서버 (웹 응답 없음) |
|
||||
| BunnyCDN MCP | bunny.inouter.com | MCP 서버 (웹 응답 없음) |
|
||||
| Gitea Runner | declare successfully ✅ | Tailscale 직접 접속, 와일드카드 인증서 정상 |
|
||||
|
||||
## HTTPRoute 목록
|
||||
|
||||
| Namespace | Name | Hosts | Port |
|
||||
|-----------|------|-------|------|
|
||||
| anvil | nginx-anvil | anvil.local, gnu.inouter.com | 80 |
|
||||
| argocd | argocd-server | argocd.inouter.com | 80 (insecure 모드) |
|
||||
| ironclad | nginx-ironclad | ironclad.local | 80 |
|
||||
| openmemory | openmemory-mcp | mem0.inouter.com | 8765 |
|
||||
| searxng | searxng | searxng.inouter.com | 8080 |
|
||||
| tools | cloud-api-emulator | emul.actions.it.com | 3000 |
|
||||
| tools | cloud-api-linode | linode.actions.it.com | 3001 |
|
||||
| tools | cloud-api-vultr | vultr.actions.it.com | 3002 |
|
||||
| tools | n8n | n8n.inouter.com | 5678 |
|
||||
| tools | namecheap-api | namecheap.api.inouter.com | 80 |
|
||||
| tools | nocodb | nocodb.inouter.com | 8080 |
|
||||
| tools | vultr-api | vultr.api.inouter.com | 80 |
|
||||
| vault | vault-mcp | hcv.inouter.com (/mcp) | 8080 |
|
||||
| vault | vault-ui | hcv.inouter.com (/) | 8200 |
|
||||
|
||||
## ArgoCD 변경사항
|
||||
|
||||
argocd-server를 insecure 모드로 변경 (configmap `argocd-cmd-params-cm`에 `server.insecure: "true"`). TLS 종료를 Gateway에서 처리.
|
||||
|
||||
## 클러스터 내부 DNS (헤어핀 방지)
|
||||
|
||||
Pod에서 외부 도메인(`gitea.inouter.com` 등)에 접근할 때 트래픽이 외부로 나갔다 돌아오는 헤어핀 NAT 문제를 방지하기 위해, CoreDNS rewrite로 서비스 도메인을 Traefik ClusterIP로 직접 해석.
|
||||
|
||||
### 설정
|
||||
|
||||
ConfigMap `coredns-custom` (kube-system)의 `hairpin.override`:
|
||||
|
||||
```
|
||||
rewrite name gitea.inouter.com traefik.kube-system.svc.cluster.local
|
||||
rewrite name argocd.inouter.com traefik.kube-system.svc.cluster.local
|
||||
rewrite name searxng.inouter.com traefik.kube-system.svc.cluster.local
|
||||
rewrite name mem0.inouter.com traefik.kube-system.svc.cluster.local
|
||||
rewrite name nocodb.inouter.com traefik.kube-system.svc.cluster.local
|
||||
rewrite name n8n.inouter.com traefik.kube-system.svc.cluster.local
|
||||
rewrite name hcv.inouter.com traefik.kube-system.svc.cluster.local
|
||||
...
|
||||
```
|
||||
|
||||
### 동작
|
||||
|
||||
1. Pod이 `gitea.inouter.com` DNS 조회
|
||||
2. CoreDNS가 `traefik.kube-system.svc.cluster.local`로 rewrite
|
||||
3. Traefik ClusterIP 반환 → 클러스터 내부에서 처리
|
||||
4. Traefik이 HTTPRoute 호스트 매칭으로 백엔드 라우팅
|
||||
|
||||
### 이전 방식과 차이
|
||||
|
||||
| 방식 | 장점 | 단점 |
|
||||
|------|------|------|
|
||||
| NodeHosts (IP 직접 등록) | 단순 | ClusterIP 변경 시 수동 갱신 필요, stale 엔트리 위험 |
|
||||
| **rewrite (현재)** | ClusterIP 자동 추종 | HTTPRoute 추가 시 rewrite 규칙도 추가 필요 |
|
||||
|
||||
### 유지보수
|
||||
|
||||
새 HTTPRoute 추가 시 `coredns-custom` ConfigMap에 rewrite 규칙 추가 후 CoreDNS 재시작:
|
||||
|
||||
```bash
|
||||
kubectl edit configmap coredns-custom -n kube-system
|
||||
kubectl rollout restart deployment coredns -n kube-system
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
- Gateway/GatewayClass를 수동 생성하면 Helm upgrade 실패 (ownership metadata 충돌)
|
||||
- HelmChartConfig로만 설정할 것
|
||||
- `namespacePolicy.from: All` 필수 (기본값은 Same)
|
||||
- ArgoCD가 관리하는 Ingress를 삭제하면 재생성될 수 있으니 Application 확인 필요
|
||||
- HTTPRoute 추가 시 `coredns-custom` hairpin rewrite도 함께 추가할 것
|
||||
84
infra/network/k3s-ingress-architecture.md
Normal file
84
infra/network/k3s-ingress-architecture.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: K3s 인그레스 아키텍처
|
||||
updated: 2026-04-13
|
||||
tags: [infra, k3s, traefik, apisix, safeline, ingress]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
K3s 클러스터에 **2개의 독립 Ingress Controller**가 병렬 운영됨. 역할에 따라 트래픽 경로가 분리됨.
|
||||
|
||||
| Ingress Controller | MetalLB VIP | 용도 | CRD |
|
||||
|---|---|---|---|
|
||||
| **Traefik** | 192.168.9.53 | 내부/관리 서비스 (*.inouter.com) | `IngressRoute` (traefik.io/v1alpha1) |
|
||||
| **APISIX** | 192.168.9.50 | WAF 보호 필요한 외부 노출 서비스 | `ApisixRoute` (apisix.apache.org/v2) |
|
||||
|
||||
## 트래픽 흐름
|
||||
|
||||
### Traefik 경로 (내부/관리)
|
||||
|
||||
```
|
||||
클라이언트 → BunnyCDN (iron-kr) → OpenWrt HAProxy (:80/:443)
|
||||
→ MetalLB 192.168.9.53 → Traefik → K3s Service → Pod
|
||||
```
|
||||
|
||||
대상: outline, vault, gitea, portainer, argocd, vector, vlogs 등 inouter.com 도메인 서비스
|
||||
|
||||
### APISIX 경로 (SafeLine WAF 경유)
|
||||
|
||||
```
|
||||
클라이언트 → BunnyCDN (iron-kr-waf) → OpenWrt HAProxy (:9080/:9443)
|
||||
→ MetalLB 192.168.9.50 → APISIX → SafeLine WAF (chaitin-waf 플러그인)
|
||||
→ K3s Service → Pod
|
||||
```
|
||||
|
||||
대상: juiceshop.keepanker.cv 등 외부 도메인 서비스. WAF 보호가 필요한 서비스는 APISIX 경로로 라우팅.
|
||||
|
||||
## APISIX + SafeLine 연동 구조
|
||||
|
||||
APISIX 라우트에 `chaitin-waf` 플러그인을 라우트별로 적용. SafeLine detector가 요청을 검사하고 차단/허용 판단.
|
||||
|
||||
```
|
||||
APISIX Pod → safeline-detector:8000 (K3s ClusterIP 10.43.253.244)
|
||||
→ 판단: 허용 → 백엔드 서비스로 전달
|
||||
→ 판단: 차단 → APISIX가 403 반환
|
||||
```
|
||||
|
||||
- SafeLine tengine(리버스 프록시)은 replica 0으로 비활성화 — APISIX가 리버스 프록시 역할 대체
|
||||
- detector만 가동하여 순수 WAF 엔진으로 사용
|
||||
- global_rule이 아닌 **라우트별 플러그인**으로 적용 (`plugins.chaitin-waf`)
|
||||
- SafeLine 관리 UI: `safeline-mgt:1443`
|
||||
|
||||
## 인그레스 리소스 유형 (통일 규칙)
|
||||
|
||||
| 유형 | 사용 여부 | 비고 |
|
||||
|------|-----------|------|
|
||||
| `IngressRoute` (Traefik CRD) | ✅ 사용 | Traefik 경로 서비스 |
|
||||
| `ApisixRoute` (APISIX CRD) | ✅ 사용 | APISIX/WAF 경로 서비스 |
|
||||
| `Ingress` (표준 K8s) | ❌ 사용 금지 | 통일을 위해 CRD로 전환. ArgoCD가 마지막 Ingress였으나 IngressRoute로 전환 완료 |
|
||||
|
||||
## 구성 요소 상세
|
||||
|
||||
### Traefik
|
||||
- K3s 기본 내장 ingress controller
|
||||
- DaemonSet (3노드)
|
||||
- TLS: cert-manager wildcard 인증서 (`wildcard-inouter-tls` 등)
|
||||
- 설정: [[k3s-migration]] 참조
|
||||
|
||||
### APISIX (K3s 내부)
|
||||
- Deployment replica 2, etcd StatefulSet 3 replica
|
||||
- Ingress Controller: `apisix-ingress-controller` (GatewayProxy 모드)
|
||||
- global_rules: `http-logger` (CrowdSec) + `limit-req` (rate 20, burst 10)
|
||||
- 상세: [[apisix]] 참조
|
||||
|
||||
### SafeLine WAF
|
||||
- detector + database + mgt + luigi + chaos + fvm
|
||||
- tengine는 replica 0 (APISIX가 대체)
|
||||
- 상세: [[crowdsec-safeline]] 참조
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[apisix]] — APISIX 전체 설정 (서울/오사카/relay)
|
||||
- [[crowdsec-safeline]] — CrowdSec LAPI + SafeLine WAF
|
||||
- [[cert-manager]] — TLS 인증서 관리
|
||||
- [[metallb]] — MetalLB LoadBalancer IP 할당
|
||||
68
infra/network/metallb.md
Normal file
68
infra/network/metallb.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: MetalLB (K3s LoadBalancer)
|
||||
updated: 2026-03-26
|
||||
tags: [infra, k3s, metallb, networking]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
K3s 클러스터에 LoadBalancer 타입 서비스를 제공하는 베어메탈 로드밸런서.
|
||||
K3s 내장 ServiceLB(Klipper)는 비활성화 (`--disable servicelb`, kr2/kr1 config.yaml).
|
||||
|
||||
## 배포 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Namespace | metallb-system |
|
||||
| Chart | metallb/metallb |
|
||||
| Helm repo | https://metallb.github.io/metallb |
|
||||
| 모드 | L2 Advertisement |
|
||||
| IP 풀 | 192.168.9.50 - 192.168.9.59 (10개) |
|
||||
| Speaker | DaemonSet (노드당 1개, 3개) |
|
||||
| Controller | Deployment (1개) |
|
||||
|
||||
## IP 할당 현황
|
||||
|
||||
| IP | Service | Namespace | Port |
|
||||
|----|---------|-----------|------|
|
||||
| 192.168.9.50 | apisix-gateway | apisix | 80, 443 |
|
||||
| 192.168.9.51 | sshpiper | sshpiper | 2222 |
|
||||
| 192.168.9.52 | teleport-cluster | teleport | 443 |
|
||||
| 192.168.9.53 | traefik | kube-system | 80, 443 |
|
||||
|
||||
## DNS 매핑
|
||||
|
||||
- `k3s.inouter.com` → 192.168.9.53 (Traefik LB, 이전 3노드 IP에서 변경)
|
||||
- `teleport.inouter.com` → 52.79.45.166 (relay4wd 경유)
|
||||
|
||||
## 설정
|
||||
|
||||
```yaml
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: IPAddressPool
|
||||
metadata:
|
||||
name: default-pool
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- 192.168.9.50-192.168.9.59
|
||||
---
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: L2Advertisement
|
||||
metadata:
|
||||
name: default
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
ipAddressPools:
|
||||
- default-pool
|
||||
```
|
||||
|
||||
## 관리 명령
|
||||
|
||||
```bash
|
||||
kubectl get ipaddresspool -n metallb-system # IP 풀 확인
|
||||
kubectl get l2advertisement -n metallb-system # L2 광고 확인
|
||||
kubectl get svc --all-namespaces -o wide | grep LoadBalancer # LB 서비스 목록
|
||||
```
|
||||
|
||||
NodePort → LoadBalancer 이전 이력: [[2026-03-24-k3s-postgresql-migration|history]] (Phase 5: MetalLB 도입)
|
||||
108
infra/network/openwrt.md
Normal file
108
infra/network/openwrt.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: OpenWrt 라우터 (서울)
|
||||
updated: 2026-03-25
|
||||
tags: [openwrt, haproxy, nftables, firewall]
|
||||
---
|
||||
|
||||
## 호스트 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| SSH | `root@100.66.60.66` (openwrt-gw) |
|
||||
| 모델 | FUJITSU FMVC04004 |
|
||||
| CPU | AMD GX-222GC 2코어 2GHz |
|
||||
| RAM | 3.6GB |
|
||||
| NIC | 1Gbps x2 (eth0: br-lan, eth1: WAN) |
|
||||
| OS | OpenWrt (x86_64, 커널 6.12) |
|
||||
|
||||
## HAProxy
|
||||
|
||||
설정 파일: `/etc/haproxy.cfg`
|
||||
stats: `:9999` (admin/admin)
|
||||
maxconn: 20000
|
||||
|
||||
### 포트 매핑
|
||||
|
||||
| Frontend | Port | Backend | 대상 | 용도 |
|
||||
|----------|------|---------|------|------|
|
||||
| ft_http | :80 | bk_traefik_http | MetalLB 192.168.9.53:80 | Traefik 메인 라우팅 |
|
||||
| ft_https | :443 | bk_traefik_https | MetalLB 192.168.9.53:443 | Traefik 메인 라우팅 |
|
||||
| ft_apisix_http | :9080 | bk_apisix_http | MetalLB 192.168.9.50:80 | APISIX SafeLine WAF |
|
||||
| ft_apisix_https | :9443 | bk_apisix_https | MetalLB 192.168.9.50:443 | APISIX SafeLine WAF |
|
||||
|
||||
백엔드: 전부 MetalLB IP 단일 엔드포인트 (2026-03-26 이전 완료, 이전에는 3노드 roundrobin)
|
||||
|
||||
## nftables 방화벽
|
||||
|
||||
### CDN IP 필터 (`/etc/nftables.d/10-cdn-filter.nft`)
|
||||
|
||||
named set 방식으로 BunnyCDN + Cloudflare IP를 관리. 80/443/9080/9443 포트에 대해 CDN IP만 허용, WAN 직접 접근 차단.
|
||||
|
||||
```
|
||||
set cdn_ipv4 { type ipv4_addr; flags interval; elements = { BunnyCDN IPs + Cloudflare IPv4 CIDRs } }
|
||||
set cdn_ipv6 { type ipv6_addr; flags interval; elements = { Cloudflare IPv6 CIDRs } }
|
||||
|
||||
chain cdn_filter {
|
||||
tcp dport { 80, 443, 9080, 9443 } ip saddr @cdn_ipv4 accept
|
||||
udp dport 443 ip saddr @cdn_ipv4 accept
|
||||
tcp dport { 80, 443, 9080, 9443 } ip6 saddr @cdn_ipv6 accept
|
||||
udp dport 443 ip6 saddr @cdn_ipv6 accept
|
||||
tcp dport { 80, 443, 9080, 9443 } iifname "eth1" drop
|
||||
udp dport 443 iifname "eth1" drop
|
||||
}
|
||||
```
|
||||
|
||||
### CDN IP 갱신 스크립트 (`/etc/cdn-filter-update.sh`)
|
||||
|
||||
BunnyCDN API + Cloudflare IP 목록을 다운로드하여 named set 재생성 → `fw4 restart`. 수동 실행: `sh /etc/cdn-filter-update.sh`
|
||||
|
||||
### QUIC 로드밸런싱 (`table ip quic_lb`)
|
||||
|
||||
nftables DNAT으로 UDP 443을 3노드 APISIX :9443에 round-robin 분배.
|
||||
|
||||
### NAT Reflection (`dstnat_lan`)
|
||||
|
||||
LAN(192.168.1.0/24, 192.168.9.0/24)에서 공인IP(220.120.65.245)로 접근 시 192.168.9.1(라우터)로 DNAT → HAProxy 경유.
|
||||
|
||||
### WAN DNAT (`dstnat_wan`)
|
||||
|
||||
WAN TCP 80/443 → 192.168.9.1:80/443 (HAProxy)로 DNAT.
|
||||
|
||||
## 백업
|
||||
|
||||
- **스크립트**: `/usr/local/bin/backup-openwrt.sh`
|
||||
- **스케줄**: cron 매일 03:30
|
||||
- **방식**: `sysupgrade -b` → scp → NAS
|
||||
- **NAS 경로**: `kaffa@192.168.9.100:/volume1/k3s-backup/openwrt/`
|
||||
- **SSH 키**: `/root/.ssh/id_ed25519`
|
||||
- **보관**: 7일 초과 자동 삭제
|
||||
- **크기**: ~18KB
|
||||
- **복원**: `sysupgrade -r backup.tar.gz`
|
||||
- **포함**: `/etc/` 전체 (haproxy.cfg, nftables.d/, config/, crontabs/, ssh 키 등)
|
||||
- **R2 연동**: NAS `/volume1/k3s-backup/` → R2 `k3s-backup` 버킷 (기존 r2-backup.timer로 자동 포함)
|
||||
|
||||
## cron 작업
|
||||
|
||||
| 스케줄 | 스크립트 | 용도 |
|
||||
|--------|----------|------|
|
||||
| 03:30 | `/usr/local/bin/backup-openwrt.sh` | 설정 백업 → NAS |
|
||||
| 04:00 | `/etc/cdn-filter-update.sh` | BunnyCDN+Cloudflare IP 갱신 |
|
||||
|
||||
## WAN DNAT (fw4)
|
||||
|
||||
`/etc/config/firewall`의 redirect 규칙. firewall restart 시 `dstnat_wan` 체인에 반영.
|
||||
|
||||
| Name | Port | 대상 | 비고 |
|
||||
|------|------|------|------|
|
||||
| APISIX-HTTP | :80 | 192.168.9.1:80 | Traefik 메인 |
|
||||
| APISIX-HTTPS | :443 | 192.168.9.1:443 | Traefik 메인 |
|
||||
| APISIX-WAF-HTTP | :9080 | 192.168.9.1:9080 | APISIX WAF |
|
||||
| APISIX-WAF-HTTPS | :9443 | 192.168.9.1:9443 | APISIX WAF |
|
||||
|
||||
DNAT 없으면 `input_wan` → `reject_from_wan`으로 차단됨. DNAT이 있어야 `ct status dnat accept`로 통과.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[infra-hosts]] — 서버 목록
|
||||
- [[apisix]] — APISIX 설정
|
||||
- [[gateway-api]] — Traefik Gateway API
|
||||
138
infra/network/smtp-relay.md
Normal file
138
infra/network/smtp-relay.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: smtp-relay (K3s 내부 SMTP 게이트웨이 via Mailgun)
|
||||
updated: 2026-04-08
|
||||
tags: [k3s, mail, postfix, mailgun, smtp, relay]
|
||||
---
|
||||
|
||||
> 코드: `gitea.inouter.com/kaffa/smtp-relay` (private)
|
||||
> ArgoCD App: `argocd/smtp-relay` (auto-sync + self-heal)
|
||||
|
||||
# smtp-relay
|
||||
|
||||
K3s 클러스터 내부 애플리케이션이 표준 SMTP로 메일을 보내면 [[mailgun|Mailgun]]을 통해 실제 발송되는 릴레이. [[k3s-backup|K3s]] 안에서만 접근 가능.
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
K3s 앱 (pod, 임의 네임스페이스)
|
||||
└─> smtp-relay.mail.svc.cluster.local:25
|
||||
└─> smtp.mailgun.org:587 (SASL auth + STARTTLS)
|
||||
```
|
||||
|
||||
## 배치
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 클러스터 | incus-kr1/kr2/hp2 단일 K3s (v1.34.5) |
|
||||
| Namespace | `mail` |
|
||||
| Deployment | `smtp-relay` 1 replica, strategy=Recreate |
|
||||
| 이미지 | `boky/postfix:v4.2.0-alpine` ([github](https://github.com/bokysan/docker-postfix)) |
|
||||
| Service | ClusterIP, port 25 → 587, port 587 → 587 |
|
||||
| Resources | req 20m CPU / 64Mi, limit 200m / 256Mi |
|
||||
| Probe | TCP 587 readiness + liveness |
|
||||
|
||||
## DNS / 접근
|
||||
|
||||
| 용도 | 주소 |
|
||||
|------|------|
|
||||
| 내부 앱 | `smtp-relay.mail.svc.cluster.local:25` |
|
||||
| 같은 네임스페이스(`mail`) | `smtp-relay:25` |
|
||||
| 외부 / incus 컨테이너 | **접근 불가** (ClusterIP only) |
|
||||
|
||||
외부 접근이 필요해지면 NodePort 또는 Gateway TCP route 추가 고려.
|
||||
|
||||
## Secret 처리
|
||||
|
||||
**Mailgun SMTP 인증 정보는 Gitea 저장소에 커밋하지 않는다.** Vault `secret/messaging/mailgun/smtp` 에서 읽어 클러스터에 out-of-band로 apply:
|
||||
|
||||
```bash
|
||||
USERNAME=$(vault kv get -field=username secret/messaging/mailgun/smtp)
|
||||
PASSWORD=$(vault kv get -field=password secret/messaging/mailgun/smtp)
|
||||
|
||||
kubectl -n mail create secret generic smtp-relay-mailgun \
|
||||
--from-literal=username="$USERNAME" \
|
||||
--from-literal=password="$PASSWORD" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
|
||||
Secret 회전 시 동일 명령 재실행 후 `kubectl -n mail rollout restart deploy smtp-relay`.
|
||||
|
||||
## ConfigMap (`smtp-relay-config`)
|
||||
|
||||
postfix 동작을 결정하는 주요 환경변수:
|
||||
|
||||
| 변수 | 값 | 의미 |
|
||||
|------|-----|------|
|
||||
| `RELAYHOST` | `[smtp.mailgun.org]:587` | 상위 릴레이 |
|
||||
| `RELAYHOST_TLS_LEVEL` | `encrypt` | STARTTLS 필수 |
|
||||
| `RELAYHOST_USERNAME`/`_PASSWORD` | Secret에서 주입 | Mailgun SASL |
|
||||
| `ALLOWED_SENDER_DOMAINS` | `inouter.com` | 발신 도메인 화이트리스트 |
|
||||
| `POSTFIX_myhostname` | `smtp-relay.mail.svc.cluster.local` | HELO 이름 |
|
||||
| `POSTFIX_mynetworks` | `10/8 127/8 172.16/12 192.168/16` | 릴레이 허용 네트워크 |
|
||||
| `POSTFIX_message_size_limit` | 25 MiB | Mailgun 메시지 한계 |
|
||||
| `POSTFIX_smtpd_helo_required` | yes | HELO 강제 |
|
||||
| `POSTFIX_maximal_queue_lifetime` / `bounce_queue_lifetime` | 1h | 큐 체류 상한 |
|
||||
|
||||
## 사용법 (내부 앱)
|
||||
|
||||
```python
|
||||
# Python 예시
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["From"] = "noreply@inouter.com"
|
||||
msg["To"] = "kaffa@inouter.com"
|
||||
msg["Subject"] = "hello from k3s"
|
||||
msg.set_content("본문")
|
||||
|
||||
with smtplib.SMTP("smtp-relay.mail.svc.cluster.local", 25) as s:
|
||||
s.send_message(msg)
|
||||
```
|
||||
|
||||
인증 불필요. `From` 도메인은 반드시 `inouter.com` (아니면 postfix가 reject).
|
||||
|
||||
## 검증 절차
|
||||
|
||||
```bash
|
||||
# TCP 접속
|
||||
kubectl -n mail run probe --rm -it --restart=Never --image=busybox -- \
|
||||
sh -c 'echo -e "EHLO test\nQUIT" | nc smtp-relay 25'
|
||||
|
||||
# 실제 발송 (swaks 사용)
|
||||
kubectl -n mail run swaks-test --restart=Never --image=nicolaka/netshoot --command -- \
|
||||
swaks --server smtp-relay:25 \
|
||||
--from claude@inouter.com \
|
||||
--to you@example.com \
|
||||
--h-Subject '테스트' \
|
||||
--body '본문'
|
||||
sleep 4
|
||||
kubectl -n mail logs swaks-test
|
||||
kubectl -n mail delete pod swaks-test
|
||||
|
||||
# postfix 큐에서 처리 내역 확인
|
||||
kubectl -n mail logs deploy/smtp-relay --tail=30 | grep -E 'status=|relay='
|
||||
```
|
||||
|
||||
성공 시 로그에 `status=sent (250 Great success)` 와 `relay=smtp.mailgun.org[...]:587` 가 나와야 한다.
|
||||
|
||||
## 배포 검증 기록 (2026-04-08 초기)
|
||||
|
||||
- Pod 1/1 Running, readiness/liveness OK
|
||||
- swaks 테스트 성공: queue ID `6895722DB89` → `to=<kaffa@inouter.com>, relay=smtp.mailgun.org[34.160.157.95]:587, status=sent (250 Great success)`
|
||||
- 지연 2.2초 (DNS lookup + STARTTLS 포함)
|
||||
|
||||
## 운영 주의사항
|
||||
|
||||
- **DKIM 서명**: postfix 자체 DKIM 설정 안 함. Mailgun이 자체 DKIM으로 재서명하므로 수신 측 SPF/DKIM 검증은 통과. 자체 도메인 DKIM 원하면 `DKIM_AUTOGENERATE=yes` 설정 + DNS TXT 추가.
|
||||
- **Rate limit**: postfix 자체 제한 없음. 폭주 시 Mailgun throttle 걸림 → 큐에 쌓이다 1h 후 bounce. 앱 레벨에서 throttle 권장.
|
||||
- **sender spoofing 차단**: `ALLOWED_SENDER_DOMAINS=inouter.com` 으로 제한. 다른 도메인 발신이 필요해지면 ConfigMap에서 쉼표로 추가.
|
||||
- **NetworkPolicy**: 현재 없음. 필요 시 특정 네임스페이스만 발송 가능하도록 제한 권장.
|
||||
- **Monitoring / 알림**: 실패 메일 bounce 처리 알림 없음. Mailgun 대시보드로 확인하거나 Discord webhook 추가 필요.
|
||||
|
||||
## 관련 리소스
|
||||
|
||||
- Vault: `secret/messaging/mailgun/smtp` (SMTP creds), `secret/messaging/mailgun/api-key` (API 키)
|
||||
- Mailgun 도메인: `inouter.com`
|
||||
- ArgoCD App 등록은 [[infra-hosts|클러스터 노드]]의 `argocd` 네임스페이스에 있음
|
||||
- 유사 GitOps 패턴 앱: `kaffa/bunnycdn-mcp`, `kaffa/cf-bouncer-manager`, `kaffa/namecheap-api`, `kaffa/vultr-api`
|
||||
134
infra/network/sshpiper.md
Normal file
134
infra/network/sshpiper.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: sshpiper SSH 리버스 프록시
|
||||
updated: 2026-03-26
|
||||
tags: [infra, ssh, proxy, k3s]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
sshpiper는 SSH를 위한 리버스 프록시. HTTP의 nginx처럼 SSH 연결을 중개하고 라우팅한다.
|
||||
- GitHub: https://github.com/tg123/sshpiper
|
||||
- 라이센스: MIT
|
||||
|
||||
## 배포 정보
|
||||
|
||||
K3s 클러스터(kr3 컨텍스트)에 Helm으로 설치 (2026-03-26)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Namespace | sshpiper |
|
||||
| Chart | sshpiper/sshpiper 0.4.6 |
|
||||
| App | sshpiperd v1.5.0 |
|
||||
| Image | farmer1992/sshpiperd:v1.5.0 |
|
||||
| Plugin | kubernetes (CRD 기반) |
|
||||
| Service | LoadBalancer 192.168.9.51:2222 |
|
||||
| Helm repo | https://tg123.github.io/sshpiper-chart |
|
||||
|
||||
## 라우팅 구조
|
||||
|
||||
Pipe CRD (`pipes.sshpiper.com/v1beta1`)로 사용자명 기반 라우팅:
|
||||
|
||||
```
|
||||
ssh jp1@192.168.9.51 -p 2222 → root@incus-jp1(100.109.123.1:22)
|
||||
```
|
||||
|
||||
## 인증 방식
|
||||
|
||||
### 패스워드 인증 (passthrough)
|
||||
|
||||
- 별도 키/시크릿 설정 불필요, Pipe CRD만 만들면 끝
|
||||
- 클라이언트가 입력한 패스워드를 sshpiper가 그대로 업스트림에 전달
|
||||
|
||||
### 키 기반 인증 (두 번 인증)
|
||||
|
||||
```
|
||||
유저(개인키A) → sshpiper(공개키A로 검증) → sshpiper(개인키S로 업스트림 접속) → 서버(공개키S 등록)
|
||||
```
|
||||
|
||||
- 키는 전달이 아니라 sshpiper가 자체 키로 업스트림에 별도 인증
|
||||
- `from.authorized_keys_data`: 유저의 공개키 등록
|
||||
- `to.private_key_secret`: sshpiper 전용 개인키 (K8s Secret)
|
||||
- 업스트림 서버의 `authorized_keys`에 sshpiper 공개키 등록
|
||||
- sshpiper 키 페어 하나로 모든 Pipe에서 공유 가능
|
||||
|
||||
| 구간 | 패스워드 방식 | 키 방식 |
|
||||
|------|-------------|---------|
|
||||
| 클라이언트 → sshpiper | 패스워드 통과(검증 안 함) | 유저 공개키로 검증 |
|
||||
| sshpiper → 서버 | 패스워드 그대로 전달 | sshpiper 전용 개인키로 별도 인증 |
|
||||
|
||||
## 지원 기능
|
||||
|
||||
SSH 세션을 통째로 프록시하므로 SSH로 할 수 있는 것 전부 지원:
|
||||
- 포트포워딩 (로컬 -L, 리모트 -R, 다이나믹 -D)
|
||||
- SCP, SFTP
|
||||
- 세션 녹화 (asciicast/typescript)
|
||||
|
||||
## 현재 Pipe 목록
|
||||
|
||||
- pipe-jp1: `jp1` → `root@100.109.123.1:22` (패스워드 인증)
|
||||
- pipe-test: `test` → `testuser@sshd-simple.test.svc.cluster.local:22` (패스워드 인증, 테스트용)
|
||||
|
||||
## 역할 분리
|
||||
|
||||
| 용도 | 도구 |
|
||||
|------|------|
|
||||
| 고객 SFTP/SSH | [[sftpgo]] — 사용자/키 관리 내장, 웹 UI |
|
||||
| 내부 서버 SSH 프록시 | sshpiper — 라우팅 프록시 |
|
||||
| 관리자 SSH/K8s | [[teleport]] — 감사 로그, MFA |
|
||||
|
||||
## Pipe CRD 예시
|
||||
|
||||
### 패스워드 방식
|
||||
|
||||
```yaml
|
||||
apiVersion: sshpiper.com/v1beta1
|
||||
kind: Pipe
|
||||
metadata:
|
||||
name: pipe-jp1
|
||||
namespace: sshpiper
|
||||
spec:
|
||||
from:
|
||||
- username: jp1
|
||||
to:
|
||||
host: 100.109.123.1:22
|
||||
ignore_hostkey: true
|
||||
username: root
|
||||
```
|
||||
|
||||
### 키 방식
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: sshpiper-key
|
||||
namespace: sshpiper
|
||||
type: Opaque
|
||||
data:
|
||||
ssh-privatekey: <base64 인코딩된 개인키>
|
||||
---
|
||||
apiVersion: sshpiper.com/v1beta1
|
||||
kind: Pipe
|
||||
metadata:
|
||||
name: pipe-jp1
|
||||
namespace: sshpiper
|
||||
spec:
|
||||
from:
|
||||
- username: jp1
|
||||
authorized_keys_data: "ssh-ed25519 AAAA... user-pubkey"
|
||||
to:
|
||||
host: 100.109.123.1:22
|
||||
ignore_hostkey: true
|
||||
username: root
|
||||
private_key_secret:
|
||||
name: sshpiper-key
|
||||
```
|
||||
|
||||
## 관리 명령
|
||||
|
||||
```bash
|
||||
kubectl get pipes -n sshpiper # Pipe 목록
|
||||
kubectl get pipes -n sshpiper -o yaml # 상세 설정
|
||||
helm get values sshpiper -n sshpiper # Helm 설정값
|
||||
helm get manifest sshpiper -n sshpiper # 전체 매니페스트
|
||||
```
|
||||
Reference in New Issue
Block a user