본문 바로가기
infra

컨테이너 보안

by marble25 2023. 7. 14.

1. 컨테이너 보안 위협

  • 컨테이너는 응용프로그램과 프로그램이 의존하는 모든 것을 하나로 묶어서 실행하는 호스트와 격리한다. 이 격리는 다른 컨테이너와도 의존성을 분리한다.
  • 컨테이너에서 공격이 가능한 경로는 다양하다.
    • 응용 프로그램 코드 취약점
    • 컨테이너 이미지 설정 오류
    • 이미지 레지스트리 공격
    • 호스트 취약점
    • 비밀 정보 노출
    • 네트워킹
    • 컨테이너 탈출 취약점
  • 보안에는 몇 가지 기본 원칙이 있다.
    • 최소 권한
    • 심층 방어: 여러 겹의 보안 경계
    • 공격 표면 축소: 공격이 들어올 수 있는 구멍 자체를 줄인다.
    • 폭발 반경 제한: 공격이 들어오더라도 영향을 미칠 수 있는 부분을 줄인다.
    • 직무 분리

2. 리눅스 시스템 호출, 접근 권한, 능력

  • 리눅스 시스템에서 보안의 토대는 파일 접근 권한이다. 리눅스 세상에는 “모든 것은 파일이다”. 응용 프로그램, 데이터, 설정 정보, 로그 뿐 아니라 화면이나 프린터 같은 물리적 장치도 파일로 대표된다.
  • 파일에 setuid 비트를 설정해 놓으면 프로세스는 파일 소유자의 ID로 실행된다. setuid나 setgid는 보통의 사용자에게는 없는 권한을 프로그램에 부여하는 용도로 쓰인다. (ex: ping 프로그램에서 네트워크 소켓 여는 기능을 위해)
  • 리눅스 커널에는 30개가 넘는 능력(capability)가 존재한다. 능력은 스레드 단위로 부여되는데, 스레드에 어떤 능력이 부여되느냐에 따라 스레드가 할 수 있는 일이 달라진다. 프로세스에는 작업에 꼭 필요한 능력만 부여하는 것이 바람직하다.
  • 컨테이너는 기본적으로 루트 권한으로 실행된다.

3. cgroups와 제어 그룹

  • control groups를 줄인 cgroups는 주어진 그룹에 속한 프로세스들이 사용할 수 있는 지원을 제한하는 수단이다. 제어 그룹은 포크 폭탄(프로세스가 자신을 복제해서 프로세스들이 지수적으로 증가하는 것)을 억제할 수 있다.
  • 리눅스에서는 관리 대상 자원의 종류마다 파일 시스템 위계구조가 있고, 위계구조마다 cgroup controller가 있다. 모든 프로세스는 한 제어 그룹에 속한다.
    • /sys/fs/cgroup에서 자원의 종류를 볼 수 있다.
  • 특정 프로세스의 메모리 사용량을 제한하고 싶으면 세 제어 그룹을 만들고 프로세스를 그 제어 그룹에 배정해야 한다.
    • memory 디렉터리에 하위 디렉터리를 만들면 제어 그룹이 새로 만들어진다.
    • cgroups.procs 파일에 프로세스 ID를 추가하면 해당 프로세스가 제어 그룹에 배정된다.
  • 컨테이너를 실행할 때 자원 한계를 간편하게 설정할 수 있고, 그 한계를 강제하는 제어그룹이 자동 생성된다.

