첫 커밋: 로컬 프로젝트 업로드

This commit is contained in:
2026-06-10 15:51:34 +09:00
commit 6a8dbeb2e9
1211 changed files with 312864 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# Docker 이미지 빌드 및 배포 가이드
이 문서는 Baron SSO 애플리케이션의 Backend와 Frontend Docker 이미지를 빌드하고, Private Registry에 푸시한 뒤, 서버에 배포하는 과정을 안내합니다.
---
### 1. Docker 이미지 빌드 및 태그
Backend와 Frontend 애플리케이션을 각각의 Dockerfile을 사용하여 빌드하고, 레지스트리에 푸시할 수 있도록 이미지에 태그를 지정합니다.
**주의:** 아래 모든 명령어는 **프로젝트 최상위 루트 디렉토리**에서 실행해야 합니다.
```bash
# Backend 이미지 빌드
# v1.2601.1-RC1 부분은 실제 배포 버전에 맞게 수정하세요.
docker build -t reg.hmac.kr/baron_sso/backend:v1.2601.1-RC1 -f backend/Dockerfile .
# Frontend 이미지 빌드
docker build -t reg.hmac.kr/baron_sso/userfront:v1.2601.1-RC1 -f userfront/Dockerfile .
```
---
### 2. Private 레지스트리 로그인
빌드한 이미지를 푸시하기 위해 Private Docker Registry(`reg.hmac.kr`)에 로그인합니다.
최초 한 번만 인증하면 이후에는 로그인 과정이 필요 없을 수 있습니다.
```bash
docker login reg.hmac.kr
```
---
### 3. Docker 이미지 푸시
로컬에 빌드된 두 이미지를 Private Registry로 업로드합니다.
이 과정을 통해 배포 서버에서 해당 이미지를 내려받을 수 있게 됩니다.
```bash
# Backend 이미지 푸시
docker push reg.hmac.kr/baron_sso/backend:v1.2601.1-RC1
# Frontend 이미지 푸시
docker push reg.hmac.kr/baron_sso/userfront:v1.2601.1-RC1
```
---
### 4. 서버 배포 및 서비스 실행
배포 서버에서 `docker-compose.deploy.yaml` 파일을 사용하여 이미지를 내려받고 컨테이너를 실행합니다.
로컬 테스트 시에는 `compose.infra.yaml`을 함께 사용하여 전체 인프라를 구동할 수 있습니다.
```bash
# 로컬 환경에서 전체 서비스(인프라 포함) 실행
# -d 옵션은 컨테이너를 백그라운드에서 실행합니다.
docker compose -f docker/docker-compose.deploy.yaml -f docker/compose.infra.yaml up -d
```
### 서비스 중지
실행 중인 모든 서비스를 중지하고 컨테이너를 삭제하려면 아래 명령어를 사용합니다.
```bash
docker compose -f docker/docker-compose.deploy.yaml -f docker/compose.infra.yaml down
```

View File

@@ -0,0 +1,24 @@
FROM debian:trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
coreutils \
curl \
docker-cli \
findutils \
git \
grep \
jq \
openssl \
perl \
postgresql-client \
sed \
tar \
util-linux \
zstd \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
CMD ["/bin/bash"]

View File

@@ -0,0 +1,46 @@
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
networks:
- baron_net
restart: always
clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: baron_clickhouse
environment:
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-baron}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-password}
networks:
- baron_net
redis:
image: redis:7-alpine
container_name: baron_redis
restart: always
command: redis-server --port 6399
ports:
- "6399:6399"
volumes:
- redis_data:/data
networks:
- baron_net
volumes:
postgres_data:
clickhouse_data:
redis_data:
networks:
baron_net:
name: baron_network
driver: bridge

View File

@@ -0,0 +1,80 @@
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
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
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
gateway:
build:
context: ./gateway
dockerfile: Dockerfile
container_name: baron_gateway
restart: always
ports:
- "${USERFRONT_PORT:-5000}:5000"
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
volumes:
postgres_data:
clickhouse_data:
redis_data:
networks:
baron_net:
external: true
name: baron_net
public_net:
external: true
name: public_net

View File

