CKA #7 Security

· Infra, Kubernetes, CKA

정말 길다고 느꼈고, 안써본 리소스들이 많아서 공부하는데 꽤 시간이 걸렸던 것 같습니다. 안써봤었던 컴포넌트들이 많아 나중에 다시 복습도 해봐야 할 것 같습니다.

Index

Security Primitives

누가 접근할 수 있는가? → Authentication

API 서버에 인증할 수 있는 다양한 방법이 존재함

무엇을 할 수 있는가? → Authorization

Authentication - 클러스터 관리에 대한 엑세스 보안

엔드 유저는 어플리케이션이 보안을 관리하니 논외로 하고, Admin과 Developer, 그리고 Bots(Application 등)이 남았다.

쿠버네티스는 사용자 계정을 직접 관리하지 않는다. 외부 소스에 의존한다. 사용자 파일, 인증서, LDAP 등. 쿠버네티스 클러스테에서 사용자를 생성하고 관리할 수 없다. 하지만 ServiceAccount는 만들고 관리할 수 있다.

모든 사용자 엑세스는 kube-apiserver에 의해 관리된다. kubectl과 curl 등의 API 요청을 포함한다. kube-apiserver는 요청을 처리하기 전에 사용자 인증을 먼저 한다.

kube-apiserver는 여러 방법의 인증 메커니즘을 지우너한다. 고정 암호 파일, 스태틱 토큰 파일, 인증서, LDAP 등.

여기서는 가장 이해하기 쉬운 Static PW File과 Static Token File을 다룬다.

사용자 이름, 암호, 토큰을 명확히 파일에 저장하는 이 인증 메커니즘은 불안정해 실제에서는 권장하지 않는다.

이 시도를 해보고 싶다면, auth 파일을 저장해둘 volumeMount도 고려하여야 한다.

Static PW File

# user-details.csv
password123,user1,u0001,group1
password123,user2,u0002,group2
password123,user3,u0003,group2
...
# kube-apiserver.service
ExecStart=/usr/local/bin/kuve-apiserver \\
  --advertise-address=...
  ...
  # 여기 추가한다.
curl -v -k https://masternodeip:6443/api/v1/pods -u "user1:password123" 

Static Token File

# user-token-details.csv
tke12139cYaa317,user1,u0001,group1
rJdasrRtj213fjd,user2,u0002,group2
AxS9xDks96Fsgos,user3,u0003,group2
--token-auth-file=user-token-details.csv
curl -v -k https://masternodeip:6443/api/v1/pods --header "Authorization: Bearer tke12139cYaa317"

TLS Certificates

다양한 컴포넌트의 인증서들이미지 출처: Certified Kubernetes Administrator (CKA) with Practice Tests(Udemy)

kube-apiserver에 인증서를 설정하는 방법이미지 출처: Certified Kubernetes Administrator (CKA) with Practice Tests(Udemy)

CA Certificate

Client Certificate

kube-apiserver에 엑세스하는 다른 구성 요소에 대한 클라이언트 인증서를 생성하는 프로세스는 모두 같다.

kube-config.yaml 파일에 인증서 정보를 담는 방법과, curl 요청을 날릴 때 옵션으로 key 파일을 지정해주는 방법이 있다. 보통 kube-config 쓴다.

issuer 정보 찾기

openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text

cert, key 파일 등은 어디있는가?

대부분의 컨트롤플레인 구성 요소들은 어디있을까

CA 서버란?

CA는 키와 인증서 파일 페어다. 이 파일 쌍에 접근할 수 있는 사람은 누구든 쿠버네티스 환경에 대한 인증서에 서명할 수 있다. CA 파일이 있으면 원하는만큼 많은 사용자를 만들 수 있다. 그래서 안전하게 보관해야 한다. → CA 서버라고 부른다. CA 서버에만 CA가 보관된다. 인증서에 서명할 때마다 해당 서버에 로그인하게 만들 수 있다.

쿠버네티스의 마스터 노드는 CA 서버이기도 하다. kubeadm 도구도 마찬가지다. CA 페어를 만들어 마스터 노드에 저장한다. 서명을 수동으로 하기에는 한계가 이기에 자동화가 필요하다. 따라서 쿠버네티스엔 내장 인증서 API를 지원해 자동화시킬 수 있다. 서명 요청을 API 콜로 보낼 수 있다.

openssl genrsa -out jane.key 2048

openssl req -new -key jane.key -sunj "/CN=jane" -out jane.csr 
# 인증서 사인 요청 개체를 생성

