Chap 13. 접근 제어

ka8main.png

쿠버네티스에서는 사용자의 인증/인가를 통해 아래 3단계를 통과해야 시스템에 접근할 수 있다..

  1. Authentication(인증): 접속한 사람의 신분을 인증하는 단계(누가 접근하는가).
  2. Authorization(인가): 어떤 권한을 가지고 어떤 자원에 어떤 행동을 할 수 있는지 확인하는 단계.
  3. Admission Control: 요청한 내용이 적절한지 확인하는 단계이다
    • LimitRange, ResourceQuota 기능이 Admission Control을 통해 Pod 요청이 적절한지 확인한 예시이다.

사용자 인증

쿠버네티스에는 크게 5가지 사용자 인증 방식이 존재한다.

  • HTTP Authentication: 토큰, 베이직 인증 사용
  • X.509 Certificate: 인증서 기반 사용자 인증
  • OpenID Connect: OAuth 기반 외부 인증
  • Webhook 인증: 외부 서버에 인증 위임
  • Proxy 인증: 프록시 서버가 인증 수행

HTTP Basic Authentication

Chap9에서 살펴본 HTTP Basic Auth 인증 방식과 마찬가지로 쿠버네티스 마스터에 사용자 인증을 받기 위해 Basic Auth를 사용할 수 있다.

그 전에, 먼저 KUBECONFIG 파일을 살펴보자. (KUBECONFIG 파일은 쿠버네티스 마스터 API 서버와 통신하기 위해 필요한 정보 - 마스터 서버 IP, 사용자 인증 정보 등 - 을 담고 있는 파일이다.)

cat $HOME/.kube/config

shell1

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: (생략)
    server: https://172.30.1.71:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: default
  user:
    password: (생략)
    username: (생략)
  • certificate-authority-data: 쿠버네티스 서버 인증서가 base64 인코딩되어 삽입되어 있음
  • cluster.server: 마스터 서버 IP:PORT 정보이다.
  • username: Basic Auth의 유저를 나타낸다.
  • password: Basic Auth의 비밀번호이다.
  • context: cluster와 user를 연결해주는 역할을 한다.

이제 username과 password 정보를 가지고 curl 헤더로 전송해서 쿠버네티스 마스터에 접근해보자

# 헤더 없이 접속
curl -kv https://172.30.1.71:6443/api
# ...생략
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/api\"",
  "reason": "Forbidden",
  "code": 403
}
# 헤더 없이 접속
curl -kv -H "Authorization: Basic $(echo -n admin:{password}... | base64)" \
  https://172.30.1.71:6443/api
# ...생략
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "172.30.1.71:6443"
    }
  ]
}

💡 k3s 로 클러스터를 구축한 경우 기본적으로 Basic Http 방식을 이용해서 인증을 처리한다.

이제 Basic Authentication에 사용자를 추가해보자

sudo vim /var/lib/rancher/k3s/server/cred/passwd

이 파일은 Basic Authentication에 사용할 사용자 정보를 정의하는 파일이다. 각 줄마다 하나의 사용자 계정을 나타내며, 형식은 다음과 같다:

사용자이름,비밀번호,사용자ID,그룹[]

예를 들어:

admin,admin123,admin,k3s:agent

위와 같이 설정하면 사용자 이름이 admin, 비밀번호가 admin123, 사용자 ID가 admink3s:agent 그룹에 속한 계정이 추가된다. 이 정보는 쿠버네티스 API 서버가 Basic Auth를 통해 사용자를 인증할 때 참조하게 된다.

이제 KUBECONFIG 파일 ($HOME/.kube/config)의 user정보(password, myuser)를 수정하고 클러스터를 재시작하면 해당 유저로 접근이 가능해진다.

💡 Group 중 system:masters 그룹은 쿠버네티스에서 특별한 의미를 가지는 예약어로, 역할을 부여하지 않아도 모든 권한을 수행할 수 있는 마스터 그룹이다.

