1주차 [1] - 도커 컨테이너 격리
1. 도커 컨테이너 격리
1.1 도커 컨테이너 격리 요약
컨테이너는 독립된 리눅스 환경(pivot-root, namespace, Overlay filesystem, cgroup)을 보장받는 프로세스이다!
컨테이너는 애플리케이션(프로세스) 동작에 필요한 파일들만 패키징된 이미지를 실행하여 동작한다.
컨테이너는 컨테이너 환경이 조성된 곳 어디서나 실행할 수 있다.
1.2 도커란?
도커는 가상 실행 환경을 제공해주는 오픈소스 플랫폼이다.
도커는 이 가상 실행 환경을 컨테이너라고 부른다.
1.3 컨테이너와 가상 머신
도커 컨테이너 vs 가상 머신
가상 머신은 운영체제 위에 하드웨어를 에뮬레이션하고 그 위에 운영체제를 올리고 프로세스를 실행한다.
도커 컨테이너는 하드웨어 에뮬레이션 없이, 커널을 공유해서 프로세스를 실행한다.
차이점은 하드웨어 에뮬레이션, 운영체제 vs 커널 공유 실행
가상머신 vs 컨테이너 환경 무엇이 격리 수준이 더 높을까?
가상 머신은 기존 서버에 하이퍼바이저가 올라간 형태
컨테이너는 운영체제를 제외한 나머지 애플리케이션 실행에 필요한 모든 파일을 패키징한다.
그러므로, 가상 머신은 고립성이 더 좋다!
컨테이너는 호스트의 커널을 공유하지만, 개별적인 사용자 공간을 가진다.
1.4 도커 아키텍처
도커에 대한 간단한 아키텍처는 다음과 같이 나타낼 수 있습니다.
Client에서 Docker Daemon으로 API 통신을 통해서 IMAGE에 대한 컨테이너 생성을 합니다.
2. 도커 기본 사용
2.1 사전 준비 사항
Linux Process에 대한 이해
cat /proc/cpuinfo # CPU 에 대한 정보를 나열합니다.
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 79
model name : Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
stepping : 1
microcode : 0xb000040
cpu MHz : 2300.029
cache size : 46080 KB
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm cpuid_fault invpcid_single pti fsgsbase bmi1 avx2 smep bmi2 erms invpcid xsaveopt
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit mmio_stale_data bhi
bogomips : 4600.01
address sizes : 46 bits physical, 48 bits virtual
cat /proc/meminfo # 메모리 사용량 현황을 보여줍니다.
ubuntu@MyServer:~$ cat /proc/meminfo
MemTotal: 972024 kB
MemFree: 504424 kB
MemAvailable: 643976 kB
Buffers: 14988 kB
Cached: 255524 kB
cat /proc/uptime # 시스템이 부팅된 후 경과된 시간을 초단위로 보여줍니다.
6839.71 6820.12
cat /proc/loadavg # 시스템의 부하를 보여줍니다.
0.00 0.00 0.00 1/124 968
cat /proc/version # 커널 버전
Linux version 6.5.0-1024-aws (buildd@lcy02-amd64-013)
cat /proc/filesystems # 커널이 인식하고 있는 파일 시스템의 목록을 보여줍니다.
nodev sysfs
nodev tmpfs
nodev bdev
nodev proc
nodev cgroup
nodev cgroup2
nodev cpuset
nodev devtmpfs
nodev configfs
nodev debugfs
nodev tracefs
nodev securityfs
nodev sockfs
nodev bpf
nodev pipefs
nodev ramfs
nodev hugetlbfs
nodev devpts
ext3
ext2
ext4
squashfs
vfat
nodev ecryptfs
fuseblk
nodev fuse
nodev fusectl
nodev efivarfs
nodev mqueue
nodev pstore
nodev autofs
nodev binfmt_misc
cat /proc/partitions # 시스템에서 인식된 파티션 정보를 제공합니다.
major minor #blocks name
7 0 25844 loop0
7 1 56996 loop1
7 2 65480 loop2
7 3 89128 loop3
7 4 39760 loop4
202 0 31457280 xvda
202 1 31343599 xvda1
202 14 4096 xvda14
202 15 108544 xvda15
2.2 도커 설치 및 확인
# UBUNTU에 Docker를 설치합니다.
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --dry-run
ubuntu@MyServer:~$ docker info # 도커 정보들 확인합니다.
docker version
Client: Docker Engine - Community
Version: 27.2.0
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.16.2
Path: /usr/libexec/docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v2.29.2
Path: /usr/libexec/docker/cli-plugins/docker-compose
3. 컨테이너 격리 (이번주의 중요한 내용!)
3.1 Chroot + 탈옥
chroot 으로 user 디렉터리를 user 프로세스에게 root 라고 속일 수 있습니다.
먼저 chroot으로 bin/sh를 실행해 봅니다.

실패하는 것을 확인 할 수 있습니다.
이렇게 의존성을 파악한뒤 라이브러리 파일들을 복사해서 가져옵니다.


의존성 파일들을 가져와서 넣어주니깐, /bin/sh 가 동작하는 것을 확인 할 수 있습니다.
하지만, 탈출이 되지 않는것을 확인 할 수 있습니다!
# 탈옥 코드
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/sh", "-i", NULL);
}
탈옥 코드를 컴파일해서 사용해 볼까요?