@@ -0,0 +1,251 @@
services:
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:-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}","${USERFRONT_URL}"]}
volumes:
- ../config/.generated/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:-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}","${USERFRONT_URL}"]}
volumes:
- ../config/.generated/ory/kratos:/etc/config/kratos
command: serve -c /etc/config/kratos/kratos.yml
depends_on:
kratos-migrate:
condition: service_completed_successfully
networks:
- ory-net
- kratosnet
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:-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:
- ../config/.generated/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:-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:
- ../config/.generated/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:-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:
- ../config/.generated/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:-v26.2.0}
container_name: oathkeeper
restart: unless-stopped
depends_on:
kratos:
condition: service_started
environment:
- APP_ENV=${APP_ENV:-development}
- LOG_LEVEL=debug
- OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}
- OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}
volumes:
- ../config/.generated/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...';
check_ready() {
name=\"$$1\";
url=\"$$2\";
max=\"$${ORY_STACK_CHECK_MAX_ATTEMPTS:-60}\";
i=1;
while [ \"$$i\" -le \"$$max\" ]; do
if curl --connect-timeout 2 --max-time 3 -fsS \"$$url\" >/dev/null; then
echo \"Ory service ready: $$name\";
return 0;
fi;
echo \"Waiting for Ory service: $$name ($$i/$$max)\";
i=$$((i + 1));
sleep 1;
done;
echo \"ERROR: Ory service not ready: $$name after $$max attempts ($$url)\" >&2;
echo \"ERROR: Check service logs: docker logs ory_$$name\" >&2;
return 1;
};
check_ready kratos http://kratos:4433/health/ready || exit 1;
check_ready hydra http://hydra:4444/health/ready || exit 1;
check_ready keto http://keto:4466/health/ready || exit 1;
echo 'Ory Stack is fully operational!';"
depends_on:
- kratos
- hydra
- keto
networks:
- ory-net
init-rp:
image: oryd/hydra:${HYDRA_CLI_VERSION:-v26.2.0}
container_name: init-rp
env_file:
- ../.env
entrypoint: ["/bin/sh", "-ec"]
command:
- |
echo "Creating/Updating OAuth2 Clients..."
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 "$${DEVFRONT_CALLBACK_URLS}"
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
networks:
- ory-net
- hydranet
volumes:
ory_postgres_data:
oathkeeper_logs:
networks:
ory-net:
external: true
name: ory-net
hydranet:
external: true
name: hydranet
kratosnet:
external: true
name: kratosnet
public_net:
external: true
name: public_net
baron_net:
external: true
name: baron_net

View File

@@ -0,0 +1,147 @@
name: baron-sso-staging
services:
backend:
image: ${BACKEND_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_backend
restart: unless-stopped
env_file:
- .env
environment:
- DB_HOST=baron_postgres
- IDP_PROVIDER=ory
- OATHKEEPER_API_URL=http://oathkeeper:4456
- PROFILE_CACHE_TTL="${PROFILE_CACHE_TTL:-30m}"
- ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=${ORGFRONT_ORGCHART_CACHE_TTL_SECONDS:-3600}
- SEED_TENANT_CSV_PATH=/app/seed-tenant.csv
ports:
- "${BACKEND_PORT:-3000}:3000"
volumes:
- ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro
depends_on:
oathkeeper:
condition: service_healthy
infra_check:
condition: service_started
networks:
- baron_net
- ory-net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 10s
timeout: 5s
retries: 10
start_period: 60s
adminfront:
image: ${ADMINFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_adminfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=stage
- API_PROXY_TARGET=http://baron_backend:3000
ports:
- "${ADMINFRONT_PORT:-5173}:5173"
networks:
- baron_net
devfront:
image: ${DEVFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_devfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=stage
- API_PROXY_TARGET=http://baron_backend:3000
ports:
- "${DEVFRONT_PORT:-5174}:5173"
networks:
- baron_net
orgfront:
image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_orgfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=stage
- API_PROXY_TARGET=http://baron_backend:3000
ports:
- "${ORGFRONT_PORT:-5175}:5175"
networks:
- baron_net
userfront:
image: ${USERFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_userfront
restart: unless-stopped
env_file:
- .env
environment:
- BACKEND_URL=${BACKEND_URL:-}
- USERFRONT_URL=${USERFRONT_URL}
- APP_ENV=stage
networks:
- baron_net
- ory-net
depends_on:
backend:
condition: service_healthy
command: >
/bin/sh -c "echo \"[userfront-runtime] BACKEND_URL configured: $${BACKEND_URL:+yes}\" &&
echo \"[userfront-runtime] USERFRONT_URL configured: $${USERFRONT_URL:+yes}\" &&
echo \"[userfront-runtime] APP_ENV=$${APP_ENV:-stage}\" &&
nginx -g 'daemon off;'"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
infra_check:
image: alpine
command: ["echo", "Infrastructure assumed running"]
networks:
- baron_net
promtail:
image: grafana/promtail:2.9.0
container_name: baron_promtail
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./docker/promtail-config.template.yaml:/etc/promtail/promtail-config.yaml:ro
command: -config.file=/etc/promtail/promtail-config.yaml -config.expand-env=true
environment:
- LOKI_URL=${LOKI_URL:-http://loki:3100/loki/api/v1/push}
networks:
- baron_net
blackbox-exporter:
image: prom/blackbox-exporter:v0.25.0
container_name: baron_blackbox_exporter
restart: unless-stopped
ports:
- "9115:9115"
volumes:
- ./docker/monitor/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
networks:
- baron_net
- ory-net
networks:
baron_net:
external: true
name: baron_net
ory-net:
external: true
name: ory-net
public_net:
external: true
name: public_net

View File

@@ -0,0 +1,121 @@
name: baron-sso
services:
backend:
image: ${BACKEND_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_backend
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=${APP_ENV:-production}
- APP_ENV=production
- COOKIE_SECRET=${COOKIE_SECRET}
- DB_HOST=postgres
- CLICKHOUSE_HOST=clickhouse
- CLICKHOUSE_PORT=${CLICKHOUSE_PORT_NATIVE:-9000}
- CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron}
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password}
- USERFRONT_URL=${USERFRONT_URL:-https://sso.hmac.kr}
- BACKEND_PORT=3000
- SEED_TENANT_CSV_PATH=/app/seed-tenant.csv
ports:
- "${PROD_BACKEND_PORT:-3010}:3000"
volumes:
- ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro
depends_on:
- infra_check
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
networks:
- baron_net
userfront:
image: ${USERFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_userfront
restart: unless-stopped
env_file:
- .env
environment:
- USERFRONT_URL=${USERFRONT_URL:-https://sso.hmac.kr}
- BACKEND_URL=${BACKEND_URL:-https://sso.hmac.kr}
ports:
- "${USERFRONT_PORT:-80}:5000"
depends_on:
backend:
condition: service_healthy
networks:
- baron_net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
adminfront:
image: ${ADMINFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_adminfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=production
- API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000}
ports:
- "${ADMINFRONT_PORT:-5173}:5173"
depends_on:
backend:
condition: service_healthy
networks:
- baron_net
devfront:
image: ${DEVFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_devfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=production
- API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000}
ports:
- "${DEVFRONT_PORT:-5174}:5173"
depends_on:
backend:
condition: service_healthy
networks:
- baron_net
orgfront:
image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_orgfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=production
- API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000}
- USERFRONT_URL=${USERFRONT_URL}
ports:
- "${ORGFRONT_PORT:-5175}:5175"
depends_on:
backend:
condition: service_healthy
networks:
- baron_net
infra_check:
image: alpine
command: ["echo", "Infrastructure assumed running"]
networks:
- baron_net
networks:
baron_net:
external: true
name: baron_network

View File

@@ -0,0 +1,42 @@
-- Metadata DB Initialization for Baron SSO
-- Purpose: Manage Relying Parties (RP) and User Consent
-- 1. Relying Parties (RP) Table
CREATE TABLE IF NOT EXISTS relying_parties (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_id VARCHAR(255) NOT NULL UNIQUE,
client_secret VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
redirect_uris TEXT[] NOT NULL,
description TEXT,
logo_url VARCHAR(2048),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 2. User Consents Table
-- Tracks which scopes/permissions a user has granted to an RP
CREATE TABLE IF NOT EXISTS user_consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255) NOT NULL, -- Subject ID from IDP
rp_id UUID NOT NULL REFERENCES relying_parties(id),
scopes TEXT[] NOT NULL,
granted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
revoked_at TIMESTAMP WITH TIME ZONE,
UNIQUE(user_id, rp_id)
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_rp_client_id ON relying_parties(client_id);
CREATE INDEX IF NOT EXISTS idx_consent_user ON user_consents(user_id);
-- 3. Seed Data (Optional)
-- Initial RP for testing purposes
INSERT INTO relying_parties (client_id, client_secret, name, redirect_uris, description)
VALUES (
'baron-admin-client',
'secret-key-12345',
'Baron Admin Console',
ARRAY['http://localhost:5000/callback', 'https://sso.hmac.kr/callback'],
'Official Admin Console for Baron SSO'
) ON CONFLICT (client_id) DO NOTHING;

View File

@@ -0,0 +1,10 @@
modules:
http_2xx:
prober: http
timeout: 5s
http:
valid_status_codes: [] # Defaults to 2xx
method: GET
follow_redirects: true
fail_if_ssl: false
fail_if_not_ssl: false

View File

@@ -0,0 +1,161 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 3,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"title": "Baron SSO Service Overview",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [
{
"options": {
"0": {
"color": "red",
"index": 1,
"text": "OFFLINE"
},
"1": {
"color": "green",
"index": 0,
"text": "ONLINE"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 3
},
"id": 2,
"options": {
"alignValue": "center",
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"mergeValues": true,
"rowHeight": 0.8,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"editorMode": "code",
"expr": "probe_success{job=\"baron-services-http-probe\"}",
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
}
],
"title": "Services Health Timeline (HTTP Probe)",
"type": "state-timeline"
},
{
"datasource": {
"type": "loki",
"uid": "Loki"
},
"gridPos": {
"h": 12,
"w": 24,
"x": 0,
"y": 11
},
"id": 3,
"options": {
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "Loki"
},
"expr": "{job=\"baron-sso-logs\"}",
"refId": "A"
}
],
"title": "Live Container Logs (Loki)",
"type": "logs"
}
],
"refresh": "5s",
"schemaVersion": 39,
"tags": ["baron-sso", "observability"],
"style": "dark",
"timezone": "browser",
"title": "Baron SSO Observability Dashboard",
"uid": "baron_sso_observability",
"version": 1,
"weekStart": ""
}

View File

@@ -0,0 +1,32 @@
CREATE DATABASE IF NOT EXISTS ory;
CREATE TABLE IF NOT EXISTS ory.oathkeeper_access_logs (
timestamp DateTime64(3) DEFAULT now64(3),
request_id String DEFAULT '',
method String DEFAULT '',
path String DEFAULT '',
status UInt16 DEFAULT 0,
latency_ms UInt32 DEFAULT 0,
client_id String DEFAULT '',
rp String DEFAULT '',
action String DEFAULT '',
target String DEFAULT '',
rule_id String DEFAULT '',
host String DEFAULT '',
scheme String DEFAULT '',
query String DEFAULT '',
upstream_url String DEFAULT '',
subject String DEFAULT '',
parent_session_id String DEFAULT '',
client_ip String DEFAULT '',
user_agent String DEFAULT '',
referer String DEFAULT '',
decision String DEFAULT '',
bytes_in UInt64 DEFAULT 0,
bytes_out UInt64 DEFAULT 0,
trace_id String DEFAULT '',
span_id String DEFAULT '',
raw String DEFAULT ''
) ENGINE = MergeTree()
ORDER BY (timestamp, request_id)
TTL timestamp + INTERVAL 30 DAY;

View File

@@ -0,0 +1,98 @@
dsn: ${HYDRA_DSN}
serve:
cookies:
same_site_mode: Lax
admin:
cors:
enabled: true
allowed_origins:
- "*"
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
- CONNECT
- HEAD
- OPTIONS
- TRACE
allowed_headers:
- Authorization
- Accept
- Content-Type
- Content-Length
- Accept-Language
- Content-Language
exposed_headers:
- Content-Type
- Cache-Control
- Expires
- Last-Modified
- Pragma
- Content-Length
- Content-Language
public:
cors:
enabled: true
allowed_origins:
- "*"
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
- CONNECT
- HEAD
- OPTIONS
- TRACE
allowed_headers:
- Authorization
- Accept
- Content-Type
- Content-Length
- Accept-Language
- Content-Language
exposed_headers:
- Content-Type
- Cache-Control
- Expires
- Last-Modified
- Pragma
- Content-Length
- Content-Language
allow_credentials: true
urls:
self:
issuer: http://127.0.0.1:4444
consent: http://127.0.0.1:3000/consent
login: http://127.0.0.1:3000/login
logout: http://127.0.0.1:3000/logout
device:
verification: http://127.0.0.1:3000/device/verify
success: http://127.0.0.1:3000/device/success
secrets:
system:
- ${HYDRA_SYSTEM_SECRET}
webfinger:
oidc_discovery:
client_registration_url: http://127.0.0.1:4444/oauth2/register
oidc:
subject_identifiers:
supported_types:
- pairwise
- public
pairwise:
salt: youReallyNeedToChangeThis
dynamic_client_registration:
enabled: true
ttl:
access_token: 15m
id_token: 15m

View File

@@ -0,0 +1,24 @@
#!/bin/bash
set -e
# 환경 변수에서 DB 이름 가져오기 (기본값 설정)
KRATOS_DB=${KRATOS_DB:-ory_kratos}
HYDRA_DB=${HYDRA_DB:-ory_hydra}
KETO_DB=${KETO_DB:-ory_keto}
# 함수 정의: DB가 없으면 생성
create_db_if_not_exists() {
local dbname=$1
if ! psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -lqt | cut -d \| -f 1 | grep -qw "$dbname"; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE DATABASE $dbname;
EOSQL
echo "Database '$dbname' created."
else
echo "Database '$dbname' already exists."
fi
}
create_db_if_not_exists "$KRATOS_DB"
create_db_if_not_exists "$HYDRA_DB"
create_db_if_not_exists "$KETO_DB"

View File

@@ -0,0 +1,15 @@
version: v0.11.0
dsn: ${KETO_DSN}
serve:
read:
host: 0.0.0.0
port: 4466
write:
host: 0.0.0.0
port: 4467
namespaces:
location: file:///etc/config/keto/namespaces.ts
log:
level: debug

View File

@@ -0,0 +1,150 @@
import { Namespace, Context, SubjectSet } from "@ory/keto-definitions"
class User implements Namespace {}
class System implements Namespace {
related: {
super_admins: User[]
authenticated_users: User[]
}
permits = {
manage_all: (ctx: Context): boolean =>
this.related.super_admins.includes(ctx.subject)
}
}
class Tenant implements Namespace {
related: {
owners: (User | SubjectSet<System, "super_admins">)[]
admins: (User | SubjectSet<System, "super_admins">)[]
members: (User | SubjectSet<System, "super_admins"> | SubjectSet<Tenant, "admins"> | SubjectSet<Tenant, "owners">)[]
parents: Tenant[]
developer_console_viewer: (User | SubjectSet<System, "super_admins">)[]
developer_console_grant_manager: (User | SubjectSet<System, "super_admins">)[]
}
permits = {
view: (ctx: Context): boolean =>
this.related.members.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject) ||
this.related.owners.includes(ctx.subject) ||
this.related.parents.traverse((p) => p.permits.view(ctx)),
manage: (ctx: Context): boolean =>
this.related.admins.includes(ctx.subject) ||
this.related.owners.includes(ctx.subject) ||
this.related.parents.traverse((p) => p.permits.manage(ctx)),
manage_admins: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) ||
this.related.parents.traverse((p) => p.permits.manage_admins(ctx)),
create_subtenant: (ctx: Context): boolean =>
this.permits.manage(ctx),
view_dev_console: (ctx: Context): boolean =>
this.related.developer_console_viewer.includes(ctx.subject) ||
this.permits.grant_dev_permissions(ctx) ||
this.permits.manage(ctx) ||
this.related.parents.traverse((p) => p.permits.view_dev_console(ctx)),
grant_dev_permissions: (ctx: Context): boolean =>
this.related.developer_console_grant_manager.includes(ctx.subject) ||
this.permits.manage_admins(ctx) ||
this.related.parents.traverse((p) => p.permits.grant_dev_permissions(ctx))
}
}
class RelyingParty implements Namespace {
related: {
admins: (User | SubjectSet<System, "super_admins"> | SubjectSet<Tenant, "admins"> | SubjectSet<Tenant, "owners">)[]
parents: Tenant[]
access: (User | SubjectSet<Tenant, "members"> | SubjectSet<System, "authenticated_users"> | SubjectSet<System, "super_admins">)[]
creator: (User | SubjectSet<System, "super_admins">)[]
config_editor: (User | SubjectSet<System, "super_admins">)[]
secret_viewer: (User | SubjectSet<System, "super_admins">)[]
secret_rotator: (User | SubjectSet<System, "super_admins">)[]
jwks_viewer: (User | SubjectSet<System, "super_admins">)[]
jwks_operator: (User | SubjectSet<System, "super_admins">)[]
consent_viewer: (User | SubjectSet<System, "super_admins">)[]
consent_revoker: (User | SubjectSet<System, "super_admins">)[]
relationship_viewer: (User | SubjectSet<System, "super_admins">)[]
audit_viewer: (User | SubjectSet<System, "super_admins">)[]
status_operator: (User | SubjectSet<System, "super_admins">)[]
}
permits = {
view: (ctx: Context): boolean =>
this.related.admins.includes(ctx.subject) ||
this.related.config_editor.includes(ctx.subject) ||
this.related.secret_viewer.includes(ctx.subject) ||
this.related.secret_rotator.includes(ctx.subject) ||
this.related.jwks_viewer.includes(ctx.subject) ||
this.related.jwks_operator.includes(ctx.subject) ||
this.related.consent_viewer.includes(ctx.subject) ||
this.related.consent_revoker.includes(ctx.subject) ||
this.related.relationship_viewer.includes(ctx.subject) ||
this.related.audit_viewer.includes(ctx.subject) ||
this.related.status_operator.includes(ctx.subject) ||
this.related.parents.traverse((t) => t.permits.view(ctx)) ||
this.related.parents.traverse((t) => t.permits.view_dev_console(ctx)),
manage: (ctx: Context): boolean =>
this.related.admins.includes(ctx.subject) ||
this.related.parents.traverse((t) => t.permits.manage(ctx)),
create: (ctx: Context): boolean =>
this.related.creator.includes(ctx.subject) ||
this.related.parents.traverse((t) => t.permits.grant_dev_permissions(ctx)) ||
this.permits.manage(ctx),
edit_config: (ctx: Context): boolean =>
this.related.config_editor.includes(ctx.subject) ||
this.permits.manage(ctx),
view_secret: (ctx: Context): boolean =>
this.related.secret_viewer.includes(ctx.subject) ||
this.permits.rotate_secret(ctx) ||
this.permits.manage(ctx),
rotate_secret: (ctx: Context): boolean =>
this.related.secret_rotator.includes(ctx.subject) ||
this.permits.manage(ctx),
view_jwks: (ctx: Context): boolean =>
this.related.jwks_viewer.includes(ctx.subject) ||
this.permits.operate_jwks(ctx) ||
this.permits.manage(ctx),
operate_jwks: (ctx: Context): boolean =>
this.related.jwks_operator.includes(ctx.subject) ||
this.permits.manage(ctx),
view_consents: (ctx: Context): boolean =>
this.related.consent_viewer.includes(ctx.subject) ||
this.permits.revoke_consents(ctx) ||
this.permits.manage(ctx),
revoke_consents: (ctx: Context): boolean =>
this.related.consent_revoker.includes(ctx.subject) ||
this.permits.manage(ctx),
view_relationships: (ctx: Context): boolean =>
this.related.relationship_viewer.includes(ctx.subject) ||
this.related.parents.traverse((t) => t.permits.grant_dev_permissions(ctx)) ||
this.permits.manage(ctx),
view_audit_logs: (ctx: Context): boolean =>
this.related.audit_viewer.includes(ctx.subject) ||
this.permits.manage(ctx),
change_status: (ctx: Context): boolean =>
this.related.status_operator.includes(ctx.subject) ||
this.permits.manage(ctx),
access: (ctx: Context): boolean =>
this.related.access.includes(ctx.subject) ||
this.permits.manage(ctx)
}
}

View File

@@ -0,0 +1,6 @@
- id: 0
name: default
- id: 1
name: roles
- id: 2
name: permissions

View File

@@ -0,0 +1,8 @@
// Kratos courier HTTP payload을 backend로 전달하는 템플릿입니다.
function(ctx)
local data = if std.objectHas(ctx, "template_data") && ctx.template_data != null then ctx.template_data else {};
{
recipient: ctx.recipient,
template_type: ctx.template_type,
template_data: data,
}

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="ko">
<body style="font-family: sans-serif; line-height: 1.6;">
<h2>Baron SSO 로그인</h2>
<p>아래 버튼을 클릭하면 로그인이 완료됩니다.</p>
<!-- 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }} -->
<p>
<a href="http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }}"
style="display: inline-block; padding: 10px 16px; background: #1a1f2c; color: #fff; text-decoration: none; border-radius: 6px;">
로그인 완료하기
</a>
</p>
<p>또는 아래 로그인 코드를 입력해도 됩니다.</p>
<p style="font-size: 18px; font-weight: bold;">{{ .LoginCode }}</p>
<p style="color: #666; font-size: 12px;">요청하지 않았다면 이 메일을 무시해 주세요.</p>
</body>
</html>

View File

@@ -0,0 +1,10 @@
Baron SSO 로그인
# 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }}
아래 링크를 클릭하면 로그인이 완료됩니다.
http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }}
로그인 코드: {{ .LoginCode }}
요청하지 않았다면 이 메일을 무시해 주세요.

