Work/개발 노트

[KANS] 1주차 - 컨테이너 격리 & 네트워크 및 보안

★용호★ 2024. 8. 31. 23:19

프로세스 개념

  • 컨테이너는 독립된 리눅스 환경을 보장 받는 프로세스

https://docs.docker.com/get-started/docker-overview/#docker-architecture

  • Docker를 설치하면 호스트에 Docker daemon이 설치되고, 클라이언트가 docker 명령을 실행하면 Docker daemon이 컨테이너를 실행함. 만약 호스트에 컨테이너 이미지가 존재하지 않으면 Registry에서 pull 받아서 생성
    • 이 때, 클라이언트와 Docker daemon 간에는 Unix socket을 사용
  • 프로세스는 실행 중인 프로그램의 인스턴스를 의미. OS에서 프로세스를 관리하며, 각 프로세스는 고유한 ID(PID)를 가짐.
    • 프로세스가 실행 되려면 CPU와 메모리 자원이 필요한데 OS 커널(Cgroup)에서 이를 관리함
  • 유용한 프로세스 관련 명령어
# 프로세스 정보 확인
ps

# /sbin/init 1번 프로세스 확인
# 프로세스별 CPU 차지율, Memory 점유율, 실제 메모리 사용량 등 확인 >> 비율로 표현되는 이유는?
ps aux
ps -ef

# pstree는 실행 중인 프로세스를 트리 구조로 표시하는 명령어
pstree --help
pstree
pstree -a
pstree -p
pstree -apn
pstree -apnT
pstree -apnTZ
pstree -apnTZ | grep -v unconfined

# 실시간 프로세스 정보 출력
top -d 1
htop

# pgrep는 실행 중인 프로세스를 검색하고 해당 프로세스의 PID를 출력하는 명령어
pgrep -h

# [터미널1]
sleep 10000

# [터미널2]
pgrep sleep
pgrep sleep -u root
pgrep sleep -u ubuntu
  • /proc를 왜 amazing directory라고 부를까?
    • /proc는 일반적인 디렉토리와는 다르게 실제 하드 디스크에 존재하지 않는 가상 파일 시스템
    • 커널에 의해 메모리에 동적으로 생성되고 관리됨
    • 실행 중인 프로세스, 하드웨어 구성, 커널 파라미터 등 시스템의 현재 상태에 대한 실시간 정보를 제공함
    • 파일을 읽으면 그 순간의 최신 정보를 얻을 수 있음
    • 일부 파일에 쓰기를 통해 커널 파라미터를 즉시 변경할 수 있음
    • 시스템 동작을 실시간으로 조정할 수 있는 강력한 인터페이스를 제공
    • 이런 독특하고 강력한 기능들로 인해 /proc는 시스템 관리와 모니터링에 매우 유용한 "놀라운(amazing)" 디렉토리로 여겨짐

Docker 구성 간단히 살펴보기

docker info 명령 실행 결과

  • docker info 명령을 실행하면 위와 같이 이번 스터디 시간에 학습할 Storage Driver, Cgroup, Network에 대한 정보를 확인할 수 있음

docker 서비스 상태 확인

  • systemctl 명령어로 docker 서비스의 상태를 살펴보면 유닉스 도메인 소켓인 /run/docker.sock를 통해 API를 리스닝 하고 있는 것을 확인할 수 있음
    • TCP/IP가 아닌 유닉스 도메인 소켓을 사용한 다는 것의 주의해야함
    • 유닉스 소켓의 사용 권한은 root만 가지고 있기 떄문에 docker를 설치하면 기본적으로 root만 실행이 가능
    • root 그룹에 일반 사용자를 추가하면 일반 사용자도 root 권한으로 docker를 실행 할 수 있음 (보안에 좋지않기 때문에 권장사항은 아님)

  • 유닉스 도메인 소켓은 동일한 시스템 내에서 실행되는 프로세스들 간의 통신을 의미 (여기서 도메인은 로컬 시스템 내의 통신 영역을 나타냄)
  • 유닉스 도메인 소켓은 프로세스간 통신이 가능하기 때문에 loopback 인터페이스를 거치는 TCP/IP 소켓 통신보다 통신 성능이 더 좋음

