본문으로 건너뛰기
← Systems Notebook / Ops
MLOps Notes04 / 15

Keycloak으로 이해하는 SSO: Realm, Client, OIDC와 SAML

Keycloak의 핵심 객체와 SSO의 동작 원리를 살펴보고, OIDC Authorization Code Flow with PKCE를 따라가며 SAML과의 선택 기준을 정리합니다.

이 글의 배경

HPE 엔지니어의 작업 자료를 바탕으로 정리한 글입니다. 개인 실험 기록이나 HPE 공식 문서와는 구분합니다. 본문의 환경과 버전은 글 작성 당시를 기준으로 합니다.

플랫폼에 GitLab, Harbor, Argo CD, Grafana가 추가될 때마다 계정을 따로 만들면 사용자와 운영자가 관리할 일이 늘어납니다. 사용자는 서비스마다 비밀번호를 입력해야 하고, 운영자는 입사·부서 이동·퇴사 때마다 여러 시스템의 계정을 변경해야 합니다. 서비스별로 MFA 적용 여부가 달라지는 등 보안 정책을 일관되게 유지하기도 어려워집니다.

SSO(Single Sign-On)는 이러한 계정 관리와 로그인 문제를 줄이기 위해 사용자 인증의 책임을 중앙 IdP(Identity Provider)에 모으고, 각 애플리케이션에는 검증 가능한 신원 정보만 전달하는 구조를 사용합니다. 로그인 화면을 통합하는 것과 함께 인증을 담당하는 주체도 한곳으로 모으는 방식입니다. Keycloak은 이 구조를 온프레미스와 폐쇄망에서도 구현할 수 있는 IAM(Identity and Access Management) 서버입니다.

이 글에서는 Keycloak 안의 객체가 각각 어떤 역할을 맡는지 살펴본 뒤 설정 메뉴로 넘어갑니다. 이어서 OIDC Authorization Code Flow with PKCE의 요청과 응답을 따라가고, SAML이 필요한 경우와 그렇지 않은 경우를 구분합니다. 다음 글에서는 groups 클레임을 구성해 Argo CD의 프로젝트 권한으로 연결합니다.

이 글에서 다루는 것

  • 인증(Authentication)과 인가(Authorization), SSO의 관계
  • Keycloak의 Realm, Client, User, Group, Role, Client Scope, Mapper
  • ID Token, Access Token, Refresh Token의 용도 차이
  • OIDC Authorization Code Flow와 PKCE의 동작 원리
  • OIDC Discovery와 JWKS가 애플리케이션 설정을 단순화하는 방식
  • OIDC와 SAML 2.0의 차이 및 선택 기준
  • 테스트용 public client를 만들고 실제 토큰의 클레임을 확인하는 실습
  • 운영 시 반드시 고려해야 할 리다이렉트 URI, 토큰, 세션, 가용성 정책

실습 환경과 치환값

이 글의 주소와 계정은 모두 문서용 예시입니다. 실제 환경의 값을 코드에 기록하지 말고 아래 의미를 유지해 치환합니다.

의미 이 글의 예시 치환 방법
Keycloak 외부 주소 https://sso.lab.example.com 조직의 Keycloak HTTPS 주소
실습 Realm mlops 운영 서비스용 Realm 이름
OIDC Client ID oidc-lab-cli 애플리케이션을 식별하는 공개 식별자
로컬 콜백 http://127.0.0.1:8085/callback 실습 중 브라우저가 돌아올 정확한 URI
테스트 사용자 student-01 Realm 안의 실습 사용자
민감값 ${KEYCLOAK_CLIENT_SECRET} 환경변수 또는 Secret에서 주입

이 글의 PKCE 실습은 client secret을 보관할 수 없는 public client를 사용하므로 ${KEYCLOAK_CLIENT_SECRET} 자체가 필요 없습니다. 서버 애플리케이션용 confidential client를 만들 때도 시크릿을 Markdown, Git 저장소, Helm values에 직접 적지 않습니다.

SSO가 해결하는 문제

인증과 인가는 다른 결정이다

인증은 “이 사용자가 누구인가”를 확인하는 절차입니다. 인가는 인증된 사용자가 “이 리소스에 어떤 행동을 할 수 있는가”를 결정합니다.

예를 들어 Keycloak이 student-01의 비밀번호와 MFA를 확인하고 서명된 토큰을 발급하는 과정은 인증입니다. Argo CD가 토큰의 groups를 보고 web 프로젝트의 동기화만 허용할지 결정하는 과정은 인가입니다. 중앙 IdP가 사용자 인증을 맡더라도, 각 애플리케이션에서 허용할 행동은 별도의 권한 정책으로 정해야 합니다.

