본문 바로가기
카테고리 없음

[AEWS-3기] 도전과제-Bottlerocket Security Features on Amazon EKS 따라해보기

by james_janghun 2025. 2. 21.

 

이번 포스팅은 AEWS 학습에서 Bottlerocket Security Features on Amazon EKS 워크숍을 그대로 따라하면서 정리한 내용이다.해당 워크숍은 EKS 노드그룹으로 Bottlerocket OS를 사용하는 내용이다. 노드그룹의 OS 업데이트를 API 방식을 통해서 진행하려고 한다. 

 

EKS Bottlerocket 노드그룹 생성

해당 스크립트를 통해서 노드 그룹을 생성하자. 2개를 생성하는데 한 개는 일반적인 bottlerocket이고 다음 것은 그래도 어느정도 설정을 스크립트 상에서 실시한 것이다.

cat << EOF > ng-br.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: myeks
  region: ap-northeast-2
  version: "1.31"

managedNodeGroups:
- name: ng-bottlerocket
  instanceType: m5.large
  amiFamily: Bottlerocket
  desiredCapacity: 1
  maxSize: 1
  minSize: 1
  labels:
    alpha.eksctl.io/cluster-name: myeks
    alpha.eksctl.io/nodegroup-name: ng-bottlerocket
    ami: bottlerocket
  subnets:
  - $PubSubnet1
  - $PubSubnet2
  - $PubSubnet3
  tags:
    alpha.eksctl.io/nodegroup-name: ng-bottlerocket
    alpha.eksctl.io/nodegroup-type: managed

- name: ng-bottlerocket-ssh
  instanceType: m5.large
  amiFamily: Bottlerocket
  bottlerocket:
    enableAdminContainer: true
    settings:
      motd: "Hello, eksctl!"
  desiredCapacity: 1
  maxSize: 1
  minSize: 1
  ssh:
    allow: true
    publicKeyName: $SSHKEYNAME
  labels:
    alpha.eksctl.io/cluster-name: myeks
    alpha.eksctl.io/nodegroup-name: ng-bottlerocket-ssh
    ami: bottlerocket
  subnets:
  - $PubSubnet1
  - $PubSubnet2
  - $PubSubnet3
  tags:
    alpha.eksctl.io/nodegroup-name: ng-bottlerocket-ssh
    alpha.eksctl.io/nodegroup-type: managed
EOF

 

해당 스크립트에서 주의 깊게 봐야할 부분이라면 다음과 같다.

 

- amiFamily 및 ami는 bottlerocket을 사용해 지정한다.

- bottlerocket 키값에 enableAdminContainer를 true로 설정하면 생성시 바로 admin container가 활성화된다.

- bottlerocket setting 중에서 다양한 값들이 존재하는데(마지막에 배운다) 이 설정을 미리 할 수 있다. 아래 스크립트에서는 motd값을 설정한 것을확인 할 수 있다.

- ssh 등 다양한 설정이 가능하다. 참고로 bottlerocket은 ssh를 권장하지 않는다.

- name: ng-bottlerocket
  instanceType: m5.large
  amiFamily: Bottlerocket
  bottlerocket:
    enableAdminContainer: true
    settings:
      motd: "Hello, eksctl!"
  ssh:
    allow: true
    publicKeyName: $SSHKEYNAME
  labels:
    alpha.eksctl.io/cluster-name: myeks
    alpha.eksctl.io/nodegroup-name: ng-bottlerocket
    ami: bottlerocket

 

 

노드 그룹 확인하기

아래 명령어를 통해서 bottlerocket을 사용하는 노드그룹을 미리 확인할 수 있는데, bottlerocker이미지를 사용하는지 판단하면 된다.

eksctl get cluster -n $EKS_CLUSTER -r $AWS_REGION -o json | jq -M ".[] | {Name,Version,Status,CreatedAt}"
eksctl get nodegroups -c $EKS_CLUSTER -r $AWS_REGION -o json | jq -M ".[] | {Cluster,Name,Status,ImageID,Type}"
eksctl get nodegroup -c $EKS_CLUSTER -n $BR_MNG_NAME -r $AWS_REGION -o json | jq -M ".[] | {Cluster,Name,Status,MaxSize,MinSize,DesiredCapacity,InstanceType,ImageID}"

 

 

