Chapter 18

서비스 어카운트와 보안 컨텍스트

  • 18.1 서비스 어카운트
  • 18.2 이미지 보안
  • 18.3 Docker 보안 기초
  • 18.4 Security Context

인증과 인가가 사람의 접근을 다뤘다면, 이번 장은 기계의 접근과 컨테이너 보안에 대한 이야기야. 서비스 어카운트로 애플리케이션에 신분을 부여하고, 프라이빗 이미지 레지스트리를 안전하게 연결하고, Security Context로 컨테이너의 권한을 세밀하게 제어하는 방법을 살펴보자.

Kubernetes에는 두 가지 유형의 계정이 있어. **사용자 계정(User Account)**은 사람이 쓰고, **서비스 계정(Service Account)**은 기계가 써. 사용자 계정은 관리자가 클러스터를 관리하거나 개발자가 앱을 배포할 때 쓰는 거고, 서비스 계정은 Prometheus 같은 모니터링 앱이 Kubernetes API를 폴링하거나, Jenkins 같은 빌드 도구가 클러스터에 앱을 배포할 때 쓰는 계정이야.

서비스 계정은 토큰이라는 것과 연결되어 있어. 이 토큰이 Kubernetes API에 인증하는 데 사용되는 거야. curl로 API를 호출할 때 Authorization 헤더에 Bearer 토큰으로 전달하는 그 토큰이지. 서비스 계정이 서비스를 위한 신분증이라고 생각하면, 토큰은 신분증에 있는 바코드 같은 거야. 게이트에서 스캔해서 신원을 증명하는.

기본적으로 Kubernetes 클러스터가 설정되면 모든 네임스페이스에 **default**라는 서비스 계정이 자동으로 만들어져. kubectl get serviceaccount로 확인할 수 있어. 파드가 생성될 때마다 이 기본 서비스 계정이 자동으로 연결돼. kubectl describe pod 출력에서 Service Account 필드를 보면 default로 지정된 걸 확인할 수 있어. 서비스 계정은 파드 내에서 **프로젝티드 볼륨(Projected Volume)**으로 마운트되는데, /var/run/secrets/kubernetes.io/serviceaccount 경로에 토큰 파일이 생겨. 이 디렉터리 내용을 보면 토큰 파일을 확인할 수 있고, 파일 내용을 보면 실제 토큰이 나와.

기본 서비스 계정은 제한이 많아서, 특정 접근 요구 사항이 있으면 커스텀 서비스 계정을 만들어야 해. kubectl create serviceaccount dashboard-sa 명령으로 만들거나 YAML 파일로 선언적으로 만들 수 있어. 커스텀 서비스 계정을 파드에 연결하려면 파드 스펙의 serviceAccountName 필드를 사용해.

apiVersion: v1
kind: Pod
metadata:
  name: my-dashboard
spec:
  serviceAccountName: dashboard-sa
  containers:
  - name: dashboard
    image: my-dashboard:latest

서비스 계정이 파드에 연결되면 Kubernetes가 수명이 짧은 토큰을 자동으로 생성하고 프로젝티드 볼륨으로 마운트해. kubelet이 이 토큰을 자동으로 회전시키고, 파드가 삭제되면 토큰도 자동으로 만료돼.

토큰이 자동으로 마운트되지 않게 하려면 **automountServiceAccountToken: false**를 설정하면 돼. 서비스 계정 레벨에서 설정하면 그 서비스 계정을 쓰는 모든 파드에 적용되고, 파드 정의 레벨에서 설정하면 해당 파드에만 적용돼.

클러스터 외부에서 사용할 토큰을 생성할 수도 있어. CI/CD 도구나 모니터링 도구, 또는 직접 만든 Kubernetes 대시보드 같은 외부 애플리케이션에 쓰려면 kubectl create token dashboard-sa 명령으로 토큰을 만들면 돼. 기본적으로 1시간 유효하고, --duration 플래그로 유효 기간을 늘릴 수 있어. 이 토큰은 어떤 시크릿에도 저장되지 않고 화면에 출력되니까 복사해서 사용해야 해. jq와 base64 유틸리티로 토큰을 디코딩하면 서비스 계정 이름, 만료일 같은 세부 정보를 확인할 수 있어.

