IT/Container&k8s

[k8s] Security

Hayley Shim 2023. 10. 29. 00:42

안녕하세요. CloudNet@ K8S Study를 진행하며 해당 내용을 이해하고 공유하기 위해 작성한 글입니다. DevOps 이정훈님의 도서 ‘24단계 실습으로 정복하는 쿠버네티스’ 의 내용을 바탕으로 스터디를 하며 어떻게 k8s에 적용할지 고민하며 정리해봤습니다.

사전 환경

# YAML 파일 다운로드
$ curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/kops-oneclick-f1.yaml

# CloudFormation 스택 배포 : 노드 인스턴스 타입 변경 - MasterNodeInstanceType=t3.xlarge WorkerNodeInstanceType=t3.large
$ aws cloudformation deploy - template-file kops-oneclick-f1.yaml - stack-name mykops - parameter-overrides KeyName=kp-hayley3 SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 MyIamUserAccessKeyID=AKIA5… MyIamUserSecretAccessKey='CVNa2…' ClusterBaseName='wellbeconnected.com' S3StateStore='hayley-bucket-test' MasterNodeInstanceType=t3.xlarge WorkerNodeInstanceType=t3.xlarge - region ap-northeast-2

# CloudFormation 스택 배포 완료 후 kOps EC2 IP 출력
$ aws cloudformation describe-stacks - stack-name mykops - query 'Stacks[*].Outputs[0].OutputValue' - output text

# 13분 후 작업 SSH 접속
$ ssh -i ~/.ssh/kp-gasida.pem ec2-user@$(aws cloudformation describe-stacks - stack-name mykops - query 'Stacks[*].Outputs[0].OutputValue' - output text)

# EC2 instance profiles 에 IAM Policy 추가(attach) : 처음 입력 시 적용이 잘 안될 경우 다시 한번 더 입력 하자! - IAM Role에서 새로고침 먼저 확인!
$ aws iam attach-role-policy - policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy - role-name masters.$KOPS_CLUSTER_NAME
$ aws iam attach-role-policy - policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy - role-name nodes.$KOPS_CLUSTER_NAME

사용자 데이터와 메타데이터

[참고 — 인스턴스 메타데이터 및 사용자 데이터]

사용자데이터와 메타데이터 관계
  • 사용자 데이터 : 인스턴스 생성과 함께 전달되는 최대 16KB 크기의 텍스트 데이터
  • 메타 데이터 : 실행 중인 인스턴스를 구성 또는 관리하는 데 사용될 수 있는 인스턴스 관련 데이터. 예를 들어 호스트 이름, 이벤트 및 보안 그룹과 같은 범주로 분류

다음 방법 중 하나를 사용하여 실행 중인 인스턴스에서 인스턴스 메타데이터에 액세스할 수 있습니다.

  • 인스턴스 메타데이터 서비스 버전 1(IMDSv1) — 요청/응답 방법
  • 인스턴스 메타데이터 서비스 버전 2(IMDSv2) — 세션 지향 방법

로컬 코드 또는 사용자가 IMDSv2를 사용해야 하도록 각 인스턴스에서 인스턴스 메타데이터 서비스를 구성할 수 있습니다.

인스턴스 메타데이터 서비스 버전 2 작동 방식

  • IMDSv2는 세션 지향 요청을 사용합니다. 세션 지향 요청을 사용하여 세션 기간을 정의하는 세션 토큰을 생성합니다. 세션 기간은 최소 1초에서 최대 6시간일 수 있습니다.
  • 지정된 기간 중에는 후속 요청에 동일한 세션 토큰을 사용할 수 있습니다. 지정된 기간이 만료된 후에는 향후 요청에 사용할 새로운 세션 토큰을 생성할 수 있습니다.

워커 노드 대 EC2 메타데이터(IMDSv2) 보안 제거

#
$ kops edit ig nodes-ap-northeast-2a
 - -
# 아래 3줄 제거
spec:
 instanceMetadata:
   httpPutResponseHopLimit: 1
   httpTokens: required
 - -

# 업데이트 적용 : 노드1대 롤링업데이트
$ kops update cluster - yes && echo && sleep 3 && kops rolling-update cluster - yes

EC IAM Role & 메타데이터

파드에서 EC2 메타데이터 사용 가능 확인

