forked from baron/baron-sso
oathkeeper 동작 확인
This commit is contained in:
@@ -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
|
||||
|
||||
30
README.md
30
README.md
@@ -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**: 대시보드 및 통합 런처 구현 (예정)
|
||||
|
||||
132
backend/cmd/server/health_monitor.go
Normal file
132
backend/cmd/server/health_monitor.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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\""
|
||||
|
||||
@@ -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:
|
||||
|
||||
92
docker/ory/oathkeeper/rules.active.json
Normal file
92
docker/ory/oathkeeper/rules.active.json
Normal 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
43
mcp/compose.mcp.ory.yaml
Normal 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
|
||||
Reference in New Issue
Block a user