
애플리케이션, 사용자, 역할 등이 묶여 격리된 환경을 제공하는 논리적 단위
사용자를 대신하여 Keycloak으로 인증/인가를 요청하는 애플리케이션
인증/인가에 필요한 정보를 토큰의 각 항목(클레임)에 매핑시키는 규칙들의 그룹
https://www.keycloak.org/guides


Manage realms > Create realm 이후 원하는 Realm Name을 기입하고 CreateKeycloak을 통해 인증/인가 과정을 수행하려는 애플리케이션을 Client로 등록해줘야 한다.

Clients > Create client
ArgoCD
Grafana
Airflow
Admins 라는 Group에 속해있는 사용자에게 각 서비스의 관리자 권한을 부여하도록 한다.
Groups > Create group
Client scopes > Create client scopeClient 생성 시에 해당 scope를 Default로 설정할지, Optional로 설정할지 결정한다


Mapper란 토큰의 클레임과 정보를 매핑시켜주는 것Mappers > Configure a new mapper
Client Secret 을 복사한 뒤 Base64로 인코딩하여 argocd-secret에 oidc.keycloak.clientSecret 값으로 기입argocd-cm 에 아래와 같이 oidc.config 추가argocd-rbac-cm에 아래와 같이 policy.csv 추가argocd-server Restart[dgyoon@kube-master ~]# echo -n '<Client Secret>' | base64
**<base64-encoded-value>**
[dgyoon@kube-master ~]# kubectl edit -n argocd secret argocd-secret
===
apiVersion: v1
kind: Secret
metadata:
labels:
app.kubernetes.io/name: argocd-secret
app.kubernetes.io/part-of: argocd
name: argocd-secret
namespace: argocd
type: Opaque
data:
...
oidc.keycloak.clientSecret: **<base64-encoded-value>**
...
===
[dgyoon@kube-master ~]# kubectl edit -n argocd cm argocd-cm
===
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd
name: argocd-cm
namespace: argocd
data:
...
oidc.config: |
name: keycloak
issuer: https://<KEYCLOAK WEB URL>/realms/<REALM NAME>
clientID: argocd
clientSecret: $oidc.keycloak.clientSecret
requestedScopes: ["openid","profile","email","groups"]
...
===
[dgyoon@kube-master ~]# kubectl edit -n argocd cm argocd-rbac-cm
===
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-rbac-cm
app.kubernetes.io/part-of: argocd
name: argocd-rbac-cm
namespace: argocd
data:
policy.csv: |
g, Admins, role:admin
===
[dgyoon@kube-master ~]# kubectl rollout restart deployment argocd-server -n argocdvalues.yaml 수정role_attribute_path 에 정의된 대로 groups 클레임에 Admins가 포함되어 있다면 Admin 권한 할당grafana.ini:
auth.generic_oauth:
enabled: true
name: Keycloak-OAuth
allow_sign_up: true
client_id: grafana
client_secret: <GRAFANA_CLIENT_SECRET>
scopes: openid email profile groups
email_attribute_path: email
login_attribute_path: username
name_attribute_path: full_name
auth_url: https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/protocol/openid-connect/auth
token_url: https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/protocol/openid-connect/token
api_url: https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/protocol/openid-connect/userinfo
role_attribute_path: contains(groups[*], 'Admins') && 'Admin' || contains(groups[*], 'editor') && 'Editor' || 'Viewer'
values.yaml 수정web:
webserverConfig:
enabled: true
stringOverride: |
import logging
from base64 import b64decode
import jwt
import requests
from cryptography.hazmat.primitives import serialization
from flask_appbuilder.security.manager import AUTH_OAUTH
from airflow.www.security import AirflowSecurityManager
log = logging.getLogger(__name__)
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_ROLES_SYNC_AT_LOGIN = True
AUTH_USER_REGISTRATION_ROLE = "Viewer"
OIDC_ISSUER = "https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>"
# Make sure you create these role on Keycloak
AUTH_ROLES_MAPPING = {
"Viewer": ["Viewer"],
"Admins": ["Admin"],
"User": ["User"],
"Public": ["Public"],
"Op": ["Op"],
}
OAUTH_PROVIDERS = [
{
"name": "keycloak",
"icon": "fa-key",
"token_key": "access_token",
"remote_app": {
"client_id": "airflow",
"client_secret": "<AIRFLOW_CLIENT_SECRET>",
"server_metadata_url": "https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/.well-known/openid-configuration",
"api_base_url": "https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/protocol/openid-connect",
"client_kwargs": {"scope": "email profile groups"},
"access_token_url": "https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/protocol/openid-connect/token",
"authorize_url": "https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/protocol/openid-connect/auth",
"request_token_url": None,
},
}
]
# Fetch public key
req = requests.get(OIDC_ISSUER)
key_der_base64 = req.json()["public_key"]
key_der = b64decode(key_der_base64.encode())
public_key = serialization.load_der_public_key(key_der)
class CustomSecurityManager(AirflowSecurityManager):
def get_oauth_user_info(self, provider, response):
if provider == "keycloak":
token = response["access_token"]
me = jwt.decode(token, public_key, algorithms=["HS256", "RS256"], audience="account")
groups = me.get("groups", [])
log.info("groups: {0}".format(groups))
if not groups:
groups = ["Viewer"]
userinfo = {
"username": me.get("preferred_username"),
"email": me.get("email"),
"first_name": me.get("given_name"),
"last_name": me.get("family_name"),
"role_keys": groups,
}
log.info("user info: {0}".format(userinfo))
return userinfo
else:
return {}
# Make sure to replace this with your own implementation of AirflowSecurityManager class
SECURITY_MANAGER_CLASS = CustomSecurityManager
Sign in via Keycloak 버튼을 통해 아이디/비밀번호를 입력해 로그인하면 다른 애플리케이션에서는 버튼 클릭만으로 로그인 가능