text
Keycloak: 사용자 인증 + 신원/그룹 클레임 발급
애플리케이션: 토큰 검증 + 자체 권한 정책 평가

이렇게 책임을 나누면 Keycloak에서는 계정을 일관되게 관리하고, 각 애플리케이션에서는 해당 서비스에 맞는 권한 정책을 유지할 수 있습니다.

SSO 세션은 비밀번호 공유가 아니다

SSO 환경에서 애플리케이션 A와 B는 사용자의 비밀번호를 받지 않습니다. 사용자가 A에 접근하면 A는 브라우저를 Keycloak으로 보냅니다. Keycloak이 인증을 마치면 A가 검증할 수 있는 토큰을 발급합니다. 이후 B에 접근했을 때 브라우저에 이미 Keycloak 세션이 있으면 로그인 화면을 다시 거치지 않고 B용 인증 결과를 발급합니다.

구조 살펴보기 / 01중앙 인증과 서비스별 권한
사용자 브라우저각 서비스에 접속
Keycloak중앙 인증과 SSO 세션
01 / 04
로그인은 중앙에서

Keycloak은 사용자 인증과 SSO 세션을 담당합니다. 서비스끼리 비밀번호를 공유하는 구조가 아닙니다.

1

사용자 브라우저 ↔ Keycloak로그인·MFA

구성 관계를 차례로 강조합니다. 단계는 실제 실행 순서가 아닙니다.

전체 단계 한눈에 읽기
  1. 로그인은 중앙에서

    Keycloak은 사용자 인증과 SSO 세션을 담당합니다. 서비스끼리 비밀번호를 공유하는 구조가 아닙니다.

    • 사용자 브라우저 ↔ Keycloak: 로그인·MFA
  2. Argo CD의 권한은 Argo CD가

    Argo CD는 OIDC 인증 결과를 확인한 뒤 자체 RBAC 정책으로 허용할 행동을 결정합니다.

    • 사용자 브라우저 → Argo CD: 서비스 접속
    • Argo CD ↔ Keycloak: OIDC
  3. Grafana도 자체 권한 확인

    같은 Keycloak을 사용해도 Grafana의 권한은 Grafana에서 정합니다.

    • 사용자 브라우저 → Grafana: 서비스 접속
    • Grafana ↔ Keycloak: OIDC
  4. Harbor도 같은 책임 분리

    Harbor도 중앙 인증을 이용하지만 저장소 접근 권한은 자체 정책에 따릅니다.

    • 사용자 브라우저 → Harbor: 서비스 접속
    • Harbor ↔ Keycloak: OIDC
도식 원문
flowchart LR
  U["사용자 브라우저"]
  KC["Keycloak<br/>중앙 인증과 SSO 세션"]
  A["Argo CD<br/>자체 RBAC"]
  G["Grafana<br/>자체 권한"]
  H["Harbor<br/>자체 권한"]

  U <-->|"로그인·MFA"| KC
  U --> A
  U --> G
  U --> H
  A <-->|"OIDC"| KC
  G <-->|"OIDC"| KC
  H <-->|"OIDC"| KC

따라서 SSO의 실질적인 이점은 다음과 같습니다.

  • 비밀번호와 MFA 정책을 중앙에서 일관되게 적용합니다.
  • 애플리케이션이 사용자 비밀번호를 저장하지 않습니다.
  • 사용자를 비활성화하고 세션을 종료하는 지점을 중앙화합니다.
  • 그룹과 역할을 토큰 클레임으로 전달해 애플리케이션별 인가에 활용합니다.
  • 로그인, 실패, 관리자 변경 같은 감사 이벤트를 한 곳에서 추적하기 쉬워집니다.

인증을 한곳에 모으면 Keycloak 장애가 신규 로그인 전체에 영향을 줍니다. SSO를 인증 의존성을 명시적인 중앙 시스템으로 만드는 기술로 이해하고, 고가용성·데이터베이스 백업·인증서 갱신·시간 동기화·비상 관리자 절차를 함께 설계해야 합니다.

Keycloak의 객체를 하나의 경계로 이해하기

구조 살펴보기 / 02Keycloak 객체와 Realm
Keycloak 인스턴스Realm을 운영하는 서버
master Realm서버 관리 전용
mlops Realm서비스 사용자 경계
01 / 05
관리와 서비스 영역 분리

master Realm은 서버 관리에 사용하고, 서비스 사용자는 별도의 mlops Realm에 둡니다.

1

Keycloak 인스턴스 → master Realm관리 Realm

2

Keycloak 인스턴스 → mlops Realm서비스 Realm

구성 관계를 차례로 강조합니다. 단계는 실제 실행 순서가 아닙니다.