View File

@@ -0,0 +1 @@
Baron SSO 로그인 링크

View File

@@ -0,0 +1,4 @@
[Baron 로그인] 로그인 링크
# 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }}
http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }}
코드: {{ .LoginCode }}

View File

@@ -0,0 +1,126 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"custom_login_ids": {
"type": "array",
"title": "Custom Login IDs",
"items": {
"type": "string",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
},
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"code": {
"identifier": true,
"via": "email"
}
},
"recovery": {
"via": "email"
},
"verification": {
"via": "email"
}
}
},
"name": {
"type": "string",
"title": "Name"
},
"phone_number": {
"type": "string",
"title": "Phone Number",
"minLength": 7,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"code": {
"identifier": true,
"via": "sms"
}
}
}
},
"department": {
"type": "string",
"title": "Department"
},
"affiliationType": {
"type": "string",
"title": "Affiliation Type"
},
"companyCode": {
"type": "string",
"title": "Company Code"
},
"role": {
"type": "string",
"title": "Role"
},
"tenant_id": {
"type": "string",
"title": "Tenant ID"
},
"displayname": {
"type": "string",
"title": "Display Name"
},
"completeForm": {
"type": "boolean",
"title": "Complete Form"
},
"team": {
"type": "string",
"title": "Team"
},
"taxCode": {
"type": "string",
"title": "Tax Code"
},
"familyCompany": {
"type": "string",
"title": "Family Company"
},
"familyUniqueKey": {
"type": "string",
"title": "Family Unique Key"
},
"personal": {
"type": "boolean",
"title": "Personal"
},
"grade": {
"type": "string",
"title": "Grade"
}
},
"required": [
"email"
],
"additionalProperties": true
}
}
}

