Bach 운영 컨테이너 보안을 강화한다

백엔드는 내부 네트워크에만 두고 Docker 비밀 파일의 API 키를 프록시에서 주입한다. 프론트엔드는 루프백 포트 18102만 사용한다.
This commit is contained in:
이창현
2026-10-03 07:32:36 +09:00
parent 7679397074
commit ba887cc6f9
6 changed files with 55 additions and 7 deletions
+1
View File
@@ -19,6 +19,7 @@ __pycache__/
# ── Environment / secrets ─────────────────────────────────────────────────────
.env
/secrets/
.env.local
.env.*.local
.env.production
+18 -2
View File
@@ -45,9 +45,25 @@ _API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
_auth_warned = False
def _get_backend_api_key() -> str | None:
"""환경 변수 또는 Docker 비밀 파일에서 백엔드 인증 키를 읽는다."""
direct = os.environ.get("BACKEND_API_KEY")
if direct is not None:
return direct
secret_file = os.environ.get("BACKEND_API_KEY_FILE")
if not secret_file:
return None
try:
with open(secret_file, encoding="utf-8") as handle:
return handle.read().strip()
except OSError:
logging.exception("백엔드 인증 키 파일을 읽지 못했습니다")
return ""
def _require_api_key(key: str | None = Security(_API_KEY_HEADER)) -> None:
global _auth_warned
expected = os.environ.get("BACKEND_API_KEY")
expected = _get_backend_api_key()
# T1-1: only treat *absent* key (None) as "auth disabled".
# An empty string means a deployment misconfiguration, not "no auth".
if expected is None:
@@ -72,7 +88,7 @@ def _startup_checks() -> None:
"""Log warnings for optional external binaries that are absent at launch."""
# M1: fail fast if auth is explicitly required but no key is configured
if os.environ.get("REQUIRE_AUTH", "").strip().lower() in ("1", "true", "yes"):
if not os.environ.get("BACKEND_API_KEY"):
if not _get_backend_api_key():
raise SystemExit(
"FATAL: REQUIRE_AUTH is set but BACKEND_API_KEY is empty. "
"Set BACKEND_API_KEY or unset REQUIRE_AUTH."
+16 -5
View File
@@ -16,28 +16,39 @@ services:
build:
context: ./backend
dockerfile: Dockerfile
container_name: bach-backend
container_name: piano-bach-backend
restart: unless-stopped
expose:
- "8000"
environment:
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:5173}
- BACKEND_API_KEY=${BACKEND_API_KEY:-}
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-https://learn.itosm.co.kr,https://learn.itosm.com}
- BACKEND_API_KEY_FILE=/run/secrets/backend_api_key
- REQUIRE_AUTH=1
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-60}
- MUSIC21_TIMEOUT_S=${MUSIC21_TIMEOUT_S:-60}
- FINGERING_TIMEOUT_S=${FINGERING_TIMEOUT_S:-120}
volumes:
# Mount audiveris.jar from host (optional - PDF import is disabled if absent)
- ./backend/bin:/app/bin:ro
secrets:
- backend_api_key
frontend:
build:
# Root context so the Dockerfile can read pnpm-workspace.yaml + shared/
context: .
dockerfile: frontend/Dockerfile
container_name: bach-frontend
container_name: piano-bach-frontend
restart: unless-stopped
ports:
- "5173:80"
- "127.0.0.1:18102:80"
depends_on:
- backend
environment:
- BACKEND_API_KEY_FILE=/run/secrets/backend_api_key
secrets:
- backend_api_key
secrets:
backend_api_key:
file: ./secrets/backend_api_key
+2
View File
@@ -22,6 +22,8 @@ FROM nginx:1.27-alpine
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
COPY frontend/docker-entrypoint.d/20-backend-api-key.sh /docker-entrypoint.d/20-backend-api-key.sh
RUN chmod 755 /docker-entrypoint.d/20-backend-api-key.sh
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,17 @@
#!/bin/sh
set -eu
secret_file="${BACKEND_API_KEY_FILE:-/run/secrets/backend_api_key}"
if [ ! -r "$secret_file" ]; then
echo "백엔드 인증 키 파일을 읽을 수 없습니다." >&2
exit 1
fi
backend_api_key="$(cat "$secret_file")"
if [ -z "$backend_api_key" ]; then
echo "백엔드 인증 키가 비어 있습니다." >&2
exit 1
fi
escaped_key="$(printf '%s' "$backend_api_key" | sed 's/[\\/&]/\\\\&/g')"
sed -i "s/__BACKEND_API_KEY__/${escaped_key}/g" /etc/nginx/conf.d/default.conf
+1
View File
@@ -34,6 +34,7 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-API-Key "__BACKEND_API_KEY__";
# Sample / soundfont uploads can be large
client_max_body_size 60M;
# OMR + transcription can take a while