사용자가 Login Via Keycloak 버튼을 클릭하면, Keycloak Login 화면으로 리디렉션시키며, URL 상의 rediect_uri 에 클라이언트의 주소를 남김
https://<KEYCLOAK_WEB_URL>/realms/<REALM_NAME>/protocol/openid-connect/auth?**client_id=argocd-dev**&**redirect_uri=https%3A%2F%<**ARGOCD_WEB_URL**>%2Fauth%2Fcallback&response_type=code&scope=openid+profile+email+groups&state=rNsquawYZwluTEsfkbNemuld**
사용자가 Keycloak Login 화면에서 로그인에 성공한다면, redirect_uri에 Authorization Code 를 담아 다시 리디렉션시킴.
https://<ARGOCD_WEB_URL>/auth/callback?state=rNsquawYZwluTEsfkbNemuld&session_state=ef7339c1-5084-413b-9df3-c40e462853e2&iss=https%3A%2F%2Fkeycloak.testworks.dev%2Frealms%2Fddock-ddock&**code=4c1d60fa-9af3-4941-8346-828ccb754fdf.ef7339c1-5084-413b-9df3-c40e462853e2.5d7e0d08-1dcc-4735-aa88-1461f93e18d2**
/auth/callback API가 호출되어 Authorization Code를 Keycloak에서 Access Token과 ID Token으로 Exchange
POST /realms/ddock-ddock/protocol/openid-connect/token Authorization Code를 통해 발급받는 이유?redirect_uri로 전달하기 때문에 Access Token이 탈취될 위험이 있음Client Secret이 필요하기 때문에 Authorization Code 가 탈취당하더라도 토큰을 발급받을 수 없음(백엔드 서버만 소유)ArgoCD는 ID Token을 기반으로 argocd.token 세션 토큰을 생성하여 사용자 브라우저에 쿠키로 저장