SRE / DevOps 시작하기 (6) - Keyclaok
SRE / DevOps 시작하기 (6) - Keyclaok

Authrization
인증과 인가는 현대 웹 애플리케이션의 꽃이라 할 수 있다.
사실 MVP 단계의 제품을 운영하는 데 복잡한 인증/인가 프로토콜을 도입하는 것은, 어떻게 보면 불필요한 작업 중 하나다. 세션 기반 로그인 하나로도 충분히 굴러가는 제품이 훨씬 많다. 하지만 플랫폼 서비스는 다르다. 기획 단계에서부터 다중 테넌트, 역할별 권한 분리, 외부 서비스 연동이 전제로 깔리기 때문에, MVP 조차 단순한 인증/인가를 허용하지 않는 경우가 생긴다.
Bearer Token과 JWT는 "이 요청을 보낸 게 누구인가"를 상태 없이(stateless) 증명하기 위한 표준이다. 서버가 세션을 들고 있지 않아도 토큰 안의 서명만 검증하면 되므로, 수평 확장이 전제인 환경에서는 사실상 기본값에 가깝다. 문제는 이 표준들이 "형식"만 정의한다는 점이다. 토큰을 누가 발급하고, 언제 만료시키고, 어떻게 폐기할지는 여전히 우리 몫이다.
SSO와 OAuth 2.0은 그 "우리 몫"을 프로토콜 수준으로 끌어올린 결과물이다. 인증의 책임을 개별 애플리케이션에서 떼어내 별도의 신뢰 주체(Identity Provider)에게 위임한다. 서비스가 늘어날수록 이 구조의 값어치는 커진다. 새 서비스를 추가할 때마다 로그인 화면을 다시 만들 필요가 없어지기 때문이다.
NextAuth(Auth.js)나 Passport 같은 라이브러리는 이 프로토콜들을 애플리케이션 코드 안에서 구현하도록 도와준다. 편리하지만 한계가 명확하다. 인증 로직이 애플리케이션의 런타임에 묶여버린다는 것. 서비스가 셋으로 늘어나면 구현체도 셋이 되고, 정책을 바꾸려면 세 번 배포해야 한다.
Keycloak
Keycloak 은 OIDC(OpenID Connect)와 SAML 2.0을 구현한 오픈소스 IAM(Identity and Access Management) 솔루션이다. Realm으로 테넌트를 격리하고, Client 단위로 애플리케이션을 등록하며, Role과 Group으로 권한을 구성한다. 앞 문단에서 나열한 개념들이 하나의 관리 콘솔 안으로 들어온다고 보면 된다.
Docker
Keycloak - Docker
Database
우선 하나 이야기하고 시작하자.
전담 DBA나 보안 인력이 없는 팀이 회원정보 DB까지 VM 위에서 Self-Managed로 운영하겠다는 건, 웬만하면 미친 짓이다
회원정보 — 이메일 주소를 비롯한 개인정보가 저장되는 데이터베이스는 단순히 "데이터가 안 날아가게" 운영하는 것으로 끝나지 않는다. 암호화, 접근 통제, 백업, 장애 복구, 보안 패치, 감사 로그를 비롯하여 고려해야 할 것이 한두 가지가 아니다.
특히 Keycloak의 Database는 단순한 애플리케이션 데이터베이스가 아니다. User, Credential, Identity Provider 설정, Client, Role, Group 등 서비스의 인증/인가 체계를 구성하는 핵심 데이터가 저장된다. 이 DB가 날아가면 게시글 몇 개가 사라지는 수준으로 끝나지 않는다. 서비스 전체의 인증 체계가 같이 날아간다.
Keycloak은 MySQL, PostgreSQL, MariaDB, Microsoft SQL Server, Oracle Database 등 다양한 데이터베이스를 지원한다 — Documents.
Oracle Database 또한 지원하는데, 여기에는 몇 가지 문제가 생긴다.
우선 첫 번째로 우리가 마주치는 장벽은 JDBC Driver다.
Oracle은 JDBC Driver의 배포와 라이선스 정책에 상당히 민감하며, 여러 Open Source Project들이 그러하듯 Keycloak의 기본 Docker Image에도 Oracle JDBC Driver가 포함되어 있지 않다. 따라서 Oracle Database를 사용하려면 별도의 Provider/JDBC Driver를 추가한 Custom Image를 빌드해야 한다.
물론 이것 자체가 어려운 일은 아니다. Dockerfile 하나 추가하면 끝난다.
문제는 그 다음부터다.
OCI Autonomous Database(ADB)를 서비스 Database로 사용한다면 TLS 연결을 위한 Wallet, 추가 인증 정보, Secret 관리까지 따라온다. Wallet을 Container Image 안에 집어넣을 수도 없고, Git Repository에 올리는 것은 더더욱 안 된다. 결국 Docker Secret, Kubernetes Secret, Vault와 같은 별도의 Secret Management 체계를 함께 설계해야 한다.
즉, Keycloak 하나 띄우려고 했더니 Oracle JDBC Driver를 넣은 Custom Image를 만들고, Wallet을 배포하고, Secret을 관리하고, TLS 연결까지 구성해야 한다.
할 수 있느냐와 해야 하느냐는 다른 문제다.
Oracle Database를 반드시 사용해야 하는 조직적 또는 기술적 이유가 없다면 Keycloak 하나를 위해 이 복잡성을 떠안을 이유는 별로 없다.
제일 만만한 것은 역시 MySQL/PostgreSQL이다.
다만 MySQL은 Oracle의 라이선스 정책과 MariaDB를 비롯한 여러 호환 구현체 사이에서 장기적으로 미묘한 차이가 발생할 여지가 있다. 이미 MySQL 기반 인프라가 구축되어 있거나 MySQL의 특성이 반드시 필요한 경우가 아니라면, 새로운 Keycloak Deployment에서는 PostgreSQL이 상대적으로 무난한 선택이다.
그리고 여기서 굳이 PostgreSQL 서버까지 직접 운영할 필요도 없다.
Supabase와 Neon 모두 Managed PostgreSQL을 제공한다. Database Provisioning, TLS, Backup과 같은 귀찮은 운영 작업 상당 부분을 Provider에게 넘길 수 있다.
Keycloak 입장에서 이들은 특별한 Database가 아니다.
결국 PostgreSQL이다.
Connection String과 Credential을 받아 Keycloak에 넘겨주면 된다.
예를 들어 Docker Compose에서는 다음과 같이 구성할 수 있다.
services:
keycloak:
image: quay.io/keycloak/keycloak:latest
command: start
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
KC_DB_USERNAME: ${DB_USERNAME}
KC_DB_PASSWORD: ${DB_PASSWORD}
KC_HOSTNAME: auth.example.com
KC_PROXY_HEADERS: xforwarded
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
ports:
- "8080:8080"
물론 실제 Production 환경에서 .env 파일 하나에 Database Password와 Admin Password를 때려 넣고 끝내자는 이야기는 아니다.
개발 환경에서는 충분할 수 있지만, Production에서는 최소한 배포 플랫폼의 Secret 기능이나 별도의 Secret Manager를 사용하는 편이 낫다.
여기서 또 하나 주의할 점이 있다.
Keycloak Container는 Stateless하게 만들 수 있지만 Keycloak 자체가 Stateless한 것은 아니다.
Realm, User, Client, Role과 같은 영속 데이터는 Database에 저장된다. 따라서 Container가 죽었다 살아나는 것은 별 문제가 아니지만 Database가 사라지는 것은 전혀 다른 이야기다.
이 차이가 중요하다.
Keycloak을 VM 하나에 설치하고 내부 PostgreSQL까지 같은 VM에 올려버리면 처음에는 굉장히 편하다.
VM
├── Reverse Proxy
├── Keycloak
└── PostgreSQL
설치도 쉽고 비용도 싸다.
그러다 VM의 Disk가 죽는다.
인증 서버와 인증 DB가 동시에 죽는다.
더 재미있는 것은 이 순간 다른 서비스가 정상적으로 살아 있어도 신규 로그인과 Token Refresh가 막히면서 플랫폼 전체가 사실상 장애 상태로 들어갈 수 있다는 점이다.
그래서 적어도 다음 정도로 Failure Domain을 분리하는 편이 낫다.
Internet
│
▼
Reverse Proxy / Load Balancer
│
▼
Keycloak
│
│ TLS
▼
Managed PostgreSQL
이렇게 해두면 Keycloak Instance는 Disposable하게 취급할 수 있다.
죽으면 다시 띄우면 된다.
Container Image와 Configuration만 남아 있다면 새로운 Instance가 기존 Database를 바라보도록 하면 된다. 이후 트래픽이 증가하면 Keycloak Instance를 여러 개로 늘리는 것도 가능하다.
이것이 인증 서버를 별도의 서비스로 분리했을 때 얻는 중요한 장점 중 하나다.
애플리케이션은 더 이상 사용자의 Password를 알 필요가 없다.
Application Database에도 Password Hash를 저장하지 않는다.
사용자는 Keycloak에 인증하고, 애플리케이션은 Keycloak이 발급한 Token을 검증한다.
User
│
│ Login
▼
Keycloak ──────── PostgreSQL
│
│ Access Token
▼
Application
│
│ Bearer Token
▼
API
이제 각 Application의 책임은 상당히 단순해진다.
"이 사용자의 비밀번호가 맞는가?"를 판단하는 것이 아니라,
"이 Token을 내가 신뢰하는 Identity Provider가 발급했는가?"
를 판단하면 된다.
Realm
Keycloak에서 가장 먼저 이해해야 할 개념은 Realm이다.
Realm은 User, Credential, Role, Group, Client 등의 Namespace이자 보안 경계다. 서로 다른 Realm의 User와 Client는 기본적으로 서로 독립되어 있다.
예를 들어 SaaS 플랫폼을 운영한다고 해서 고객사마다 Realm을 하나씩 만드는 것이 항상 좋은 설계는 아니다.
Keycloak
├── Realm: company-a
├── Realm: company-b
├── Realm: company-c
└── Realm: company-d
처음에는 굉장히 깔끔해 보인다.
하지만 고객사가 수백 개가 되기 시작하면 이야기가 달라진다.
Realm마다 Client 설정, Identity Provider, Role Mapping, Theme, 정책을 관리해야 하고 공통 설정을 변경할 때마다 이를 어떻게 동기화할 것인지 고민해야 한다.
따라서 Realm을 곧바로 Tenant와 1:1 대응시키기 전에 정말 Identity Boundary 자체를 분리해야 하는가를 먼저 생각해야 한다.
단순히 고객사별 User와 Permission을 구분하는 것이 목적이라면 하나의 Realm 안에서 Group, Role, Organization 등의 기능을 이용하는 편이 더 적절할 수도 있다.
반대로 서로 다른 고객이 완전히 독립적인 Identity Provider를 사용하고, 인증 정책과 관리 권한까지 분리해야 한다면 Realm 분리가 의미를 가진다.
중요한 것은 Keycloak이 기능을 제공한다는 이유만으로 그 기능을 곧바로 Architecture에 대응시키지 않는 것이다.
Client
Realm을 만들었다면 다음은 Client다.
Client는 Keycloak을 사용하는 Application이라고 생각하면 쉽다.
Frontend, Backend, Admin Console, Internal Tool 등이 각각 별도의 Client가 될 수 있다.
Realm
├── Client: web
├── Client: api
├── Client: admin
└── Client: internal-tool
여기서부터 처음 이야기했던 OAuth 2.0과 OpenID Connect가 실제 Architecture 안으로 들어오기 시작한다.
사용자는 Application에 Password를 전달하지 않는다.
Application은 사용자를 Keycloak으로 Redirect하고, Keycloak에서 인증이 끝나면 Authorization Code를 전달받는다. Application은 이 Code를 Token으로 교환한다.
이른바 Authorization Code Flow다.
Browser
│
│ 1. Login
▼
Application
│
│ 2. Redirect
▼
Keycloak
│
│ 3. Authorization Code
▼
Application
│
│ 4. Code → Token
▼
Keycloak
그리고 이 순간부터 서비스의 인증 로직은 애플리케이션 코드에서 상당 부분 빠져나간다.
Password Policy를 바꾸기 위해 Frontend와 Backend를 다시 배포할 필요가 없다.
Google Login을 추가하기 위해 모든 서비스에 OAuth Library를 다시 붙일 필요도 없다.
MFA를 활성화한다고 애플리케이션의 Login Flow 전체를 뜯어고칠 필요도 없다.
Identity Layer에서 처리하면 된다.
이게 Keycloak을 쓰는 이유다.
Keycloak이 로그인 화면을 예쁘게 만들어줘서도 아니고, JWT를 쉽게 발급해줘서도 아니다.
인증과 인가를 Application Lifecycle에서 분리할 수 있기 때문이다.
서비스가 하나일 때는 이 차이가 별것 아닌 것처럼 보인다.
서비스가 열 개가 되면 이야기가 달라진다.
그리고 서비스가 열 개가 된 뒤에 인증 체계를 뜯어고치는 것보다, 앞으로 열 개가 될 가능성이 높은 서비스의 인증 경계를 처음부터 분리해두는 편이 훨씬 싸다.
물론 모든 MVP에 Keycloak이 필요하다는 이야기는 아니다.
로그인 하나 필요한 작은 서비스라면 여전히 Session 하나가 더 좋은 Architecture일 수 있다.
Infrastructure는 복잡할수록 좋은 것이 아니다.
필요한 복잡성을 애플리케이션 열 곳에 흩어놓을 것인지, 한 곳에 모아 관리할 것인지 선택하는 것이다.
Keycloak은 후자를 선택하기 위한 도구다.