k8s의 보안

xlwdn·2023년 8월 13일
0

Service Account


k8s에서는 권한을 제어하기 위해 UserAccount와 ServiceAccount를 제공한다. 이때 UserAccount는 GKE의 google 계정, EKS에서의 IAM 계정과 연결되어 있어 k8s 관리 대상이 아니다.

UserAccount는 Cluster 수준에서 사용되며, 이에 반해 ServiceAccount는 네임스페이스에서 사용된다. 또한 어떠한 pod라도 반드시 SA가 하당되어야하며, 이를 통해 SA 기반 인증/인가를 진행하고 만약 이를 할당하지 않을 시 기본 account로 할당한다.

실습

$ k create serviceaccount sample-serviceaccount
serviceaccount/sample-serviceaccount created
$ k delete sa sample-serviceaccount
serviceaccount "sample-serviceaccount" deleted

위 명령을 통해 sa를 생성/삭제할 수 있다.

또한 private repository 접근을 위해 secret을 이용한다면 다음과 같이 기존 방법을 교체할 수도 있다.

기존

...
spec:
  ...
  template:
    ...
    spec: 
      imagePullSecrets:
      - name: regsecret 

이후

$ k patch serviecaccount default -p '{"imagePullSecrets": [{"name": "regsecret"}]}'
#또는
$ k get serviceaccounts default -o yaml > sa.yaml
$ cat <<EOF >> sa.yaml
> imagePullSecrets:
> - name: regsecret
> EOF

위와 같이 default sa에 imagePullSecrets를 추가하여 리소스가 secret을 사용할 수 있도록 하였다.

ServiceAccount Token


k8s에서는 두 개의 sa token 종류가 있다

  • long-lived token
  • time bound token

long-lived token

long-lived token은 만료되지 않는다. 때문에 보안 적으로 취약하며 사용에 유의하여야한다.

k8s 1.24 버전 이전에 자동으로 생성되었던 token이 해당 유형이었으며 때문에 보안 및 확장성 문제로 인하여 제거되었다.

k8s에서 추천되지는 않지만 long-lived token은 다음 방법을 통해 생성할 수 있다.

k apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata: 
  name: my-long-lived-secret
  annotations:
    kubernetes.io/service-account.name: my-sevice-account
type: kubernetes.io/service-account-token
EOF

time bound token

1.22버전 이후, k8s는 TokenRequest API를 제공한다. 해당 API를 통해 생성된 토큰은 시간이 지난 이후 만료되며 default service accoutn와 custom-defined service accounts에 모두 적용할 수 있다.

$ kubectl create token my-time-bound-token

위 명령어를 통해 토큰을 생성할 수 있다.

다만, 실제 사용 시에는 pod launch 시 automountServiceAccountToken이 true로 설정되어있다면 자동으로 volume을 마운트한다. 이후 node에서 동작중인 kubelet agent가 해당 볼륨에 token을 마운트한다.

Token Expiration

token은 한 시간 이내에 만료된다. 다만, 많은 legacy application들이 non-expiring token과 함께 작동하므로 k8s는 —service-account-extend-token-expiration=true를 Kube API Server에 지정할 수 있게 한다.

해당 플래그 지정 시 일시적으로 더 긴 만료 시간(365 days)을 가지게 되고 legacy token들의 사용처를 기록한다.

만약 token-expiration 플래그가 true로 지정 시 k8s는 한 시간 이내, 또는 원할 때에 만료시킬 수 있는 토큰을 만들거나 마운트할 수 있도록한다.

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - image: nginx
    name: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/tokens
      name: my-proj-vol
  serviceAccountName: my-service-account #service acount 
  volumes:
  - name: my-proj-vol
    projected:
      sources:
      - serviceAccountToken:
          path: my-proj-vol
          expirationSeconds: 3600 #specify the desired epiration time in seconds

Pod에 ServiceAccount 할당

default sa와 달리 custom sa는 pod에 자동으로 할당되지 않는다. 때문에 다음과 같이 구성하여야한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-serviceaccount-noautomount-pod
  namespace: default
spec:
  serviceAccountName: sample-serviceaccount-noautomount
  automountServiceAccountToken: true
  containers:
  - name: nginx-container
    image: nginx:1.16

위와 같이 automountServiceAccountToken을 통해 자동으로 토큰 마운트를 설정할 수 있다.(비활성화를 위해선 false로 지정한다.)