Tip) Jenkins에서 다른 docker 컨테이너와 통신하거나 컨테이너를 생성하려면?

docker run -d -p 8080:8080 -p 50000:50000 --name jenkins-server --restart=on-failure -v jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker jenkins/jenkins
  • Jenkins 컨테이너를 실행시킬 때 다른 컨테이너와 통신할 수 있도록 유닉스 도메인 소켓을 볼륨 마운트
    • Linux는 모든 것을 파일로 관리하고, 위에서 언급했던 것처럼 컨테이너 프로세스간 통신은 유닉스 도메인 소켓을 사용하기 때문에 해당 파일을 Jenkins 컨테이너로 마운트하면 통신이 가능해짐
    • 마찬가지로 docker 명령을 실행하는 바이너리 파일을 Jenkins 컨테이너로 볼륨 마운트하면 컨테이너 명령을 실행할 수 있게 됨. 이는 컨테이너가 커널을 공유하고 있고 유닉스 도메인 소켓으로 통신할 수 있는 구조가 되었기 때문에 root 권한만 있으면 컨테이너 안에서 docker 명령을 실행할 수 있음
      • 단, Docker in Docker의 개념은 아님

root 디렉토리 격리

https://speakerdeck.com/kakao/ige-dwaeyo-dokeo-eobsi-keonteineo-mandeulgi?slide=200

  • chroot 명령을 실행해서 root 파일 시스템을 격리하면 아래 그림과 같이 root 디렉토리 내용이 달라진 것을 볼 수 있음

  • chroot는 탈옥이 가능해서 컨테이너에서 이제 사용 안함. 아래와 같이 탈옥할 수 있는 코드를 실행하면 호스트의 root 파일 시스템으로 이동할 수 있음

  • 탈옥을 방지하기 위해 현재 컨테이너는 Mount 네임스페이스와 pivot_root를 사용함
    • Mount 네임스페이스는 프로세스 그룹이 볼 수 있는 파일시스템 마운트 지점 집합을 격리함. 이를 통해 컨테이너는 호스트와 다른 독립적인 파일시스템 계층 구조를 가질 수 있고, 컨테이너 내부의 마운트 작업이 호스트에 영향을 주지는 않음
    • pivot_root는 프로세스의 루트 파일시스템을 변경하는 데 사용함
    • 결과적으로 컨테이너는 호스트와 완전히 다른 파일시스템 뷰를 갖고, 컨테이너 내부의 파일시스템 변경이 호스트에 영향을 주지 않으며. 컨테이너는 호스트의 민감한 파일에 접근할 수 없게 되기 때문에 보안과 격리가 강화됨

Tip) 패키징된 컨테이너 이미지 내용 확인

docker export $(docker create nginx) | tar -C nginx-root -xvf -;
  • docker export 명령을 사용하면 컨테이너 이미지를 파일로 다운로드 받을 수 있음
  • 이 이미지 파일의 압축을 해제하면 nginx를 실행하기 위해 패키징된 파일 리스트를 확인할 수 있음

  • 내용을 잘 살펴보면 마치 하나의 Linux OS의 파일 시스템 같은 구조로 되어 있음
  • 컨테이너가 어떤 환경에서도 동일한 실행을 보장한다는 것은 바로 이런 이유로 가능