# netshoot-pod 생성
$ cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
 name: netshoot-pod
spec:
 replicas: 2
 selector:
 matchLabels:
 app: netshoot-pod
 template:
 metadata:
 labels:
 app: netshoot-pod
 spec:
 containers:
 - name: netshoot-pod
 image: nicolaka/netshoot
 command: ["tail"]
 args: ["-f", "/dev/null"]
 terminationGracePeriodSeconds: 0
EOF

# 파드 이름 변수 지정
$ PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
$ PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})

# EC2 메타데이터 정보 확인
$ kubectl exec -it $PODNAME1 - curl 169.254.169.254 ;echo
$ kubectl exec -it $PODNAME2 - curl 169.254.169.254 ;echo

# 파드1에서 EC2 메타데이터 정보 확인
$ kubectl exec -it $PODNAME1 - curl 169.254.169.254/latest ;echo
$ kubectl exec -it $PODNAME1 - curl 169.254.169.254/latest/meta-data/iam/security-credentials/ ;echo
$ kubectl exec -it $PODNAME1 - curl 169.254.169.254/latest/meta-data/iam/security-credentials/nodes.$KOPS_CLUSTER_NAME | jq

# 파드2에서 EC2 메타데이터 정보 확인
$ kubectl exec -it $PODNAME2 - curl 169.254.169.254/latest ;echo
$ kubectl exec -it $PODNAME2 - curl 169.254.169.254/latest/meta-data/iam/security-credentials/ ;echo
$ kubectl exec -it $PODNAME2 - curl 169.254.169.254/latest/meta-data/iam/security-credentials/nodes.$KOPS_CLUSTER_NAME | jq

# 메타데이터 메모해두기
{
 "Code": "Success",
 "LastUpdated": "2023–03–01T14:11:26Z",
 "Type": "AWS-HMAC",
 "AccessKeyId": "XXXXXXXXXXXXXXXXXXX",
 "SecretAccessKey": "XXXXXXXXXXXXXXXXX",
 "Token": "XXXXXXXXXXXXXXXXXXXXXXXX",
 "Expiration": "2023–03–01T20:12:11Z"
}

파드(컨테이너)탈취 후 EC2 메타데이터의 IAM Role 토큰 정보를 활용해 python boto3 를 통해 SDK로 AWS 서비스 강제 사용 [참고-aws boto3]

  • Boto3란 : Python용 AWS SDK
  • 기본적으로 AWS 리소스에 접근하기 위해서는 필연적으로 AWS IAM(AWS Identity and Access Management) 으로부터 권한을 받아야합니다.
  • Boto3에는 자격증명(credentials)(aws_access_key_id , aws_secret_access_key 및 aws_session_token 항목) 과 비자격증명(non-credentials)(사용할 리전 또는 Amazon S3에 사용할 주소 지정 스타일과 같은 항목)의 두 가지 구성 데이터 유형이 있습니다.

Boto3가 AWS의 자격증명(Credentials)을 찾는 메커니즘은 아래와 같습니다.

  1. boto.client()메서드 에서 자격 증명을 매개 변수로 전달
# 예시
import boto3

client = boto3.client(
    's3',
    aws_access_key_id=ACCESS_KEY,
    aws_secret_access_key=SECRET_KEY,
    aws_session_token=SESSION_TOKEN
)

2. Session개체를 만들 때 자격 증명을 매개 변수로 전달

# 예시
import boto3

session = boto3.Session(
    aws_access_key_id=ACCESS_KEY,
    aws_secret_access_key=SECRET_KEY,
    aws_session_token=SESSION_TOKEN
)

3. 환경 변수

# 예시
AWS_ACCESS_KEY_ID- AWS 계정의 액세스 키.

AWS_SECRET_ACCESS_KEY- AWS 계정의 비밀 키.

AWS_SESSION_TOKEN- AWS 계정의 세션 키. 임시 자격 증명을 사용하는 경우에만 필요.

4. 공유 자격 증명 파일( ~/.aws/credentials)

# 예시
[default]
aws_access_key_id=foo
aws_secret_access_key=bar

[dev]
aws_access_key_id=foo2
aws_secret_access_key=bar2

[prod]
aws_access_key_id=foo3
aws_secret_access_key=bar3