cat jane.csr | base64 
# 인코딩하고, 이를 CertificateSigningRequest yaml 파일에 담는다.
# jane-csr.yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: jane
spec:
  signerName: kubernetes.io/kube-apiserver-client
  expirationSeconds: 600 # seconds
  usages:
  - digital signature
  - key encipherment
  - server auth
  request:
    # base64로 인코딩된 csr 파일 내용

관리자의 승인

kubectl get csr

kubectl certificate approve jane # 승인 명령어
kubectl certificate deny agent-smith # 거절 명령어

CSR-Approving & CSR-Signing

이 작업은, Controller Manager가 한다. Kube-Apiserver가 할 줄 알았는데… 의외였다.

kubeconfig

./kube/config에 위치하고 있는 파일. cluster - context - user 구조로 이루어져 있다.

API Groups

쿠버네티스 kube-apiserver가 제공하는 api에는 대표적으로 /api/apis 가 있다.

API Groups이미지 출처: Certified Kubernetes Administrator (CKA) with Practice Tests(Udemy)

API Groups이미지 출처: Certified Kubernetes Administrator (CKA) with Practice Tests(Udemy)

API Groups이미지 출처: Certified Kubernetes Administrator (CKA) with Practice Tests(Udemy)

kubectl proxy

kubectl proxy를 이용하면, proxy가 인증서 파일을 가져다 사용하기 때문에 proxy를 사용하는 상태에서는 인증서 파일을 따로 명시해주지 않아도 바로 kube-apiserver에 인증 명령어 없이도 요청을 날릴 수 있게 된다.

Authorization

접속하고 나서, 뭘 할 수 있는가?가 인가의 정의이다.

우리는 지금까지 Admin으로 들어와서 모든 작업을 처리할 수 있었다. 그런데 다른 사람이 클러스터에 엑세스하고자 한다면? 혹은 다른 어플리케이션이 들어와서 클러스터에 접근하려고 한다면? 접근할 수 있게 해줘야 하지만, 접근 가능한 권한에는 차이를 두고 싶을 것이다. 클러스터를 서로 다른 조직이나 팀으로 나누어 공유할 때 논리적으로 네임스페이스를 이용해 분할해 사용자 접근을 제한할 수 있다. 클러스터 내에서의 권한은 이러한 작업을 가능하게 한다.

Node Authorizer

ABAC (Attribute Based Access Control)

사용자, 리소스, 속성, 환경에 따라 권한을 결정한다.

ABAC은 관리하기 어렵다. 정책이 바뀔 때마다 정의 파일을 수정하고, kube-apiserver를 재시작해야 한다.