참고) 만약 Bottlerocket을 사용하는 이미지의 노드그룹이 없다면 아래 명령을 통해서 바로 scale up 할 수 있다.

if [ `kubectl get nodes -l eks.amazonaws.com/nodegroup=$BR_MNG_NAME -o json | jq -r '.items | length'` -gt 0 ]; then 
  echo -e "\nBottlerocket Managed Node Group has nodes. No need to scale.\n\n"
else
  echo -e "\nBottlerocket Managed Node Group has no nodes. Scaling to one node.\n\n"
  eksctl scale nodegroup -c $EKS_CLUSTER -n $BR_MNG_NAME -r $AWS_REGION --nodes 1
fi

 

scale up 했을 경우 Ready 상태를 기다리기 위해 다음 명령어를 사용한다.

while [ "`kubectl get nodes -l eks.amazonaws.com/nodegroup=$BR_MNG_NAME | grep -v STATUS | awk '{print $2}'`" != "Ready" ]
do 
  echo -e "`date` - Waiting for the Bottlerocket Node to be ready. Please wait...\n"
  sleep 15
done
echo -e "\nBottlerocket Node is ready. Please proceed with the next steps.\n"

 

Bottlerocket 호스트 엑세스

잘보면 SSM을 통해서 bottlerocket EC2에 접속이 가능하다. 아마 AWS에서 가장 권장하는 방법이 아닐까 싶다.

기본적으로 Bottlerocket EC2에 접속하면 다음과 같은 메시지가 나온다. 내용을 한번 살펴보자.

aws ssm start-session --target $INSTANCE_ID

 

일단 우리가 처음 접속하면 Bottlerocket의 컨트롤 컨테이너에 접속된다.

 

접속자의 세션 ID

Starting session with SessionId: james-y79kzlpced59i6bzagujb4ieta

 

접속된 세션 ID를 해당 콘솔에 그대로 표시해준다.

“Welcome to Bottlerocket’s control container!”

 

우리가 Bottlerocket control 컨테이너에 접속했음을 알린다.

또한 이 컨테이너를 통해 Bottlerocket API에 접속할 수 있고, 시스템 구성이나 검사가 가능하다고 알려준다.

 

예를 들어 여기서 알려주는 apiclient라는 명령을 통해서 설정 값을 조회하는 것을 해보자.

apiclient -u /settings

 

여기서 json으로 정보가 엄청 나게 나열되는데, json parser를 통해서 나오는 정보를 트리구조로 보았다.

 

 

우리가 여기서 확인해볼 의미있는 정보는 커널이나 host-containers 정보 정도가 될 것 같다.

개념에서 살펴본 것과 같이 host-container는 admin과 control 컨테이너로 나뉘고 있다. 기본적으로 admin 컨테이너는 비활성화 상태지만 스크립트 상 활성화 시켰기 때문에 활성화상태이다.

 

구체적인 정보를 조회하고 싶다면 apiclient get을 통해서 직접 조회가 가능하다.

추후에 만약 해당 정보를 가지고 업그레이드 여부를 판단해야한다면 바로 apiclient를 통해서 해당 정보를 추적하는 것이 좋겠다고 생각이 들었다.

apiclient get settings.host-containers

아까 위의 조회 정보와 동일하다.

 

Bottlerocket 이미지 구성

여기서 잠깐 개념설명을 하고 가자면 Bottlerocket 이미지는 다음과 같이 admin과 control 호스트 컨테이너가 존재한다. 이는 잘보면 호스트 서버 내에서도 containerd 런타임을 통해서 또 한번 분리되어 시스템 영향을 최소화 시키는 것을 확인할 수 있다. 우리가 애플리케이션을 돌리는 부분은 오른쪽 kubelet을 통해 pod를 구동시키는 것을 확인할 수 있다.

https://bottlerocket.dev/en/os/1.30.x/concepts/components/

 