5. AWS 구성 파일( ~/.aws/config)

# 예시
[default]
aws_access_key_id=foo
aws_secret_access_key=bar

[profile dev]
aws_access_key_id=foo2
aws_secret_access_key=bar2

[profile prod]
aws_access_key_id=foo3
aws_secret_access_key=bar3

6. 역할 공급자 가정

# 예시
# In ~/.aws/credentials:
[development]
aws_access_key_id=foo
aws_access_key_id=bar

# In ~/.aws/config
[profile crossaccount]
role_arn=arn:aws:iam:...
source_profile=development

7. Boto2 구성 파일( /etc/boto.cfg및 ~/.boto)

# Example ~/.boto file
[Credentials]
aws_access_key_id = foo
aws_secret_access_key = bar

8. IAM 역할이 구성된 Amazon EC2 인스턴스의 인스턴스 메타데이터 서비스

  • Amazon EC2에서 실행 중이고 위의 공급자가 자격 증명을 찾지 못한 경우 Boto3는 인스턴스 메타데이터 서비스에서 자격 증명을 로드하려고 시도합니다. 이 기능을 활용하려면 EC2 인스턴스를 시작할 때 사용할 IAM 역할을 지정해야 합니다.
# boto3 사용을 위한 파드 생성
$ cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
 name: boto3-pod
spec:
 replicas: 2
 selector:
 matchLabels:
 app: boto3
 template:
 metadata:
 labels:
 app: boto3
 spec:
 containers:
 - name: boto3
 image: jpbarto/boto3
 command: ["tail"]
 args: ["-f", "/dev/null"]
 terminationGracePeriodSeconds: 0
EOF


# 파드1에서 boto3 사용
$ kubectl exec -it $PODNAME1 - sh
 - - - - - - 
$ cat <<EOF> ec2.py
import boto3

ec2 = boto3.client('ec2', region_name = 'ap-northeast-2')
response = ec2.describe_instances()
print(response)
EOF

python ec2.py # aws ec2 describe-vpcs
exit
 - - - - - - 

# 파드2에서 boto3 사용
$ kubectl exec -it $PODNAME2 - sh
 - - - - - - 
$ cat <<EOF> ec2.py
import boto3

ec2 = boto3.client('ec2', region_name = 'ap-northeast-2')
response = ec2.describe_instances()
print(response)
EOF

python ec2.py # aws ec2 describe-vpcs
exit
 - - - - - - 

# 실습 완료 후 삭제
$ kubectl delete deploy boto3-pod
  • 대응 방안 : EC2 메타데이터 접속 제한 or AWS IRSA(IAM Role for Service Account) 사용

AWS IRSA : POD별로 권한 분리를 하기 위한 방안(Service Account IAM Role을 설정하고 Service Account를 Pod에 할당) (또는 OIDC를 이용하여 권한 분리)

Kubescape

  • 보안 권고 사항 기반 현재 쿠버네티스 클러스터의 취약점을 점검합니다.
# 설치
$ curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash

# Download all artifacts and save them in the default path (~/.kubescape)
$ kubescape download artifacts
$ tree ~/.kubescape/
/root/.kubescape/
├── allcontrols.json
├── armobest.json
├── attack-tracks.json
├── cis-eks-t1.2.0.json
├── cis-v1.23-t1.0.1.json
├── controls-inputs.json
├── devopsbest.json
├── exceptions.json
├── mitre.json
└── nsa.json

0 directories, 10 files

$ cat ~/.kubescape/attack-tracks.json | jq

# 제공하는 보안 프레임워크 확인
$ kubescape list frameworks --format json | jq '.[]'
"AllControls"
"ArmoBest"
"DevOpsBest"
"MITRE"
"NSA"
"cis-eks-t1.2.0"
"cis-v1.23-t1.0.1"

# 제공하는 통제 정책 확인
$ kubescape list controls

파드/컨테이너 보안 컨텍스트

  1. 컨테이너 보안 컨텍스트 SecurityContext : 각 컨테이너에 대한 보안 설정을 통해 침해사고 발생 시 침해사고를 당한 권한을 최대한 축소하여 그 사고에 대한 확대를 방지합니다.