전체 단계 한눈에 읽기
  1. 관리와 서비스 영역 분리

    master Realm은 서버 관리에 사용하고, 서비스 사용자는 별도의 mlops Realm에 둡니다.

    • Keycloak 인스턴스 → master Realm: 관리 Realm
    • Keycloak 인스턴스 → mlops Realm: 서비스 Realm
  2. 앱과 사용자 등록

    Clients는 인증을 사용하는 앱이고 Users는 이 Realm의 사용자입니다. 화살표는 포함 관계를 뜻합니다.

    • mlops Realm → Clients: 앱 등록
    • mlops Realm → Users: 사용자 관리
  3. 그룹과 역할 구성

    Groups와 Realm / Client Roles로 사용자 분류와 역할을 구성합니다.

    • mlops Realm → Groups: 그룹 관리
    • mlops Realm → Roles: 역할 관리
  4. 토큰에 담을 정보

    Client Scopes와 Protocol Mappers가 토큰에 포함할 클레임을 구성합니다.

    • mlops Realm → Client Scopes: 범위 설정
    • Client Scopes → Protocol Mappers: 클레임 구성
  5. 외부 계정·인증 연결

    User Federation은 LDAP 디렉터리를 연결하고, Identity Providers는 외부 인증에 위임합니다. 서로 다른 연계 방식입니다.

    • mlops Realm → User Federation: 디렉터리 연결
    • mlops Realm → Identity Providers: 인증 위임
도식 원문
flowchart TD
  I["Keycloak 인스턴스"] --> M["master Realm<br/>서버 관리 전용"]
  I --> R["mlops Realm<br/>서비스 사용자 경계"]
  R --> C["Clients<br/>Argo CD·Grafana 등"]
  R --> U["Users"]
  R --> G["Groups"]
  R --> RO["Realm / Client Roles"]
  R --> S["Client Scopes"]
  S --> PM["Protocol Mappers<br/>토큰 클레임 생성"]
  R --> F["User Federation<br/>LDAP 디렉터리 연결"]
  R --> P["Identity Providers<br/>외부 인증 위임"]

Realm: 사용자와 정책의 격리 경계

Realm은 사용자, 자격증명, 세션, 그룹, 역할, 클라이언트를 묶는 최상위 격리 단위입니다. 서로 다른 Realm의 사용자는 같은 이름을 가질 수 있고 정책과 세션도 분리됩니다.

master Realm은 Keycloak 서버 자체를 관리하는 특수 영역입니다. 업무 애플리케이션을 master에 연결하면 관리 계정과 서비스 계정의 경계가 흐려집니다. 이 글에서는 별도의 mlops Realm을 사용합니다.

Realm을 나누면 사용자와 세션도 분리되므로 팀마다 별도 Realm이 필요한지 먼저 판단해야 합니다. 보안·규제·테넌트 격리가 필요한 경우에는 Realm을 나누고, 같은 조직 안의 부서 구분은 대개 Group과 Role로 표현하는 편이 관리하기 단순합니다.

Client: 인증을 위임하는 애플리케이션

Client는 Keycloak에 등록된 애플리케이션입니다. argocd, grafana, oidc-lab-cli 같은 Client ID로 구분합니다.

형태 시크릿 보관 대표 대상 기본 보호 수단
서버 측 웹 애플리케이션 가능 백엔드 웹, 서버형 관리 도구 confidential client + client 인증
브라우저 SPA·모바일·CLI 불가능 공개 바이너리와 브라우저 코드 public client + Authorization Code + PKCE
API 리소스 서버 사용자 로그인 시작 안 함 토큰을 받는 API issuer, audience, signature, expiry 검증

Client ID는 공개할 수 있는 식별자이지만 Client secret은 비밀로 보관해야 하는 값입니다. SPA JavaScript나 모바일 앱 바이너리에 secret을 넣으면 사용자가 추출할 수 있어 비밀로 유지할 수 없습니다. 이런 클라이언트에는 PKCE를 사용합니다.

User, Group, Role: 사람과 권한을 섞지 않기

  • User는 로그인 주체입니다. 로컬로 생성하거나 외부 디렉터리에서 가져올 수 있습니다.
  • Group은 조직 또는 책임 단위의 사용자 집합입니다. 사용자의 이동은 그룹 멤버십 변경으로 표현하기 좋습니다.
  • Role은 권한의 이름입니다. Realm 전체 역할과 특정 Client 역할로 나뉩니다.

운영에서는 사용자에게 애플리케이션 권한을 직접 하나씩 붙이기보다 다음 연결이 관리하기 쉽습니다.

text
사용자 → 그룹 → 토큰의 groups 클레임 → 애플리케이션 내부 역할 → 리소스 권한