{"kind": "Policy", "spec": {"user": "dev-user", "namespace":"*", "resource":"pods", "apiGroup: "*"}}
{"kind": "Policy", "spec": {"user": "dev-user-2", "namespace":"*", "resource":"pods", "apiGroup: "*"}}
{"kind": "Policy", "spec": {"user": "dev-users", "namespace":"*", "resource":"pods", "apiGroup: "*"}}
{"kind": "Policy", "spec": {"user": "security-1", "namespace":"*", "resource":"csr", "apiGroup: "*"}}

RBAC (Role Based Access Control)

역할을 기준으로 권한을 결정한다. 쿠버네티스 클러스터 내에서 엑세스 관리에 대한 표준적 접근법을 제공한다.

Webhook

모든 승인 메커니즘을 외부에 위임하고자 할 때 사용한다. Open Policy Agent를 활용하면 kube-apiserver가 인가 내용이 필요할 때, Open Policy Agent에게 인가 여부를 물어보게 만들 수 있다.

AlwaysAllow, AlwaysDeny

승인 확인 없이 모두 허용하거나 모두 거부하는 모드이다.

앞서 살펴본 인가 모드들은 kube-apiserver를 시작할 때, --authorization-mode=<mode-name> 옵션으로 설정할 수 있다. 쉼표를 이용해 여러 모드를 사용할 수 있다. 지정된 순서대로 각각의 요청을 사용할 권한이 부여된다.

--authorization-mode=Node,RBAC,Webhook

사용자가 요청을 보내면 Node Authorizer가 먼저 처리하는데, 거부될 때마다 다음 모듈로 넘어가 다시 인가 절차를 거친다. Node Authorizer가 거부하면 RBAC에서 처리하고, 그다음은 Webhook에서 처리한다.

RBAC

역할을 만든다. Role 파일에서는 해당 역할이 어떤 리소스에 어떤 행동(verb)을 취할 수 있는지를 결정한다.

# developer-role.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["list", "get", "create", "update", "delete"]
- apiGroups: [""]
  resources: ["ConfigMap"]
  verbs: ["create"]

역할을 사용자에게 Binding하는 RoleBinding 파일을 만든다.

dev-user라는 사용자는 이제 developer의 Role을 가지게 되어 developer Role에서 지정한 권한을 행사할 수 있다. 네임스페이스를 제한하고 싶다면, metadata 안에서 namespace를 지정하면 된다.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: devuser-developer-binding 
subjects: # 사용자 세부 정보를 지정하는 곳
- kind: User
  name: dev-user
  apiGroup: rbac.authorization.k8s.io
roleRef: # 롤의 세부 정보를 지정하는 곳
  kind: Role
  name: developer
  apiGroup: rbac.authorization.k8s.io
kubectl get roles
kubectl get rolebindings
kubectl describe role developer
kubectl describe rolebinding devuser-developer-binding

Check Access

kubectl auth can-i create deployments
kubectl auth can-i delete nodes
kubectl auth can-i create deployments --as dev-user
kubectl auth can-i create pods --as dev-user

Resource Names

만약 전체 파드가 아니라, 일부 파드만 접근 권한을 주고 싶다면 Resource Names를 사용할 수 있다.

# developer-role.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["list", "get", "create", "update", "delete"]
- apiGroups: [""]
  resources: ["ConfigMap"]
  verbs: ["create"]
  resourceNames: ["blue", "orange"]

Cluster Roles and Role Bindings

Role은 하나의 namespace 내에서만 동작한다. namespace를 지정하지 않으면, 기본 namespace에서 동작하게 된다. 네임스페이스는 격리에 도움이 되는 존재다. 그렇다면 노드는 어떻게 관리하나? 노드는 네임스페이스에 종속되지 않는다. 노드는 클러스터 범위의 리소스다.

리소스는 네임스페이스와 클러스터 스코프로 분리된다.

Cluster Scope, Namespace Scope이미지 출처: Certified Kubernetes Administrator (CKA) with Practice Tests(Udemy)

kubectl api-resources --namespaced=true
kubectl api-resources --namespaced=false
# developer-role.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-administrator
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["list", "get", "create", "delete"]
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-admin-role-binding
subjects: # 사용자 세부 정보를 지정하는 곳
- kind: User
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
roleRef: # 롤의 세부 정보를 지정하는 곳
  kind: ClusterRole
  name: cluster-administrator
  apiGroup: rbac.authorization.k8s.io

클러스터롤은 네임스페이스 스코프의 권한도 만들 수 있다.

kubectl get clusterroles --no-headers  | wc -l
kubectl get clusterroles --no-headers  -o json | jq '.items | length'

Service Accounts

쿠버네티스에는 User Account와 Service Account가 있다. User Account는 사람을 위한 시스템이고 ServiceAccount는 프로그램이 쿠버네티스와 상호작용할 때 사용한다. 프로메테우스 같은 것들이 있다.

kubectl create serviceaccount dashboard-sa
kubectl get serviceaccount

ServiceAccount를 생성하면 토큰이 자동으로 생성된다. 토큰은 외부앱이 쿠버네티스 API에 인증하는 동안 사용된다. 토큰은 Secret 객체로 저장된다. ServiceAccount가 생성되면, ServiceAccount를 위한 토큰이 생성되고, Secret을 만들어 토큰을 저장한다. Secret은 ServiceAccount에 링크된다.

kubectl describe secret dashboard-sa-token-kbbdm

토큰은 Header Authorization Bearer를 이용해 넘길 수 있다. 서비스 계정을 만들고, RoleBased Access Control 매커니즘을 사용할 수 있다.

모든 네임스페이스는 default serviceaccount를 자동으로 생성한다. 파드가 생성될 때마다 default 토큰이 자동으로 볼륨 마운트된다. 파드 내의 /var/run/sercrets/kubernetes.io/serviceaccount 경로를 확인하면 serviceaccount token 파일을 확인할 수 있다. default serviceaccount는 매우 기본적인 쿠버네티스 API 쿼리를 실행하는 권한만 가지고 있다. 다른 serviceaccount를 사용하고 싶다면 pod definition yaml에서 serviceAccountName으로 설정해줄 수 있다.

v1.22 개선 내용 - KEP 1205

v1.22에서 default로 생성되는 serviceaccount token은 제한 기간이 없다. 그리고 각 serviceaccount당 token을 담기 위한 1개의 별개의 secret을 필수적으로 요구한다.

KEP 1205를 통해 serviceaccount token 개선안이 제시되었다.

v1.22 이후로는 정해진 수명을 가진 해당 Pod를 위한 token이 TokenRequestAPI를 통해 생성된다.

v1.24 개선 내용 - KEP 2799

과거에는 serviceaccount가 생성되면, 만료되지 않는 토큰이 secret으로 생성되었고, 이 토큰은 해당 serviceaccount를 사용하는 파드에 자동으로 마운트되었다. 하지만 v1.22에서는 자동으로 secret을 파드에 volume으로 마운트하는 대신, TokenRequestAPI를 사용해 만료기간이 있는 토큰을 발급해 마운트하도록 변경되었다.

따라서, serviceaccount를 생성하고, 이제는 토큰을 별개로 생성해야 한다. 토큰을 해독해보면 만료 기간이 정의된 것을 확인할 수 있다. 추가 옵션을 통해 만료 기간을 늘릴 수 있다.

kubectl create serviceaccount dashboard-sa
kubectl create token dashboard-sa # 토큰을 화면에 프린트함

jq -R 'split(".") | select(length > 0) | .[0],.[1] | @base64d | fromjson' <<< (앞서프린트된토큰)
# 토큰을 JSON 형태로 해독

v1.24에서 만료되지 않는 토큰을 만들고 싶다면, secret을 만들어 해결할 수 있다.

# 만료되지 않는 토큰 만들기 (서비스어카운트를 먼저 생성하고 시크릿을 만들어야 함)
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: mysecretname
  annotations:
    kubernetes.io/service-account.name: dashboard-sa
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: read-pods
  namespace: default
subjects:
- kind: ServiceAccount
  name: dashboard-sa # Name is case sensitive
  namespace: default
roleRef:
  kind: Role #this must be Role or ClusterRole
  name: pod-reader # this must match the name of the Role or ClusterRole you wish to bind to
  apiGroup: rbac.authorization.k8s.io

---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups:
  - ''
  resources:
  - pods
  verbs:
  - get
  - watch
  - list

기존 deployment에 serviceaccount 추가하기

kubectl set serviceaccount deploy/web-dashboard dashboard-sa
spec.template.spec.serviceAccountName: dashboard-sa

Image Security

image: nginx

docker.io/library/nginx
docker.io -> registry
library -> UserAccount
nginx -> ImageRepository

nginx는 무엇일까? → Image Repository를 뜻한다.

쿠버네티스 클러스터는 어떻게 Private Registry에 접근할 Credentials를 확인하는가?

kubectl create secret deocker-registry regcred \
  --docker-server=private-registry.io \
  --docker-username=registry-user \ 
  --docker-password=registry-password \
  --docker-email=registry-user@org.com
apiVersion: v1
kind: Pod
metadata:
  name: private-reg
spec:
  containers:
  - name: private-reg-container
  image: <your-private-image>
  imagePullSecrets:
  - name: regcred

Security Contexts

도커 컨테이너는 고유 네임스페이스에 존재하고 있다. 그래서 자신의 고유 프로세스만 볼 수 있다. 바깥이나 다른 네임스페이스에 대해서는 아무 것도 알 수 없다.

도커 호스트에서 하위 네임스페이스에 있는 모든 프로세스는 전부 프로세스로 보인다. 단, 도커 컨테이너 내에서 보이는 PID와 도커 호스트에서 보이는 동일한 프로세스에 대한 PID는 다르게 보인다.

→ 프로세스는 다른 네임스페이스에서는 다른 프로세스ID를 가질 수 있다. (프로세스 격리)

도커 호스트는 사용자 집합을 갖고 있다. 루트 유저와 비루트 유저들. 도커는 루트 사용자로써 컨테이너 내의 프로세스를 실행한다. 도커 안과 밖에서 프로세스는 루트 사용자로 실행된다. 컨테이너 내의 프로세스가 루트 사용자로 실행되길 원하지 않는다면 Docker 실행 명령 안에서 사용자 옵션을 사용해 새 사용자 ID를 명시할 수 있다.

docker run --user=1000 ubuntu sleep 3600

사용자 보안을 시행하는 다른 방법은, 도커 이미지 자체에서 생성 시 사용자를 정의하는 방법이 있다.

# Dockerfile

FROM ubuntu
USER 1000
docker build -t my-ubuntu-image .
docker run my-ubuntu-image sleep 3600

루트 유저로써 컨테이너를 실행하면 어떻게 될까? 컨테이너 안의 루트 사용자와 호스트의 루트 사용자는 같은 인물인가? 컨테이너 안의 실행된 프로세스도 루트 사용자가 시스템에서 할 수 있는걸 시행할 수 있는가? 그렇다면 보안 문제가 있지 않나?

도커는 컨테이너 내 루트 사용자의 능력을 제한하는 보안 기능 집합을 구현한다. 컨테이너 안의 루트 사용자는 호스트의 루트 사용자와는 다른 존재다. 루트 사용자는 시스템의 모든 것을 할 수 있다. 그리고 루트에 의해 실행된 프로세스도 마찬가지로 시스템에 대한 모든 것을 처리할 수 있다. 어떻게 제한하는가? 기본 값으로 도커는 컨테이너를 실행하는데, 제한된 기능만을 시행할 수 있게끔 되어 있다. 재부팅, 어드민 등의 권한을 제외했다. 만약 이 권한을 재정의하고 싶다면 --cap 옵션을 사용해 권한을 수정할 수 있다.

docker run --cap-add MAC_ADMIN ubuntu
docker run --cap-drop KILL ubuntu
docker run --privileged ubuntu # 전체 권한 부여

쿠버네티스는 컨테이너를 파드로 캡슐화한다. 컨테이너 레벨이나 파드 레벨에서 보안 설정을 진행할 수 있다.

파드 레벨에서 보안을 설정하면, 파드 내의 모든 컨테이너가 영향을 받는다.

파드 레벨과 컨테이너 레벨의 보안을 둘 다 설정하면, 컨테이너 레벨이 파드 레벨을 덮어쓴다.

apiVersion: v1 
kind: Pod
metadata:
  name: web-pod
spec:
  securityContext: # 파드 레벨
    runAsUser: 1000 # 유저 설정
  containers:
  - name: ubuntu
    image: ubuntu
    command: ["sleep", "3600"]
    securityContext: # 컨테이너 레벨
      runAsUser: 1000 # 유저 설정
      capabilities:
        add: ["MAC_ADMIN"]

컨테이너나 파드가 어떤 권한으로 실행되었는지 알려면 다음 명령어를 사용하면 된다.

kubectl exec ubuntu-sleeper -- whoami

Network Policy

쿠버네티스 네트워킹의 전제 조건 중 하나는 어떤 솔루션을 구현하든, 추가적인 설정 없이 파드가 서로 통신할 수 있어야 한다는 것이다.

쿠버네티스는 기본값으로 모든 팟과 서비스에 대해서 AllAlow 규칙으로 구성된다.

파드 간 통신을 가능하게 하거나, 불가능하게 하기 위해서 Network Policy를 설정하게 된다. Network Policy는 쿠버네티스의 리소스 중 하나로, 파드의 네트워크 규칙을 관리한다. Network Policy 안에서 규칙을 정의할 수 있다.

kubectl get netpol
apiVersion: netwokring.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress # Allow Ingress
  ingress:
  - from: # From API-Prod Pod
    - podSelector:
      matchLabels:
        name: api-prod
    ports: # On TCP 3306 Port
    - protocol: TCP
      port: 3306

NetworkPolicy를 지원하는 구현체

EXAMPLE - DB Pod 관리하기

DB Pod's Network Policy이미지 출처: Certified Kubernetes Administrator (CKA) with Practice Tests(Udemy)

apiVersion: netwokring.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db
    policyTypes:
    - Ingress
    ingress:
    - from:
      - podSelector:
        matchLabels:
          name: api-pod 
      ports:
      - protocol: TCP
        port: 3306
apiVersion: netwokring.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db
    policyTypes:
    - Ingress
    ingress:
    - from:
      - podSelector:
          matchLabels:
            name: api-pod 
        namespaceSelector:
          matchLabels:
            name: prod
      ports:
      - protocol: TCP
        port: 3306

EXAMPLE2 - BackupServer to DB Pod

만약 클러스터 외부의 서버가 DB에 접근하고 싶다면?

특정 IP 주소에서 접근할 수 있도록 Network Policy를 추가할 수 있다.

apiVersion: netwokring.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db
    policyTypes:
    - Ingress
    ingress:
    - from:
      - podSelector:
          matchLabels:
            name: api-pod 
        namespaceSelector:
          matchLabels:
            name: prod
      - ipBlock:
          cidr: 192.168.5.10/32
      ports:
      - protocol: TCP
        port: 3306

podSelector와 namespaceSelector는 하나의 Array 요소 안에 들어있다. 즉 이 둘은 AND 연산으로 동작한다. 하지만 ipBlock은 다른 Array 요소이므로, OR로 동작한다.