init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# ForwardAuth · Cold Path (최초 로그인)
|
||||
|
||||
세션 쿠키가 없는 첫 요청. OIDC Authorization Code Flow + PKCE 전 구간이 1 회 일어난다. **사용자당 세션 만료 주기마다 한 번** 만 발생 — 일상 운영 트래픽의 99% 는 [warm path](forward-auth-warm.md) 다.
|
||||
|
||||
> Traefik 의 path-based Ingress 라우팅이 전제: `project.com/oauth2/*` 는 oauth2-proxy 로, 그 외 path 는 ForwardAuth Middleware 를 거쳐 백엔드로 간다. 이 라우팅 결정이 그림의 모든 분기의 기반이다.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant Traefik
|
||||
participant OAuth as oauth2-proxy
|
||||
participant KC as Keycloak
|
||||
|
||||
User->>Traefik: GET project.com/api/me
|
||||
Traefik->>OAuth: ForwardAuth GET /oauth2/auth (no cookie)
|
||||
OAuth-->>Traefik: 401 Unauthorized
|
||||
Traefik-->>User: 302 to /oauth2/start
|
||||
|
||||
User->>Traefik: GET /oauth2/start
|
||||
Note over Traefik: Ingress 가 /oauth2/* 를 oauth2-proxy 로 라우팅
|
||||
Traefik->>OAuth: forward
|
||||
Note over OAuth: PKCE code_verifier 생성, code_challenge 산출
|
||||
OAuth-->>User: 302 to Keycloak authorize with code_challenge
|
||||
|
||||
User->>KC: GET /realms/platform/protocol/openid-connect/auth
|
||||
KC-->>User: 로그인 폼
|
||||
User->>KC: POST 자격증명
|
||||
KC-->>User: 302 to /oauth2/callback with auth code
|
||||
|
||||
User->>Traefik: GET /oauth2/callback with code
|
||||
Traefik->>OAuth: forward
|
||||
OAuth->>KC: POST /token (code, code_verifier)
|
||||
KC-->>OAuth: id_token, access_token, refresh_token
|
||||
|
||||
OAuth->>KC: GET /realms/platform/protocol/openid-connect/certs
|
||||
KC-->>OAuth: JWKS 공개키
|
||||
Note over OAuth: id_token 서명 검증 with JWKS, nonce 일치 확인
|
||||
|
||||
OAuth-->>User: Set-Cookie _oauth2_proxy + 302 to /api/me
|
||||
Note over User: 이후 요청은 warm path
|
||||
```
|
||||
|
||||
## 핵심 인사이트
|
||||
|
||||
- **Ingress 라우팅이 분기의 뿌리**: 그림의 Note 가 가리키듯 `/oauth2/*` 와 그 외 path 가 *Ingress 단에서* 갈린다. 이 라우팅이 없으면 cold path 가 시작 자체를 못 한다.
|
||||
- **PKCE 가 핵심 보안 장치**: `code_verifier` 는 메시지 7~8 에서 oauth2-proxy 가 생성해 자기 세션에 저장하고, 메시지 16 에서 token exchange 시 함께 보낸다. Keycloak 은 `code_challenge` 와 매칭 검증. **authorization code 가 중간에 가로채지더라도 verifier 없이는 token 으로 교환 불가**. oauth2-proxy v7.5+ 는 PKCE 가 기본 활성.
|
||||
- **JWKS 검증의 위치**: 메시지 18~19 (`GET .../certs`) 가 별개의 호출이다. oauth2-proxy 는 JWKS 를 *처음 1 회 fetch 후 캐시* 하고, Keycloak 의 JWKS endpoint 가 회전 가능 (`kid` 헤더로 식별). **id_token 서명 검증 (메시지 20 의 Note) 이 끝나야 쿠키가 발급되므로**, 이후 warm path 에서 백엔드가 받는 `X-Forwarded-User` 는 *이미 검증된 사용자* 다.
|
||||
- **TLS 검증 전제**: 현재 oauth2-proxy 설정은 `ssl_insecure_skip_verify=false` 이다. 따라서 Keycloak 공개 호스트(`keycloak.dev.example.com`) 인증서 체인이 정상이어야 token 교환과 JWKS 검증 흐름이 끝까지 진행된다.
|
||||
|
||||
## 평소 요청 흐름은?
|
||||
|
||||
→ [forward-auth-warm.md](forward-auth-warm.md)
|
||||
@@ -0,0 +1,47 @@
|
||||
# ForwardAuth · Warm Path (세션 쿠키 보유)
|
||||
|
||||
쿠키 검증으로 끝나는 평소 요청 경로. **운영 트래픽의 99% 가 이 흐름** 이다. cold path 의 OIDC handshake 는 세션 만료 시에만 다시 일어난다.
|
||||
|
||||
3 가지 결과가 있다: (1) 쿠키 정상 → 즉시 통과, (2) access_token 만료 → silent refresh 후 통과, (3) 쿠키 위조 또는 refresh 실패 → cold path 재진입.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant Traefik
|
||||
participant OAuth as oauth2-proxy
|
||||
participant KC as Keycloak
|
||||
participant App as auth-server
|
||||
|
||||
User->>Traefik: GET project.com/api/me with cookie
|
||||
Traefik->>OAuth: ForwardAuth GET /oauth2/auth
|
||||
|
||||
alt 쿠키 HMAC 유효 + access_token 미만료
|
||||
OAuth-->>Traefik: 202 Accepted with X-Auth-Request-User
|
||||
else access_token 만료, refresh_token 유효
|
||||
Note over OAuth: silent refresh
|
||||
OAuth->>KC: POST /token with refresh_token
|
||||
KC-->>OAuth: 새 access_token
|
||||
OAuth-->>Traefik: 202 Accepted with X-Auth-Request-User
|
||||
else 쿠키 위조 또는 refresh 실패
|
||||
OAuth-->>Traefik: 401 Unauthorized
|
||||
Traefik-->>User: 302 to /oauth2/start
|
||||
Note over User: cold path 재진입
|
||||
end
|
||||
|
||||
Note over Traefik: 클라이언트 X-Forwarded 헤더 strip 후 oauth2-proxy 응답 헤더만 주입
|
||||
|
||||
Traefik->>App: GET /api/me with X-Forwarded-User alice
|
||||
App-->>User: 200 OK
|
||||
```
|
||||
|
||||
## 핵심 인사이트
|
||||
|
||||
- **백엔드가 헤더만 신뢰해도 안전한 이유**: Traefik 의 ForwardAuth Middleware 가 *클라이언트로부터 들어온* `X-Forwarded-*` 헤더를 strip 하고, *oauth2-proxy 응답에 담긴* 헤더만 백엔드로 전달한다. 클라이언트가 위조한 `X-Forwarded-User: admin` 은 도달하지 못한다. **이 strip 동작이 무너지면 권한 우회 취약점**이 되므로 Traefik Middleware 의 `authResponseHeaders` 와 (Traefik global) `forwardedHeaders` 설정이 핵심.
|
||||
- **백엔드 코드 단순화의 실체**: `auth-server` 의 컨트롤러는 `request.getHeader("X-Forwarded-User")` 한 줄만 본다. JWT 라이브러리, JWKS 캐시, 쿠키 파서, 세션 스토어가 모두 사라진다. 단위 테스트도 헤더 1 개 주입으로 인증된 사용자 시나리오가 만들어진다.
|
||||
- **silent refresh 는 사용자에게 보이지 않음**: alt 의 두 번째 분기가 그 경우. 사용자 브라우저는 redirect 를 안 본다 — Traefik ForwardAuth 호출 안에서 refresh 가 끝나고 같은 응답이 202 로 돌아온다.
|
||||
- **위조 시 회귀 경로**: 세 번째 분기. 쿠키 HMAC 가 안 맞거나 refresh 가 실패하면 oauth2-proxy 가 401 을 반환하고, Traefik 이 cold path 의 시작점인 `/oauth2/start` 로 돌려보낸다. 즉 **공격자가 쿠키를 위조해 봤자 결과는 로그인 페이지로의 redirect 일 뿐**이다.
|
||||
|
||||
## 처음 로그인 시 흐름은?
|
||||
|
||||
→ [forward-auth-cold.md](forward-auth-cold.md)
|
||||
@@ -0,0 +1,44 @@
|
||||
# Secret Pipeline · Bootstrap (1 회)
|
||||
|
||||
`tasks/vault-init.sh` 가 클러스터 최초 셋업 시 한 번만 수행하는 흐름. Vault 의 Kubernetes auth method 와 두 개의 role/policy, 그리고 VSO 가 사용할 VaultAuth CR 까지 준비한다. 모든 단계는 멱등 체크 후 차이만 적용된다 — Note 에 명시된 read 호출이 그 체크 지점.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Op as Operator
|
||||
participant Vault
|
||||
participant K8s as K8s API
|
||||
|
||||
Op->>Vault: operator init (5 unseal keys)
|
||||
Op->>Vault: operator unseal (3 keys)
|
||||
Vault-->>Op: Unsealed
|
||||
|
||||
Op->>K8s: apply ClusterRoleBinding (system auth-delegator)
|
||||
Note over K8s: Vault Pod SA 에 TokenReview 권한 위임
|
||||
|
||||
Op->>Vault: auth enable kubernetes + write config
|
||||
Note over Vault: vault auth list 후 미존재 시에만 enable
|
||||
|
||||
Op->>Vault: secrets enable kv-v2 at path secret
|
||||
Note over Vault: vault secrets list 후 미존재 시에만 enable
|
||||
|
||||
Op->>Vault: policy write x2 + role write x2 (auth-platform, storage)
|
||||
Note over Vault: 각 policy/role read 후 차이만 적용
|
||||
|
||||
Op->>Vault: kv put auth-server-db, keycloak-db, minio-tenant-env
|
||||
|
||||
Op->>K8s: apply VaultAuth x2
|
||||
Op->>K8s: apply VaultStaticSecret x7
|
||||
Note over K8s: VaultAuth 가 먼저, VaultStaticSecret 나중 - 그래야 reconcile 성공
|
||||
```
|
||||
|
||||
## 핵심 인사이트
|
||||
|
||||
- **두 role 의 의도**: VSO 의 ServiceAccount 는 `vault-secrets-operator/mnt` 한 개뿐이다. 그러나 Vault 에 role 두 개를 두고 각각 다른 policy 를 묶었다. **VaultStaticSecret 마다 자기 도메인의 VaultAuth CR 을 참조**하므로, auth-platform role 의 토큰이 유출돼도 minio secret 은 못 읽는다.
|
||||
- **`system:auth-delegator` 의 위치**: 이 ClusterRoleBinding 은 *Vault Pod 의 SA* 에 부여된다. Vault 가 VSO 의 SA JWT 를 검증하기 위해 K8s 의 `TokenReview` API 를 호출할 권한이 필요하기 때문. VSO 측이 아니라 Vault 측에 붙는다는 점이 자주 헷갈리는 지점.
|
||||
- **멱등성의 위치**: 각 enable / write 호출 직전에 `vault auth list`, `vault secrets list`, `vault policy read`, `vault read auth/kubernetes/role/<name>` 으로 현재 상태를 체크하고 차이만 적용한다. 따라서 이 다이어그램의 모든 단계는 *재실행 안전*.
|
||||
- **마지막 두 단계의 순서**: VaultAuth 가 먼저, VaultStaticSecret 이 나중. 그래야 VSO 가 첫 reconcile 에서 `vaultAuthRef` 를 정상 해석한다.
|
||||
|
||||
## 정상 운영 시 reconcile 흐름은?
|
||||
|
||||
→ [secret-pipeline-runtime.md](secret-pipeline-runtime.md)
|
||||
@@ -0,0 +1,54 @@
|
||||
# Secret Pipeline · Steady-State Reconcile
|
||||
|
||||
VSO 가 VaultStaticSecret CR 을 reconcile 할 때마다 일어나는 흐름. **이 다이어그램은 한 reconcile 사이클** 만 다룬다 — 부트스트랩(정책/role/CR 적용) 은 [secret-pipeline-bootstrap.md](secret-pipeline-bootstrap.md) 에서 이미 끝난 상태를 전제한다.
|
||||
|
||||
VSO 는 controller-runtime 기반이라 informer 가 *시작 시 1 회 watch 등록* 하고, 이후 K8s API 가 push 하는 이벤트로 reconcile 이 트리거된다. 즉 매 cycle 마다 watch 호출이 새로 일어나는 게 아니다.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant VSO as VSO Operator
|
||||
participant K8s as K8s API
|
||||
participant Vault
|
||||
participant Sec as K8s Secret
|
||||
|
||||
Note over VSO,K8s: informer 가 시작 시 1 회 watch 등록 후 이벤트 수신 대기
|
||||
|
||||
K8s-->>VSO: event for VaultStaticSecret auth-server-db-creds
|
||||
VSO->>K8s: read VaultStaticSecret spec
|
||||
K8s-->>VSO: vaultAuthRef, path
|
||||
|
||||
VSO->>K8s: read VaultAuth vault-auth-auth-platform
|
||||
K8s-->>VSO: role vso-auth-platform, mount kubernetes
|
||||
|
||||
VSO->>Vault: POST auth/kubernetes/login (role, jwt)
|
||||
Vault->>K8s: TokenReview (VSO SA JWT)
|
||||
Note over Vault,K8s: system auth-delegator 권한 사용
|
||||
K8s-->>Vault: ok, sa vault-secrets-operator
|
||||
Vault-->>VSO: Vault token (policy vso-auth-platform, ttl 1h)
|
||||
|
||||
VSO->>Vault: GET secret/data/auth-server/db
|
||||
Vault-->>VSO: username, password, jdbc-url
|
||||
|
||||
Note over VSO: destination overwrite false 면 기존 Secret 유지
|
||||
VSO->>K8s: create or update Secret auth-server-db
|
||||
K8s-->>Sec: stored
|
||||
|
||||
Note over Sec: kubelet 이 Pod 시작 시 envFrom 으로 마운트 (시퀀스 외)
|
||||
|
||||
loop every refreshAfter (1h)
|
||||
VSO->>Vault: GET secret/data/auth-server/db
|
||||
Vault-->>VSO: 최신 값
|
||||
opt 값이 변경된 경우
|
||||
VSO->>K8s: update Secret auth-server-db
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## 핵심 인사이트
|
||||
|
||||
- **이 다이어그램의 시작점은 K8s 가 던지는 event**: VSO 가 매 cycle 마다 watch API 를 새로 호출하는 게 아니다. controller-runtime 의 informer 가 startup 에 watch 를 establish 하고, K8s API 가 변경 사항을 push 하면 reconcile loop 이 깨어난다. 그래서 메시지 1 의 화살표 방향이 K8s → VSO.
|
||||
- **TokenReview 는 VSO 가 부르는 게 아니라 Vault 가 부른다**: 메시지 7 (`Vault to K8s API: TokenReview`) 가 그 호출. Vault 가 *받은* SA JWT 가 진짜 VSO 의 것인지 확인하기 위해 K8s 에 위임 검증한다.
|
||||
- **role 결정은 VaultAuth CR 이 한다**: 메시지 4~5 에서 VSO 는 *VaultStaticSecret 이 가리키는 VaultAuth* 를 읽고, 거기에 박힌 `role: vso-auth-platform` 으로 Vault login 한다. 같은 SA 라도 어느 VaultAuth 를 거쳤느냐에 따라 받는 policy 가 달라진다.
|
||||
- **`overwrite=false` 의 책임 위치**: 이건 K8s API 의 동작이 아니라 *VSO reconciler 가 update 호출 전에 자기 로직으로 결정* 한다. 그래서 Note 가 VSO 위에 붙는다.
|
||||
- **즉시 반영**: Vault 값 변경 직후 반영하려면 `kubectl -n mnt delete secret auth-server-db`. 다음 reconcile 에서 VSO 가 위 흐름을 다시 돌아 새 값으로 재생성한다 — Pod 는 envFrom 으로 받은 값이 바뀌었음을 자동으로 알 수 없으므로 rollout 도 함께.
|
||||
Reference in New Issue
Block a user