# SecurityContext 예시
apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo-2
spec:
  securityContext:
    runAsUser: 1000
  containers:
  - name: sec-ctx-demo-2
    image: gcr.io/google-samples/node-hello:1.0
    securityContext:
      runAsUser: 2000
      allowPrivilegeEscalation: false

위의 예시에 표시된 SecurityContext 의 의미는 아래와 같습니다.

  • runAsUser : 실행 사용자
  • allowPrivilegeEscalation : 컨테이너 실행 시 상위 프로세스보다 많은 권한을 부여할지 여부
$ kubectl get pod -n kube-system -o jsonpath={.items[*].spec.containers[*].securityContext} | jq
{
  "allowPrivilegeEscalation": false,
  "readOnlyRootFilesystem": true,
  "runAsNonRoot": true
}
{
  "capabilities": {
    "add": [
      "NET_ADMIN",
      "NET_RAW"
    ]
  }
}
...
  • readOnlyRootFilesystem : root 파일 시스템을 읽기 전용으로 사용
#
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: rootfile-readonly
spec:
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
    securityContext:
      readOnlyRootFilesystem: true
  terminationGracePeriodSeconds: 0
EOF

# 파일 생성 시도
$ kubectl exec -it rootfile-readonly -- touch /tmp/text.txt
touch: /tmp/text.txt: Read-only file system
command terminated with exit code 1
  • Linux Capabilities : Linux는 전통적으로 수퍼유저와 관련된 권한을 별개의 단위인 Capabilities으로 나눕니다. 즉, Capabilities는 thread별 속성입니다.
# capsh --print
Current: =ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
Ambient set =
Current IAB: 
Securebits: 00/0x0/1'b0 (no-new-privs=0)
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=0(root) euid=0(root)
gid=0(root)
groups=0(root)
Guessed mode: UNCERTAIN (0)

# proc 에서 확인 : bit 별 Capabilities
# cat /proc/1/status | egrep 'CapPrm|CapEff'
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff

# 마스터 노드 Linux Capabilities 확인
$ ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME capsh --print
WARNING: libcap needs an update (cap=40 should have a name).
Current: =
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,38,39,40
Ambient set =
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=1000(ubuntu) euid=1000(ubuntu)
gid=1000(ubuntu)
groups=4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),118(netdev),119(lxd),1000(ubuntu)
Guessed mode: UNCERTAIN (0)
  • 파드의 Linux Capabilities 기본 확인
# 샘플 파드 생성
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: sample-capabilities
spec:
  containers:
  - name: nginx-container
    image: masayaaoyama/nginx:capsh
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

# 파드의 Linux Capabilities 기본 확인
$ kubectl 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_mknod,cap_audit_write,cap_setfcap+ep

# proc 에서 확인 : bit 별 Capabilities - 링크
$ kubectl exec -it sample-capabilities -- cat /proc/1/status | egrep 'CapPrm|CapEff'
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb

# 파드에서 시간 변경 시도
$ kubectl exec -it sample-capabilities -- date
Thu Apr  6 16:21:22 UTC 2023

# 파드에서 시간 변경 시도
$ kubectl exec -it sample-capabilities -- date -s "12:00:00"
date: cannot set date: Operation not permitted
Thu Apr  6 12:00:00 UTC 2023
command terminated with exit code 1

$ kubectl exec -it sample-capabilities -- date
Thu Apr  6 16:22:52 UTC 2023

# 파드가 배포된 워커노드 확인
NAME                            READY   STATUS    RESTARTS   AGE    IP              NODE                  NOMINATED NODE   READINESS GATES
awscli-pod-5c87478747-2chct     1/1     Running   0          145m   172.30.62.48    i-0d8582550153273dc   <none>           <none>
awscli-pod-5c87478747-44jc2     1/1     Running   0          145m   172.30.85.5     i-02f69111c92e36490   <none>           <none>
netshoot-pod-7757d5dd99-kvbhh   1/1     Running   0          157m   172.30.53.131   i-0d8582550153273dc   <none>           <none>
netshoot-pod-7757d5dd99-mz6tc   1/1     Running   0          157m   172.30.85.4     i-02f69111c92e36490   <none>           <none>
rootfile-readonly               1/1     Running   0          45m    172.30.53.132   i-0d8582550153273dc   <none>           <none>
sample-capabilities             1/1     Running   0          23m    172.30.85.6     i-02f69111c92e36490   <none>           <none>