4. 컨테이너 격리

  • 컨테이너 안의 환경은 VM과 거의 비슷해 보인다. 컨테이너에는 자신만의 네트워크 스택이 있고, 자신만의 파일 시스템이 있다.
  • namespace는 프로세스가 볼 수 있는 것들을 제한한다. 하나의 프로세스는 이름공간 종류당 하나씩의 이름공간에 속한다.
  • unshare는 부모와 공유하지 않는 이름공간들로 프로그램을 실행한다. 일반적으로는 부모 프로세스의 이름 공간을 공유하는데, unshare를 이용하면 자신만의 이름 공간을 가지게 된다.
    • 호스트네임
    • 프로세스
    • 루트 디렉터리
    • 마운트 이름공간
    • 네트워크 이름공간
    • 사용자 이름공간
      • 컨테이너 안에서 루트 ID 0을 호스트의 비루트 계정에 부과할 수 있다. 이렇게 되면 공격자가 컨테이너에서 탈출해서 호스트로 간다 하더라도 권한 없는 비루트 계정에 불과하게 된다.
      • 하지만 사용자 이름공간 전환과정에서 권한들이 잘못 변환되는 이슈가 있어서 널리 사용되지는 않는다.
    • IPC 이름공간: 메모리 공유 영역이나 공유 메세지 대기열을 이용해서 통신하는 기능 분리
    • 제어그룹 이름공간
  • 컨테이너는 호스트 컴퓨터에서 실행되는 하나의 리눅스 프로세스이지만, 호스트 컴퓨터의 일부만 볼 수 있고, 전체 파일 시스템의 한 부분 트리에만 접근할 수 있다.
    • 호스트에서 컨테이너 프로세스를 볼 수 있다는 점은 컨테이너와 VM의 근본적인 차이다.
  • 컨테이너 프로그램들은 전용 호스트에서 실행되는 것이 권장된다.
    • 호스트에 직접 접근할 필요가 적기 때문에 관리가 쉽고, 보안 측면에서 좋다.
    • 컨테이너 실행에 특화된 thin OS를 이용한다면 공격 표면이 적다.
    • 모든 호스트 컴퓨터가 동일한 설정을 공유할 수 있다.

5. VM과 컨테이너

  • 컴퓨터를 처음 부팅하면 BIOS가 실행되어 여러 장치들을 점검한다.
  • 부트로더가 실행되면 운영체제의 커널 코드가 메모리에 적재되어 실행된다. 커널 코드는 응용 프로그램보다 높은 권한으로 실행된다.
  • VMM은 자신이 관리하는 VM 각각에 자원을 배정하고 가상 장치들을 설정하고, 게스트 커널을 실행한다.
    • type 1 VMM(하이퍼바이저): 호스트 운영체제 없이 하이퍼바이저 위에 게스트 OS가 올라간다.
    • type 2 VMM: 호스트 OS 위에 VMM이 올라가서 게스트 OS를 구동한다.
    • Kernel-based VM: 호스트 OS 커널 안에 VMM이 포함되어 실행된다.
  • non-virtualizable 명령은 sensitive(컴퓨팅 자원 접근)하지만 priviledged하지 않은 명령을 말한다.
    • 이진 번역: 적절한 명령으로 변환한다.
    • 반가상화: 가상화 불가 명령을 실행하지 않도록 게스트 OS 자체를 재작성한다.
    • 하드웨어 가상화: 하이퍼바이저가 새로운 권한 수준에서 실행된다.
  • VM은 컨테이너와 다르게 다른 VM의 프로세스를 보는 것은 아예 불가능하기 때문에 격리 수준이 더 높다.
  • VM은 구동에 시간이 훨씬 많이 걸린다. 컨테이너는 하나의 리눅스 프로세스이지만, VM은 모든 부팅 및 초기화 과정을 거쳐야 하기 때문에 민첩하지 못하다.
  • VM은 커널 전체를 실행해야 하므로 오버헤드가 높다.