View File

@@ -0,0 +1,98 @@
version: v26.2.0
dsn: ${KRATOS_DSN}
serve:
public:
base_url: ${KRATOS_BROWSER_URL:-http://localhost:4433/}
cors:
enabled: true
allowed_origins:
- http://localhost:5000
- http://localhost:5173
- http://localhost:5174
- http://localhost:5175
- http://backend:3000
- http://baron_backend:3000
admin:
base_url: ${KRATOS_ADMIN_URL:-http://localhost:4434/}
session:
cookie:
domain: ${KRATOS_SESSION_COOKIE_DOMAIN}
same_site: Lax
path: /
selfservice:
default_browser_return_url: ${KRATOS_UI_URL:-http://localhost:5000/}
allowed_return_urls:
${KRATOS_ALLOWED_RETURN_URLS_YAML}
methods:
password:
enabled: true
link:
enabled: true
code:
enabled: true
passwordless_enabled: true
flows:
error:
ui_url: ${KRATOS_UI_URL:-http://localhost:5000}/error
settings:
ui_url: ${KRATOS_UI_URL:-http://localhost:5000}/error?error=settings_disabled
privileged_session_max_age: 15m
recovery:
ui_url: ${KRATOS_UI_URL:-http://localhost:5000}/recovery
use: code
verification:
ui_url: ${KRATOS_UI_URL:-http://localhost:5000}/verification
use: code
logout:
after:
default_browser_return_url: ${KRATOS_UI_URL:-http://localhost:5000}/login
login:
ui_url: ${KRATOS_UI_URL:-http://localhost:5000}/login
lifespan: 10m
registration:
ui_url: ${KRATOS_UI_URL:-http://localhost:5000}/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://baron_backend:3000/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,45 @@
#!/usr/bin/env sh
set -eu
APP_ENV_VALUE="${APP_ENV:-}"
case "$APP_ENV_VALUE" in
production|prod)
RULES_FILE="/etc/config/oathkeeper/rules.prod.json"
;;
stage|staging)
RULES_FILE="/etc/config/oathkeeper/rules.stage.json"
;;
*)
RULES_FILE="/etc/config/oathkeeper/rules.json"
;;
esac
export RULES_FILE
echo "[oathkeeper] APP_ENV=$APP_ENV_VALUE rules=$RULES_FILE"
RUNTIME_DIR="/tmp/oathkeeper"
RULES_ACTIVE="${RUNTIME_DIR}/rules.active.json"
if [ ! -f "$RULES_FILE" ]; then
echo "[oathkeeper] rules file not found: $RULES_FILE"
exit 1
fi
mkdir -p "$RUNTIME_DIR"
cp -f "$RULES_FILE" "$RULES_ACTIVE"
LOG_DIR="/var/log/oathkeeper"
LOG_FILE="${LOG_DIR}/access.log"
mkdir -p "$LOG_DIR"
if ! touch "$LOG_FILE" 2>/dev/null; then
echo "[oathkeeper] log file not writable: $LOG_FILE"
ls -ld "$LOG_DIR" || true
LOG_FILE=""
fi
if [ -n "$LOG_FILE" ]; then
exec /bin/sh -c "oathkeeper serve proxy -c /etc/config/oathkeeper/oathkeeper.yml 2>&1 | tee -a \"$LOG_FILE\""
fi
exec /bin/sh -c "oathkeeper serve proxy -c /etc/config/oathkeeper/oathkeeper.yml"

View File

@@ -0,0 +1,69 @@
serve:
proxy:
port: 4455
api:
port: 4456
log:
level: info
format: json
errors:
fallback:
- json
access_rules:
repositories:
- file:///tmp/oathkeeper/rules.active.json
authenticators:
noop:
enabled: true
cookie_session:
enabled: true
config:
check_session_url: http://kratos:4433/sessions/whoami
preserve_path: true
extra_from: "@this"
subject_from: "identity.id"
oauth2_introspection:
enabled: true
config:
introspection_url: http://hydra:4444/oauth2/introspect
pre_authorization:
enabled: true
client_id: ${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}
client_secret: ${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}
token_url: http://hydra:4444/oauth2/token
jwt:
enabled: true
config:
jwks_urls:
- http://hydra:4444/.well-known/jwks.json
trusted_issuers:
- http://hydra:4444/
scope_strategy: none
authorizers:
allow:
enabled: true
remote_json:
enabled: true
config:
remote: http://keto:4466/check
payload: |
{
"namespace": "permissions",
"object": "{{ print .Request.URL.Path }}",
"relation": "access",
"subject_id": "{{ print .Subject }}"
}
mutators:
noop:
enabled: true
id_token:
enabled: true
config:
issuer_url: http://127.0.0.1:4456/
jwks_url: file:///etc/config/oathkeeper/jwks.json

View File

@@ -0,0 +1,159 @@
[
{
"id": "public-health",
"description": "공개 헬스체크",
"match": {
"url": "<.*>://<[^/]+>/health",
"methods": ["GET"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-preflight",
"description": "CORS preflight",
"match": {
"url": "<.*>://<[^/]+>/api/v1/<.*>",
"methods": ["OPTIONS"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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" }]
}
]

View File

@@ -0,0 +1,91 @@
[
{
"id": "public-health",
"description": "공개 헬스체크 (TODO: 도메인 제한)",
"match": {
"url": "http://<.*>/health",
"methods": ["GET"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-auth",
"description": "인증/회원가입 등 공개 엔드포인트",
"match": {
"url": "http://<.*>/api/v1/auth/<.*>",
"methods": ["GET", "POST", "OPTIONS"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "backend-command",
"description": "Command 요청은 Backend로 전달 (Audit 강제)",
"match": {
"url": "http://<.*>/api/v1/<.*>",
"methods": ["POST", "PUT", "PATCH", "DELETE"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "cookie_session" }],
"authorizer": { "handler": "remote_json" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "backend-query",
"description": "Backend Query (admin/dev 포함)",
"match": {
"url": "http://<.*>/api/v1/<.*>",
"methods": ["GET"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "cookie_session" }],
"authorizer": { "handler": "remote_json" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "hydra-public",
"description": "Hydra Public API를 /hydra로 노출",
"match": {
"url": "http://<.*>/hydra/<.*>",
"methods": ["GET", "POST", "OPTIONS"]
},
"upstream": {
"url": "http://hydra:4444",
"strip_path": "/hydra"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "rp-host-template",
"description": "RP 호스트 기반 템플릿. redirect_uri의 host를 기준으로 매칭합니다.",
"match": {
"url": "<.*>://rp.example.com/<.*>",
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
},
"upstream": {
"url": "http://rp_upstream:8080"
},
"authenticators": [
{ "handler": "cookie_session" },
{ "handler": "oauth2_introspection" },
{ "handler": "jwt" }
],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
}
]

View File

@@ -0,0 +1,159 @@
[
{
"id": "public-health",
"description": "공개 헬스체크",
"match": {
"url": "<.*>://<[^/]+>/health",
"methods": ["GET"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-preflight",
"description": "CORS preflight",
"match": {
"url": "<.*>://<[^/]+>/api/v1/<.*>",
"methods": ["OPTIONS"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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" }]
}
]

View File

@@ -0,0 +1,159 @@
[
{
"id": "public-health",
"description": "공개 헬스체크 (PROD)",
"match": {
"url": "<.*>://<[^/]+>/health",
"methods": ["GET"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-preflight",
"description": "CORS preflight (PROD)",
"match": {
"url": "<.*>://<[^/]+>/api/v1/<.*>",
"methods": ["OPTIONS"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-auth",
"description": "인증/회원가입 등 공개 엔드포인트 (PROD)",
"match": {
"url": "<.*>://<[^/]+>/api/v1/auth/<.*>",
"methods": ["GET", "POST", "OPTIONS"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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" }]
}
]

View File

@@ -0,0 +1,159 @@
[
{
"id": "public-health",
"description": "공개 헬스체크",
"match": {
"url": "<.*>://<[^/]+>/health",
"methods": ["GET"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-preflight",
"description": "CORS preflight",
"match": {
"url": "<.*>://<[^/]+>/api/v1/<.*>",
"methods": ["OPTIONS"]
},
"upstream": {
"url": "http://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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://baron_backend:3000"
},
"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" }]
}
]

View File

@@ -0,0 +1,183 @@
[sources.oathkeeper_file]
type = "file"
include = ["/var/log/oathkeeper/access.log"]
read_from = "beginning"
[transforms.oathkeeper_parse]
type = "remap"
inputs = ["oathkeeper_file"]
source = '''
raw = to_string(.message) ?? ""
parsed = object!(parse_json(raw) ?? {})
request_method = to_string(get(parsed, ["request", "method"]) ?? "") ?? ""
if request_method == "" { request_method = to_string(get(parsed, ["http_request", "method"]) ?? "") ?? "" }
request_path = to_string(get(parsed, ["request", "path"]) ?? "") ?? ""
if request_path == "" { request_path = to_string(get(parsed, ["http_request", "path"]) ?? "") ?? "" }
request_url = to_string(get(parsed, ["request", "url"]) ?? "") ?? ""
if request_url == "" { request_url = to_string(get(parsed, ["http_url"]) ?? "") ?? "" }
request_host = to_string(get(parsed, ["request", "host"]) ?? "") ?? ""
if request_host == "" { request_host = to_string(get(parsed, ["http_request", "host"]) ?? "") ?? "" }
request_scheme = to_string(get(parsed, ["request", "scheme"]) ?? "") ?? ""
if request_scheme == "" { request_scheme = to_string(get(parsed, ["http_request", "scheme"]) ?? "") ?? "" }
request_query = to_string(get(parsed, ["request", "query"]) ?? "") ?? ""
if request_query == "" { request_query = to_string(get(parsed, ["http_request", "query"]) ?? "") ?? "" }
response_status = to_int(get(parsed, ["response", "status"]) ?? 0) ?? 0
if response_status == 0 { response_status = to_int(get(parsed, ["http_response", "status"]) ?? 0) ?? 0 }
response_size = to_int(get(parsed, ["response", "size"]) ?? 0) ?? 0
if response_size == 0 { response_size = to_int(get(parsed, ["http_response", "size"]) ?? 0) ?? 0 }
response_took = to_int(get(parsed, ["response", "took"]) ?? 0) ?? 0
if response_took == 0 { response_took = to_int(get(parsed, ["http_response", "took"]) ?? 0) ?? 0 }
identity_id = to_string(get(parsed, ["identity", "id"]) ?? "") ?? ""
if identity_id == "" { identity_id = to_string(get(parsed, ["subject"]) ?? "") ?? "" }
headers = object(get(parsed, ["headers"]) ?? {}) ?? {}
if length(headers) == 0 { headers = object(get(parsed, ["http_request", "headers"]) ?? {}) ?? {} }
user_agent = to_string(get(headers, ["User-Agent"]) ?? "") ?? ""
if user_agent == "" { user_agent = to_string(get(headers, ["user-agent"]) ?? "") ?? "" }
referer = to_string(get(headers, ["Referer"]) ?? "") ?? ""
if referer == "" { referer = to_string(get(headers, ["referer"]) ?? "") ?? "" }
rule_id = to_string(get(parsed, ["rule", "id"]) ?? "") ?? ""
if rule_id == "" { rule_id = to_string(get(parsed, ["rule_id"]) ?? "") ?? "" }
upstream_url = to_string(get(parsed, ["upstream", "url"]) ?? "") ?? ""
if upstream_url == "" { upstream_url = to_string(get(parsed, ["http_url"]) ?? "") ?? "" }
client_id = to_string(get(parsed, ["client", "id"]) ?? "") ?? ""
parent_session_id = to_string(get(parsed, ["extra", "parent_session_id"]) ?? "") ?? ""
parsed_url = parse_url(request_url) ?? {}
query_params = get(parsed_url, ["query"]) ?? {}
url_path = to_string(get(parsed_url, ["path"]) ?? "") ?? ""
parsed_request_query = parse_url("http://localhost/?" + request_query) ?? {}
request_query_params = get(parsed_request_query, ["query"]) ?? {}
event_path = to_string(parsed.path) ?? to_string(parsed.http_path) ?? ""
if event_path == "" { event_path = request_path }
if event_path == "" { event_path = url_path }
if event_path == "" { event_path = request_url }
event_client_id = to_string(parsed.client_id) ?? ""
if event_client_id == "" { event_client_id = client_id }
if event_client_id == "" { event_client_id = to_string(get(query_params, ["client_id"]) ?? "") ?? "" }
if event_client_id == "" { event_client_id = to_string(get(query_params, ["clientId"]) ?? "") ?? "" }
if event_client_id == "" { event_client_id = to_string(get(request_query_params, ["client_id"]) ?? "") ?? "" }
if event_client_id == "" { event_client_id = to_string(get(request_query_params, ["clientId"]) ?? "") ?? "" }
event_latency_ms = to_int(parsed.latency_ms) ?? to_int(parsed.duration_ms) ?? 0
if event_latency_ms == 0 && response_took != 0 {
event_latency_ms = to_int(to_float(response_took) / 1000000.0)
}
event_client_ip = to_string(parsed.client_ip) ?? to_string(parsed.remote_ip) ?? to_string(parsed.ip) ?? ""
if event_client_ip == "" { event_client_ip = to_string(get(headers, ["X-Real-Ip"]) ?? "") ?? "" }
if event_client_ip == "" { event_client_ip = to_string(get(headers, ["x-real-ip"]) ?? "") ?? "" }
if event_client_ip == "" { event_client_ip = to_string(get(headers, ["X-Forwarded-For"]) ?? "") ?? "" }
if event_client_ip == "" { event_client_ip = to_string(get(headers, ["x-forwarded-for"]) ?? "") ?? "" }
event_decision = to_string(parsed.decision) ?? to_string(parsed.result) ?? ""
if event_decision == "" && exists(parsed.granted) {
if parsed.granted == true {
event_decision = "granted"
} else {
event_decision = "denied"
}
}
event_status = to_int(get(parsed, ["status"]) ?? 0) ?? 0
if event_status == 0 { event_status = to_int(get(parsed, ["status_code"]) ?? 0) ?? 0 }
if event_status == 0 { event_status = response_status }
event_bytes_out = to_int(get(parsed, ["bytes_out"]) ?? 0) ?? 0
if event_bytes_out == 0 { event_bytes_out = to_int(get(parsed, ["response_bytes"]) ?? 0) ?? 0 }
if event_bytes_out == 0 { event_bytes_out = response_size }
event_method = to_string(get(parsed, ["method"]) ?? "") ?? ""
if event_method == "" { event_method = to_string(get(parsed, ["http_method"]) ?? "") ?? "" }
if event_method == "" { event_method = request_method }
event_host = to_string(get(parsed, ["host"]) ?? "") ?? ""
if event_host == "" { event_host = to_string(get(parsed, ["http_host"]) ?? "") ?? "" }
if event_host == "" { event_host = request_host }
event_scheme = to_string(get(parsed, ["scheme"]) ?? "") ?? ""
if event_scheme == "" { event_scheme = request_scheme }
event_query = to_string(get(parsed, ["query"]) ?? "") ?? ""
if event_query == "" { event_query = request_query }
event_user_agent = to_string(get(parsed, ["user_agent"]) ?? "") ?? ""
if event_user_agent == "" { event_user_agent = to_string(get(parsed, ["http_user_agent"]) ?? "") ?? "" }
if event_user_agent == "" { event_user_agent = user_agent }
. = {
"request_id": to_string(parsed.request_id) ?? to_string(parsed.req_id) ?? "",
"method": event_method,
"path": event_path,
"status": event_status,
"latency_ms": event_latency_ms,
"client_id": event_client_id,
"rp": to_string(parsed.rp) ?? "",
"action": to_string(parsed.action) ?? "",
"target": to_string(parsed.target) ?? "",
"rule_id": to_string(parsed.rule_id) ?? rule_id,
"host": event_host,
"scheme": event_scheme,
"query": event_query,
"upstream_url": to_string(parsed.upstream_url) ?? upstream_url,
"subject": to_string(parsed.subject) ?? identity_id,
"parent_session_id": to_string(parsed.parent_session_id) ?? parent_session_id,
"client_ip": event_client_ip,
"user_agent": event_user_agent,
"referer": referer,
"decision": event_decision,
"bytes_in": to_int(parsed.bytes_in) ?? to_int(parsed.request_bytes) ?? 0,
"bytes_out": event_bytes_out,
"trace_id": to_string(parsed.trace_id) ?? "",
"span_id": to_string(parsed.span_id) ?? "",
"raw": raw
}
'''
[sinks.clickhouse]
type = "clickhouse"
inputs = ["oathkeeper_parse"]
endpoint = "http://ory_clickhouse:8123"
database = "ory"
table = "oathkeeper_access_logs"
compression = "gzip"
auth.strategy = "basic"
auth.user = "${ORY_CLICKHOUSE_USER}"
auth.password = "${ORY_CLICKHOUSE_PASSWORD}"
[[tests]]
name = "parses_oathkeeper_v26_completed_request"
[[tests.inputs]]
insert_at = "oathkeeper_parse"
type = "log"
[tests.inputs.log_fields]
message = '{"http_request":{"headers":{"user-agent":"Mozilla/5.0","referer":"http://localhost:5173/","x-real-ip":"172.19.0.1"},"host":"localhost","method":"GET","path":"/oauth2/auth","query":"client_id=orgfront&response_type=code","remote":"172.23.0.2:56744","scheme":"http"},"http_response":{"status":302,"size":1339,"took":4854092},"http_url":"http://hydra:4444/oauth2/auth?client_id=orgfront&redirect_uri=http%3A%2F%2Flocalhost%3A5175%2Fauth%2Fcallback","level":"info","msg":"completed handling request","subject":"","time":"2026-05-06T01:40:51.46074548Z"}'
[[tests.outputs]]
extract_from = "oathkeeper_parse"
[[tests.outputs.conditions]]
type = "vrl"
source = '''
assert_eq!(.method, "GET")
assert_eq!(.path, "/oauth2/auth")
assert_eq!(.status, 302)
assert_eq!(.client_id, "orgfront")
assert_eq!(.host, "localhost")
assert_eq!(.scheme, "http")
assert_eq!(.user_agent, "Mozilla/5.0")
assert_eq!(.referer, "http://localhost:5173/")
'''
[[tests]]
name = "parses_oathkeeper_v26_granted_request"
[[tests.inputs]]
insert_at = "oathkeeper_parse"
type = "log"
[tests.inputs.log_fields]
message = '{"audience":"application","granted":true,"http_host":"hydra:4444","http_method":"GET","http_url":"http://hydra:4444/oauth2/auth?client_id=orgfront&redirect_uri=http%3A%2F%2Flocalhost%3A5175%2Fauth%2Fcallback&response_type=code","http_user_agent":"curl/8.10.1","level":"info","msg":"Access request granted","service_name":"ORY Oathkeeper","service_version":"v26.2.0","subject":"","time":"2026-05-06T01:52:25.431Z"}'
[[tests.outputs]]
extract_from = "oathkeeper_parse"
[[tests.outputs.conditions]]
type = "vrl"
source = '''
assert_eq!(.method, "GET")
assert_eq!(.path, "/oauth2/auth")
assert_eq!(.status, 0)
assert_eq!(.client_id, "orgfront")
assert_eq!(.decision, "granted")
'''

View File

@@ -0,0 +1,41 @@
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: ${LOKI_URL:-http://loki:3100/loki/api/v1/push}
scrape_configs:
- job_name: baron-sso-container-logs
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 10s
relabel_configs:
# 1. 원본 메타데이터에서 Baron 및 Ory 관련 컨테이너만 필터링
- source_labels: ['__meta_docker_container_name']
regex: '/(baron_.*|oathkeeper|kratos|hydra|keto|ory_.*)'
action: keep
# 2. 필수 라벨 선부여 (강제성 확보를 위해 __address__ 참조)
- source_labels: ['__address__']
target_label: 'job'
replacement: 'baron-sso-logs'
- source_labels: ['__address__']
target_label: 'app_env'
replacement: '${APP_ENV:-development}'
# 3. 컨테이너 이름 추출
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container_name'
# 4. 서비스 상세 라벨 부여 (baron_ 접두사 제거 등)
- source_labels: ['container_name']
regex: 'baron_(.*)'
target_label: 'service'
- source_labels: ['container_name']
regex: 'baron_(.*)'
target_label: 'job'

View File

@@ -0,0 +1,590 @@
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
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
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
restart: unless-stopped
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:-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:
- ./config/.generated/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:-v26.2.0}
container_name: ory_kratos
restart: unless-stopped
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:
- ./config/.generated/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:-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:-v26.2.0}
container_name: ory_hydra
restart: unless-stopped
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:
- ./config/.generated/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:-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:
- ./config/.generated/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:-v26.2.0}
container_name: ory_keto
restart: unless-stopped
environment:
- DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20
volumes:
- ./config/.generated/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_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
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:-development}
- LOG_LEVEL=debug
- OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}
- OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}
volumes:
- ./config/.generated/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_clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: ory_clickhouse
restart: unless-stopped
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
ory_stack_check:
image: alpine:latest
container_name: ory_stack_check
command: >
/bin/sh -c "
apk add --no-cache curl;
echo 'Wait for services...';
check_ready() {
name=\"$$1\";
url=\"$$2\";
max=\"$${ORY_STACK_CHECK_MAX_ATTEMPTS:-60}\";
i=1;
while [ \"$$i\" -le \"$$max\" ]; do
if curl --connect-timeout 2 --max-time 3 -fsS \"$$url\" >/dev/null; then
echo \"Ory service ready: $$name\";
return 0;
fi;
echo \"Waiting for Ory service: $$name ($$i/$$max)\";
i=$$((i + 1));
sleep 1;
done;
echo \"ERROR: Ory service not ready: $$name after $$max attempts ($$url)\" >&2;
echo \"ERROR: Check service logs: docker logs ory_$$name\" >&2;
return 1;
};
check_ready kratos http://kratos:4433/health/ready || exit 1;
check_ready hydra http://hydra:4444/health/ready || exit 1;
check_ready keto http://keto:4466/health/ready || exit 1;
echo 'Ory Stack is fully operational!';"
depends_on:
- kratos
- hydra
- keto
networks:
- ory-net
init-rp:
image: oryd/hydra:${HYDRA_CLI_VERSION:-v26.2.0}
env_file:
- .env
entrypoint: ["/bin/sh", "-ec"]
command:
- |
# 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 "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:
- hydranet
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: baron_backend
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=${APP_ENV:-development}
- GO_ENV=${APP_ENV:-development}
- WORKS_ADMIN_API_BASE_URL=${WORKS_ADMIN_API_BASE_URL}
- WORKS_ADMIN_OAUTH_TOKEN_URL=${WORKS_ADMIN_OAUTH_TOKEN_URL}
- 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}
- ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=${ORGFRONT_ORGCHART_CACHE_TTL_SECONDS:-3600}
- 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
adminfront:
build:
context: .
dockerfile: ./adminfront/Dockerfile
args:
VITE_ADMIN_PUBLIC_URL: ${ADMINFRONT_URL:-}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY:-}
VITE_OIDC_CLIENT_ID: adminfront
ORGFRONT_URL: ${ORGFRONT_URL:-}
container_name: baron_adminfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=${APP_ENV:-stage}
- API_PROXY_TARGET=http://baron_backend:3000
ports:
- "${ADMINFRONT_PORT:-5173}:5173"
networks:
- baron_net
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:5173/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 12
start_period: 90s
devfront:
build:
context: .
dockerfile: ./devfront/Dockerfile
args:
VITE_DEVFRONT_PUBLIC_URL: ${DEVFRONT_URL:-}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY:-}
VITE_OIDC_CLIENT_ID: devfront
container_name: baron_devfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=${APP_ENV:-stage}
- API_PROXY_TARGET=http://baron_backend:3000
ports:
- "${DEVFRONT_PORT:-5174}:5173"
networks:
- baron_net
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:5173/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 12
start_period: 90s
orgfront:
build:
context: .
dockerfile: ./orgfront/Dockerfile
args:
VITE_ORGFRONT_PUBLIC_URL: ${ORGFRONT_URL:-}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY:-}
VITE_OIDC_CLIENT_ID: orgfront
container_name: baron_orgfront
restart: unless-stopped
env_file:
- .env
environment:
- APP_ENV=${APP_ENV:-stage}
- API_PROXY_TARGET=http://baron_backend:3000
- USERFRONT_URL=${USERFRONT_URL}
ports:
- "${ORGFRONT_PORT:-5175}:5175"
networks:
- baron_net
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:5175/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 12
start_period: 90s
userfront:
build:
context: .
dockerfile: userfront/Dockerfile
target: production
container_name: baron_userfront
restart: unless-stopped
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 "echo \"[userfront-runtime] BACKEND_URL configured: $${BACKEND_URL:+yes}\" &&
echo \"[userfront-runtime] USERFRONT_URL configured: $${USERFRONT_URL:+yes}\" &&
echo \"[userfront-runtime] APP_ENV=$${APP_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
promtail:
image: grafana/promtail:2.9.0
container_name: baron_promtail
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./docker/promtail-config.template.yaml:/etc/promtail/promtail-config.yaml:ro
command: -config.file=/etc/promtail/promtail-config.yaml -config.expand-env=true
environment:
- LOKI_URL=${LOKI_URL:-http://loki:3100/loki/api/v1/push}
networks:
- baron_net
blackbox-exporter:
image: prom/blackbox-exporter:v0.25.0
container_name: baron_blackbox_exporter
restart: unless-stopped
ports:
- "9115:9115"
volumes:
- ./docker/monitor/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
networks:
- baron_net
- ory-net
volumes:
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