그룹은 “누가 어느 팀인가”를 표현하고, 애플리케이션은 “그 팀이 무엇을 할 수 있는가”를 결정합니다. 다음 글의 Argo CD RBAC가 바로 이 패턴을 사용합니다.

Client Scope와 Protocol Mapper: 토큰의 내용 설계

Client Scope는 여러 Client가 재사용할 수 있는 클레임과 역할 범위의 묶음입니다. Protocol Mapper는 사용자 속성, 그룹, 역할을 OIDC 클레임 또는 SAML 속성으로 변환합니다.

예를 들어 Group Membership Mapper를 사용하면 다음과 같은 ID Token을 만들 수 있습니다.

json
{
  "iss": "https://sso.lab.example.com/realms/mlops",
  "aud": "oidc-lab-cli",
  "sub": "opaque-user-id",
  "preferred_username": "student-01",
  "groups": ["argocd-web"]
}

토큰에 담긴 정보는 브라우저, 프록시, 로그를 거칠 수 있으며, 토큰 크기는 HTTP 헤더 한도에도 영향을 줍니다. 따라서 애플리케이션에 필요한 최소 속성만 매핑해야 합니다. 주민번호, 내부 인사정보, 불필요한 전체 그룹 목록은 제외합니다.

User Federation과 외부 Identity Provider의 차이

두 기능은 모두 외부 사용자와 연결되지만 동작 방식이 다릅니다.

구분 User Federation 외부 Identity Provider 연결
대표 대상 LDAP, Active Directory 다른 OIDC 또는 SAML IdP
비밀번호 검증 Keycloak이 디렉터리에 조회/bind 브라우저를 외부 IdP로 이동
Keycloak의 역할 사용자 저장소 연결 인증 브로커
사용 예 기존 사내 계정을 그대로 사용 별도 인증 체계를 Keycloak 앞에 연결

어느 방식을 쓰더라도 최종 애플리케이션이 Keycloak 하나만 신뢰하게 만들면 Client 설정을 일관되게 유지할 수 있습니다.

OIDC는 OAuth 2.0과 무엇이 다른가

OAuth 2.0은 자원 접근 권한을 위임하기 위한 프레임워큽니다. 그것만으로 “로그인한 사람이 누구인지”를 표준화하지 않습니다. OIDC(OpenID Connect)는 OAuth 2.0 위에 인증 계층을 추가하고, ID Token과 표준 클레임, UserInfo, Discovery 규칙을 정의합니다.

토큰을 받은 뒤에는 누가 어떤 목적으로 사용할 값인지 구분해야 합니다. 다음 표에서 세 토큰의 소비자와 검증 책임을 비교할 수 있습니다.

토큰 소비자 목적 주의점
ID Token 로그인한 Client 사용자 인증 결과와 신원 클레임 API 호출용 bearer token으로 쓰지 않습니다
Access Token Resource Server/API API 접근 권한 API는 issuer·audience·서명·만료를 검증합니다
Refresh Token Client 새 토큰 발급 장기 자격증명에 가까우므로 노출·저장 통제를 강화합니다

JWT의 payload는 Base64 디코딩으로 읽을 수 있지만, 이것만으로 발급자를 신뢰할 수는 없습니다. 서명을 검증하지 않으면 공격자가 만든 토큰도 받아들일 수 있기 때문입니다. 이 실습의 디코딩은 클레임을 관찰하기 위한 과정이며, 실제 애플리케이션에서는 검증된 OIDC 라이브러리를 사용해야 합니다.

Authorization Code Flow with PKCE

PKCE(Proof Key for Code Exchange)는 인증 요청을 시작한 클라이언트만 authorization code를 토큰으로 교환할 수 있게 합니다. 클라이언트는 임의의 code_verifier를 만들고 그 해시인 code_challenge만 먼저 보냅니다. 토큰 교환 때 원본 verifier를 제시하면 Keycloak이 둘의 관계를 확인합니다.

단계별 흐름 / 03OIDC Authorization Code와 PKCE
ClientPKCE 준비·토큰 검증
01 / 06
PKCE 값 준비

Client가 code_verifier를 만들고 SHA-256으로 code_challenge를 계산합니다.

1

Client → Clientcode_verifier 생성

2

Client → Clientcode_challenge 계산

단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.

