데브옵스 이야기/Kubernetes

kubernetes 인증/인가 - OIDC (로컬 환경 실습)

lakescript 2026. 3. 13. 19:22
728x90

지난 글에서는 k8s의 인증(Authentication)과 인가(Authorization)에 대해 개념을 공부하고 실습을 진행했습니다.
 
하지만 실제 업무에서는 개발자가 `kubectl`로 pod에 접근하거나 Devops 엔지니어가 `cluster` 상태를 확인한다거나, 여러 `cluster`를 관리한다거나 처럼 사람이 k8s에 접근하는 경우가 많습니다.
 
이때, k8s는 OIDC(OpenID Connect)를 이용하여 외부 인증 시스템과 연동할 수 있습니다. 
 

OIDC(OpenID Connect)

 
 
`OIDC`는 `OAuth 2.0` 기반의 인증 프로토콜 입니다. 
 

OAuth 2.0과 OIDC의 관계

`OIDC`를 이해하려면 먼저 `OAuth 2.0` 을 알아야 합니다.

`OAuth 2.0`은 인가 프로토콜로,  "이 앱이 내 구글 캘린더에 접근해도 좋다"처럼 리소스 접근 권한을 위임하는 것이 목적입니다. 하지만 "지금 로그인한 사람이 누구인가?"는 알려주지 않습니다.
 
여기서 `OIDC`는 `OAuth 2.0` 위에 인증 레이어를 얹은 프로토콜인데,  `OAuth 2.0 Authorization Code Flow`를 그대로 사용하면서, 응답에 `ID Token(JWT)` 을 추가로 발급합니다. 그리고 이 `JWT Token`에  `OAuth2.0` 프로토콜에서 알 수 없었던 "누가 로그인했는지"에 대한 정보가 담기게 됩니다.
 

- OAuth 2.0 → 인가 (Authorization) : "무엇을 할 수 있는가"
- OIDC         → 인증 (Authentication) : "누구인가" + OAuth 2.0

 

OIDC Flow

여기서 중요한 점은 k8s가 직접 로그인을 처리하지 않는다는 것입니다.
 

로그인 → IdP
권한 → Kubernetes

 
즉, k8s는 IdP를 통해 발급된 JWT 토큰을 검증하는 역할만 합니다.
 

토큰 정보(claim)

{
  "iss": "http://127.0.0.1:5556",
  "sub": "CmMxMjMSBWxvY2Fs",
  "aud": "kubernetes",
  "exp": 1773479527,
  "iat": 1773393127,
  "email": "admin@example.com"
}

 
OIDC 인증 후 받은 JWT 토큰을 디코딩하면 위와 같은 정보를 볼 수 있는데, 이를 `claim`이라고 합니다. 
 

Claim 설명
iss (Issuer) 토큰을 발급한 OIDC 제공자(IdP) 를 의미합니다.

`--oidc-issuer-url` 설정과 일치하는지 확인하여 토큰을 검증합니다.
sub (Subject) 사용자의 고유 식별자입니다.

IdP 내부에서 사용자를 구분하기 위한 값으로, 일반적으로 변하지 않는 ID입니다.
email 사용자의 이메일 정보입니다.

`--oidc-username-claim` 설정을 통해 사용자 이름으로 사용할 수 있습니다.
groups 사용자가 속한 그룹 정보입니다.

`--oidc-groups-claim` 설정을 통해 RBAC 그룹 권한과 매핑할 수 있습니다.
aud (Audience) 이 토큰이 어떤 서비스(Client)를 대상으로 발급되었는지를 나타냅니다.

`--oidc-client-id` 값과 일치해야 합니다.

 
즉, OIDC의 claim은 토큰 내부에 포함된 사용자 및 토큰 메타데이터로, k8s는 iss, aud 등을 통해 토큰을 검증하고 email, groups 등을 이용해 사용자와 RBAC 권한을 매핑합니다.
 

OIDC 도구

대표적인 OIDC 도구들은 아래와 같습니다.

도구  설명
Dex Kubernetes 환경에서 많이 사용하는 경량 OIDC Provider
링크
Keycloak RedHat에서 개발한 오픈소스 IdP
링크
Okta SaaS 형태의 상용 IdP 서비스
링크
Google / Azure AD 클라우드 계정 기반 OIDC 인증 제공

 

Dex

 
 
 
`Dex`는 `OpenID Connect`를 사용하여 다른 앱의 인증을 지원하는 경량화 오픈소스입니다. `Google`, `GitHub` 등 다양한 `IdP` 앞에 위치하여 `Kubernetes`가 이해할 수 있는 `JWT` 토큰으로 변환해주는 역할을 합니다.
 
 

