Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ STORAGE_LOCAL_ROOT=/var/lib/stackup-ai/storage
REALTIME_PORT=38020
REALTIME_LOG_LEVEL=info

# 호스트에 공개하는 포트의 바인드 주소. 기본 127.0.0.1 — nginx 와 SSH 터널만 닿으면 되고,
# 서비스끼리는 도커 네트워크로 통신한다. LAN 에 열어야 할 때만 0.0.0.0 으로 바꾼다.
# (도커는 ufw 를 우회해 publish 한 포트를 그대로 연다 — 0.0.0.0 은 곧 인터넷 공개다.)
# BIND_ADDR=127.0.0.1

# Backend Core (Spring)
BACKEND_PORT=38010
SPRING_PROFILES_ACTIVE=local
Expand Down
40 changes: 40 additions & 0 deletions ai/tests/test_compose_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,43 @@ def test_no_empty_string_fallbacks_for_settings_keys() -> None:
if key in settings_fields and re.fullmatch(r"\$\{[A-Z_0-9]+:-\}", value)
)
assert not bad, f"빈 문자열 fallback 이 코드 기본값을 덮는다: {bad}"


def test_published_ports_bind_loopback_by_default():
"""호스트 공개 포트는 전부 루프백에 묶여야 한다.

2026-09-28 점검에서 스택 포트 8개가 전부 0.0.0.0 이었다 — Core·RealTime·AI 뿐 아니라
PostgreSQL·RabbitMQ·MinIO 까지. 서버가 자기 공인 IP 로 38010 을 부르면 200 이 왔다
(호스트 방화벽은 막지 않는다 — 도커가 제 iptables 체인을 ufw 앞에 끼워 넣는다).

필요한 곳이 없다: nginx 는 127.0.0.1 로만 프록시하고 서비스끼리는 도커 네트워크를 쓴다.
"0.0.0.0" 은 기본값이 아니라 **명시적 선택**(BIND_ADDR)이어야 한다. 이 한 줄은 지워져도
컴파일·테스트가 통과하고 운영에서도 조용하므로 CI 가 대신 본다.
"""
text = COMPOSE.read_text(encoding="utf-8")
offenders = []
in_ports = False
for raw in text.splitlines():
line = raw.strip()
if line.startswith("#"):
continue
if line == "ports:":
in_ports = True
continue
if in_ports:
if not line.startswith("- "):
in_ports = False
continue
entry = line[2:].strip().strip('"').strip("'")
# "HOST:CONTAINER" 는 모든 인터페이스, "ADDR:HOST:CONTAINER" 만 허용한다.
if entry.count(":") < 2 or not (
entry.startswith("${BIND_ADDR:-127.0.0.1}")
or entry.startswith("127.0.0.1")
):
offenders.append(entry)

assert (
not offenders
), "포트가 모든 인터페이스에 열린다 — BIND_ADDR 를 앞에 붙일 것: " + ", ".join(
offenders
)
32 changes: 24 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# 호스트에 공개하는 포트는 **루프백에만** 묶는다 (BIND_ADDR, 기본 127.0.0.1).
#
# 2026-09-28 점검에서 스택 포트 8개가 전부 0.0.0.0 에 묶여 있었다 — Core·RealTime·AI 뿐
# 아니라 PostgreSQL(38040)·RabbitMQ(38050/38051)·MinIO(38060/38061)까지. 확인한 사실:
# · 서버가 **자기 공인 IP** 로 http://<public>:38010/api/system/health 를 부르면 200 이다
# → 호스트 방화벽이 막지 않는다. ufw 가 있어도 도커는 제 iptables 체인을 앞에 끼워 넣어
# publish 한 포트를 그냥 통과시킨다(잘 알려진 함정).
# · 외부에서는 TCP 핸드셰이크만 간헐적으로 성립하고 HTTP 응답은 오지 않는다
# → 상류(학교망) 어딘가가 걸러 주고 있는 것으로 보인다. **그건 우리 것이 아니다.**
#
# 필요한 곳이 하나도 없다: nginx 는 127.0.0.1:38010 / 127.0.0.1:38020 으로만 프록시하고,
# 서비스끼리는 도커 네트워크 이름(backend:38010, minio:38060, rabbitmq:38050)으로 통신한다.
# 38051/38061 콘솔은 SSH 터널로 계속 볼 수 있다(-L 38061:127.0.0.1:38061).
#
# LAN 에 열어야 하면 BIND_ADDR=0.0.0.0 으로 명시적으로 켠다 — 기본값이 아니라 선택이어야 한다.

name: stackup