실행 시키면, 탈출이 되는것을 확인할 수 있습니다.
3.2 마운트 네임스페이스 + Pivot_root
chroot 차단 위해서, pivot_root 와 mount ns 를 사용합니다.
루트 파일 시스템을 변경하게되고 프로세스 환경에 대한 격리를 하게됩니다.
unshare --mount /bin/sh
를 사용했을때, 마운트 정보 확인해서 호스트와 동일한지 확인

처음은 동일한것을 확인하였습니다.
마운트 네임스페이스를 unshare를 진행하였습니다.
mkdir new_root
mount -t tmpfs none new_root


/root/new_root 가 unshared되서 안보이는 상태가 되었습니다!
이번에는, pivot_root를 사용해서 변경될 root 파일 시스템 경로로 진입하겠습니다.
mkdir new_root/put_old
cd new_root
pivot_root . put_old
탈옥이 되는지 실험해 보겠습니다!
# 탈옥 코드
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/sh", "-i", NULL);
}
코드를 돌려도 탈출이 되지 않는것을 확인할 수 있습니다!!
3.3 격리 Namespace
네임 스페이스와 관련된 프로세스에 대한 특징
모든 프로세스들은 네임스페이스 타입별로 특정 네임스페이스에 속한다.
Child는 Parent 네임스페이슬르 상속 받습니다.
프로세스는 네임 스페이스의 타입별로 일부는 호스트 네임스페이스를 사용하고 일부는 컨테이너의 네임스페이스를 사용합니다.
Namespace라는 격리 공간을 통해, Host System의 자원을 할당받고, 다른 namespace와 별개로 동작한다고한다.
name space는 짧게 얘기하면, 프로세스가 볼수 있는 범위이다.
lsns -p 1 ## List system namespaces

// mount name sapce에 대한 unshare 하고 나서 보면,
root는 자기자신이 보이지만, 다른 프로세스들은 보이지 않게 된다.
mnt 만 PID가 2600 인것을 볼 수 있다.

UTS 네임스페이스 격리
# 호스트, 도메인명 격리

PID 네임스페이스 만들기
1. /proc 파일시스템 마운트
unshare -fp --mount-proc /bin/sh
lsns -t pid -p 1
2. 내부에서 PID NS 확인 : 아래 터미널2에서 lsns -t pid -p <위 출력된 PID>와 비교
lsns -t pid -p 1
3. 격리된 PROC NAMESPACE와 호스트 NAMESPACE 비교를 합니다.
ps -ef
ps aux
ps aux | grep '/bin/sh'
lsns -t pid -p [PID]
Docker Root 사용으로 컨테이너 프로세스를 종료 할 수 있습니다.
sudo usermod -aG docker ubuntu
# 유저 호스트와 ID 비교
docker run -it ubuntu /bin/sh
id
uid=0(root) gid=0(root) groups=0(root)
# 컨테이너 User namespace
readlink /proc/$$/ns/user
4026531837
#--------------------------------------------------#
readlink /proc/$$/ns/user ## host에서 수행
4026531837
동일한것을 확인 할 수 잇다
이제 UserNamespace 격리를 진행한다.
UID/GID 넘버스페이스 격리로, 컨테이너의 루트 권한문제를 해결 합니다.
보안 관점에서는 큰 개선
1 . usernamespace를 격리합니다.
그리고 id를 확인합니다.

grep 시에, 일반 유저로 보인다.
3.4 자원 관리, cgroups
Cgroup 이란, 컨테이너별로 자원을 분배하고 limit 내에 운용할 수 있도록 함
# Cgroups를 설치합니다.
apt install cgroup-tools stress -y
# stree 실행 후에 cpu 사용률을 확인할 수 있습니다.
htop 으로 두번째 터미널에 실행 시킨뒤
# cpu.max 제한 설정 : 첫 번쨰 값은 허용된 시간(마이크로초) 두 번째 값은 총 기간 길이 > 1/10 실행 설정
echo 100000 1000000 > /sys/fs/cgroup/test_cgroup_parent/cpu.max
# 부하를 만들면 cpu 최대 사용률이 10퍼센트 이하로 찍히는것을 볼 수 있습니다.
stress -c 1

5. 컨테이너 네트워크 & IP Tables
5.1 요약
컨테이너는 네트워크 네임스페이스로 호스트와 네트워크 격리 환경이 구성된다.
리눅스는 방화벽을 제공하는 IPTables로 호스트와 컨테이너 통신에 관여한다.
5.2 RED < - > BLUE 네트워크 네임스페이스 간 통신
물리 인스턴스를 가상화하여서 가상 인터페이스를 만들면, 컨테이너의 가상 네트워크 장치를 사용할 수 있다.
여러 네이트워크 네임스페이스에 동시에 존재 가능하며 다른 네트워크 네임스페이스로 이동할 수 있다.

먼저 RED, BLUE 네트워크 네임스페이스들을 생성합니다.
# 인터페이스를 추가합니다.
ip link add veth0 type veth peer name veth1
# 네임스페이스를 생성합니다.
root@MyServer:~#
ip netns add RED
ip netns add BLUE
root@MyServer:~# ip netns list
BLUE
RED
# 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
# veth1 을 BLUE 네트워크 네임스페이스로 옮김
ip link set veth1 netns BLUE
ip -c link
ip netns exec BLUE ip -c a
# 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

네임스페이스 변경 이후의 값 확인

라우팅에 대한 정보를 확인해보면 정상적으로 맵핑되어 있다!

맵핑된이후 tcp dump 를 떠서, ping을 줬는데 packet transmitted가 실패했다.
이부분은 추가적으로 진행할 예정이다.
Last updated