실습 - Dex를 활용한 OIDC 인증

로컬 실행이 간편하고, static password 지원하기 때문에 `Google` 계정이나 `LDAP` 없이도 동작하기 때문에 `Dex`를 통한 `OIDC` 실습을 진행합니다. 
 

인증서 구성

`kube-apiserver`는 `--oidc-issuer-url` `HTTPS`여야만 정상적으로 동작합니다. (`cluster`를 생성할 때 `HTTP`라면 생성이 안됩니다.) 그렇기 때문에 `Dex`를 `HTTPS`로 띄워야 하는데, 로컬 환경에서는 `Let's Encrypt` 같은 공인 `CA`에서 인증서를 받을 수 없습니다. (`host.docker.internal`은 공인 도메인이 아니기 때문)
 
그렇기 때문에 local에서 실습을 하기 위해선 직접 `CA`를 만들고, 그 `CA`로 `Dex` 서버 인증서에 서명하는 방식을 써야 합니다.
 

인증서 디렉토리 생성

mkdir ./certs

 

CA 키 생성

서버 인증서에 서명할 때 사용하기 위해 `ca.key`를 생성합니다.

openssl genrsa -out certs/ca.key 2048

 
`genrsa` 명령을 통해 `RSA` 개인키를 생성하고, 로컬 실습용이기 때문에 길이는 2048로 설정합니다.
 

자체 서명된 CA 인증서 생성

`ca.key`로 서명한 인증서를 신뢰할 수 있게 하는 `ca.crt`를 생성합니다.

openssl req -x509 -new -nodes \
  -key certs/ca.key \
  -sha256 -days 3650 \
  -subj "/CN=dex-ca" \
  -out certs/ca.crt

 

Dex 서버 개인키 생성

openssl genrsa -out certs/tls.key 2048

 
`Dex`가 `HTTPS` 통신에 사용할 개인키로, `CA`와 동일한 방식으로 생성합니다.
 

CSR 생성

생성한 공개키에 대한 인증서를 발급하기 위한 `CSR`을 생성합니다.

openssl req -new \
  -key certs/tls.key \
  -subj "/CN=host.docker.internal" \
  -out certs/tls.csr

 
`/CN=host.docker.internal` 을 통해 `docker` 기반의 `kind`에서 해당 도메인을 인증서에 사용하도록 합니다.
 

SAN 파일 생성

cat > certs/tls.ext <<EOF
[v3_req]
subjectAltName = DNS:host.docker.internal,IP:127.0.0.1
EOF

 
브라우저와 `TLS` 클라이언트는 `CN` 대신 `SAN`을 기준으로 도메인을 검증 합니다.
 

CA로 서버 인증서 서명

openssl x509 -req \
  -in certs/tls.csr \
  -CA certs/ca.crt \
  -CAkey certs/ca.key \
  -CAcreateserial \
  -out certs/tls.crt \
  -days 3650 \
  -sha256 \
  -extfile certs/tls.ext \
  -extensions v3_req

 
위에서 만든 `SAN` 파일을 적용하여 생성합니다.
 

서명 검증 확인

openssl verify -CAfile certs/ca.crt certs/tls.crt

 
`verify` 명령을 통해 생성한 인증서들에 대한 검증을 진행합니다.
 

 
이 검증이 통과해야 `kube-apiserver`와 `kubelogin`이 `Dex` 인증서를 신뢰할 수 있습니다.
 

Kind로 Cluster 생성

kind 설정 파일

cat > kind-oidc.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  image: kindest/node:v1.34.0
  extraMounts:
  - hostPath: ./certs/ca.crt
    containerPath: /etc/kubernetes/pki/dex-ca.crt
    readOnly: true
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        oidc-issuer-url: "https://host.docker.internal:5556"
        oidc-client-id: "kubernetes"
        oidc-username-claim: "email"
        oidc-groups-claim: "groups"
        oidc-ca-file: "/etc/kubernetes/pki/dex-ca.crt"
- role: worker
  image: kindest/node:v1.34.0
EOF

 
여기서 특이한 점은 `nodes.extraMounts` 부분입니다. 
 
`kube-apiserver`가 `Dex`의 `self-signed` 인증서를 신뢰하려면`CA`가 필요하기 때문에 위에서 생성한 CA 인증서를 control plane 안으로 마운트 시킵니다.
 

kind cluster 생성

kind create cluster --config kind-oidc.yaml --name oidc-study

 
위에서 생성한 kind config 파일로 oidc-study 라는 cluster를 생성합니다.
 

 

node 확인

kubectl get nodes

 

 

plugin 설치