X.509 인증서

쿠버네티스에서는 사용자 인증을 위해 X.509 인증서를 사용할 수도 있다.

HTTPS 통신을 하기 위해 인증서(X.509 Certificate)를 서버측에서 제공한다. 사용자는 서버에서 전달받은 인증서를 확인하고 서버의 신원을 확인한다.

이러한 원리를 동일하게 이용하여 사용자의 신원을 확인하기 위해 사용자 측에서 서버로 사용자의 인증서를 전달할 수 있다.

이때 쿠버네티스가 인증한 사용자 인증서를 사용해야하는데, CSR 문서를 이용하여 쿠버네티스가 보유한 CA(Certificate Authority)로 부터 서명을 받아야 한다.

💡 용어 정리
CA : 서명된 인증서를 발급하는 기관을 의미한다.
CSR(Certificate Signing Request) : 인증서 서명 요청으로 쿠버네티스가 보유한 CA로부터 인증서 서명을 받기위해 요청하는 문서이다.

사용자 인증서를 만들기 위해 cloudflare에서 만든 cfssl툴을 설치한다.

# cfssl 설치
sudo curl -L -o /usr/local/bin/cfssl https://github.com/cloudflare/cfssl/releases/latest/download/cfssl-linux-amd64
sudo chmod +x /usr/local/bin/cfssl

# cfssljson 설치
sudo curl -L -o /usr/local/bin/cfssljson https://github.com/cloudflare/cfssl/releases/latest/download/cfssljson-linux-amd64
sudo chmod +x /usr/local/bin/cfssljson

인증서 생성 방법은 다음과 같다.

  1. CSR 파일 생성
  2. CA로부터 인증서 서명
  3. 발급된 인증서(인증서 및 개인키)를 KUBDECONFIG 파일에 설정

이제 cfssl 툴에서 사용하는 json 형식의 CSR 파일을 생성하자.

cat > client-cert-csr.json <<EOF
{
  "CN": "client-cert",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "O": "system:masters"
    }
  ]
}
EOF

이제 생성한 CSR 파일을 이용해서 CA에 서명 요청을 한다. k3s의 경우 CA는 다음 위치에 존재한다.

  • 인증서: /var/lib/rancher/k3s/server/tls/client-ca.crt
  • 개인키: /var/lib/rancher/k3s/server/tls/client-ca.key

사용자 인증서를 발급하기 위한 CA config 파일을 생성한다.

