From 45a14163bf6351a888fa8eac79b1a93816950163 Mon Sep 17 00:00:00 2001 From: Lectom Date: Thu, 7 May 2026 10:27:31 +0900 Subject: [PATCH] =?UTF-8?q?ory=EC=8A=A4=ED=83=9D=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=85=20=EB=B0=8F=20=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9URL?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 19 +- .gitea/workflows/staging_code_pull.yml | 12 +- Makefile | 6 +- README.md | 4 +- compose.ory.yaml | 157 +-- deploy/create-instance.sh | 12 + deploy/templates/.env.template | 22 + deploy/templates/docker-compose.yaml | 228 ++++- deploy/templates/gateway/nginx.conf | 1 - deploy/templates/ory/kratos/kratos.yml | 62 +- deploy/templates/ory/oathkeeper/rules.json | 150 ++- docker/compose.ory.yaml | 120 +-- docker/ory/kratos/kratos.yml | 60 +- docker/ory/oathkeeper/rules.active.json | 28 +- docker/ory/oathkeeper/rules.json | 28 +- docker/ory/oathkeeper/rules.prod.json | 103 +- docker/ory/oathkeeper/rules.stage.json | 28 +- docker/staging_pull_compose.template.yaml | 962 +++++++++--------- docs/devfront_auth_flow_explanation.md | 8 +- ...oidc_redirect_mapping_validation_policy.md | 41 +- docs/ory-usage.md | 48 +- gateway/nginx.conf | 1 - scripts/auth_config.sh | 17 +- test/make_dev_targets_test.sh | 35 + test/ory_v26_compose_policy_test.sh | 210 ++++ 25 files changed, 1583 insertions(+), 779 deletions(-) diff --git a/.env.sample b/.env.sample index 6d647b2d..2cc41e84 100644 --- a/.env.sample +++ b/.env.sample @@ -76,20 +76,20 @@ HYDRA_DB=ory_hydra KETO_DB=ory_keto # Ory Kratos Configuration -KRATOS_VERSION=v25.4.0-distroless +KRATOS_VERSION=v26.2.0-distroless # KRATOS_PUBLIC_PORT=4433 # Internal only # KRATOS_ADMINFRONT_PORT=4434 # Internal only -KRATOS_UI_NODE_VERSION=v25.4.0 +KRATOS_UI_NODE_VERSION=v26.2.0 # KRATOS_UI_PORT=4455 # Internal only # Ory Hydra Configuration -HYDRA_VERSION=v25.4.0-distroless +HYDRA_VERSION=v26.2.0-distroless # HYDRA_PUBLIC_PORT=4441 # Internal only # HYDRA_ADMINFRONT_PORT=4445 # Internal only # Ory Keto Configuration -KETO_VERSION=v25.4.0-distroless +KETO_VERSION=v26.2.0-distroless # KETO_READ_PORT=4466 # Internal only # KETO_WRITE_PORT=4467 # Internal only KETO_READ_URL=http://keto:4466 @@ -109,16 +109,21 @@ KRATOS_UI_URL=http://localhost:5000 HYDRA_ADMIN_URL=http://hydra:4445 # Oathkeeper가 /oidc 경로를 Hydra Public API로 라우팅합니다. HYDRA_PUBLIC_URL=${OATHKEEPER_PUBLIC_URL}/oidc +# 선택: Hydra 화면 핸드오프 URL을 USERFRONT_URL 기준 기본값과 다르게 둘 때만 설정합니다. +# HYDRA_LOGIN_URL=https://sso.hmac.kr/login +# HYDRA_CONSENT_URL=https://sso.hmac.kr/consent +# HYDRA_ERROR_URL=https://sso.hmac.kr/error # Kratos allowed_return_urls 확장 목록 (콤마 구분, 선택) # 기본값은 KRATOS_UI_URL, USERFRONT_URL, 각 callback URL을 자동 포함합니다. KRATOS_ALLOWED_RETURN_URLS_EXTRA=[] +KRATOS_ALLOWED_RETURN_URLS_JSON=["http://localhost:5000","http://localhost:5000/","https://sso.hmac.kr","https://sso.hmac.kr/","https://sso.hmac.kr/ko","https://sso.hmac.kr/ko/","https://sso.hmac.kr/en","https://sso.hmac.kr/en/","https://sso.hmac.kr/auth/callback","https://sso.hmac.kr/ko/auth/callback","https://sso.hmac.kr/en/auth/callback","http://localhost:5173/auth/callback","http://localhost:5174/auth/callback","http://localhost:5175/auth/callback","https://sso.hmac.kr/orgfront/auth/callback"] # Oathkeeper JWKS (내부 통신용) JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json # Oathkeeper 실행 사용자/프로브 설정 -OATHKEEPER_VERSION=v25.4.0 +OATHKEEPER_VERSION=v26.2.0 OATHKEEPER_UID=1001 OATHKEEPER_GID=1001 OATHKEEPER_HEALTH_URL=http://oathkeeper:4456/health/ready @@ -140,5 +145,5 @@ VITE_OIDC_CLIENT_ID=devfront VITE_OIDC_AUTHORITY=https://sso.hmac.kr/oidc DEVFRONT_URL=http://localhost:5174 DEVFRONT_CALLBACK_URLS=http://localhost:5174/auth/callback,https://sso.hmac.kr/devfront/auth/callback -ORGFRONT_CALLBACK_URLS=http://localhost:5175/auth/callback,http://172.16.10.176:5175/auth/callback,https://baron-orgchart.hmac.kr/auth/callback -VITE_ORGCHART_URL= \ No newline at end of file +ORGFRONT_CALLBACK_URLS=http://localhost:5175/auth/callback,https://sso.hmac.kr/orgfront/auth/callback +VITE_ORGCHART_URL= diff --git a/.gitea/workflows/staging_code_pull.yml b/.gitea/workflows/staging_code_pull.yml index 2abd5158..8e5f2f8a 100644 --- a/.gitea/workflows/staging_code_pull.yml +++ b/.gitea/workflows/staging_code_pull.yml @@ -124,11 +124,13 @@ jobs: CSRF_COOKIE_NAME=${{ vars.CSRF_COOKIE_NAME }} CSRF_COOKIE_SECRET=${{ secrets.STG_CSRF_COOKIE_SECRET }} - # Frontend OIDC configs for Staging - VITE_OIDC_AUTHORITY=https://sso.hmac.kr/oidc - ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback,https://sso.hmac.kr/auth/callback,http://172.16.10.176:5173/auth/callback,https://sadmin.hmac.kr/auth/callback - DEVFRONT_CALLBACK_URLS=http://localhost:5174/auth/callback,https://sso.hmac.kr/devfront/auth/callback,http://172.16.10.176:5174/auth/callback,https://sdev.hmac.kr/auth/callback - ORGFRONT_CALLBACK_URLS=http://localhost:5175/auth/callback,http://172.16.10.176:5175/auth/callback,https://baron-orgchart.hmac.kr/auth/callback + # Frontend/Ory URL configs for Staging + VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }} + ADMINFRONT_CALLBACK_URLS=${{ vars.ADMINFRONT_CALLBACK_URLS }} + DEVFRONT_CALLBACK_URLS=${{ vars.DEVFRONT_CALLBACK_URLS }} + ORGFRONT_CALLBACK_URLS=${{ vars.ORGFRONT_CALLBACK_URLS }} + KRATOS_ALLOWED_RETURN_URLS_JSON=${{ vars.KRATOS_ALLOWED_RETURN_URLS_JSON }} + KRATOS_ALLOWED_RETURN_URLS_EXTRA=${{ vars.KRATOS_ALLOWED_RETURN_URLS_EXTRA }} # OATHKEEPER_INTROSPECT_CLIENT_ID=${{ vars.OATHKEEPER_INTROSPECT_CLIENT_ID }} # OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} EOF diff --git a/Makefile b/Makefile index 0c688341..f4bd4fa0 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ ifneq (,$(wildcard ./.env)) COMPOSE_DROP_ENV_ARGS += --env-file .env endif -.PHONY: build-auth-config validate-auth-config verify-auth-config up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app +.PHONY: build-auth-config validate-auth-config verify-auth-config up up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app # --- 인증 설정 빌드/검증 --- build-auth-config: @@ -47,6 +47,8 @@ verify-auth-config: validate-auth-config # --- 기본 실행 --- # 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음) +up: up-all + up-all: ensure-networks validate-auth-config @echo "Starting ALL stacks (infra + ory + app)..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d @@ -61,7 +63,7 @@ up-ory: ensure-networks validate-auth-config docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d up-app: ensure-networks validate-auth-config - @echo "Starting App stack (backend/userfront/adminfront/devfront)..." + @echo "Starting App stack (backend/userfront/adminfront/devfront/orgfront)..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d up-backend: ensure-networks validate-auth-config diff --git a/README.md b/README.md index 776bb0bc..dd0385bb 100644 --- a/README.md +++ b/README.md @@ -395,11 +395,13 @@ USERFRONT_URL=https://sso.example.com - `KRATOS_BROWSER_URL`: 보통 `${OATHKEEPER_PUBLIC_URL}/auth` - `KRATOS_UI_URL`: UserFront UI URL (로컬 예: `http://localhost:5000`) - `ADMINFRONT_CALLBACK_URLS`: 콤마 구분 콜백 목록 (예: `http://localhost:5173/auth/callback`) -- `DEVFRONT_CALLBACK_URLS`: 콤마 구분 콜백 목록 (예: `http://localhost:5174/callback`) +- `DEVFRONT_CALLBACK_URLS`: 콤마 구분 콜백 목록 (예: `http://localhost:5174/auth/callback`) - 주의: callback URL 끝에 `/`가 붙으면 `make validate-auth-config`에서 실패 처리됩니다. - `KRATOS_ALLOWED_RETURN_URLS_EXTRA`: 추가 허용 return URL (선택) - 빈값: `[]` - 다중값: `["https://a.example.com/callback","https://b.example.com/callback"]` 또는 `https://a.example.com/callback,https://b.example.com/callback` +- `KRATOS_ALLOWED_RETURN_URLS_JSON`: stage/prod에서 권장하는 전체 허용 return URL 목록 + - 공개 도메인, `/ko`, `/en`, `/auth/callback`, `/ko/auth/callback`, `/en/auth/callback`, 각 front callback을 포함해야 합니다. - `CLIENT_LOG_DEBUG`: 클라이언트 로그 디버그 모드 강제 (기본: 비운영 `true`, 운영 `false`) - 운영(`APP_ENV=production|prod`)에서 `true|1|on|yes` 설정 시 `INFO/DEBUG` 클라이언트 로그 수집 허용 - 미설정(기본) 시 운영에서는 `WARN/ERROR`만 수집 diff --git a/compose.ory.yaml b/compose.ory.yaml index 5b6faede..99213589 100644 --- a/compose.ory.yaml +++ b/compose.ory.yaml @@ -23,20 +23,20 @@ services: # --- Kratos --- kratos-migrate: - image: oryd/kratos:${KRATOS_VERSION:-v25.4.0} + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL:-http://localhost:4433} - - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000} - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["http://localhost:5000","http://localhost:5000/"]} - - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error - - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error?error=settings_disabled - - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/recovery - - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/verification - - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/login - - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/registration - - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000}/login + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/"]} + - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error + - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled + - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery + - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification + - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login + - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration + - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login volumes: - ./docker/ory/kratos:/etc/config/kratos command: migrate sql up -e -c /etc/config/kratos/kratos.yml --yes @@ -47,22 +47,22 @@ services: - ory-net kratos: - image: oryd/kratos:${KRATOS_VERSION:-v25.4.0} + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} container_name: ory_kratos environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - COOKIE_SECRET=${COOKIE_SECRET:-localcookie123} - - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL:-http://localhost:4433} - - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000} - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["http://localhost:5000","http://localhost:5000/"]} - - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error - - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error?error=settings_disabled - - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/recovery - - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/verification - - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/login - - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/registration - - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000}/login + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/"]} + - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error + - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled + - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery + - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification + - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login + - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration + - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login volumes: - ./docker/ory/kratos:/etc/config/kratos command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier @@ -75,7 +75,7 @@ services: # --- Hydra --- hydra-migrate: - image: oryd/hydra:${HYDRA_VERSION:-v25.4.0} + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 command: migrate sql up -e --yes @@ -86,14 +86,14 @@ services: - ory-net hydra: - image: oryd/hydra:${HYDRA_VERSION:-v25.4.0} + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} container_name: ory_hydra environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 - - URLS_SELF_ISSUER=${USERFRONT_URL:-http://localhost:5000}/oidc - - URLS_LOGIN=${USERFRONT_URL:-http://localhost:5000}/login - - URLS_CONSENT=${USERFRONT_URL:-http://localhost:5000}/consent - - URLS_ERROR=${USERFRONT_URL:-http://localhost:5000}/error + - URLS_SELF_ISSUER=${HYDRA_PUBLIC_URL} + - URLS_LOGIN=${HYDRA_LOGIN_URL:-${USERFRONT_URL}/login} + - URLS_CONSENT=${HYDRA_CONSENT_URL:-${USERFRONT_URL}/consent} + - URLS_ERROR=${HYDRA_ERROR_URL:-${USERFRONT_URL}/error} - SECRETS_SYSTEM=${ORY_POSTGRES_PASSWORD} volumes: - ./docker/ory/hydra:/etc/config/hydra @@ -107,7 +107,7 @@ services: # --- Keto --- keto-migrate: - image: oryd/keto:${KETO_VERSION:-v25.4.0} + image: oryd/keto:${KETO_VERSION:-v26.2.0} environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 volumes: @@ -120,7 +120,7 @@ services: - ory-net keto: - image: oryd/keto:${KETO_VERSION:-v25.4.0} + image: oryd/keto:${KETO_VERSION:-v26.2.0} container_name: ory_keto environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 @@ -136,14 +136,19 @@ services: # --- Oathkeeper --- oathkeeper_logs_init: image: alpine:latest - command: ["sh", "-c", "mkdir -p /var/log/oathkeeper && chown -R ${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001} /var/log/oathkeeper"] + command: + [ + "sh", + "-c", + "mkdir -p /var/log/oathkeeper && chown -R ${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001} /var/log/oathkeeper", + ] volumes: - oathkeeper_logs:/var/log/oathkeeper networks: - ory-net oathkeeper: - image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v25.4.0} + image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v26.2.0} container_name: ory_oathkeeper user: "${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001}" ports: @@ -220,56 +225,56 @@ services: - /bin/sh - -ec - | - apk add --no-cache curl tar - HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}" - HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}" - HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}" - curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz" - tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra - rm /tmp/hydra.tar.gz + apk add --no-cache curl tar + HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}" + HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}" + HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}" + curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz" + tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra + rm /tmp/hydra.tar.gz - hydra delete oauth2-client --endpoint http://hydra:4445 adminfront >/dev/null 2>&1 || true - hydra delete oauth2-client --endpoint http://hydra:4445 devfront >/dev/null 2>&1 || true - hydra delete oauth2-client --endpoint http://hydra:4445 orgfront >/dev/null 2>&1 || true - hydra delete oauth2-client --endpoint http://hydra:4445 ${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} >/dev/null 2>&1 || true + hydra delete oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" adminfront >/dev/null 2>&1 || true + hydra delete oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" devfront >/dev/null 2>&1 || true + hydra delete oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" orgfront >/dev/null 2>&1 || true + hydra delete oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" "$${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}" >/dev/null 2>&1 || true - hydra create oauth2-client \ - --endpoint http://hydra:4445 \ - --id adminfront \ - --name "AdminFront" \ + hydra create oauth2-client \ + --endpoint "$${HYDRA_ADMIN_URL}" \ + --id adminfront \ + --name "AdminFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri ${ADMINFRONT_CALLBACK_URLS} + + hydra create oauth2-client \ + --endpoint "$${HYDRA_ADMIN_URL}" \ + --id devfront \ + --name "DevFront" \ --grant-type authorization_code,refresh_token \ --response-type code \ --scope openid,offline_access,profile,email \ --token-endpoint-auth-method none \ - --redirect-uri ${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback} + --redirect-uri ${DEVFRONT_CALLBACK_URLS} - hydra create oauth2-client \ - --endpoint http://hydra:4445 \ - --id devfront \ - --name "DevFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri ${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/auth/callback} + hydra create oauth2-client \ + --endpoint "$${HYDRA_ADMIN_URL}" \ + --id orgfront \ + --name "OrgFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri ${ORGFRONT_CALLBACK_URLS} - hydra create oauth2-client \ - --endpoint http://hydra:4445 \ - --id orgfront \ - --name "OrgFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri ${ORGFRONT_CALLBACK_URLS:-http://localhost:5175/auth/callback,https://baron-orgchart.hmac.kr/auth/callback} - - hydra create oauth2-client \ - --endpoint http://hydra:4445 \ - --id ${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} \ - --secret ${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret} \ - --grant-type client_credentials \ - --response-type token \ - --scope openid,offline_access,profile,email + hydra create oauth2-client \ + --endpoint "$${HYDRA_ADMIN_URL}" \ + --id "$${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}" \ + --secret "$${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}" \ + --grant-type client_credentials \ + --response-type token \ + --scope openid,offline_access,profile,email depends_on: ory_stack_check: condition: service_completed_successfully diff --git a/deploy/create-instance.sh b/deploy/create-instance.sh index 9ff61865..ecbe46e8 100644 --- a/deploy/create-instance.sh +++ b/deploy/create-instance.sh @@ -18,11 +18,15 @@ echo "🚀 Creating instance: ${INSTANCE_NAME} (Port Prefix: ${PORT_PREFIX}xxx)" # 1. 폴더 구조 생성 mkdir -p "${TARGET_DIR}/gateway" +mkdir -p "${TARGET_DIR}/ory/init-db" mkdir -p "${TARGET_DIR}/ory/kratos" +mkdir -p "${TARGET_DIR}/ory/hydra" +mkdir -p "${TARGET_DIR}/ory/keto" mkdir -p "${TARGET_DIR}/ory/oathkeeper" mkdir -p "${TARGET_DIR}/userfront" mkdir -p "${TARGET_DIR}/adminfront" mkdir -p "${TARGET_DIR}/devfront" +mkdir -p "${TARGET_DIR}/orgfront" # 2. .env 생성 및 변수 로드 sed "s/{{INSTANCE_NAME}}/${INSTANCE_NAME}/g; s/{{PORT_PREFIX}}/${PORT_PREFIX}/g" \ @@ -34,6 +38,7 @@ USERFRONT_PORT="${PORT_PREFIX}500" DOMAIN_SUFFIX=$(grep "DOMAIN_SUFFIX=" "${TARGET_DIR}/.env" | cut -d'=' -f2 | tr -d '\r') ADMINFRONT_DOMAIN="${INSTANCE_NAME}-admin.${DOMAIN_SUFFIX}" DEVFRONT_DOMAIN="${INSTANCE_NAME}-dev.${DOMAIN_SUFFIX}" +ORGFRONT_DOMAIN="${INSTANCE_NAME}-org.${DOMAIN_SUFFIX}" # 3. Docker Compose & Config 복사 및 치환 cp "${BASE_DIR}/templates/docker-compose.yaml" "${TARGET_DIR}/" @@ -55,15 +60,22 @@ sed "s/{{ADMINFRONT_DOMAIN}}/${ADMINFRONT_DOMAIN}/g; s/{{BACKEND_PORT}}/${BACKEN "${BASE_DIR}/templates/adminfront/vite.config.ts" > "${TARGET_DIR}/adminfront/vite.config.ts" sed "s/{{DEVFRONT_DOMAIN}}/${DEVFRONT_DOMAIN}/g; s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" \ "${BASE_DIR}/templates/devfront/vite.config.ts" > "${TARGET_DIR}/devfront/vite.config.ts" +sed "s/{{ORGFRONT_DOMAIN}}/${ORGFRONT_DOMAIN}/g; s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" \ + "${BASE_DIR}/templates/orgfront/vite.config.ts" > "${TARGET_DIR}/orgfront/vite.config.ts" # 4. 프론트엔드 auth.ts 주입 (하드코딩된 포트 해결) sed "s/{{USERFRONT_PORT}}/${USERFRONT_PORT}/g; s/{{CLIENT_ID}}/adminfront/g" \ "${BASE_DIR}/templates/auth.template.ts" > "${TARGET_DIR}/adminfront/auth.ts" sed "s/{{USERFRONT_PORT}}/${USERFRONT_PORT}/g; s/{{CLIENT_ID}}/devfront/g" \ "${BASE_DIR}/templates/auth.template.ts" > "${TARGET_DIR}/devfront/auth.ts" +sed "s/{{USERFRONT_PORT}}/${USERFRONT_PORT}/g" \ + "${BASE_DIR}/templates/orgfront/auth.ts" > "${TARGET_DIR}/orgfront/auth.ts" # 5. Ory 정적 설정 복사 +if [ -d "${BASE_DIR}/../docker/ory/init-db" ]; then cp -n "${BASE_DIR}/../docker/ory/init-db/"* "${TARGET_DIR}/ory/init-db/" 2>/dev/null || true; fi if [ -d "${BASE_DIR}/../docker/ory/kratos" ]; then cp -n "${BASE_DIR}/../docker/ory/kratos/"* "${TARGET_DIR}/ory/kratos/" 2>/dev/null || true; fi +if [ -d "${BASE_DIR}/../docker/ory/hydra" ]; then cp -n "${BASE_DIR}/../docker/ory/hydra/"* "${TARGET_DIR}/ory/hydra/" 2>/dev/null || true; fi +if [ -d "${BASE_DIR}/../docker/ory/keto" ]; then cp -n "${BASE_DIR}/../docker/ory/keto/"* "${TARGET_DIR}/ory/keto/" 2>/dev/null || true; fi if [ -d "${BASE_DIR}/../docker/ory/oathkeeper" ]; then cp -n "${BASE_DIR}/../docker/ory/oathkeeper/"* "${TARGET_DIR}/ory/oathkeeper/" 2>/dev/null || true; fi # 6. 마무리 diff --git a/deploy/templates/.env.template b/deploy/templates/.env.template index 130f5fc7..822f3095 100644 --- a/deploy/templates/.env.template +++ b/deploy/templates/.env.template @@ -17,6 +17,7 @@ BACKEND_PORT=${P}000 USERFRONT_PORT=${P}500 ADMINFRONT_PORT=${P}173 DEVFRONT_PORT=${P}174 +ORGFRONT_PORT=${P}175 OATHKEEPER_PROXY_PORT=${P}467 # === [3] 도메인 설정 (별도 도메인 구조) === @@ -25,23 +26,44 @@ DOMAIN_SUFFIX=hmac.kr USERFRONT_URL=https://{{INSTANCE_NAME}}-sso.${DOMAIN_SUFFIX} ADMINFRONT_URL=https://{{INSTANCE_NAME}}-admin.${DOMAIN_SUFFIX} DEVFRONT_URL=https://{{INSTANCE_NAME}}-dev.${DOMAIN_SUFFIX} +ORGFRONT_URL=https://{{INSTANCE_NAME}}-org.${DOMAIN_SUFFIX} # OIDC/Auth URL VITE_OIDC_AUTHORITY=${USERFRONT_URL}/oidc ADMINFRONT_CALLBACK_URLS=${ADMINFRONT_URL}/auth/callback DEVFRONT_CALLBACK_URLS=${DEVFRONT_URL}/auth/callback +ORGFRONT_CALLBACK_URLS=${ORGFRONT_URL}/auth/callback # Ory URL KRATOS_UI_URL=${USERFRONT_URL}/auth KRATOS_BROWSER_URL=${USERFRONT_URL}/auth +KRATOS_ADMIN_URL=http://kratos:4434 HYDRA_PUBLIC_URL=${USERFRONT_URL}/oidc +HYDRA_ADMIN_URL=http://hydra:4445 OATHKEEPER_PUBLIC_URL=${USERFRONT_URL} +KETO_READ_URL=http://keto:4466 +KETO_WRITE_URL=http://keto:4467 + +# Ory versions +KRATOS_VERSION=v26.2.0 +HYDRA_VERSION=v26.2.0 +KETO_VERSION=v26.2.0 +OATHKEEPER_VERSION=v26.2.0 +ORY_POSTGRES_TAG=17-alpine # === [4] IDP 및 DB Config === IDP_PROVIDER=ory DB_PASSWORD=password ORY_POSTGRES_USER=ory ORY_POSTGRES_PASSWORD=generated_secret_here +ORY_POSTGRES_DB=ory +KRATOS_DB=ory_kratos +HYDRA_DB=ory_hydra +KETO_DB=ory_keto +OATHKEEPER_UID=1001 +OATHKEEPER_GID=1001 +OATHKEEPER_INTROSPECT_CLIENT_ID=oathkeeper-introspect +OATHKEEPER_INTROSPECT_CLIENT_SECRET=oathkeeper-secret CLICKHOUSE_PASSWORD=password REDIS_ADDR=redis:6379 diff --git a/deploy/templates/docker-compose.yaml b/deploy/templates/docker-compose.yaml index c6294da9..5544039f 100644 --- a/deploy/templates/docker-compose.yaml +++ b/deploy/templates/docker-compose.yaml @@ -10,7 +10,7 @@ services: ports: - "${DB_PORT}:5432" volumes: - - db_data_${INSTANCE_NAME}:/var/lib/postgresql/data + - db_data:/var/lib/postgresql/data networks: [app_net] healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] @@ -33,42 +33,228 @@ services: - "${CLICKHOUSE_PORT_HTTP}:8123" - "${CLICKHOUSE_PORT_NATIVE}:9000" volumes: - - clickhouse_data_${INSTANCE_NAME}:/var/lib/clickhouse + - clickhouse_data:/var/lib/clickhouse networks: [app_net] # --- Ory Stack --- postgres_ory: - image: postgres:17-alpine + image: postgres:${ORY_POSTGRES_TAG:-17-alpine} container_name: ${COMPOSE_PROJECT_NAME}_ory_db environment: - - POSTGRES_USER=${ORY_POSTGRES_USER} + - POSTGRES_USER=${ORY_POSTGRES_USER:-ory} - POSTGRES_PASSWORD=${ORY_POSTGRES_PASSWORD} + - POSTGRES_DB=${ORY_POSTGRES_DB:-ory} volumes: - - ory_db_data_${INSTANCE_NAME}:/var/lib/postgresql/data + - ory_db_data:/var/lib/postgresql/data + - ./ory/init-db:/docker-entrypoint-initdb.d:ro networks: [app_net] healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${ORY_POSTGRES_USER}"] + test: ["CMD-SHELL", "pg_isready -U ${ORY_POSTGRES_USER:-ory} -d ${KRATOS_DB:-ory_kratos}"] interval: 5s - kratos: - image: oryd/kratos:v25.4.0 - container_name: ${COMPOSE_PROJECT_NAME}_kratos + kratos-migrate: + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} env_file: .env + environment: + - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]} + - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error + - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled + - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery + - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification + - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login + - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration + - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login volumes: - ./ory/kratos:/etc/config/kratos:ro - command: serve -c /etc/config/kratos/kratos.yml --dev + command: migrate sql up -e -c /etc/config/kratos/kratos.yml --yes networks: [app_net] depends_on: postgres_ory: { condition: service_healthy } + kratos: + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} + container_name: ${COMPOSE_PROJECT_NAME}_kratos + env_file: .env + environment: + - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 + - COOKIE_SECRET=${COOKIE_SECRET} + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]} + - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error + - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled + - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery + - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification + - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login + - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration + - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login + volumes: + - ./ory/kratos:/etc/config/kratos:ro + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + networks: [app_net] + depends_on: + kratos-migrate: { condition: service_completed_successfully } + + hydra-migrate: + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} + env_file: .env + environment: + - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 + command: migrate sql up -e --yes + networks: [app_net] + depends_on: + postgres_ory: { condition: service_healthy } + + hydra: + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} + container_name: ${COMPOSE_PROJECT_NAME}_hydra + env_file: .env + environment: + - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 + - URLS_SELF_ISSUER=${HYDRA_PUBLIC_URL} + - URLS_LOGIN=${HYDRA_LOGIN_URL:-${USERFRONT_URL}/login} + - URLS_CONSENT=${HYDRA_CONSENT_URL:-${USERFRONT_URL}/consent} + - URLS_ERROR=${HYDRA_ERROR_URL:-${USERFRONT_URL}/error} + - SECRETS_SYSTEM=${ORY_POSTGRES_PASSWORD} + volumes: + - ./ory/hydra:/etc/config/hydra:ro + command: serve -c /etc/config/hydra/hydra.yml all --dev + networks: [app_net] + depends_on: + hydra-migrate: { condition: service_completed_successfully } + + keto-migrate: + image: oryd/keto:${KETO_VERSION:-v26.2.0} + env_file: .env + environment: + - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 + volumes: + - ./ory/keto:/etc/config/keto:ro + command: ["migrate", "up", "-c", "/etc/config/keto/keto.yml", "--yes"] + networks: [app_net] + depends_on: + postgres_ory: { condition: service_healthy } + + keto: + image: oryd/keto:${KETO_VERSION:-v26.2.0} + container_name: ${COMPOSE_PROJECT_NAME}_keto + env_file: .env + environment: + - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 + volumes: + - ./ory/keto:/etc/config/keto:ro + command: serve -c /etc/config/keto/keto.yml + networks: [app_net] + depends_on: + keto-migrate: { condition: service_completed_successfully } + + oathkeeper_logs_init: + image: alpine:latest + command: ["sh", "-c", "mkdir -p /var/log/oathkeeper && chown -R ${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001} /var/log/oathkeeper"] + volumes: + - oathkeeper_logs:/var/log/oathkeeper + networks: [app_net] + oathkeeper: - image: oryd/oathkeeper:v25.4.0 + image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v26.2.0} container_name: ${COMPOSE_PROJECT_NAME}_oathkeeper env_file: .env + user: "${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001}" ports: - "${OATHKEEPER_PROXY_PORT}:4455" + environment: + - APP_ENV=${APP_ENV:-production} + - LOG_LEVEL=debug + - OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} + - OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret} volumes: - ./ory/oathkeeper:/etc/config/oathkeeper:ro + - oathkeeper_logs:/var/log/oathkeeper + entrypoint: ["/etc/config/oathkeeper/entrypoint.sh"] + networks: [app_net] + depends_on: + oathkeeper_logs_init: { condition: service_completed_successfully } + kratos: { condition: service_started } + hydra: { condition: service_started } + + ory_stack_check: + image: alpine:latest + container_name: ${COMPOSE_PROJECT_NAME}_ory_stack_check + command: > + /bin/sh -c " + apk add --no-cache curl; + echo 'Wait for Ory services...'; + until curl -s http://kratos:4433/health/ready; do sleep 1; done; + until curl -s http://hydra:4444/health/ready; do sleep 1; done; + until curl -s http://keto:4466/health/ready; do sleep 1; done; + echo 'Ory stack is ready.';" + depends_on: + - kratos + - hydra + - keto + networks: [app_net] + + init-rp: + image: alpine:latest + env_file: .env + command: + - /bin/sh + - -ec + - | + apk add --no-cache curl tar + HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}" + HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}" + HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}" + curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz" + tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra + rm /tmp/hydra.tar.gz + + upsert_client() { + ID=$$1 + shift + if hydra get oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" "$$ID" >/dev/null 2>&1; then + hydra update oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" "$$ID" "$$@" + else + hydra create oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" --id "$$ID" "$$@" + fi + } + + upsert_client "adminfront" \ + --name "AdminFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${ADMINFRONT_CALLBACK_URLS:-$${ADMINFRONT_URL}/auth/callback}" + + upsert_client "devfront" \ + --name "DevFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${DEVFRONT_CALLBACK_URLS:-$${DEVFRONT_URL}/auth/callback}" + + upsert_client "orgfront" \ + --name "OrgFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${ORGFRONT_CALLBACK_URLS:-$${ORGFRONT_URL}/auth/callback}" + + upsert_client "$${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}" \ + --secret "$${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}" \ + --grant-type client_credentials \ + --response-type token \ + --scope openid,offline_access,profile,email + depends_on: + ory_stack_check: { condition: service_completed_successfully } networks: [app_net] # --- Application Services --- @@ -78,6 +264,14 @@ services: env_file: .env environment: - PORT=${BACKEND_PORT} + - APP_ENV=${APP_ENV:-production} + - IDP_PROVIDER=${IDP_PROVIDER:-ory} + - USERFRONT_URL=${USERFRONT_URL} + - KRATOS_ADMIN_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} + - HYDRA_ADMIN_URL=${HYDRA_ADMIN_URL:-http://hydra:4445} + - HYDRA_PUBLIC_URL=${HYDRA_PUBLIC_URL} + - KETO_READ_URL=${KETO_READ_URL:-http://keto:4466} + - KETO_WRITE_URL=${KETO_WRITE_URL:-http://keto:4467} - DB_HOST=postgres - REDIS_ADDR=redis:6379 - CLICKHOUSE_HOST=clickhouse @@ -90,6 +284,7 @@ services: depends_on: postgres: { condition: service_healthy } redis: { condition: service_started } + oathkeeper: { condition: service_started } gateway: image: nginx:alpine @@ -147,6 +342,11 @@ networks: name: ${COMPOSE_PROJECT_NAME}_net volumes: - db_data_${INSTANCE_NAME}: - ory_db_data_${INSTANCE_NAME}: - clickhouse_data_${INSTANCE_NAME}: + db_data: + name: db_data_${INSTANCE_NAME} + ory_db_data: + name: ory_db_data_${INSTANCE_NAME} + clickhouse_data: + name: clickhouse_data_${INSTANCE_NAME} + oathkeeper_logs: + name: oathkeeper_logs_${INSTANCE_NAME} diff --git a/deploy/templates/gateway/nginx.conf b/deploy/templates/gateway/nginx.conf index 5590645e..656c5e2e 100644 --- a/deploy/templates/gateway/nginx.conf +++ b/deploy/templates/gateway/nginx.conf @@ -29,7 +29,6 @@ http { } location /oidc { - rewrite ^/oidc/(.*)$ /$1 break; proxy_pass http://oathkeeper_srv; proxy_set_header Host $host; } diff --git a/deploy/templates/ory/kratos/kratos.yml b/deploy/templates/ory/kratos/kratos.yml index b70ca3f2..45fa5952 100644 --- a/deploy/templates/ory/kratos/kratos.yml +++ b/deploy/templates/ory/kratos/kratos.yml @@ -1,16 +1,20 @@ -version: v25.4.0 +version: v26.2.0 dsn: ${DSN} serve: public: - base_url: http://localhost:4433/ + base_url: ${KRATOS_BROWSER_URL} cors: enabled: true allowed_origins: - http://backend:{{BACKEND_PORT}} + - ${USERFRONT_URL} + - ${ADMINFRONT_URL} + - ${DEVFRONT_URL} + - ${ORGFRONT_URL} admin: - base_url: http://localhost:4434/ + base_url: ${KRATOS_ADMIN_URL} session: cookie: @@ -19,30 +23,22 @@ session: path: / selfservice: - default_browser_return_url: http://localhost:{{USERFRONT_PORT}}/ + default_browser_return_url: ${KRATOS_UI_URL} allowed_return_urls: - - http://backend:{{BACKEND_PORT}} - - http://backend:{{BACKEND_PORT}}/ - - http://localhost:{{USERFRONT_PORT}} - - https://app.brsw.kr - - https://app.brsw.kr/ - - https://sss.hmac.kr - - https://sss.hmac.kr/ - - https://sso.hmac.kr - - https://sso.hmac.kr/ - - https://ssologin.hmac.kr - - https://ssologin.hmac.kr/ - - https://sso-test.hmac.kr - - https://sso-test.hmac.kr/ - - https://ssob.hmac.kr - - https://ssob.hmac.kr/ - - https://ssob.hmac.kr/ko - - https://ssob.hmac.kr/ko/ - - https://ssob.hmac.kr/en - - https://ssob.hmac.kr/en/ - - https://ssob.hmac.kr/auth/callback - - https://ssob.hmac.kr/ko/auth/callback - - https://ssob.hmac.kr/en/auth/callback + - ${KRATOS_UI_URL} + - ${KRATOS_UI_URL}/ + - ${USERFRONT_URL} + - ${USERFRONT_URL}/ + - ${USERFRONT_URL}/ko + - ${USERFRONT_URL}/ko/ + - ${USERFRONT_URL}/en + - ${USERFRONT_URL}/en/ + - ${USERFRONT_URL}/auth/callback + - ${USERFRONT_URL}/ko/auth/callback + - ${USERFRONT_URL}/en/auth/callback + - ${ADMINFRONT_URL}/auth/callback + - ${DEVFRONT_URL}/auth/callback + - ${ORGFRONT_URL}/auth/callback methods: password: @@ -55,24 +51,24 @@ selfservice: flows: error: - ui_url: http://localhost:{{USERFRONT_PORT}}/error + ui_url: ${KRATOS_UI_URL}/error settings: - ui_url: http://localhost:{{USERFRONT_PORT}}/error?error=settings_disabled + ui_url: ${KRATOS_UI_URL}/error?error=settings_disabled privileged_session_max_age: 15m recovery: - ui_url: http://localhost:{{USERFRONT_PORT}}/recovery + ui_url: ${KRATOS_UI_URL}/recovery use: code verification: - ui_url: http://localhost:{{USERFRONT_PORT}}/verification + ui_url: ${KRATOS_UI_URL}/verification use: code logout: after: - default_browser_return_url: http://localhost:{{USERFRONT_PORT}}/login + default_browser_return_url: ${KRATOS_UI_URL}/login login: - ui_url: http://localhost:{{USERFRONT_PORT}}/login + ui_url: ${KRATOS_UI_URL}/login lifespan: 10m registration: - ui_url: http://localhost:{{USERFRONT_PORT}}/registration + ui_url: ${KRATOS_UI_URL}/registration lifespan: 10m log: diff --git a/deploy/templates/ory/oathkeeper/rules.json b/deploy/templates/ory/oathkeeper/rules.json index 00fe02e3..ea15e287 100644 --- a/deploy/templates/ory/oathkeeper/rules.json +++ b/deploy/templates/ory/oathkeeper/rules.json @@ -1,9 +1,52 @@ [ { - "id": "backend-api-rule", + "id": "public-health", + "description": "공개 헬스체크", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", - "methods": ["GET", "POST", "PUT", "DELETE", "PATCH"] + "url": "<.*>://<[^/]+>/health", + "methods": ["GET"] + }, + "upstream": { + "url": "http://backend:{{BACKEND_PORT}}" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "public-preflight", + "description": "CORS preflight", + "match": { + "url": "<.*>://<[^/]+>/api/v1/<.*>", + "methods": ["OPTIONS"] + }, + "upstream": { + "url": "http://backend:{{BACKEND_PORT}}" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "public-auth", + "description": "인증/회원가입 등 공개 엔드포인트", + "match": { + "url": "<.*>://<[^/]+>/api/v1/auth/<.*>", + "methods": ["GET", "POST", "OPTIONS"] + }, + "upstream": { + "url": "http://backend:{{BACKEND_PORT}}" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "backend-command", + "description": "Command 요청은 Backend로 전달 (Audit 강제)", + "match": { + "url": "<.*>://<[^/]+>/api/v1/<.*>", + "methods": ["POST", "PUT", "PATCH", "DELETE"] }, "upstream": { "url": "http://backend:{{BACKEND_PORT}}" @@ -11,5 +54,106 @@ "authenticators": [{ "handler": "cookie_session" }], "authorizer": { "handler": "remote_json" }, "mutators": [{ "handler": "noop" }] + }, + { + "id": "backend-query", + "description": "Backend Query (admin/dev 포함)", + "match": { + "url": "<.*>://<[^/]+>/api/v1/<.*>", + "methods": ["GET"] + }, + "upstream": { + "url": "http://backend:{{BACKEND_PORT}}" + }, + "authenticators": [{ "handler": "cookie_session" }], + "authorizer": { "handler": "remote_json" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-well-known", + "description": "Hydra OIDC Discovery & JWKS", + "match": { + "url": "<.*>://<[^/]+>/.well-known/<.*>", + "methods": ["GET", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-well-known-oidc", + "description": "Hydra OIDC Discovery & JWKS (with /oidc prefix)", + "match": { + "url": "<.*>://<[^/]+>/oidc/.well-known/<.*>", + "methods": ["GET", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444", + "strip_path": "/oidc" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-oauth2", + "description": "Hydra OAuth2 Endpoints", + "match": { + "url": "<.*>://<[^/]+>/oauth2/<.*>", + "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-oauth2-oidc", + "description": "Hydra OAuth2 Endpoints (with /oidc prefix)", + "match": { + "url": "<.*>://<[^/]+>/oidc/oauth2/<.*>", + "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444", + "strip_path": "/oidc" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-userinfo", + "description": "Hydra Userinfo", + "match": { + "url": "<.*>://<[^/]+>/userinfo", + "methods": ["GET", "POST", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-userinfo-oidc", + "description": "Hydra Userinfo (with /oidc prefix)", + "match": { + "url": "<.*>://<[^/]+>/oidc/userinfo", + "methods": ["GET", "POST", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444", + "strip_path": "/oidc" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] } ] diff --git a/docker/compose.ory.yaml b/docker/compose.ory.yaml index a360fb9c..02a455e9 100644 --- a/docker/compose.ory.yaml +++ b/docker/compose.ory.yaml @@ -22,13 +22,13 @@ services: retries: 5 kratos-migrate: - image: oryd/kratos:${KRATOS_VERSION:-v25.4.0} + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - - KRATOS_SERVE_PUBLIC_BASE_URL="${KRATOS_BROWSER_URL:-http://localhost:4433}" - - KRATOS_SERVE_ADMIN_BASE_URL="${KRATOS_ADMIN_URL:-http://kratos:4434}" - - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL="${KRATOS_UI_URL:-http://localhost:5000}" - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS='["${KRATOS_UI_URL:-http://localhost:5000}","${USERFRONT_URL:-http://localhost:5000}"]' + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${USERFRONT_URL}"]} volumes: - ./docker/ory/kratos:/etc/config/kratos command: migrate sql up -e -c /etc/config/kratos/kratos.yml --yes @@ -39,15 +39,15 @@ services: - ory-net kratos: - image: oryd/kratos:${KRATOS_VERSION:-v25.4.0} + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} container_name: ory_kratos environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - - COOKIE_SECRET="${COOKIE_SECRET:-localcookie123}" - - KRATOS_SERVE_PUBLIC_BASE_URL="${KRATOS_BROWSER_URL:-http://localhost:4433}" - - KRATOS_SERVE_ADMIN_BASE_URL="${KRATOS_ADMIN_URL:-http://kratos:4434}" - - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL="${KRATOS_UI_URL:-http://localhost:5000}" - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS='["${KRATOS_UI_URL:-http://localhost:5000}","${USERFRONT_URL:-http://localhost:5000}"]' + - COOKIE_SECRET=${COOKIE_SECRET:-localcookie123} + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${USERFRONT_URL}"]} volumes: - ./docker/ory/kratos:/etc/config/kratos command: serve -c /etc/config/kratos/kratos.yml @@ -59,7 +59,7 @@ services: - kratosnet hydra-migrate: - image: oryd/hydra:${HYDRA_VERSION:-v25.4.0} + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 command: migrate sql up -e --yes @@ -70,13 +70,14 @@ services: - ory-net hydra: - image: oryd/hydra:${HYDRA_VERSION:-v25.4.0} + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} container_name: ory_hydra environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 - - URLS_SELF_ISSUER=${USERFRONT_URL:-http://localhost:5000}/oidc - - URLS_LOGIN=${USERFRONT_URL:-http://localhost:5000}/login - - URLS_CONSENT=${USERFRONT_URL:-http://localhost:5000}/consent + - URLS_SELF_ISSUER=${HYDRA_PUBLIC_URL} + - URLS_LOGIN=${HYDRA_LOGIN_URL:-${USERFRONT_URL}/login} + - URLS_CONSENT=${HYDRA_CONSENT_URL:-${USERFRONT_URL}/consent} + - URLS_ERROR=${HYDRA_ERROR_URL:-${USERFRONT_URL}/error} - SECRETS_SYSTEM=${ORY_POSTGRES_PASSWORD} volumes: - ./docker/ory/hydra:/etc/config/hydra @@ -89,7 +90,7 @@ services: - hydranet keto-migrate: - image: oryd/keto:${KETO_VERSION:-v25.4.0} + image: oryd/keto:${KETO_VERSION:-v26.2.0} environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 volumes: @@ -102,7 +103,7 @@ services: - ory-net keto: - image: oryd/keto:${KETO_VERSION:-v25.4.0} + image: oryd/keto:${KETO_VERSION:-v26.2.0} container_name: ory_keto environment: - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 @@ -116,21 +117,24 @@ services: - ory-net oathkeeper: - image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v0.40.6} + image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v26.2.0} container_name: oathkeeper restart: unless-stopped depends_on: kratos: condition: service_started environment: + - APP_ENV=${APP_ENV:-development} - LOG_LEVEL=debug - command: serve proxy --config /etc/config/oathkeeper/oathkeeper.yml + - OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} + - OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret} volumes: - ./docker/ory/oathkeeper:/etc/config/oathkeeper - oathkeeper_logs:/var/log/oathkeeper + entrypoint: ["/etc/config/oathkeeper/entrypoint.sh"] networks: - ory-net - - baron_net + - baron_net - public_net ports: - "4455:4455" @@ -168,47 +172,47 @@ services: - /bin/sh - -ec - | - apk add --no-cache curl tar - HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}" - HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}" - HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}" - curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz" - tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra - rm /tmp/hydra.tar.gz + apk add --no-cache curl tar + HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}" + HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}" + HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}" + curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz" + tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra + rm /tmp/hydra.tar.gz - echo "Creating/Updating OAuth2 Clients..." - - hydra create oauth2-client \ - --endpoint http://hydra:4445 \ - --id adminfront \ - --name "AdminFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri ${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback,http://172.16.10.176:5173/auth/callback} + echo "Creating/Updating OAuth2 Clients..." - hydra create oauth2-client \ - --endpoint http://hydra:4445 \ - --id devfront \ - --name "DevFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri ${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/auth/callback,http://172.16.10.176:5174/auth/callback} + hydra create oauth2-client \ + --endpoint "$${HYDRA_ADMIN_URL}" \ + --id adminfront \ + --name "AdminFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${ADMINFRONT_CALLBACK_URLS}" - hydra create oauth2-client \ - --endpoint http://hydra:4445 \ - --id orgfront \ - --name "OrgFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri ${ORGFRONT_CALLBACK_URLS:-http://localhost:5175/auth/callback,http://172.16.10.176:5175/auth/callback,https://baron-orgchart.hmac.kr/auth/callback} + hydra create oauth2-client \ + --endpoint "$${HYDRA_ADMIN_URL}" \ + --id devfront \ + --name "DevFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${DEVFRONT_CALLBACK_URLS}" - echo "All RP clients initialized successfully." + hydra create oauth2-client \ + --endpoint "$${HYDRA_ADMIN_URL}" \ + --id orgfront \ + --name "OrgFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${ORGFRONT_CALLBACK_URLS}" + + echo "All RP clients initialized successfully." depends_on: ory_stack_check: condition: service_completed_successfully diff --git a/docker/ory/kratos/kratos.yml b/docker/ory/kratos/kratos.yml index 9ccd01f2..d1bd22cd 100644 --- a/docker/ory/kratos/kratos.yml +++ b/docker/ory/kratos/kratos.yml @@ -4,23 +4,18 @@ dsn: ${DSN} serve: public: - base_url: http://localhost:4433/ + base_url: ${KRATOS_BROWSER_URL} cors: enabled: true allowed_origins: - - http://localhost:5173 - - http://localhost:5174 - - http://localhost:5175 - - http://localhost:5000 + - ${USERFRONT_URL} + - ${ADMINFRONT_URL} + - ${DEVFRONT_URL} + - ${ORGFRONT_URL} - http://backend:3000 - http://baron_backend:3000 - - https://ssologin.hmac.kr - - https://sso-test.hmac.kr - - https://app.brsw.kr - - https://sss.hmac.kr - - https://sso.hmac.kr admin: - base_url: http://localhost:4434/ + base_url: ${KRATOS_ADMIN_URL} session: cookie: @@ -29,21 +24,22 @@ session: path: / selfservice: - default_browser_return_url: http://localhost:5000/ + default_browser_return_url: ${KRATOS_UI_URL} allowed_return_urls: - - http://baron_backend:3000 - - http://baron_backend:3000/ - - http://localhost:5000 - - https://app.brsw.kr - - https://app.brsw.kr/ - - https://sss.hmac.kr - - https://sss.hmac.kr/ - - https://sso.hmac.kr - - https://sso.hmac.kr/ - - https://ssologin.hmac.kr - - https://ssologin.hmac.kr/ - - https://sso-test.hmac.kr - - https://sso-test.hmac.kr/ + - ${KRATOS_UI_URL} + - ${KRATOS_UI_URL}/ + - ${USERFRONT_URL} + - ${USERFRONT_URL}/ + - ${USERFRONT_URL}/ko + - ${USERFRONT_URL}/ko/ + - ${USERFRONT_URL}/en + - ${USERFRONT_URL}/en/ + - ${USERFRONT_URL}/auth/callback + - ${USERFRONT_URL}/ko/auth/callback + - ${USERFRONT_URL}/en/auth/callback + - ${ADMINFRONT_URL}/auth/callback + - ${DEVFRONT_URL}/auth/callback + - ${ORGFRONT_URL}/auth/callback methods: password: @@ -56,24 +52,24 @@ selfservice: flows: error: - ui_url: http://localhost:5000/error + ui_url: ${KRATOS_UI_URL}/error settings: - ui_url: http://localhost:5000/error?error=settings_disabled + ui_url: ${KRATOS_UI_URL}/error?error=settings_disabled privileged_session_max_age: 15m recovery: - ui_url: http://localhost:5000/recovery + ui_url: ${KRATOS_UI_URL}/recovery use: code verification: - ui_url: http://localhost:5000/verification + ui_url: ${KRATOS_UI_URL}/verification use: code logout: after: - default_browser_return_url: http://localhost:5000/login + default_browser_return_url: ${KRATOS_UI_URL}/login login: - ui_url: http://localhost:5000/login + ui_url: ${KRATOS_UI_URL}/login lifespan: 10m registration: - ui_url: http://localhost:5000/registration + ui_url: ${KRATOS_UI_URL}/registration lifespan: 10m log: diff --git a/docker/ory/oathkeeper/rules.active.json b/docker/ory/oathkeeper/rules.active.json index 4a0735da..e1331f74 100755 --- a/docker/ory/oathkeeper/rules.active.json +++ b/docker/ory/oathkeeper/rules.active.json @@ -3,7 +3,7 @@ "id": "public-health", "description": "공개 헬스체크", "match": { - "url": "<.*>://<.*>/health", + "url": "<.*>://<[^/]+>/health", "methods": ["GET"] }, "upstream": { @@ -17,7 +17,7 @@ "id": "public-preflight", "description": "CORS preflight", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["OPTIONS"] }, "upstream": { @@ -31,7 +31,7 @@ "id": "public-auth", "description": "인증/회원가입 등 공개 엔드포인트", "match": { - "url": "<.*>://<.*>/api/v1/auth/<.*>", + "url": "<.*>://<[^/]+>/api/v1/auth/<.*>", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { @@ -45,7 +45,7 @@ "id": "backend-command", "description": "Command 요청은 Backend로 전달 (Audit 강제)", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["POST", "PUT", "PATCH", "DELETE"] }, "upstream": { @@ -59,7 +59,7 @@ "id": "backend-query", "description": "Backend Query (admin/dev 포함)", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["GET"] }, "upstream": { @@ -73,7 +73,7 @@ "id": "hydra-well-known", "description": "Hydra OIDC Discovery & JWKS", "match": { - "url": "<.*>://<.*>/.well-known/<.*>", + "url": "<.*>://<[^/]+>/.well-known/<.*>", "methods": ["GET", "OPTIONS"] }, "upstream": { @@ -87,12 +87,12 @@ "id": "hydra-well-known-oidc", "description": "Hydra OIDC Discovery & JWKS (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/.well-known/<.*>", + "url": "<.*>://<[^/]+>/oidc/.well-known/<.*>", "methods": ["GET", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, @@ -102,7 +102,7 @@ "id": "hydra-oauth2", "description": "Hydra OAuth2 Endpoints", "match": { - "url": "<.*>://<.*>/oauth2/<.*>", + "url": "<.*>://<[^/]+>/oauth2/<.*>", "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] }, "upstream": { @@ -116,12 +116,12 @@ "id": "hydra-oauth2-oidc", "description": "Hydra OAuth2 Endpoints (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/oauth2/<.*>", + "url": "<.*>://<[^/]+>/oidc/oauth2/<.*>", "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, @@ -131,7 +131,7 @@ "id": "hydra-userinfo", "description": "Hydra Userinfo", "match": { - "url": "<.*>://<.*>/userinfo", + "url": "<.*>://<[^/]+>/userinfo", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { @@ -145,12 +145,12 @@ "id": "hydra-userinfo-oidc", "description": "Hydra Userinfo (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/userinfo", + "url": "<.*>://<[^/]+>/oidc/userinfo", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, diff --git a/docker/ory/oathkeeper/rules.json b/docker/ory/oathkeeper/rules.json index fd6bfb2d..da75325f 100755 --- a/docker/ory/oathkeeper/rules.json +++ b/docker/ory/oathkeeper/rules.json @@ -3,7 +3,7 @@ "id": "public-health", "description": "공개 헬스체크", "match": { - "url": "<.*>://<.*>/health", + "url": "<.*>://<[^/]+>/health", "methods": ["GET"] }, "upstream": { @@ -17,7 +17,7 @@ "id": "public-preflight", "description": "CORS preflight", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["OPTIONS"] }, "upstream": { @@ -31,7 +31,7 @@ "id": "public-auth", "description": "인증/회원가입 등 공개 엔드포인트", "match": { - "url": "<.*>://<.*>/api/v1/auth/<.*>", + "url": "<.*>://<[^/]+>/api/v1/auth/<.*>", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { @@ -45,7 +45,7 @@ "id": "backend-command", "description": "Command 요청은 Backend로 전달 (Audit 강제)", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["POST", "PUT", "PATCH", "DELETE"] }, "upstream": { @@ -59,7 +59,7 @@ "id": "backend-query", "description": "Backend Query (admin/dev 포함)", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["GET"] }, "upstream": { @@ -73,7 +73,7 @@ "id": "hydra-well-known", "description": "Hydra OIDC Discovery & JWKS", "match": { - "url": "<.*>://<.*>/.well-known/<.*>", + "url": "<.*>://<[^/]+>/.well-known/<.*>", "methods": ["GET", "OPTIONS"] }, "upstream": { @@ -87,12 +87,12 @@ "id": "hydra-well-known-oidc", "description": "Hydra OIDC Discovery & JWKS (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/.well-known/<.*>", + "url": "<.*>://<[^/]+>/oidc/.well-known/<.*>", "methods": ["GET", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, @@ -102,7 +102,7 @@ "id": "hydra-oauth2", "description": "Hydra OAuth2 Endpoints", "match": { - "url": "<.*>://<.*>/oauth2/<.*>", + "url": "<.*>://<[^/]+>/oauth2/<.*>", "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] }, "upstream": { @@ -116,12 +116,12 @@ "id": "hydra-oauth2-oidc", "description": "Hydra OAuth2 Endpoints (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/oauth2/<.*>", + "url": "<.*>://<[^/]+>/oidc/oauth2/<.*>", "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, @@ -131,7 +131,7 @@ "id": "hydra-userinfo", "description": "Hydra Userinfo", "match": { - "url": "<.*>://<.*>/userinfo", + "url": "<.*>://<[^/]+>/userinfo", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { @@ -145,12 +145,12 @@ "id": "hydra-userinfo-oidc", "description": "Hydra Userinfo (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/userinfo", + "url": "<.*>://<[^/]+>/oidc/userinfo", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, diff --git a/docker/ory/oathkeeper/rules.prod.json b/docker/ory/oathkeeper/rules.prod.json index ef5524d5..6728bb7a 100755 --- a/docker/ory/oathkeeper/rules.prod.json +++ b/docker/ory/oathkeeper/rules.prod.json @@ -1,9 +1,9 @@ [ { "id": "public-health", - "description": "공개 헬스체크 (PROD 도메인)", + "description": "공개 헬스체크 (PROD)", "match": { - "url": "https://app.brsw.kr/health", + "url": "<.*>://<[^/]+>/health", "methods": ["GET"] }, "upstream": { @@ -15,9 +15,9 @@ }, { "id": "public-preflight", - "description": "CORS preflight (PROD 도메인)", + "description": "CORS preflight (PROD)", "match": { - "url": "https://app.brsw.kr/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["OPTIONS"] }, "upstream": { @@ -29,9 +29,9 @@ }, { "id": "public-auth", - "description": "인증/회원가입 등 공개 엔드포인트 (PROD 도메인)", + "description": "인증/회원가입 등 공개 엔드포인트 (PROD)", "match": { - "url": "https://app.brsw.kr/api/v1/auth/<.*>", + "url": "<.*>://<[^/]+>/api/v1/auth/<.*>", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { @@ -45,7 +45,7 @@ "id": "backend-command", "description": "Command 요청은 Backend로 전달 (Audit 강제)", "match": { - "url": "https://app.brsw.kr/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["POST", "PUT", "PATCH", "DELETE"] }, "upstream": { @@ -59,7 +59,7 @@ "id": "backend-query", "description": "Backend Query (admin/dev 포함)", "match": { - "url": "https://app.brsw.kr/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["GET"] }, "upstream": { @@ -68,5 +68,92 @@ "authenticators": [{ "handler": "cookie_session" }], "authorizer": { "handler": "remote_json" }, "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-well-known", + "description": "Hydra OIDC Discovery & JWKS (PROD)", + "match": { + "url": "<.*>://<[^/]+>/.well-known/<.*>", + "methods": ["GET", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-well-known-oidc", + "description": "Hydra OIDC Discovery & JWKS with /oidc prefix (PROD)", + "match": { + "url": "<.*>://<[^/]+>/oidc/.well-known/<.*>", + "methods": ["GET", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444", + "strip_path": "/oidc" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-oauth2", + "description": "Hydra OAuth2 Endpoints (PROD)", + "match": { + "url": "<.*>://<[^/]+>/oauth2/<.*>", + "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-oauth2-oidc", + "description": "Hydra OAuth2 Endpoints with /oidc prefix (PROD 도메인)", + "match": { + "url": "<.*>://<[^/]+>/oidc/oauth2/<.*>", + "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444", + "strip_path": "/oidc" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-userinfo", + "description": "Hydra Userinfo (PROD)", + "match": { + "url": "<.*>://<[^/]+>/userinfo", + "methods": ["GET", "POST", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] + }, + { + "id": "hydra-userinfo-oidc", + "description": "Hydra Userinfo with /oidc prefix (PROD 도메인)", + "match": { + "url": "<.*>://<[^/]+>/oidc/userinfo", + "methods": ["GET", "POST", "OPTIONS"] + }, + "upstream": { + "url": "http://hydra:4444", + "strip_path": "/oidc" + }, + "authenticators": [{ "handler": "noop" }], + "authorizer": { "handler": "allow" }, + "mutators": [{ "handler": "noop" }] } ] diff --git a/docker/ory/oathkeeper/rules.stage.json b/docker/ory/oathkeeper/rules.stage.json index 4a0735da..e1331f74 100755 --- a/docker/ory/oathkeeper/rules.stage.json +++ b/docker/ory/oathkeeper/rules.stage.json @@ -3,7 +3,7 @@ "id": "public-health", "description": "공개 헬스체크", "match": { - "url": "<.*>://<.*>/health", + "url": "<.*>://<[^/]+>/health", "methods": ["GET"] }, "upstream": { @@ -17,7 +17,7 @@ "id": "public-preflight", "description": "CORS preflight", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["OPTIONS"] }, "upstream": { @@ -31,7 +31,7 @@ "id": "public-auth", "description": "인증/회원가입 등 공개 엔드포인트", "match": { - "url": "<.*>://<.*>/api/v1/auth/<.*>", + "url": "<.*>://<[^/]+>/api/v1/auth/<.*>", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { @@ -45,7 +45,7 @@ "id": "backend-command", "description": "Command 요청은 Backend로 전달 (Audit 강제)", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["POST", "PUT", "PATCH", "DELETE"] }, "upstream": { @@ -59,7 +59,7 @@ "id": "backend-query", "description": "Backend Query (admin/dev 포함)", "match": { - "url": "<.*>://<.*>/api/v1/<.*>", + "url": "<.*>://<[^/]+>/api/v1/<.*>", "methods": ["GET"] }, "upstream": { @@ -73,7 +73,7 @@ "id": "hydra-well-known", "description": "Hydra OIDC Discovery & JWKS", "match": { - "url": "<.*>://<.*>/.well-known/<.*>", + "url": "<.*>://<[^/]+>/.well-known/<.*>", "methods": ["GET", "OPTIONS"] }, "upstream": { @@ -87,12 +87,12 @@ "id": "hydra-well-known-oidc", "description": "Hydra OIDC Discovery & JWKS (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/.well-known/<.*>", + "url": "<.*>://<[^/]+>/oidc/.well-known/<.*>", "methods": ["GET", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, @@ -102,7 +102,7 @@ "id": "hydra-oauth2", "description": "Hydra OAuth2 Endpoints", "match": { - "url": "<.*>://<.*>/oauth2/<.*>", + "url": "<.*>://<[^/]+>/oauth2/<.*>", "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] }, "upstream": { @@ -116,12 +116,12 @@ "id": "hydra-oauth2-oidc", "description": "Hydra OAuth2 Endpoints (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/oauth2/<.*>", + "url": "<.*>://<[^/]+>/oidc/oauth2/<.*>", "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, @@ -131,7 +131,7 @@ "id": "hydra-userinfo", "description": "Hydra Userinfo", "match": { - "url": "<.*>://<.*>/userinfo", + "url": "<.*>://<[^/]+>/userinfo", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { @@ -145,12 +145,12 @@ "id": "hydra-userinfo-oidc", "description": "Hydra Userinfo (with /oidc prefix)", "match": { - "url": "<.*>://<.*>/oidc/userinfo", + "url": "<.*>://<[^/]+>/oidc/userinfo", "methods": ["GET", "POST", "OPTIONS"] }, "upstream": { "url": "http://hydra:4444", - "strip_path_prefix": "/oidc" + "strip_path": "/oidc" }, "authenticators": [{ "handler": "noop" }], "authorizer": { "handler": "allow" }, diff --git a/docker/staging_pull_compose.template.yaml b/docker/staging_pull_compose.template.yaml index 15195a52..d4a91f9c 100644 --- a/docker/staging_pull_compose.template.yaml +++ b/docker/staging_pull_compose.template.yaml @@ -1,501 +1,521 @@ services: - postgres: - image: postgres:17-alpine - container_name: baron_postgres - environment: - POSTGRES_USER: "${DB_USER:-baron}" - POSTGRES_PASSWORD: "${DB_PASSWORD:-password}" - POSTGRES_DB: "${DB_NAME:-baron_sso}" - ports: - - "${DB_PORT:-5432}:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./docker/init-metadata:/docker-entrypoint-initdb.d - networks: - - baron_net - healthcheck: - test: - [ - "CMD-SHELL", - "pg_isready -U ${DB_USER:-baron} -d ${DB_NAME:-baron_sso}", - ] - interval: 5s - timeout: 5s - retries: 5 - restart: always + postgres: + image: postgres:17-alpine + container_name: baron_postgres + environment: + POSTGRES_USER: "${DB_USER:-baron}" + POSTGRES_PASSWORD: "${DB_PASSWORD:-password}" + POSTGRES_DB: "${DB_NAME:-baron_sso}" + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/init-metadata:/docker-entrypoint-initdb.d + networks: + - baron_net + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U ${DB_USER:-baron} -d ${DB_NAME:-baron_sso}", + ] + interval: 5s + timeout: 5s + retries: 5 + restart: always - clickhouse: - image: clickhouse/clickhouse-server:latest - container_name: baron_clickhouse - restart: always - volumes: - - clickhouse_data:/var/lib/clickhouse - environment: - CLICKHOUSE_USER: "${CLICKHOUSE_USER:-baron}" - CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD:-password}" - networks: - - baron_net - healthcheck: - test: ["CMD", "clickhouse-client", "--query", "SELECT 1"] - interval: 5s - timeout: 5s - retries: 5 + clickhouse: + image: clickhouse/clickhouse-server:latest + container_name: baron_clickhouse + restart: always + volumes: + - clickhouse_data:/var/lib/clickhouse + environment: + CLICKHOUSE_USER: "${CLICKHOUSE_USER:-baron}" + CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD:-password}" + networks: + - baron_net + healthcheck: + test: ["CMD", "clickhouse-client", "--query", "SELECT 1"] + interval: 5s + timeout: 5s + retries: 5 - redis: - image: redis:7-alpine - container_name: baron_redis - restart: always - command: redis-server --port 6389 - ports: - - "6389:6389" - volumes: - - redis_data:/data - networks: - - baron_net - healthcheck: - test: ["CMD", "redis-cli", "-p", "6389", "ping"] - interval: 5s - timeout: 5s - retries: 5 + redis: + image: redis:7-alpine + container_name: baron_redis + restart: always + command: redis-server --port 6389 + ports: + - "6389:6389" + volumes: + - redis_data:/data + networks: + - baron_net + healthcheck: + test: ["CMD", "redis-cli", "-p", "6389", "ping"] + interval: 5s + timeout: 5s + retries: 5 - gateway: - image: nginx:alpine - container_name: baron_gateway - restart: always - ports: - - "${USERFRONT_PORT:-5000}:5000" - volumes: - - ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro - networks: - - baron_net - - public_net - healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 10s + gateway: + image: nginx:alpine + container_name: baron_gateway + restart: always + ports: + - "${USERFRONT_PORT:-5000}:5000" + volumes: + - ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - baron_net + - public_net + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s - postgres_ory: - image: postgres:${ORY_POSTGRES_TAG:-17-alpine} - container_name: ory_postgres - environment: - - POSTGRES_USER=${ORY_POSTGRES_USER:-ory} - - POSTGRES_PASSWORD=${ORY_POSTGRES_PASSWORD:-secret} - - POSTGRES_DB=${ORY_POSTGRES_DB:-ory} - volumes: - - ./docker/ory/init-db:/docker-entrypoint-initdb.d - - ory_postgres_data:/var/lib/postgresql/data - networks: - - ory-net - healthcheck: - test: - [ - "CMD-SHELL", - "pg_isready -U ${ORY_POSTGRES_USER:-ory} -d ${KRATOS_DB:-ory_kratos}", - ] - interval: 5s - timeout: 5s - retries: 5 + postgres_ory: + image: postgres:${ORY_POSTGRES_TAG:-17-alpine} + container_name: ory_postgres + environment: + - POSTGRES_USER=${ORY_POSTGRES_USER:-ory} + - POSTGRES_PASSWORD=${ORY_POSTGRES_PASSWORD:-secret} + - POSTGRES_DB=${ORY_POSTGRES_DB:-ory} + volumes: + - ./docker/ory/init-db:/docker-entrypoint-initdb.d + - ory_postgres_data:/var/lib/postgresql/data + networks: + - ory-net + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U ${ORY_POSTGRES_USER:-ory} -d ${KRATOS_DB:-ory_kratos}", + ] + interval: 5s + timeout: 5s + retries: 5 - kratos-migrate: - image: oryd/kratos:${KRATOS_VERSION:-v25.4.0} - environment: - - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL:-http://localhost:4433} - - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000} - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=["${KRATOS_UI_URL:-http://localhost:5000}","${USERFRONT_URL:-http://localhost:5000}"] - - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error - - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error?error=settings_disabled - - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/recovery - - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/verification - - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/login - - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/registration - - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000}/login - volumes: - - ./docker/ory/kratos:/etc/config/kratos - command: migrate sql up -e -c /etc/config/kratos/kratos.yml --yes - depends_on: - postgres_ory: - condition: service_healthy - networks: - - ory-net + kratos-migrate: + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} + environment: + - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]} + - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error + - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled + - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery + - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification + - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login + - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration + - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login + volumes: + - ./docker/ory/kratos:/etc/config/kratos + command: migrate sql up -e -c /etc/config/kratos/kratos.yml --yes + depends_on: + postgres_ory: + condition: service_healthy + networks: + - ory-net - kratos: - image: oryd/kratos:${KRATOS_VERSION:-v25.4.0} - container_name: ory_kratos - environment: - - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - - COOKIE_SECRET=${COOKIE_SECRET:-localcookie123} - - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL:-http://localhost:4433} - - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000} - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=["${KRATOS_UI_URL:-http://localhost:5000}","${USERFRONT_URL:-http://localhost:5000}"] - - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error - - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error?error=settings_disabled - - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/recovery - - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/verification - - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/login - - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/registration - - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000}/login - volumes: - - ./docker/ory/kratos:/etc/config/kratos - command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier - depends_on: - kratos-migrate: - condition: service_completed_successfully - networks: - - ory-net - - kratosnet + kratos: + image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} + container_name: ory_kratos + environment: + - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 + - COOKIE_SECRET=${COOKIE_SECRET:-localcookie123} + - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} + - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL} + - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]} + - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error + - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled + - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery + - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification + - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login + - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration + - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login + volumes: + - ./docker/ory/kratos:/etc/config/kratos + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + depends_on: + kratos-migrate: + condition: service_completed_successfully + networks: + - ory-net + - kratosnet - hydra-migrate: - image: oryd/hydra:${HYDRA_VERSION:-v25.4.0} - environment: - - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 - command: migrate sql up -e --yes - depends_on: - postgres_ory: - condition: service_healthy - networks: - - ory-net + hydra-migrate: + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} + environment: + - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 + command: migrate sql up -e --yes + depends_on: + postgres_ory: + condition: service_healthy + networks: + - ory-net - hydra: - image: oryd/hydra:${HYDRA_VERSION:-v25.4.0} - container_name: ory_hydra - environment: - - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 - - URLS_SELF_ISSUER=${USERFRONT_URL:-http://localhost:5000}/oidc - - URLS_LOGIN=${USERFRONT_URL:-http://localhost:5000}/login - - URLS_CONSENT=${USERFRONT_URL:-http://localhost:5000}/consent - - SECRETS_SYSTEM=${ORY_POSTGRES_PASSWORD} - volumes: - - ./docker/ory/hydra:/etc/config/hydra - command: serve -c /etc/config/hydra/hydra.yml all --dev - depends_on: - hydra-migrate: - condition: service_completed_successfully - networks: - - ory-net - - hydranet + hydra: + image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} + container_name: ory_hydra + environment: + - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 + - URLS_SELF_ISSUER=${HYDRA_PUBLIC_URL} + - URLS_LOGIN=${HYDRA_LOGIN_URL:-${USERFRONT_URL}/login} + - URLS_CONSENT=${HYDRA_CONSENT_URL:-${USERFRONT_URL}/consent} + - URLS_ERROR=${HYDRA_ERROR_URL:-${USERFRONT_URL}/error} + - SECRETS_SYSTEM=${ORY_POSTGRES_PASSWORD} + volumes: + - ./docker/ory/hydra:/etc/config/hydra + command: serve -c /etc/config/hydra/hydra.yml all --dev + depends_on: + hydra-migrate: + condition: service_completed_successfully + networks: + - ory-net + - hydranet - keto-migrate: - image: oryd/keto:${KETO_VERSION:-v25.4.0} - environment: - - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 - volumes: - - ./docker/ory/keto:/etc/config/keto - command: ["migrate", "up", "-c", "/etc/config/keto/keto.yml", "--yes"] - depends_on: - postgres_ory: - condition: service_healthy - networks: - - ory-net + keto-migrate: + image: oryd/keto:${KETO_VERSION:-v26.2.0} + environment: + - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 + volumes: + - ./docker/ory/keto:/etc/config/keto + command: ["migrate", "up", "-c", "/etc/config/keto/keto.yml", "--yes"] + depends_on: + postgres_ory: + condition: service_healthy + networks: + - ory-net - keto: - image: oryd/keto:${KETO_VERSION:-v25.4.0} - container_name: ory_keto - environment: - - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 - volumes: - - ./docker/ory/keto:/etc/config/keto - command: serve -c /etc/config/keto/keto.yml - depends_on: - keto-migrate: - condition: service_completed_successfully - networks: - - ory-net + keto: + image: oryd/keto:${KETO_VERSION:-v26.2.0} + container_name: ory_keto + environment: + - DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 + volumes: + - ./docker/ory/keto:/etc/config/keto + command: serve -c /etc/config/keto/keto.yml + depends_on: + keto-migrate: + condition: service_completed_successfully + networks: + - ory-net - oathkeeper: - image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v0.40.6} - container_name: oathkeeper - restart: unless-stopped - depends_on: - kratos: - condition: service_started - environment: - - LOG_LEVEL=debug - command: serve proxy --config /etc/config/oathkeeper/oathkeeper.yml - volumes: - - ./docker/ory/oathkeeper:/etc/config/oathkeeper - - oathkeeper_logs:/var/log/oathkeeper - networks: - - ory-net - - baron_net - - public_net - ports: - - "4455:4455" - - "4456:4456" - healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4456/health/ready"] - interval: 5s - timeout: 5s - retries: 5 + oathkeeper_logs_init: + image: alpine:latest + command: + [ + "sh", + "-c", + "mkdir -p /var/log/oathkeeper && chown -R ${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001} /var/log/oathkeeper", + ] + volumes: + - oathkeeper_logs:/var/log/oathkeeper + networks: + - ory-net - ory_clickhouse: - image: clickhouse/clickhouse-server:latest - container_name: ory_clickhouse - environment: - - CLICKHOUSE_USER=${ORY_CLICKHOUSE_USER:-ory} - - CLICKHOUSE_PASSWORD=${ORY_CLICKHOUSE_PASSWORD:-orypass} - volumes: - - ory_clickhouse_data:/var/lib/clickhouse - - ./docker/ory/clickhouse:/docker-entrypoint-initdb.d - networks: - - ory-net + oathkeeper: + image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v0.40.6} + container_name: oathkeeper + restart: unless-stopped + depends_on: + oathkeeper_logs_init: + condition: service_completed_successfully + kratos: + condition: service_started + user: "${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001}" + environment: + - APP_ENV=${APP_ENV:-stage} + - LOG_LEVEL=debug + - OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} + - OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret} + volumes: + - ./docker/ory/oathkeeper:/etc/config/oathkeeper + - oathkeeper_logs:/var/log/oathkeeper + entrypoint: ["/etc/config/oathkeeper/entrypoint.sh"] + networks: + - ory-net + - baron_net + - public_net + ports: + - "4455:4455" + - "4456:4456" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4456/health/ready"] + interval: 5s + timeout: 5s + retries: 5 - ory_stack_check: - image: alpine:latest - container_name: ory_stack_check - command: > - /bin/sh -c " - apk add --no-cache curl; - echo 'Wait for services...'; - until curl -s http://kratos:4433/health/ready; do sleep 1; done; - until curl -s http://hydra:4444/health/ready; do sleep 1; done; - until curl -s http://keto:4466/health/ready; do sleep 1; done; - echo 'Ory Stack is fully operational!';" - depends_on: - - kratos - - hydra - - keto - networks: - - ory-net + ory_clickhouse: + image: clickhouse/clickhouse-server:latest + container_name: ory_clickhouse + environment: + - CLICKHOUSE_USER=${ORY_CLICKHOUSE_USER:-ory} + - CLICKHOUSE_PASSWORD=${ORY_CLICKHOUSE_PASSWORD:-orypass} + volumes: + - ory_clickhouse_data:/var/lib/clickhouse + - ./docker/ory/clickhouse:/docker-entrypoint-initdb.d + networks: + - ory-net - init-rp: - image: alpine:latest - env_file: - - .env - command: - - /bin/sh - - -ec - - | - apk add --no-cache curl tar - HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}" - HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}" - HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}" - curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz" - tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra - rm /tmp/hydra.tar.gz + ory_stack_check: + image: alpine:latest + container_name: ory_stack_check + command: > + /bin/sh -c " + apk add --no-cache curl; + echo 'Wait for services...'; + until curl -s http://kratos:4433/health/ready; do sleep 1; done; + until curl -s http://hydra:4444/health/ready; do sleep 1; done; + until curl -s http://keto:4466/health/ready; do sleep 1; done; + echo 'Ory Stack is fully operational!';" + depends_on: + - kratos + - hydra + - keto + networks: + - ory-net - # Function to create or update OAuth2 client (Idempotency) - upsert_client() { - ID=$$1 - shift - if hydra get oauth2-client --endpoint http://hydra:4445 "$$ID" >/dev/null 2>&1; then - echo "Updating existing client: $$ID" - hydra update oauth2-client --endpoint http://hydra:4445 "$$ID" "$$@" - else - echo "Creating new client: $$ID" - hydra create oauth2-client --endpoint http://hydra:4445 --id "$$ID" "$$@" - fi - } + init-rp: + image: alpine:latest + env_file: + - .env + command: + - /bin/sh + - -ec + - | + apk add --no-cache curl tar + HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}" + HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}" + HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}" + curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz" + tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra + rm /tmp/hydra.tar.gz - upsert_client "adminfront" \ - --name "AdminFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri "$${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback}" + # Function to create or update OAuth2 client (Idempotency) + upsert_client() { + ID=$$1 + shift + if hydra get oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" "$$ID" >/dev/null 2>&1; then + echo "Updating existing client: $$ID" + hydra update oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" "$$ID" "$$@" + else + echo "Creating new client: $$ID" + hydra create oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" --id "$$ID" "$$@" + fi + } - upsert_client "devfront" \ - --name "DevFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri "$${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/auth/callback}" + upsert_client "adminfront" \ + --name "AdminFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${ADMINFRONT_CALLBACK_URLS:-$${ADMINFRONT_URL}/auth/callback}" - upsert_client "orgfront" \ - --name "OrgFront" \ - --grant-type authorization_code,refresh_token \ - --response-type code \ - --scope openid,offline_access,profile,email \ - --token-endpoint-auth-method none \ - --redirect-uri "$${ORGFRONT_CALLBACK_URLS:-http://localhost:5175/auth/callback,http://172.16.10.176:5175/auth/callback, https://baron-orgchart.hmac.kr/auth/callback}" + upsert_client "devfront" \ + --name "DevFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${DEVFRONT_CALLBACK_URLS:-$${DEVFRONT_URL}/auth/callback}" - upsert_client "$${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}" \ - --secret "$${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}" \ - --grant-type client_credentials \ - --response-type token \ - --scope openid,offline_access,profile,email - depends_on: - ory_stack_check: - condition: service_completed_successfully - networks: - - hydranet + upsert_client "orgfront" \ + --name "OrgFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri "$${ORGFRONT_CALLBACK_URLS:-$${ORGFRONT_URL}/auth/callback}" - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: baron_backend - env_file: - - .env - environment: - - APP_ENV=${APP_ENV:-development} - - GO_ENV=${APP_ENV:-development} - - COOKIE_SECRET=${COOKIE_SECRET} - - JWT_SECRET=${JWT_SECRET} - - NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY} - - NAVER_CLOUD_SECRET_KEY=${NAVER_CLOUD_SECRET_KEY} - - NAVER_CLOUD_SERVICE_ID=${NAVER_CLOUD_SERVICE_ID} - - NAVER_SENDER_PHONE_NUMBER=${NAVER_SENDER_PHONE_NUMBER} - - USERFRONT_URL=${USERFRONT_URL} - - REDIS_ADDR=${REDIS_ADDR} - - IDP_PROVIDER=${IDP_PROVIDER:-ory} - - KRATOS_ADMIN_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - - HYDRA_ADMIN_URL=${HYDRA_ADMIN_URL:-http://hydra:4445} - - HYDRA_PUBLIC_URL=${HYDRA_PUBLIC_URL:-http://hydra:4444} - - DB_HOST=postgres - - CLICKHOUSE_HOST=clickhouse - - CLICKHOUSE_PORT=${CLICKHOUSE_PORT_NATIVE:-9000} - - CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron} - - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password} - - SEED_TENANT_CSV_PATH=/app/seed-tenant.csv - depends_on: - clickhouse: - condition: service_healthy - redis: - condition: service_healthy - oathkeeper: - condition: service_healthy - kratos: - condition: service_started - hydra: - condition: service_started - keto: - condition: service_started - infra_check: - condition: service_started - networks: - - baron_net - - ory-net - volumes: - - ./backend:/app - - ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro - command: ["go", "run", "./cmd/server"] - healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 10s + upsert_client "$${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}" \ + --secret "$${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}" \ + --grant-type client_credentials \ + --response-type token \ + --scope openid,offline_access,profile,email + depends_on: + ory_stack_check: + condition: service_completed_successfully + networks: + - hydranet - adminfront: - build: - context: ./adminfront - dockerfile: Dockerfile - container_name: baron_adminfront - env_file: - - .env - environment: - - APP_ENV=${APP_ENV:-development} - - API_PROXY_TARGET=http://baron_backend:3000 - ports: - - "${ADMINFRONT_PORT:-5173}:5173" - volumes: - - ./adminfront:/app - - /app/node_modules - networks: - - baron_net + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: baron_backend + env_file: + - .env + environment: + - APP_ENV=${APP_ENV:-development} + - GO_ENV=${APP_ENV:-development} + - COOKIE_SECRET=${COOKIE_SECRET} + - JWT_SECRET=${JWT_SECRET} + - NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY} + - NAVER_CLOUD_SECRET_KEY=${NAVER_CLOUD_SECRET_KEY} + - NAVER_CLOUD_SERVICE_ID=${NAVER_CLOUD_SERVICE_ID} + - NAVER_SENDER_PHONE_NUMBER=${NAVER_SENDER_PHONE_NUMBER} + - USERFRONT_URL=${USERFRONT_URL} + - REDIS_ADDR=${REDIS_ADDR} + - IDP_PROVIDER=${IDP_PROVIDER:-ory} + - KRATOS_ADMIN_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} + - HYDRA_ADMIN_URL=${HYDRA_ADMIN_URL:-http://hydra:4445} + - HYDRA_PUBLIC_URL=${HYDRA_PUBLIC_URL} + - DB_HOST=postgres + - CLICKHOUSE_HOST=clickhouse + - CLICKHOUSE_PORT=${CLICKHOUSE_PORT_NATIVE:-9000} + - CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron} + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password} + - SEED_TENANT_CSV_PATH=/app/seed-tenant.csv + depends_on: + clickhouse: + condition: service_healthy + redis: + condition: service_healthy + oathkeeper: + condition: service_healthy + kratos: + condition: service_started + hydra: + condition: service_started + keto: + condition: service_started + infra_check: + condition: service_started + networks: + - baron_net + - ory-net + volumes: + - ./backend:/app + - ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro + command: ["go", "run", "./cmd/server"] + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s - devfront: - build: - context: ./devfront - dockerfile: Dockerfile - container_name: baron_devfront - env_file: - - .env - environment: - - APP_ENV=${APP_ENV:-development} - - API_PROXY_TARGET=http://baron_backend:3000 - ports: - - "${DEVFRONT_PORT:-5174}:5173" - volumes: - - ./devfront:/app - - /app/node_modules - networks: - - baron_net + adminfront: + build: + context: ./adminfront + dockerfile: Dockerfile + container_name: baron_adminfront + env_file: + - .env + environment: + - APP_ENV=${APP_ENV:-development} + - API_PROXY_TARGET=http://baron_backend:3000 + ports: + - "${ADMINFRONT_PORT:-5173}:5173" + volumes: + - ./adminfront:/app + - /app/node_modules + networks: + - baron_net - orgfront: - build: - context: ./orgfront - dockerfile: Dockerfile - container_name: baron_orgfront - env_file: - - .env - environment: - - APP_ENV=${APP_ENV:-development} - - API_PROXY_TARGET=http://baron_backend:3000 - - USERFRONT_URL=${USERFRONT_URL} - ports: - - "${ORGFRONT_PORT:-5175}:5175" - volumes: - - ./orgfront:/app - - /app/node_modules - networks: - - baron_net + devfront: + build: + context: ./devfront + dockerfile: Dockerfile + container_name: baron_devfront + env_file: + - .env + environment: + - APP_ENV=${APP_ENV:-development} + - API_PROXY_TARGET=http://baron_backend:3000 + ports: + - "${DEVFRONT_PORT:-5174}:5173" + volumes: + - ./devfront:/app + - /app/node_modules + networks: + - baron_net - userfront: - build: - context: . - dockerfile: userfront/Dockerfile - container_name: baron_userfront - env_file: - - .env - environment: - - BACKEND_URL=${BACKEND_URL:-} - - USERFRONT_URL=${USERFRONT_URL} - - APP_ENV=${APP_ENV} - networks: - - baron_net - - ory-net - depends_on: - backend: - condition: service_healthy - command: > - /bin/sh -c "mkdir -p /usr/share/nginx/html/assets && - echo \"BACKEND_URL=$${BACKEND_URL}\" >> /usr/share/nginx/html/assets/.env && - echo \"USERFRONT_URL=$${USERFRONT_URL}\" >> /usr/share/nginx/html/assets/.env && - echo \"APP_ENV=$${APP_ENV}\" >> /usr/share/nginx/html/assets/.env && - cp /usr/share/nginx/html/assets/.env /usr/share/nginx/html/.env && - nginx -g 'daemon off;'" - healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 10s + orgfront: + build: + context: ./orgfront + dockerfile: Dockerfile + container_name: baron_orgfront + env_file: + - .env + environment: + - APP_ENV=${APP_ENV:-development} + - API_PROXY_TARGET=http://baron_backend:3000 + - USERFRONT_URL=${USERFRONT_URL} + ports: + - "${ORGFRONT_PORT:-5175}:5175" + volumes: + - ./orgfront:/app + - /app/node_modules + networks: + - baron_net - infra_check: - image: alpine - command: ["echo", "Infrastructure assumed running"] - networks: - - baron_net + userfront: + build: + context: . + dockerfile: userfront/Dockerfile + container_name: baron_userfront + env_file: + - .env + environment: + - BACKEND_URL=${BACKEND_URL:-} + - USERFRONT_URL=${USERFRONT_URL} + - APP_ENV=${APP_ENV} + networks: + - baron_net + - ory-net + depends_on: + backend: + condition: service_healthy + command: > + /bin/sh -c "mkdir -p /usr/share/nginx/html/assets && + echo \"BACKEND_URL=$${BACKEND_URL}\" >> /usr/share/nginx/html/assets/.env && + echo \"USERFRONT_URL=$${USERFRONT_URL}\" >> /usr/share/nginx/html/assets/.env && + echo \"APP_ENV=$${APP_ENV}\" >> /usr/share/nginx/html/assets/.env && + cp /usr/share/nginx/html/assets/.env /usr/share/nginx/html/.env && + nginx -g 'daemon off;'" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + + infra_check: + image: alpine + command: ["echo", "Infrastructure assumed running"] + networks: + - baron_net volumes: - postgres_data: - clickhouse_data: - redis_data: - ory_postgres_data: - ory_clickhouse_data: - oathkeeper_logs: + postgres_data: + clickhouse_data: + redis_data: + ory_postgres_data: + ory_clickhouse_data: + oathkeeper_logs: networks: - baron_net: - external: true - name: baron_net - public_net: - external: true - name: public_net - ory-net: - external: true - name: ory-net - hydranet: - external: true - name: hydranet - kratosnet: - external: true - name: kratosnet + baron_net: + external: true + name: baron_net + public_net: + external: true + name: public_net + ory-net: + external: true + name: ory-net + hydranet: + external: true + name: hydranet + kratosnet: + external: true + name: kratosnet diff --git a/docs/devfront_auth_flow_explanation.md b/docs/devfront_auth_flow_explanation.md index 9b9a17fa..46b86dff 100644 --- a/docs/devfront_auth_flow_explanation.md +++ b/docs/devfront_auth_flow_explanation.md @@ -23,7 +23,7 @@ sequenceDiagram UF->>HY: 로그인 승인 요청 HY->>User: 권한 동의(Consent) 화면 표시 User->>HY: '허용' 클릭 - HY-->>DF: 인증 코드와 함께 리다이렉트 (/callback?code=...) + HY-->>DF: 인증 코드와 함께 리다이렉트 (/auth/callback?code=...) DF->>HY: 토큰 교환 요청 (Code -> ID/Access Token) HY-->>DF: 토큰 발급 Note over DF: [FIX] 백엔드 /api/me 호출 대신
ID Token에서 프로필 정보 직접 추출 @@ -46,7 +46,7 @@ sequenceDiagram * 사용자가 '허용'을 누르면 Hydra는 `devfront`가 신뢰할 수 있는 앱임을 기록합니다. 4. **인증 코드 전달 및 토큰 교환 (Callback)**: - * Hydra는 사용자를 `devfront`의 콜백 페이지(`http://localhost:5174/callback?code=...`)로 보냅니다. + * Hydra는 사용자를 `devfront`의 콜백 페이지(`http://localhost:5174/auth/callback?code=...`)로 보냅니다. * `devfront`는 이 코드를 Hydra의 토큰 엔드포인트로 보내 **ID Token**과 **Access Token**을 발급받습니다. 5. **사용자 정보 로드 (Profile Recovery)**: @@ -71,9 +71,9 @@ hydra clients create --response-types code --scope openid,offline_access,profile,email --token-endpoint-auth-method none \ # Public Client (PKCE 사용) - --callbacks http://localhost:5174/callback; + --callbacks http://localhost:5174/auth/callback; ``` -이 설정으로 인해 `devfront`라는 ID의 클라이언트가 미리 존재하게 되며, `localhost:5174`로의 리다이렉션이 안전하게 허용됩니다. +이 설정으로 인해 `devfront`라는 ID의 클라이언트가 미리 존재하게 되며, `localhost:5174/auth/callback`으로의 리다이렉션이 안전하게 허용됩니다. --- diff --git a/docs/oidc_redirect_mapping_validation_policy.md b/docs/oidc_redirect_mapping_validation_policy.md index f71a1680..d46fdfc9 100644 --- a/docs/oidc_redirect_mapping_validation_policy.md +++ b/docs/oidc_redirect_mapping_validation_policy.md @@ -7,7 +7,8 @@ ## 적용 범위 - UserFront, AdminFront, DevFront의 로그인/콜백 경로 - Ory Stack(Hydra/Kratos/Oathkeeper) 설정 -- `compose.ory.yaml`, `gateway/nginx.conf`, `docker/ory/oathkeeper/rules*.json` +- `compose.ory.yaml`, `docker/compose.ory.yaml`, `docker/staging_pull_compose.template.yaml` +- `gateway/nginx.conf`, `deploy/templates/gateway/nginx.conf`, `docker/ory/oathkeeper/rules*.json` - `Makefile` 기반 사전 검증/스모크 검증 단계 ## 핵심 원칙 @@ -27,8 +28,8 @@ 2. `mapped_match` - Public URL과 Internal URL이 다르지만, 아래가 모두 성립 -- Gateway 라우팅 규칙 존재 (예: `/oidc` rewrite) -- Oathkeeper `match`와 `upstream` 규칙 존재 (예: `strip_path_prefix=/oidc`) +- Gateway 라우팅 규칙 존재: `/oidc` prefix를 제거하지 않고 Oathkeeper로 전달 +- Oathkeeper `match`와 `upstream` 규칙 존재: `/oidc/*` rule이 `strip_path=/oidc`로 Hydra에 전달 - 최종 업스트림이 기대 서비스(Hydra/Kratos)로 연결 3. `unmapped_fail` @@ -42,6 +43,8 @@ - `ADMINFRONT_CALLBACK_URLS`, `DEVFRONT_CALLBACK_URLS` URL 유효성/중복/경로 규약 - Gateway `/oidc`, `/auth` 라우팅 규칙 존재 여부 - Oathkeeper `rules*.json`의 Hydra/Kratos 매핑 규칙 존재 여부 +- staging pull/deploy template의 Oathkeeper entrypoint 사용 여부 +- `KRATOS_ALLOWED_RETURN_URLS_JSON`에 공개 도메인, locale path, callback/return path가 포함되는지 여부 2. 런타임 검증 (`make verify-oidc-config`) - OIDC Discovery endpoint 조회 가능 여부 @@ -49,9 +52,38 @@ - 필요 시 Gateway 경유 endpoint probe로 매핑 체인 확인 ## 경로 규약 -- DevFront callback: `/callback` +- DevFront callback: `/auth/callback` - AdminFront callback: `/auth/callback` +- OrgFront callback: `/auth/callback` - UserFront OIDC 진입점: `/oidc/*` (Gateway 경유) +- locale return path: `/ko`, `/en`, `/ko/auth/callback`, `/en/auth/callback` + +## `/oidc` 책임 경계 +- Gateway는 `/oidc` prefix를 보존합니다. +- Oathkeeper는 `/oidc/.well-known/*`, `/oidc/oauth2/*`, `/oidc/userinfo` rule에서 `strip_path=/oidc`를 적용합니다. +- Hydra는 prefix가 제거된 내부 경로(`/.well-known/*`, `/oauth2/*`, `/userinfo`)를 받습니다. +- 따라서 gateway template이나 staging pull compose에서 `rewrite ^/oidc`가 다시 들어가면 dev/stage/prod 간 책임 경계가 달라지므로 실패로 간주합니다. + +## Oathkeeper rules 선택 정책 +- Oathkeeper는 직접 `command: serve proxy ...`로 시작하지 않고 `/etc/config/oathkeeper/entrypoint.sh`를 통해 시작합니다. +- entrypoint는 `APP_ENV`에 따라 다음 파일을 선택하고 `/tmp/oathkeeper/rules.active.json`으로 복사합니다. + - `stage|staging`: `rules.stage.json` + - `production|prod`: `rules.prod.json` + - 그 외: `rules.json` +- `oathkeeper.yml`은 `file:///tmp/oathkeeper/rules.active.json`만 읽습니다. + +## Kratos allowed return URL 정책 +- stage/prod에서는 `KRATOS_ALLOWED_RETURN_URLS_JSON`을 명시하는 것을 우선합니다. +- 최소 포함 대상: + - `KRATOS_UI_URL`, `KRATOS_UI_URL/` + - `USERFRONT_URL`, `USERFRONT_URL/` + - `USERFRONT_URL/ko`, `USERFRONT_URL/ko/` + - `USERFRONT_URL/en`, `USERFRONT_URL/en/` + - `USERFRONT_URL/auth/callback` + - `USERFRONT_URL/ko/auth/callback` + - `USERFRONT_URL/en/auth/callback` + - `ADMINFRONT_CALLBACK_URLS`, `DEVFRONT_CALLBACK_URLS`, `ORGFRONT_CALLBACK_URLS` +- private IP, legacy domain, comma-space가 포함된 URI 항목은 stage/prod 기본값으로 두지 않습니다. ## 운영 지침 1. 환경별 URL은 동일할 필요가 없고, 매핑 체인이 검증 가능해야 합니다. @@ -65,3 +97,4 @@ - #272 - #274 - #276 +- #710 diff --git a/docs/ory-usage.md b/docs/ory-usage.md index e0c6d37f..4f2de115 100644 --- a/docs/ory-usage.md +++ b/docs/ory-usage.md @@ -33,12 +33,14 @@ Ory 구성은 **컨테이너 내부 통신 URL**과 **브라우저 접근 URL** ### 내부 통신용 URL(컨테이너 네트워크) - `KRATOS_PUBLIC_URL=http://kratos:4433` - `KRATOS_ADMIN_URL=http://kratos:4434` -- `HYDRA_PUBLIC_URL=http://hydra:4444` - `HYDRA_ADMIN_URL=http://hydra:4445` +- Hydra public upstream은 Oathkeeper rule 내부에서 `http://hydra:4444`로 전달합니다. ### 브라우저 접근용 URL(외부 도메인/프록시) -- `KRATOS_BROWSER_URL` : Kratos Public의 외부 URL +- `KRATOS_BROWSER_URL` : Kratos Public의 외부 URL. 보통 `${OATHKEEPER_PUBLIC_URL}/auth` - `KRATOS_UI_URL` : UserFront의 외부 URL (Kratos UI 역할) +- `HYDRA_PUBLIC_URL` : Hydra issuer/OIDC discovery의 외부 URL. 보통 `${OATHKEEPER_PUBLIC_URL}/oidc` +- `VITE_OIDC_AUTHORITY` : 프론트엔드 OIDC authority. `HYDRA_PUBLIC_URL`과 같아야 합니다. 예시(로컬): ```env @@ -48,8 +50,11 @@ KRATOS_UI_URL=http://localhost:5000 예시(리버스 프록시/도메인): ```env -KRATOS_BROWSER_URL=https://sso.example.com +OATHKEEPER_PUBLIC_URL=https://sso.example.com +KRATOS_BROWSER_URL=https://sso.example.com/auth KRATOS_UI_URL=https://sso.example.com +HYDRA_PUBLIC_URL=https://sso.example.com/oidc +VITE_OIDC_AUTHORITY=https://sso.example.com/oidc ``` ### 포트 노출 정책 @@ -64,6 +69,7 @@ Kratos는 self-service UI URL을 설정값으로 사용합니다. **UserFront의 - `KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL` - `KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS` - `KRATOS_SELFSERVICE_FLOWS_*_UI_URL` +- `KRATOS_ALLOWED_RETURN_URLS_JSON` compose에서 기본적으로 다음과 같이 오버라이드합니다: - `KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login` @@ -72,18 +78,44 @@ compose에서 기본적으로 다음과 같이 오버라이드합니다: - `KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery` - `KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification` -## 5) 트러블슈팅 -### 5.1 로그인 클릭 시 동작 없음 +stage/prod에서는 `KRATOS_ALLOWED_RETURN_URLS_JSON`에 공개 도메인과 callback/locale 경로를 명시합니다. + +필수 후보: +- `${KRATOS_UI_URL}`, `${KRATOS_UI_URL}/` +- `${USERFRONT_URL}`, `${USERFRONT_URL}/` +- `${USERFRONT_URL}/ko`, `${USERFRONT_URL}/ko/` +- `${USERFRONT_URL}/en`, `${USERFRONT_URL}/en/` +- `${USERFRONT_URL}/auth/callback` +- `${USERFRONT_URL}/ko/auth/callback` +- `${USERFRONT_URL}/en/auth/callback` +- `${ADMINFRONT_CALLBACK_URLS}`, `${DEVFRONT_CALLBACK_URLS}`, `${ORGFRONT_CALLBACK_URLS}` + +## 5) `/oidc` Gateway/Oathkeeper 책임 경계 +Gateway는 `/oidc` prefix를 rewrite하지 않습니다. `/oidc/*` 요청은 prefix를 보존한 채 Oathkeeper로 전달하고, Oathkeeper rule이 `strip_path=/oidc`로 Hydra 내부 upstream(`http://hydra:4444`)에 전달합니다. + +이 정책은 `gateway/nginx.conf`, `deploy/templates/gateway/nginx.conf`, `docker/ory/oathkeeper/rules*.json`, `docker/staging_pull_compose.template.yaml`에서 동일해야 합니다. + +## 6) Oathkeeper active rules +Oathkeeper는 `/etc/config/oathkeeper/entrypoint.sh`를 통해 시작해야 합니다. entrypoint는 `APP_ENV`에 따라 env별 rules 파일을 고르고 `/tmp/oathkeeper/rules.active.json`을 생성합니다. + +- `APP_ENV=stage|staging`: `rules.stage.json` +- `APP_ENV=production|prod`: `rules.prod.json` +- 그 외: `rules.json` + +`docker/ory/oathkeeper/oathkeeper.yml`은 `file:///tmp/oathkeeper/rules.active.json`을 읽습니다. compose나 배포 템플릿이 entrypoint를 우회해 `oathkeeper serve proxy`를 직접 실행하면 active rules 생성이 누락될 수 있습니다. + +## 7) 트러블슈팅 +### 7.1 로그인 클릭 시 동작 없음 - 원인: Kratos 기동 실패(설정 파싱 실패 등) 또는 브라우저용 URL이 내부 도메인(`kratos:...`)으로 설정됨 - 확인: - `docker logs ory_kratos`에서 config 오류 여부 확인 - 브라우저 네트워크 탭에서 `/self-service/login/browser` 응답 확인(302 Location 헤더) -### 5.2 kratos.yml에 ${...} 환경 변수 치환 실패 +### 7.2 kratos.yml에 ${...} 환경 변수 치환 실패 - Kratos 설정 파일은 `${ENV}` 치환을 지원하지 않음 - 해결: compose 환경 변수로 `KRATOS_SELFSERVICE_*`, `KRATOS_SERVE_*` 오버라이드 사용 -## 6) 네트워크 접근 테스트 +## 8) 네트워크 접근 테스트 아래 스크립트는 **ory-net에서 Admin 포트 접근 가능** / **baron_net(Frontend 영역)에서 접근 불가**를 검증합니다. ```bash @@ -101,7 +133,7 @@ docker run --rm --network baron_net curlimages/curl:8.10.1 -fsS http://hydra:444 docker run --rm --network baron_net curlimages/curl:8.10.1 -fsS http://kratos:4434/health/ready ``` -## 7) 참고 파일 +## 9) 참고 파일 - `compose.ory.yaml` - `docker/ory/kratos/kratos.yml` - `.env.sample` diff --git a/gateway/nginx.conf b/gateway/nginx.conf index a113d968..2bc97d70 100644 --- a/gateway/nginx.conf +++ b/gateway/nginx.conf @@ -61,7 +61,6 @@ server { # Hydra Public API location /oidc { - rewrite ^/oidc/(.*)$ /$1 break; proxy_pass $oathkeeper_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/scripts/auth_config.sh b/scripts/auth_config.sh index 735320ed..544638ac 100755 --- a/scripts/auth_config.sh +++ b/scripts/auth_config.sh @@ -16,11 +16,12 @@ fi USERFRONT_URL="${USERFRONT_URL:-http://localhost:5000}" OATHKEEPER_PUBLIC_URL="${OATHKEEPER_PUBLIC_URL:-$USERFRONT_URL}" HYDRA_PUBLIC_URL="${HYDRA_PUBLIC_URL:-${OATHKEEPER_PUBLIC_URL%/}/oidc}" +HYDRA_ADMIN_URL="${HYDRA_ADMIN_URL:-http://hydra:4445}" KRATOS_UI_URL="${KRATOS_UI_URL:-http://localhost:5000}" ADMINFRONT_URL="${ADMINFRONT_URL:-https://sadmin.hmac.kr}" DEVFRONT_URL="${DEVFRONT_URL:-https://sdev.hmac.kr}" -ADMINFRONT_CALLBACK_URLS="${ADMINFRONT_CALLBACK_URLS:-http://172.16.10.176:5173/auth/callback}" -DEVFRONT_CALLBACK_URLS="${DEVFRONT_CALLBACK_URLS:-http://172.16.10.176:5174/auth/callback}" +ADMINFRONT_CALLBACK_URLS="${ADMINFRONT_CALLBACK_URLS:-${ADMINFRONT_URL%/}/auth/callback}" +DEVFRONT_CALLBACK_URLS="${DEVFRONT_CALLBACK_URLS:-${DEVFRONT_URL%/}/auth/callback}" KRATOS_ALLOWED_RETURN_URLS_EXTRA="${KRATOS_ALLOWED_RETURN_URLS_EXTRA:-}" declare -a WARNINGS=() @@ -258,13 +259,10 @@ validate_gateway_mapping() { if ! grep -Eq 'location /oidc' "$ROOT_DIR/gateway/nginx.conf"; then mode="unmapped_fail" fi - if ! grep -Eq 'rewrite \^/oidc/\(\.\*\)\$ /\$1 break;' "$ROOT_DIR/gateway/nginx.conf"; then + if ! grep -Eq '"url": "<\.\*>://<(\.\*|\[\^/\]\+)>/oidc/oauth2/<\.\*>"' "$ROOT_DIR/docker/ory/oathkeeper/rules.json"; then mode="unmapped_fail" fi - if ! grep -Eq '"url": "<\.\*>://<\.\*>/oidc/oauth2/<\.\*>"' "$ROOT_DIR/docker/ory/oathkeeper/rules.json"; then - mode="unmapped_fail" - fi - if ! grep -Eq '"strip_path_prefix": "/oidc"' "$ROOT_DIR/docker/ory/oathkeeper/rules.json"; then + if ! grep -Eq '"strip_path(_prefix)?": "/oidc"' "$ROOT_DIR/docker/ory/oathkeeper/rules.json"; then mode="unmapped_fail" fi fi @@ -358,10 +356,10 @@ verify_runtime_hydra_clients() { fi local admin_info dev_info - if ! admin_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint http://hydra:4445 adminfront 2>/dev/null)"; then + if ! admin_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint "$HYDRA_ADMIN_URL" adminfront 2>/dev/null)"; then fail "failed to read hydra client 'adminfront' from running container" fi - if ! dev_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint http://hydra:4445 devfront 2>/dev/null)"; then + if ! dev_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint "$HYDRA_ADMIN_URL" devfront 2>/dev/null)"; then fail "failed to read hydra client 'devfront' from running container" fi @@ -382,6 +380,7 @@ run_validation() { validate_dotenv_line_safety "BACKEND_URL" validate_dotenv_line_safety "OATHKEEPER_PUBLIC_URL" validate_dotenv_line_safety "HYDRA_PUBLIC_URL" + validate_dotenv_line_safety "HYDRA_ADMIN_URL" validate_dotenv_line_safety "KRATOS_BROWSER_URL" validate_dotenv_line_safety "KRATOS_UI_URL" validate_dotenv_line_safety "ADMINFRONT_URL" diff --git a/test/make_dev_targets_test.sh b/test/make_dev_targets_test.sh index 92a02b75..367574ef 100644 --- a/test/make_dev_targets_test.sh +++ b/test/make_dev_targets_test.sh @@ -3,6 +3,19 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +dry_run_default_dev="$( + make --dry-run --always-make -C "$repo_root" dev 2>&1 +)" + +default_app_up_line="$( + grep -E "docker compose .* -f docker-compose.yaml up .*backend.*adminfront.*devfront.*orgfront.*userfront" <<<"$dry_run_default_dev" | tail -1 +)" + +if [[ -z "$default_app_up_line" ]]; then + echo "make dev must include orgfront in the default development app services." >&2 + exit 1 +fi + dry_run_dev="$( make --dry-run --always-make -C "$repo_root" dev DEV_SERVICES="backend adminfront" 2>&1 )" @@ -45,10 +58,32 @@ if ! grep -q "Ensuring Ory stack" <<<"$dry_run_up_dev"; then exit 1 fi +dry_run_up_app="$( + make --dry-run --always-make -C "$repo_root" up-app 2>&1 +)" + +if ! grep -q "Starting App stack (backend/userfront/adminfront/devfront/orgfront)" <<<"$dry_run_up_app"; then + echo "make up-app must announce orgfront as part of the app stack." >&2 + exit 1 +fi + dry_run_up_all="$( make --dry-run --always-make -C "$repo_root" up-all 2>&1 )" +if ! dry_run_up="$( + make --dry-run --always-make -C "$repo_root" up 2>&1 +)"; then + echo "make up must be available as the default full-stack startup target." >&2 + echo "$dry_run_up" >&2 + exit 1 +fi + +if ! grep -q "Starting ALL stacks (infra + ory + app)" <<<"$dry_run_up"; then + echo "make up must delegate to the full-stack startup flow." >&2 + exit 1 +fi + if ! grep -q "Ensuring Docker networks" <<<"$dry_run_up_all"; then echo "make up-all must ensure external Docker networks before compose up." >&2 exit 1 diff --git a/test/ory_v26_compose_policy_test.sh b/test/ory_v26_compose_policy_test.sh index 5c1d3a29..df6be834 100644 --- a/test/ory_v26_compose_policy_test.sh +++ b/test/ory_v26_compose_policy_test.sh @@ -10,6 +10,26 @@ docker_config="$( docker compose --env-file "$repo_root/.env" -f "$repo_root/docker/compose.ory.yaml" config )" +override_env="$(mktemp)" +cp "$repo_root/.env" "$override_env" +cat >> "$override_env" <<'EOF' +USERFRONT_URL=https://compose-policy.example.test/sso +HYDRA_PUBLIC_URL=https://compose-policy.example.test/sso/oidc +KRATOS_UI_URL=https://compose-policy.example.test/ui +KRATOS_BROWSER_URL=https://compose-policy.example.test/auth +ADMINFRONT_CALLBACK_URLS=https://compose-policy.example.test/admin/callback +DEVFRONT_CALLBACK_URLS=https://compose-policy.example.test/dev/callback +ORGFRONT_CALLBACK_URLS=https://compose-policy.example.test/org/callback +EOF +trap 'rm -f "$override_env"' EXIT + +override_config="$( + docker compose --env-file "$override_env" -f "$repo_root/compose.ory.yaml" config +)" +override_docker_config="$( + docker compose --env-file "$override_env" -f "$repo_root/docker/compose.ory.yaml" config +)" + for service in kratos hydra keto oathkeeper; do version_key="$(tr '[:lower:]' '[:upper:]' <<<"$service")_VERSION" expected_version="$(grep -E "^${version_key}=" "$repo_root/.env" | cut -d= -f2-)" @@ -28,6 +48,40 @@ if grep -q "oryd/hydra:v25.4.0" <<<"$root_config"; then exit 1 fi +for compose_file in "$repo_root/compose.ory.yaml" "$repo_root/docker/compose.ory.yaml"; do + if grep -Eq 'redirect-uri .*:-.*https?://' "$compose_file"; then + echo "ERROR: $compose_file must not hard-code external redirect URI fallbacks; use .env variables." >&2 + exit 1 + fi + if grep -Eq 'KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=.*https?://localhost' "$compose_file"; then + echo "ERROR: $compose_file must not hard-code Kratos allowed return URL fallbacks; use .env variables." >&2 + exit 1 + fi + if awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^ init-rp:/ { in_block=1 } in_block { print }' "$compose_file" | grep -q -- '--endpoint http://hydra:4445'; then + echo "ERROR: $compose_file init-rp must use HYDRA_ADMIN_URL instead of hard-coded Hydra admin endpoint." >&2 + exit 1 + fi + if awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^[[:space:]]+oathkeeper:/ { in_block=1 } in_block { print }' "$compose_file" | grep -q "command: serve proxy --config /etc/config/oathkeeper/oathkeeper.yml"; then + echo "ERROR: $compose_file Oathkeeper must use entrypoint.sh instead of bypassing rules.active.json generation." >&2 + exit 1 + fi +done + +for expected_url in \ + "https://compose-policy.example.test/sso/oidc" \ + "https://compose-policy.example.test/sso/login" \ + "https://compose-policy.example.test/sso/consent" \ + "https://compose-policy.example.test/sso/error" \ + "https://compose-policy.example.test/admin/callback" \ + "https://compose-policy.example.test/dev/callback" \ + "https://compose-policy.example.test/org/callback" +do + if ! grep -q "$expected_url" <<<"$override_config$override_docker_config"; then + echo "ERROR: Ory compose config must render env override URL: $expected_url" >&2 + exit 1 + fi +done + root_init_rp="$( awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^ init-rp:/ { in_block=1 } in_block { print }' "$repo_root/compose.ory.yaml" )" @@ -53,3 +107,159 @@ if grep -q "releases/download/v25.4.0" "$repo_root/docker/staging_pull_compose.t echo "ERROR: staging pull compose must not download a hard-coded Hydra v25.4.0 CLI." >&2 exit 1 fi + +staging_pull_template="$repo_root/docker/staging_pull_compose.template.yaml" + +if ! grep -q 'entrypoint: \["/etc/config/oathkeeper/entrypoint.sh"\]' "$staging_pull_template"; then + echo "ERROR: staging pull Oathkeeper must use the env-aware entrypoint." >&2 + exit 1 +fi + +if grep -q "command: serve proxy --config /etc/config/oathkeeper/oathkeeper.yml" "$staging_pull_template"; then + echo "ERROR: staging pull Oathkeeper must not bypass entrypoint.sh with a direct command." >&2 + exit 1 +fi + +if ! grep -q "URLS_SELF_ISSUER=\${HYDRA_PUBLIC_URL}" "$staging_pull_template"; then + echo "ERROR: staging pull Hydra issuer must use HYDRA_PUBLIC_URL." >&2 + exit 1 +fi + +if grep -Eq '(KRATOS_(SERVE|SELFSERVICE|UI|BROWSER|PUBLIC|ADMIN).*:-http://localhost|URLS_.*:-http://localhost)' "$staging_pull_template"; then + echo "ERROR: staging pull Ory browser URLs must not fall back to localhost." >&2 + exit 1 +fi + +if ! grep -q 'KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON' "$staging_pull_template"; then + echo "ERROR: staging pull Kratos allowed_return_urls must be driven by KRATOS_ALLOWED_RETURN_URLS_JSON." >&2 + exit 1 +fi + +for return_path in '/ko' '/en' '/auth/callback' '/ko/auth/callback' '/en/auth/callback'; do + if ! grep -q "$return_path" "$staging_pull_template" "$repo_root/deploy/templates/.env.template" "$repo_root/.gitea/workflows/staging_code_pull.yml"; then + echo "ERROR: staging/prod allowed_return_urls must include locale/callback path: $return_path" >&2 + exit 1 + fi +done + +if grep -Eq 'ORGFRONT_CALLBACK_URLS=.*(172\.16\.10\.176|baron-orgchart\.hmac\.kr|, https?://)' "$staging_pull_template" "$repo_root/.gitea/workflows/staging_code_pull.yml"; then + echo "ERROR: staging pull OrgFront callbacks must not keep private IP, legacy orgchart domain, or comma-space URI entries." >&2 + exit 1 +fi + +if grep -q "rewrite \\^/oidc" "$repo_root/gateway/nginx.conf"; then + echo "ERROR: gateway must preserve the /oidc prefix and let Oathkeeper strip it." >&2 + exit 1 +fi + +for rules_file in \ + "$repo_root/docker/ory/oathkeeper/rules.json" \ + "$repo_root/docker/ory/oathkeeper/rules.stage.json" \ + "$repo_root/docker/ory/oathkeeper/rules.prod.json" +do + for rule_id in hydra-well-known hydra-well-known-oidc hydra-oauth2 hydra-oauth2-oidc hydra-userinfo hydra-userinfo-oidc; do + if ! grep -q "\"id\": \"$rule_id\"" "$rules_file"; then + echo "ERROR: Oathkeeper rules must expose Hydra public route in $rules_file: $rule_id" >&2 + exit 1 + fi + done + for prefixed_rule in hydra-well-known-oidc hydra-oauth2-oidc hydra-userinfo-oidc; do + if ! awk -v id="\"id\": \"$prefixed_rule\"" ' + $0 ~ id { in_rule = 1 } + in_rule && /strip_path/ && /\/oidc/ { found = 1 } + in_rule && /^ }[,]?$/ { in_rule = 0 } + END { exit found ? 0 : 1 } + ' "$rules_file"; then + echo "ERROR: prefixed Oathkeeper route must strip /oidc in $rules_file: $prefixed_rule" >&2 + exit 1 + fi + done +done + +for wildcard_rules_file in \ + "$repo_root/docker/ory/oathkeeper/rules.json" \ + "$repo_root/docker/ory/oathkeeper/rules.stage.json" +do + if grep -q "<\\.\\*>://<\\.\\*>/" "$wildcard_rules_file"; then + echo "ERROR: wildcard Oathkeeper host must not swallow path segments in $wildcard_rules_file." >&2 + exit 1 + fi +done + +deploy_template="$repo_root/deploy/templates/docker-compose.yaml" +deploy_env_template="$repo_root/deploy/templates/.env.template" +deploy_gateway_template="$repo_root/deploy/templates/gateway/nginx.conf" +deploy_kratos_template="$repo_root/deploy/templates/ory/kratos/kratos.yml" +deploy_oathkeeper_rules_template="$repo_root/deploy/templates/ory/oathkeeper/rules.json" + +for required_template in \ + "$repo_root/deploy/templates/orgfront/vite.config.ts" \ + "$repo_root/deploy/templates/orgfront/auth.ts" \ + "$repo_root/docker/ory/init-db/01_create_dbs.sh" \ + "$repo_root/docker/ory/hydra/hydra.yml" \ + "$repo_root/docker/ory/keto/keto.yml" \ + "$repo_root/docker/ory/oathkeeper/entrypoint.sh" \ + "$repo_root/docker/ory/oathkeeper/oathkeeper.yml" +do + if [[ ! -f "$required_template" ]]; then + echo "ERROR: deploy instance generation requires missing source file: $required_template" >&2 + exit 1 + fi +done + +if grep -Eq "oryd/(kratos|hydra|keto|oathkeeper):v25\\.4\\.0" "$deploy_template"; then + echo "ERROR: deploy template Ory stack must not hard-code v25.4.0 images." >&2 + exit 1 +fi + +for prod_sensitive_file in \ + "$repo_root/docker/ory/oathkeeper/rules.prod.json" \ + "$repo_root/docker/ory/kratos/kratos.yml" \ + "$repo_root/deploy/templates/ory/kratos/kratos.yml" +do + if grep -q "app\\.brsw\\.kr" "$prod_sensitive_file"; then + echo "ERROR: Ory production-sensitive config must not hard-code app.brsw.kr: $prod_sensitive_file" >&2 + exit 1 + fi +done + +for service in kratos-migrate kratos hydra-migrate hydra keto-migrate keto oathkeeper_logs_init oathkeeper; do + if ! grep -q "^ $service:" "$deploy_template"; then + echo "ERROR: deploy template Ory stack must include service: $service" >&2 + exit 1 + fi +done + +for version_key in KRATOS_VERSION HYDRA_VERSION KETO_VERSION OATHKEEPER_VERSION; do + if ! grep -q "^$version_key=v26\\.2\\.0$" "$deploy_env_template"; then + echo "ERROR: deploy env template must define $version_key=v26.2.0." >&2 + exit 1 + fi +done + +if ! grep -q 'entrypoint: \["/etc/config/oathkeeper/entrypoint.sh"\]' "$deploy_template"; then + echo "ERROR: deploy template Oathkeeper must use the env-aware entrypoint." >&2 + exit 1 +fi + +if grep -q "rewrite \\^/oidc" "$deploy_gateway_template"; then + echo "ERROR: deploy template gateway must preserve the /oidc prefix." >&2 + exit 1 +fi + +if ! grep -q '^version: v26.2.0$' "$deploy_kratos_template"; then + echo "ERROR: deploy Kratos template config version must match v26.2.0." >&2 + exit 1 +fi + +for rule_id in hydra-well-known hydra-well-known-oidc hydra-oauth2 hydra-oauth2-oidc hydra-userinfo hydra-userinfo-oidc; do + if ! grep -q "\"id\": \"$rule_id\"" "$deploy_oathkeeper_rules_template"; then + echo "ERROR: deploy Oathkeeper rules must expose Hydra public route: $rule_id" >&2 + exit 1 + fi +done + +if ! grep -q '"strip_path": "/oidc"' "$deploy_oathkeeper_rules_template"; then + echo "ERROR: deploy Oathkeeper prefixed routes must strip /oidc with strip_path." >&2 + exit 1 +fi