1
0
forked from baron/baron-sso

Baron SSO 다중 인스턴스 배포 템플릿

This commit is contained in:
2026-04-16 14:04:37 +09:00
parent 001f29ca5f
commit 35ce7853fa
11 changed files with 652 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
# === [1] 프로젝트 식별 (중요: 인스턴스마다 다르게 설정) ===
INSTANCE_NAME={{INSTANCE_NAME}}
COMPOSE_PROJECT_NAME=baron-sso-{{INSTANCE_NAME}}
APP_ENV=production
# === [2] 포트 Prefix 설정 (예: 23 입력 시 23000, 23432 등 생성) ===
P={{PORT_PREFIX}}
# 인프라 포트
DB_PORT=${P}432
REDIS_PORT=${P}399
CLICKHOUSE_PORT_HTTP=${P}123
CLICKHOUSE_PORT_NATIVE=${P}000
# 서비스 포트
BACKEND_PORT=${P}000
USERFRONT_PORT=${P}500
ADMINFRONT_PORT=${P}173
DEVFRONT_PORT=${P}174
OATHKEEPER_PROXY_PORT=${P}467
# === [3] 도메인 설정 (별도 도메인 구조) ===
# {{INSTANCE_NAME}}이 stg면 sso-stg.hmac.kr 형식이 되도록 가이드
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}
# OIDC/Auth URL
VITE_OIDC_AUTHORITY=${USERFRONT_URL}/oidc
ADMINFRONT_CALLBACK_URLS=${ADMINFRONT_URL}/auth/callback
DEVFRONT_CALLBACK_URLS=${DEVFRONT_URL}/auth/callback
# Ory URL
KRATOS_UI_URL=${USERFRONT_URL}/auth
KRATOS_BROWSER_URL=${USERFRONT_URL}/auth
HYDRA_PUBLIC_URL=${USERFRONT_URL}/oidc
OATHKEEPER_PUBLIC_URL=${USERFRONT_URL}
# === [4] IDP 및 DB Config ===
IDP_PROVIDER=ory
DB_PASSWORD=password
ORY_POSTGRES_USER=ory
ORY_POSTGRES_PASSWORD=generated_secret_here
CLICKHOUSE_PASSWORD=password
REDIS_ADDR=redis:6379
# Secrets (At least 32 chars)
COOKIE_SECRET=at_least_32_characters_long_secret_12345
JWT_SECRET=at_least_32_characters_long_secret_12345
CSRF_COOKIE_SECRET=at_least_32_characters_long_secret_12345
# Admin 초기 계정
ADMIN_EMAIL=admin@baron.co.kr
ADMIN_PASSWORD=adminPasswordIsNotSimple

View File