# 파드가 배포된 워커노드 IP 확인 
$ kubectl get node -o wide | grep i-02f69111c92e36490
i-02f69111c92e36490   Ready    node            168m   v1.24.11   172.30.80.213   52.79.52.36     Ubuntu 20.04.5 LTS   5.15.0-1031-aws   containerd://1.6.18

# 워커 노드 외부 IP로 접근
$ ssh -i ~/.ssh/id_rsa ubuntu@52.79.52.36

# 워커 노드에 systemd-timesyncd 종료
root@i-02f69111c92e36490:/home/ubuntu# systemctl stop systemd-timesyncd


# sample-capabilities 파드 시간이 변경되지않음을 알 수 있다.
$ kubectl exec -it sample-capabilities -- date
Thu Apr  6 16:48:42 UTC 2023

$ kubectl exec -it sample-capabilities -- date -s "12:00:00"
date: cannot set date: Operation not permitted
Thu Apr  6 12:00:00 UTC 2023
command terminated with exit code 1

$ kubectl exec -it sample-capabilities -- date
Thu Apr  6 16:48:52 UTC 2023

# 파드에 Linux Capabilities 부여 및 삭제
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: sample-capabilities2
spec:
  containers:
  - name: nginx-container
    image: masayaaoyama/nginx:capsh
    command: ["tail"]
    args: ["-f", "/dev/null"]
    securityContext:
      capabilities:
        add: ["NET_ADMIN", "SYS_TIME"]
        drop: ["AUDIT_WRITE"]
  terminationGracePeriodSeconds: 0
EOF

# 파드의 Linux Capabilities 기본 확인
$ kubectl exec -it sample-capabilities2 -- 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_admin,cap_net_raw,cap_sys_chroot,cap_sys_time,cap_mknod,cap_setfcap+ep

# 파드 상세 정보 확인
$ kubectl get pod sample-capabilities2 -o jsonpath={.spec.containers[0].securityContext} | jq
{
  "capabilities": {
    "add": [
      "NET_ADMIN",
      "SYS_TIME"
    ],
    "drop": [
      "AUDIT_WRITE"
    ]
  }
}

$ kubectl exec -it sample-capabilities2 -- cat /proc/1/status | egrep 'CapPrm|CapEff'
CapPrm: 000000008a0435fb
CapEff: 000000008a0435fb

# 파드가 배포된 워커노드 확인
NAME                            READY   STATUS    RESTARTS   AGE    IP              NODE                  NOMINATED NODE   READINESS GATES
awscli-pod-5c87478747-2chct     1/1     Running   0          145m   172.30.62.48    i-0d8582550153273dc   <none>           <none>
awscli-pod-5c87478747-44jc2     1/1     Running   0          145m   172.30.85.5     i-02f69111c92e36490   <none>           <none>
netshoot-pod-7757d5dd99-kvbhh   1/1     Running   0          157m   172.30.53.131   i-0d8582550153273dc   <none>           <none>
netshoot-pod-7757d5dd99-mz6tc   1/1     Running   0          157m   172.30.85.4     i-02f69111c92e36490   <none>           <none>
rootfile-readonly               1/1     Running   0          45m    172.30.53.132   i-0d8582550153273dc   <none>           <none>
sample-capabilities             1/1     Running   0          23m    172.30.85.6     i-02f69111c92e36490   <none>           <none>
sample-capabilities2            1/1     Running   0          11m    172.30.53.133   i-0d8582550153273dc   <none>           <none>

# 파드가 배포된 워커노드 IP 확인 
$ kubectl get node -o wide | grep i-02f69111c92e36490
i-0d8582550153273dc   Ready    node            3h10m   v1.24.11   172.30.59.106   3.39.6.121      Ubuntu 20.04.5 LTS   5.15.0-1031-aws   containerd://1.6.18

# 워커 노드 외부 IP로 접근
$ ssh -i ~/.ssh/id_rsa ubuntu@3.39.6.121

# 워커 노드에 systemd-timesyncd 종료
root@i-0d8582550153273dc:/home/ubuntu# systemctl stop systemd-timesyncd

