Keycloak이란?
Keycloak은 오픈 소스 Identity & Access Management(IDAM) 솔루션이다.
즉, 여러 애플리케이션의 로그인·세션·사용자/그룹·권한을 중앙에서 관리하는 도구이다.
- SSO(싱글 로그인) : 한 번 로그인하면 여러 앱에 재로그인 없이 접근
- 중앙 사용자 관리 : 모든 사용자/비밀번호/세션/계정을 Keycloak에서 관리
- Role / Group 기반 RBAC : 조직 혹은 역할 기반으로 앱 접근 권한 제어
- LDAP/AD 연동(User Federation) : 회사 조직도 그대로 반영 가능
- OAuth2/OIDC/SAML 같은 표준 인증 프로토콜 : 애플리케이션은 로그인 구현 없이 Token만 검증하면 됨
- 레거시 앱은 OAuth2 Proxy로 보호 가능 : 앱이 OIDC를 몰라도 Keycloak 인증 강제 가능
이번 스터디에서는 oauth-proxy를 연동해 구성하고 간단한 keycloak 개념 확인과 SSO연동을 실습해보았다.
Keycloak 실습환경
이전과 동일하게 Windows 11의 WSL2를 통해서 Ubuntu 24.04를 실행하고 Kind를 통해서 구성하였다.
- Host : Windows 11
- WSL2 : Ubuntu 24.04
- kind / kubectl / helm 설치
kind 클러스터 + Ingress-nginx
이전 실습들에서 자주 설치했기 때문에 자세한 설명은 생략한다.
kind 클러스터 생성
cat <<EOF | kind create cluster --name myk8s --image kindest/node:v1.32.8 --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
labels:
ingress-ready: "true"
extraPortMappings:
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443
- containerPort: 30000
hostPort: 30000
EOF
Ingress-NGINX 설치 + SSL Passthrough
OAuth2 Proxy를 써야하기 때문에 Ingress 구성이 필요하다.
kubectl apply -f \
https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
TLS passthrough 활성화
kubectl get deployment ingress-nginx-controller -n ingress-nginx -o yaml \
| sed '/--publish-status-address/ a\
- --enable-ssl-passthrough' \
| kubectl apply -f -
Keycloak 배포
keycload은 앞서 설명과 마찬가지로 아래와 같은 기능을 제공한다.
- 사용자/그룹/역할 관리
- 로그인 UI 제공
- OAuth2/OIDC/SAML 인증 처리
- 토큰 발행
- 세션 관리
실제 운영에서는 DB를 연결하거나·HA 구성이 필요하지만, 간단한 기능 테스트 용이므로 "start-dev" 옵션을 줘서 구성한다.
kubectl create ns keycloak
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
namespace: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.4.0
args: ["start-dev"]
env:
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
value: admin
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: keycloak
spec:
selector:
app: keycloak
ports:
- port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak
namespace: keycloak
spec:
ingressClassName: nginx
rules:
- host: keycloak.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
EOF
Ingress를 통해서 접속하기 위해 keycloak.example.com 도메인을 hosts파일에 넣어준다.
이후에 사용할 도메인도 미리 추가한다.
# Linux 환경은 아래 파일에 추가
cat <<EOF | sudo tee -a /etc/hosts
127.0.0.1 keycloak.example.com
127.0.0.1 auth.example.com
127.0.0.1 app1.example.com
127.0.0.1 app2.example.com
EOF
# Windows 환경은 아래 파일에 추가
# powershell 관리자 권한으로 실행 필요
notepad C:\Windows\System32\drivers\etc\hosts
PS C:\Windows\System32> type C:\Windows\System32\drivers\etc\hosts | findstr 127
# 127.0.0.1 localhost
127.0.0.1 keycloak.example.com
127.0.0.1 auth.example.com
127.0.0.1 app1.example.com
127.0.0.1 app2.example.com
http://keycloak.example.com 에 접속해 admin / admin 으로 로그인


CoreDNS에 Keycloak 서비스 IP 매핑
# Keycloak Service IP 확인
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# KEYCLOAK_SVC_IP=$(kubectl get svc -n keycloak keycloak -o jsonpath='{.spec.clusterIP}')
echo "KEYCLOAK_SVC_IP = $KEYCLOAK_SVC_IP"
KEYCLOAK_SVC_IP = 10.96.72.176
# CoreDNS ConfigMap 수정
...
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
hosts {
# 여기 IP를 아까 KEYCLOAK_SVC_IP 값으로 교체
10.96.72.176 keycloak.example.com
# fallthrough는 다른 도메인은 기존 규칙으로 흘려보내기 위해 설정
fallthrough
}
...
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl rollout restart deployment coredns -n kube-system
deployment.apps/coredns restarted
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get pods -n kube-system -l k8s-app=kube-dns
NAME READY STATUS RESTARTS AGE
coredns-5c9899c9fb-69fxj 1/1 Running 0 3s
coredns-5c9899c9fb-mrd2r 1/1 Running 0 3s
coredns-668d6bf9bc-44x9p 1/1 Terminating 0 168m
coredns-668d6bf9bc-56kgs 1/1 Terminating 0 168m
Keycloak의 Realm / User / Role / Group 확인
Keycloak은 조직/권한 모델링을 중앙에서 처리
- Realm : 조직 단위 환경 분리
- Keycloak 서버에서 여러 회사/팀/환경을 분리하는 구조
- 독립적인 사용자/클라이언트/정책 공간