이곳에서 정말 자세하게 다루고 있으니 확인해보면 좋을것이다.

참고로 ECS는 다음과 같은 구조이다.

 

ECS Bottlerocket

차이라고 한다면 ECS를 위한 ecs-agent가 있다는 점이다.

 

admin container 활성화

다시 원래대로 SSM으로 돌아와서 admin container를 활성화 시켜보자. 기본적으로는 비활성화 상태이므로 다음 명령어를 통해서 실행해줘야한다. admin 컨테이너는 시스템 디버깅이나 루트의 작업을 요하는 경우 혹은 추가적인 네트워크 경로 등을 필요로 하는 경우에 액세스를 제공한다.

enable-admin-container

 

이 명령어를 통해서 admin container로 들어갈 수 있다.

enter-admin-container

참고로 활성화된 admin container를 비활성화 시키려면 간단하게 disable 시키면 된다.

disable-admin-container

 

Bottlerocket 호스트 업그레이드

Bottlerocket OS의 업그레이드가 필요할 경우 다음과 같이 진행한다. 2가지 방식이 있는데 Node 교체, In-place 업그레이드이다.
Node 교체 방식
말 그대로 기존 Bottlerocket 노드를 그대로 두고 새로운 OS의 노드를 생성한 뒤 노드를 교체하는 방식이다. bottlerocket 노드를 생성한 방식과 비슷하게 eksctl이나 콘솔을 통해서 진행할 수 있다. 만약 Karpenter를 사용하는 운영자라면 Drift 노드 교체 허용한다고 하니 해당 내용을 같이 보기 바란다.
공식문서에 따르면 aws-k8s-* 네임룰 을 따르는 EKS bottlerocket 노드들은 eksctl 혹은 콘솔 내 EKS 서비스에서 직접 업그레이드가 가능하다.

 

In-place 업그레이드 
이 방식은 별도의 노드 교체 없이 자체 노드에서 업그레이드하는 방식으로 apiclient를 사용한다. 워크숍에서는 해당 명령어를 이용해 업그레이드 하는 방식을 알아본다.

updates 정보 확인

apiclient를 통해서 updates 정보를 확인할 수 있다.
apiclient get settings.updates
이 출력정보에 대해 알아보면 다음과 같다.
ignore-waves  : 이 값은 업데이트가 wave처럼 점진적으로 배포할지 여부에 대한 설정이다. 기본값은 false이며, false일 때는 모든 시스템에 일괄적으로 업데이트가 적용되는게 아니라 순차적으로 배포되도록 한다. true일 경우 모든 파도를 무시하고 즉시 업데이트가 적용될 수 있다.
metadata-base-url : 이 url은 업데이트 메타데이터가 저장되는 위치를 지정한다. Bottlerocket은 이 URL을 통해서 업데이트 관련 메타데이터를 다운로드하고 해당 메타데이터를 기반으로 업데이트를 관리하고 있다.

 

한번 nslookup으로 해당 주소를 찍어봤는데 CDN이 나오는 것으로 보아 S3에 해당 파일을 관리하고 있는 것 같다.

\

seed : 업데이트 시드 값으로 이 값을 통해 업데이트 버전이나 배포 상태를 추적하는데 사용된다.

 

targets-base-url : 업데이트 타겟 파일을 다운로드할 위치를 가리킨다. 이 위치에서 파일을 다운로드해 사용한다.

version-lock : 업데이트 버전을 지정할 수 있다. lastest의 경우 가장 최신 버전으로 자동 업그레이드되는 것을 의미한다. 만약 특정 버전을 고정하고 싶다면 해당 버전을 명시하면된다. 예를들어 1.27을 명시하면 1.27 버전으로 고정된다.

 

사용 가능한 OS 버전 확인

다음 명령어를 통해서 사용 가능한 OS 버전을 확인할 수 있다.

apiclient update check

version-lock 설정에 따라 최신버전으로 설정되게 되는데 현재 available_updates가 1.32.0이고, 현재 버전 역시 1.32.0으로 최신버전인 상태이다. 

 

