feat(gateway): API Key 인증 + HTTPS (Caddy) 보안 강화
- gateway.py: auth middleware — /api/* 엔드포인트에 Bearer token 필수 - Caddyfile: Let's Encrypt 자동 HTTPS 리버스 프록시 - docker-compose.yml: Caddy 추가, Gateway 포트 내부 전용 - config.py: GATEWAY_API_KEY 설정 추가 - .env: 키 생성 명령어 가이드 포함
This commit is contained in:
9
Caddyfile
Normal file
9
Caddyfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Gravity Gateway — Caddy Reverse Proxy
|
||||||
|
# Automatic HTTPS via Let's Encrypt
|
||||||
|
#
|
||||||
|
# 도메인을 실제 도메인으로 변경하세요 (예: gateway.variet.net)
|
||||||
|
# Caddy가 자동으로 Let's Encrypt 인증서를 발급합니다.
|
||||||
|
|
||||||
|
gateway.variet.net {
|
||||||
|
reverse_proxy gateway:8585
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ class Config:
|
|||||||
# Bot mode: 'local' (file-based bridge) or 'remote' (HTTP polling — future)
|
# Bot mode: 'local' (file-based bridge) or 'remote' (HTTP polling — future)
|
||||||
BOT_MODE: str = os.getenv("BOT_MODE", "local")
|
BOT_MODE: str = os.getenv("BOT_MODE", "local")
|
||||||
REMOTE_BRIDGE_URL: str = os.getenv("REMOTE_BRIDGE_URL", "")
|
REMOTE_BRIDGE_URL: str = os.getenv("REMOTE_BRIDGE_URL", "")
|
||||||
|
GATEWAY_API_KEY: str = os.getenv("GATEWAY_API_KEY", "")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls) -> list[str]:
|
def validate(cls) -> list[str]:
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: gravity-gateway
|
container_name: gravity-gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
# Port NOT exposed directly — Caddy handles external access
|
||||||
- "8585:8585"
|
expose:
|
||||||
|
- "8585"
|
||||||
environment:
|
environment:
|
||||||
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
||||||
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
|
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
|
||||||
- BOT_MODE=gateway
|
- BOT_MODE=gateway
|
||||||
- GATEWAY_PORT=8585
|
- GATEWAY_PORT=8585
|
||||||
# Brain path inside container (not used in gateway mode, but needed for config validation)
|
- GATEWAY_API_KEY=${GATEWAY_API_KEY}
|
||||||
- BRAIN_PATH=/app/data/brain
|
- BRAIN_PATH=/app/data/brain
|
||||||
volumes:
|
volumes:
|
||||||
- gateway-data:/app/data
|
- gateway-data:/app/data
|
||||||
@@ -20,5 +21,21 @@ services:
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
container_name: gravity-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy-data:/data
|
||||||
|
- caddy-config:/config
|
||||||
|
depends_on:
|
||||||
|
- gateway
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
gateway-data:
|
gateway-data:
|
||||||
|
caddy-data:
|
||||||
|
caddy-config:
|
||||||
|
|||||||
29
gateway.py
29
gateway.py
@@ -28,11 +28,12 @@ logger = logging.getLogger(__name__)
|
|||||||
class GatewayAPI:
|
class GatewayAPI:
|
||||||
"""HTTP API server for Collector ↔ Gateway communication."""
|
"""HTTP API server for Collector ↔ Gateway communication."""
|
||||||
|
|
||||||
def __init__(self, bot, host: str = "0.0.0.0", port: int = 8585):
|
def __init__(self, bot, host: str = "0.0.0.0", port: int = 8585, api_key: str = ""):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.app = web.Application()
|
self.api_key = api_key
|
||||||
|
self.app = web.Application(middlewares=[self._auth_middleware])
|
||||||
self._setup_routes()
|
self._setup_routes()
|
||||||
|
|
||||||
# In-memory stores (Gateway is stateless across restarts)
|
# In-memory stores (Gateway is stateless across restarts)
|
||||||
@@ -47,6 +48,27 @@ class GatewayAPI:
|
|||||||
self.app.router.add_post("/api/register", self._post_register)
|
self.app.router.add_post("/api/register", self._post_register)
|
||||||
self.app.router.add_get("/api/commands/{project}", self._get_commands)
|
self.app.router.add_get("/api/commands/{project}", self._get_commands)
|
||||||
|
|
||||||
|
# ─── Auth Middleware ───
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def _auth_middleware(self, request: web.Request, handler):
|
||||||
|
"""Reject requests without valid API key on /api/* routes."""
|
||||||
|
# Health endpoint is public
|
||||||
|
if request.path == "/health":
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
# All /api/* routes require auth
|
||||||
|
if request.path.startswith("/api/") and self.api_key:
|
||||||
|
auth = request.headers.get("Authorization", "")
|
||||||
|
if auth != f"Bearer {self.api_key}":
|
||||||
|
logger.warning(f"[GATEWAY] 401 Unauthorized: {request.method} {request.path} from {request.remote}")
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Unauthorized", "detail": "Invalid or missing API key"},
|
||||||
|
status=401,
|
||||||
|
)
|
||||||
|
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
# ─── Health ───
|
# ─── Health ───
|
||||||
|
|
||||||
async def _health(self, request: web.Request) -> web.Response:
|
async def _health(self, request: web.Request) -> web.Response:
|
||||||
@@ -170,4 +192,5 @@ class GatewayAPI:
|
|||||||
await runner.setup()
|
await runner.setup()
|
||||||
site = web.TCPSite(runner, self.host, self.port)
|
site = web.TCPSite(runner, self.host, self.port)
|
||||||
await site.start()
|
await site.start()
|
||||||
logger.info(f"[GATEWAY] HTTP API started on {self.host}:{self.port}")
|
auth_status = "API Key enabled" if self.api_key else "⚠️ NO AUTH (set GATEWAY_API_KEY!)"
|
||||||
|
logger.info(f"[GATEWAY] HTTP API started on {self.host}:{self.port} [{auth_status}]")
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -84,7 +84,7 @@ async def main():
|
|||||||
if Config.BOT_MODE == 'gateway':
|
if Config.BOT_MODE == 'gateway':
|
||||||
from gateway import GatewayAPI
|
from gateway import GatewayAPI
|
||||||
gateway_port = int(os.environ.get('GATEWAY_PORT', '8585'))
|
gateway_port = int(os.environ.get('GATEWAY_PORT', '8585'))
|
||||||
gateway = GatewayAPI(bot, port=gateway_port)
|
gateway = GatewayAPI(bot, port=gateway_port, api_key=Config.GATEWAY_API_KEY)
|
||||||
await gateway.start()
|
await gateway.start()
|
||||||
logger.info(f"Gateway API running on port {gateway_port}")
|
logger.info(f"Gateway API running on port {gateway_port}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user