네임스페이스란?

  • 컨테이너에서 호스트의 다른 프로세스들이나 포트가 보이거나 컨테이너 안에 루트 권한 사용이 가능하다면 보안에 큰 위협이 될 수 있음
  • 컨테이너 별로 불필요하게 부여된 권한들은 최소화하고 격리 시키는 것이 중요
    • 격리 대상 : Mount(파일시스템), Network(네트워크), PID(프로세스 id), User(계정), ipc(프로세스간 통신), Uts(Unix time sharing, 호스트네임), cgroup
    • 격리할 때는 unshare 명령 사용
  • ls -al /proc/<PID>/ns 명령으로 특정 프로세스의 네임스페이스 정보를 확인 할 수 있지만 보통 lsns 명령을 사용함

여기서 $$은 현재 프로세스의 PID를 의미

  • 다른 프로세스와 네임스페이스의 아이노드 값이 동일하다면 해당 유형의 네임스페이스는 서로 공유된다는 것을 의미
  • NPROCS 컬럼은 현재 표시된 아이노드의 네임스페이스를 공유해서 사용하는 프로세스의 수를 의미

Tip) 컨테이너 간 공유 메모리 사용

# [터미널1]
docker run --rm --name test1 --ipc=shareable -it ubuntu bash
---------------------
ipcs -m
ipcmk -M 2000

lsns -p $$

# [터미널2]
# 호스트에서 확인
ipcs -m

# 컨테이너 생성 시 IPC 공유 : 해당 컨테이너는 test1 컨테이너와 IPC 네임스페이스를 공유
docker run --rm --name test2 --ipc=container:test1 -it ubuntu bash
---------------------
ipcs -m

lsns -p $$

  • Container1에서 공유 메모리를 만든 후 Container2를 생성할 때 Container1의 ipc 를 share 하도록 설정
  • Container1과 Container2의 ipc 네임스페이스 아이노드값이 동일하고, 이는 Shared Memory, Pipe, Message Queue등의 자원을 공유해서 사용할 수 있다는 의미

프로세스 네임스페이스

  • 아래 그림과 같이 호스트에서 컨테이너의 프로세스 아이디를 다 볼 수 있음. unshare 명령으로 프로세스를 격리한 후 PID를 확인해보면 현재 프로세스가 1번이고, 호스트에서는 PID 6035 프로세스가 보임. 격리된 프로세스에서 sleep 프로세스를 실행하면 마찬가지로 호스트에서 6044번으로 보임

  • 호스트에서 컨테이너가 실행 될 때 프로세스가 fork되고, 컨테이너를 위해 실행된 최초의 프로세스가 컨테이너 안에서는 PID 1번이 됨
    • 호스트에서 컨테이너의 프로세스를 Kill 할 수 있고, 1번 프로세스가 Kill되면 컨테이너가 종료됨

User 네임스페이스

https://speakerdeck.com/kakao/ige-dwaeyo-dokeo-eobsi-keonteineo-mandeulgi?slide=171

  • 컨테이너에서 루트 권한을 사용할 수 있게 되면 컨테이너가 탈취 됐을 경우 호스트에도 루트 권한으로 명령을 실행할 수 있게 되기 때문에 보안상 좋지 않음
    • Docker를 설치하면 컨테이너 생성 시 기본으로 root 권한의 프로세스가 실행됨
    • 처음부터 User 네임스페이스를 격리해버리면 필요한 패키지 설치가 불가능하기 때문에 설정 난이도가 너무 높아지기 때문에 기본은 root 권한으로 실행됨
    • 설정 변경으로 User 네임스페이스를 사용할 수 있고, Kubernetes에서도 User 네임스페이스를 지원하려고 함 (1.30에서 베타)

  • 위 그림에서 보면 컨테이너를 생성하면서 실행된 프로세스를 호스트에서 확인하면 root 권한으로 실행된 것을 확인할 수 있음
  • 네임스페이스 정보를 컨테이너와 호스트를 각각 비교해보면 동일한 User 네임스페이스를 사용하고 있는 것을 알 수 있음
  • 즉, Docker 설치 시 기본은 User 네임스페이스 격리를 사용하지 않는다는 의미이고, 보안을 강화하려면 User 네임스페이스를 적용하는 것이 좋음