apiVersion: v1
kind: ServiceAccount
metadata:
  name: sample-serviceaccount-noautomount
  namespace: default
automountServiceAccountToken: false

위와 같이 sa에 설정할 수도 있으며, 이렇게 되면 화이트리스트 방식으로 운영된다.(true 시에는 블랙 리스트)

Docker registry 인증 정보 설정


apiVersion: v1
kind: ServiceAccount
metadata:
  name: sample-serviceaccount-pullsecret
imagePullSecrets:
- name: sample-registry-auth

위와 같이 sa에 imagePullSecrets 설정을 하게되면 할당한 파드에서 사용할 수 있다.

$ kgp sample-serviceaccount-pullsecret-pod -o yaml
...
imagePullSecrets:
  - name: sample-registry-auth
...

RBAC


RBAC는 어떠한 조작을 허용할지 설정하는 Role을 SA에 연결하여 권한을 부여한다. 또한 AggregationRule을 사용하여 여러 롤을 집약할 수도 있다.

Role과 RoleBinding에는 namespace 수준의 resource와 Cluster 수준의 resource가 존재하며 세부적으로는 namespace 수준에서는 Role과 RoleBinding, cluster 수준에서는 ClusterRole과 ClusterRoleBinding이 존재한다.

기존에는 ABAC도 있었으나 현재는 RBAC를 권장한다.

Role && ClusterRole


role과 clusterRole은 둘 다 namespace 범위의 리소스를 대상으로 인가 설정을 할 수 있으며, clusterRole은 node/namespace/persistenceVolume과 같은 cluster 범위의 resource나 /version또는 /healthz와 같은 k8s API 정보를 가져오는 nonResourceURL에 대한 권한도 설정할 수 있다.

Role, ClusterRole 생성 시 주의사항

  • deployment resource에 대해 롤 기술 시 deployment는 extensions/v1beta1, extensions/v1beta2, apps/v1으로 apiGroup이 변화해왔다. 때문에 role 작성 시 주의하여야한다.
  • deployment resouce와 deployment scale resource는 개별적으로 지정해야한다. deployment/scale 미지정 시 레플리카 수를 변경하는 스케일 처리를 할 수 없다.

Aggregated Clusterrole


---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sub-clusterrole1
  labels:
    app: sample-rbac
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sub-clusterrole2
  labels:
    app: sample-rbac