- User : 실제 로그인 대상

- Role : 권한 묶음으로 RBAC(역할 기반 접근 제어)에서 핵심 구성 요소

- Group : 조직 단위 권한 묶음

테스트용 애플리케이션 준비
2개의 앱을 통해 keycloak의 기능을 확인한다.
kubectl create ns app1
kubectl create ns app2
for ns in app1 app2; do
cat <<EOF | kubectl apply -n $ns -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
spec:
replicas: 1
selector:
matchLabels: { app: httpbin }
template:
metadata: { labels: { app: httpbin } }
spec:
containers:
- name: httpbin
image: kennethreitz/httpbin
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
spec:
selector: { app: httpbin }
ports:
- port: 80
targetPort: 80
EOF
done
OAuth2 Proxy 구성
httpbin 같은 앱은 OIDC 프로토콜을 스스로 처리할 수 없기 때문에 OAuth2 Proxy를 구성해 Keycloak과 인증을 주고받고 인증된 사용자만 backend로 전달하도록 구성한다.
Keycloak Client 생성
Realm: demo → Clients → Create
- Client ID: oauth2-proxy
- Client Authentication: On
- Valid redirect URI
http://auth.example.com/oauth2/callback - Web origins
http://auth.example.com
Credentials 탭 → Client secret 복사

OAuth2 Proxy 설치
kubectl create ns auth || true
helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
helm repo update
MY_CLIENT_SECRET="<KEYCLOAK_CLIENT_SECRET>"
MY_COOKIE_SECRET=$(tr -dc A-Za-z0-9 </dev/urandom | head -c32; echo)
helm upgrade --install oauth2-proxy oauth2-proxy/oauth2-proxy --version 8.5.1 \
-n auth --create-namespace \
--set config.clientID="oauth2-proxy" \
--set config.clientSecret="$MY_CLIENT_SECRET" \
--set config.cookieSecret="$MY_COOKIE_SECRET" \
--set config.emailDomains="*" \
--set extraArgs.provider="oidc" \
--set extraArgs.oidc-issuer-url="http://keycloak.example.com/realms/demo" \
--set extraArgs.redirect-url="http://auth.example.com/oauth2/callback" \
--set extraArgs.cookie-secure="false" \
--set extraArgs.cookie-domain=".example.com" \
--set extraArgs.cookie-samesite="lax" \
--set extraArgs.whitelist-domain=".example.com" \
--set extraArgs.oidc-email-claim="email" \
--set extraArgs.insecure-oidc-allow-unverified-email="true" \
--set extraArgs.set-xauthrequest="true" \
--set extraArgs.set-authorization-header="true" \
--set extraArgs.pass-access-token="true"
OAuth2 Proxy Ingress 구성
cat <<EOF | kubectl apply -n auth -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: oauth2-proxy
spec:
ingressClassName: nginx
rules:
- host: auth.example.com
http:
paths:
- path: /oauth2/
pathType: Prefix
backend:
service:
name: oauth2-proxy
port:
number: 80
EOF
앱(httpbin)을 Keycloak 인증으로 보호
for ns in app1 app2; do
cat <<EOF | kubectl apply -n $ns -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: httpbin
annotations:
# 내부에서 oauth2-proxy 서비스로 직접 호출
nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.auth.svc.cluster.local/oauth2/auth"
# 외부(브라우저)는 auth.example.com으로 리다이렉트
nginx.ingress.kubernetes.io/auth-signin: "http://auth.example.com/oauth2/start?rd=\$scheme://\$host\$request_uri"
nginx.ingress.kubernetes.io/auth-response-headers: Authorization, X-Auth-Request-User, X-Auth-Request-Email
spec:
ingressClassName: nginx
rules:
- host: ${ns}.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: httpbin
port:
number: 80
EOF
done
앱 접속을 위한 Ingress 도메인 추가
# Linux 환경은 아래 파일에 추가
echo "127.0.0.1 app1.example.com app2.example.com" | sudo tee -a /etc/hosts
# Windows 환경은 아래 파일에 추가
# powershell 관리자 권한으로 실행 필요
notepad C:\Windows\System32\drivers\etc\hosts
PS C:\Windows\System32> type C:\Windows\System32\drivers\etc\hosts | findstr 127
# 127.0.0.1 localhost
127.0.0.1 keycloak.example.com
127.0.0.1 auth.example.com
127.0.0.1 app1.example.com app2.example.com
SSO 기능 테스트
브라우저에서 http://app1.example.com 접속 시 로그인 페이지(Keycloak)로 이동 확인