결과에서 사용 가능한 업데이트가 없다면 chosen_update : null, update_state: Idle이 표시된다.

 

업데이트 실행

해당 명령어를 통해서 업데이트를 진행할 수 있다.

apiclient update apply

 

만약 업데이트가 성공적이라면 아래와 같은 결과가 나오면서 업데이트 된다.

21:11:32 [INFO] Downloading and applying update to disk...
21:11:37 [INFO] Still waiting for updated status, will wait up to 594.5s longer...
21:11:41 [INFO] Setting the update active so it will apply on the next reboot...
21:11:42 [INFO] Update has been applied and will take effect on next reboot.

 

하지만 지금과 같이 최신상태에서는 다음과 같이 실패 메시지를 볼 수 있다.

15:46:06 [INFO] Downloading and applying update to disk...
Failed to apply update: Failed to prepare update.  This could mean that we don't have a list of updates yet or that an update is already applied.

 

업데이트가 되고 난 후에는 bottlerocket의 전체 파일 시스템 이미지를 다운로드하고 재부팅하는 과정을 거친다.

apiclient update check

해당 명령어에서 active_partition은 현재 버전, staging_partition은 새로운 버전으로 표시되고 update_state: Ready로 표시될 것이다.

 

reboot를 통해서 재실행하게 되면 결국 staging_partition이 active_partition으로 변경되게 된다.

apiclient reboot
exit

 

Bootstrap(부트스트랩) 컨테이너 탐색

 

Bootstrap Containers

Setting up the host with containers that start during boot.

bottlerocket.dev

 

부트스트랩 컨테이너는 Bottlerocket 운영 체제가 부팅될 때 최초로 실행되는 컨테이너 이다. 주로 하는 역할은 시스템 초기화와 환경설정 작업이다. 이 컨테이너는 Bottlerocket 운영 체제에서 초기화 과정이 끝난 후 종료되고 다른 애플리케이션 컨테이너가 실행될 수 있는 환경을 만들어 준다.

 

Bootstrap container의 역할

- 시스템 초기화 : Bottlerocket 운영체제 부팅시 가장 먼저 실행되는 init container의 역할을 한다. 따라서 시스템의 기본 설정을 담당한다.

- 설정 관리 : 네트워크 및 보안 설정을 초기화하고 Bottlerocket API와 통신하면서 구성 정보를 가져온다.

- 보안 및 설정 : SELinux 기능 등을 활성화하고 시스템 필요한 보안 설정을 수행한다.

 

수명주기(라이프사이클)

부트 스트랩 컨테이너는 systemd에 의해서 관리되는데 다음과 같은 과정을 거친다.

 

- systemd가 모든 부트스트랩 컨테이너를 동시에 실행한다. (병렬처리)

systemd는 모든 부트스트랩 컨테이너가 실행하고 종료될 때까지 (1사이클이 끝날때까지) 다음 타겟(systemd.target)으로 넘어가지 않는다. (즉 시스템 초기화 과정에서 모든 준비가 마치고 다른 작업을 시작하겠다는 의미)

https://bottlerocket.dev/en/os/1.30.x/concepts/bootstrap-containers/#examples

 

- mode=once라고 설정된 부트스트랩 컨테이너는 완료되면 mode=off로 변환된다.

(즉 1번만 실행하는 컨테이너는 mode=once로 표기하여 실행 후 mode=off로 자동 변환되고 재실행되지 않는다.)

https://bottlerocket.dev/en/os/1.30.x/concepts/bootstrap-containers/#examples

 

- essential=true로 설정된 부트스트랩 컨테이너가 비정상 종료되면 부팅 과정이 중단된다.

주로 핵심적인 부트스트랩 컨테이너에 설정되며, 컨테이너가 비정상 종료(exit0이 아닌 값으로 종료)되면 부팅과정이 중단된다.

https://bottlerocket.dev/en/os/1.30.x/concepts/bootstrap-containers/#examples

- 중단 없이 계속 진행된다면 부트스트랩 컨테이너가 종료되고 오케스트레이터 에이전트인 kubelet이나 ECS agent를 로드한다.

 