services:
Expand All @@ -6,7 +22,7 @@ services:
container_name: stackup-postgres
command: ["postgres", "-c", "port=38040"]
ports:
- "${POSTGRES_PORT:-38040}:38040"
- "${BIND_ADDR:-127.0.0.1}:${POSTGRES_PORT:-38040}:38040"
environment:
POSTGRES_DB: ${POSTGRES_DB:-stackup}
POSTGRES_USER: ${POSTGRES_USER:-stackup}
Expand All @@ -24,8 +40,8 @@ services:
build: ./infra/rabbitmq
container_name: stackup-rabbitmq
ports:
- "${RABBITMQ_PORT:-38050}:38050"
- "${RABBITMQ_MANAGEMENT_PORT:-38051}:38051"
- "${BIND_ADDR:-127.0.0.1}:${RABBITMQ_PORT:-38050}:38050"
- "${BIND_ADDR:-127.0.0.1}:${RABBITMQ_MANAGEMENT_PORT:-38051}:38051"
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-stackup}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-stackup}
Expand All @@ -41,8 +57,8 @@ services:
build: ./infra/minio
container_name: stackup-minio
ports:
- "${MINIO_API_PORT:-38060}:38060"
- "${MINIO_CONSOLE_PORT:-38061}:38061"
- "${BIND_ADDR:-127.0.0.1}:${MINIO_API_PORT:-38060}:38060"
- "${BIND_ADDR:-127.0.0.1}:${MINIO_CONSOLE_PORT:-38061}:38061"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
Expand Down Expand Up @@ -74,7 +90,7 @@ services:
build: ./ai
container_name: stackup-ai
ports:
- "${AI_PORT:-38030}:38030"
- "${BIND_ADDR:-127.0.0.1}:${AI_PORT:-38030}:38030"
# 값이 붙은 항목만 compose 가 소유한다 — 컨테이너 토폴로지(서비스 호스트명)와
# MINIO_* → S3_* 이름 변환처럼 settings.py 가 알 수 없는 것들.
#
Expand Down Expand Up @@ -147,7 +163,7 @@ services:
build: ./realtime
container_name: stackup-realtime
ports:
- "${REALTIME_PORT:-38020}:38020"
- "${BIND_ADDR:-127.0.0.1}:${REALTIME_PORT:-38020}:38020"
environment:
REALTIME_LISTEN_ADDR: :38020
REALTIME_RABBITMQ_URL: amqp://${RABBITMQ_USER:-stackup}:${RABBITMQ_PASSWORD:-stackup}@rabbitmq:38050/
Expand All @@ -170,7 +186,7 @@ services:
context: ./backend
container_name: stackup-backend
ports:
- "${BACKEND_PORT:-38010}:38010"
- "${BIND_ADDR:-127.0.0.1}:${BACKEND_PORT:-38010}:38010"
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-local}
SERVER_PORT: "38010"
Expand Down
19 changes: 19 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# 보안 가이드라인

> **2026-09-28 — 호스트 포트 바인딩 정정.** 스택 포트 8개가 전부 `0.0.0.0` 에 묶여 있었다:
> Core(38010)·RealTime(38020)·AI(38030) 뿐 아니라 **PostgreSQL(38040)·RabbitMQ(38050/38051)·
> MinIO(38060/38061)** 까지. `docker-compose.yml` 이 짧은 형식(`"PORT:PORT"`)을 써서 모든
> 인터페이스에 열렸고, **ufw 가 있어도 도커가 제 iptables 체인을 앞에 끼워 넣어 그냥 통과**한다.
>
> 측정한 사실만 적는다:
> - 서버가 **자기 공인 IP** 로 `http://<public>:38010/api/system/health` 를 부르면 **200**.
> → 호스트 방화벽은 막지 않는다.
> - 외부(다른 네트워크)에서는 TCP 핸드셰이크만 간헐 성립하고 **HTTP 응답은 오지 않았다**.
> → 상류(학교망)가 걸러 주는 것으로 보인다. **그건 우리 통제 밖이고 바뀔 수 있다.**
>
> 필요한 곳은 하나도 없었다 — nginx 는 `127.0.0.1:38010`/`127.0.0.1:38020` 로만 프록시하고,
> 서비스끼리는 도커 네트워크 이름으로 통신한다. `BIND_ADDR`(기본 `127.0.0.1`)로 전부 루프백에
> 묶었고, LAN 노출이 필요하면 명시적으로 켠다. CI 가드:
> `ai/tests/test_compose_defaults.py::test_published_ports_bind_loopback_by_default`.
>
> 관리 콘솔은 SSH 터널로 계속 볼 수 있다: `ssh -L 38061:127.0.0.1:38061 stackup@stack-up.shop`


> 인증·인가·암호화·개인정보 처리 규약. 모든 신규 기능은 이 문서 체크리스트를 통과해야 한다.

---
Expand Down
Loading