전체 단계 한눈에 읽기
  1. PKCE 값 준비

    Client가 code_verifier를 만들고 SHA-256으로 code_challenge를 계산합니다.

    • Client → Client: code_verifier 생성
    • Client → Client: code_challenge 계산
  2. 인증 요청 보내기

    Client가 브라우저에서 authorize URL을 엽니다. 요청에는 client_id, redirect_uri, state, challenge가 포함됩니다.

    • Client → 사용자 브라우저: authorize URL 열기
    • 사용자 브라우저 → Keycloak: 인증 파라미터
  3. 로그인 후 인증 코드 수신

    Keycloak에서 로그인·MFA를 마치면 브라우저가 authorization code와 state를 받습니다.

    • Keycloak → 사용자 브라우저: 로그인·MFA
    • Keycloak → 사용자 브라우저: authorization code + state
  4. 등록된 callback으로 복귀

    브라우저는 정확히 등록된 callback으로 Client에 돌아갑니다. 인증 코드는 아직 Access Token이 아닙니다.

    • 사용자 브라우저 → Client: 등록된 callback으로 이동
  5. 코드를 토큰으로 교환

    Client가 code와 code_verifier를 제출하면 Keycloak이 challenge와 verifier를 검증합니다.

    • Client → Keycloak: code + code_verifier
    • Keycloak → Keycloak: PKCE 검증
  6. 토큰을 받은 뒤에도 검증

    Client는 ID Token과 Access Token을 받은 뒤 issuer·audience·서명·만료·nonce 등 필요한 검증을 수행합니다.

    • Keycloak → Client: ID Token + Access Token
    • Client → Client: 토큰 검증
도식 원문
sequenceDiagram
  participant U as 사용자 브라우저
  participant C as Client
  participant K as Keycloak

  C->>C: code_verifier 생성
  C->>C: SHA-256 → code_challenge
  C->>U: authorize URL 열기
  U->>K: client_id, redirect_uri, state, challenge
  K->>U: 로그인·MFA
  K-->>U: authorization code와 state
  U-->>C: 정확히 등록된 callback으로 이동
  C->>K: code + code_verifier로 토큰 요청
  K->>K: challenge와 verifier 검증
  K-->>C: ID Token + Access Token
  C->>C: issuer·audience·서명·만료·nonce 검증

state는 인증 응답이 자신이 시작한 요청과 연결되는지 확인해 CSRF를 막는 데 쓰입니다. nonce는 ID Token 재사용 공격 방어에 사용합니다. 프레임워크가 값을 생성하고 검증하도록 맡기되, 세션 저장소와 쿠키 보안 설정을 정확히 구성해야 합니다.

OIDC와 SAML 2.0 비교

SAML 2.0은 XML 기반의 성숙한 페더레이션 표준이고, OIDC는 JSON/JWT와 HTTP API 생태계에 맞춰진 표준입니다. “새것과 낡은 것”이라는 구분보다는 애플리케이션의 지원 범위와 기존 인증 운영 방식에 맞춰 선택해야 합니다.

관점 OIDC SAML 2.0
주요 메시지 ID Token·Access Token, JSON/JWT SAML Assertion, XML
대표 참여자 OP(Keycloak)·RP(Client) IdP(Keycloak)·SP(애플리케이션)
설정 발견 Discovery와 JWKS IdP/SP 메타데이터 XML과 인증서
주 사용처 웹, SPA, 모바일, CLI, API 브라우저 기반 엔터프라이즈 웹 SSO
API 권한 위임 OAuth 2.0과 자연스럽게 결합 주 목적이 아님
디버깅 HTTP/JSON 도구가 편리 XML 서명·binding·metadata 이해 필요
Keycloak 매핑 Protocol Mapper로 claim 생성 SAML Mapper로 attribute 생성

OIDC Authorization Code Flow는 브라우저로 인증한 뒤 Client가 token endpoint와 백채널 통신해 토큰을 받습니다. SAML의 대표적인 SP-initiated Web Browser SSO는 SP가 AuthnRequest를 보내고 브라우저가 서명된 SAML Response를 ACS(Assertion Consumer Service)로 POST합니다.

단계별 흐름 / 04SAML 로그인과 응답 검증
브라우저인증 요청과 응답 전달
Service Provider보호된 서비스·ACS
01 / 05
보호된 서비스에 접근

브라우저가 Service Provider의 보호된 페이지를 요청합니다.

1

브라우저 → Service Provider보호된 페이지 요청

단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.

전체 단계 한눈에 읽기
  1. 보호된 서비스에 접근

    브라우저가 Service Provider의 보호된 페이지를 요청합니다.

    • 브라우저 → Service Provider: 보호된 페이지 요청
  2. 인증 요청을 IdP로 전달

    서비스가 SAML AuthnRequest로 리다이렉트하면 브라우저가 Keycloak IdP로 전달합니다.

    • Service Provider → 브라우저: SAML AuthnRequest 리다이렉트
    • 브라우저 → Keycloak IdP: AuthnRequest 전달
  3. Keycloak에서 로그인

    사용자는 Keycloak의 로그인 절차를 거칩니다.

    • Keycloak IdP → 브라우저: 로그인
  4. 브라우저가 SAML 응답 전달

    Keycloak의 서명된 SAML Response는 브라우저를 거쳐 서비스의 ACS에 HTTP POST로 전달됩니다.

    • Keycloak IdP → 브라우저: 서명된 SAML Response
    • 브라우저 → Service Provider: ACS에 HTTP POST
  5. 서비스가 응답 검증

    Service Provider는 issuer·서명·audience·시간 조건을 확인합니다. 응답을 받았다는 사실만으로 접근을 허용하지 않습니다.

    • Service Provider → Service Provider: SAML 응답 검증
