1
0
forked from baron/baron-sso

oathkeeper 동작 확인

This commit is contained in:
Lectom C Han
2026-01-28 20:07:52 +09:00
parent 39594f8e21
commit ff17259117
9 changed files with 368 additions and 68 deletions

View File

@@ -96,6 +96,15 @@ HYDRA_ADMIN_URL=http://hydra:4445
HYDRA_PUBLIC_URL=http://hydra:4444
JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json
# Oathkeeper 실행 사용자/프로브 설정
OATHKEEPER_VERSION=v25.4.0
OATHKEEPER_UID=1001
OATHKEEPER_GID=1001
OATHKEEPER_HEALTH_URL=http://oathkeeper:4456/health/ready
OATHKEEPER_HEALTH_INTERVAL_SECONDS=10
OATHKEEPER_HEALTH_TIMEOUT_SECONDS=2
OATHKEEPER_HEALTH_ENABLED=true
# Kratos Selfservice UI required secrets (local only)
COOKIE_SECRET=localcookie123
CSRF_COOKIE_NAME=__HOST-baronSSO_csrf

View File

@@ -207,36 +207,36 @@ docker compose -f compose.ory.yaml --profile mcp up -d hydra-mcp-server kratos-m
```
- MCP 서버는 stdio 기반이라 외부 포트를 열지 않습니다.
- MCP 클라이언트에서 `npx`로 실행하는 설정 예시입니다.
- `hydra-mcp`는 첫 실행 시 캐시 디렉터리에 의존성을 자동 설치합니다(수동 `npm install` 불필요).
- 최초 실행시거나 빌드된 이미지가 없으면 `docker compose -f mcp/compose.mcp.ory.yaml build' 후에 사용 가능합니다
```toml
[mcp_servers.kratos-mcp]
command = "npx"
args = ["-y", "mcp-ory-kratos"]
command = "docker"
args = ["compose", "-f", "mcp/compose.mcp.ory.yaml", "run", "--rm", "--no-deps", "kratos-mcp-server"]
[mcp_servers.kratos-mcp.env]
KRATOS_ADMIN_URL = "http://localhost:4434"
KRATOS_ADMIN_URL = "http://kratos:4434"
[mcp_servers.hydra-mcp]
command = "npx"
args = ["-y", "/home/lectom/repos/baron-sso/mcp/hydra-mcp"]
command = "docker"
args = ["compose", "-f", "mcp/compose.mcp.ory.yaml", "run", "--rm", "--no-deps", "hydra-mcp-server"]
[mcp_servers.hydra-mcp.env]
HYDRA_PUBLIC_URL = "http://localhost:4441"
HYDRA_ADMIN_URL = "http://localhost:4445"
HYDRA_PUBLIC_URL = "http://hydra:4444"
HYDRA_ADMIN_URL = "http://hydra:4445"
[mcp_servers.keto-mcp]
command = "npx"
args = ["-y", "/home/lectom/repos/baron-sso/mcp/keto-mcp"]
command = "docker"
args = ["compose", "-f", "mcp/compose.mcp.ory.yaml", "run", "--rm", "--no-deps", "keto-mcp-server"]
[mcp_servers.keto-mcp.env]
KETO_READ_URL = "http://localhost:4466"
KETO_WRITE_URL = "http://localhost:4467"
KETO_READ_URL = "http://keto:4466"
KETO_WRITE_URL = "http://keto:4467"
```
### 로컬 개발 (Manual)
Docker 없이 코드를 수정하며 개발하려면:
Docker 없이는 개발할 수 없지만 Backend 및 [user/admin/dev]Front 코드는 개발모드로 수정하며 개발가능.
백그라운드로 infra 및 ory stack이 구동중이라는 가정
**Backend:**
```bash
@@ -292,6 +292,6 @@ baron_sso/
## 📝 상태 및 로드맵 (Status & Roadmap)
- [x] **Phase 1**: 초기 설정 및 아키텍처 설계 (완료)
- [x] **Phase 2**: Backend Audit API 구현 (일부 완료)
- [x] **Phase 3**: userfront 로그인 UI 인증 로직 (완료)
- [ ] **Phase 3**: userfront 로그인 UI 인증 로직 (예정)
- [ ] **Phase 4**: adminfront 기능 추가 (예정)
- [ ] **Phase 5**: 대시보드 및 통합 런처 구현 (예정)

View File

@@ -0,0 +1,132 @@
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"sync"
"time"
)
type HTTPProbe struct {
name string
url string
interval time.Duration
timeout time.Duration
client *http.Client
mu sync.RWMutex
status string
lastError string
lastChecked time.Time
lastSuccess time.Time
}
type ProbeSnapshot struct {
Status string
Error string
LastChecked time.Time
LastSuccess time.Time
}
func NewHTTPProbe(name, url string, interval, timeout time.Duration) *HTTPProbe {
if interval <= 0 {
interval = 10 * time.Second
}
if timeout <= 0 {
timeout = 2 * time.Second
}
return &HTTPProbe{
name: name,
url: url,
interval: interval,
timeout: timeout,
client: &http.Client{
Timeout: timeout,
},
}
}
// Start는 프로브를 백그라운드에서 주기적으로 실행합니다.
func (p *HTTPProbe) Start() {
go func() {
p.checkOnce()
ticker := time.NewTicker(p.interval)
defer ticker.Stop()
for range ticker.C {
p.checkOnce()
}
}()
}
func (p *HTTPProbe) Snapshot() ProbeSnapshot {
p.mu.RLock()
defer p.mu.RUnlock()
return ProbeSnapshot{
Status: p.status,
Error: p.lastError,
LastChecked: p.lastChecked,
LastSuccess: p.lastSuccess,
}
}
func (p *HTTPProbe) StatusText() string {
s := p.Snapshot()
if s.Status == "ok" {
return "ok"
}
if s.Status == "" {
return "unknown"
}
if s.Error == "" {
return "error"
}
return "error: " + s.Error
}
func (p *HTTPProbe) checkOnce() {
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.url, nil)
if err != nil {
p.update("error", fmt.Sprintf("request build failed: %v", err), false)
return
}
resp, err := p.client.Do(req)
if err != nil {
p.update("error", err.Error(), false)
return
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
p.update("error", fmt.Sprintf("status=%d", resp.StatusCode), false)
return
}
p.update("ok", "", true)
}
func (p *HTTPProbe) update(status, errMsg string, success bool) {
p.mu.Lock()
prevStatus := p.status
p.status = status
p.lastError = errMsg
p.lastChecked = time.Now()
if success {
p.lastSuccess = p.lastChecked
}
p.mu.Unlock()
if prevStatus == status {
return
}
if status == "ok" {
slog.Info("Service probe recovered", "name", p.name, "url", p.url)
return
}
slog.Error("Service probe failed", "name", p.name, "url", p.url, "error", errMsg)
}

View File

@@ -158,6 +158,28 @@ func main() {
slog.Warn("Failed to connect to Redis. Auth features may fail.", "error", err)
}
// Oathkeeper 상태를 주기적으로 확인해 다운을 감지합니다.
var oathkeeperProbe *HTTPProbe
if strings.ToLower(getEnv("OATHKEEPER_HEALTH_ENABLED", "true")) != "false" {
intervalSec, err := strconv.Atoi(getEnv("OATHKEEPER_HEALTH_INTERVAL_SECONDS", "10"))
if err != nil || intervalSec <= 0 {
intervalSec = 10
}
timeoutSec, err := strconv.Atoi(getEnv("OATHKEEPER_HEALTH_TIMEOUT_SECONDS", "2"))
if err != nil || timeoutSec <= 0 {
timeoutSec = 2
}
oathkeeperProbe = NewHTTPProbe(
"oathkeeper",
getEnv("OATHKEEPER_HEALTH_URL", "http://oathkeeper:4456/health/ready"),
time.Duration(intervalSec)*time.Second,
time.Duration(timeoutSec)*time.Second,
)
oathkeeperProbe.Start()
} else {
slog.Info("Oathkeeper probe disabled")
}
// 2. Initialize Handlers
auditHandler := handler.NewAuditHandler(auditRepo)
authHandler := handler.NewAuthHandler(redisService, idpProvider)
@@ -321,6 +343,32 @@ func main() {
status = "degraded"
}
// Check Oathkeeper
if oathkeeperProbe != nil {
snapshot := oathkeeperProbe.Snapshot()
switch snapshot.Status {
case "ok":
checks["oathkeeper"] = "ok"
case "":
checks["oathkeeper"] = "unknown"
if status != "error" {
status = "degraded"
}
default:
if snapshot.Error == "" {
checks["oathkeeper"] = "error"
} else {
checks["oathkeeper"] = "error: " + snapshot.Error
}
status = "error"
}
} else {
checks["oathkeeper"] = "disabled"
if status != "error" {
status = "degraded"
}
}
if status == "error" {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
"status": status,

View File

@@ -71,22 +71,6 @@ services:
- ory-net
- kratosnet
kratos-mcp-server:
build:
context: ./mcp/kratos-mcp
container_name: mcp_ory_kratos
profiles:
- mcp
stdin_open: true
tty: true
init: true
environment:
- KRATOS_ADMIN_URL=http://kratos:4434
depends_on:
- kratos
networks:
- ory-net
kratos-ui:
image: oryd/kratos-selfservice-ui-node:${KRATOS_UI_NODE_VERSION:-v25.4.0}
container_name: ory_kratos_ui
@@ -133,39 +117,7 @@ services:
- ory-net
- hydranet
hydra-mcp-server:
build:
context: ./mcp/hydra-mcp
container_name: mcp_ory_hydra
profiles:
- mcp
stdin_open: true
tty: true
init: true
environment:
- HYDRA_PUBLIC_URL=http://hydra:4444
- HYDRA_ADMIN_URL=http://hydra:4445
depends_on:
- hydra
networks:
- ory-net
keto-mcp-server:
build:
context: ./mcp/keto-mcp
container_name: mcp_ory_keto
profiles:
- mcp
stdin_open: true
tty: true
init: true
environment:
- KETO_READ_URL=http://keto:4466
- KETO_WRITE_URL=http://keto:4467
depends_on:
- keto
networks:
- ory-net
# --- Keto ---
keto-migrate:
@@ -197,17 +149,18 @@ services:
# --- Oathkeeper ---
oathkeeper:
image: oryd/oathkeeper:v0.40.6
image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v25.4.0}
container_name: ory_oathkeeper
user: "${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001}"
ports:
- "4457:4455" # Proxy
environment:
- LOG_LEVEL=debug
- LOG_LEVEL=${OATHKEEPER_LOG_LEVEL:info}
- APP_ENV=${APP_ENV:-development}
volumes:
- ./docker/ory/oathkeeper:/etc/config/oathkeeper
- ./docker/ory/oathkeeper/logs:/var/log/oathkeeper
command: ["/etc/config/oathkeeper/entrypoint.sh"]
entrypoint: ["/etc/config/oathkeeper/entrypoint.sh"]
networks:
- ory-net

View File

@@ -19,4 +19,20 @@ export RULES_FILE
echo "[oathkeeper] APP_ENV=$APP_ENV_VALUE rules=$RULES_FILE"
exec /bin/sh -c "oathkeeper serve proxy -c /etc/config/oathkeeper/oathkeeper.yml 2>&1 | tee /var/log/oathkeeper/access.log"
RULES_ACTIVE="/etc/config/oathkeeper/rules.active.json"
if [ ! -f "$RULES_FILE" ]; then
echo "[oathkeeper] rules file not found: $RULES_FILE"
exit 1
fi
cp "$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
exit 1
fi
exec /bin/sh -c "oathkeeper serve proxy -c /etc/config/oathkeeper/oathkeeper.yml 2>&1 | tee \"$LOG_FILE\""

View File

@@ -14,7 +14,7 @@ errors:
access_rules:
repositories:
- file://${RULES_FILE:-/etc/config/oathkeeper/rules.json}
- file:///etc/config/oathkeeper/rules.active.json
authenticators:
noop:
@@ -34,6 +34,13 @@ authorizers:
enabled: true
config:
remote: http://keto:4466/check
payload: |
{
"namespace": "permissions",
"object": "{{ print .Request.URL.Path }}",
"relation": "access",
"subject_id": "{{ print .Subject }}"
}
mutators:
noop:

View File

@@ -0,0 +1,92 @@
[
{
"id": "public-health",
"description": "공개 헬스체크",
"match": {
"url": "http://<.*>/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": "http://<.*>/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": "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" }
]
}
]

43
mcp/compose.mcp.ory.yaml Normal file
View File

@@ -0,0 +1,43 @@
services:
kratos-mcp-server:
build:
context: ./kratos-mcp
container_name: mcp_ory_kratos
stdin_open: true
tty: true
init: true
environment:
- KRATOS_ADMIN_URL=http://kratos:4434
networks:
- ory-net
hydra-mcp-server:
build:
context: ./hydra-mcp
container_name: mcp_ory_hydra
stdin_open: true
tty: true
init: true
environment:
- HYDRA_PUBLIC_URL=http://hydra:4444
- HYDRA_ADMIN_URL=http://hydra:4445
networks:
- ory-net
keto-mcp-server:
build:
context: ./keto-mcp
container_name: mcp_ory_keto
stdin_open: true
tty: true
init: true
environment:
- KETO_READ_URL=http://keto:4466
- KETO_WRITE_URL=http://keto:4467
networks:
- ory-net
networks:
ory-net:
external: true
name: ory-net