538 lines
26 KiB
Markdown
538 lines
26 KiB
Markdown
# 🔒 MAM 개인 전용 브로커(Private Broker) 구축 및 연동 가이드 (`PRIVATE_SERVER.md`)
|
|
|
|
- **작성일**: 2026-08-20 (Rev.2)
|
|
- **문서 목적**: MAM(Multi-Agent Mux)의 공개 브로커 의존성 및 보안 결함(A-2)을 해소하기 위한 개인 전용 브로커(NATS / Mosquitto) 구축, 운영, 다능성 활용 및 MAM 클라이언트 연동 가이드.
|
|
- **연계 문서**: [`NATS_REPORT.md`](NATS_REPORT.md), [`implementation_plan.md`](implementation_plan.md), [`IMPROVEMENTS.md`](IMPROVEMENTS.md)
|
|
|
|
---
|
|
|
|
## 1. 개요 및 도입 배경
|
|
|
|
현재 MAM 프레임워크의 기본 메시징 브로커는 공개 서버(`broker.hivemq.com:1883`)로 설정되어 있습니다. 개인 전용 브로커(Private Broker)를 구축하여 연결하면 **클라이언트 코드 변경 없이(0줄 변경)** 보안 위험을 원천 차단하고 네트워크 안정성을 대폭 향상시킬 수 있습니다.
|
|
|
|
```
|
|
[MAM Orchestrator / Agents]
|
|
│
|
|
▼ (MQTT 3.1.1 / TLS)
|
|
[Private Dedicated Broker] ───> 사설망/개인 서버 (NATS Server / Mosquitto)
|
|
• 외부 불법 트래픽 100% 차단 (A-2 보안 해소)
|
|
• JetStream 영속성 및 NKey/JWT ACL 지원
|
|
• 초저지연 (<1ms) 및 무제한 대역폭
|
|
```
|
|
|
|
MAM의 제어 평면(`run_loop.sh`의 `wait_for_job` 파일시스템 폴링)은 브로커와 100% 독립적으로 작동하므로, 브로커는 **비동기 관측(observability) 사이드카** 역할을 수행합니다.
|
|
|
|
---
|
|
|
|
## 2. 해결 영역 매트릭스 (브로커 전환 vs 코드 패치)
|
|
|
|
전용 브로커 구축으로 즉시 해결되는 영역과, 로컬 코드 패치(Track 0)가 병행되어야 하는 영역의 명확한 구분입니다.
|
|
|
|
| 구분 | 당면 과제 | 개인 브로커 구축 시 | 로컬 코드 패치 필요 여부 (Track 0) |
|
|
|---|---|:---:|:---:|
|
|
| **보안 (A-2)** | 공개 브로커 노출 및 외부 악의적 이벤트 수신 위협 | 🟢 **100% 즉시 해소** (사설망/ACL 격리) | Track 2에서 토큰 발급 강제 |
|
|
| **안정성** | 공개 브로커의 예고 없는 순단 및 속도 제한(Rate-limit) | 🟢 **100% 즉시 해소** (전용 리소스) | — |
|
|
| **내결함성 (B-14)** | 브로커 일시 장애 시 65분 루프 정지(Hang) 결함 | ⚠️ 브로커 점검/순단 시 여전히 위험 | 🔴 **필수 (Track 0 Step 1 선행 패치)** |
|
|
| **지연/오판 (B-15)** | 브로커 다운 시 120초 지연 및 정상 작업의 에러 오판정 | ⚠️ 브로커 점검/순단 시 여전히 위험 | 🔴 **필수 (Track 0 Step 2 & 3 선행 패치)** |
|
|
|
|
> [!IMPORTANT]
|
|
> **방탄 아키텍처 원칙:**
|
|
> "Track 0(`B-14`, `B-15`) 패치를 통해 브로커가 다운되어도 루프가 100% 정상 완주하도록 로컬 디스크 내결함성을 먼저 확보하고, 개인 브로커를 연결하여 A-2 보안과 성능을 완결합니다."
|
|
|
|
---
|
|
|
|
## 3. 전용 브로커 추천 및 비교
|
|
|
|
MAM 클라이언트는 표준 `paho-mqtt`를 사용하므로, MQTT 3.1.1을 지원하는 모든 브로커와 100% 호환됩니다.
|
|
|
|
| 비교 항목 | 🏆 `nats-server` (강력 권장) | `eclipse-mosquitto` (대안) |
|
|
|---|---|---|
|
|
| **아키텍처** | Go 단일 정적 바이너리 (Zero Dependency) | C 기반 경량 오픈소스 브로커 |
|
|
| **주요 특징** | • 내장 MQTT 3.1.1 리스너 (`mqtt { port: 1883 }`)<br>• JetStream 엔진 내장 (이벤트 영속화 및 복구)<br>• NKey/JWT 기반 계정 및 Subject별 세분화된 ACL<br>• WebSocket 및 NATS 네이티브 프로토콜 동시 서빙 | • 가장 널리 쓰이는 표준 경량 MQTT 브로커<br>• 낮은 메모리 점유율 (~10MB) |
|
|
| **추천 용도** | 모던 인프라, 확장성, 감사 로그 영속화, 홈랩 통합 | 정통 초경량 임베디드/단일 목적 환경 |
|
|
| **배포 난이도** | 🟢 바이너리 1개 실행 또는 Docker 1줄 | 🟢 패키지 매니저 (`apt`, `brew`) 또는 Docker |
|
|
|
|
---
|
|
|
|
## 4. 개인 서버 브로커 배포 가이드
|
|
|
|
### 4.1 `nats-server` 배포 (권장)
|
|
|
|
`nats-server`에서 MQTT를 활성화하려면 설정 파일(`nats.conf`)에 `mqtt { port: 1883 }` 블록과 `jetstream { }` 블록이 반드시 포함되어야 합니다.
|
|
|
|
> [!NOTE]
|
|
> `nats-server`의 `-m` 플래그는 HTTP 모니터링 포트(`--http_port`)를 지정하는 옵션이며, MQTT를 켜는 플래그가 아닙니다. MQTT 활성화는 반드시 `-c nats.conf` 설정 파일을 통해 구성해야 합니다.
|
|
|
|
#### 1) 공통 설정 파일 (`nats.conf`)
|
|
```conf
|
|
# nats.conf
|
|
server_name: mam-hub
|
|
|
|
# JetStream 영속 스토리지 (MQTT QoS 1 및 Retained 메시지 처리에 필수)
|
|
jetstream {
|
|
store_dir: "/data" # Docker 환경 기본 스토리지 (네이티브 실행 시 $HOME 전개)
|
|
max_file: 10G # 홈랩 디스크 상한 설정
|
|
}
|
|
|
|
# HTTP 모니터링 엔드포인트 (/varz, /jsz 대시보드)
|
|
http_port: 8222
|
|
|
|
# 평면 A: MAM MQTT 3.1.1 프로토콜 리스너
|
|
mqtt {
|
|
port: 1883
|
|
}
|
|
|
|
# 평면 B: 홈랩/웹 브라우저 대시보드용 WebSocket 리스너 (선택 사항)
|
|
websocket {
|
|
port: 8080
|
|
no_tls: true # 내부 사설망 한정
|
|
}
|
|
```
|
|
|
|
#### 2) 배포 방법 A. Docker / Docker Compose (권장)
|
|
|
|
**단일 Docker 실행:**
|
|
```bash
|
|
# 호스트에 nats.conf 생성 후 실행 (D-2: alpine 고정 핀, D-3: 루프백/바인드 한정)
|
|
docker run -d \
|
|
--name mam-nats \
|
|
--restart unless-stopped \
|
|
-p "${MQTT_BIND:-127.0.0.1}:1883:1883" \
|
|
-p "${NATS_BIND:-127.0.0.1}:4222:4222" \
|
|
-p "127.0.0.1:8222:8222" \
|
|
-p "${WS_BIND:-127.0.0.1}:8080:8080" \
|
|
-v ./nats.conf:/etc/nats/nats.conf:ro \
|
|
-v nats-data:/data \
|
|
nats:2.12-alpine \
|
|
-c /etc/nats/nats.conf
|
|
```
|
|
|
|
**Docker Compose (`docker-compose.yml`):**
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
nats:
|
|
image: nats:2.12-alpine
|
|
container_name: mam-nats
|
|
restart: unless-stopped
|
|
command: ["-c", "/etc/nats/nats.conf"]
|
|
ports:
|
|
- "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 포트 (평면 A: MAM)
|
|
- "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS 기본 포트 (평면 B)
|
|
- "127.0.0.1:8222:8222" # HTTP 모니터링 (/varz, /jsz, /healthz)
|
|
- "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (평면 B)
|
|
volumes:
|
|
- ./nats.conf:/etc/nats/nats.conf:ro
|
|
- nats-data:/data
|
|
|
|
volumes:
|
|
nats-data:
|
|
```
|
|
|
|
#### 3) 배포 방법 B. 네이티브 바이너리 설치 (macOS / Linux — 비루트 사용자 공간)
|
|
|
|
macOS의 sealed APFS 루트 볼륨(`/data`) 권한 문제를 방지하기 위해 사용자 홈 디렉터리(`~/.config/nats/`, `~/.local/share/nats/data`)를 기본 스토리지로 사용합니다.
|
|
|
|
```bash
|
|
# 설정 및 데이터 디렉터리 생성 (sudo 불필요)
|
|
mkdir -p ~/.config/nats ~/.local/share/nats/data
|
|
|
|
# 설정 파일 작성 (D-1: 비인용 heredoc <<EOF 으로 $HOME 을 파일 생성 시점에 절대경로로 고정)
|
|
cat <<EOF > ~/.config/nats/nats.conf
|
|
server_name: mam-hub
|
|
jetstream {
|
|
store_dir: "$HOME/.local/share/nats/data"
|
|
max_file: 10G
|
|
}
|
|
http_port: 8222
|
|
mqtt {
|
|
port: 1883
|
|
}
|
|
websocket {
|
|
port: 8080
|
|
no_tls: true
|
|
}
|
|
EOF
|
|
|
|
# macOS (Homebrew 설치 및 실행)
|
|
brew install nats-server
|
|
nats-server -c ~/.config/nats/nats.conf &
|
|
|
|
# Linux (x86_64 단일 바이너리 설치 및 실행)
|
|
curl -L https://github.com/nats-io/nats-server/releases/download/v2.10.20/nats-server-v2.10.20-linux-amd64.tar.gz | tar xz
|
|
sudo mv nats-server-v2.10.20-linux-amd64/nats-server /usr/local/bin/
|
|
nats-server -c ~/.config/nats/nats.conf &
|
|
```
|
|
|
|
---
|
|
|
|
### 4.2 `mosquitto` 배포 (대안)
|
|
|
|
#### Docker 실행:
|
|
```bash
|
|
docker run -d \
|
|
--name mam-mosquitto \
|
|
--restart unless-stopped \
|
|
-p 1883:1883 \
|
|
-v ./mosquitto.conf:/mosquitto/config/mosquitto.conf \
|
|
eclipse-mosquitto:latest
|
|
```
|
|
|
|
**기본 `mosquitto.conf` 설정 파일 예시:**
|
|
```conf
|
|
listener 1883
|
|
allow_anonymous true
|
|
persistence true
|
|
persistence_location /mosquitto/data/
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 하나의 서버로 여러 프로젝트 — `nats-server` 다능성 (Versatility)
|
|
|
|
`nats-server`의 다능성은 **MAM을 네이티브 NATS로 이관할 이유가 아니라, MAM 코드를 한 줄도 바꾸지 않고도 얻을 수 있는 부가적 이득**입니다.
|
|
|
|
### 5.1 두 개의 소비 평면 (Two Consumption Planes)
|
|
|
|
`nats-server`는 단일 프로세스 내에서 여러 프로토콜 리스너를 동시에 구동하므로, MAM의 단순성과 개인 홈랩의 확장성을 완벽히 양립시킵니다.
|
|
|
|
```
|
|
┌───────────────────────────────────────────────────────────┐
|
|
│ nats-server (단일 인스턴스) │
|
|
├─────────────────────────────┬─────────────────────────────┤
|
|
│ 평면 A: MAM 워크로드 │ 평면 B: 홈랩/개인 프로젝트 │
|
|
├─────────────────────────────┼─────────────────────────────┤
|
|
프로토콜 │ MQTT 3.1.1 (포트 1883) │ NATS(4222), WebSocket(8080) │
|
|
클라이언트 │ paho-mqtt (코드 변경 0줄) │ nats-py, nats.js, CLI 등 자유 │
|
|
사용 기능 │ QoS 1, Retain, 와일드카드, TLS│ JetStream 리플레이, KV, Object│
|
|
설계 원칙 │ 초경량 동기 CLI 핫패스 보존 │ 고급 비동기 이벤트 스트리밍 │
|
|
공유 자원 │ └───── 단일 정적 바이너리 / JetStream 스토리지 / ACL ─────┘│
|
|
└───────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 5.2 교차 프로토콜 브리징 (Cross-Protocol Bridging)
|
|
- `nats-server`는 내부적으로 MQTT 토픽(`/`)을 NATS Subject(`.`)로 실시간 자동 변환합니다.
|
|
- MAM 에이전트가 MQTT 토픽 `python/mqtt/jobs/<job_id>/events`로 이벤트를 발행하면, 웹 브라우저나 타 프로젝트의 NATS 구독자는 NATS Subject `python.mqtt.jobs.<job_id>.events` 또는 `python.mqtt.jobs.*.events`로 즉시 실시간 수신할 수 있습니다.
|
|
- **실용적 이점**: MAM 소스 코드를 단 1줄도 수정하지 않고도 React/Vue 웹 대시보드나 터미널 모니터링 툴을 즉시 부착할 수 있습니다.
|
|
- **경계 (필수 인지 — N-1)**: 교차 프로토콜 브리징은 **라이브 스트리밍에 한정**됩니다. MQTT의 retained 메시지는 MQTT 구독자에게만 전달되므로(`mqttSendRetainedMsgsToNewSubs`가 MQTT SUBSCRIBE 경로 전용), 잡이 끝난 뒤 접속한 NATS/WebSocket 대시보드는 그 잡의 **종료 이벤트를 수신하지 못합니다**. 사후 상태가 필요하면 (a) 대시보드를 MQTT(1883)로 연결하거나 (b) §5.3 JetStream 리플레이 스트림을 옵트인하십시오.
|
|
- **계정 배치**: 관측 클라이언트는 §5.5 처방대로 `MAM` 계정 안의 읽기 전용 사용자(`mam_observer`)로 접속해야 합니다. 다른 계정에서는 subject가 보이지 않습니다.
|
|
- **주의 사항**: 토픽 레벨 내에 마침표(`.`)가 포함되면 NATS 계층에서 토큰이 분리될 수 있으나, MAM의 `job_id`는 8자리 hex, 워크스페이스 지문은 12자리 hex이므로 안전합니다.
|
|
|
|
### 5.3 JetStream 이벤트 리플레이 (Event Replay)
|
|
- `python.mqtt.jobs.>` Subject를 구독하는 JetStream 스트림을 생성하면, 지난 작업의 이벤트 스트림 전체를 시점 지정(Time-based) 또는 시퀀스 지정(Sequence-based)으로 사후 리플레이할 수 있습니다.
|
|
- **주의 사항**: 이 기능은 옵트인(Opt-in)이며, MQTT QoS 1 처리를 위한 내부 시스템 스트림(`$MQTT_*`)과 별개로 관리됩니다. 디스크 용량 관리를 위해 `max_age`나 `max_bytes` 상한을 반드시 설정해야 합니다.
|
|
|
|
### 5.4 내장 Key-Value (KV) 및 Object Store
|
|
- 홈랩 및 개인 프로젝트에서 Redis나 MinIO 같은 별도 인프라를 띄우지 않고도 `nats-server` 내장 KV 및 Object Store를 즉시 사용할 수 있습니다.
|
|
- **금지 사항 (Non-Goal)**: MAM의 로컬 레지스트리(`.mam/jobs/*.json`)를 JetStream KV로 대체해서는 안 됩니다 (`wait_for_job`의 fcntl 및 파일시스템 폴링 계약 유지).
|
|
|
|
### 5.5 멀티테넌트 계정 분리 및 보안
|
|
- 단일 서버 내에서 `MAM` 전용 계정과 `HOME` 개인 계정을 분리하여 리소스 쿼터와 권한을 완벽히 격리할 수 있습니다.
|
|
- **권고 배치**: MAM과 이를 관측하는 대시보드는 동일한 계정(`MAM`)에 배치하고(관측자는 읽기 전용 `mam_observer` 역할 부여), 무관한 홈랩 서비스는 별도 계정(`HOME`)에 배치합니다.
|
|
|
|
---
|
|
|
|
## 6. MAM 클라이언트 연동 설정 (`.mam.env`)
|
|
|
|
개인 서버 브로커가 구동되면, MAM 저장소 루트의 [`.mam.env`](file:///.mam.env) 파일에 개인 서버 주소를 등록합니다.
|
|
|
|
> [!NOTE]
|
|
> MAM 코드(`mqtt_common.py`)는 `MQTT_*` 접두사의 환경변수를 읽습니다. 이전 비공식 문서의 `MAM_MQTT_*` 변수는 무효하므로 반드시 아래의 표준 변수명을 사용해야 합니다.
|
|
|
|
```bash
|
|
# ==============================================================================
|
|
# MAM Private MQTT Broker Configuration (.mam.env)
|
|
# ==============================================================================
|
|
|
|
# ── 모델 T (Tailscale 사설 오버레이망 권장) ───────────────────────
|
|
MQTT_BROKER="mam-hub.tailXXXX.ts.net" # 또는 100.x.y.z (평문이므로 IP 사용 가능)
|
|
MQTT_PORT=1883
|
|
MQTT_TLS=0
|
|
MQTT_USERNAME=mam_agent
|
|
MQTT_PASSWORD=replace_me_with_token
|
|
MQTT_KEEPALIVE=60 # WAN 구간 권장 연결 유지 시간
|
|
|
|
# ── 모델 P (공개 TLS / Let's Encrypt 모델) ───────────────────────
|
|
# MQTT_BROKER="mam-broker.example.com" # D-4: 반드시 인증서 SAN 의 DNS 이름 (IP 금지)
|
|
# MQTT_PORT=8883
|
|
# MQTT_TLS=1
|
|
# MQTT_USERNAME=mam_agent
|
|
# MQTT_PASSWORD=replace_me_with_token
|
|
# MQTT_CA_CERTS 는 Let's Encrypt 사용 시 '설정하지 않음' (시스템 신뢰 저장소 사용)
|
|
```
|
|
|
|
*참고: OS 환경변수에 동일한 이름이 이미 `export`되어 있는 경우 OS 환경변수가 `.mam.env` 파일 설정보다 우선합니다.*
|
|
|
|
---
|
|
|
|
## 7. 연동 및 동작 검증 테스트 (4-Step Verification)
|
|
|
|
개인 서버 브로커와의 연동 상태를 정확하게 검증하는 4단계 절차입니다.
|
|
|
|
### Step 1. 브로커 리스너 및 JetStream 상태 확인
|
|
```bash
|
|
# MQTT 리스너 활성화 확인
|
|
curl -s http://127.0.0.1:8222/varz | grep -i mqtt
|
|
|
|
# JetStream 엔진 정상 구동 확인
|
|
curl -s http://127.0.0.1:8222/jsz
|
|
```
|
|
|
|
### Step 2. 임시 잡 등록 및 연결 검증 이벤트 발행
|
|
`publish_event.py`는 레지스트리에 등록된 잡에 대해서만 발행을 수행하므로, 임시 잡을 등록하고 발행한 후 완료 처리합니다.
|
|
|
|
```bash
|
|
# 1) 임시 잡 등록 (자동 채번된 JID 캡처)
|
|
JID=$(.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \
|
|
--registry-dir .mam/jobs \
|
|
register \
|
|
--prompt "Private broker connectivity test" \
|
|
--agent-session "herdr:test")
|
|
echo "registered test job: $JID"
|
|
|
|
# 2) 이벤트 발행 (상세 로그 출력 및 rc=0 단언)
|
|
.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py \
|
|
--registry-dir .mam/jobs \
|
|
--job "$JID" \
|
|
--event progress \
|
|
--detail "Private broker connection verified" -v
|
|
|
|
# 3) 테스트 잡 종결 처리 (미종결 시 --wait-any 유령 잡 잔존 방지)
|
|
.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \
|
|
--registry-dir .mam/jobs status --job "$JID" --set completed
|
|
```
|
|
|
|
### Step 3. 접속 대상 브로커 IP 단언
|
|
Step 2의 `-v` 출력 로그 또는 `.mam/delegate_job_logs/$JID/events.ndjson` 파일에서 실제 접속 호스트가 개인 브로커 IP로 나타나고 `broker.hivemq.com`이 포함되지 않았는지 확인합니다.
|
|
|
|
### Step 4. 단위 회귀 테스트 검증
|
|
```bash
|
|
.venv/bin/python -m pytest tests/ -q
|
|
```
|
|
*참고: MAM의 기본 단위/컴포넌트 테스트 스위트는 모의(Mock) 객체를 사용하므로 브로커 연결 여부와 무관하게 100% 통과합니다. 실제 네트워크 연동 검증은 Step 1~3이 담당합니다.*
|
|
|
|
---
|
|
|
|
## 8. 권장 실행 순서
|
|
|
|
```
|
|
[Phase 1: 내결함성 확보] ──> [Phase 2: 개인 브로커 가동] ──> [Phase 3: A-2 보안 완전 종결]
|
|
Track 0 (B-14, B-15) nats-server (nats.conf) 지문 토픽 및 인증 토큰 발급
|
|
로컬 디스크 폴백 패치 .mam.env 환경변수 연동 외부 간섭 100% 차단
|
|
```
|
|
|
|
1. **Phase 1 (Track 0 선행 패치)**: `publish_event.py`와 `job_subscriber.py`의 로컬 디스크 폴백(`B-14`, `B-15`)을 먼저 적용하여 브로커 다운 시에도 루프가 멈추지 않는 방탄 구조를 확립합니다.
|
|
2. **Phase 2 (개인 브로커 가동)**: 개인 서버에 `nats-server -c nats.conf`를 구동하고 `.mam.env`에 `MQTT_BROKER`를 연결합니다.
|
|
3. **Phase 3 (A-2 보안 완전 종결)**: 워크스페이스 지문 토픽(`mam/<sha256[:12]>/jobs/...`) 및 무조건 토큰 발급을 적용하여 공개 브로커 위험을 완전히 영구 폐기합니다.
|
|
|
|
---
|
|
|
|
## 9. 원격 서버 `nats-server` Docker 프로덕션 배포 가이드 (Track 1R)
|
|
|
|
원격 VPS 또는 상시 가동 홈랩 서버에 프로덕션 수준의 `nats-server`를 Docker 기반으로 구축하고 MAM과 연동하는 표준 절차입니다.
|
|
|
|
### 9.1 프로덕션 `nats.conf`
|
|
|
|
```conf
|
|
# nats.conf — 원격 프로덕션 (컨테이너 내부 절대경로 기준)
|
|
server_name: mam-hub
|
|
|
|
# ── JetStream: MQTT retained/QoS1 저장소. MAM 종료 이벤트 재수신이 여기에 의존 ──
|
|
jetstream {
|
|
store_dir: "/data" # D-1: 절대경로 고정. '~' 도, 인용된 "$HOME" 도 확장되지 않음
|
|
max_file: 10G
|
|
max_mem: 256M
|
|
}
|
|
|
|
http_port: 8222 # D-3: 호스트 게시는 loopback 한정 (§9.3)
|
|
|
|
mqtt {
|
|
port: 1883 # 공개 TLS 모델에서는 8883 + tls 블록 (§9.3)
|
|
ack_wait: 60s # WAN RTT 흡수 (기본 30s)
|
|
max_ack_pending: 1024 # 기본 100 → 다중 에이전트 동시 발행 여유
|
|
}
|
|
|
|
websocket {
|
|
port: 8080
|
|
no_tls: true # 사설망/tailnet 한정
|
|
}
|
|
|
|
# ── 인증 및 멀티테넌시 (Rev.2: C1/C2 반영) ────────────────────────────────
|
|
accounts {
|
|
MAM: {
|
|
jetstream: enabled # MQTT 내부 스트림이 이 계정 안에 생성됨
|
|
users: [
|
|
# 발행자 겸 구독자 — MAM 에이전트 본체
|
|
{ user: mam_agent, password: $MAM_BROKER_PASS }
|
|
|
|
# 관측자 — 대시보드/모니터링. PRIVATE_SERVER.md §5.5 의 '동일 계정 배치' 처방
|
|
{ user: mam_observer, password: $MAM_OBSERVER_PASS,
|
|
permissions: {
|
|
subscribe: { allow: ["python.mqtt.jobs.>"] } # M3 이후: "mam.<fp>.jobs.>"
|
|
publish: { deny: [">"] }
|
|
}
|
|
}
|
|
]
|
|
}
|
|
# MAM 과 무관한 홈랩 서비스 전용. MAM subject 는 보이지 않음(의도된 격리)
|
|
HOME: { jetstream: enabled, users: [ { user: home, password: $HOME_BROKER_PASS } ] }
|
|
SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] }
|
|
}
|
|
system_account: SYS
|
|
```
|
|
|
|
> [!IMPORTANT]
|
|
> **관측자는 반드시 `MAM` 계정 안에 둡니다.** NATS 계정은 하드 격리 경계이므로 `user: home`(계정 `HOME`)으로 접속한 클라이언트는 `python.mqtt.jobs.>`를 구독해도 **0건**을 받습니다. 서로 다른 신뢰 도메인이라 계정을 반드시 갈라야 하는 예외 상황은 **부록 X(Option A)** 를 따르십시오.
|
|
|
|
### 9.2 프로덕션 `docker-compose.yml`
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
nats:
|
|
image: nats:2.12-alpine # D-2: latest(=scratch)는 healthcheck 불가
|
|
container_name: mam-nats
|
|
restart: unless-stopped
|
|
command: ["-c", "/etc/nats/nats.conf"]
|
|
environment:
|
|
# 미해결 $VAR 는 파싱 에러 → 시크릿 누락 시 '기동 실패'로 fail-closed
|
|
MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set in .env}
|
|
MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set in .env}
|
|
HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set in .env}
|
|
SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set in .env}
|
|
ports:
|
|
- "${MQTT_BIND:-127.0.0.1}:1883:1883"
|
|
- "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS 네이티브 프로토콜 (Plane B)
|
|
- "127.0.0.1:8222:8222" # D-3: 무인증 모니터링은 loopback 한정
|
|
- "${WS_BIND:-127.0.0.1}:8080:8080"
|
|
volumes:
|
|
- ./nats.conf:/etc/nats/nats.conf:ro
|
|
- nats-data:/data
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 20s
|
|
logging:
|
|
driver: json-file
|
|
options: { max-size: "10m", max-file: "3" }
|
|
|
|
volumes:
|
|
nats-data:
|
|
```
|
|
|
|
> [!WARNING]
|
|
> **Docker의 published 포트는 UFW를 우회합니다.** Docker가 삽입하는 NAT/FORWARD 규칙이 `ufw`의 INPUT 체인보다 먼저 평가되므로, `ufw deny 1883`을 걸어도 `-p 1883:1883`으로 게시한 포트는 인터넷에 열립니다. 본 구성이 방화벽에만 의존하지 않고 **published 포트 자체에 바인드 주소를 명시**(`${MQTT_BIND}:1883:1883`)하는 이유입니다.
|
|
|
|
### 9.3 원격 네트워킹 & 보안 가이드
|
|
|
|
| 항목 | 🏆 **모델 T: Tailscale/WireGuard 오버레이 (권장)** | 모델 P: 공개 TLS (Let's Encrypt) |
|
|
|---|---|---|
|
|
| 인터넷 노출 면적 | **0** (공개 리스너 없음) | 8883 1개 |
|
|
| 인증서 필요 | 불필요 (`MQTT_TLS=0`) | 필수 + 90일 갱신 |
|
|
| **D-4 호스트명 제약** | **해당 없음** | 도메인 필수, IP 불가 |
|
|
| 도메인 필요 | 불필요 | **필수** |
|
|
| 이동성 | 자동 (Tailscale mesh) | 자동 |
|
|
|
|
**방화벽 (UFW) 설정 (모델 T):**
|
|
```bash
|
|
sudo ufw default deny incoming
|
|
sudo ufw default allow outgoing
|
|
sudo ufw allow 22/tcp
|
|
|
|
# tailnet 인터페이스만 허용
|
|
sudo ufw allow in on tailscale0 to any port 1883 proto tcp
|
|
sudo ufw allow in on tailscale0 to any port 4222 proto tcp
|
|
sudo ufw allow in on tailscale0 to any port 8080 proto tcp
|
|
|
|
sudo ufw enable && sudo ufw status verbose
|
|
```
|
|
|
|
**시크릿 생성 및 바인드 주소 주입 (`.env`):**
|
|
```bash
|
|
# 서버 측 compose 옆 .env
|
|
cat <<EOF > .env
|
|
MQTT_BIND=$(tailscale ip -4)
|
|
WS_BIND=$(tailscale ip -4)
|
|
MAM_BROKER_PASS=$(openssl rand -base64 32)
|
|
MAM_OBSERVER_PASS=$(openssl rand -base64 32)
|
|
HOME_BROKER_PASS=$(openssl rand -base64 32)
|
|
SYS_BROKER_PASS=$(openssl rand -base64 32)
|
|
EOF
|
|
chmod 600 .env
|
|
```
|
|
|
|
### 9.4 원격 검증 플레이북 (R-1 ~ R-10)
|
|
|
|
| ID | 검증 항목 | 방법 | 통과 기준 |
|
|
|---|---|---|---|
|
|
| **R-1** | 브로커 헬스 | SSH 터널 후 `curl -sf http://127.0.0.1:8222/healthz` | **HTTP 200** |
|
|
| **R-2** | 리스너 + TLS 신원 | `curl -s .../varz \| grep -i mqtt`; 모델 P는 `openssl s_client -connect H:8883 -servername H` | MQTT 리스너 노출, SAN에 `MQTT_BROKER` 포함 |
|
|
| **R-3** | 노출 면적 단언 | 외부 망에서 `nmap -Pn -p 1883,4222,8222,8080 <공개IP>` | 전부 closed/filtered |
|
|
| **R-4** | 왕복 pub/sub + 계정 JetStream | 임시 잡 등록 → 구독자 기동 → `progress` 발행 → 수신 → 종결 | 수신 성공, `JetStream not enabled for account` **미발생** |
|
|
| **R-5** | **retained 종료 이벤트 (MQTT)** | `--event completed` 발행 **후** 신규 `job_subscriber.py` 기동 | 즉시 최종 이벤트 수신 |
|
|
| **R-6** | 브로커 신원 단언 | `grep -c 'broker.hivemq.com' .mam/delegate_job_logs/$JID/events.ndjson` | **0**, 원격 호스트 등장 |
|
|
| **R-7** | freeze 회귀 (H-1/H-4) | freeze 경로 `publish_event.py`를 `MAM_ENV_FILE` 없이 실행 / 오타 경로 실행 | 공개 브로커로 나가지 않음 |
|
|
| **R-8** | 전체 회귀 스위트 | `.venv/bin/python -m pytest tests/ -q` | **전건 통과** |
|
|
| **R-9** | **관측자 계정 경계** | `mam_observer`로 NATS 구독 → 수신 확인. 이어서 `home`으로 동일 구독 | `mam_observer` **수신**, `home` **0건** |
|
|
| **R-10** | **retained 경계 확인 (N-1)** | 잡 종료 **후** NATS 네이티브 구독자를 새로 붙임 | **0건 수신** (retained는 MQTT 전용) |
|
|
|
|
**WAN 지연 시간 측정 (Latency Probe):**
|
|
```bash
|
|
.venv/bin/python - <<'PY'
|
|
import sys, time, statistics
|
|
sys.path.insert(0, '.agents/skills/multi-agent-mux-delegate-job/scripts')
|
|
import mqtt_common as m
|
|
cfg = m.broker_config_from_env()
|
|
print(f"target: {cfg.host}:{cfg.port} tls={cfg.tls}")
|
|
conn, rtt = [], []
|
|
for _ in range(5):
|
|
c = m.make_client("latency", cfg)
|
|
t0 = time.perf_counter(); c.connect(cfg.host, cfg.port, 10); c.loop_start()
|
|
conn.append((time.perf_counter() - t0) * 1000)
|
|
t1 = time.perf_counter()
|
|
info = c.publish("mam/latency/probe", b"x", qos=1); info.wait_for_publish(10)
|
|
rtt.append((time.perf_counter() - t1) * 1000)
|
|
c.loop_stop(); c.disconnect()
|
|
print(f"connect p50={statistics.median(conn):.1f}ms max={max(conn):.1f}ms")
|
|
print(f"qos1 rtt p50={statistics.median(rtt):.1f}ms max={max(rtt):.1f}ms")
|
|
PY
|
|
```
|
|
|
|
### 9.5 전환(Cutover) 절차 (H-2 대응)
|
|
|
|
```
|
|
[1 드레인] ──> [2 잔여 스캔] ──> [3 .mam.env 교체] ──> [4 R-1~R-10] ──> [5 레거시 차단]
|
|
```
|
|
1. **드레인**: 신규 위임 중단, 진행 중 잡이 모두 terminal 될 때까지 대기.
|
|
2. **잔여 스캔**: 옛 브로커에 핀 고정된 레코드 스캔 후 정리.
|
|
3. **`.mam.env` 교체**: 원격 브로커 주소 및 토큰 적용.
|
|
4. **검증**: R-1 ~ R-10 전건 통과 확인.
|
|
5. **레거시 차단**: 공개 브로커 설정 완전 제거.
|
|
|
|
---
|
|
|
|
### 부록 X. Option A (계정 간 Export / Import) 예외 경로
|
|
|
|
관측자가 **서로 다른 신뢰 도메인**에 속해 계정을 엄격히 분리해야 하는 경우:
|
|
|
|
```conf
|
|
accounts {
|
|
MAM: {
|
|
jetstream: enabled
|
|
users: [ { user: mam_agent, password: $MAM_BROKER_PASS } ]
|
|
exports: [ { stream: "python.mqtt.jobs.>", accounts: [HOME] } ]
|
|
}
|
|
HOME: {
|
|
users: [ { user: home, password: $HOME_BROKER_PASS } ]
|
|
imports: [ { stream: { account: MAM, subject: "python.mqtt.jobs.>" } } ]
|
|
}
|
|
SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] }
|
|
}
|
|
system_account: SYS
|
|
```
|
|
|