cgroup이란?

  • cgroup(Control Group)은 프로세스 그룹의 시스템 리소스 사용을 제한하고 격리하는 커널 기능
    • 주로 리소스 관리와 제한에 사용되기 때문에 네임스페이스와는 다른 개념
    • cgroup 네임스페이스라는 것이 있긴 하지만 이는 cgroup 자체의 뷰를 격리하는 네임스페이스 유형이고, cgroup 기능과는 다른 개념임
  • 주요 특징
    • 리소스 제한: CPU, 메모리, 디스크 I/O, 네트워크 대역폭 등의 사용량 제한
    • 우선순위 지정: 리소스 경합 시 특정 그룹에 우선순위 부여
    • 계정 관리: 그룹별 리소스 사용량을 모니터링하고 보고
    • 제어: 그룹 내 모든 프로세스의 상태를 한 번에 제어
  • /sys/fs/cgroup 디렉토리에서 계층적 구조로 cgroup 정보를 확인할 수 있고, 서브 시스템(cpu, memory, blkio, devices, pids)이 존재함
  • cgroup v1과 v2의 차이점
    • 계층 구조:
      • v1: 각 리소스 컨트롤러(CPU, 메모리 등)가 독립적인 계층 구조를 가짐
      • v2: 단일 통합 계층 구조 사용
    • 마운트 방식:
      • v1: 각 컨트롤러를 별도의 디렉토리에 마운트
      • v2: 모든 컨트롤러가 하나의 디렉토리(/sys/fs/cgroup)에 마운트됨
    • 프로세스 관리:
      • v1: 프로세스를 중간 노드에 바인딩할 수 있음
      • v2: 프로세스는 루트(/) 디렉토리와 리프 노드에만 바인딩할 수 있음
    • 컨트롤러 활성화:
      • v1: 마운트된 컨트롤러를 바로 사용할 수 있음
      • v2: 하위 디렉토리에서 컨트롤러를 사용하려면 cgroup.subtree_control에서 활성화해야 함
    • 리소스 관리 기능:
      • v2: 향상된 리소스 할당 관리 및 격리 기능 제공
    • 메모리 관리:
      • v2: 네트워크 메모리, 커널 메모리 등 다양한 유형의 메모리 할당에 대한 통합 계정 제공
    • 성능 모니터링:
      • v2: Pressure Stall Information과 같은 새로운 기능 제공
    • 호환성:
      • v1: 오래된 시스템과의 호환성이 더 좋음
      • v2: 최신 Linux 배포판에서 기본적으로 사용되며, 컨테이너 생태계에서 더 많은 지원을 받고 있음
      1.  

네트워크 네임스페이스

  • ifconfig 명령으로는 pair로 묶인 veth 정보를 확인할 수 없지만 ip 명령으로는 확인할 수 있음
ip link add veth0 type veth peer name veth1
ip -c link

  • 네트워크 네임스페이스를 만들고, 격리된 네트워크 네임스페이스 안으로 미리 생성한 veth 네트워크 인터페이스를 등록하면 호스트에서 해당 네트워크 네임스페이스가 더이상 보이지 않게됨
# 네트워크 네임스페이스 생성 , man ip-netns
ip netns add RED
ip netns add BLUE

# 네트워크 네임스페이스 확인 
ip netns list

# veth0 을 RED 네트워크 네임스페이스로 옮김
ip link set veth0 netns RED
ip netns list
## 호스트의 ip a 목록에서 보이지 않음, veth1의 peer 정보가 변경됨
ip -c link
## RED 네임스페이스에서 ip a 확인됨(상태 DOWN), peer 정보 확인, link-netns RED, man ip-netns
ip netns exec RED ip -c a

  • 네트워크 인터페이스는 추가되면 기본적으로 Down 상태이고 ip도 할당되어 있지 않음. Up 시키고 Ip를 부여하는 절차를 진행해야함