cat > rootCA-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "root-ca": {
        "usages": ["signing", "key encipherment", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF

그리고 다음 명령을 이용해서 인증서를 발급받는다.

sudo cfssl gencert \
  -ca=/var/lib/rancher/k3s/server/tls/client-ca.crt \
  -ca-key=/var/lib/rancher/k3s/server/tls/client-ca.key \
  -config=rootCA-config.json \
  -profile=root-ca \
  client-cert-csr.json | cfssljson -bare client-cert

그럼 이제 아래 파일들이 생긴것을 볼 수 있다.

  • 사용자 인증서(≈아이디): client-cert.pem
  • 사용자 개인키(≈비밀번호): client-cert-key.pem

CSR을 통해 쿠버네티스 CA로부터 사용자 인증서와 개인키를 발급받은 것이다.

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: (생략)
    server: https://172.30.1.71:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: default
  user:
    client-certificate-data: (생략)
    client-key-data: (생략)

client-certificate-data, client-key-data에 인증서와 개인키를 base64로 인코딩해서 포함하면 된다.

역할 기반 접근제어(RBAC)

사용자 인증 후 권한허가 단계에서는 역할 기반 접근 제어(Role Based Access Control, RBAC)를 통해 사용자들의 권한을 관리한다.

역할기반 접근 제어는 사용자나 그룹의 역할을 기반으로 쿠버네티스의 다양한 리소스의 접근을 관리하는 방법이다.

RBAC에는 크게 3가지 리소스가 있다.

  • Role(ClusterRole): 어떤 권한을 소유하고 있는지 정의
  • Subjects: Role을 부여할 대상 (User, Group, 혹은 ServiceAccount)
  • RoleBinding(ClusterRoleBinding): Role과 Subject의 연결을 정의한다.

Role

역할에는 2가지 리소스가 존재한다. 하나는 네임스페이스 안에서 역할을 정의하는 Role 리소스, 다른 하나는 네임스페이스 영역 밖에서 클러스터 레벨로 역할을 정의하는 ClusterRole 리소스.

다음은 role 리소스 에제를 살펴보자.

# role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"] # pod에 대한 권한만 설정한다.
  verbs: ["get", "watch", "list"]
  • rules: 이 Role이 가진 권한 목록
  • apiGroups: 접근하려는 리소스가 속한 API 그룹 (""은 core group을 의미)
  • resources: 접근할 리소스 종류 (예: pods, services 등)
  • verbs: 수행 가능한 작업 (예: get, list, watch, create, delete 등)

Subjects

Subjects에는 크게 User, Group, ServiceAccount가 있다. Subjects는 Role을 부여받을 객체로, 쿠버네티스에는 User와 Group이라는 리소스가 명시적으로 구현되어있는 대신, 개념적으로만 존재한다.

예를 들어, 앞에서 배운 X.509 인증 방식에서는 인증서의 Common Name(CN) 부분이 쿠버네티스에서 User로 인식되고 Organization(O) 부분이 Group으로 인식된다.

ServiceAccount는 User나 Group과 달리 쿠버네티스에 명시적으로 구현되어 있는 리소스로 다음과 같이 입력 시 생성된 ServiceAccount 리소스를 확인할 수 있다.

kubectl get serviceaccount #sa

ServiceAccount 리소스는 네임스페이스 레벨에서 동작하며 사용자가 Pod 리소스 생성 시, 명시적으로 ServiceAccount를 지정하지 않으면 기본적으로 default Service Account가 사용된다. 따라서 모든 namespace를 생성할때에는 default Service Account가 자동으로 생성된다.

ServiceAccount의 목적은 사용자가 아닌 프로그램(Pod)이 쿠버네티스와 통신할때 사용하는 신원(Identity)이다.

ServiceAccount를 생성하는 법은 간단하다.

kubectl create sa mysa

RoleBinding, ClusterRoleBinding

마지막 RoleBinding 리소스는 이름에서 알 수 있듯이 Role과 Subject를 엮는 역할을 담당한다. 이흫 통해, 특정 사용자가 부여받은 특정 권한을 사용할 수 있게 한다.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: default
subjects:
- kind: ServiceAccount
  name: mysa
roleRef:
  kind: Role
  name: pod-viewer
  apiGroup: rbac.authorization.k8s.io
  • subjects: 역할을 부여할 신원을 지정한다. 방금 생성한 mysa라는 ServiceAccount를 선택한다.
  • roleRef: 연결할 역할을 지정한다. 앞서 생성한 pod-viewer Role을 연결해 준다.

위 예제는 mysa에 default 네임스페이스에 있는 모든 Pod의 view(get, list, watch) 권한을 부여하는 정의서이다.

이제 Pod에 SA를 붙여보자.

apiVersion: v1
kind: pod
metatdata:
  name: nginx-sa
spec:
  containers:
  - image: nginx
    name: nginx
  # mysa 사용
  serviceAccountName: mysa

serviceAccountName 생략시, default라는 이름의 sa가 설정된다.

이제 해당 pod에 k8s cli 설치 후 클러스터에 접근하면, Pod에 대한 읽기만 가능한 것을 확인할 수 있다.

네트워크 접근 제어

k8s에는 Pod의 네트워크 접근을 제어할 수 있는 메커니즘이 존재한다. 다만, 모든 제품이 접근제어기능을 지원하지는 않고 일부 CNI(Container Network Interface)만 지원한다.

NetworkPolicy 모듈 설치 - Canal

k3s 기본적으로 flannel을 네트워크 제공자로 사용한다. flannel에 Network Policy를 적용하려면 Canal을 설치해야 한다.

kubectl apply -f https://docs.projectcalico.org/manifests/canal.yaml

쿠버네티스 네트워크 기본 정책

쿠버네티스 네트워크 기본 정책은 다음과 같다.

  1. 기본적으로 네트워크 정책은 없다.
  2. 하나도 설정된게 없다면 네임스페이스의 모든 트래픽이 열려있다. (default-allow)
  3. 만약 한 개의 네트워크 정책이라도 설정이 되면 정책의 영향을 받는 Pod에 대해서 다른 모든 트래픽은 막힌다. (default-deny)

NetworkPolicy 문법

관리자는 네트워크를 제어하기 위해서 NetworkPolicy 리소스를 사용한다. NetworkPolicy 정책은 네임스페이스 레벨에서 동작하며 라벨 셀렉터를 이용하여 특정 Pod에 네트워크 정책을 적용한다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - {}
  egress:
  - {}
  • namespace: 네임스페이스를 지정
  • podSelector: 적용할 Pod를 선택
  • policyTypes: 정책의 유형
  • ingress: 들어오는 트래픽 정책
  • egress: 나가는 트래픽 정책

예제의 네트워크 정책의 의미는 default 네임스페이스에 대해서 전체 오픈된 규칙이다. 이는 default 네임스페이스에 존재하는 모든 Pod의 인바운드, 아웃바운드 트래픽이 열려져 있다는 것을 의미한다.

  • podSelector: {}의 의미는 특정 라벨을 지정하지 않는 모든 Pod을 의미한다.
  • policyType: 인바운드와 아웃바운드에 대해서 둘 다 규칙을 지정하는 것을 의미한다.
  • ingress: - {}: 인바운드로 들어오는 모든 트래픽을 허용한다.
  • egress: - {}: 아웃바운드로 나가는 모든 트래픽을 허용한다.

네트워크 구성

책에는 네트워크 접근 제어를 활용한 다양한 예제가 있지만, 그 중에서 Private Zone을 만드는 방법과 80번 포트만 열어두는 Web 전용 네트워크 구성을 설정하는 두 개의 예제만 다뤄보겠다.

Private Zone 외부 트래픽이 들어올 수 없게 private zone을 만들어보자

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata: 
  name: deny-all
  namespace: default
spec:
  podSelector: {}
  ingress: []

전체 인바운드 트래픽을 허용한 allow-all 네트워크 정책 문법과 비교했을 때,

전체을 허용하고 싶을 때는 ingress 리스트에 1개의 빈 사전({})을 선언했다.
이것은 한 개의 정책을 허용하는데, 그때 허용되는 인바운드 트래픽은 특정 조건을 가지지 않는 모든 트래픽을 뜻한다.

deny-all은 빈 리스트([])를 선언한다, 이것은 0개의 정책을 허용한다는 것을 의미하며 default 네임스페이스에 대해서 기본적으로 모든 인바운드 트래픽을 막는다.

Web pod 오픈 보통 웹 서비스는 외부 사용자들이 서비스에 접근할 수 있도록 열어둔다. 하지만 전체 트래픽이 막혀있어서 run-web 라벨이 적용된 Pod의 80포트만 허용하려 한다. 다음과 같이 Wev-open 정책을 생성한다.

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata: 
  name: web-open
  namespace: default
spec:
  podSelector:
    matchLabels:
      run: web
  ingress:
  - from:
    - ipBlock:
        cidr: 0.0.0.0/0
      podSelector: {}
    ports:
    - protocol: TCP
      port: 80
  • ipBlock.cidr: 모든 IP 지정
  • podSelector: {}: 모든 pod 라벨 지정