refactor: organize infra/ into compute/network/security/data/platform
This commit is contained in:
64
infra/security/cert-manager.md
Normal file
64
infra/security/cert-manager.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: cert-manager SSL 인증서 관리
|
||||
updated: 2026-03-26
|
||||
tags: [infra, k3s, ssl, cert-manager]
|
||||
---
|
||||
|
||||
## 인증
|
||||
|
||||
Bearer token is from [[vault]] secret/infra/cert-manager's api_token
|
||||
|
||||
## 배포 엔드포인트
|
||||
|
||||
POST http://100.84.111.28:8000/certificates/deploy
|
||||
|
||||
## 도메인-인스턴스 매핑
|
||||
|
||||
cert-manager 도메인-인스턴스 매핑: config.json의 domain_instance_map으로 설정
|
||||
|
||||
## 보안/신뢰성
|
||||
|
||||
cert-manager 보안/신뢰성: Bearer 토큰 인증 (/health 제외), 도메인 입력 검증 (path traversal 차단), tenacity 재시도 ([[apisix]], Google EAB), Discord DM 알림 (실패 시 봇 DarkRouter가 DM), [[vault]] 토큰 만료 대응 (auto_renew 전 config 리로드 + 403 감지). 매일 03:00 UTC 자동 갱신 (만료 30일 이내)
|
||||
|
||||
## Gitea 레포
|
||||
|
||||
[[gitea]] repository is https://gitea.inouter.com/kaffa/cert-manager (private)
|
||||
|
||||
## DNS
|
||||
|
||||
DNS 전용 API Token 사용, propagation 60초 필요
|
||||
|
||||
## 관리 대상 도메인
|
||||
|
||||
관리 대상 도메인: actions.it.com, inouter.com, inouter.com, ironclad.it.com, keepanker.cv, servidor.it.com
|
||||
|
||||
## it.com 참고
|
||||
|
||||
it.com은 TLD(도메인 레지스트리)이며 kaffa가 소유한 zone이 아님
|
||||
|
||||
## kr 도메인
|
||||
|
||||
*.kr.inouter.com → kr1만
|
||||
|
||||
## K3s cert-manager (Google Trust Services)
|
||||
|
||||
ClusterIssuer: `google-trust-prod`, DNS-01 챌린지 (Cloudflare API).
|
||||
Secret: `cloudflare-api-token` (cert-manager 네임스페이스).
|
||||
reflector로 전체 네임스페이스에 TLS 시크릿 자동 복제.
|
||||
|
||||
### 와일드카드 인증서 목록
|
||||
|
||||
| Certificate | Secret | 도메인 |
|
||||
|------------|--------|--------|
|
||||
| wildcard-inouter | wildcard-inouter-tls | *.inouter.com |
|
||||
| wildcard-anvil-it-com | wildcard-anvil-it-com-tls | *.inouter.com |
|
||||
| wildcard-actions-it-com | wildcard-actions-it-com-tls | *.actions.it.com |
|
||||
| wildcard-ironclad-it-com | wildcard-ironclad-it-com-tls | *.ironclad.it.com |
|
||||
| wildcard-keepanker-cv | wildcard-keepanker-cv-tls | *.keepanker.cv |
|
||||
| wildcard-servidor-it-com | wildcard-servidor-it-com-tls | *.servidor.it.com |
|
||||
| wildcard-api-inouter | wildcard-api-inouter-tls | *.api.inouter.com |
|
||||
| wildcard-mcp-inouter | wildcard-mcp-inouter-tls | *.mcp.inouter.com |
|
||||
|
||||
### Traefik Gateway 등록 인증서
|
||||
|
||||
websecure 리스너에 등록: wildcard-inouter-tls, wildcard-anvil-it-com-tls, wildcard-actions-it-com-tls, wildcard-api-inouter-tls, wildcard-mcp-inouter-tls
|
||||
210
infra/security/cloudflare.md
Normal file
210
infra/security/cloudflare.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: Cloudflare 서비스
|
||||
updated: 2026-04-10
|
||||
tags: [infra, cloudflare, cdn, dns]
|
||||
---
|
||||
|
||||
Syn 이 엣지 관점에서 소유. 일반 DNS 관리 협업은 Heimdall.
|
||||
|
||||
## 계정
|
||||
|
||||
- ID: `d8e5997eb4040f8b489f09095c0f623c` (kappa@inouter.com)
|
||||
- API 토큰: Vault `secret/cloud/cloudflare` (`api_token`, `email`)
|
||||
|
||||
### API 토큰 권한 범위 (2026-04-13 실측)
|
||||
|
||||
| 엔드포인트 | 권한 |
|
||||
|-----------|------|
|
||||
| Zone Read | ✅ |
|
||||
| DNS Read/Write | ✅ |
|
||||
| Turnstile Read | ✅ (Write는 ❌ 403) |
|
||||
| Workers Read | ✅ |
|
||||
| KV Read | ✅ |
|
||||
| Rulesets (WAF/Rate Limit 규칙) | ❌ 403 |
|
||||
| Firewall Rules | ❌ 403 |
|
||||
| Rate Limits (legacy) | ❌ 403 |
|
||||
| Zone Settings | ❌ 403 |
|
||||
|
||||
⚠️ **Rate Limit, WAF 규칙, Firewall 설정은 API로 조회/변경 불가 — 대시보드에서만 관리 가능**. 토큰 스코프 확장이 필요하면 CF 대시보드 > My Profile > API Tokens에서 편집.
|
||||
|
||||
## Zone
|
||||
|
||||
| Zone | Zone ID | Status | Plan | NS | DNS rec | 비고 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **actions.it.com** | `dd7db273…` | active | Free | earl/raegan | 2 | apex+wildcard CNAME → BunnyCDN. `*.actions.it.com → actions.b-cdn.net` 는 dead 풀존 가리킴 (미정리) |
|
||||
| **anvil.it.com** | `742f4c98…` | active | Free | earl/raegan | 9 | apex/wildcard CNAME → iron-jp, ssh.anvil.it.com A, Cloudflare Email Routing |
|
||||
| **inouter.com** | `cd84743d…` | active | Free | earl/raegan | 32 | 메인 zone. 와일드카드 → k3s.inouter.com (LAN), 다수 서브가 iron-kr / iron-git 으로 분기 |
|
||||
| **ironclad.it.com** | `bc8761b3…` | active | Free | earl/raegan | 14 | apex+wildcard A `172.233.93.180` (proxied), Mailgun MX, AWS SES DKIM |
|
||||
| **keepanker.cv** | `118ac337…` | active | Free | lochlan/olivia | 3 | apex/wildcard → Cloudflare Tunnel (`cfargotunnel.com`), juiceshop CNAME → iron-kr-waf |
|
||||
| **servidor.it.com** | `a6e1a3f1…` | active | Free | earl/raegan | 0 | DNS 0건 orphan. cert-manager wildcard + CrowdSec bouncer + Turnstile 위젯은 세팅된 상태 (배포 준비용 유지) |
|
||||
|
||||
별도 계정 [[netbis]] 의 6개 zone 은 본 문서 범위 외 — netbis-cf-bouncer 가 별도로 관리.
|
||||
|
||||
## DNS 레코드 — zone 별 핵심
|
||||
|
||||
### actions.it.com
|
||||
|
||||
| Name | Type | Value | Proxied |
|
||||
|---|---|---|---|
|
||||
| `actions.it.com` | CNAME | `iron-kr.b-cdn.net` | ✅ |
|
||||
| `*.actions.it.com` | CNAME | `actions.b-cdn.net` (dead 풀존) | ✅ |
|
||||
|
||||
### anvil.it.com
|
||||
|
||||
| Name | Type | Value | Proxied |
|
||||
|---|---|---|---|
|
||||
| `anvil.it.com` | CNAME | `iron-jp.b-cdn.net` | off |
|
||||
| `*.anvil.it.com` | CNAME | `iron-jp.b-cdn.net` | off |
|
||||
| `n8n.anvil.it.com` | CNAME | `iron-jp.b-cdn.net` | off |
|
||||
| `kroki.anvil.it.com` | CNAME | `k3s.inouter.com` | off |
|
||||
| `ssh.anvil.it.com` | A | `139.162.71.52` | off |
|
||||
| MX×3 Cloudflare Email Routing, DKIM TXT | | | |
|
||||
|
||||
### inouter.com
|
||||
|
||||
| Name | Type | Value | Proxied | 메모 |
|
||||
|---|---|---|---|---|
|
||||
| `*.inouter.com` | CNAME | `k3s.inouter.com` | off | LAN 직통 (BunnyCDN 우회) |
|
||||
| `k3s.inouter.com` | A | `192.168.9.53` | off | Traefik MetalLB VIP |
|
||||
| `kr.inouter.com` | A | `220.120.65.245` | off | OpenWrt 공인 IP |
|
||||
| `jp.inouter.com` | A | `42.125.196.116` | off | jp1 |
|
||||
| `derp.inouter.com` | A | `42.125.196.116` | off | jp1 derp |
|
||||
| `gitea.inouter.com` | CNAME | `iron-git.b-cdn.net` | off | 별도 풀존 (미들웨어 우회) |
|
||||
| `n8n.inouter.com` | CNAME | `iron-kr.b-cdn.net` | off | |
|
||||
| `jarvis.inouter.com` | CNAME | `iron-kr.b-cdn.net` | off | |
|
||||
| `vault.inouter.com` | CNAME | `iron-kr.b-cdn.net` | off | |
|
||||
| `outline.inouter.com` | CNAME | `iron-kr.b-cdn.net` | off | |
|
||||
| `telegram-webhook.inouter.com` | CNAME | `iron-kr.b-cdn.net` | off | |
|
||||
| `desk-api.inouter.com` | A | `172.233.93.180` | ✅ | osaka 직통, CF proxied |
|
||||
| `sftp.inouter.com` | A | `220.120.65.245` | ✅ | OpenWrt 직통, CF proxied |
|
||||
| `git.inouter.com` | A | `52.79.45.166` | off | relay4wd Lightsail |
|
||||
| `teleport.inouter.com` | A | `52.79.45.166` | off | relay4wd Lightsail |
|
||||
| `twilio.jp.inouter.com` | A | `172.233.93.180` | off | |
|
||||
| `bunny.mcp.inouter.com` | CNAME | `k3s.inouter.com` | off | |
|
||||
| `vl.inouter.com` | CNAME | `k3s.inouter.com` | off | [[victorialogs|VictoriaLogs]] |
|
||||
| `emulator.api.inouter.com` | CNAME | `k3s.inouter.com` | off | |
|
||||
| `linode.api.inouter.com` | CNAME | `k3s.inouter.com` | off | |
|
||||
| `namecheap.api.inouter.com` | CNAME | `k3s.inouter.com` | off | |
|
||||
| `vultr.api.inouter.com` | CNAME | `k3s.inouter.com` | off | |
|
||||
| `email.inouter.com` | CNAME | `mailgun.org` | off | |
|
||||
| MX×2 Mailgun, TXT (SPF/DMARC/DKIM/google verification) | | | | |
|
||||
|
||||
`nas.inouter.com` 은 특정 A/CNAME 레코드 없음. `*.inouter.com` 와일드카드 폴백으로 `192.168.9.53` (LAN MetalLB VIP) 로 해석되어 K3s `lan-proxies/nas` IngressRoute 를 통해 Synology DSM (`192.168.9.100:5000`) 으로 reverse proxy. 상세 [[nas-storage#DSM Reverse Proxy — `nas.inouter.com`]].
|
||||
|
||||
### ironclad.it.com
|
||||
|
||||
| Name | Type | Value | Proxied |
|
||||
|---|---|---|---|
|
||||
| `ironclad.it.com` | A | `172.233.93.180` | ✅ |
|
||||
| `*.ironclad.it.com` | A | `172.233.93.180` | ✅ |
|
||||
| MX×2 Mailgun, AWS SES DKIM CNAME×3, TXT (SPF/DMARC/google) | | | |
|
||||
|
||||
ironclad.it.com 은 **BunnyCDN 풀존을 거치지 않음**. CF proxied A 로 osaka `172.233.93.180` 직통. 보호는 CF Worker (`ironclad-site` apex + bouncer wildcard) 단독.
|
||||
|
||||
### keepanker.cv
|
||||
|
||||
| Name | Type | Value | Proxied |
|
||||
|---|---|---|---|
|
||||
| `keepanker.cv` | CNAME | `f1aa79f2-….cfargotunnel.com` | ✅ |
|
||||
| `*.keepanker.cv` | CNAME | `f1aa79f2-….cfargotunnel.com` | ✅ |
|
||||
| `juiceshop.keepanker.cv` | CNAME | `iron-kr-waf.b-cdn.net` | off |
|
||||
|
||||
apex/wildcard 는 Cloudflare Tunnel. juiceshop 은 BunnyCDN iron-kr-waf 로 override.
|
||||
|
||||
### servidor.it.com
|
||||
|
||||
DNS 레코드 없음. zone 만 등록. 워커 라우트 + Turnstile 위젯 + cert-manager wildcard cert 는 세팅되어 있으며 배포 준비 상태로 유지.
|
||||
|
||||
## Workers
|
||||
|
||||
| Worker | 라우트 부착 zones | 비고 |
|
||||
|---|---|---|
|
||||
| **crowdsec-cloudflare-worker-bouncer** | actions, ironclad, keepanker, servidor | CrowdSec CF bouncer 본체. cs-cf-worker-bouncer (jp1) 가 168h 마다 secret rotate. 정본 [[crowdsec-safeline]] |
|
||||
| **ironclad-site** | ironclad.it.com (apex) | 정적 사이트, has_assets |
|
||||
| **cf-multisite** | `*.actions.it.com/*` | 라우팅 워커 |
|
||||
| chat-worker | (없음) | workers.dev only |
|
||||
| cloud-instances-api | (없음) | workers.dev only |
|
||||
| cloud-orchestrator | (없음) | workers.dev only |
|
||||
| telegram-ai-support | (없음) | workers.dev only |
|
||||
| telegram-cli-web | (없음) | workers.dev only |
|
||||
| telegram-summary-bot | (없음) | workers.dev only |
|
||||
|
||||
### Worker 라우트 (zone 별)
|
||||
|
||||
| Zone | Pattern | Script |
|
||||
|---|---|---|
|
||||
| actions.it.com | `*actions.it.com/*` | crowdsec-cloudflare-worker-bouncer |
|
||||
| actions.it.com | `*.actions.it.com/*` | cf-multisite |
|
||||
| actions.it.com | `vultr.actions.it.com/*` | null (orphan, 미정리) |
|
||||
| actions.it.com | `linode.actions.it.com/*` | null (orphan, 미정리) |
|
||||
| ironclad.it.com | `ironclad.it.com/*` | ironclad-site |
|
||||
| ironclad.it.com | `*ironclad.it.com/*` | crowdsec-cloudflare-worker-bouncer |
|
||||
| keepanker.cv | `*keepanker.cv/*` | crowdsec-cloudflare-worker-bouncer |
|
||||
| servidor.it.com | `*servidor.it.com/*` | crowdsec-cloudflare-worker-bouncer |
|
||||
|
||||
inouter.com / anvil.it.com 에는 Worker 라우트 없음. DNS proxied=false 라 CF 엣지를 거치지 않으므로 enforce 불가 — 이들 zone 의 엣지 보호는 BunnyCDN 미들웨어 64811 이 단독 책임.
|
||||
|
||||
Worker Custom Domains (계정 레벨): 0건.
|
||||
|
||||
## Turnstile 위젯
|
||||
|
||||
| Sitekey | 이름 | mode | 도메인 | 역할 |
|
||||
|---|---|---|---|---|
|
||||
| `0x4AAAAAABvmO8BKc1ss5d-S` | `crowdsec-captcha` | managed | actions / anvil / charon.my / ironclad / keepanker / n8n.my / servidor / subin.my | multi-domain 운영용 (8 도메인). 수동 관리 (bouncer 외) |
|
||||
| `0x4AAAAAACbmaudAjITah7y7` | `inouter` | managed | anvil.it.com | 이름·도메인 불일치 (legacy/orphan 후보, 결정 미정) |
|
||||
| `0x4AAAAAAC3otPWhldI96Aks` | `inouter-bunny-middleware` | managed | inouter.com | BunnyCDN 미들웨어 64811 의 `TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` env baked-in. 이름이 bouncer auto 패턴과 달라 cs-cf-worker-bouncer 가 관리 대상 외. **수동 관리**, sitekey/secret 변경 시 미들웨어 64811 env 동시 갱신 필수 |
|
||||
| `0x4AAAAAAC3nIMLBRKWfiY8A` | `crowdsec-cloudflare-worker-bouncer-widget` | managed | actions.it.com | cs-cf-worker-bouncer 자동 (168h rotation). 수동 편집 금지 |
|
||||
| `0x4AAAAAAC3nIYV_A5OA0Xzv` | 〃 | managed | ironclad.it.com | 〃 |
|
||||
| `0x4AAAAAAC3nHnAB6Q9dlvHM` | 〃 | managed | keepanker.cv | 〃 |
|
||||
| `0x4AAAAAAC3nH0xXSU8kbwsn` | 〃 | managed | servidor.it.com | 〃 |
|
||||
|
||||
`inouter-bunny-middleware` 의 secret 은 Vault `secret/cloud/cloudflare/turnstile-inouter-bunny` (`sitekey`, `secret`, `name`).
|
||||
|
||||
### Turnstile 토큰 권한
|
||||
|
||||
- **Vault `secret/cloud/cloudflare.api_token`** (`pUIZdTV0…`): DNS R/W + Turnstile **read-only**. 위젯 생성/수정/삭제 불가
|
||||
- **jp1 bouncer 호스트의 `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` 내 `token: seUKZID4…`**: Turnstile **read+write**. bouncer 의 자체 인증 자산이며 Vault 에 복제되어 있지 않음
|
||||
|
||||
## 특이사항
|
||||
|
||||
- **`*.actions.it.com → actions.b-cdn.net`** — 와일드카드가 dead 풀존 가리킴. 일부 서브가 5xx 가능. 미정리 (영향 평가 후 별건)
|
||||
- **Worker routes `vultr.actions.it.com/*`, `linode.actions.it.com/*`** — `script: null` orphan. 미정리
|
||||
- **Turnstile `inouter` (`…CbmaudAjITah7y7`)** — 이름·도메인 불일치 legacy 후보. 미정리
|
||||
|
||||
### CF proxy on/off 패턴
|
||||
|
||||
| Zone | Proxy | 보호 |
|
||||
|---|---|---|
|
||||
| inouter.com / anvil.it.com | off | BunnyCDN 미들웨어 64811 단독 |
|
||||
| actions.it.com / ironclad.it.com | on | CF Worker (bouncer + cf-multisite / ironclad-site) |
|
||||
| keepanker.cv | on (Tunnel) | CF Tunnel + juiceshop 만 iron-kr-waf override |
|
||||
|
||||
## cfb-manager (CrowdSec CF Worker bouncer 관리 API)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 위치 | K3s `default/cfb-manager` (`10.43.68.76:8000`) |
|
||||
| 구현 | Python FastAPI |
|
||||
| 기능 | bouncer 보호 도메인 추가/삭제, decision 조회, CF zone 동기화, bouncer 재시작 트리거 |
|
||||
| SSH 키 | K8s `default/cfb-ssh-key` (ed25519) |
|
||||
|
||||
```bash
|
||||
BASE=http://cfb-manager.default.svc.cluster.local:8000
|
||||
curl $BASE/status # bouncer_running, pids, protected_domains
|
||||
curl $BASE/domains # 보호 중인 zone 상세
|
||||
curl $BASE/decisions # 현재 결정
|
||||
curl -X POST $BASE/domains/<zone> # zone 추가
|
||||
curl -X DELETE $BASE/domains/<zone> # zone 제거 (destructive — bouncer restart + widget rotate)
|
||||
```
|
||||
|
||||
**중요**: `DELETE /domains/<zone>` 는 단순 config 항목 제거가 아니라 bouncer restart 를 트리거하며, restart 가 모든 zone 의 widget 을 force-rotate 하고 제거된 zone 의 widget 을 destroy 한다. 외부에서 baked-in 으로 사용 중인 sitekey 가 있다면 동시에 깨진다.
|
||||
|
||||
BunnyCDN 미들웨어와는 무관. BunnyCDN 미들웨어 64811 의 bloom filter 동기화는 jp1 `infra-tool` 컨테이너 `/opt/crowdsec-bouncer/bouncer.py` (3분 delta + 매시 full sync) 가 담당.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[netbis]] — Netbis 계정 Cloudflare (별도)
|
||||
- [[cf-multisite]] — `*.actions.it.com` 라우팅 워커
|
||||
- [[crowdsec-safeline]] — CrowdSec CF Worker Bouncer 연동
|
||||
- [[bunnycdn-security]] — BunnyCDN 엣지 보안
|
||||
- [[nas-storage]] — Synology NAS + `nas.inouter.com` reverse proxy
|
||||
303
infra/security/crowdsec-safeline.md
Normal file
303
infra/security/crowdsec-safeline.md
Normal file
@@ -0,0 +1,303 @@
|
||||
---
|
||||
title: CrowdSec 및 SafeLine WAF
|
||||
updated: 2026-04-10
|
||||
---
|
||||
|
||||
## CrowdSec LAPI
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 위치 | jp1 Incus `crowdsec` 컨테이너 |
|
||||
| LAPI | `http://10.253.100.240:8080` |
|
||||
| 관리 | `ssh incus-jp1 "incus exec crowdsec -- cscli ..."` |
|
||||
|
||||
## 로그 수집 (Acquisition)
|
||||
|
||||
### Traefik → CrowdSec (Vector)
|
||||
|
||||
```
|
||||
Traefik DaemonSet (stdout JSON accessLog)
|
||||
→ Vector Agent DaemonSet (K3s logging ns, kubernetes_logs source)
|
||||
→ VRL transform (access log만 필터, non-JSON abort)
|
||||
→ HTTP sink (배치 50건, 5초)
|
||||
→ CrowdSec HTTP acquisition (:8086/traefik-logs)
|
||||
→ crowdsecurity/traefik-logs 파서
|
||||
```
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Vector | Helm `vector/vector` 0.51.0, Agent DaemonSet (3노드) |
|
||||
| Values | `~/k8s/vector/values.yaml` |
|
||||
| CrowdSec 포트 | 8086 |
|
||||
| 인증 | `Authorization: traefik-crowdsec-log-2024` |
|
||||
| 파서 | `crowdsecurity/traefik-logs` (Hub, JSON 모드) |
|
||||
|
||||
### APISIX → VictoriaLogs → CrowdSec (서울+오사카 통합)
|
||||
|
||||
```
|
||||
서울 APISIX (K3s) stdout → Vector DaemonSet → VictoriaLogs (ES bulk API)
|
||||
오사카 APISIX (Docker) stdout → Vector (Docker) → VictoriaLogs (ES bulk API)
|
||||
→ CrowdSec victorialogs acquisition (tail 모드, 실시간)
|
||||
→ custom/apisix-json-logs 파서
|
||||
+ anomaly-detect (5분 폴링, AI 분석)
|
||||
```
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| VictoriaLogs | `vl.inouter.com` (K3s logging ns, Traefik IngressRoute) |
|
||||
| CrowdSec acquisition | `/etc/crowdsec/acquis.d/victorialogs-apisix.yaml` (`source: victorialogs`, `mode: tail`, `query: program:apisix log_type:access`) |
|
||||
| 서울 Vector | K3s DaemonSet (Helm `vector/vector`), `parse_apisix` transform → `vlogs` ES sink |
|
||||
| 오사카 Vector | Docker `timberio/vector:0.45.0-debian`, `/etc/vector/vector.yaml`, `docker_logs` source → `parse_apisix` → `vlogs` ES sink. `location: osaka` 필드 추가 |
|
||||
| 파서 | `custom/apisix-json-logs` (로컬) |
|
||||
|
||||
### APISIX → log-collector → CrowdSec (sandbox-tokyo)
|
||||
|
||||
```
|
||||
sandbox-tokyo APISIX http-logger
|
||||
→ log-collector (jp1 crowdsec 컨테이너, :8087)
|
||||
→ SQLite (/var/lib/log-collector/requests.db)
|
||||
→ CrowdSec HTTP acquisition (:8085/apisix-logs) 포워딩
|
||||
```
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| log-collector | Go HTTP 서버, `/usr/local/bin/log-collector` (jp1 crowdsec 컨테이너) |
|
||||
| 소스 | jp1 `~/log-collector/main.go` |
|
||||
| 수신 포트 | 8087 |
|
||||
| 인증 | `Authorization: apisix-crowdsec-log-2024` |
|
||||
| SQLite | `/var/lib/log-collector/requests.db` |
|
||||
| 기능 | APISIX 로그 수신 → SQLite 저장 → CrowdSec 포워딩, 타임스탬프 밀리초 보정 |
|
||||
|
||||
### SafeLine → CrowdSec (실시간, PG LISTEN/NOTIFY)
|
||||
|
||||
```
|
||||
SafeLine WAF 차단 → mgt_detect_log_basic INSERT
|
||||
→ PG 트리거 (notify_detect_log) → pg_notify('safeline_detect', JSON)
|
||||
→ safeline-listener (kr2, Go) → detail 테이블 enrichment (x-real-ip, user-agent)
|
||||
→ CrowdSec HTTP acquisition (:8088/safeline-logs)
|
||||
→ custom/safeline-http-logs 파서
|
||||
→ custom/safeline-waf-blocked 시나리오 (trigger, 즉시 밴)
|
||||
```
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| safeline-listener | Go, `/usr/local/bin/safeline-listener` (kr2) |
|
||||
| 소스 | kr2 `~/safeline-listener/main.go` |
|
||||
| systemd | `safeline-listener.service` (kr2) |
|
||||
| PG DSN | SafeLine CE DB (10.43.8.243:5432, safeline-ce) |
|
||||
| CrowdSec 포트 | 8088 |
|
||||
| 인증 | `Authorization: safeline-crowdsec-2026` |
|
||||
| acquis 설정 | `/etc/crowdsec/acquis.d/safeline-http.yaml` |
|
||||
| 파서 | `custom/safeline-http-logs` (`/etc/crowdsec/parsers/s01-parse/safeline-http-logs.yaml`) |
|
||||
| 시나리오 | `custom/safeline-waf-blocked` (trigger 타입, 즉시 밴) |
|
||||
| enrichment | req_header에서 `x-real-ip`, `user-agent` 추출, detail 테이블에서 method/payload 조회 |
|
||||
|
||||
PG 트리거 (SafeLine CE DB에 설치):
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION notify_detect_log() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('safeline_detect', json_build_object(
|
||||
'id', NEW.id, 'ts', NEW.created_at,
|
||||
'src_ip', NEW.src_ip, 'host', NEW.host,
|
||||
'url_path', NEW.url_path, 'attack_type', NEW.attack_type
|
||||
)::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_detect_log
|
||||
AFTER INSERT ON mgt_detect_log_basic
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_detect_log();
|
||||
```
|
||||
|
||||
### ~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)
|
||||
|
||||
> [!warning] 폐기됨
|
||||
> 2026-04-08 분석기 서비스 정지 + 삭제. "이 방식으로는 안 될 것 같다"는 판단 (60초 폴링 + Claude CLI 동기 호출 구조의 한계). 새로운 AI 분석 아키텍처는 [[victorialogs|VictoriaLogs]] 기반으로 재설계 예정.
|
||||
>
|
||||
> **제거된 항목**: `ddos-detect.service` (systemd), `/var/lib/log-collector/ddos-detect/` Go 바이너리 + 소스, `ddos-detect.sh`, `extract_behavior.py`, `ddos-logs/` (분석 결과 markdown). `requests.db`는 별개의 `log-collector` 데몬이 사용 중이라 보존. Gitea repo `kaffa/ddos-detect`는 보존 (코드 reference).
|
||||
|
||||
이전 동작 (참고):
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 위치 | jp1 crowdsec 컨테이너 `/var/lib/log-collector/ddos-detect/` |
|
||||
| Gitea | `gitea.inouter.com/kaffa/ddos-detect` |
|
||||
| 기능 | SQLite 폴링 → IP별 행위 패턴 추출 → Claude AI 분석 → cscli 자동 밴 |
|
||||
| 설정 | `config.yaml` (poll 60s, Claude sonnet, ban 4h, max_workers 3) |
|
||||
|
||||
### 리얼 IP 처리
|
||||
|
||||
| 경로 | 설정 | source IP 추출 |
|
||||
|------|------|---------------|
|
||||
| BunnyCDN → HAProxy → **Traefik** | `forwardedHeaders.trustedIPs: 127/8, 10/8, 172.16/12, 192.168/16` + `externalTrafficPolicy: Local` | `ClientHost` (X-Forwarded-For에서 추출) |
|
||||
| BunnyCDN → HAProxy → **APISIX** | `real_ip_header: X-Forwarded-For` + `real_ip_from: 127/8, 10.42/16, 10.43/16, 192.168.9/24, 100.64/10` | `client_ip` (nginx real_ip 모듈) |
|
||||
|
||||
Traefik values: `~/k8s/traefik/values.yaml`
|
||||
|
||||
## 시나리오
|
||||
|
||||
| 유형 | 시나리오 | 출처 |
|
||||
|------|---------|------|
|
||||
| HTTP | http-probing, http-crawl-non_statics, http-sensitive-files, http-backdoors-attempts, http-sqli-probing, http-xss-probing 등 | Hub |
|
||||
| SafeLine | custom/safeline-waf-blocked (trigger), custom/safeline-waf-repeated (3+/5min) | 로컬 |
|
||||
| **APISIX** (2026-04-08 신규) | custom/apisix-high-rate-per-ip, custom/apisix-499-burst, custom/apisix-single-path-flood, custom/apisix-5xx-burst | 로컬 |
|
||||
| CAPI | http:dos, http:scan, ssh:bruteforce (커뮤니티) | 자동 |
|
||||
|
||||
### APISIX 시나리오 (4종, 2026-04-08 작성)
|
||||
|
||||
ddos-detect AI 분석기 폐기 후 deterministic 패턴 매칭으로 대체. 모두 leaky bucket, ban 4h. **K3s 서울 APISIX 트래픽만 보고 판단** (osaka/zlambda는 http-logger 송신 안 함).
|
||||
|
||||
| 시나리오 | 매개변수 | 의도 | filter |
|
||||
|---|---|---|---|
|
||||
| `apisix-high-rate-per-ip` | capacity 1000, leakspeed 100ms (≈ sustained 10 req/s, burst 1000) | 일반 HTTP flood. APISIX 자체 limit-req(20 req/s)에 안 걸리는 sustained 패턴 | `log_type=http_access-log`, groupby `source_ip` |
|
||||
| `apisix-499-burst` | capacity 30, leakspeed 2s | connection exhaustion / Slowloris (클라이언트 강제 끊김) | `http_status == "499"` |
|
||||
| `apisix-single-path-flood` | capacity 50, leakspeed 600ms | 동일 path 반복 (CAPTCHA 우회, login bf, 동일 자원 폭격) | groupby `source_ip + ":" + http_path` |
|
||||
| `apisix-5xx-burst` | capacity 20, leakspeed 3s | 백엔드 부하 유발성 공격, 취약점 스캔 | `http_status startsWith "5"` |
|
||||
|
||||
⚠️ **운영 주의**:
|
||||
- `5xx-burst`: 백엔드 장애 시 false positive 가능. 운영 알람과 분리해서 검토.
|
||||
- `499-burst`: 모바일 클라이언트 끊김으로 정상 발생할 수 있어 30 임계값을 며칠 관찰 후 조정 권장.
|
||||
- `high-rate-per-ip`: APISIX `limit-req` 글로벌 룰(20 req/s burst 10)을 통과한 트래픽만 도달 → 1차 통과 후 sustained pattern을 잡는 2차 layer.
|
||||
- 현재 모두 `remediation: true` (즉시 ban). dry-run으로 시작 안 함 — false positive 발생 시 임계값 또는 ban duration 조정.
|
||||
|
||||
|
||||
과거 인시던트 및 변경 이력은 `history/` 참조. 예: `history/2026-04-10-edge-cleanup.md` (cf-audit-cleanup-2 3-incident chain, Turnstile sitekey 교체, 미들웨어 64811 `/__captcha/verify` 버그 수정 등), [[2026-04-15-apisix-http-logger-removal|2026-04-15 APISIX http-logger 레거시 제거]].
|
||||
|
||||
### 발견 사항: K3s APISIX 글로벌 limit-req
|
||||
|
||||
```json
|
||||
{
|
||||
"key_type": "var",
|
||||
"key": "remote_addr",
|
||||
"rate": 20,
|
||||
"burst": 10,
|
||||
"rejected_code": 429
|
||||
}
|
||||
```
|
||||
|
||||
`/apisix/global_rules/limit-req` (etcd). 모든 라우트에 적용. CrowdSec 시나리오는 이 1차 차단을 통과한 트래픽만 본다는 점을 고려해서 임계값 설계.
|
||||
|
||||
## Bouncer
|
||||
|
||||
### cs-cf-worker-bouncer (Cloudflare Worker)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 위치 | jp1 Incus `cs-cf-worker-bouncer` 컨테이너 |
|
||||
| 설정 | `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` |
|
||||
| 정본 | `gitea.inouter.com/kaffa/k8s` → `configs/crowdsec/crowdsec-cloudflare-worker-bouncer.yaml` |
|
||||
| 동기화 | 10초 (decision stream polling) |
|
||||
| 방식 | LAPI → bouncer → Cloudflare Worker KV (bloom filter) → Worker에서 차단/captcha |
|
||||
| 보호 zone | keepanker.cv, actions.it.com, ironclad.it.com, servidor.it.com |
|
||||
| 비보호 zone | inouter.com, anvil.it.com — DNS `proxied: false` 라 CF 엣지를 거치지 않아 bouncer 라우트가 enforce 되지 않음. 보호는 BunnyCDN 미들웨어 64811 단독 |
|
||||
| Turnstile 위젯 | 4개 bouncer-managed (`crowdsec-cloudflare-worker-bouncer-widget` 이름, 168h rotation). BunnyCDN 미들웨어용 `inouter-bunny-middleware` 는 별도 수동 관리 — [[cloudflare#Turnstile 위젯]] |
|
||||
| yaml writer | `cfb-manager` 단일 (K8s `default/cfb-manager` REST API). 과거 `/etc/cron.d/cf-zone-sync` + `auto-add-cf-zones.sh` 자동 쓰기는 coordination 문제로 제거됨 — 신규 zone 추가는 `cfb-manager POST /domains` 또는 수동 yaml edit + systemctl restart |
|
||||
|
||||
#### Decision 수동 관리
|
||||
|
||||
bouncer의 `lapi_key`는 **읽기 전용**. decision 추가/삭제는 `crowdsec` 컨테이너에서 `cscli`로:
|
||||
|
||||
```bash
|
||||
# ban 추가
|
||||
ssh incus-jp1 "incus exec crowdsec -- cscli decisions add --ip 1.2.3.4 --duration 2m --reason 'manual ban' --type ban"
|
||||
|
||||
# 확인
|
||||
ssh incus-jp1 "incus exec crowdsec -- cscli decisions list --ip 1.2.3.4"
|
||||
|
||||
# 삭제
|
||||
ssh incus-jp1 "incus exec crowdsec -- cscli decisions delete --ip 1.2.3.4"
|
||||
```
|
||||
|
||||
bouncer 컨테이너에서 `curl POST /v1/decisions`로 직접 넣으면 등록 안 됨 — API 키 권한 제한.
|
||||
|
||||
#### 설정 변경 시 주의
|
||||
|
||||
**`sed -i` 사용 금지** — Incus 컨테이너 내에서 `sed -i`로 수정 시 이전 내용이 파일 끝에 남아 YAML 깨짐. 반드시 전체 파일 덮어쓰기:
|
||||
|
||||
```bash
|
||||
cat updated-config.yaml | ssh incus-jp1 "incus file push - cs-cf-worker-bouncer/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml"
|
||||
ssh incus-jp1 "incus exec cs-cf-worker-bouncer -- systemctl restart crowdsec-cloudflare-worker-bouncer"
|
||||
```
|
||||
|
||||
### netbis-cf (Cloudflare Worker — Netbis 계정)
|
||||
|
||||
Kappa 계정용 `cs-cf-worker-bouncer`와 별도 컨테이너로 분리 운영. 상세: [[netbis]]
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 위치 | jp1 Incus `netbis-cf-bouncer` 컨테이너 (10.253.103.33) |
|
||||
| 바운서 이름 | netbis-cf |
|
||||
| 설정 | `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` |
|
||||
| 동기화 | 10초 |
|
||||
| CF 계정 | Netbis (netbis@netbis.io, Account ID 8fcf3c7876332aba33e974cbbfdad951) |
|
||||
| 보호 zone | fall-vip.com, fall-mvp.com, fall-vip7.com, psd777.com, rss-555.com, rss-7790.com |
|
||||
| Turnstile | 6개 zone managed 모드, 168시간 secret key 로테이션 |
|
||||
| 로그 소스 | sandbox-tokyo APISIX → CrowdSec http-logger (8085/apisix-logs) |
|
||||
|
||||
### bunny-cdn-bouncer (BunnyCDN Edge Script)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 동기화 | jp1 `infra-tool` 컨테이너, `/opt/crowdsec-bouncer/bouncer.py` |
|
||||
| 크론 | 3분 delta sync, 매시 full sync |
|
||||
| 방식 | LAPI → Bloom filter → BunnyCDN Edge Script 임베딩 → 퍼블리시 |
|
||||
| Edge Script | ID 64811 (`crowdsec-bouncer-middleware`), FNV-1a bloom filter |
|
||||
| 차단 시 | Cloudflare Turnstile CAPTCHA + LibSQL verified IP 캐싱(4h) |
|
||||
| 적용 풀존 | iron-jp (5555247), iron-kr (5555227) — 두 풀존 모두 `MiddlewareScriptId: 64811` |
|
||||
| API 키 | Vault `secret/infra/crowdsec-bunny-bouncer` |
|
||||
| 소스 | `~/crowdsec-bunny-bouncer/` |
|
||||
|
||||
## 보안 구조
|
||||
|
||||
```
|
||||
클라이언트 → BunnyCDN Edge Script (CrowdSec bouncer, 0차)
|
||||
→ BunnyCDN WAF (OWASP CRS, 1차)
|
||||
→ Traefik / APISIX + SafeLine WAF (2차)
|
||||
→ CrowdSec 로그 분석 (3차) → bouncer 피드백 루프
|
||||
```
|
||||
|
||||
### 0차: BunnyCDN Edge Script (CrowdSec bouncer)
|
||||
- Bloom filter로 악성 IP 즉시 차단
|
||||
- false positive 시 Turnstile CAPTCHA로 구제
|
||||
|
||||
### 1차: BunnyCDN WAF (OWASP CRS)
|
||||
- CDN 에지에서 오리진 도달 전 차단
|
||||
- 차단: SQLi, XSS, CMDi, SSRF, Shellshock, Log4j
|
||||
- 비활성화: DATA LEAKAGES SQL (id=911) — NocoDB 오탐 방지
|
||||
- 통과: Request Smuggling, NoSQLi, 경로 스캔
|
||||
|
||||
### 2차: SafeLine WAF (chaitin-waf)
|
||||
- APISIX plugin_metadata → detector `10.43.253.244:8000`
|
||||
- SafeLine v9.3.2, K3s safeline ns
|
||||
- 관리: safeline.inouter.com, API 헤더 `X-SLCE-API-TOKEN`
|
||||
- 토큰: Vault `secret/infra/safeline`
|
||||
- tengine 미사용, APISIX 직접 연동
|
||||
|
||||
### 3차: CrowdSec 로그 분석
|
||||
- Traefik / APISIX 로그: Vector → VictoriaLogs(`vl.inouter.com`) → CrowdSec `victorialogs` acquisition (tail 모드)
|
||||
- SafeLine 차단: PG NOTIFY → safeline-listener → CrowdSec HTTP acquisition(`:8088`)
|
||||
- sandbox-tokyo APISIX: http-logger → log-collector(`:8087`) → CrowdSec
|
||||
- HTTP 시나리오 매칭 → decision → bouncer 피드백
|
||||
|
||||
## iron-kr-waf BunnyCDN Pull Zone (구 waf-kr)
|
||||
|
||||
APISIX 외부 인입 경로 (juiceshop SafeLine 통합 테스트 용도). APISIX 자체는 독립 LoadBalancer gateway (MetalLB 192.168.9.50) 이지 "SafeLine 전용" 이 아님 — 현재 1 route 가 SafeLine 용일 뿐.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Zone ID | 5555224 |
|
||||
| Name | iron-kr-waf |
|
||||
| Origin | https://220.120.65.245:9443 |
|
||||
| 경로 | 인터넷 → BunnyCDN(iron-kr-waf) → OpenWrt HAProxy(:9443) → MetalLB 192.168.9.50:443 → APISIX(K3s 서울) → chaitin-waf → SafeLine detector → K3s svc |
|
||||
|
||||
등록 호스트: `iron-kr-waf.b-cdn.net`, `juiceshop.keepanker.cv` (WAF 테스트용). ApisixRoute `juiceshop` (chaitin-waf block) → juiceshop:3000.
|
||||
|
||||
> 옛 메모의 `waf-kr (5554681)` 는 더 이상 존재하지 않음 — `iron-kr-waf (5555224)` 로 통합·재명명됨.
|
||||
|
||||
## 참고
|
||||
|
||||
- BunnyCDN WAF 차단 시 오리진에 로그 안 옴 → CrowdSec에 미수신
|
||||
- OpenWrt CrowdSec firewall bouncer는 DNAT 구조라 리얼 IP 매칭 불가
|
||||
- chaitin-waf 플러그인은 `plugin_attr`이 아닌 **`plugin_metadata`(etcd)**에서 detector 노드를 읽음
|
||||
- Incus 컨테이너에서 `sed -i`로 설정 수정 시 파일 손상 주의 (전체 파일 push 사용)
|
||||
106
infra/security/external-secrets.md
Normal file
106
infra/security/external-secrets.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: External Secrets Operator (ESO)
|
||||
updated: 2026-04-13
|
||||
tags: [infra, k3s, vault, eso, secrets]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
HashiCorp Vault의 시크릿을 K8s Secret으로 자동 동기화. Vault가 단일 진실 소스(Single Source of Truth).
|
||||
|
||||
## 구성 요소
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 설치 방식 | Helm (external-secrets/external-secrets) |
|
||||
| 네임스페이스 | external-secrets |
|
||||
| 버전 | v2.3.0 |
|
||||
| ClusterSecretStore | `vault` |
|
||||
| Vault 주소 | `http://10.253.101.58:8200` (K3s 내부 직접 접근) |
|
||||
| Vault 인증 | Token (Secret `vault-token` in external-secrets ns) |
|
||||
| KV 엔진 | v2 |
|
||||
| refresh 주기 | 1시간 |
|
||||
|
||||
### Vault 접근 경로
|
||||
|
||||
ESO는 K3s Pod에서 Vault에 접근해야 하므로 CDN URL(`hcv.inouter.com`)이 아닌 **내부 IP 직접 접근** 사용.
|
||||
- Vault 위치: incus-jp1 `vault` 컨테이너 (10.253.101.58)
|
||||
- `hcv.inouter.com`은 BunnyCDN을 거치며 API 경로가 404 반환 → ESO 사용 불가
|
||||
|
||||
## ExternalSecret 목록
|
||||
|
||||
| ExternalSecret | 네임스페이스 | K8s Secret 이름 | Vault 경로 | 키 매핑 |
|
||||
|---|---|---|---|---|
|
||||
| smtp-relay-mailgun | mail | smtp-relay-mailgun | `messaging/mailgun/smtp` | username, password |
|
||||
| bunnycdn-secrets | mcp | bunnycdn-secrets | `cloud/bunnycdn` | api_key → api-key |
|
||||
| outline-secrets | outline | outline-secrets | `apps/outline` | dataFrom extract (전체 키) |
|
||||
| openmemory-secrets | openmemory | openmemory-secrets | `ai/openai` | API_KEY → OPENAI_API_KEY |
|
||||
| cfb-ssh-key | tools | cfb-ssh-key | `apps/cfb-manager` | ssh_key → id_rsa |
|
||||
| namecheap-api-env | tools | namecheap-api-env | `domain/namecheap` | API_KEY, API_USER, CLIENT_IP, USERNAME, SANDBOX |
|
||||
|
||||
## ESO로 관리하지 않는 Secret
|
||||
|
||||
| Secret | 네임스페이스 | 이유 |
|
||||
|--------|-------------|------|
|
||||
| searxng-ca-bundle | searxng | 270KB CA 인증서, Vault 부적합. ArgoCD prune 제외 어노테이션 적용 |
|
||||
| searxng-config | searxng | Helm 차트 values에서 생성 |
|
||||
|
||||
## Vault 시크릿 경로 정리
|
||||
|
||||
```
|
||||
secret/
|
||||
├── ai/
|
||||
│ └── openai # OPENAI_API_KEY (openmemory 등)
|
||||
├── apps/
|
||||
│ ├── outline # DB URL, OIDC, SECRET_KEY 등 11개 키
|
||||
│ ├── cfb-manager # ssh_key
|
||||
│ ├── portainer # api_token, google_client_id/secret
|
||||
│ └── gitea # api_token 등
|
||||
├── cloud/
|
||||
│ └── bunnycdn # api_key
|
||||
├── domain/
|
||||
│ └── namecheap # API_KEY, API_USER, CLIENT_IP, USERNAME, SANDBOX
|
||||
├── messaging/
|
||||
│ └── mailgun/smtp # username, password, host, port 등
|
||||
└── infra/
|
||||
└── ... # SSH 키, Tailscale 등
|
||||
```
|
||||
|
||||
## 운영 가이드
|
||||
|
||||
### 새 시크릿 추가 절차
|
||||
|
||||
1. Vault에 시크릿 저장: `vault kv put -mount=secret <path> key=value`
|
||||
2. ExternalSecret 매니페스트 작성 및 적용
|
||||
3. `kubectl get externalsecret -A` 로 SecretSynced 확인
|
||||
4. Pod에서 참조 확인
|
||||
|
||||
### Vault 토큰 갱신
|
||||
|
||||
ESO가 사용하는 토큰은 `external-secrets/vault-token` Secret에 저장.
|
||||
토큰 만료 시:
|
||||
```
|
||||
vault token create -policy=default -ttl=768h
|
||||
kubectl -n external-secrets create secret generic vault-token --from-literal=token=<new-token> --dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
|
||||
### 트러블슈팅
|
||||
|
||||
```bash
|
||||
# ExternalSecret 상태 확인
|
||||
kubectl get externalsecret -A
|
||||
|
||||
# 상세 에러 확인
|
||||
kubectl get externalsecret <name> -n <ns> -o jsonpath='{.status.conditions}'
|
||||
|
||||
# 강제 재동기화
|
||||
kubectl annotate externalsecret <name> -n <ns> force-sync=$(date +%s) --overwrite
|
||||
|
||||
# ClusterSecretStore 상태
|
||||
kubectl get clustersecretstore vault
|
||||
```
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[helm-charts]] — Helm 차트 관리 체계
|
||||
- [[k3s-ingress-architecture]] — 인그레스 구조
|
||||
165
infra/security/teleport.md
Normal file
165
infra/security/teleport.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: Teleport (접근 관리)
|
||||
updated: 2026-03-27
|
||||
tags: [infra, teleport, k3s, security]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
Gravitational Teleport — SSH/K8s/웹앱 접근 관리 플랫폼.
|
||||
|
||||
## 배포 정보
|
||||
|
||||
K3s 클러스터(kr3 컨텍스트)에 Helm으로 설치 (2026-03-26)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Namespace | teleport |
|
||||
| Chart | teleport/teleport-cluster 18.7.3 |
|
||||
| App | Teleport 18.7.3 |
|
||||
| Mode | standalone, multiplex |
|
||||
| Proxy | LoadBalancer 192.168.9.52:443 |
|
||||
| Auth | ClusterIP 3025/3026 |
|
||||
| TLS | wildcard-inouter-tls (cert-manager) |
|
||||
| Storage | Longhorn 5Gi |
|
||||
| 클러스터명 | teleport.inouter.com |
|
||||
| DNS | teleport.inouter.com → 52.79.45.166 (Cloudflare, proxied: false) |
|
||||
|
||||
## 외부 접근 경로
|
||||
|
||||
```
|
||||
teleport.inouter.com (52.79.45.166)
|
||||
→ relay4wd:443
|
||||
→ iptables REDIRECT :8443
|
||||
→ APISIX stream_route (teleport)
|
||||
→ 192.168.9.52:443 (MetalLB)
|
||||
→ Teleport proxy Pod
|
||||
```
|
||||
|
||||
## 사용자
|
||||
|
||||
| User | Roles |
|
||||
|------|-------|
|
||||
| admin | editor, access, auditor |
|
||||
|
||||
## 인증
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 인증 방식 | local (패스워드 + MFA) |
|
||||
| MFA | OTP + WebAuthn (secondFactors) |
|
||||
| Passwordless | 활성화 (`authentication.passwordless: true`, `webauthn.rp_id: teleport.inouter.com`) |
|
||||
| MFA 비활성화 | 불가 — 한번 등록하면 `off`로 변경 시 auth 기동 실패 |
|
||||
| CLI 로그인 (패스워드) | `tsh login --proxy=teleport.inouter.com --user=admin --auth=local` (OTP 입력) |
|
||||
| CLI 로그인 (패스키) | `tsh login --proxy=teleport.inouter.com --auth=passwordless` (FIDO2 키 필요) |
|
||||
| Touch ID/패스키 | brew tsh 미지원, Teleport Connect 앱도 불안정 — **웹 UI 사용 권장** |
|
||||
| admin logins | teleport, root (`tctl users update admin --set-logins=teleport,root`) |
|
||||
|
||||
## Agentless OpenSSH 설정
|
||||
|
||||
기존 sshd를 Teleport agent 없이 연동하는 방식.
|
||||
|
||||
### 핵심 주의사항
|
||||
|
||||
> **Teleport는 OpenSSH agentless용으로 별도의 `openssh` CA를 사용한다.**
|
||||
> `tctl auth export --type=user`로 나오는 CA가 아니라,
|
||||
> `cert_authority` 리소스에서 `type=openssh`인 CA를 sshd에 등록해야 한다.
|
||||
|
||||
### OpenSSH CA 추출
|
||||
|
||||
```bash
|
||||
# openssh CA 추출 (TrustedUserCAKeys에 넣을 키)
|
||||
tctl get cert_authority --format=json | python3 -c "
|
||||
import json, sys, base64
|
||||
data = json.load(sys.stdin)
|
||||
for ca in data:
|
||||
kind = ca.get('spec', {}).get('type', 'unknown')
|
||||
if kind == 'openssh':
|
||||
keys = ca.get('spec', {}).get('active_keys', {}).get('ssh', [])
|
||||
for k in keys:
|
||||
pub = base64.b64decode(k.get('public_key', '')).decode()
|
||||
print(pub.strip())
|
||||
"
|
||||
```
|
||||
|
||||
현재 openssh CA:
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPPCY1YoqnuOej43Y3THmgWZToWdjhF0g2R9/LzTNDqx
|
||||
fingerprint: SHA256:IKtA0dnz80YkkUrP9t3mnTsolSWIobDNflUK5fgXlCM
|
||||
```
|
||||
|
||||
### 호스트 인증서 발급
|
||||
|
||||
```bash
|
||||
# 호스트 인증서 생성 (principals에 호스트명, IP, UUID 모두 포함)
|
||||
tctl auth sign \
|
||||
--host=<hostname>,<FQDN>,<node-name>,localhost,127.0.0.1,::1 \
|
||||
--format=openssh \
|
||||
--out=myhost \
|
||||
--ttl=87600h
|
||||
# 결과: myhost (private key), myhost-cert.pub (host certificate)
|
||||
```
|
||||
|
||||
### sshd_config 필수 설정
|
||||
|
||||
```
|
||||
TrustedUserCAKeys /etc/ssh/teleport-user-ca.pub # openssh CA 공개키
|
||||
HostKey /etc/ssh/myhost # 호스트 비밀키
|
||||
HostCertificate /etc/ssh/myhost-cert.pub # 호스트 인증서
|
||||
```
|
||||
|
||||
### 노드 등록
|
||||
|
||||
```bash
|
||||
# tctl로 agentless 노드 등록
|
||||
cat > node.yaml << 'EOF'
|
||||
kind: node
|
||||
version: v2
|
||||
sub_kind: openssh
|
||||
metadata:
|
||||
name: <unique-name>
|
||||
labels:
|
||||
env: prod
|
||||
spec:
|
||||
addr: <host>:<port>
|
||||
hostname: <display-name>
|
||||
EOF
|
||||
tctl create -f node.yaml
|
||||
```
|
||||
|
||||
### 서버 요구사항
|
||||
|
||||
- sshd 계정이 locked 상태면 안 됨 (`usermod -p '*' <username>`)
|
||||
- HostKey/HostCertificate 파일 권한: private key 600, cert.pub 644
|
||||
- sshd가 `-h` 플래그로 호스트키를 지정하는 이미지는 HostCertificate가 무시될 수 있음
|
||||
|
||||
### 테스트 환경 (K8s)
|
||||
|
||||
test 네임스페이스에 debian:bookworm-slim 기반 sshd Pod 배포하여 검증 완료 (2026-03-26).
|
||||
|
||||
## 관리 명령
|
||||
|
||||
```bash
|
||||
# 사용자 목록
|
||||
kubectl exec -n teleport deploy/teleport-cluster-auth -- tctl users ls
|
||||
|
||||
# 패스워드 리셋
|
||||
kubectl exec -n teleport deploy/teleport-cluster-auth -- tctl users reset <username>
|
||||
|
||||
# 유저 logins 설정
|
||||
tctl users update <username> --set-logins=<login1>,<login2>
|
||||
|
||||
# Helm 설정
|
||||
helm get values teleport-cluster -n teleport
|
||||
|
||||
# SSH 접속
|
||||
tsh ssh <login>@<hostname>
|
||||
|
||||
# 등록된 노드 목록
|
||||
tsh ls
|
||||
```
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[apisix]] — relay4wd stream_route로 Teleport 포워딩
|
||||
- [[sshpiper]] — SSH 리버스 프록시 (별도 서비스)
|
||||
111
infra/security/vault.md
Normal file
111
infra/security/vault.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Vault 시크릿 관리
|
||||
updated: 2026-04-15
|
||||
tags: [infra, vault, security]
|
||||
---
|
||||
|
||||
## K3s 배포
|
||||
|
||||
HashiCorp Vault v1.21.2, K3s에 HA Raft 3노드로 배포 (Helm chart hashicorp/vault 0.32.0). namespace: vault. 스토리지: Longhorn 10Gi PVC. Unseal key 5개 (threshold 3), 키 파일: ~/vault-keys.json
|
||||
|
||||
## 접근
|
||||
|
||||
- UI/API: https://hcv.inouter.com
|
||||
- 트래픽 흐름: BunnyCDN (pull zone: inouter, ID 5316471) → SafeLine WAF → [[apisix]] (라우트 hcv-inouter-com) → K3s Traefik (192.168.9.134/140/214:443) → vault-active:8200
|
||||
- K3s Ingress: vault-ui (class traefik, TLS wildcard-inouter-com-tls)
|
||||
- APISIX upstream: hcv-inouter-com (roundrobin, 3노드 443)
|
||||
|
||||
## Root Token
|
||||
|
||||
Vault root token은 만료 없음 (TTL: 0s)
|
||||
|
||||
## 접근 정책
|
||||
|
||||
접근 정책: infra-read(읽기 전용), infra-admin(읽기/쓰기)
|
||||
|
||||
## 시크릿 구조 (KV v1)
|
||||
|
||||
⚠️ **KV v1** — 버전 관리 없음. 덮어쓰기 주의.
|
||||
⚠️ **시크릿 읽을 때 모든 키를 확인할 것** — 한 경로에 여러 키가 있음 (예: cloudflare에 api_token과 global_api_key 둘 다 있음)
|
||||
|
||||
### 전수 목록 (2026-04-15 실측)
|
||||
|
||||
| 카테고리 | 경로 | 내용 |
|
||||
|----------| ai/ | brave, context7, deepseek, google/drive-mcp, openai, openrouter, pinecone, testsprite, vertex |
|
||||
| apps/ | anomaly-detect, cfb-manager, cf-multisite, discord, figma, gitea, gitea/registry, k3s, myapp, n8n, nocodb, ops-agents-ssh, outline, portainer, postgres, sftpgo, telegram-ai-support, trader, twilio, waf-saas |
|
||||
| auth/ | api-keys/openai, api-keys/stripe, github/oauth-gitea, google/ca/external-account-key, google/ca/service-account, google/oauth-gitea |
|
||||
| cloud/ | alibaba, aws, backblaze, backblaze/restic, bunnycdn, cloudflare, cloudflare-netbis, cloudflare/r2, cloudflare/turnstile-crowdsec-captcha, cloudflare/turnstile-inouter-bunny, latitude, lightsail, linode, r2-gitea, r2-multisite, r2-sftpgo, supabase, vultr, zenlayer |
|
||||
| company/ | bank, info, ironclad, korbit, koreaexim, popbill |
|
||||
| database/ | bunnydb/cs-blocklist, postgres, redis |
|
||||
| domain/ | globalping, maxmind, namecheap, namecheap/api, namecheap/api-server, namecheap/deposit-api, namecheap/registrant |
|
||||
| infra/ | apisix, argocd, cert-manager, cf-tunnel-manager, crowdsec-bunny-bouncer, google/eab, ilo, k8s/infra-tool, mariadb, safeline, ssh, ssh/id_ed25519, tailscale, vault-sync |
|
||||
| messaging/ | discord/bot, discord-brokkr, discord/claudechannel, discord-claude-code, discord/nocodb-webhook, discord/webhook-heimdall, discord/webhook-relay, mailgun/api-key, mailgun/smtp, telegram |
|
||||
| openclaw/ | discord/main-bot, gateway/local, gitea/main, integrations/discord, oauth/gmail, runtime/test, test, tools/brave |
|
||||
| product/ | irondesk/ton-wallet, irondesk/tron |
|
||||
|
||||
|
||||
### 자주 사용하는 시크릿 (빠른 참조)
|
||||
|
||||
| 용도 | 경로 | 주요 키 | 주의사항 |
|
||||
|------|------|---------|----------|
|
||||
| **Cloudflare API** | `cloud/cloudflare` | `api_token` (제한), **`global_api_key`** (전체 권한), `email`, `account_id` | Rulesets/Firewall/Rate Limits는 `global_api_key`만 접근 가능 |
|
||||
| BunnyCDN API | `cloud/bunnycdn` | `api_key` | |
|
||||
| Gitea API | `apps/gitea` | `api_token`, `url`, `admin_password` | |
|
||||
| Outline API | `apps/outline` | `api-key` (kappa), `brokkr-api-key` (에이전트) | |
|
||||
| Portainer API | `apps/portainer` | `api_token`, `url` | |
|
||||
| Turnstile (crowdsec) | `cloud/cloudflare/turnstile-crowdsec-captcha` | `sitekey`, `secret_key` | |
|
||||
| Mailgun | `messaging/mailgun/api-key` | | |
|
||||
| Discord 봇 | `messaging/discord/bot` | | |
|
||||
| ops 에이전트 SSH | `apps/ops-agents-ssh` | `private_key`, `public_key` | |
|
||||
|
||||
## SSH CA (Signed Certificates)
|
||||
|
||||
Vault SSH Secrets Engine (ssh-client-signer/) 활성화. CA 키: ed25519. 역할: admin (allowed_users: root,kaffa,admin, TTL 8h, max 24h).
|
||||
|
||||
CA 등록 완료 서버:
|
||||
- [[infra-hosts|apisix-osaka]] (100.108.39.107) — root
|
||||
- [[infra-hosts|incus-jp1]] (100.109.123.1) — kaffa
|
||||
- [[infra-hosts|incus-kr1]] (100.84.111.28) — kaffa
|
||||
- [[infra-hosts|incus-kr2]] (100.119.109.41) — kaffa
|
||||
- [[zlambda]] (100.78.51.18, 구 sandbox-tokyo) — root **[2026-04-08 NixOS 재설치 후 미등록, 재등록 필요]**
|
||||
- [[infra-hosts|jump-seoul]] (100.120.61.54) — admin
|
||||
|
||||
미등록: safeline-osaka (응답 없음)
|
||||
|
||||
자동화: ~/.ssh/vault-sign.sh가 인증서 만료 시 자동 재발급 (vault CLI 기반). 인증서에 root,kaffa,admin principals 포함. ~/.ssh/config에 Match exec로 연동. `ssh apisix-osaka` 등 일반 SSH처럼 사용 가능.
|
||||
|
||||
## MCP 서버
|
||||
|
||||
### 실 배포 (2026-04-15 정정)
|
||||
|
||||
vault-mcp-server v0.2.0 은 **jp1 Incus `vault` 컨테이너 (10.253.101.58) 단일 인스턴스**. systemd 유닛 `vault-mcp-server.service`로 streamable-http 모드 가동 (`--transport-host=0.0.0.0 --transport-port=8080 --mcp-endpoint=/mcp`). 같은 컨테이너에 HashiCorp Vault 본체(포트 8200)도 동일 프로세스 공간에 존재 → vault 백엔드는 `http://127.0.0.1:8200`.
|
||||
|
||||
> [!warning] 과거 문서 오류 정정 (2026-04-15)
|
||||
> 이전 기록에 "K3s vault namespace Deployment 배포", "vault-active.vault.svc.cluster.local:8200" 이라 적혀 있었으나 실제와 다름. K3s 안에는 vault 파드/Deployment 없음. 상세: `history/2026-04-15-vault-mcp-duplicate-investigation.md`
|
||||
|
||||
### 접근 경로 (3가지, 모두 같은 jp1 프로세스로 수렴)
|
||||
|
||||
```
|
||||
hcv.inouter.com → Traefik@K3s:443 → 10.253.101.58:8200 (Vault UI/API)
|
||||
vault-mcp.inouter.com → Traefik@K3s:443 → 10.253.101.58:8080 (MCP)
|
||||
http://10.253.101.58:8080/mcp (Tailscale 직결, kappa Claude 현행)
|
||||
```
|
||||
|
||||
K3s `tools` 네임스페이스의 `vault-mcp` 는 **Pod 없는 리버스 프록시 파사드**: Service(selector 비움) + 수동 EndpointSlice가 10.253.101.58 로 고정 + IngressRoute 2개(hcv, vault-mcp). ArgoCD 앱 `argocd/vault-mcp`, Helm chart `kaffa/helm-charts charts/app` + `values/vault-mcp.yaml` (deployment 없는 ingress-only 렌더링).
|
||||
|
||||
### 주의
|
||||
|
||||
- **`hcv.inouter.com/mcp` 는 MCP 엔드포인트 아님** — Vault가 /mcp 경로를 UI(/ui/)로 307 리다이렉트. 외부 MCP hostname 필요하면 `vault-mcp.inouter.com/mcp` 사용.
|
||||
- 외부 공개 경로는 BunnyCDN/Cloudflare 비경유 (Traefik wildcard cert 직접 TLS 종단). 내부 LAN/Tailscale 전용.
|
||||
- kappa Claude MCP는 현재 `http://10.253.101.58:8080/mcp` (Tailscale 암호화 의존). 장기적으로 `https://vault-mcp.inouter.com/mcp` 로 이전 검토 가능 (Traefik TLS + wildcard cert 혜택).
|
||||
|
||||
### 재배포 자료
|
||||
|
||||
- Helm chart: `gitea.inouter.com/kaffa/helm-charts.git charts/app`
|
||||
- values: `kaffa/helm-charts values/vault-mcp.yaml`
|
||||
- jp1 컨테이너 내부 바이너리: `/usr/local/bin/vault-mcp-server`
|
||||
- systemd: `/etc/systemd/system/vault-mcp-server.service`
|
||||
|
||||
## 관련 서비스
|
||||
|
||||
[[cert-manager]], [[gitea]], [[irondesk]] 등에서 Vault 시크릿을 참조
|
||||
Reference in New Issue
Block a user