서비스 계정이 API 접근 권한을 관리한다면, 이미지 보안은 컨테이너 이미지 자체를 안전하게 가져오는 문제야. 이미지 이름의 구조부터 알아보자. 파드 정의 파일에서 image: nginx라고 쓰면, 실제로는 **docker.io/library/nginx**야. Docker의 이미지 명명 규칙을 따르는 거지. 첫 번째 부분이 레지스트리(docker.io), 두 번째가 사용자 또는 계정 이름(library), 세 번째가 이미지 이름(nginx). 사용자/계정 이름을 안 쓰면 Docker 공식 이미지가 저장되는 기본 계정인 library로 가정하고, 레지스트리를 안 쓰면 Docker Hub(docker.io)로 가정하는 거야.

자기만의 계정을 만들면 docker.io/myname/myapp 이런 식이 되고. Google의 레지스트리는 **gcr.io**인데, Kubernetes 관련 이미지가 많이 저장되어 있어. AWS ECR, Azure ACR 같은 클라우드 레지스트리도 있고.

공개하면 안 되는 사내 애플리케이션은 비공개 레지스트리를 써야 해. Docker Hub에서 리포지토리를 비공개로 설정하거나, 클라우드 서비스의 프라이빗 레지스트리를 쓰거나, 내부에 직접 호스팅할 수도 있어.

Docker에서 비공개 이미지를 쓰려면 먼저 docker login으로 레지스트리에 로그인하고 나서 이미지를 실행해. Kubernetes에서는 파드 정의 파일의 image 필드에 프라이빗 레지스트리의 전체 경로를 지정해.

spec:
  containers:
  - name: my-app
    image: private-registry.io/apps/internal-app

인증은 어떻게 할까? 이미지는 워커 노드에서 컨테이너 런타임이 가져오는 건데, 런타임에 자격 증명을 어떻게 전달하느냐가 문제야. 먼저 자격 증명이 포함된 시크릿 오브젝트를 만들어.

kubectl create secret docker-registry regcred \
  --docker-server=private-registry.io \
  --docker-username=registry-user \
  --docker-password=registry-password \
  --docker-email=user@example.com

**docker-registry**는 Docker 자격 증명을 저장하기 위해 만들어진 내장 시크릿 타입이야. 레지스트리 서버, 사용자 이름, 비밀번호, 이메일을 지정하면 돼. 그 다음 파드 정의 파일의 imagePullSecrets 섹션에 이 시크릿을 지정해.

spec:
  containers:
  - name: my-app
    image: private-registry.io/apps/internal-app
  imagePullSecrets:
  - name: regcred

파드가 생성되면 kubelet이 시크릿의 자격 증명을 사용해서 프라이빗 레지스트리에서 이미지를 가져와.

이미지를 안전하게 가져왔으면, 이제 컨테이너가 실행될 때의 보안을 생각해야 해. Kubernetes의 Security Context를 이해하려면 Docker 보안부터 알아야 해.

Docker가 설치된 호스트에는 여러 OS 프로세스, Docker 데몬, SSH 서버 등이 돌아가고 있어. 여기서 Ubuntu 컨테이너를 하나 실행하면, 컨테이너는 호스트와 같은 커널을 공유하지만 네임스페이스로 격리돼. 컨테이너 내부에서 프로세스를 보면 자기 네임스페이스 안의 프로세스만 보이고, PID가 1번으로 나와. 하지만 호스트에서 프로세스를 보면 같은 sleep 프로세스가 다른 PID로 표시돼. 프로세스가 서로 다른 네임스페이스에서 다른 PID를 가질 수 있기 때문이야. 이게 Docker가 컨테이너를 격리하는 방식이야.