# sample-capabilities2 파드 시간이 변경됨을 알 수 있다.
$ kubectl exec -it sample-capabilities2 -- date -s "12:00:00"
Thu Apr  6 12:00:00 UTC 2023

$ kubectl exec -it sample-capabilities2 -- date
Thu Apr  6 12:00:06 UTC 2023

2. 파드 보안 컨텍스트 : 파드에 포함된 모든 컨테이너가 영향을 받습니다.

  • runAsUser 변경 : runuser 파드는 실행 사용자를 nobody(UID:65534) 사용자로 실행, 실행권한에 서브그룹 1001/1002 추가
#
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: rundefault
spec:
  containers:
  - name: centos
    image: centos:7
    command: ["tail"]
    args: ["-f", "/dev/null"]
    securityContext:
      readOnlyRootFilesystem: true
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: runuser
spec:
  securityContext:
    runAsUser: 65534
    runAsGroup: 65534
    supplementalGroups:
    - 1001
    - 1002
  containers:
  - name: centos
    image: centos:7
    command: ["tail"]
    args: ["-f", "/dev/null"]
    securityContext:
      readOnlyRootFilesystem: true
  terminationGracePeriodSeconds: 0
EOF

#
$ kubectl get pod rundefault -o jsonpath={.spec.securityContext} | jq
$ kubectl get pod runuser -o jsonpath={.spec.securityContext} | jq
{
  "runAsGroup": 65534,
  "runAsUser": 65534,
  "supplementalGroups": [
    1001,
    1002
  ]
}

# 실행 사용자 정보 확인
$ kubectl exec -it rundefault -- id
uid=0(root) gid=0(root) groups=0(root)

$ kubectl exec -it rundefault -- id
uid=0(root) gid=0(root) groups=0(root)

$ kubectl exec -it runuser    -- id
uid=65534 gid=65534 groups=65534,1001,1002

# 프로세스 정보 확인
$ kubectl exec -it rundefault -- ps -axo uid,user,gid,group,pid,comm
  UID USER       GID GROUP        PID COMMAND
    0 root         0 root           1 tail
    0 root         0 root          19 ps

$ kubectl exec -it runuser    -- ps -axo uid,user,gid,group,pid,comm
  UID USER       GID GROUP        PID COMMAND
65534 65534    65534 65534          1 tail
65534 65534    65534 65534         12 ps
  • runAsNonRoot 로 실행 제한 시 아래와 같은 에러 발생
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: nonroot
spec:
  securityContext:
    runAsNonRoot: true
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

# 이벤트 확인
$ kubectl events --for pod/nonroot
LAST SEEN               TYPE      REASON      OBJECT        MESSAGE
4h47m                   Normal    Pulled      Pod/nonroot   Successfully pulled image "nicolaka/netshoot" in 1.32982942s
4h47m                   Warning   Failed      Pod/nonroot   Error: container has runAsNonRoot and image has non-numeric user (root), cannot verify user is non-root (pod: "nonroot_default(9796696b-d387-42e8-9a14-0c5ad32526d1)", container: netshoot)
4h47m (x2 over 4h47m)   Normal    Pulling     Pod/nonroot   Pulling image "nicolaka/netshoot"
3s                      Normal    Scheduled   Pod/nonroot   Successfully assigned default/nonroot to i-0d8582550153273dc

— Error: container has runAsNonRoot and image has non-numeric user (root), cannot verify user is non-root (pod: “nonroot_default(9796696b-d387–42e8–9a14–0c5ad32526d1)”, container: netshoot)

  • fsGroup(파일시스템그룹) 지정 : 일반적으로 마운트한 볼륨의 소유자와 그룹은 root:root로 되어 있습니다. 실행 사용자를 변경한 경우에는 마운트한 볼륨에 권한이 없는 경우가 있습니다. 따라서 마운트하는 볼륨의 그룹을 변경할 수 있습니다.
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: fsgoup1
spec:
  volumes:
  - name: vol1
    emptyDir: {}
  containers:
  - name: centos
    image: centos:7
    command: [ "sh", "-c", "sleep 1h" ]
    volumeMounts:
    - name: vol1
      mountPath: /data/demo
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: fsgoup2
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  volumes:
  - name: vol2
    emptyDir: {}
  containers:
  - name: centos
    image: centos:7
    command: [ "sh", "-c", "sleep 1h" ]
    volumeMounts:
    - name: vol2
      mountPath: /data/demo
  terminationGracePeriodSeconds: 0