기존에 생성해둔 demo Realm의 miri 계정으로 사용자 정보 업데이트 후 로그인
keycloak 로그인까지는 되는데 500 에러가 발생한다.

...
10.244.0.1:59864 - b859295c-337e-4801-86f8-4caa6129419c - - [2025/11/29 19:01:52] 10.244.0.30:4180 GET - "/ping" HTTP/1.1 "kube-probe/1.32" 200 2 0.000
10.244.0.1:59878 - 8b051c2d-6233-4d5d-85f3-2e27fd9e0785 - - [2025/11/29 19:01:53] 10.244.0.30:4180 GET - "/ready" HTTP/1.1 "kube-probe/1.32" 200 2 0.000
[2025/11/29 19:01:58] [oauthproxy.go:898] Error redeeming code during OAuth2 callback: email in id_token (test@test.com) isn't verified
...
이메일 인증이 안되어서 로그인 안되는 것이므로, keycloak 설정에서 이메일 검증을 on으로 바꿔준다.

이메일 검증 설정 확인 후 새로 로그인하면 아래와 같이 app1에 로그인할 수 있다.

이 상황에서 브라우저 탭을 켜고 app2로 접근을 시도하면 별도 로그인 절차 없이 바로 app2로 접근되는 것을 확인할 수 있다.

Scope/Claims 테스트
Scope/Claim 은 "로그인 이후 앱이 사용자에게 어떤 권한을 줄지 결정하는 정보" 라고 볼 수 있다.
다양한 사용자 정보를 가지고 특정 정보를 가진 사용자에게 권한을 세부적으로 제한하거나 부여할 수 있다.
https://auth0.com/docs/get-started/apis/scopes
Scopes
Understand the principle of scopes and explore general examples of their use.
auth0.com
사용되는 Scope 확인
Keycloak → demo realm -> Clients → oauth2-proxy → Client scopes 탭
Default client scopes에 있는 것 목록 확인

현재 사용되는 세션의 Scope 확인을 위해 http://app1.example.com/headers 에 접근한다.

Authorization에 보이는 문자열을 Bearer를 제외하고 JSON Web Tokens - jwt.io 사이트에 입력해서 파싱하면 아래와 같이 사용되는 Scope를 확인해볼 수 있다.

Keycloak 중앙에서 Sessions 강제 종료 테스트
Keycloak에서 모든 서비스 세션을 종료할 수 있는 기능이 있는데 아래와 같은 장점이 있다.
- 퇴사자 계정 즉시 차단
- 해킹 의심 시 세션 일괄 만료
- 여러 서비스의 세션 상태를 한 번에 관리 가능
아래와 같이 miri 계정으로 로그인을 해둔다.

Keycloak → Users → miri 계정의 Session 탭에 보면 현재 로그인 중인 세션 목록을 확인할 수 있다.

해당 관리창에서 세션을 강제로 로그아웃 시키는 등 세션을 관리할 수 있다.

다만 여기서 진행한 실습 구조에서는 세션이 바로 끊기거나 로그아웃되진 않는데, 지금 구성에서 Keycloak 세션 로그아웃은 "Keycloak 자체 SSO 세션"만 끊기 때문이다.
즉 Keycloak의 sso-session cookie (KEYCLOAK_IDENTITY) 를 만료시키는 동작이지만, oauth2-proxy는 Keycloak의 세션 쿠키를 사용하지 않고 oauth2-proxy는 자체적으로 만든 _oauth2_proxy 암호화 쿠키로 세션을 유지하기 때문에 세션이 끊기지 않는다.
이 세션 로그아웃 기능을 제대로 사용하려면 Keycloak과 클라이언트 사이에 backchannel logout 연동이 필요하다.

다만 조금 찾아보았으나 oauth-proxy에서는 아직 backchannel logout을 지원하지는 않는것으로 보인다.
https://github.com/oauth2-proxy/oauth2-proxy/issues/1224
[Feature Request] Backchannel sign-out endpoint to invalidate session · Issue #1224 · oauth2-proxy/oauth2-proxy
Hi, To support single-sign-out for Keycloak, in the Keycloak client registration it is possible to specify a backchannel logout URL. If a user authenticated in a realm signs out using any client, k...
github.com