도식 원문
sequenceDiagram
  participant U as 브라우저
  participant S as Service Provider
  participant K as Keycloak IdP
  U->>S: 보호된 페이지 요청
  S-->>U: SAML AuthnRequest로 리다이렉트
  U->>K: AuthnRequest 전달
  K->>U: 로그인
  K-->>U: 서명된 SAML Response
  U->>S: ACS에 HTTP POST
  S->>S: issuer·서명·audience·시간 조건 검증

선택권이 있는 신규 클라우드 네이티브 애플리케이션에는 OIDC를 우선 고려합니다. 애플리케이션이 SAML만 지원하거나 기존 SAML 메타데이터·인증서 운영 체계가 요구사항이면 SAML을 선택합니다. Keycloak은 한 Realm 안에서도 두 프로토콜의 Client를 함께 운영할 수 있습니다.

실습: OIDC public client와 PKCE 토큰 교환

1. 실습 Realm과 사용자 준비

Keycloak Admin Console에서 다음 순서로 설정합니다.

  1. 우측 상단 Realm 메뉴에서 Create realm을 선택하고 이름을 mlops로 지정합니다.
  2. Users → Add user에서 Username을 student-01로 만듭니다.
  3. 사용자의 Credentials 탭에서 실습자가 정한 임시 비밀번호를 설정합니다.
  4. 운영 환경에서는 MFA, 비밀번호 정책, brute-force detection을 먼저 설계합니다. 이 글에 비밀번호를 기록하지 않습니다.

master Realm이 아니라 방금 만든 mlops Realm을 선택했는지 확인합니다.

2. public OIDC Client 생성

Clients → Create client에서 다음 값을 사용합니다.

설정 값
Client type OpenID Connect
Client ID oidc-lab-cli
Client authentication Off
Standard flow On
Direct access grants Off
Valid redirect URIs http://127.0.0.1:8085/callback
Web origins 비워 둠
PKCE method S256

Keycloak은 등록된 리다이렉트 URI로 인증 결과를 보냅니다. 여기에 넓은 와일드카드를 사용하면 code가 공격자 위치로 전달될 수 있으므로, 이 실습에서는 *를 넣지 않고 정확한 URI를 등록합니다.

3. Discovery 문서 확인

bash
export KEYCLOAK_URL='https://sso.lab.example.com'
export REALM='mlops'
export CLIENT_ID='oidc-lab-cli'
export REDIRECT_URI='http://127.0.0.1:8085/callback'

curl --fail --silent --show-error \
  "${KEYCLOAK_URL}/realms/${REALM}/.well-known/openid-configuration" \
  | python3 -m json.tool

응답의 issuer가 ${KEYCLOAK_URL}/realms/${REALM}와 정확히 같아야 합니다. 또한 authorization_endpoint, token_endpoint, jwks_uri가 표시됩니다. 애플리케이션은 일반적으로 issuer 하나를 받아 이 문서를 자동 조회합니다.

SAML 메타데이터가 필요하면 다음 엔드포인트를 사용합니다.

bash
curl --fail --silent --show-error \
  "${KEYCLOAK_URL}/realms/${REALM}/protocol/saml/descriptor" \
  | head

4. verifier, challenge, state, nonce 생성

bash
export CODE_VERIFIER="$(openssl rand -base64 64 | tr -d '=+/' | cut -c1-64)"
export CODE_CHALLENGE="$(printf '%s' "${CODE_VERIFIER}" \
  | openssl dgst -binary -sha256 \
  | openssl base64 -A \
  | tr '+/' '-_' \
  | tr -d '=')"
export OIDC_STATE="$(openssl rand -hex 16)"
export OIDC_NONCE="$(openssl rand -hex 16)"

printf 'state=%s\nnonce=%s\nchallenge=%s\n' \
  "${OIDC_STATE}" "${OIDC_NONCE}" "${CODE_CHALLENGE}"

터미널을 하나 더 열고 콜백을 받을 간단한 로컬 서버를 실행합니다.

bash
python3 -m http.server 8085 --bind 127.0.0.1

다음 명령은 브라우저에 붙여 넣을 authorize URL을 출력합니다.

bash
python3 - <<'PY'
import os
from urllib.parse import urlencode