EOF

$ kubectl get pod fsgoup1 -o jsonpath={.spec.securityContext} | jq
$ kubectl get pod fsgoup2 -o jsonpath={.spec.securityContext} | jq
{
  "fsGroup": 2000,
  "runAsGroup": 3000,
  "runAsUser": 1000
}

# 실행 사용자 정보 확인
$ kubectl exec -it fsgoup1 -- id
uid=0(root) gid=0(root) groups=0(root)

$ kubectl exec -it fsgoup2 -- id
uid=1000 gid=3000 groups=3000,2000

# 프로세스 정보 확인
$ kubectl exec -it fsgoup1 -- ps -axo uid,user,gid,group,pid,comm
 UID USER       GID GROUP        PID COMMAND
    0 root         0 root           1 sleep
    0 root         0 root          19 ps

# 디렉터리 정보 확인 : fsgoup2파드의 마운트 볼륨 그룹의 GID가 2000 (fsGroup: 2000)
$ kubectl exec -it fsgoup2 -- ps -axo uid,user,gid,group,pid,comm
  UID USER       GID GROUP        PID COMMAND
 1000 1000      3000 3000           1 sleep
 1000 1000      3000 3000          14 ps

$ kubectl exec -it fsgoup1 -- ls -l /data
total 4
drwxrwxrwx 2 root root 4096 Apr  6 16:59 demo

$ kubectl exec -it fsgoup2 -- ls -l /data
total 4
drwxrwsrwx 2 root 2000 4096 Apr  6 16:59 demo

# fsgoup2파드에서 파일 생성 및 확인
$kubectl exec -it fsgoup2 -- sh -c "echo write > /data/demo/sample.txt"
$# kubectl exec -it fsgoup2 -- cat /data/demo/sample.txt
write
$ kubectl exec -it fsgoup2 -- ls -l /data/demo/sample.txt
-rw-r--r-- 1 1000 2000 6 Apr  6 17:01 /data/demo/sample.txt

# fsgoup2파드에서 다른 디렉토리에 파일 생성 시도 - 실패
$  kubectl exec -it fsgoup2 -- sh -c "echo write > /data/sample.txt"
sh: /data/sample.txt: Permission denied
command terminated with exit code 1
  • sysctl을 사용한 커널 파라미터 설정 : 커널 파라미터 변경 적용을 위해서는 컨테이너에서도 설정이 필요합니다. 네임스페이스 sysctl을 구성하기 위해서 파드 securityContext를 사용합니다. securityContext는 동일한 파드의 모든 컨테이너에 적용됩니다.
  • sysctl은 safe sysctl과 unsafe sysctl로 구성되어 있습니다.
  • 즉, 하나의 파드에 safe sysctl을 설정한다는 것은 다음을 의미합니다.

— 노드의 다른 파드에 영향을 미치지 않아야 한다

— 노드의 상태를 손상시키지 않아야 한다

— CPU 또는 메모리 리소스가 파드의 리소스 제한에 벗어나는 것을 허용하지 않아야 한다

다음 sysctl은 safe 명령을 지원합니다.

  • kernel.shm_rmid_forced,
  • net.ipv4.ip_local_port_range,
  • net.ipv4.tcp_syncookies,
  • net.ipv4.ping_group_range (쿠버네티스 1.18 이후),
  • net.ipv4.ip_unprivileged_port_start (쿠버네티스 1.22 이후).

# unsafe 파라미터를 변경 시도
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: unsafe
spec:
  securityContext:
    sysctls:
    - name: net.core.somaxconn
      value: "12345"
  containers:
    - name: centos-container
      image: centos:7
      command: ["/bin/sleep", "3600"]
  terminationGracePeriodSeconds: 0
EOF

$ kubectl events --for pod/unsafe
LAST SEEN   TYPE      REASON            OBJECT       MESSAGE
4h47m       Warning   SysctlForbidden   Pod/unsafe   forbidden sysctl: "net.core.somaxconn" not allowlisted
4s          Normal    Scheduled         Pod/unsafe   Successfully assigned default/unsafe to i-0d8582550153273dc
# safe 파라미터 수정 - sysctl2 파드가 배포된 노드의 net.ipv4.ip_local_port_range 값과 다를 경우