# veth0, veth1 상태 활성화(state UP)
ip netns exec RED ip link set veth0 up
ip netns exec RED ip -c a
ip netns exec BLUE ip link set veth1 up
ip netns exec BLUE ip -c a

# veth0, veth1 에 IP 설정
ip netns exec RED ip addr add 11.11.11.2/24 dev veth0
ip netns exec RED ip -c a
ip netns exec BLUE ip addr add 11.11.11.3/24 dev veth1
ip netns exec BLUE ip -c a

  • 현재까지의 네트워크 설정은 veth0와 veth1 간에만 통신이 가능한 구조이기 때문에 다른 서버나 인터넷망을 통해 외부로 트래픽을 보낼 수 없음
  • 외부와 통신을 하기 위해 중간 다리 역할을 하는 무언가가 필요한데 이게 바로 브리지 네트워크
  • 브리지 네트워크와 통신을 하려면 서로 연결해주는 아래 그림과 같이 가상의 네트워크 인터페이스 필요함

  • 먼저 아래 명령으로 reth1과 beth1 pair를 생성하여 각각 RED와 BLUE 네트워크 네임스페이스에 추가
# 네트워크 네임스페이스 및 veth 생성
ip link add reth0 type veth peer name reth1
ip link set reth0 netns RED
ip link add beth0 type veth peer name beth1
ip link set beth0 netns BLUE

# 확인
ip netns list
ip -c link
ip netns exec RED ip -c a
ip netns exec BLUE ip -c a

  • 브리지 네트워크를 추가하고, reth1과 beth1을 연결한 후 각 네트워크 인터페이스를 UP 상태 전환 및 IP 할당
# br0 브리지 생성
ip link add br0 type bridge

# reth1 beth1 을 br0 연결
ip link set reth1 master br0
ip link set beth1 master br0
brctl show br0
brctl showmacs br0
ip -br -c link

# reth0 beth0 에 IP 설정 및 활성화, br0 활성화
ip netns exec RED  ip addr add 11.11.11.2/24 dev reth0
ip netns exec BLUE ip addr add 11.11.11.3/24 dev beth0
ip netns exec RED  ip link set reth0 up; ip link set reth1 up
ip netns exec BLUE ip link set beth0 up; ip link set beth1 up
ip link set br0 up
ip -br -c addr

  • 이제 컨테이너 간 통신도 가능하고 외부와 브리지 네트워크를 통해 통신할 수 있는 길이 열렸지만 바로 통신은 안됨
  • iptables에서는 기본적으로 FORWARD 설정이 DROP으로 되어있어서 포워딩 되는 트래픽이 차단됨
    • 트래픽을 받은 서버에서 라우팅 역할을 하는 경우에는 FORWARD가 필요한데 docker 사용 시 FORWARD 필요 (브리지 네트워크를 통할 때 FORWARD 됨)
    • Kubernetes에서도 네트워크 문제가 발생했을 때 iptables를 볼 줄 알아야함
  • 아래 명령으로 FORWARD 될 수 있도록 iptables 수정 필요
# iptables 설정 추가 -t(table), -I(insert chain), -j(jump to - ACCEPT 허용)
iptables -t filter -I DOCKER-USER -j ACCEPT
iptables -nvL -t filter
iptables -t filter -S
iptables -t nat -S  | grep '\-P'

  • 이 후 BLUE 네트워크 네임스페이스에서 RED의 네트워크 인터페이스로 ping을 요청해보면 정상적으로 트래픽이 전송되는 걸 확인할 수 있음

  • 지금까지 네트워크 네임스페이스를 직접 만들어서 통신 가능하도록 만든 구조가 Docker를 설치 했을 때 자동으로 생성되는 docker0 브리지 네트워크와 내부 컨테이너들의 통신 구조와 동일