base = f"{os.environ['KEYCLOAK_URL']}/realms/{os.environ['REALM']}/protocol/openid-connect/auth"
query = urlencode({
    "client_id": os.environ["CLIENT_ID"],
    "response_type": "code",
    "scope": "openid profile email",
    "redirect_uri": os.environ["REDIRECT_URI"],
    "state": os.environ["OIDC_STATE"],
    "nonce": os.environ["OIDC_NONCE"],
    "code_challenge": os.environ["CODE_CHALLENGE"],
    "code_challenge_method": "S256",
})
print(f"{base}?{query}")
PY

출력된 URL을 브라우저에서 열고 student-01로 로그인합니다. 로컬 서버는 /callback 파일을 찾지 못해 404를 반환할 수 있지만, 주소 표시줄에는 다음 형태의 쿼리가 남습니다.

text
http://127.0.0.1:8085/callback?state=...&session_state=...&code=...

주소의 state가 앞에서 만든 ${OIDC_STATE}와 같은지 먼저 확인합니다. 그다음 code 값만 현재 셸의 환경변수로 넣습니다. authorization code는 짧게 만료되고 일회용이므로 즉시 교환합니다.

bash
export AUTHORIZATION_CODE='<브라우저 콜백의 code 값>'

TOKEN_RESPONSE="$(curl --fail --silent --show-error \
  -X POST \
  "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "client_id=${CLIENT_ID}" \
  --data-urlencode "redirect_uri=${REDIRECT_URI}" \
  --data-urlencode "code=${AUTHORIZATION_CODE}" \
  --data-urlencode "code_verifier=${CODE_VERIFIER}")"

printf '%s' "${TOKEN_RESPONSE}" \
  | python3 -c 'import json,sys; d=json.load(sys.stdin); print({k:d.get(k) for k in ("token_type","expires_in","scope")})'

토큰 원문은 출력하거나 셸 히스토리에 남기지 않습니다. ID Token의 payload만 학습 목적으로 확인합니다.

bash
printf '%s' "${TOKEN_RESPONSE}" | python3 -c '
import base64
import json
import os
import sys

data = json.load(sys.stdin)
payload = data["id_token"].split(".")[1]
payload += "=" * (-len(payload) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload))
for key in ("iss", "aud", "sub", "preferred_username", "exp", "nonce"):
    print(f"{key}: {claims.get(key)}")
if claims.get("nonce") != os.environ["OIDC_NONCE"]:
    raise SystemExit("nonce mismatch")
'

위 코드는 클레임을 관찰만 합니다. 서명을 검증하지 않습니다. 실제 애플리케이션에서는 검증된 OIDC 라이브러리로 Discovery와 JWKS를 사용해 iss, aud, exp, nonce, 서명 알고리즘을 검증합니다.

5. PKCE가 실제로 code 탈취를 막는지 확인

이미 사용한 code를 다시 보내거나 다른 verifier로 보내면 토큰 엔드포인트가 실패해야 합니다.

bash
curl --silent --show-error \
  -X POST \
  "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "client_id=${CLIENT_ID}" \
  --data-urlencode "redirect_uri=${REDIRECT_URI}" \
  --data-urlencode "code=${AUTHORIZATION_CODE}" \
  --data-urlencode 'code_verifier=wrong-verifier'

이 요청에서는 invalid_grant 계열 오류가 반환되어야 합니다. 재사용한 code나 잘못된 verifier로 토큰을 발급받지 못하는지 확인하는 실습이므로, 토큰 교환이 거절되는 것이 정상적인 결과입니다.

검증 체크리스트

  • Discovery의 issuer가 애플리케이션에 설정한 issuer와 한 글자도 다르지 않습니다.
  • authorization endpoint에서 로그인 후 정확히 등록한 callback으로 돌아옵니다.
  • callback의 state가 요청 전에 생성한 값과 일치합니다.
  • ID Token의 nonce가 인증 요청 전에 생성한 값과 일치합니다.
  • 올바른 verifier로 code를 한 번만 교환할 수 있습니다.
  • ID Token의 iss, aud, preferred_username, exp가 예상과 일치합니다.
  • 다른 verifier 또는 재사용한 code로는 토큰 발급이 거절됩니다.
  • SAML descriptor 엔드포인트가 XML 메타데이터를 반환합니다.

자주 만나는 문제