rules:
- apiGroups: [""]
  resources: ["services"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sample-aggregated-clusterrole
aggregationRule:
  clusterRoleSelectors:
  - matchLabels:
      app: sample-rbac
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get"]

위 yaml 내용에 따라, ClusterRole 생성 시 aggragationRule을 추가하여 selector와 일치하는 cluster role을 자동으로 집계해 적용시킬 수 있다. 이때, 집계하는 role은 집계되는 영역의 role이 생성 이후에 변경된다 할지라도 지속적으로 변경을 적용한다.

k8s가 생성하는 clusterrole

k8s은 몇 개의 clusterrole을 프리셋으로 제공한다.

  • cluster-admin: 모든 리소스 관리 가능
  • admin: 클러스터롤 편집 + namespace 수준의 RBAC
  • edit: 읽기 쓰기
  • view: 읽기 전용

(시스템 구성 요소에 대한 role은 system:으로 시작한다.(ex. system:controller:clusterrole-aggregation-controller)

RoleBinding, ClusterroleBinding

  • rolebinding: 사용자에 대해 특정 네임스페이스에서 role 또는 clusterRole에 정의한 권한을 부여한다.
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: sample-rolebinding
      namespace: default
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: sample-role
    subjects:
    - kind: ServiceAccount
      name: sample-serviceaccount
      namespace: default	
  • clusterRoleBinding: cluster 수준의 role binding
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: sample-clusterrolebinding
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: sample-clusterrole
    subjects:
    - kind: ServiceAccount
      name: sample-serviceaccount
      namespace: default

보안 Context

각 container에 대한 보안 설정이다.

  • privileged
    apiVersion: v1
    kind: Pod
    metadata:
      name: sample-privileged
    spec:
      containers:
      - name: nginx-container
        image: nginx:1.16
        securityContext:
          privileged: true
    • 특수 권한을 가진 Container로 실행
    • spec.containers[].securityContext.privileged가 true일 시 Container 내부에서 기동하는 프로세스의 linux capabilities가 호스트와 동일한 권한을 가진다.
  • capabilities
    apiVersion: v1
    kind: Pod
    metadata:
      name: sample-capabilities
    spec:
      containers:
      - name: tools-container
        image: amsy810/tools:v2.0
        securityContext:
          capabilities:
            add: ["SYS_ADMIN"]
            drop: ["AUDIT_WRITE"]
    • Capabilities의 추가와 삭제

      $ k exec -it sample-capabilities -- capsh --print | grep Current
      Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_sys_admin,cap_mknod,cap_setfcap+ep
  • allowPrivilegeEscalation
    • Container 실행 시 상위 프로세스보다 많은 권한을 부여할지 여부
  • readOnlyRootFilesystem
    apiVersion: v1
    kind: Pod
    metadata:
      name: sample-rootfile-readonly
    spec:
      containers:
      - name: tools-container
        image: amsy810/tools:v2.0
        securityContext:
          readOnlyRootFilesystem: true
    • root 파일 시스템을 읽기 전용으로 할지 여부

      $ k exec -it sample-rootfile-readonly -- touch /var/test
      touch: cannot touch '/var/test': Read-only file system
      command terminated with exit code 1
  • runAsUser
    apiVersion: v1
    kind: Pod
    metadata:
      name: sample-runuser
    spec:
      securityContext:
        runAsUser: 65534
        runAsGroup: 65534
        supplementalGroups:
        - 1001
        - 1002
      containers:
      - name: tools-container
        image: amsy810/tools:v2.0
    • 실행 사용자를 지정한다. 위 매니페스트에서는 nobody로 지정한다.

      $ k exec -it sample-runuser -- id
      uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup),1001,1002
  • runAsGroup: 실행 그룹
  • runAsNonRoot
    apiVersion: v1
    kind: Pod
    metadata:
      name: sample-nonroot
    spec:
      securityContext:
        runAsNonRoot: true
      containers:
      - name: nginx-container
        image: nginx:1.16
    • root에서 실행 거부
  • seLinuxOptions: SELinux 옵션

File system group

일반적으로 mount 시 해당 volume fs의 권한은 root:root로 설정되어있다. 때문에 위의 runAsNonRoot, 또는 runAsUser 사용 시 이를 변경해야한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-fsgroup
spec:
  securityContext:
    fsGroup: 1001
  containers:
  - image: nginx:1.16
    name: nginx-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}
$ k exec -it sample-fsgroup -- ls -ld /cache
drwxrwsrwx 2 root 1001 6 Aug 10 00:32 /cache

pod 내부 커널 파라미터 제어

k8s에서는 하나의 pod 내의 containers가 공유하는 커널 파라미터를 제어할 수 있다. 다만 커널 파라미터는 unsafe와 safe로 나뉘는데, unsafe 파라미터 제어 시 클러스터 제공자가 명시적으로 허용하지 않으면 파드 기동에 실패한다. 뿐만 아니라 k8s node 측에서 시스템 구성 요소의 기동 옵션(kubelet)에서 명시적으로 지정해야하므로 관리형 서비스에서 이용하기 어렵다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-sysctl
spec:
  securityContext:
    sysctls:
    - name: net.core.somaxconn
      value: "12345"
  containers:
  - name: tools-container
    image: amsy810/tools:v2.0
$ kgp
sample-sysctl              0/1     SysctlForbidden   0          5s
$ k describe pod sample-sysctl
...
Events:
  Type     Reason           Age   From               Message
  ----     ------           ----  ----               -------
  Warning  SysctlForbidden  11s   kubelet            forbidden sysctl: "net.core.somaxconn" not allowlisted
...

다만, privileged 옵션 true로 설정한 init container를 활용하여 unsafe한 커널 파라미터를 강제적으로 변경할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-sysctl-initcontainer
spec:
  initContainers:
  - name: initialize-sysctl
    image: busybox:1.27
    command:
    - /bin/sh
    - -c
    - |
      sysctl -w net.core.somaxconn=12345
    securityContext:
      privileged: true
  containers:
  - name: tools-container
    image: amsy810/tools:v2.0
$ k exec -it sample-sysctl-initcontainer -- bash
Defaulted container "tools-container" out of: tools-container, initialize-sysctl (init)
root@sample-sysctl-initcontainer:/# sysctl net.core.somaxconn
net.core.somaxconn = 12345

