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: str = os.getenv("BOT_MODE", "local")
|
||||
REMOTE_BRIDGE_URL: str = os.getenv("REMOTE_BRIDGE_URL", "")
|
||||
GATEWAY_API_KEY: str = os.getenv("GATEWAY_API_KEY", "")
|
||||
|
||||
@classmethod
|
||||
def validate(cls) -> list[str]:
|
||||
|
||||
@@ -3,14 +3,15 @@ services:
|
||||
build: .
|
||||
container_name: gravity-gateway
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8585:8585"
|
||||
# Port NOT exposed directly — Caddy handles external access
|
||||
expose:
|
||||
- "8585"
|
||||
environment:
|
||||
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
||||
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
|
||||
- BOT_MODE=gateway
|
||||
- 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
|
||||
volumes:
|
||||
- gateway-data:/app/data
|
||||
@@ -20,5 +21,21 @@ services:
|
||||
max-size: "10m"
|
||||
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:
|
||||
gateway-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
29
gateway.py
29
gateway.py
@@ -28,11 +28,12 @@ logger = logging.getLogger(__name__)
|
||||
class GatewayAPI:
|
||||
"""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.host = host
|
||||
self.port = port
|
||||
self.app = web.Application()
|
||||
self.api_key = api_key
|
||||
self.app = web.Application(middlewares=[self._auth_middleware])
|
||||
self._setup_routes()
|
||||
|
||||
# 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_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 ───
|
||||
|
||||
async def _health(self, request: web.Request) -> web.Response:
|
||||
@@ -170,4 +192,5 @@ class GatewayAPI:
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, self.host, self.port)
|
||||
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':
|
||||
from gateway import GatewayAPI
|
||||
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()
|
||||
logger.info(f"Gateway API running on port {gateway_port}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user