Bach 운영 컨테이너 보안을 강화한다
백엔드는 내부 네트워크에만 두고 Docker 비밀 파일의 API 키를 프록시에서 주입한다. 프론트엔드는 루프백 포트 18102만 사용한다.
This commit is contained in:
@@ -19,6 +19,7 @@ __pycache__/
|
|||||||
|
|
||||||
# ── Environment / secrets ─────────────────────────────────────────────────────
|
# ── Environment / secrets ─────────────────────────────────────────────────────
|
||||||
.env
|
.env
|
||||||
|
/secrets/
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
.env.production
|
.env.production
|
||||||
|
|||||||
+18
-2
@@ -45,9 +45,25 @@ _API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
|
|||||||
_auth_warned = 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:
|
def _require_api_key(key: str | None = Security(_API_KEY_HEADER)) -> None:
|
||||||
global _auth_warned
|
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".
|
# T1-1: only treat *absent* key (None) as "auth disabled".
|
||||||
# An empty string means a deployment misconfiguration, not "no auth".
|
# An empty string means a deployment misconfiguration, not "no auth".
|
||||||
if expected is None:
|
if expected is None:
|
||||||
@@ -72,7 +88,7 @@ def _startup_checks() -> None:
|
|||||||
"""Log warnings for optional external binaries that are absent at launch."""
|
"""Log warnings for optional external binaries that are absent at launch."""
|
||||||
# M1: fail fast if auth is explicitly required but no key is configured
|
# 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 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(
|
raise SystemExit(
|
||||||
"FATAL: REQUIRE_AUTH is set but BACKEND_API_KEY is empty. "
|
"FATAL: REQUIRE_AUTH is set but BACKEND_API_KEY is empty. "
|
||||||
"Set BACKEND_API_KEY or unset REQUIRE_AUTH."
|
"Set BACKEND_API_KEY or unset REQUIRE_AUTH."
|
||||||
|
|||||||
+16
-5
@@ -16,28 +16,39 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: bach-backend
|
container_name: piano-bach-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
expose:
|
expose:
|
||||||
- "8000"
|
- "8000"
|
||||||
environment:
|
environment:
|
||||||
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:5173}
|
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-https://learn.itosm.co.kr,https://learn.itosm.com}
|
||||||
- BACKEND_API_KEY=${BACKEND_API_KEY:-}
|
- BACKEND_API_KEY_FILE=/run/secrets/backend_api_key
|
||||||
|
- REQUIRE_AUTH=1
|
||||||
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-60}
|
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-60}
|
||||||
- MUSIC21_TIMEOUT_S=${MUSIC21_TIMEOUT_S:-60}
|
- MUSIC21_TIMEOUT_S=${MUSIC21_TIMEOUT_S:-60}
|
||||||
- FINGERING_TIMEOUT_S=${FINGERING_TIMEOUT_S:-120}
|
- FINGERING_TIMEOUT_S=${FINGERING_TIMEOUT_S:-120}
|
||||||
volumes:
|
volumes:
|
||||||
# Mount audiveris.jar from host (optional - PDF import is disabled if absent)
|
# Mount audiveris.jar from host (optional - PDF import is disabled if absent)
|
||||||
- ./backend/bin:/app/bin:ro
|
- ./backend/bin:/app/bin:ro
|
||||||
|
secrets:
|
||||||
|
- backend_api_key
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
# Root context so the Dockerfile can read pnpm-workspace.yaml + shared/
|
# Root context so the Dockerfile can read pnpm-workspace.yaml + shared/
|
||||||
context: .
|
context: .
|
||||||
dockerfile: frontend/Dockerfile
|
dockerfile: frontend/Dockerfile
|
||||||
container_name: bach-frontend
|
container_name: piano-bach-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "5173:80"
|
- "127.0.0.1:18102:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
environment:
|
||||||
|
- BACKEND_API_KEY_FILE=/run/secrets/backend_api_key
|
||||||
|
secrets:
|
||||||
|
- backend_api_key
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
backend_api_key:
|
||||||
|
file: ./secrets/backend_api_key
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ FROM nginx:1.27-alpine
|
|||||||
|
|
||||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
||||||
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
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
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
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
|
||||||
@@ -34,6 +34,7 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-API-Key "__BACKEND_API_KEY__";
|
||||||
# Sample / soundfont uploads can be large
|
# Sample / soundfont uploads can be large
|
||||||
client_max_body_size 60M;
|
client_max_body_size 60M;
|
||||||
# OMR + transcription can take a while
|
# OMR + transcription can take a while
|
||||||
|
|||||||
Reference in New Issue
Block a user