OIDC 로그인은 `kubectl` 기본 기능이 아니라 외부 인증 플러그인 방식으로 동작하기 때문에 `external credential plugin` 이 필요합니다.
 

인증 방식

k8s에서 `kubectl`이 `api server` 에 요청을 할 때는 반드시 인증 정보가 필요합니다.

인증 방식 동작
client certificate kubeconfig에 인증서 저장
serviceaccount token token 사용
static token kubeconfig token
OIDC external credential plugin 필요

 
OIDC의 경우 사용자가 IdP에 로그인해야 JWT 토큰을 발급받을 수 있기 때문에 `kubectl` 내부에서 직접 처리하지 않습니다. 그렇기 때문에 `external credential plugin` 이 필요합니다.
 

kubelogin plugin 설치

kubelogin(kubectl-oidc-login)은 int128/kubelogin에서 개발된 오픈소스 kubectl plugin입니다.
 
`OIDC` 인증에서 브라우저 로그인 → `Authorization Code` 교환 → Token 발급 과정을 자동화해주며, `kubectl`은 `exec credential plugin` 방식으로 이 플러그인을 호출하여, 토큰을 `stdout JSON`으로 반환합니다.

brew install kubelogin

 

plugin 확인

kubectl plugin list

 

 

Dex 생성

namespace 생성

kubectl create namespace dex

 

TLS Secret 생성

kubectl create secret tls dex-tls \
  --cert=certs/tls.crt \
  --key=certs/tls.key \
  -n dex

 
위에서 생성한 인증서 파일을 컨테이너 이미지에 포함시키거나 `configMap`에 평문으로 넣지 않기 위해서 `secret`을 생성합니다.

 

Dex configmap 파일 생성

cat > dex-config.yaml << 'EOF'
issuer: https://host.docker.internal:5556

storage:
  type: memory

web:
  http: 0.0.0.0:5556

enablePasswordDB: true

staticClients:
- id: kubernetes
  redirectURIs:
  - 'http://localhost:8000'
  name: 'Kubernetes'
  secret: kubernetes-secret

staticPasswords:
- email: "admin@example.com"
  hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
  username: "admin"
  userID: "123"
EOF

 
`issuer`는 `kube-apiserver`의 `--oidc-issuer-url`과 반드시 동일해야 합니다.
 
추가로 위로 생성된 dex의 인증 정보는 아래와 같습니다.

ID : admin@example.com
PW : password

 

Dex configmap 생성

kubectl create configmap dex-config \
--from-file=config.yaml=dex-config.yaml \
-n dex

 

 

Dex Pod 생성

cat > dex-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dex
  namespace: dex
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dex
  template:
    metadata:
      labels:
        app: dex
    spec:
      containers:
      - name: dex
        image: ghcr.io/dexidp/dex:v2.37.0
        command: ["dex", "serve", "/etc/dex/config.yaml"]
        ports:
        - containerPort: 5556
        volumeMounts:
        - name: config
          mountPath: /etc/dex
        - name: tls
          mountPath: /etc/dex/tls
          readOnly: true
      volumes:
      - name: config
        configMap:
          name: dex-config
      - name: tls
        secret:
          secretName: dex-tls

EOF

 

Dex 배포

kubectl apply -f dex-deployment.yaml

 

 

생성된 Dex Pod 확인

kubectl get pods -n dex

 

 

Dex Service 생성

cat > dex-service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
  name: dex
  namespace: dex
spec:
  ports:
  - port: 5556
    targetPort: 5556
  selector:
    app: dex
EOF

 

kubectl apply -f dex-service.yaml

 

 
 

RBAC 설정

Role 

cat > oidc-role.yaml << 'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: oidc-user
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get","list"]
EOF

 
`default namespace`에서 `pods`를 `get`, `list`할 수 있는 `Role`을 생성하기 위해 `yaml` 파일을 생성합니다.
 

RoleBinding

cat > oidc-rolebinding.yaml << 'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: oidc-binding
  namespace: default
subjects:
- kind: User
  name: admin@example.com
roleRef:
  kind: Role
  name: oidc-user
  apiGroup: rbac.authorization.k8s.io
EOF

 
위에서 생성한 `oidc-user`라는 `role`과 `kube-apiserver`가 `JWT`에서 추출한 `email claim`을 검증하여 권한을 적용하는 `rolebinding yaml` 파일을 생성합니다.
 

Role & Rolebinding 생성

kubectl apply -f oidc-role.yaml
kubectl apply -f oidc-rolebinding.yaml

 

Dex Port-forwad 설정

kubectl -n dex port-forward svc/dex 5556:5556

 
`kubelogin`이 local에서 `Dex` 에 접근하려면 port-forward가 필요하기 때문에 port-forward를 설정합니다.
 