$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: sysctl1
spec:
  containers:
    - name: centos-container
      image: centos:7
      command: ["/bin/sleep", "3600"]
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: sysctl2
spec:
  securityContext:
    sysctls:
    - name: net.ipv4.ip_local_port_range
      value: "1025 61000"
  containers:
    - name: centos-container
      image: centos:7
      command: ["/bin/sleep", "3600"]
  terminationGracePeriodSeconds: 0
EOF

#
$ kubectl get pod sysctl1 -o jsonpath={.spec.securityContext} | jq
$ kubectl get pod sysctl2 -o jsonpath={.spec.securityContext} | jq
{
  "sysctls": [
    {
      "name": "net.ipv4.ip_local_port_range",
      "value": "1025 61000"
    }
  ]
}

$ kubectl exec -it sysctl1 -- sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 60999

$ kubectl exec -it sysctl2 -- sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 1025 61000


# initContainer 와 privileged 를 활용하여 unsafe 커널 파라미터를 강제로 변경

$ curl -s -O https://raw.githubusercontent.com/MasayaAoyama/kubernetes-perfect-guide/ko/2nd-edition/samples/chapter13/sample-sysctl-initcontainer.yaml
$ cat sample-sysctl-initcontainer.yaml| yh
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

$ kubectl apply -f sample-sysctl-initcontainer.yaml

$ kubectl exec -it sample-sysctl-initcontainer -c tools-container -- sysctl net.core.somaxconn
net.core.somaxconn = 12345

polaris

  • 오픈소스 보안 점검 도구입니다.
# 설치
$ kubectl create ns polaris

#
$ cat <<EOT > polaris-values.yaml
dashboard:
  replicas: 1
  service:
    type: LoadBalancer
EOT

# 배포
$ helm repo add fairwinds-stable https://charts.fairwinds.com/stable
$ helm install polaris fairwinds-stable/polaris --namespace polaris --version 5.7.2 -f polaris-values.yaml

# CLB에 ExternanDNS 로 도메인 연결
$ kubectl annotate service polaris-dashboard "external-dns.alpha.kubernetes.io/hostname=polaris.$KOPS_CLUSTER_NAME" -n polaris

# 웹 접속 주소 확인 및 접속
$ echo -e "Polaris Web URL = http://polaris.$KOPS_CLUSTER_NAME"

K8S 인증/인가 & RBAC

  • 서비스 어카운트(Service Account)
  • API 서버 사용 : kubectl(config, 다수 클러스터 관리 가능), 서비스 어카운트, https(x.509 Client Certs)
  • API 서버 접근 과정 : 인증 → 인가 → Admission Control(API 요청 검증, 필요 시 변형 — 예. ResourceQuota, LimitRange)

인증(Authentication)

  • X.509 Client Certs : kubeconfig 에 CA crt(발급 기관 인증서) , Client crt(클라이언트 인증서) , Client key(클라이언트 개인키) 를 통해 인증
  • kubectl : 여러 클러스터(kubeconfig)를 관리 가능 — contexts 에 클러스터와 유저 및 인증서/ 참고
  • Service Account : 기본 서비스 어카운트(default) — 시크릿(CA crt 와 token)
참고 : 김태민 — 기술 블로그(인증(Authentication))

인가(Authorization)

  • 인가 방식 : RBAC(Role, RoleBinding), ABAC, Webhook, Node Authorization
  • RBAC : 역할 기반의 권한 관리, 사용자와 역할을 별개로 선언 후 두가지를 조합(binding)해서 사용자에게 권한을 부여하여 kubectl or API로 관리 가능
  • Namespace/Cluster — Role/ClusterRole, RoleBinding/ClusterRoleBinding, Service Account
  • Role(롤) — (RoleBinding 롤 바인딩) — Service Account(서비스 어카운트) : 롤 바인딩은 롤과 서비스 어카운트를 연결
  • Role(네임스페이스내 자원의 권한) vs ClusterRole(클러스터 수준의 자원의 권한)
참고 : 김태민 — 기술 블로그(인가(Authorization))
 

 

blog migration project

written in 2023.4.6

https://medium.com/techblog-hayleyshim/k8s-security-11147c78ee28