증상 가능한 원인 확인 및 해결
Invalid parameter: redirect_uri 등록값과 요청값 불일치 scheme, host, port, path, 마지막 /까지 정확히 비교합니다
로그인 후 로컬 페이지가 404 단순 HTTP 서버가 callback 파일을 모름 주소 표시줄의 code와 state를 확인하는 실습상 정상 동작입니다
invalid_grant code 만료·재사용 또는 verifier 불일치 authorize 요청부터 다시 시작하고 같은 셸의 verifier를 씁니다
issuer mismatch Realm 또는 외부 URL 설정 오류 Discovery의 issuer를 기준으로 애플리케이션 설정을 맞춥니다
토큰에 사용자 이름이 없음 scope/mapper 부족 profile scope와 해당 mapper를 확인합니다
토큰에 그룹이 없음 Group Membership Mapper 미설정 다음 글의 groups client scope와 mapper 절차를 적용합니다
서버 간 TLS 오류 사설 CA 신뢰 체인 누락 TLS 검증을 끄지 말고 Keycloak/애플리케이션 trust store에 CA를 배포합니다
로그인은 되지만 API가 401 ID Token을 API에 보냄 또는 audience 불일치 API에는 Access Token을 보내고 Resource Server의 audience 검증을 확인합니다

운영과 보안에서 놓치기 쉬운 것

리다이렉트 URI는 정확하게 제한한다

https://app.lab.example.com/*보다 실제 callback 경로 하나를 등록합니다. 개발용 localhost URI와 운영 URI는 가능하면 Client를 분리합니다. Web Origins도 무조건 *로 열지 않습니다.

public client와 confidential client를 구분한다

브라우저·모바일·CLI에는 secret을 넣지 않고 PKCE S256을 강제합니다. 서버가 secret을 보관하는 경우에는 Kubernetes Secret 또는 외부 비밀 관리 시스템을 사용하고, Git과 일반 ConfigMap에서 제외합니다. 가능한 경우 client secret rotation 정책도 운영 절차에 포함합니다.

토큰의 수명과 세션 수명은 함께 설계한다

Access Token을 지나치게 길게 발급하면 탈취 시 피해 시간이 늘어납니다. 너무 짧으면 갱신 부하와 사용자 경험 문제가 생깁니다. SSO Session Idle/Max, Client Session, Access Token, Refresh Token 수명을 애플리케이션 특성에 맞춰 함께 조정합니다.

토큰에는 최소 클레임만 넣는다

그룹 전체 경로나 개인정보를 습관적으로 싣지 않습니다. 애플리케이션별 Client Scope로 필요한 클레임만 허용하고, 로그 수집기와 프록시가 Authorization 헤더나 토큰을 기록하지 않도록 필터링합니다.

가용성과 복구 절차를 인증 설계에 포함한다

운영 Keycloak에는 외부 데이터베이스와 복수 인스턴스를 준비하고, 프록시 헤더·상태 점검·백업 복구 훈련을 함께 구성해야 합니다. Pod replica를 늘려도 데이터베이스 장애나 Realm 설정의 잘못된 삭제까지 복구할 수는 없기 때문입니다. 관리 콘솔은 업무용 로그인 주소보다 더 엄격하게 접근을 제한합니다.

로그아웃을 과신하지 않는다

브라우저의 Keycloak SSO 세션, 애플리케이션 로컬 세션, 이미 발급된 Access Token은 서로 수명이 다를 수 있습니다. Single Logout 호환성과 토큰 취소 정책을 실제 Client별로 검증합니다. 퇴사 처리에서는 사용자 비활성화뿐 아니라 활성 세션 종료와 고위험 토큰의 짧은 수명이 함께 필요합니다.

요약

  • Keycloak은 Realm이라는 격리 경계 안에서 사용자·그룹·역할·Client·세션을 관리하는 중앙 IAM입니다.
  • SSO는 비밀번호를 여러 애플리케이션에 공유하는 구조가 아니라, 중앙 인증 결과를 서명된 프로토콜 메시지로 전달하는 구조입니다.
  • OIDC는 OAuth 2.0에 인증 계층을 추가하며, ID Token과 Access Token의 소비자를 구분해야 합니다.
  • public client는 secret을 가질 수 없으므로 Authorization Code Flow with PKCE S256을 사용합니다.
  • 신규 웹·CLI·API 통합에는 OIDC가 자연스럽고, SAML만 지원하는 엔터프라이즈 애플리케이션이나 기존 SAML 운영 요건에는 SAML을 사용합니다.
  • 인증은 Keycloak이 담당하지만 최종 인가는 애플리케이션이 자체 정책으로 결정합니다.

다음 글에서는 토큰에 groups를 넣고, Argo CD가 그 값을 읽어 프로젝트별로 필요한 권한만 부여하도록 구성합니다.

← 이전 글: Kubernetes 스토리지 입문 — PV/PVC부터 NFS CSI와 RWX까지
→ 다음 글: Keycloak에서 Argo CD 권한까지: OIDC 그룹과 프로젝트별 RBAC

공식 참고자료