/etc/hosts에 host.docker.internal 등록

sudo sh -c 'echo "127.0.0.1 host.docker.internal" >> /etc/hosts'

 

OIDC 로그인

kubectl oidc-login get-token \
--oidc-issuer-url=https://host.docker.internal:5556 \
--oidc-client-id=kubernetes \
--oidc-client-secret=kubernetes-secret \
--oidc-extra-scope=email \
--certificate-authority=$(pwd)/certs/ca.crt

 
위 명령어는 Dex 서버에 로그인해서 api server 인증에 사용할 jwt 토큰을 발급받는 명령입니다.
 
`--oidc-issuer-url=https://host.docker.internal:5556`
위 옵션은 로그인할 OIDC 서버 주소를 의미합니다. 여기서는 Dex의 port를 명시하여 실행합니다.
 
`--oidc-client-id=kubernetes`
OIDC에서는 사용자가 로그인할 때 어떤 애플리케이션이 인증을 요청했는지 구분해서 명시해야 합니다.
 

staticClients:
- id: kubernetes
  redirectURIs:
  - 'http://localhost:8000'
  name: 'Kubernetes'
  secret: kubernetes-secret

 
 
위에서 작성한 dex conifg 파일을 보면 위와 같이 `staticClients` 를 통해 클라이언트를 등록해놨었고, 옵션으로 해당 클라이언트ID를 명시했습니다.
 
`--oidc-client-secret=kubernetes-secret`
 
OIDC에서는 단순히 clientID만 전달하면 누구든지 그 ID를 사용할 수 있기 때문에 보안 문제가 발생할 수 있습니다. 그렇기 때문에 client secret을 함께 사용하여 인증을 진행합니다.
 
마찬가지로 위에서 설정했던 Dex Config 파일에 `secret: kubernetes-secret` 부분을 적어놨고, 옵션으로 해당 값을 입력합니다.
 

 
위와 같이 로그인 화면이 나왔다면, 위에서 생성했던 계정 정보를 통해 로그인을 합니다.

 
위와 같은 화면이 보일텐데, OIDC 허용에 관한 화면입니다. 
 
즉, `kubectl oidc-login get-token`이 Dex에 로그인한 뒤 이 클라이언트(kubernetes)가 토큰을 받아도 되는지 승인받는 단계입니다.
 

1. Dex가 인증 코드를 발급
2. 브라우저가 http://localhost:8000 쪽으로 리다이렉트
3. kubectl oidc-login이 그 코드를 받아 토큰으로 교환
4. 터미널에 토큰이 출력되거나, 다음 단계로 진행

 

 
인증이 완료되면 위와 같이 `Authenticated` 화면이 보이게 됩니다.

 
그리고 다시 터미널로 돌아와보면 `JWT` 이 발급된것을 확인할 수 있습니다. 
 

kubeconfig 등록

credentials 등록

kubectl config set-credentials oidc-user \
  --exec-api-version=client.authentication.k8s.io/v1 \
  --exec-command=kubectl \
  --exec-arg=oidc-login \
  --exec-arg=get-token \
  --exec-arg=--oidc-issuer-url=https://host.docker.internal:5556 \
  --exec-arg=--oidc-client-id=kubernetes \
  --exec-arg=--oidc-client-secret=kubernetes-secret \
  --exec-arg=--oidc-extra-scope=email \
  --exec-arg=--certificate-authority=$(pwd)/certs/ca.crt \
  --exec-interactive-mode=IfAvailable

 

`kubeconfig context` 에 연결된 사용자가 `exec` 방식으로 설정되어 있으면, `kubectl`은 API 요청 시점에 `kubectl oidc-login get-token` 명령을 실행하여 `OIDC Token`을  발급받을 수 있습니다.

 

 
결과적으로 `~/.kube/config`에 아래와 같이 저장됩니다.

 

context 등록 및 전환

kubectl config set-context oidc-context \
  --cluster=kind-oidc-study \
  --user=oidc-user

 

kubectl config use-context oidc-context

 

이후 모든 `kubectl` 명령이 `kind-oidc-study` 클러스터에 `oidc-user` 자격증명으로 실행됩니다.
 

로그인 테스트

default namespace

kubectl get pods -n default

 
로그인 완료 후 `kubectl`이 토큰을 받아 `kube-apiserver`에 요청을 보내고 정상적으로 결과가 반환된 것을 확인할 수 있습니다.
 

kube-system namespace

kubectl get pods -n kube-system

 
`RBAC`에서 해당 `user`에게 `kube-system namespace` 접근 권한을 주지 않았기 때문에 거부된 것을 확인할 수 있습니다.
 

728x90