6. 컨테이너 이미지

  • 하나의 컨테이너 이미지는 두 부분으로 구성되는데, 하나는 루트 파일 시스템이고, 하나는 이미지 설정 정보이다.
  • 이미지 구축시 docker build를 하게 되는데, docker 명령 자체는 그리 많은 일을 하지 않고, 도커 데몬에게 API 요청을 하는 역할만 수행한다. 도커 데몬은 백그라운드에서 계속 실행되는 프로세스로, 컨테이너 및 컨테이너 이미지의 실질적 실행과 관리를 책임진다.
    • 도커가 루트 계정으로 실행되기 때문에 보안 위협이 존재한다.
  • 한 계층에서 만들어진 파일은 이후 계층에서 삭제된다 하더라도 이미지에 포함된다.
  • 이미지를 push/pull할 때 다이제스트를 이용해서 이미지를 명시적으로 지정할 수 있다. 태그는 한 이미지에서 다른 이미지로 옮길 수 있기 때문에 항상 같은 이미지라는 보장은 없지만, 다이제스트는 항상 같은 이미지를 가리킨다.
  • Dockerfile을 작성할 때 보안에 신경써야 한다.
    • 신뢰할 수 있는 기반 이미지에서 시작하자.
    • 기반 이미지는 최대한 용량을 줄이자.
    • 기반 이미지를 태그로 지칭할지 다이제스트로 지칭할지 신중히 결정하자. 태그는 보안 패치 적용된 새 버전을 손쉽게 반영 가능하고, 다이제스트는 구축 과정 재현이 쉽다.
    • multi-stage build를 적극 활용해서 이미지 크기를 줄이자. 공격 표면 역시 줄어든다.
    • 민감 디렉토리를 컨테이너에 마운트하지 않았는지 점검하자.

7. 컨테이너 이미지의 소프트웨어 취약점

  • CI 파이프라인에 취약점 스캐너를 포함시키자.

8. 컨테이너 격리의 강화

  • 더 높은 수준의 컨테이너 격리를 구현한 방법에는 여러 가지가 있다.
    • seccomp, AppArmor, SELinux 등은 허용하는 시스템 콜의 개수를 줄이거나, 프로세스가 접근할 수 있는 범위를 다르게 정의하여 기본적인 컨테이너 격리를 강화한다.
    • 파이어크래커나 유니커널은 VM을 이용하되, 불필요한 디바이스 로딩을 없애거나 불필요한 응용 프로그램을 없애서 부팅 속도를 향상시킨다.
    • gVisor는 실행되는 프로세스를 숨기면서 VM과 컨테이너의 중간 형태의 격리를 제공한다.

9. 컨테이너 격리 깨기

  • 기본적으로 컨테이너는 루트로 실행된다. 중요한 것은 루트 계정이 컨테이너 내에서만 루트가 아니라 호스트 자체의 루트이다.
  • 비루트 사용자가 실행해도 도커는 컨테이너를 루트로 실행하는 것은 일종의 권한 확대이다.
  • 루트 없는 컨테이너는 컨테이너의 사용자 이름 공간을 호스트의 것과 완전히 분리한다. 루트 없는 컨테이너의 프로세스는 컨테이너의 관점에서는 루트로 실행되는 것처럼 보이지만, 호스트 관점에서는 보통 사용자로 인식된다.
  • --priviledged 플래그는 굉장히 위험하다. 파일 시스템 마운팅이나 이름공간 수정 등 많은 위험한 작업을 할 수 있게 된다. 도커가 이 플래그를 도입한 것은 docker in docker를 가능하게 하기 위함으로 이미지 구축 도구나 CI/CD 시스템에서 주로 사용된다.
  • 응용 프로그램 컨테이너의 작업 일부를 떼어서 맡긴 보조 컨테이너를 사이드카 컨테이너라고 부른다. 이 때 원활한 작업을 위해 이름공간 여러 개에 대한 접근 권한을 의도적으로 보조 컨테이너에 부여한다. 사이드카 컨테이너는 여러 곳에서 사용된다.
    • 네트워킹 기능을 서비스 메시 사이드카 컨테이너에서 담당
    • 로깅, 트레이스, 모니터링 등