부트스트랩 컨테이너의 기능

- 루트 파일 시스템 엑세스가 가능

Bottlerocket 운영 체제의 루트 파일 시스템 (/.bottlerocket/rootfs)을 포함하는 /.bottlerocket/*에 접근할 수 있다. 이 루트 파일 시스템은 시스템의 중요한 기본 파일과 디렉터리를 포함한다. 따라서 부트스트랩 컨테이너는 기본 파일 시스템을 사용할 수 있다는 말이된다. 다만 루트 파일 시스템은 변경할 수 없다.

 

- 컨테이너 권한은 CAP_SYS_ADMIN을 할당받음

부트스트랩 컨테이너는 CAP_SYS_ADMIN이라는 특별한 권한을 가지고 실행한다. CAP_SYS_ADMIN 권한은 시스템에서 관리자와 같은 권한으로 파일, 디렉터리, 마운트 등 시스템 자원을 관리할 수 있다. 다만, 이는 루트 파일 시스템 자체의 변경을 의미하지는 않는다.

 

- /.bottlerocket/rootfs/mnt에 바인드 마운트를 사용함

이 경로에 바인드 마운트가 설정되어 이 경로를 통해 호스트 시스템과 파일을 공유하거나 호스트 파일 시스템에 접근할 수 있도록 해준다.

 

부트스트랩 컨테이너 생성

먼저 컨테이너 이미지를 생성하고 이를 보관하기 위해 ECR을 사용한다. 다음 명령어를 통해서 ECR을 생성하자.

복잡하게 써져있지만 br-bootstrap이라는 ECR이 없으면 만들라는 내용이 전부다.

AWS_ACCOUNT_ID=your-account-id
AWS_REGION=ap-northeast-2

export ECR_REPO="br-bootstrap"
echo "export ECR_REPO=$ECR_REPO" | tee -a ~/.bash_profile
export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
export ECR_REPO_URI=$(aws ecr describe-repositories --repository-name ${ECR_REPO} --region ${AWS_REGION} | \
                      jq -r '.repositories[0].repositoryUri')

if [ -z "$ECR_REPO_URI" ]
then
      echo "${ECR_REGISTRY}/${ECR_REPO} does not exist. So creating it..."
      ECR_REPO_URI=$(aws ecr create-repository \
        --repository-name $ECR_REPO \
        --region $AWS_REGION \
        --image-tag-mutability IMMUTABLE \
        --query 'repository.repositoryUri' \
        --output text)
      echo "export ECR_REPO_URI=$ECR_REPO_URI" | tee -a ~/.bash_profile
      echo -e "\n${ECR_REPO_URI} repo created..."
else
      echo "${ECR_REPO_URI} already exists..."
      echo "export ECR_REPO_URI=$ECR_REPO_URI" | tee -a ~/.bash_profile
fi

다음은 dockerfile을 생성한다. 이 예제에서는 eks-workshop 폴더를 /.bottlerocket/rootfs/mnt에 바인드 마운트 시킨다. 즉 eks-workshop 폴더를 만들고 /.bottlerocket/rootfs/mnt에 생성할 것이다. 이미지를 빌드하고 ECR에 푸쉬하는 작업을 거쳐보자.

cat << EoF > Dockerfile
FROM alpine
ADD test-bootstrap ./
RUN chmod +x ./test-bootstrap
ENTRYPOINT ["sh", "test-bootstrap"]
EoF

cat << EoF > test-bootstrap
#!/usr/bin/env bash
set -ex

mkdir -p /.bottlerocket/rootfs/mnt/eks-workshop
EoF

docker build -t $ECR_REPO:v1 .
docker tag $ECR_REPO:v1 $ECR_REPO_URI:v1
docker push $ECR_REPO_URI:v1

 

참고로 해당 작업 전에 ECR 로그인을 해주는게 좋다.

aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 160889332534.dkr.ecr.ap-northeast-2.amazonaws.com

 

 

이제 admin container로 접속해 봅시다.

enter-admin-container

 

일단 /.bottlerocket/rootfs/mnt에서 루트 파일시스템의 바인드 마운트가

df -h /.bottlerocket/rootfs/mnt/

if [ -d "/.bottlerocket/rootfs/mnt/eks-workshop" ]; then

  echo -e "\nDirectory /.bottlerocket/rootfs/mnt/eks-workshop exists"

else

  echo -e "\nDirectory /.bottlerocket/rootfs/mnt/eks-workshop does not exist"

fi

exit

 

apiclient reboot; exit

 

 

 

이렇게 노드가 처음 부트스트랩 하는 단계에서 해당 컨테이너 이미지를 사용해 사용환경을 세팅할 수 있다는 것을 확인했다.

 

제한된 파일 시스템 탐색

일반적으로 호스트 파일 시스템에 접근할 일은 별로 없지만 일부 로그나 설정파일 등 호스트 파일 시스템에 접근해야할 경우가 생긴다면 제한적으로 이용할 수 있도록 하는 방법이다. 워크숍에서는 개념 설명이 부족해서 다음 공식문서의 내용을 요약해보았다.

 

기본적으로 Bottlerocket에서는 다음 소개하는 3가지 방식으로 파일시스템의 접근을 관리하고 있다.

Immutable은 dm-verity라는 기술을 통해서, mutable은 SELinux의 기능을 통해서, 그리고 일부 변경가능한 임시 저장소의 경우는 임시라는 특징을 통해 재부팅 후 변경 사항이 소멸되는 방식으로 보안성을 높이고 있다. 세부적으로 알아보자.

 

변경 불가능한 파일 시스템(Immutable Filesystem)

Bottlerocket의 루트 파일 시스템은 dm-verity라는 기술로 읽기 전용으로 설정된다. 따라서 루트 파일 시스템을 사용자 프로세스가 수정할 수 없도록 하여 안정성을 보장한다.

 

dm-verity는 블록 수준의 보호를 제공하는 이 기술은 해시 트리를 사용해 파일 시스템의 무결성을 검증한다.

각 물리적인 블록에 대해 암호화된 해시 값을 계산하고, 이 해시 값들의 결합으로 루트 블록의 해시를 계산한다.

이후 모든 쓰기 작업에서 새로운 루트 블록 해시와 기존의 루트 블록 해시를 비교해 변경사항을 감지한다.

 

아래 그림처럼 물리적 블록이 변경되면(Changed block) 이후 각 계층에서 다른 다이제스트가 생성되면서 다른 루트 블록 다이제스트가 생성된다.

결국 루트 파일 시스템이 변경되면 해시 값이 달라지므로 Changed root block이 생기면서  dm-verity는 시스템 변경사항을 알리고 커널은 시스템을 재부팅한다. 따라서 루트 파일 시스템을 보호하고 루트킷 등 악성 공격으로 부터 유지한다.

 

변경 가능한 파일 시스템 (Mutable Filesystem)

기본적으로 루트 파일 시스템에 대해 가변적인 것은 실용적이지도 보안적이지도 않기 때문에 잘 사용되지 않는다. 다만 컨테이너 로그를 기록하거나 설정 파일을 변경해할 경우 사용된다고 볼 수 있다. Bottlerocket은 SELinux를 활용해 보안성을 갖춘다. 특히 파일 및 디렉터리에 대해 세부적인 보안 정책을 설정하고, 각 자원에 대한 액세스 권한을 세밀하게 조정할 수 있다.

 

SELinux 보안 정책에 대해 조금 더 살펴보자.

 

- SELinux는 각 파일이나 리소스에 대해 라벨을 설정해 보호하고, 특정 리소스를 읽기/쓰기 할 수 있도록 권한 설정을 할 수 있다.

예를 들면 kubelet이 /var/log/containers에 로그를 기록하거나 bottlerocket API 서버가 /etc/motd 파일을 변경할 수 있도록 할 수 ㅣ있다.

 

- SELinux는 기본적으로 enforcing 모드이다. 이 모드는 로그를 기록하거나 불법적인 변경을 방지하는 기능이 있다.

 

일시적인 저장소 (Ephemeral Storage)

변경가능한 파일 시스템에서 일부는 일시적 저장소로 존재한다. 예를 들면 /etc 디렉터리는 재부팅 시 변경 사항이 유지되지 않는다.

 

장점으로는 구성 변경 경로의 신뢰성을 높여줄 수 있다. API 또는 CNI, CSI와 같은 특별한 컨테이너 명세를 통해 구성을 변경할 수 있다.

공격자가 변경 사항을 영구적으로 남기기 어렵게 한다. 재부팅 후 일시적으로 변경한 내용은 초기화 되기 때문에 악성 변경을 차단할 수 있다.

 

각 설정 확인하기

일단 admin 컨테이너에서 루트 파일 시스템이 읽기 전용 볼륨으로 마운트 되었는지 확인해보자.

다음 명령어를 통해서 루트 파일 시스템의 options에 ro(읽기 전용)으로 마운트 됨을 확인할 수 있다.

findmnt /.bottlerocket/rootfs/

 

기본적으로 읽기 전용으로 파일 시스템이 관리되어야 보안성이 높은건 당연할 것이다.

 

이제 admin 컨테이너에서 sheltie 스크립트를 실행하여 Bottlerocket 호스트의 루트 파일시스템에 접근 가능한 권한을 사용할 수 있도록 해보자.

sudo sheltie

 

이와 같이 bash로 넘어온 것을 볼 수 있다.

 

sheltie로 접근한 경우 루트 쉘을 사용하게 되며 dm_verity_hash에 있는 블록을 덮어쓰기해보도록 한다. 변경사항을 가하는 행위이다.

기본적으로 Bottlerocket 호스트에서는 커널에서 dm-verity라는 기술을 이용해 루트 파일시스템이 의도되지 않은 변경사항에 대해 변경되는 것을 차단한다. 이 방식은 hash 트리를 사용하여 체킹하는 방식이고 아까 공식문서에 확인했었다.

따라서 우리는 아래 명령어를 통해서 일부러 해시를 손상시켜보자. 그러면 호스트는 해시가 손상된 것을 체크하고 작동을 멈출 것이다.

dd if=/dev/zero of=`blkid -t TYPE=DM_verity_hash | cut -d":" -f1` bs=1M count=1

 

잠시만 기다려보면 호스트가 작동을 멈추는 것을 확인할 수 있다.

dd 명령어(파일 복사 및 변환 수행)를 실행해보자.

dd

 

아무리 시도해도 호스트는 상태는 구동되고 있지만 아무 생산적인 동작은 수행하지 못한다.

 

이제 다시 reboot을 통해 복구 여부를 확인해보자.

reboot

 

reboot을 해도 여전히 NotReady 상태임을 확인할 수 있으며, 더 이상 사용이 불가능합니다. 이미 정합성이 깨졌기 때문에 보안을 위해서 서버가 정상적인 기능을 하지 못한다.

 

호스트가 재 부팅하는 동안에는 READY 상태일 수 있는데 이는 시스템이 부팅될때마다 dm-verity는 루트 파일 시스템의 무결성 검사를 하기 때문에 해당 검사를 마치고 나서 운영체제 부팅을 거부하므로 일시적인 현상이다.

 

작동하지 않는 호스트는 종료하고 새로운 인스턴스를 생성해서 사용하자.

# 기존 bottlerocket 인스턴스 제거
kubectl get nodes -l eks.amazonaws.com/nodegroup=$BR_MNG_NAME | grep NotReady | awk '{print $1}' | xargs kubectl delete node
kubectl get nodes -l eks.amazonaws.com/nodegroup=$BR_MNG_NAME
aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region $AWS_REGION

# 새로운 노드그룹 추가
while [ "`kubectl get nodes -l eks.amazonaws.com/nodegroup=$BR_MNG_NAME | grep -v STATUS | awk '{print $2}'`" != "Ready" ]
do
  echo -e "`date` - Waiting for the Bottlerocket Node to be ready. Please wait...\n"
  sleep 15
done

kubectl get nodes -l eks.amazonaws.com/nodegroup=$BR_MNG_NAME

echo -e "\nBottlerocket Node is ready."

export INSTANCE_IP=$(kubectl get nodes -l eks.amazonaws.com/nodegroup=$BR_MNG_NAME -o json | jq -r '.items[0].metadata.annotations."alpha.kubernetes.io/provided-node-ip"')

export INSTANCE_ID=$(aws ec2 describe-instances --filters Name=private-ip-address,Values=$INSTANCE_IP | jq -r .[][].Instances[].InstanceId)

echo "export INSTANCE_ID=$INSTANCE_ID" | tee -a ~/.bash_profile

aws ssm start-session --target $INSTANCE_ID

 

 

자 이번에는 다시 새로운 노드를 생성해서 로그인 한 뒤 변경 가능한 mutable의 예시를 살펴보자.

다시 한 번 관리형 컨테이너에 로그인 한 뒤 sheltie 명령어를 통해 root shell에 진입한다.

enter-admin-container;exit
sudo sheltie

 

Bottlerocket은 기본적으로 SELinux가 enforcing 모드로 실행되고, 커널에서는 이를 비활성화하는 것이 방지되도록 컴파일 되어있다.

 

 

따라서 /etc 디렉터리 내에 수정을 해볼 예정이다. 기본적으로 bottlerocket에서는 /etc 디렉터리는 일시적인 저장소이므로 재부팅시 변경사항이 초기화 된다. 따라서 다음 내용을 /motd에 기록해보고 재부팅해보자.

cat /etc/motd

echo "Attempting local modifications to /etc/motd" > /etc/motd

cat /etc/motd

reboot

 

일단 기본적으로 motd에는 Welcome to Bottlerocket! 이라는 내용의 파일이 존재했는데 Attempting local modifications to /etc/motd라는 내용으로 변경한걸 확인할 수 있다.

 

자 이제 다시 들어가보자. 다시 들어가면 Welcom to Bottlerocket으로 다시 초기화 된것을 확인할 수 있다.

aws ssm start-session --target $INSTANCE_ID
enter-admin-container
sudo sheltie; exit
cat /etc/motd

 

Bottlerocket 설정 

일단 bottlerocket에서 설정할 수 있는 정---말로 많은 내용이 있으므로 반드시 여기를 참고하기 바란다.

너무 많아서 극히 일부만 캡처했다.

 

apiclient get settings.kubernetes.authentication-mode

설정은 apiclient를 통해서 control 컨테이너에서 진행한다. 특히 쿠버네티스(EKS)는 kubelet을 통해서 API 서버와 통신하는데 이 통신은 매우 안전해야 하므로 인증서 서명 API를 통해 서로 안전성을 지키고 있다. 이처럼 Bottlerocket에서도 aws-iam-authenticator를 사용해 인증토큰을 생성하고 EKS의 API 서버에 인증서 서명 요청(CSR)을 제출한다.

 

https://bottlerocket.dev/en/os/1.30.x/api/settings/kubernetes/#authentication-mode

 

kubernetes

Settings related to Kubernetes (`settings.kubernetes.*`)

bottlerocket.dev

 

apiclient get settings.kubernetes.authentication-mode

 

 

커널 잠금기능은 커널의 보안 강화를 위해 필요하다. 기본적으로 Integrity Mode를 사용하는데 이 모드는 커널 코드나 메모리 변경을 차단해 공격자가 시스템을 침해하는 것을 막아준다. 이 모드는 커널의 무결성을 보장해주므로 특히 메모리를 덮어쓰거나 수정하는 행위를 방지한다.

apiclient get settings.kernel.lockdown

 

모든 리소스 삭제

실습을 위해서 생성한 EKS 노드 그룹 및 ECR을 삭제한다. 비용조심하자!!

eksctl scale nodegroup -c $EKS_CLUSTER -n $BR_MNG_NAME -r $AWS_REGION --nodes 0

docker rmi $ECR_REPO:v1

docker rmi $ECR_REPO_URI:v1

aws ecr delete-repository \
  --repository-name $ECR_REPO \
  --region $AWS_REGION \
  --force

 

이상으로 실습을 마치겠다.