보안 관점에서 사용자를 보자. 기본적으로 Docker는 루트 사용자로 컨테이너 프로세스를 실행해. 루트로 실행하고 싶지 않으면 docker run --user=1000 ubuntu sleep 3600 이런 식으로 사용자 ID를 지정할 수 있어. 또는 Dockerfile에서 USER 1000으로 지정해서 이미지를 빌드하면, 실행할 때 따로 지정하지 않아도 해당 사용자 ID로 실행돼.

그럼 루트 사용자로 컨테이너를 실행하면 위험하지 않을까? 컨테이너 내 루트가 호스트의 루트와 같은 건 아니야. Docker가 Linux Capabilities를 사용해서 컨테이너 내 루트 사용자의 권한을 제한하거든.

루트 사용자는 원래 시스템에서 뭐든 할 수 있어. 파일 소유권 변경, 프로세스 생성/종료, 네트워크 포트 바인딩, 시스템 재부팅, 시스템 시계 조작 등등. 이 모든 게 Linux Capabilities라는 것들인데, 전체 목록은 /usr/include/linux/capability.h에서 확인할 수 있어.

Docker는 기본적으로 제한된 Capabilities로 컨테이너를 실행해. 호스트를 재부팅하거나 다른 컨테이너를 방해할 수 있는 권한은 빠져 있는 거야. 이 동작을 바꾸고 싶으면 --cap-add 옵션으로 특정 권한을 추가하거나 **--cap-drop**으로 제거할 수 있고, --privileged 플래그를 쓰면 모든 권한을 활성화할 수도 있어. 물론 --privileged는 위험하니까 신중하게 써야 해.

이 Docker 보안 개념이 Kubernetes의 Security Context에서 그대로 적용돼. Docker에서 --user, --cap-add, --cap-drop 같은 옵션으로 설정하던 보안 표준을 Kubernetes에서는 securityContext 필드로 구성해.

설정할 수 있는 레벨이 두 가지야. 파드 레벨에서 설정하면 파드 내 모든 컨테이너에 적용되고, 컨테이너 레벨에서 설정하면 해당 컨테이너에만 적용돼. 파드와 컨테이너 모두에 설정하면 컨테이너 설정이 파드 설정보다 우선해.

파드 레벨에서 사용자 ID를 설정하려면 파드의 spec 섹션 아래에 securityContext 필드를 추가하고 runAsUser 옵션을 쓰면 돼.

apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  securityContext:
    runAsUser: 1000
  containers:
  - name: ubuntu
    image: ubuntu
    command: ["sleep", "3600"]

컨테이너 레벨에서 설정하려면 컨테이너 spec 아래로 securityContext 섹션을 이동시키면 돼. Capabilities를 추가하려면 capabilities 옵션을 사용해서 추가할 기능 목록을 지정해. 한 가지 주의할 점은, capabilities는 컨테이너 레벨에서만 설정할 수 있고 파드 레벨에서는 안 된다는 거야.

apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  containers:
  - name: ubuntu
    image: ubuntu
    command: ["sleep", "3600"]
    securityContext:
      runAsUser: 1000
      capabilities:
        add: ["MAC_ADMIN", "NET_ADMIN"]

정리

18장 읽고 기억할 거 세 가지:

  1. 서비스 계정은 기계용 신분증이야. 파드마다 default 서비스 계정이 자동 연결되고, 토큰은 프로젝티드 볼륨으로 마운트되며, serviceAccountName으로 커스텀 서비스 계정을 지정할 수 있다.
  2. 프라이빗 레지스트리에서 이미지를 가져오려면 docker-registry 타입의 시크릿을 만들고 파드의 imagePullSecrets에 연결해야 한다. 이미지 이름은 레지스트리/계정/이미지 구조다.
  3. securityContext는 Docker의 --user, --cap-add 같은 보안 옵션을 Kubernetes로 가져온 것이며, 파드 레벨과 컨테이너 레벨로 설정할 수 있고, capabilities는 컨테이너 레벨에서만 지정 가능하다.