PSA(Pod Security Admission) & PSS(Pod Security Standards)

기존에는 보안 정책 설정을 위해 pod security policy를 사용하였으나, 직관적이지 않은 방식으로 인해 관리자의 의도와는 다른 결함을 포함하고 있거나 적절하게 보호되지 않는 상태로 리소스를 운영하게 되는 경우가 발생함에따라 1.25부터는 deprecated 되었다.

이후 PSP는 PSA로 교체되었고 PSA는 Adminssion Controller로서 PSS에 정의된 보안 통제 항목을 구현하는 데에 사용되며 파드가 생성되기 전 파드의 보안 설정을 평가하고 이에 대한 조치를 구성할 수 있다.

PSS는 리소스에 대한 보안 정책을 정의하며 이러한 보안 정책에는 3개 분류의 세부 항목이 존재한다.

  • Privileged
    • 전적으로 제한이 없다.
    • 권한이 있고 신뢰있는 사용자가 관리하는 시스템 및 인프라 수준의 워크로드를 대상으로 한다.
  • Baseline
    • baseline은 권한 상승을 방지하면서 일반적인 container 워크로드에 대해 정책 채택을 쉽게 하는 것을 목표로 한다.
  • Restricted
    • 일부 호환성을 희생하면서 파드 보안 강화를 위한 모범 사례를 따른다.

PSA는 다음 3가지 운영 모드를 통해 PSS 정책에 정의되어 있는 통제 항목들을 구현한다.

  • enforce
    • 정책 위반 시 pod는 reject 처리된다.
  • audit
    • 정책 위반 시 audit log에 이벤트를 기록하기 위한 감사 정보가 추가되어 기록되지만 pod 자체는 허용된다.
  • warn
    • 정책을 위반하게 되면 사용자에게 경고를 표시하지만 pod 자체는 허용된다.

Network policy

cluster 내부에서 pod 간 통신할 경우 트래픽 롤을 규정하는 것이다. network policy를 사용하지 않을 경우 cluster 내부의 모든 pod는 서로 통신이 가능하다.

Network policy 활성화

networki policy 활성화를 위해선 이를 지원하는 CNI를 사용해야한다. EKS에서 기본적으로 사용하는 CNI는 aws CNI이므로 다른 CNI로 교체해야한다.

network policy는 ingress와 egress로 이루어져있으며 이를 podSelector를 통해 트래픽 롤을 통해 제어한다. 또한 network policy는 namespace별로 생성해야한다.

예제

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: cloud-networkpolicy
spec:
  podSelector: {}
  egress:
  - {}
  policyTypes:
  - Ingress
  - Egress

위 manifest는 egress의 경우 모두 혀용하고 ingress를 막는다.

위와 같이 기본 설정 구성 후 다른 NetworkPolicy를 추가하여 화이트박스 보안 설정을 구성할 수 있다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: cloud-inbound-allow80
spec:
  podSelector:
    matchLabels:
      app: dev
  policyTypes:
  - Ingress
  ingress:
    - from:
      - ipBlock:
          cidr: 0.0.0.0/0
      ports:
      - protocol: TCP
        port: 80

Admission Control

k8s에서의 인증은 Authentication → Authorization → Admission Control으로 구성되어있다. Authentication과 Authorization을 받은 사용자 중에서도 별도로 그 요청을 허가할지 판단하거나 리소스를 변경하여 등록할 수 있다.

Admission Control에선 다음 플러그인을 사용할 수 있다.

  • NamespaceLifeCycle
  • LimitRanger
  • ServiceAccount
  • DefaultStorageClass
  • DefaultTolerationSeconds
  • MutatingAdmissionWebhook
  • ValidatingAdmissionWebhook
  • ResourceQuota
  • PodPreset
  • PersistentVolumeClaimResize
  • PodSecurityPolicy

위 플러그인을 통해 API 요청에 대한 기본적인 제어/수정이 이루어진다.

Pod Preset - 사라진 기능


pod가 시작되기 전에 환경변수나 스토리지 리소스의 기본값을 추가하여 특정 레이블을 가지는 pod에 대해 적용시키는 기능이었다. 다만 해당 기능은 alpha 단계에서 폐기되었다.

1개의 댓글

comment-user-thumbnail
2023년 8월 13일

좋은 글 감사합니다. 자주 방문할게요 :)

답글 달기