Docker용 AppArmor 보안 프로필
Docker용 AppArmor 보안 프로필
AppArmor는 리눅스 커널 보안 모듈로, 운영체제와 애플리케이션을 보안 위협으로부터 보호하는 역할을 합니다. 시스템 관리자는 각 프로그램에 AppArmor 보안 프로필을 연결해서 사용하는데, Docker도 마찬가지로 AppArmor 정책이 로드되고 적용되기를 기대합니다. Docker는 컨테이너를 위한 기본 프로필인 docker-default를 자동으로 생성하고 로드하는데, 이 프로필은 tmpfs에 생성된 후 커널에 로드됩니다.
참고: 이 프로필은 Docker 데몬이 아니라 컨테이너에 적용됩니다. Docker Engine 데몬용 프로필도 존재하지만 현재
deb패키지에는 포함되어 설치되지 않습니다. 데몬 프로필의 소스 코드가 궁금하다면 Docker Engine 소스 저장소의 contrib/apparmor에서 확인할 수 있습니다.
출처: 공식문서
정책 이해하기
docker-default 프로필은 컨테이너 실행 시 기본으로 사용되는 프로필입니다. 보호 수준은 중간 정도이면서도 다양한 애플리케이션과 호환되도록 설계되었습니다. 이 프로필은 다음 템플릿에서 생성됩니다.
컨테이너를 실행할 때 security-opt 옵션으로 재정의하지 않으면 기본적으로 docker-default 정책이 사용됩니다. 예를 들어, 다음 명령은 기본 정책을 명시적으로 지정하는 경우입니다.
$ docker run --rm -it --security-opt apparmor=docker-default hello-world
기본 프로필 커스터마이징
Docker Engine 29.8부터는 데몬을 설정해서 docker-default 프로필을 사용자 정의 템플릿으로 생성할 수 있습니다. 이렇게 하면 docker-default를 사용하는 컨테이너의 정책이 변경됩니다. 명시적인 AppArmor 프로필이 없는 특권(privileged) 컨테이너는 unconfined로 실행되고, 다른 프로필로 설정된 컨테이너는 기존 설정을 계속 사용합니다.
템플릿은 Go text/template 문법을 사용합니다. Docker는 데몬이 시작될 때 템플릿을 렌더링하고 로드합니다. 내장된 baseTemplate을 시작점으로 사용하는 것이 좋습니다. 지원되는 호스트 전반에서 호환성에 필요한 규칙과 템플릿 치환이 포함되어 있기 때문입니다.
주의: 사용자 정의 템플릿은 컨테이너 격리 수준을 낮출 수 있습니다. 내장 정책을 변경할 때는 반드시 검토하고 테스트하세요.
템플릿 만들기
baseTemplate 백틱 사이의 내용을 복사해서 /etc/docker/apparmor/docker-default로 저장합니다. 이 템플릿은 Docker가 렌더링하기 전까지는 완전한 AppArmor 프로필이 아닙니다. 파일을 AppArmor 프로필 디렉터리(/etc/apparmor.d) 밖에 저장해야 합니다. 호스트의 AppArmor 서비스가 Docker가 렌더링하기 전에 템플릿을 파싱하려고 시도할 수 있기 때문입니다.
필요에 따라 정책 규칙을 수정합니다. Docker는 다음 템플릿 데이터를 제공합니다.
.Abi: 호스트가 해당 AppArmor ABI를 제공하면abi/3.0, 그렇지 않으면 빈 문자열.Name: 프로필 이름,docker-default.DaemonProfile: 데몬의 AppArmor 프로필 또는unconfined.Imports: 전역 범위에 대한 호스트별 선언 및 include.InnerImports: 프로필 범위에 대한 호스트별 include
내장 템플릿의 .Abi, .Name, .Imports, .InnerImports 치환은 유지하세요. 데몬의 신호를 허용하는 규칙에는 .DaemonProfile을 사용하세요.
데몬 설정하기
데몬 구성 파일에서 apparmor-profile을 템플릿 경로로 설정합니다.
/etc/docker/daemon.json
{
"apparmor-profile": "/etc/apparmor.d/containers/my-custom-docker-profile.tpl"
}
또는 dockerd --apparmor-profile=/etc/apparmor.d/containers/my-custom-docker-profile.tpl 플래그로 경로를 전달할 수도 있습니다. 플래그와 daemon.json에 동시에 설정하지 마세요.
재시작 및 확인
Docker 데몬을 재시작합니다.
$ sudo systemctl restart docker
렌더링된 프로필을 로드하는 동안 오류가 있는지 데몬 로그를 확인합니다.
$ sudo journalctl -u docker.service --since "5 minutes ago" --no-pager
재시작 로그에 Failed to load default apparmor profile이 포함되어 있지 않은지 확인하세요. apparmor_parser가 렌더링된 프로필을 거부하면 데몬은 계속 실행되며 이전에 로드된 docker-default 프로필을 유지합니다.
데몬이 템플릿 경로로 설정되었는지 확인합니다.
$ docker info --format '{{json .SecurityOptions}}'
출력에 name=apparmor,profile=/etc/apparmor.d/containers/my-custom-docker-profile.tpl이 포함되어 있어야 합니다.
프로필 로드 및 언로드
컨테이너에서 사용할 새 프로필을 AppArmor에 로드하려면:
$ apparmor_parser -r -W /path/to/your_profile
그런 다음 --security-opt 옵션으로 사용자 정의 프로필을 실행합니다.
$ docker run --rm -it --security-opt apparmor=your_profile hello-world
AppArmor에서 프로필을 언로드하려면:
# unload the profile
$ apparmor_parser -R /path/to/profile
프로필 작성 참고 자료
AppArmor의 파일 글로빙(globbing) 문법은 다른 글로빙 구현과 조금 다릅니다. AppArmor 프로필 문법에 관해서는 아래 자료를 꼭 살펴보는 것이 좋습니다.
Nginx 예제 프로필
이 예제에서는 Nginx용 사용자 정의 AppArmor 프로필을 만들어 봅니다. 아래는 사용자 정의 프로필입니다.
#include <tunables/global>
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
network inet udp,
network inet icmp,
deny network raw,
deny network packet,
file,
umount,
deny /bin/** wl,
deny /boot/** wl,
deny /dev/** wl,
deny /etc/** wl,
deny /home/** wl,
deny /lib/** wl,
deny /lib64/** wl,
deny /media/** wl,
deny /mnt/** wl,
deny /opt/** wl,
deny /proc/** wl,
deny /root/** wl,
deny /sbin/** wl,
deny /srv/** wl,
deny /tmp/** wl,
deny /sys/** wl,
deny /usr/** wl,
audit /** w,
/var/run/nginx.pid w,
/usr/sbin/nginx ix,
deny /bin/dash mrwklx,
deny /bin/sh mrwklx,
deny /usr/bin/top mrwklx,
capability chown,
capability dac_override,
capability setuid,
capability setgid,
capability net_bind_service,
deny @{PROC}/* w, # deny write for all files directly in /proc (not in a subdir)
# deny write to files not in /proc/<number>/** or /proc/sys/**
deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w,
deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel)
deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/mem rwklx,
deny @{PROC}/kmem rwklx,
deny @{PROC}/kcore rwklx,
deny mount,
deny /sys/[^f]*/** wklx,
deny /sys/f[^s]*/** wklx,
deny /sys/fs/[^c]*/** wklx,
deny /sys/fs/c[^g]*/** wklx,
deny /sys/fs/cg[^r]*/** wklx,
deny /sys/firmware/** rwklx,
deny /sys/kernel/security/** rwklx,
}
- 사용자 정의 프로필을
/etc/apparmor.d/containers/docker-nginx파일로 저장합니다.
이 예제의 파일 경로는 필수 사항이 아닙니다. 실제 운영 환경에서는 다른 경로를 사용해도 됩니다.
- 프로필을 로드합니다.
$ sudo apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx
- 프로필로 컨테이너를 실행합니다.
nginx를 분리 모드로 실행하려면:
$ docker run --security-opt "apparmor=docker-nginx" \
-p 80:80 -d --name apparmor-nginx nginx
- 실행 중인 컨테이너에 exec로 접속합니다.
$ docker container exec -it apparmor-nginx bash
- 몇 가지 작업을 시도해서 프로필을 테스트합니다.
root@6da5a2a930b9:~# ping 8.8.8.8
ping: Lacking privilege for raw socket.
root@6da5a2a930b9:/# top
bash: /usr/bin/top: Permission denied
root@6da5a2a930b9:~# touch ~/thing
touch: cannot touch 'thing': Permission denied
root@6da5a2a930b9:/# sh
bash: /bin/sh: Permission denied
root@6da5a2a930b9:/# dash
bash: /bin/dash: Permission denied
이렇게 해서 사용자 정의 AppArmor 프로필로 보호되는 컨테이너를 배포했습니다.
AppArmor 디버깅
문제를 디버깅할 때는 dmesg를 사용하고, 로드된 프로필을 확인하려면 aa-status를 사용할 수 있습니다.
dmesg 사용하기
AppArmor 관련 문제를 디버깅할 때 도움이 되는 몇 가지 팁입니다.
AppArmor는 dmesg에 매우 상세한 메시지를 보냅니다. 일반적으로 AppArmor 로그 줄은 다음과 같은 형태입니다.
[ 5442.864673] audit: type=1400 audit(1453830992.845:37): apparmor="ALLOWED" operation="open" profile="/usr/bin/docker" name="/home/jessie/docker/man/man1/docker-attach.1" pid=10923 comm="docker" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0
위 예제에서 profile=/usr/bin/docker를 볼 수 있습니다. 이는 사용자가 docker-engine(Docker Engine 데몬) 프로필을 로드했다는 뜻입니다.
다른 로그 줄을 살펴보겠습니다.
[ 3256.689120] type=1400 audit(1405454041.341:73): apparmor="DENIED" operation="ptrace" profile="docker-default" pid=17651 comm="docker" requested_mask="receive" denied_mask="receive"
이번에는 프로필이 docker-default입니다. 이 프로필은 privileged 모드가 아닌 한 컨테이너에서 기본으로 실행됩니다. 이 줄은 AppArmor가 컨테이너 내부에서 ptrace를 거부했음을 보여줍니다. 이는 정확히 예상된 동작입니다.
aa-status 사용하기
로드된 프로필을 확인해야 한다면 aa-status를 사용할 수 있습니다. 출력은 다음과 같습니다.
$ sudo aa-status
apparmor module is loaded.
14 profiles are loaded.
1 profiles are in enforce mode.
docker-default
13 profiles are in complain mode.
/usr/bin/docker
/usr/bin/docker///bin/cat
/usr/bin/docker///bin/ps
/usr/bin/docker///sbin/apparmor_parser
/usr/bin/docker///sbin/auplink
/usr/bin/docker///sbin/blkid
/usr/bin/docker///sbin/iptables
/usr/bin/docker///sbin/mke2fs
/usr/bin/docker///sbin/modprobe
/usr/bin/docker///sbin/tune2fs
/usr/bin/docker///sbin/xtables-multi
/usr/bin/docker///sbin/zfs
/usr/bin/docker///usr/bin/xz
38 processes have profiles defined.
37 processes are in enforce mode.
docker-default (6044)
...
docker-default (31899)
1 processes are in complain mode.
/usr/bin/docker (29756)
0 processes are unconfined but have a profile defined.
위 출력은 다양한 컨테이너 PID에서 실행 중인 docker-default 프로필이 enforce 모드에 있음을 보여줍니다. 즉, AppArmor가 docker-default 프로필의 범위를 벗어나는 모든 활동을 적극적으로 차단하고 dmesg에 기록하고 있다는 뜻입니다.
또한 위 출력은 /usr/bin/docker(Docker Engine 데몬) 프로필이 complain 모드로 실행 중임을 보여줍니다. 이는 AppArmor가 프로필 범위를 벗어난 활동을 dmesg에 기록만 한다는 의미입니다. (Ubuntu Trusty의 경우 일부 흥미로운 동작이 강제되는 예외가 있습니다.)
Docker의 AppArmor 코드에 기여하기
고급 사용자와 패키지 관리자는 Docker Engine 소스 저장소의 contrib/apparmor 아래에서 /usr/bin/docker(Docker Engine 데몬)용 프로필을 찾을 수 있습니다.
컨테이너용 docker-default 프로필은 profiles/apparmor에 있습니다.