CloudNet@팀에서 진행하는 쿠버네티스 네트워크 스터디 3기 참가글입니다.
‘쿠버네티스’ 네트워크에 대해서 직접 실습을 통해 장애 시 해결 능력을 갖추고, 학습 내용을 전파 및 공유합니다!
1. Docker
쿠버네티스를 이야기 하기 전에, 컨테이너 가상화를 이야기 안하고 넘어갈 수는 없다.
가상화(Virtualization)
하나 또는 다수의 물리적 서버를 효율적으로 활용하기 위해서 서버 가상화의 개념이 고안되었다. 하나의 대형 하드웨어를 논리적으로 작은 크기로 나누어 사용하거나, 다수의 하드웨어를 논리적으로 하나로 통합하여 사용 목적에 맞게 효율적으로 운용할 수 있다.
그 구체적인 방법으로, 먼저 가상머신(Virtual Machine, VM)을 통한 가상화가 사용되었다. 이는 가상화 도구(Hypervisor)를 사용해 다수의 Physical Guest OS를 구현하였다.
- 장점: 자체 OS를 포함한 모든 구성요소를 사용할 수 있는 완전한 머신. 상호 격리되어 보안성 상승
- 단점: OS위에 추가적인 독립 OS들을 구성하기 때문에 리소스의 낭비(운영간접비,Overhead) 발생,
> Guest 1의 resouce(CPU, RAM, 등)가 사용되지 않더라도 Guest 2가 해당 리소스를 사용하지 못함
그에 비해 컨테이너를 통한 가상화는 VM과 달리 호스트 OS를 공유하며, 프로세스 단위로 구분되는 OS레벨의 가상화로 구현되었다.
- 장점: 단일 어플리케이션 구동을 위한 최소한의 환경만 구분 > 빠른 속도
- 단점: VM 처럼 다양한 OS 사용 불가, 보안적으로 완전히 격리되어있지않음
Docker는 리눅스 컨테이너를 이용하여 프로세스 단위별 가상화 실행 환경을 제공하는 오픈소스 플랫폼이다. 도커는 client-server 구조를 가지고 있어, 사용자의 명령(client, cli)을 통해 호스트에 실행되는 daemon(server, dockerd)이 실제 프로세스(컨테이너)를 실행, 중지, 제거를 실행한다. 또한 사용을 용이하게 할 수 있도록, 컨테이너 실행에 필요한 이미지 저장소인 Registry 및 기타 Extensions와 Plugins를 제공한다.
Docker architecture, 구버전(좌측) / 신버전(우측)
도커의 컨테이너 격리는, 리눅스 기반 시스템에서 프로세스 간 격리를 위해 사용하던 기술들을 조합해서 발전해왔다. 이는 1979년 chroot부터, 2008년 LXC(Linux Container)의 cgroups, namespace 등의 기반 기술을 통해 2013년 docker 까지 이어지게 되었다.
- chroot: 특정 디렉토리를 root(최상위 디렉토리)로 인식
- namespace: 리눅스 시스템 자원을 프로세스에 할당
- cgroup: 특정 프로세스가 CPU/Mem같은 특정 자원을 과다 사용 방지
2. Docker Container 격리 이해
Docker가 linux의; 여러 기반기술을 사용해 프로세스 단위로 격리된 환경을 제공한다는 것을 알았으니, docker 없이 컨테이너를 만들어보며 그 기반기술의 작동을 확인해볼 것이다.
2.1. Linux Process의 이해
Linux에서 프로세스는 실행 중인 프로그램을 의미한다. 디스크에 저장된 프로그램이 실제로 메모리에 로드되고 CPU에서 실행되는 상태이며, 각 프로세스는 고유한 ID(PID)를 갖는다. 프로세스는 CPU와 메모리를 사용하는 기본 단위로, OS 커널(Cgroup)에서 각 프로세스의 자원을 관리한다.
실습 스크립트
# 프로세스 정보 확인
ps
# /sbin/init 1번 프로세스 확인
# 프로세스별 CPU 차지율, Memory 점유율, 실제 메모리 사용량 등 확인 >> 비율로 표현되는 이유는?
ps aux
ps -ef
# 프로세스 트리 확인
pstree --help
pstree
pstree -a
pstree -p
pstree -apn
pstree -apnT
pstree -apnTZ
pstree -apnTZ | grep -v unconfined
# 실시간 프로세스 정보 출력
top -d 1
htop
# 특정 프로세스 정보 찾기
pgrep -h
# [터미널1]
sleep 10000
# [터미널2]
pgrep sleep
pgrep sleep -u root
pgrep sleep -u ubuntu
결과
- ps (프로세스 정보 확인)
- 프로세스별 CPU 차지율, Memory 점유율, 실제 메모리 사용량 등 확인
>> 비율로 표현되는 이유는? - 동일행의 VSZ(가상메모리),RSS(물리메모리)처럼 실제 사용량(byte)으로만 표시되면 출력값도 길어지고, 접근한 서버의 전체 스펙은 어떤지 재확인필요
- 직관적으로 파악하기엔 비율이 구체적인 수치 대비 용이
- pstree (프로세스 트리 확인)
- top/htop (실시간 프로세스 정보 출력)
- pgrep (특정 프로세스 정보 찾기)
- 리눅스의 /proc 디렉터리는 커널이 동적으로 생성하는 정보를 실시간 제공 : 시스템 상태, 프로세스(/proc/[PID]), HW 정보
/proc/cpuinfo
: CPU에 대한 정보가 포함되어 있습니다. CPU 모델, 코어 수, 클럭 속도 등의 정보를 확인할 수 있습니다./proc/meminfo
: 메모리 사용 현황을 보여줍니다. 전체 메모리, 사용 중인 메모리, 가용 메모리, 캐시 메모리 등 다양한 메모리 관련 정보를 제공합니다./proc/uptime
: 시스템이 부팅된 후 경과된 시간을 초 단위로 보여줍니다. 첫 번째 숫자는 총 가동 시간, 두 번째 숫자는 시스템의 유휴 시간입니다./proc/loadavg
: 시스템의 현재 부하 상태를 나타냅니다. 첫 번째 세 개의 숫자는 1, 5, 15분간의 시스템 부하 평균을 의미하며, 네 번째 숫자는 현재 실행 중인 프로세스와 총 프로세스 수, 마지막 숫자는 마지막으로 실행된 프로세스의 PID를 나타냅니다./proc/version
: 커널 버전, GCC 버전 및 컴파일된 날짜와 같은 커널의 빌드 정보를 포함합니다./proc/filesystems
: 커널이 인식하고 있는 파일 시스템의 목록을 보여줍니다./proc/partitions
: 시스템에서 인식된 파티션 정보를 제공합니다. 디스크 장치와 해당 파티션 크기 등을 확인할 수 있습니다.
- 프로세스(/proc/[PID]) 별 정보
/proc/[PID]/cmdline
: 해당 프로세스를 실행할 때 사용된 명령어와 인자를 포함합니다./proc/[PID]/cwd
: 프로세스의 현재 작업 디렉터리에 대한 심볼릭 링크입니다.ls -l
로 확인하면 해당 프로세스가 현재 작업 중인 디렉터리를 알 수 있습니다./proc/[PID]/environ
: 프로세스의 환경 변수를 나타냅니다. 각 변수는 NULL 문자로 구분됩니다./proc/[PID]/exe
: 프로세스가 실행 중인 실행 파일에 대한 심볼릭 링크입니다./proc/[PID]/fd
: 프로세스가 열어놓은 모든 파일 디스크립터에 대한 심볼릭 링크를 포함하는 디렉터리입니다. 이 파일들은 해당 파일 디스크립터가 가리키는 실제 파일이나 소켓 등을 참조합니다./proc/[PID]/maps
: 프로세스의 메모리 맵을 나타냅니다. 메모리 영역의 시작과 끝 주소, 접근 권한, 매핑된 파일 등을 확인할 수 있습니다./proc/[PID]/stat
: 프로세스의 상태 정보를 포함한 파일입니다. 이 파일에는 프로세스의 상태, CPU 사용량, 메모리 사용량, 부모 프로세스 ID, 우선순위 등의 다양한 정보가 담겨 있습니다./proc/[PID]/status
: 프로세스의 상태 정보를 사람이 읽기 쉽게 정리한 파일입니다. PID, PPID(부모 PID), 메모리 사용량, CPU 사용률, 스레드 수 등을 확인할 수 있습니다.
실습 스크립트
#
mount -t proc
findmnt /proc
TARGET SOURCE FSTYPE OPTIONS
/proc proc proc rw,nosuid,nodev,noexec,relatime
#
ls /proc
tree /proc -L 1
tree /proc -L 1 | more
# 커널이 동적으로 생성하는 정보
cat /proc/cpuinfo
cat /proc/meminfo
cat /proc/uptime
cat /proc/loadavg
cat /proc/version
cat /proc/filesystems
cat /proc/partitions
# 실시간(갱신) 정보
cat /proc/uptime
cat /proc/uptime
cat /proc/uptime
# 프로세스별 정보
ls /proc > 1.txt
# [터미널1]
sleep 10000
# [터미널2]
## 프로세스별 정보
ls /proc > 2.txt
ls /proc
diff 1.txt 2.txt
pstree -p
ps -C sleep
pgrep sleep
## sleep 프로세스 디렉터리 확인
tree /proc/$(pgrep sleep) -L 1
tree /proc/$(pgrep sleep) -L 2 | more
## 해당 프로세스가 실행한 명령 확인
cat /proc/$(pgrep sleep)/cmdline ; echo
## 해당 프로세스의 Current Working Directory 확인
ls -l /proc/$(pgrep sleep)/cwd
## 해당 프로세스가 오픈한 file descriptor 목록 확인
ls -l /proc/$(pgrep sleep)/fd
## 해당 프로세스의 환경 변수 확인
cat /proc/$(pgrep sleep)/environ ; echo
cat /proc/$(pgrep sleep)/environ | tr '\000' '\n'
## 해당 프로세스의 메모리 정보 확인
cat /proc/$(pgrep sleep)/maps
## 해당 프로세스의 자원 사용량 확인
cat /proc/$(pgrep sleep)/status
## 기타 정보
ls -l /proc/$(pgrep sleep)/exe
cat /proc/$(pgrep sleep)/stat
결과
- sleep 명령 실행 후 해당 명령어를 실행하는 3264 pid 생성됨 확인
- sleep 프로세스 디렉터리 확인
- 기타 실행정보 확인
2.2 Unix Domain Socket으로 통신하는 docker process
Docker daemon(server)는 root user로 실행되며, TCP포트가 아닌 Unix socket에 할당되어있다. 해당 소켓은 root의 소유권에 속해 있으며 다른 유저는 sudo등을 사용해야 접근이 가능하다. Manage Docker as a non-root user
네트워크 통신구조를 7단계로 설명한 근-본 OSI 7 Layer외에, 최근에는 이를 좀더 단순화하여 4단계로 통합한 TCP/IP모델을 많이 사용한다. 작업중인 프로세스가 네트워크 통신이 필요한 경우 응용계층에서 물리계층간 캡슐화/역캡슐화를 통해 통신한다.
TCP/IP Socket 통신의 경우 전송 계층(Transport Layer)와 네트워크 계층(Network Layer), 또는 loopback인터페이스(127.0.0.1, 동일 컴퓨터 내)을 사용해 통신할 수 있다.
UNIX Domain Socket(UDS) 방식의 경우 응용 계층(Application Layer)에서 전송 계층(Transport Layer)으로 전달되고 바로 응용 계층으로 전달되어 TCP/IP 4개층을 통신해야하는 TCP/IP Socket통신 대비 속도가 빠르다는 것이 장점이다.
Unix Socket 과 TCP Socket은 어떤 차이가 있을까?
UNIX Domain Socket 통신의 경우 파일(*.sock)을 기반으로 작동하기에 다음과 같은 장점을 추가로 갖는다.
- 파일 권한에 따른 접근제어 가능
- 신뢰성있는 통신 가능
- file descriptor를 전송하여 신뢰할수 없는 파일을 처리하기에 유용하다는데.. checksum처럼 파일원본의 진위여부를 보장한다는 의미인지?흠
실습 스크립트
# docker 설치
curl -fsSL https://get.docker.com | sudo sh
# [터미널2] 일반 유저 ubuntu 로 실습 진행
whoami
# 도커 서버 정보 획득 실패
docker info
...
Server:
ERROR: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.46/info": dial unix /var/run/docker.sock: connect: permission denied
errors pretty printing info
#
ls -l /run/docker.sock /var/run/docker.sock
file /var/run/docker.sock
# [터미널1] 관리자
sudo su -
#
sudo systemctl status docker -l --no-pager
docker info
# 소켓 정보 확인 : tcp, udp, sctp, Unix Domain
ss -h | grep sockets
ss -tl # 혹은 ss --tcp --listening
ss -xl # 혹은 ss --unix --listening
ss -xl | grep -i docker
u_str LISTEN 0 4096 /run/docker.sock 69239 * 0
u_str LISTEN 0 4096 /var/run/docker/metrics.sock 69882 * 0
u_str LISTEN 0 4096 /var/run/docker/libnetwork/914c2d2f1446.sock 69422 * 0
# 특정 소켓 파일을 사용하는 프로세스 확인
lsof /run/docker.sock
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root 40u unix 0xffff96bd96236a80 0t0 69239 /run/docker.sock type=STREAM
dockerd 5178 root 4u unix 0xffff96bd96236a80 0t0 69239 /run/docker.sock type=STREAM
# unix domain socket 중 docker 필터링
lsof -U | grep -i docker
# [터미널2] 일반 유저 ubuntu 로 실습 진행
whoami
# Create the docker group : 도커 스크립트 생성 시 자동 생성되어 그룹 확인만 진행
sudo groupadd docker
getent group | tail -n 3
# Add your user to the docker group.
echo $USER
sudo usermod -aG docker $USER
# ssh logout
exit
# ssh 재접속 후 확인
#
docker info
# 컨테이너 실행
docker run hello-world
#
docker ps
docker ps -a
docker images
# 중지된 컨테이너 삭제
docker ps -aq
docker rm -f $(docker ps -aq)
docker ps -a
결과
- 일반 유저의 docker 사용 불가 / root는 가능
- root 유저의 소켓정보확인
- docker group 등록 후 일반 유저의 사용 가능
도커 설치 이후 기본 정보 확인
# [터미널1] 관리자 권한
# 프로세스 확인 - 셸변수
ps -ef
pstree -p
# 시스템에 (마운트 된) disk free 디스크 여유 공간 확인
df -hT
# 네트워크 정보 확인 >> docker0 네트워크 인터페이스가 추가됨, 현재는 DOWN 상태
ip -br -c addr
ip -c addr
ip -c link
ip -br -c link
ip -c route
# 이더넷 브리지 정보 확인
brctl show
# iptables 정책 확인
iptables -t filter -S
iptables -t nat -S
## filter 에 FORWARD 가 기존 ACCEPT 에서 DROP 로 변경됨
## filter 에 FORWARD 에 docker0 에서 docker0 혹은 외부로 전달 허용 정책이 추가됨
iptables -t filter -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
## nat POSTROUTING 에 172.17.0.0/16 에서 외부로 전달 시 매스커레이딩(SNAT) 정책이 추가됨
iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
결과
- docker0 네트워크 인터페이스 정보 확인
- iptables 규칙 확인
3. 도커 없이 컨테이너 만들기
3.1. Chroot + 탈옥 – Youtube , Github , Docs
chroot root directory : user 디렉터리를 user 프로세스에게 root 디렉터리를 속임
chroot 생성하기
# [터미널1] 관리자 전환
sudo su -
whoami
#
cd /tmp
mkdir myroot
# chroot 사용법 : [옵션] NEWROOT [커맨드]
chroot myroot /bin/sh
chroot: failed to run command ‘/bin/sh’: No such file or directory
#
tree myroot
which sh
ldd /bin/sh
# 바이러리 파일과 라이브러리 파일 복사
mkdir -p myroot/bin
cp /usr/bin/sh myroot/bin/
mkdir -p myroot/{lib64,lib/x86_64-linux-gnu}
tree myroot
cp /lib/x86_64-linux-gnu/libc.so.6 myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 myroot/lib64
tree myroot/
#
w
--------------------
ls
exit
--------------------
#
which ls
ldd /usr/bin/ls
#
cp /usr/bin/ls myroot/bin/
mkdir -p myroot/bin
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 myroot/lib64
tree myroot
#
chroot myroot /bin/sh
--------------------
ls /
## 탈출 가능한지 시도
cd ../../../
ls /
# 아래 터미널2와 비교 후 빠져나오기
exit
--------------------
# chroot 요약 : 경로를 모으고(패키징), 경로에 가둬서 실행(격리)
# [터미널2]
# chroot 실행한 터미널1과 호스트 디렉터리 비교
ls /
결과
- myroot 디렉토리를 생성하여도 내부는 비어있어서 명령어 실행불가
- 필요한 바이너리 확인 후 cp
- chroot 했을 경우 호스트와 달리 fake root 디렉토리에 갇힘
chroot 에서 ps 실행해보기 – Link
# copy ps
ldd /usr/bin/ps;
cp /usr/bin/ps /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libprocps.so.8,libc.so.6,libsystemd.so.0,liblzma.so.5,libgcrypt.so.20,libgpg-error.so.0,libzstd.so.1,libcap.so.2} /tmp/myroot/lib/x86_64-linux-gnu/;
mkdir -p /tmp/myroot/usr/lib/x86_64-linux-gnu;
cp /usr/lib/x86_64-linux-gnu/liblz4.so.1 /tmp/myroot/usr/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;
# copy mount
ldd /usr/bin/mount;
cp /usr/bin/mount /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libmount.so.1,libc.so.6,libblkid.so.1,libselinux.so.1,libpcre2-8.so.0} /tmp/myroot/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;
# copy mkdir
ldd /usr/bin/mkdir;
cp /usr/bin/mkdir /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} /tmp/myroot/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;
# tree 확인
tree myroot
#
chroot myroot /bin/sh
---------------------
# 왜 ps가 안될까요?
ps
Error, do this: mount -t proc proc /proc
#
mount -t proc proc /proc
mount: /proc: mount point does not exist.
#
mkdir /proc
mount -t proc proc /proc
mount -t proc
# ps는 /proc 의 실시간 정보를 활용
ps
ps auf
ps aux
ls -l /proc
exit
---------------------
# 실습 시 사용한 proc 마운트 제거
mount -t proc
sudo umount /tmp/myroot/proc
mount -t proc
결과
- /proc을 mount 해야만 사용가능한 ps
ps 명령어에 마운트가 필요했던 이유
처음에는 /sh 기본 명령어 여부차이라고 생각했는데 ls도 별도 바이너리 필요한 명령어라서 생각으로는 모르겠어 GPT 돌렸읍니다…
cd나 ls 같은 명령어는 file system만으로도 작동(생성,조회,이동,제거 등)이 가능
ps는 현재 프로세스 정보 표시를 위해 /proc fs에 접근해야하므로 마운트 필요
- 왜 cd나 ls같은 명령어는 되는데 ps는 마운트가 필요해?
cd
나 ls
같은 명령어는 ps
와 달리 파일 시스템 내에서의 디렉터리와 파일을 탐색하거나 목록을 나열하는 기능을 수행합니다. 이러한 명령어들은 기본적으로 프로세스 정보나 커널 데이터에 접근할 필요가 없습니다.
반면에, ps
는 현재 실행 중인 프로세스의 상태와 정보를 보여주는 명령어로, 운영체제의 가상 파일 시스템인 /proc
에 접근하여 필요한 정보를 가져옵니다.
- 왜
ps
는/proc
이 필요해?
ps
명령어는 시스템의 현재 프로세스에 대한 정보를 표시하기 위해 /proc
파일 시스템을 사용합니다. /proc
파일 시스템은 실제로는 디스크에 존재하지 않는 가상 파일 시스템으로, 메모리 내에 있는 커널 데이터 구조를 파일 시스템처럼 접근할 수 있게 만들어줍니다. 이곳에는 각 프로세스의 상태, 메모리 사용량, CPU 시간, 실행 중인 프로그램의 경로 등의 정보가 포함되어 있습니다.
- 그외에 기본적으로 사용할 수 있는 명령어와 마운트해야만 사용할 수 있는 명령어를 나열해줘
chroot 환경에서 사용할 수 있는 명령어는 기본적으로 시스템 파일이나 가상 파일 시스템(/proc, /sys, /dev)에 대한 의존성 여부에 따라 달라집니다. 기본적으로 사용할 수 있는 명령어와 마운트해야만 사용할 수 있는 명령어를 아래에 나누어 설명하겠습니다.
기본적으로 사용할 수 있는 명령어
이 명령어들은 기본적으로 파일 시스템의 파일이나 디렉터리를 직접적으로 다루기 때문에, 가상 파일 시스템(/proc, /sys, /dev)에 의존하지 않습니다.
cd: 디렉터리를 변경하는 명령어.
ls: 파일과 디렉터리 목록을 나열하는 명령어.
cp: 파일이나 디렉터리를 복사하는 명령어.
mv: 파일이나 디렉터리를 이동하거나 이름을 변경하는 명령어.
rm: 파일이나 디렉터리를 삭제하는 명령어.
mkdir: 새 디렉터리를 생성하는 명령어.
rmdir: 빈 디렉터리를 삭제하는 명령어.
echo: 문자열을 출력하는 명령어.
cat: 파일 내용을 출력하는 명령어.
touch: 빈 파일을 생성하거나 파일의 타임스탬프를 변경하는 명령어.
grep: 텍스트에서 패턴을 검색하는 명령어.
find: 파일과 디렉터리를 검색하는 명령어.
chmod: 파일이나 디렉터리의 권한을 변경하는 명령어.
chown: 파일이나 디렉터리의 소유자를 변경하는 명령어.
head, tail: 파일의 시작 또는 끝 부분을 출력하는 명령어.
ln: 심볼릭 링크나 하드 링크를 생성하는 명령어.
이 명령어들은 일반적으로 시스템의 표준 라이브러리와 디스크에 있는 파일을 사용하는데, 가상 파일 시스템에 대한 접근이 필요하지 않습니다.
마운트해야만 사용할 수 있는 명령어
이 명령어들은 시스템 정보, 하드웨어 상태, 프로세스 상태 등에 대한 정보를 필요로 하며, 이러한 정보는 일반적으로 가상 파일 시스템(/proc, /sys, /dev)에서 제공됩니다.
ps: 현재 실행 중인 프로세스의 상태를 표시합니다. /proc 파일 시스템을 통해 프로세스 정보를 가져옵니다.
top, htop: 현재 실행 중인 프로세스의 실시간 상태를 모니터링합니다. /proc 파일 시스템에 의존합니다.
free: 시스템의 메모리 사용 상태를 보여줍니다. /proc/meminfo에 의존합니다.
df: 파일 시스템의 디스크 사용량을 보여줍니다. /proc/mounts나 /proc/self/mountinfo에 의존할 수 있습니다.
mount: 파일 시스템을 마운트하는 명령어. /proc 파일 시스템이 마운트되어야 /proc/mounts와 같은 파일을 통해 마운트된 파일 시스템을 확인할 수 있습니다.
umount: 파일 시스템을 마운트 해제하는 명령어. 마운트된 파일 시스템 목록은 /proc을 통해 접근됩니다.
dmesg: 커널의 메시지 버퍼를 표시합니다. /dev/kmsg 또는 /proc/kmsg에 접근해야 합니다.
lscpu: CPU 아키텍처 정보를 표시합니다. /proc/cpuinfo에 접근합니다.
lsblk: 블록 장치 정보를 나열합니다. /proc과 /sys에 접근해야 합니다.
fdisk, parted: 디스크 파티션을 조작하는 명령어. /proc/partitions와 /dev에 의존합니다.
ifconfig, ip: 네트워크 인터페이스 정보를 표시하고 설정합니다. /proc/net과 /sys/class/net에 접근합니다.
uptime: 시스템이 얼마나 오래 실행 중인지 표시합니다. /proc/uptime에 접근해야 합니다.
lsof: 열린 파일 목록을 나열합니다. /proc 파일 시스템에 있는 정보를 사용합니다.
요약
기본적으로 사용할 수 있는 명령어는 주로 파일 시스템 내에서 작업을 수행하는 명령어들로, 가상 파일 시스템의 마운트가 필요하지 않습니다.
마운트해야만 사용할 수 있는 명령어는 시스템 상태, 프로세스 정보, 하드웨어 상태와 같은 정보를 필요로 하며, 이러한 정보는 /proc, /sys, /dev와 같은 가상 파일 시스템에 의존합니다. 따라서, 이러한 명령어를 chroot 환경에서 사용하려면 해당 가상 파일 시스템을 적절히 마운트해야 합니다.
남이 만든 이미지 chroot 해보기 : 컨테이너 이미지는 실행되는 프로세스의 동작에 필요한 모든 관련 파일을 묶어서 패키징
#
mkdir nginx-root
tree nginx-root
# nginx 컨테이너 압축 이미지를 받아서 압축 풀기
docker export $(docker create nginx) | tar -C nginx-root -xvf -;
docker images
#
tree -L 1 nginx-root
tree -L 2 nginx-root | more
#
chroot nginx-root /bin/sh
---------------------
ls /
#
nginx -g "daemon off;"
# 터미널1에서 아래 확인 후 종료
CTRL +C # nginx 실행 종료
exit
---------------------
# [터미널2]
## 루트 디렉터리 비교 및 확인
ls /
ps -ef |grep nginx
curl localhost:80
sudo ss -tnlp
탈옥코드 작성 및 탈출하기
vi escape_chroot.c
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/sh", "-i", NULL);
}
---
# 탈옥 코드를 컴파일하고 new-root 에 복사
# 컴파일
gcc -o myroot/escape_chroot escape_chroot.c
tree -L 1 myroot
file myroot/escape_chroot
# chroot 실행
chroot myroot /bin/sh
-----------------------
ls /
cd ../../
cd ../../
ls /
# 탈출!
./escape_chroot
ls /
# 종료
exit
exit
-----------------------
# [터미널2]
## 루트 디렉터리 비교 및 확인
ls /
결과
- 탈옥
3.2. 마운트 네임스페이스 + Pivot_root
chroot 차단을 위해서, pivot_root + mount ns(호스트 영향 격리) 를 사용 : 루트 파일 시스템을 변경(부착 mount) + 프로세스 환경 격리
마운트 네임스페이스 : 마운트 포인트를 격리(unshare)
마운트 네임스페이스
주요 명령어
pivot_root
# pivot_root [new-root] [old-root]
## 사용법은 심플합니다 ~ new-root와 old-root 경로를 주면 됩니다
mount
# mount -t [filesystem type] [device_name] [directory - mount point]
## root filesystem tree에 다른 파일시스템을 붙이는 명령
## -t : filesystem type ex) -t tmpfs (temporary filesystem : 임시로 메모리에 생성됨)
## -o : 옵션 ex) -o size=1m (용량 지정 등 …)
## 참고) * /proc/filesystems 에서 지원하는 filesystem type 조회 가능
unshare
# unshare [options] [program] [arguments]]
## "새로운 네임스페이스를 만들고 나서 프로그램을 실행" 하는 명령어입니다
# [터미널1]
unshare --mount /bin/sh
-----------------------
# 아래 터미널2 호스트 df -h 비교 : mount unshare 시 부모 프로세스의 마운트 정보를 복사해서 자식 네임스페이스를 생성하여 처음은 동일
df -h
-----------------------
# [터미널2]
df -h
# [터미널1]
-----------------------
#
mkdir new_root
mount -t tmpfs none new_root
ls -l
tree new_root
## 마운트 정보 비교 : 마운트 네임스페이스를 unshare
df -h
mount | grep new_root
findmnt -A
## 파일 복사 후 터미널2 호스트와 비교
cp -r myroot/* new_root/
tree new_root/
-----------------------
# [터미널2]
cd /tmp
ls -l
tree new_root
df -h
mount | grep new_root
findmnt -A
## 안보이는 이유 : 마운트 네임스페이스를 unshare 된 상태
tree new_root/
- pivot_root
실습 스크립트
# 터미널1
-----------------------
mkdir new_root/put_old
## pivot_root 실행
cd new_root # pivot_root 는 실행 시, 변경될 root 파일시스템 경로로 진입
pivot_root . put_old # [신규 루트] [기존 루트]
##
cd /
ls / # 터미널2와 비교
ls put_old
-----------------------
# 터미널2
ls /
탈옥 시도
# 터미널1
-----------------------
./escape_chroot
cd ../../../
ls /
exit
exit
-----------------------
3.3. 격리 Namespece – Docs , Youtube
- 네임스페이스와 관련된 프로세스의 특징
- 모든 프로세스들은 네임스페이스 타입별로 특정 네임스페이스에 속합니다
- Child 는 Parent 의 네임스페이스를 상속받습니다
- 프로세스는 네임스페이스 타입별로 일부는 호스트(root) 네임스페이스를 사용하고 일부는 컨테이너의 네임스페이스를 사용할 수 있습니다.
- mount 네임스페이스는 컨테이너의 것으로 격리하고, network 네임스페이스는 호스트 것을 사용
- Mount(파일시스템), Network(네트워크), PID(프로세스 id), User(계정), ipc(프로세스간 통신), Uts(Unix time sharing, 호스트네임), cgroup
환경 설정
# [터미널 1,2] 관리자
sudo su -
cd /tmp
# 네임스페이스 확인 방법 1 : 프로세스 별 네임스페이스 확인
ls -al /proc/$$/ns
lrwxrwxrwx 1 root root 0 Aug 25 13:45 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 net -> 'net:[4026531840]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Aug 25 13:45 uts -> 'uts:[4026531838]'
## 특정 네임스페이스의 inode 값만 확인
readlink /proc/$$/ns/mnt
readlink /proc/$$/ns/net
# 네임스페이스 확인 방법 2 : lsns - List system namespaces
lsns -h
lsns -p 1
lsns -p $$
## -t 네임스페이스 타입 , -p 조회할 PID
## NPROCS : 해당 네임스페이스에 속해있는 프로세스 갯수
## PID : 해당 네임스페이스의 (최초) 주인 프로세스
lsns -t mnt -p 1
lsns -t mnt -p $$
결과
1. 마운트 네임스페이스 MOUNT (mnt) Namespace
2002년, 마운트 포인트 격리, 최초의 네임스페이스
- echo $$ – 링크
# PID 1과 현재 Shell 속한 프로세스의 MNT NS 정보 확인
lsns -t mnt -p 1
lsns -t mnt -p $$
# [터미널1] /tmp 디렉터리
# unshare -m [명령어] : -m 옵션을 주면 [명령어]를 mount namespace 를 isolation 하여 실행합니다
unshare -m # *[명령어]를 지정하지 않으면 환경변수 $SHELL 실행
-----------------------------------
# NPROCS 값과 PID 값의 의미 확인
lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 112 1 root /sbin/init
4026531835 cgroup 112 1 root /sbin/init
4026531836 pid 112 1 root /sbin/init
4026531837 user 112 1 root /sbin/init
4026531838 uts 108 1 root /sbin/init
4026531839 ipc 112 1 root /sbin/init
4026531840 net 112 1 root /sbin/init
4026532206 mnt 2 5834 root -bash
# PID 1과 비교
lsns -p 1
# 빠져나오기
exit
-----------------------------------
결과
- NPROCS: 해당 네임스페이스에 속한 프로세스의 수 / PID: 프로세스 ID
2. UTS 네임스페이스 Namespace
2006년, Unix Time Sharing (여러 사용자 작업 환경 제공하고자 서버 시분할 나눠쓰기), 호스트명, 도메인명 격리
# unshare -u [명령어]
# -u 옵션을 주면 [명령어]를 UTS namespace 를 isolation 하여 실행
# [터미널1] /tmp 디렉터리
unshare -u
-----------------------------------
lsns -p $$
lsns -p 1
## 기본은 부모 네임스페스의 호스트 네임을 상속
hostname
## 호스트 네임 변경
hostname KANS
## 아래 터미널2에서 hostname 비교
hostname
exit
-----------------------------------
# [터미널2] /tmp 디렉터리
hostname
결과
- hostname 다르게 표시 확인
3. IPC 네임스페이스
2006년, Inter-Process Communication 격리, 프로세스 간 통신 자원 분리 관리 – Shared Memory, Pipe, Message Queue 등
# [터미널1] /tmp 디렉터리
unshare -i
-----------------------------------
lsns -p $$
lsns -p 1
exit
-----------------------------------
# [터미널1]
ipcs -m
docker run --rm --name test1 --ipc=shareable -it ubuntu bash
---------------------
ipcs -m
ipcmk -M 2000
Shared memory id: 0
ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x6427dde0 0 root 644 2000 0
lsns -p $$
# 아래 실습 확인 후 종료
exit
---------------------
# [터미널2]
# 호스트에서 확인
ipcs -m
# 컨테이너 생성 시 IPC 공유 : 해당 컨테이너는 test1 컨테이너와 IPC 네임스페이스를 공유
docker run --rm --name test2 --ipc=container:test1 -it ubuntu bash
---------------------
ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x6427dde0 0 root 644 2000 0
lsns -p $$
# 실습 확인 후 종료
exit
---------------------
결과
- ipc namespace의 NPROCS/ PID 차이
- 동일한 0x30f7ac4e 공유메모리 키, lsns 결과 확인
4. PID 네임스페이스
2008년, Process ID 격리
- 부모-자식 네임스페이스 중첩 구조, 부모 네임스페이스 에서는 → 자식 네임스페이스를 볼 수 있음
- 자식 네임스페이스는 parent tree 의 id 와 subtree 의 id 두 개를 가짐
ps -ef | head -n 3
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Aug31 ? 00:00:05 /sbin/init
root 2 0 0 Aug31 ? 00:00:00 [kthreadd]
- unshare 할 때 fork 하여, 자식 PID 네임스페이스의 pid 1로 실행
- pid 1 (init) 이 종료되면 pid namespace 도 종료
- PID Namespace 생성
# unshare -p [명령어]
## -p 옵션을 주면 [명령어]를 PID namespace 를 isolation 하여 실행합니다
## -f(fork) : PID namespace 는 child 를 fork 하여 새로운 네임스페이스로 격리함
## --mount-proc : namespace 안에서 ps 명령어를 사용하려면 /proc 를 mount 하기위함
# [터미널1] /proc 파일시스템 마운트
echo $$
unshare -fp --mount-proc /bin/sh
--------------------------------
# 터미널2 호스트와 비교
echo $$
ps -ef
ps aux
# 내부에서 PID NS 확인 : 아래 터미널2에서 lsns -t pid -p <위 출력된 PID>와 비교
lsns -t pid -p 1
--------------------------------
# [터미널2]
ps -ef
ps aux
ps aux | grep '/bin/sh'
root 6186 0.0 0.0 6192 1792 pts/2 S 15:08 0:00 unshare -fp --mount-proc /bin/sh
root 6187 0.0 0.0 2892 1664 pts/2 S+ 15:08 0:00 /bin/sh
# 터미널1 PID NS와 비교
lsns -t pid -p <위 출력된 PID>
lsns -t pid -p 6187
- 호스트에서 컨테이너 프로세스 종료 해보기
# [터미널1]
--------------------------------
# fork
sleep 10000
# 아래 종료로 자동으로 sleep 가 exit 됨
echo $$
# 아래 종료로 자동으로 exit됨 : 컨테이너의 PID 1 프로세스 종료 시
--------------------------------
echo $$
# [터미널2]
ps aux | grep sleep
## 호스트에서 sleep 종료 시켜보기 : 어떻게 되는가?
kill -l
kill -SIGKILL $(pgrep sleep)
## 호스트에서 /bin/sh 종료 시켜보기 : 어떻게 되는가?
ps aux | grep '/bin/sh'
kill -SIGKILL <위 출력된 PID>
kill -9 6187
5. 네트워크 네임스페이스
2009년, 이후 실습에서 상세설명
6. USER 네임스페이스
2012년, UID/GID 넘버스페이스 격리(Remap_, 컨테이너의 루트권한 문제를 해결함, 부모-자식 네임스페이스의 중첩 구조
- 네임스페이스 안과 밖의 UID/GID 를 다르게 설정할 수 있음
- 사전 준비 : 터미널1(ubuntu 일반 유저, docker 실행 가능 상태) , 터미널2(ubuntu 일반 유저)
# 터미널1,2
exit
whoami
# 터미널1
docker run -it ubuntu /bin/sh
-----------------------------
# 아래 터미널2와 비교
whoami
id
# 아래 터미널2와 비교
ps -ef
# User 네임스페이스는 도커 컨테이너 실행 시, 호스트 User 를 그대로 사용
readlink /proc/$$/ns/user
lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 2 1 root /bin/sh
4026531837 user 2 1 root /bin/sh
4026532208 mnt 2 1 root /bin/sh
4026532209 uts 2 1 root /bin/sh
4026532210 ipc 2 1 root /bin/sh
4026532211 pid 2 1 root /bin/sh
4026532212 net 2 1 root /bin/sh
4026532273 cgroup 2 1 root /bin/sh
# 아래 동작 확인 후 종료
exit
-----------------------------
# 터미널2
whoami
id
## root 로 실행됨
ps -ef |grep "/bin/sh"
ubuntu 6733 5348 0 15:34 pts/0 00:00:00 docker run -it ubuntu /bin/sh
root 6790 6768 0 15:34 pts/0 00:00:00 /bin/sh
##
readlink /proc/$$/ns/user
lsns -p $$
lsns -p $$ -t user
NS TYPE NPROCS PID USER COMMAND
4026531837 user 5 2391 ubuntu /lib/systemd/systemd --user
- 컨테이너를 탈취 후, 해당 프로세스를 기반으로 호스트에 Action 이 가능할 경우, root 계정 권한 실행이 가능 ⇒ 보안상 취약
# 터미널1
unshare -U --map-root-user /bin/sh
-----------------------------
# 내부에서는 여전히 root로 보임
whoami
id
# User 네임스페이스를 호스터(터미널2)와 비교
readlink /proc/$$/ns/user
lsns -p $$
# 아래 동작 확인 후 종료
exit
-----------------------------
# 터미널2
readlink /proc/$$/ns/user
lsns -p $$
## ubuntu 로 실행됨
ps -ef |grep "/bin/sh"
ubuntu 6874 5348 0 15:42 pts/0 00:00:00 /bin/sh
3.4. 자원 관리, cgroups – Docs , Youtube
- control groups : 프로세스들의 자원의 사용(CPU, 메모리, 디스크 입출력, 네트워크 등)을 제한, 격리시키는 리눅스 커널 기능 – Link
- 하나 또는 복수의 장치를 묶어서 하나의 그룹을 만들 수 있으며 개별 그룹은 시스템에서 설정한 값만큼 하드웨어를 사용
- 시스템의 프로세스들은 장치별로 특정한 cgroup에 속하여 프로세스가 사용하는 하드웨어 자원의 총량에 제한을 받음
- process를 계층적인 group으로 구성해서, resource 사용을 제한하고 모니터링할 수 있는 linux kernel feature
- cgroup의 interface는 cgroupfs이라 불리는 pseudo-filesystem을 통해 제공됨
- cgroupfs의 subdirectory를 생성/삭제/변경하면서 정의됨
- /proc 와 /sys
- Runtime의 메모리 정보를 파일시스템 폴더에 mount한 pseudo-filesystem
- 커널 2.x 버전까지는 /proc 폴더 하나만 있었지만, 3.x 버전 부터 /sys를 추가로 구분하였음
- /proc, /sys 는 커널이 관리하는 메모리를 마운트했기 때문에 해당 디렉토리에 접근하는 순간 메모리에서 정보를 읽어 오게 됨
실습 스크립트
#
mount -t cgroup
mount -t cgroup2
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
#
findmnt -t cgroup2
# TARGET SOURCE FSTYPE OPTIONS
/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot
# cgroup2 이외에 proc, bpf 도 있음
findmnt -A
TARGET SOURCE FSTYPE OPTIONS
/ /dev/nvme0n1p1 ext4 rw,relatime,discard,errors=remount-ro
...
├─/proc proc proc rw,nosuid,nodev,noexec,relatime
...
├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/security securityfs securityfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot
│ ├─/sys/fs/pstore pstore pstore rw,nosuid,nodev,noexec,relatime
│ ├─/sys/firmware/efi/efivars efivarfs efivarfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/bpf bpf bpf rw,nosuid,nodev,noexec,relatime,mode=700
...
# cgroupv1 만 지원 시, cgroup2 출력되지 않음
grep cgroup /proc/filesystems
nodev cgroup
nodev cgroup2
stat -fc %T /sys/fs/cgroup/
cgroup2fs
# 터미널2
sleep 100000
# /proc에 cgroup 정보 확인
cat /proc/cgroups
cat /proc/$(pgrep sleep)/cgroup
0::/user.slice/user-1000.slice/session-7.scope
tree /proc/$(pgrep sleep) -L 2
...
├── ns
│ ├── cgroup -> cgroup:[4026531835]
│ ├── ipc -> ipc:[4026531839]
│ ├── mnt -> mnt:[4026531841]
│ ├── net -> net:[4026531840]
...
# cgroup 목록 확인
ls /sys/fs/cgroup
cat /sys/fs/cgroup/cgroup.controllers
#
tree /sys/fs/cgroup/ -L 1
tree /sys/fs/cgroup/ -L 2
tree /sys/fs/cgroup/user.slice -L 1
tree /sys/fs/cgroup/user.slice/user-1000.slice -L 1
tree /sys/fs/cgroup/user.slice/user-1000.slice -L 2
# stress test
# 터미널1,2 관리자로 실습 진행
sudo su -
whoami
# 툴 설치
apt install cgroup-tools stress -y
# 터미널2 : 아래 stress 실행 후 CPU 사용률 확인
htop
# 터미널1에서 실습 진행
# 먼저 부하 발생 확인
stress -c 1
# 디렉터리 이동
cd /sys/fs/cgroup
# 서브 디렉터리 생성 후 확인 확인
mkdir test_cgroup_parent && cd test_cgroup_parent
tree
# 제어 가능 항목 확인
cat cgroup.controllers
# cpu를 subtree이 추가하여 컨트롤 할 수 있도록 설정 : +/-(추가/삭제)
cat cgroup.subtree_control
echo "+cpu" >> /sys/fs/cgroup/test_cgroup_parent/cgroup.subtree_control
# cpu.max 제한 설정 : 첫 번쨰 값은 허용된 시간(마이크로초) 두 번째 값은 총 기간 길이 > 1/10 실행 설정
echo 100000 1000000 > /sys/fs/cgroup/test_cgroup_parent/cpu.max
# test용 자식 디렉토리를 생성하고, pid를 추가하여 제한을 걸어줌
mkdir test_cgroup_child && cd test_cgroup_child
echo $$ > /sys/fs/cgroup/test_cgroup_parent/test_cgroup_child/cgroup.procs
cat /sys/fs/cgroup/test_cgroup_parent/test_cgroup_child/cgroup.procs
cat /proc/$$/cgroup
# 부하 발생 확인 : 터미널2에 htop 확인
stress -c 1
# 값 수정
echo 1000000 1000000 > /sys/fs/cgroup/test_cgroup_parent/cpu.max
# 부하 발생 확인 : 터미널2에 htop 확인
stress -c 1
# cgroup 삭제
exit
sudo su -
rmdir /sys/fs/cgroup/test_cgroup_parent/test_cgroup_child
rmdir /sys/fs/cgroup/test_cgroup_parent
# 아래는 cgroup v1 경우
---------------------------
## 1. 제어그룹 생성 : mycgroup
### -a : owner 설정 (control group's file)
### -g : cgroup 설정 <controllers>:<path>
### -g cpu:mycgroup ~ cpu controller 를 사용하고 path 는 mycgroup
cgcreate -a root -g cpu:mycgroup
tree /sys/fs/cgroup/cpu/mycgroup
## 2. 제어그룹 리소스 설정 : CPU 사용률 설정
### cpu 사용률(%CPU)
### cpu.cfs_quota_us / cat cpu.cfs_period_us * 100
### 참고 1000us = 1ms
### cpu사용률(30%)을 설정 (30,000/100,000)x100=30%
cgset -r cpu.cfs_quota_us=30000 mycgroup
cat /sys/fs/cgroup/cpu/mycgroup/cpu.cfs_quota_us
## 3. 제어그룹 프로세스 할당 : stress 실행
cgexec -g cpu:mycgroup stress -c 1
결과
- cgroup 목록 확인
- stress test
- 1/10 설정일시 10% 내외로 사용됨 확인
-
- 1/1 설정일시 100% 사용 확인
-
4. 컨테이너 네트워크 & IPTables
요약
- 컨테이너는 네트워크 네임스페이스로 호스트와 네트워크 격리 환경을 구성됩니다.
- 리눅스에서 방화벽 기능을 제공하는 IPtables 는 호스트와 컨테이너의 통신에 관여를 합니다.
- [Sam.0] Netfilter and iptables – Link , A Deep Dive into Iptables and Netfilter Architecture – Link
4.1. 도커 없이 네트워크 네임스페이스 환경에서 통신 구성
4.1.1. Red <> Blue 네임스페이스 간 통신
결론 : 네트워크 네임스페이스 생성 시 호스트 네트워크와 구별된다
실습 스크립트
# 터미널1~3 관리자
sudo su -
whoami
# veth (가상 이더넷 디바이스) 생성, man ip-link
ip link add veth0 type veth peer name veth1
# veth 생성 확인(상태 DOWN), ifconfig 에는 peer 정보 확인 안됨
# very pair 정보 확인 : ({iface}@if{pair#N})
ip -c link
ip -c addr # 축약 ip -c a
ifconfig -a
# 네트워크 네임스페이스 생성 , man ip-netns
ip netns add RED
ip netns add BLUE
# 네트워크 네임스페이스 확인
ip netns list
# BLUE
# RED
# veth0 을 RED 네트워크 네임스페이스로 옮김
ip link set veth0 netns RED
ip netns list
# BLUE
# RED (id: 0)
## 호스트의 ip a 목록에서 보이지 않음, veth1의 peer 정보가 변경됨
ip -c link
# 12: veth1@if13: ~
## 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
# 터미널1 (RED 11.11.11.2)
## nsenter : 네임스페이스에 attach 하여 지정한 프로그램을 실행
tree /var/run/netns
nsenter --net=/var/run/netns/RED
ip -c a
## neighbour/arp tables management , man ip-neighbour
ip -c neigh
## 라우팅 정보, iptables 정보
ip -c route
iptables -t filter -S
iptables -t nat -S
# 터미널2 (호스트)
lsns -t net # nsenter 실행 후 TYPE(net) CMD(-bash) 생성 확인
ip -c a
ip -c neigh
ip -c route
iptables -t filter -S
iptables -t nat -S
# 터미널3 (BLUE 11.11.11.3)
nsenter --net=/var/run/netns/BLUE
ip -c a
ip -c neigh
ip -c route
iptables -t filter -S
iptables -t nat -S
# ping 통신 확인
# 터미널3 (BLUE 11.11.11.3)
tcpdump -i veth1
ip -c neigh
exit
# 터미널1 (RED 11.11.11.2)
ping 11.11.11.3 -c 1
ip -c neigh
exit
# 삭제
ip netns delete RED
ip netns delete BLUE
결과
- veth0, veth1 peer 최초 생성
- veth0, veth1을 각각 RED, BLUE 네트워크 네임스페이스 이동
- ip 설정 후 ping 및 tcpdump로 확인
4.1.2. RED ← Bridge(br0) → BLUE 간 통신
결론 : arp table, route table, iptables 와 호스트의 bridge fdb 를 통하여 통신
기본 환경 설정 및 확인
# 네트워크 네임스페이스 및 veth 생성
ip netns add RED
ip link add reth0 type veth peer name reth1
ip link set reth0 netns RED
ip netns add BLUE
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
# 브리지 정보 확인
brctl show
# br0 브리지 생성
ip link add br0 type bridge
# br0 브리지 정보 확인
brctl show br0
brctl showmacs br0
brctl showstp br0
# 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
# 터미널1 (RED 11.11.11.2)
nsenter --net=/var/run/netns/RED
ip -c a;echo; ip -c route;echo; ip -c neigh
## 현재 네트워크 네임스페이스 정보 확인
ip netns identify $$
RED
# 터미널2 (호스트)
brctl showmacs br0
bridge fdb show
bridge fdb show dev br0
iptables -t filter -S
iptables -t filter -L -n -v
# 터미널3 (BLUE 11.11.11.3)
nsenter --net=/var/run/netns/BLUE
ip -c a;echo; ip -c route;echo; ip -c neigh
## 현재 네트워크 네임스페이스 정보 확인
ip netns identify $$
BLUE
==============================================================
# 터미널2 (호스트)
# ping 통신 전 사전 설정
## iptables 정보 확인
iptables -t filter -S | grep '\-P'
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
iptables -nvL -t filter
## Ubuntu 호스트에서 패킷 라우팅 설정 확인 : 커널의 IP Forwarding (routing) 기능 확인 - 0(off), 1(on)
## echo 1 > /proc/sys/net/ipv4/ip_forward
cat /proc/sys/net/ipv4/ip_forward
1
==============================================================
# ping 통신 테스트
# 터미널1 (RED 11.11.11.2) >> ping 왜 실패했을까요?
ping -c 1 11.11.11.3
# 터미널2 (호스트)
tcpdump -l -i br0
watch -d 'iptables -v --numeric --table filter --list FORWARD'
watch -d 'iptables -v --numeric --table filter --list FORWARD;echo;iptables -v --numeric --table filter --list DOCKER-USER;echo;iptables -v --numeric --table filter --list DOCKER-ISOLATION-STAGE-1'
# 터미널3 (BLUE 11.11.11.3)
tcpdump -l -i beth0
결과
- 브릿지, 네트워크네임스페이스, veth 생성 및 UP
- PING 실패
RED > BLUE ping 허용 설정
ㅂ,/proc/sys/net/ipv4/ip_forward ㅅ
브릿지의 /proc/sys/net/ipv4/ip_forward
설정은 켜져있지만(1) 추가로 iptables단에서의 설정이 필요하다.
호스트 입장에서는 “외부(RED, src) → 외부(BLUE, dst)” 패킷이므로 FORWARD 체인의 filter 테이블 룰을 봐야합니다 → 허용 정책 추가하자 – 링크
# 터미널2 (호스트)
# iptables 설정 정보 확인
iptables -t filter -S
iptables -t nat -S | grep '\-P'
# 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'
watch -d 'iptables -v --numeric --table filter --list FORWARD;echo;iptables -v --numeric --table filter --list DOCKER-USER;echo;iptables -v --numeric --table filter --list DOCKER-ISOLATION-STAGE-1'
tcpdump -l -i br0
==============================================================
# ping 통신 테스트
# 터미널1 (RED 11.11.11.2)
ping -c 1 11.11.11.3
ip -c neigh
# 터미널2 (호스트)
watch -d 'iptables -v --numeric --table filter --list FORWARD;echo;iptables -v --numeric --table filter --list DOCKER-USER;echo;iptables -v --numeric --table filter --list DOCKER-ISOLATION-STAGE-1'
tcpdump -l -i br0
## 정보 확인
ip -c neigh
# 터미널3 (BLUE 11.11.11.3)
tcpdump -l -i beth0
ip -c neigh
==============================================================
# (옵션) 추가 방안1 : 출발지 IP 11.2, 11.3 허용
## -t(table), -A(APPEND chain rule), -s(출발지), -j(jump to - ACCEPT 허용)
iptables -t filter -A FORWARD -s 11.11.11.2/32 -j ACCEPT
iptables -t filter -A FORWARD -s 11.11.11.3/32 -j ACCEPT
## 추가 정책 삭제 시
iptables -t filter -D FORWARD -s 11.11.11.2/32 -j ACCEPT
iptables -t filter -D FORWARD -s 11.11.11.3/32 -j ACCEPT
# (옵션) 추가 방안2 : 11.11.11.0/24 대역 출발지 허용
iptables -t filter -A FORWARD -s 11.11.11.0/24 -j ACCEPT
## 추가 정책 삭제 시
iptables -t filter -D FORWARD -s 11.11.11.0/24 -j ACCEPT
# (옵션) 추가 방안3 : FORWARD 기본 정책 허용으로 변경
iptables -t filter -P FORWARD ACCEPT
## 추가 정책 삭제 시
iptables -t filter -P FORWARD DROP
결과
- filter 테이블에 ACCEPT 정책의 체인을 넣어서 허용
4.1.3. RED/BLUE → 호스트 & 외부(인터넷) 통신
결론 : 호스트에 RED/BLUE와 통신 가능한 IP 설정 및 라우팅 추가, iptables NAT 를 통하여 통신
호스트에서 RED 나 BLUE 로 ping 통신 → RED 에서 외부로 통신
# 터미널1 (RED 11.11.11.2)
nsenter --net=/var/run/netns/RED
tcpdump -i any
# 터미널3 (호스트)
exit
tcpdump -i br0 -n
# 터미널2 (호스트) >> 호스트에서 RED 로 통신이 안되는 이유가 무엇일까요?
ping -c 1 11.11.11.2
ip -c route
ip -c addr
==============================================================
# 터미널2 (호스트) >> br0 에 IP 추가(라우팅 정보)
ip addr add 11.11.11.1/24 dev br0
ping -c 1 11.11.11.2
ping -c 1 11.11.11.3
# 터미널1 (RED 11.11.11.2) >> 192.168.50.10 와 통신이 안되는 이유는 무엇일까요?
ping -c 1 11.11.11.1
ping -c 1 192.168.50.10
ip -c route
ip -c addr
==============================================================
# 터미널3 (호스트)
tcpdump -i any icmp -n
# 터미널1 (RED 11.11.11.2)
ip route add default via 11.11.11.1
ping -c 1 192.168.50.10
ip -c route
ping -c 1 8.8.8.8 >> 외부 대역(인터넷)과 통신이 안되는 이유가 무엇일까요?
# 터미널2 (호스트)
iptables -S -t nat
iptables -nvL -t nat
## POSTROUTING : 라우팅 Outbound or 포워딩 트래픽에 의해 트리거되는 netfilter hook
## POSTROUTING 에서는 SNAT(Source NAT) 설정
iptables -t nat -A POSTROUTING -s 11.11.11.0/24 -j MASQUERADE
watch -d 'iptables -v --numeric --table nat --list POSTROUTING'
iptables -nvL -t nat
conntrack -L --src-nat
# 터미널1 (RED 11.11.11.2)
ping -c 1 8.8.8.8
exit
# 터미널3 (BLUE 11.11.11.3)
nsenter --net=/var/run/netns/BLUE # 주의, 꼭 실행 후 아래 진행 할 것
ip route add default via 11.11.11.1
ping -c 1 8.8.8.8
exit
# 삭제
ip netns delete RED
ip netns delete BLUE
ip link delete br0
iptables -t filter -D DOCKER-USER -j ACCEPT
iptables -t nat -D POSTROUTING -s 11.11.11.0/24 -j MASQUERADE
결과
- 별도 설정 X시 통신 실패
- 호스트입장에서 RED대역(및 브릿지)에 대한 정보가 없음
- br0에 대한 라우팅 추가 후 정상 작동
- RED에서 호스트(192.168.50.10)로의 Network is unreachable
- RED입장에서 호스트(및 브릿지)에 대한 정보가 없기 때문
- 추가 후 정상 작동 및 퍼블릭망 (8.8.8.8) 로의 실패
- 호스트를 거쳐 외부로 나가기 위해서는, 호스트에서의 NAT 및 마스커레이드 설정 필요
4.2. 도커 네트워크 모델
4.2.1. 도커 네트워크 모드
기본 네트워크 모드 : Bridge, Host, None + 추가 네트워크 플러그인 : macvlan, ipvlan, overlay
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# 8095975b5a73 bridge bridge local
# b4732f503666 host host local
# bca1c064b34c none null local
docker info | grep Network
# Network: bridge host ipvlan macvlan null overlay
- Host 는 호스트의 환경을 그대로 사용. 애플리케이션 별도 포트 포워딩 없이 바로 서비스 가능. 컨테이너의 호스트 이름도 호스트 머신의 이름과 동일.
- None 는 말 그래도 아무런 네트워크를 쓰지 않는 것. 외부와의 연결이 단절됨. 컨테이너 내부에 lo 인터페이스만 존재.
4.2.2. Bridge 모드
O’REILLY – Networking and Kubernetes 책
- 도커에서 기본적으로 쓸 수 있는 네트워크 확인, 컨테이너 기본 생성 시 자동으로 docker0 브리지를 사용
- 기본 172.17.0.0/16 대역을 컨테이너가 사용, 대역 변경 설정 가능
- 도커는 IPtables 의 PREROUTING POSTROUTING 의 NAT Chains 를 변경한다
- 컨테이너 → 외부 : POSTROUTING 의 SNAT 처리
- 외부 → 컨테이너(Exposed services ports) : PREROUTING 에서 DNAT 처리
실습 스크립트
# 도커 네트워크 모드 확인
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# 8095975b5a73 bridge bridge local
# b4732f503666 host host local
# bca1c064b34c none null local
# 도커 네트워크(플러그인) 정보 확인
docker info
docker info | grep Network
# 도커 bridge 상세 정보 확인 = docker inspect --type network bridge 동일
# 아래 "Gateway": "172.17.0.1" 정보가 출력되지 않을 경우에는 systemctl restart docker 입력 후 다시 확인
docker network inspect bridge | jq
# [
# {
# "Name": "bridge",
# "Id": "8095975b5a739c13f52c4174cfed366bb548cd6a871540641e742a324949a631",
# "Created": "2024-09-01T02:48:33.114287761+09:00",
# "Scope": "local",
# "Driver": "bridge",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "default",
# "Options": null,
# "Config": [
# {
# "Subnet": "172.17.0.0/16",
# "Gateway": "172.17.0.1"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {},
# "Options": {
# "com.docker.network.bridge.default_bridge": "true",
# "com.docker.network.bridge.enable_icc": "true",
# "com.docker.network.bridge.enable_ip_masquerade": "true",
# "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
# "com.docker.network.bridge.name": "docker0",
# "com.docker.network.driver.mtu": "1500"
# },
# "Labels": {}
# }
# ]
#
# 브릿지 확인
brctl show
# bridge name bridge id STP enabled interfaces
# docker0 8000.024231575f68 no
# 네트워크 인터페이스 확인
ip -c addr show docker0
# 3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
# link/ether 02:42:31:57:5f:68 brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
# valid_lft forever preferred_lft forever
# inet6 fe80::42:31ff:fe57:5f68/64 scope link
# valid_lft forever preferred_lft forever
# SNAT 정책 확인
iptables -t nat -S
iptables -t nat -S | grep MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
# 라우팅 확인
ip -c route | grep docker0
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
컨테이너(busybox) 2대 생성
# 터미널1 (PINK) : PINK 이름의 busybox 컨테이너 생성
docker run -it --name=PINK --rm busybox
ip a
ip neigh
# 터미널3 (ORANGE) : ORANGE 이름의 busybox 컨테이너 생성
docker run -it --name=ORANGE --rm busybox
ip a
ip neigh
# 터미널2 (호스트)
## 컨테이너 생성 확인
docker ps
## veth 에 각각 서로 연결되는 veth peer 가 추가됬음을 확인, docker UP 확인
ip -br -c link
## 브릿지 정보 확인
brctl show docker0
결과
- 최초실행
컨테이너 통신 확인
- 컨테이너 내부 통신
# 터미널2 (호스트)
tcpdump -i docker0 -n
iptables -t filter -S
-A FORWARD -i docker0 -o docker0 -j ACCEPT # 컨테이너 끼리는 FORWARD 가 ACCEPT(허용)
# 터미널1 (PINK)
## ORANGE 로 ping 테스트(IP는 다를 수 있습니다)
ping -c 1 172.17.0.3
ip neigh
route -n
# 터미널3 (ORNAGE)
## ORANGE 로 ping 테스트(IP는 다를 수 있습니다)
ping -c 1 172.17.0.2
ip neigh
route -n
- 컨테이너 외부 통신
# 터미널2 (호스트)
tcpdump -i any icmp
iptables -t nat -S
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
conntrack -L --src-nat
icmp 1 29 src=172.17.0.5 dst=8.8.8.8 type=8 code=0 id=7 src=8.8.8.8 dst=10.0.2.15 type=0 code=0 id=7 mark=0 use=1
conntrack v1.4.5 (conntrack-tools): 1 flow entries have been shown.
# 터미널1 (PINK)
ping 8.8.8.8
exit # 실습 완료 후
# 터미널3 (ORNAGE)
ping 8.8.8.8
exit # 실습 완료 후
결과
- 내부통신 확인
- 외부통신 확인 (ping 2번 실행 > conntrack 기록2번)
도커 호스트가 다수 일 때, 컨테이너들 끼리 직접 통신을 하기 위해서는 네트워크 대역이 중복되지 않게 설정해야 되고 overlay 혹은 직접 라우팅이 가능하게 설정 등이 필요합니다. 이런 부분을 쿠버네티스에서는 CNI(Network Interface) 플러그인 (예. calico 등)이 처리하게 됩니다.
도커 컨테이너의 NET 네임스페이스 정보 확인 – 링크
4.2.3. Host 모드
호스트의 환경을 그대로 사용 가능. 호스트 드라이버의 네트워크는 별도로 생성할 필요 없이 기존의 host 라는 이름의 네트워크를 사용 – 링크 링크2
컨테이너의 호스트 이름도 호스트 머신의 이름과 동일. 네트워크도 동일. 애플리케이션 별도 포트 포워딩 없이 바로 서비스 할 수 있음.
실습 스크립트
# 컨테이너 실행
docker run --rm -d --network host --name my_nginx nginx
# HostConfig.NetworkMode "host" , Config.ExposedPorts "80/tcp" , NetworkSettings.Networks "host" 확인
docker inspect my_nginx
# curl 접속 확인
curl -s localhost | grep -o '<title>.*</title>'
# 호스트에서 tcp 80 listen 확인
ss -tnlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=3694,fd=7),("nginx",pid=3693,fd=7),("nginx",pid=3649,fd=7))
# 추가 실행 시도
docker run -d --network host --name my_nginx_2 nginx
# 확인
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
369c40cf6dfd nginx "/docker-entrypoint.…" 7 seconds ago Exited (1) 3 seconds ago my_nginx_2
...
# 삭제
docker container stop my_nginx
결과
- bind() to [::]:80 failed (98: Address already in use) 로 인한 중복실행불가
4.2.4. 컨테이너 외부에 노출
설정 및 확인 : bridge mode – 링크
실습 스크립트
# nginx:alpine 웹 컨테이너 3대 실행
# -p 옵션은 컨테이너 포트를 호스트 포트와 바인딩 연결 [호스트의 포트]:[컨테이너의 포트]
## -p 80 만 사용 시 호스트 포트 중 하나(랜덤)과 컨테이너의 80포트와 연결
## -p 여러개 사용하여 여러개 포트 개방
docker run -d --name=web1 -p 10001:80 --rm nginx:alpine
docker run -d --name=web2 -p 10002:80 --rm nginx:alpine
docker run -d --name=web3 -p 10003:80 --rm nginx:alpine
# 컨테이너 확인
docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# d74323a5594e nginx:alpine "/docker-entrypoint.…" 6 seconds ago Up 4 seconds 0.0.0.0:10003->80/tcp, [::]:10003->80/tcp web3
# 8e213c23a7ef nginx:alpine "/docker-entrypoint.…" 6 seconds ago Up 5 seconds 0.0.0.0:10002->80/tcp, [::]:10002->80/tcp web2
# 7b214b668ced nginx:alpine "/docker-entrypoint.…" 6 seconds ago Up 5 seconds 0.0.0.0:10001->80/tcp, [::]:10001->80/tcp web1
# iptables 정보 확인
iptables -t nat -S | grep :80
# -A DOCKER ! -i docker0 -p tcp -m tcp --dport 10001 -j DNAT --to-destination 172.17.0.2:80
# -A DOCKER ! -i docker0 -p tcp -m tcp --dport 10002 -j DNAT --to-destination 172.17.0.3:80
# -A DOCKER ! -i docker0 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 172.17.0.4:80
iptables -t filter -S
-A FORWARD -o docker0 -j DOCKER
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
...
# 외부(자신의 PC)에서 접속(curl 혹은 웹브라우저)
curl -s 192.168.50.10:10001 | grep -o '<title>.*</title>'
curl -s 192.168.50.10:10002 | grep -o '<title>.*</title>'
curl -s 192.168.50.10:10003 | grep -o '<title>.*</title>'
# 연결 정보 확인
conntrack -L
iptables -t nat -L -n -v
# 삭제
docker stop $(docker ps -a -q)