10. 컨테이너 네트워크 보안

  • 컨테이너 방화벽은 컨테이너들 사이를 오가는 트래픽을 제한한다.
  • IP 패킷이 전송되는 과정을 살펴보자. 요청이 처리되는 첫 단계는 DNS 조회인데, 로컬에서 /etc/hosts를 조회해서 수행할 수 있고, 원격 DNS 서버에 DNS 요청을 보낼 수도 있다. 목적지의 IP 주소를 알아낸 후 라우팅 과정을 통해 IP 네트워크의 다음 경유지를 알아낸다. 그 후 다음 경유지 IP 주소를 ARP를 통해 MAC 주소를 구한다. 이렇게 목적지에 도착할 때까지 반복해서 결국 패킷이 위로 올라가면서 응용 프로그램에 도착한다.
  • 한 파드에 존재하는 모든 컨테이너는 동일한 네트워크 이름공간을 공유하기 때문에 같은 IP 주소를 사용한다.
  • iptables는 커널이 IP 패킷을 처리할때 적용하는 규칙을 설정할 수 있다. 여러 개의 테이블 중 필터 테이블은 패킷을 폐기할지를 결정하고, NAT 테이블은 주소 변환에 사용된다.
  • 쿠버네티스의 kube-proxy는 iptables 규칙을 이용해서 쿠버네티스 서비스에 대한 트래픽의 로드 밸런싱을 처리한다. 다만 호스트마다 규칙이 너무 많으면 성능이 떨어질 수 있다.
  • 네트워크 정책은 기본적으로 다음과 같은 규칙을 따르는 것이 좋다.
    • 모든 인바운드, 아웃바운드 트래픽을 기본적으로 거부
    • 파드 ↔ 파드 트래픽 제한
    • 포트 제한
  • 쿠버네티스에서 서비스 메시는 프로그램 파드에 사이드카 컨테이너로 붙어서 프로그램 컨테이너의 네트워킹을 처리하는 형태로 적용된다. 파드로 들어가거나 나가는 트래픽은 모두 이 사이드카 프록시를 거친다.

11. TLS를 이용한 구성요소 간 보안연결

  • HTTPS에서는 전송 계층에서 작용하는 TLS 프로토콜을 사용한다. 이 프로토콜은 X.509 인증서를 활용한다. 인증서에는 다음과 같은 정보를 포함한다.
    • 인증 대상(도메인 형태)
    • 공개키
    • 인증서 발급한 CA dlfma
    • 인증서 만료 기간
  • 인증 기관(CA)는 인증서에 서명함으로써 인증서에 담긴 신원이 정확함을 확인해주는 신뢰된 개체이다. CA를 안전하게 식별하려면 CA 인증서 자체도 확인해야 하는데, 이러면 체인이 끝도 없기 때문에 CA가 스스로 서명한 self-signed 인증서로 끝이 난다. 웹 브라우저는 잘 알려진, 신뢰된 CA가 서명한 인증서(루트 CA)가 미리 설치되어 있다.
  • CSR(인증서 서명 요청)
    • 서버: CA에 사이트 정보와 공개키를 보낸다.
    • CA: 본인의 개인키로 사이트 정보와 공개키에 대해 암호화하여 인증서를 생성한다.
    • CA: 생성된 인증서를 서버에 전송한다.
  • TLS 연결은 TCP 연결이 확립된 후,
    • 서버: 인증서를 보낸다.
    • 클라이언트: 브라우저에 내장된 CA의 공개키를 이용해서 사이트 인증서를 복호화하면서 인증서가 유효한지 검증하고, 사이트의 공개키를 가져온다.
    • 클라이언트: 사용할 대칭키를 만들고 대칭키를 서버 공개키로 암호화해서 서버로 전송한다.
    • 서버: 자신의 개인키를 이용해서 대칭키를 얻어낸다.
    • 이후 통신은 대칭키를 이용해서 암호화/복호화를 하게 된다.

12. 비밀 정보를 컨테이너에 전달

  • 비밀 값은 반드시 암호화해서 저장해야 한다.
  • 비밀 값을 전송할 때에도 암호화된 형태로 전송해야 한다.
  • 비밀 값을 폐기할 수단도 필요하다.
  • 비밀 값을 순환, 또는 변경하는 능력도 필요하다.
  • 비밀 값을 전달하는 가장 바람직한 방법은 컨테이너가 볼륨 마운트를 통해 접근할 수 있는 임시 디렉토리(메모리에만 존재하는)에 저장하는 것이다.
    • 비밀 값 갱신이 쉽다는 장점이 있다.

'infra' 카테고리의 다른 글

[TIL] mig 해제하기  (0) 2023.12.14
[TIL] js pm2 패키지  (0) 2023.11.05
무중단 배포 프로세스  (0) 2023.09.17
[TIL] GSLB(Global Server Load Balancing)  (0) 2023.03.30
[TIL] DNS 서버 Round Robin  (0) 2023.03.30