@@ -0,0 +1,29 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
envPrefix: ["VITE_", "USERFRONT_"],
server: {
host: "127.0.0.1",
// 인스턴스별 도메인을 자동으로 허용
allowedHosts: ["{{ADMINFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
preview: {
host: "127.0.0.1",
port: 5173,
allowedHosts: ["{{ADMINFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
});

View File

@@ -0,0 +1,21 @@
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
import type { AuthProviderProps } from "react-oidc-context";
export const oidcConfig: AuthProviderProps = {
authority:
import.meta.env.VITE_OIDC_AUTHORITY || `${window.location.protocol}//${window.location.hostname}:{{USERFRONT_PORT}}/oidc`,
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "{{CLIENT_ID}}",
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code",
scope: "openid offline_access profile email",
post_logout_redirect_uri: window.location.origin,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: false,
};
export const userManager = new UserManager({
...oidcConfig,
authority: oidcConfig.authority || "",
client_id: oidcConfig.client_id || "",
redirect_uri: oidcConfig.redirect_uri || "",
});

View File

@@ -0,0 +1,29 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
envPrefix: ["VITE_", "USERFRONT_"],
server: {
host: "127.0.0.1",
// 인스턴스별 도메인을 자동으로 허용
allowedHosts: ["{{DEVFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
preview: {
host: "127.0.0.1",
port: 5173,
allowedHosts: ["{{DEVFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
});

View File

@@ -0,0 +1,135 @@
name: ${COMPOSE_PROJECT_NAME}
services:
# --- Infrastructure ---
postgres:
image: postgres:17-alpine
container_name: ${COMPOSE_PROJECT_NAME}_db
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
ports:
- "${DB_PORT}:5432"
volumes:
- db_data_${INSTANCE_NAME}:/var/lib/postgresql/data
networks: [app_net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
redis:
image: redis:7-alpine
container_name: ${COMPOSE_PROJECT_NAME}_redis
ports:
- "${REDIS_PORT}:6379"
networks: [app_net]
clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: ${COMPOSE_PROJECT_NAME}_clickhouse
environment:
- CLICKHOUSE_USER=baron
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
ports:
- "${CLICKHOUSE_PORT_HTTP}:8123"
- "${CLICKHOUSE_PORT_NATIVE}:9000"
volumes:
- clickhouse_data_${INSTANCE_NAME}:/var/lib/clickhouse
networks: [app_net]
# --- Ory Stack ---
postgres_ory:
image: postgres:17-alpine
container_name: ${COMPOSE_PROJECT_NAME}_ory_db
environment:
- POSTGRES_USER=${ORY_POSTGRES_USER}
- POSTGRES_PASSWORD=${ORY_POSTGRES_PASSWORD}
volumes:
- ory_db_data_${INSTANCE_NAME}:/var/lib/postgresql/data
networks: [app_net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${ORY_POSTGRES_USER}"]
interval: 5s
kratos:
image: oryd/kratos:v25.4.0
container_name: ${COMPOSE_PROJECT_NAME}_kratos
env_file: .env
volumes:
- ./ory/kratos:/etc/config/kratos:ro
command: serve -c /etc/config/kratos/kratos.yml --dev
networks: [app_net]
depends_on:
postgres_ory: { condition: service_healthy }
oathkeeper:
image: oryd/oathkeeper:v25.4.0
container_name: ${COMPOSE_PROJECT_NAME}_oathkeeper
env_file: .env
ports:
- "${OATHKEEPER_PROXY_PORT}:4455"
volumes:
- ./ory/oathkeeper:/etc/config/oathkeeper:ro
networks: [app_net]
# --- Application Services ---
backend:
image: baron-backend:latest
container_name: ${COMPOSE_PROJECT_NAME}_backend
env_file: .env
environment:
- PORT=${BACKEND_PORT}
- DB_HOST=postgres
- REDIS_ADDR=redis:6379
- CLICKHOUSE_HOST=clickhouse
ports:
- "${BACKEND_PORT}:${BACKEND_PORT}"
networks: [app_net]
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_started }
gateway:
image: nginx:alpine
container_name: ${COMPOSE_PROJECT_NAME}_gateway
ports:
- "${USERFRONT_PORT}:80"
volumes:
- ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
networks: [app_net]
adminfront:
image: node:20-alpine
container_name: ${COMPOSE_PROJECT_NAME}_adminfront
working_dir: /app
env_file: .env
ports:
- "${ADMINFRONT_PORT}:5173"
volumes:
- ../../adminfront:/app
- ./adminfront/vite.config.ts:/app/vite.config.ts:ro
- ./adminfront/auth.ts:/app/src/lib/auth.ts:ro
command: npm run dev -- --host 0.0.0.0
networks: [app_net]
devfront:
image: node:20-alpine
container_name: ${COMPOSE_PROJECT_NAME}_devfront
working_dir: /app
env_file: .env
ports:
- "${DEVFRONT_PORT}:5173"
volumes:
- ../../devfront:/app
- ./devfront/vite.config.ts:/app/vite.config.ts:ro
- ./devfront/auth.ts:/app/src/lib/auth.ts:ro
command: npm run dev -- --host 0.0.0.0
networks: [app_net]
networks:
app_net:
name: ${COMPOSE_PROJECT_NAME}_net
volumes:
db_data_${INSTANCE_NAME}:
ory_db_data_${INSTANCE_NAME}:
clickhouse_data_${INSTANCE_NAME}:

View File

@@ -0,0 +1,43 @@
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
# 인스턴스별로 계산된 포트 주입
upstream backend_srv {
server backend:{{BACKEND_PORT}};
}
upstream oathkeeper_srv {
server oathkeeper:4455;
}
server {
listen 80;
# SSO 메인 도메인 및 API 처리
location /api {
proxy_pass http://backend_srv;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /auth {
proxy_pass http://oathkeeper_srv;
proxy_set_header Host $host;
}
location /oidc {
rewrite ^/oidc/(.*)$ /$1 break;
proxy_pass http://oathkeeper_srv;
proxy_set_header Host $host;
}
# 기본 정적 파일 (UserFront)
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
}

View File

@@ -0,0 +1,114 @@
version: v25.4.0
dsn: ${DSN}
serve:
public:
base_url: http://localhost:4433/
cors:
enabled: true
allowed_origins:
- http://backend:{{BACKEND_PORT}}
admin:
base_url: http://localhost:4434/
session:
cookie:
domain: hmac.kr
same_site: Lax
path: /
selfservice:
default_browser_return_url: http://localhost:{{USERFRONT_PORT}}/
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
methods:
password:
enabled: true
link:
enabled: true
code:
enabled: true
passwordless_enabled: true
flows:
error:
ui_url: http://localhost:{{USERFRONT_PORT}}/error
settings:
ui_url: http://localhost:{{USERFRONT_PORT}}/error?error=settings_disabled
privileged_session_max_age: 15m
recovery:
ui_url: http://localhost:{{USERFRONT_PORT}}/recovery
use: code
verification:
ui_url: http://localhost:{{USERFRONT_PORT}}/verification
use: code
logout:
after:
default_browser_return_url: http://localhost:{{USERFRONT_PORT}}/login
login:
ui_url: http://localhost:{{USERFRONT_PORT}}/login
lifespan: 10m
registration:
ui_url: http://localhost:{{USERFRONT_PORT}}/registration
lifespan: 10m
log:
level: debug
format: text
leak_sensitive_values: true
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
cipher:
- 32-LONG-SECRET-NOT-SECURE-AT-ALL
ciphers:
algorithm: xchacha20-poly1305
hashers:
algorithm: bcrypt
bcrypt:
cost: 8
identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
courier:
template_override_path: /etc/config/kratos/courier-templates
delivery_strategy: http
http:
request_config:
url: http://backend:{{BACKEND_PORT}}/api/v1/auth/webhooks/kratos-courier
method: POST
body: file:///etc/config/kratos/courier-http.jsonnet
headers:
Content-Type: application/json
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true

View File

@@ -0,0 +1,15 @@
[
{
"id": "backend-api-rule",
"match": {
"url": "<.*>://<.*>/api/v1/<.*>",
"methods": ["GET", "POST", "PUT", "DELETE", "PATCH"]
},
"upstream": {
"url": "http://backend:{{BACKEND_PORT}}"
},
"authenticators": [{ "handler": "cookie_session" }],
"authorizer": { "handler": "remote_json" },
"mutators": [{ "handler": "noop" }]
}
]

View File

@@ -0,0 +1,71 @@
# ISO8601 시간을 "YYYY-MM-DD HH:mm:ss" 형식으로 변환
map $time_iso8601 $time_custom {
"~^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})" "$1-$2-$3 $4:$5:$6";
}
# Go slog 포맷과 맞춘 JSON 액세스 로그
log_format json_combined escape=json
'{'
'"time":"$time_custom",'
'"level":"INFO",'
'"msg":"http_access",'
'"svc":"baron-userfront",'
'"status":$status,'
'"method":"$request_method",'
'"path":"$request_uri",'
'"latency":"${request_time}s",'
'"ip":"$remote_addr",'
'"forwarded_for":"$http_x_forwarded_for",'
'"user_agent":"$http_user_agent"'
'}';
server {
listen 5000;
include /etc/nginx/mime.types;
types {
application/javascript mjs;
application/wasm wasm;
}
error_log /dev/stderr warn;
access_log /var/log/nginx/access.log json_combined;
# --- Backend API Proxy ---
location /api {
proxy_pass http://backend:{{BACKEND_PORT}};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# --- UserFront Static Files ---
location ~* \.(js|css|html|json|mjs|wasm)$ {
root /usr/share/nginx/html;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri =404;
}
location ~* \.mjs$ {
root /usr/share/nginx/html;
default_type application/javascript;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri =404;
}
# dart2wasm 바이너리 MIME 명시
location ~* \.wasm$ {
root /usr/share/nginx/html;
default_type application/wasm;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri =404;
}
location / {
root /usr/share/nginx/html;
index index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri $uri/ /index.html;
}
}