From 480fdf27a3e49915d8454efe4e524ad3d76ae2ec Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Mon, 2 May 2016 16:23:50 +0300 Subject: [PATCH 01/11] migrating shell scripts to bats testing framework --- helper_lib.sh | 3 + tests/1_host_configuration.bats | 111 ++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 tests/1_host_configuration.bats diff --git a/helper_lib.sh b/helper_lib.sh index 74c8849..2b6f1b0 100644 --- a/helper_lib.sh +++ b/helper_lib.sh @@ -1,5 +1,8 @@ #!/bin/sh +# echo to stderr +echoerr() { echo "$@" 1>&2; } + # Returns the absolute path of a given string abspath () { case "$1" in /*)printf "%s\n" "$1";; *)printf "%s\n" "$PWD/$1";; esac; } diff --git a/tests/1_host_configuration.bats b/tests/1_host_configuration.bats new file mode 100644 index 0000000..2944f5f --- /dev/null +++ b/tests/1_host_configuration.bats @@ -0,0 +1,111 @@ +#!/usr/bin/env bats + +setup() { + . "$BATS_TEST_DIRNAME/../helper_lib.sh" +} + +# 1.1 +@test "1.1 - Create a separate partition for containers" { + grep /var/lib/docker /etc/fstab + [ $status -eq 0 ] +} + +# 1.2 +@test "1.2 - Use an updated Linux Kernel" { + kernel_version=$(uname -r | cut -d "-" -f 1) + run do_version_check 3.10 "$kernel_version" + [ $status -eq 9 ] || [ $status -eq 10 ] +} + +# 1.4 +@test "1.4 - Remove all non-essential services from the host - Network" { + # Check for listening network services. + listening_services=$(netstat -na | grep -v tcp6 | grep -v unix | grep -c LISTEN) + if [ "$listening_services" -eq 0 ]; then + echoerr "1.4 - Failed to get listening services for check: $BATS_TEST_NAME" + else + if [ "$listening_services" -gt 5 ]; then + echoerr " * Host listening on: $listening_services ports" + fi + fi + [ "$listening_services" -ne 0 ] && [ "$listening_services" -le 5 ] +} + +# 1.5 +@test "1.5 - Keep Docker up to date" { + docker_version=$(docker version | grep -i -A1 '^server' | grep -i 'version:' \ + | awk '{print $NF; exit}' | tr -d '[:alpha:]-,') + docker_current_version="1.11.1" + docker_current_date="2016-04-27" + run do_version_check "$docker_current_version" "$docker_version" + if [ $status -eq 11 ]; then + echoerr " * Using $docker_version, when $docker_current_version is current as of $docker_current_date" + echoerr " * Your operating system vendor may provide support and security maintenance for docker" + else + pass "$check_1_5" + echoerr " * Using $docker_version which is current as of $docker_current_date" + echoerr " * Check with your operating system vendor for support and security maintenance for docker" + fi + [ $status -eq 9 ] || [ $status -eq 10 ] +} + +# 1.6 +@test "1.6 - Only allow trusted users to control Docker daemon" { + docker_users=$(getent group docker) + echoerr "$BATS_TEST_NAME" + for u in $docker_users; do + echoerr " * $u" + done +} + +# 1.7 +@test "1.7 - Audit docker daemon - /usr/bin/docker" { + file="/usr/bin/docker" + run command -v auditctl + if [ $status -eq 0 ]; then + auditctl -l | grep "$file" >/dev/null 2>&1 + else + echoerr " * Failed to inspect: auditctl command not found." + fi + [ $status -eq 0 ] +} + +# 1.8 +@test "1.8 - Audit Docker files and directories - /var/lib/docker" { + skip "TODO: need to implement" +} + +# 1.9 +@test "1.9 - Audit Docker files and directories - /etc/docker" { + skip "TODO: need to implement" +} + +# 1.10 +@test "1.10 - Audit Docker files and directories - docker.service" { + skip "TODO: need to implement" +} + +# 1.11 +@test "1.11 - Audit Docker files and directories - docker.socket" { + skip "TODO: need to implement" +} + +# 1.12 +@test "1.12 - Audit Docker files and directories - /etc/default/docker" { + skip "TODO: need to implement" +} + +# 1.13 +@test "1.13 - Audit Docker files and directories - /etc/docker/daemon.json" { + skip "TODO: need to implement" +} + +# 1.14 +@test "1.14 - Audit Docker files and directories - /usr/bin/docker-containerd" { + skip "TODO: need to implement" +} + +# 1.15 +@test "1.15 - Audit Docker files and directories - /usr/bin/docker-runc" { + skip "TODO: need to implement" +} From 5c34a4108efe7308f108bf93dcee46d15c2da749 Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Thu, 5 May 2016 16:17:32 +0300 Subject: [PATCH 02/11] Add bats-support library --- .gitmodules | 3 +++ test/test_helper/bats-support | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 test/test_helper/bats-support diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c527cf1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/test_helper/bats-support"] + path = test/test_helper/bats-support + url = https://github.com/ztombol/bats-support diff --git a/test/test_helper/bats-support b/test/test_helper/bats-support new file mode 160000 index 0000000..d0a1318 --- /dev/null +++ b/test/test_helper/bats-support @@ -0,0 +1 @@ +Subproject commit d0a131831c487a1f1141e76d3ab386c89642cdff From 78fdb9a3a0b9b17e385d3e3633953418f3cb10bf Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Thu, 5 May 2016 16:18:15 +0300 Subject: [PATCH 03/11] Add bats-assert library --- .gitmodules | 3 +++ test/test_helper/bats-assert | 1 + 2 files changed, 4 insertions(+) create mode 160000 test/test_helper/bats-assert diff --git a/.gitmodules b/.gitmodules index c527cf1..dc270e5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "test/test_helper/bats-support"] path = test/test_helper/bats-support url = https://github.com/ztombol/bats-support +[submodule "test/test_helper/bats-assert"] + path = test/test_helper/bats-assert + url = https://github.com/ztombol/bats-assert diff --git a/test/test_helper/bats-assert b/test/test_helper/bats-assert new file mode 160000 index 0000000..9f88b42 --- /dev/null +++ b/test/test_helper/bats-assert @@ -0,0 +1 @@ +Subproject commit 9f88b4207da750093baabc4e3f41bf68f0dd3630 From 370e3315f1db283da1bc034dff62d740c531af90 Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Thu, 5 May 2016 18:50:47 +0300 Subject: [PATCH 04/11] fighting with bats --- helper_lib.sh | 3 - {tests => test}/1_host_configuration.bats | 75 +++++++++++++++-------- 2 files changed, 50 insertions(+), 28 deletions(-) rename {tests => test}/1_host_configuration.bats (53%) diff --git a/helper_lib.sh b/helper_lib.sh index 2b6f1b0..74c8849 100644 --- a/helper_lib.sh +++ b/helper_lib.sh @@ -1,8 +1,5 @@ #!/bin/sh -# echo to stderr -echoerr() { echo "$@" 1>&2; } - # Returns the absolute path of a given string abspath () { case "$1" in /*)printf "%s\n" "$1";; *)printf "%s\n" "$PWD/$1";; esac; } diff --git a/tests/1_host_configuration.bats b/test/1_host_configuration.bats similarity index 53% rename from tests/1_host_configuration.bats rename to test/1_host_configuration.bats index 2944f5f..05b7c94 100644 --- a/tests/1_host_configuration.bats +++ b/test/1_host_configuration.bats @@ -1,34 +1,31 @@ #!/usr/bin/env bats +load 'test_helper/bats-support/load' +load 'test_helper/bats-assert/load' + setup() { . "$BATS_TEST_DIRNAME/../helper_lib.sh" } # 1.1 @test "1.1 - Create a separate partition for containers" { - grep /var/lib/docker /etc/fstab - [ $status -eq 0 ] + run grep /var/lib/docker /etc/fstab + assert_success } # 1.2 @test "1.2 - Use an updated Linux Kernel" { kernel_version=$(uname -r | cut -d "-" -f 1) run do_version_check 3.10 "$kernel_version" - [ $status -eq 9 ] || [ $status -eq 10 ] + assert [ $status -eq 9 -o $status -eq 10 ] } # 1.4 @test "1.4 - Remove all non-essential services from the host - Network" { # Check for listening network services. listening_services=$(netstat -na | grep -v tcp6 | grep -v unix | grep -c LISTEN) - if [ "$listening_services" -eq 0 ]; then - echoerr "1.4 - Failed to get listening services for check: $BATS_TEST_NAME" - else - if [ "$listening_services" -gt 5 ]; then - echoerr " * Host listening on: $listening_services ports" - fi - fi - [ "$listening_services" -ne 0 ] && [ "$listening_services" -le 5 ] + refute [ "$listening_services" -eq 0 ] "1.4 - Failed to get listening services for check: $BATS_TEST_NAME" + refute [ "$listening_services" -gt 5 ] "Host listening on: $listening_services ports" } # 1.5 @@ -39,22 +36,26 @@ setup() { docker_current_date="2016-04-27" run do_version_check "$docker_current_version" "$docker_version" if [ $status -eq 11 ]; then - echoerr " * Using $docker_version, when $docker_current_version is current as of $docker_current_date" - echoerr " * Your operating system vendor may provide support and security maintenance for docker" - else - pass "$check_1_5" - echoerr " * Using $docker_version which is current as of $docker_current_date" - echoerr " * Check with your operating system vendor for support and security maintenance for docker" + fail "Using $docker_version, when $docker_current_version is current as of $docker_current_date. Your operating system vendor may provide support and security maintenance for docker." fi - [ $status -eq 9 ] || [ $status -eq 10 ] + assert [ $status -eq 9 -o $status -eq 10 ] } # 1.6 @test "1.6 - Only allow trusted users to control Docker daemon" { - docker_users=$(getent group docker) - echoerr "$BATS_TEST_NAME" - for u in $docker_users; do - echoerr " * $u" + declare -a trusted_users=("vagrant" "docker" "ubuntu") + users_string=$(awk -F':' '/^docker/{print $4}' /etc/group) + docker_users=(${users_string//,/ }) + for u in ${docker_users[@]}; do + local found=1 + for tu in ${trusted_users[@]}; do + if [ "$u" = "$tu" ]; then + found=0 + fi + done + if [ $found -eq 1 ]; then + fail "User $u is not a trusted user!" + fi done } @@ -65,19 +66,43 @@ setup() { if [ $status -eq 0 ]; then auditctl -l | grep "$file" >/dev/null 2>&1 else - echoerr " * Failed to inspect: auditctl command not found." + fail "Failed to inspect: auditctl command not found." fi [ $status -eq 0 ] } # 1.8 @test "1.8 - Audit Docker files and directories - /var/lib/docker" { - skip "TODO: need to implement" + directory="/var/lib/docker" + if [ -d "$directory" ]; then + run command -v auditctl >/dev/null + if [ $status -eq 0 ]; then + auditctl -l | grep $directory >/dev/null 2>&1 + else + fail "1.8 - Failed to inspect: auditctl command not found." + fi + [ $status -eq 0 ] + else + fail " * '$directory' Directory not found" + [ -d "$directory" ] + fi } # 1.9 @test "1.9 - Audit Docker files and directories - /etc/docker" { - skip "TODO: need to implement" + directory="/etc/docker" + if [ -d "$directory" ]; then + run command -v auditctl >/dev/null + if [ $status -eq 0 ]; then + auditctl -l | grep $directory >/dev/null 2>&1 + else + fail "1.9 - Failed to inspect: auditctl command not found." + fi + [ $status -eq 0 ] + else + fail "'$directory' Directory not found" + [ -d "$directory" ] + fi } # 1.10 From 09f19482e8277fbf30fb4653bcae6cd6032e2cb7 Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Sat, 7 May 2016 15:56:35 +0300 Subject: [PATCH 05/11] convert second CIS section to bats --- test/1_host_configuration.bats | 94 ++++++++++++---------- test/2_docker_daemon_configuration.bats | 102 ++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 42 deletions(-) create mode 100644 test/2_docker_daemon_configuration.bats diff --git a/test/1_host_configuration.bats b/test/1_host_configuration.bats index 05b7c94..dfa283d 100644 --- a/test/1_host_configuration.bats +++ b/test/1_host_configuration.bats @@ -1,11 +1,8 @@ #!/usr/bin/env bats -load 'test_helper/bats-support/load' -load 'test_helper/bats-assert/load' - -setup() { - . "$BATS_TEST_DIRNAME/../helper_lib.sh" -} +load "test_helper/bats-support/load" +load "test_helper/bats-assert/load" +load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.1 @test "1.1 - Create a separate partition for containers" { @@ -63,74 +60,87 @@ setup() { @test "1.7 - Audit docker daemon - /usr/bin/docker" { file="/usr/bin/docker" run command -v auditctl - if [ $status -eq 0 ]; then - auditctl -l | grep "$file" >/dev/null 2>&1 - else - fail "Failed to inspect: auditctl command not found." - fi - [ $status -eq 0 ] + assert_success + run auditctl -l | grep "$file" + assert_success } # 1.8 @test "1.8 - Audit Docker files and directories - /var/lib/docker" { directory="/var/lib/docker" - if [ -d "$directory" ]; then - run command -v auditctl >/dev/null - if [ $status -eq 0 ]; then - auditctl -l | grep $directory >/dev/null 2>&1 - else - fail "1.8 - Failed to inspect: auditctl command not found." - fi - [ $status -eq 0 ] - else - fail " * '$directory' Directory not found" - [ -d "$directory" ] - fi + refute [ -d "$directory" ] "'$directory' Directory not found" + run command -v auditctl >/dev/null + assert_success + run auditctl -l | grep $directory + assert_success } # 1.9 @test "1.9 - Audit Docker files and directories - /etc/docker" { directory="/etc/docker" - if [ -d "$directory" ]; then - run command -v auditctl >/dev/null - if [ $status -eq 0 ]; then - auditctl -l | grep $directory >/dev/null 2>&1 - else - fail "1.9 - Failed to inspect: auditctl command not found." - fi - [ $status -eq 0 ] - else - fail "'$directory' Directory not found" - [ -d "$directory" ] - fi + refute [ -d "$directory" ] "'$directory' Directory not found" + run command -v auditctl + assert_success + run auditctl -l | grep $directory + assert_success } # 1.10 @test "1.10 - Audit Docker files and directories - docker.service" { - skip "TODO: need to implement" + file="$(get_systemd_service_file docker.service)" + refute [ -f "$file" ] "'docker.service' file not found" + run command -v auditctl + assert_success + run auditctl -l | grep "$file" + assert_success } # 1.11 @test "1.11 - Audit Docker files and directories - docker.socket" { - skip "TODO: need to implement" + file="$(get_systemd_service_file docker.socket)" + refute [ -e "$file" ] "'docker.socket' file not found" + run command -v auditctl + assert_success + run auditctl -l | grep "$file" + assert_success } # 1.12 @test "1.12 - Audit Docker files and directories - /etc/default/docker" { - skip "TODO: need to implement" + file="/etc/default/docker" + refute [ -f "$file" ] "'$file' file not found" + run command -v auditctl + assert_success + run auditctl -l | grep $file + assert_success } # 1.13 @test "1.13 - Audit Docker files and directories - /etc/docker/daemon.json" { - skip "TODO: need to implement" + file="/etc/docker/daemon.json" + refute [ -f "$file" ] "'$file' file not found" + run command -v auditctl + assert_success + run auditctl -l | grep $file + assert_success } # 1.14 @test "1.14 - Audit Docker files and directories - /usr/bin/docker-containerd" { - skip "TODO: need to implement" + file="/usr/bin/docker-containerd" + refute [ -f "$file" ] "'$file' file not found" + run command -v auditctl + assert_success + run auditctl -l | grep $file + assert_success } # 1.15 @test "1.15 - Audit Docker files and directories - /usr/bin/docker-runc" { - skip "TODO: need to implement" + file="/usr/bin/docker-runc" + refute [ -f "$file" ] "'$file' file not found" + run command -v auditctl + assert_success + run auditctl -l | grep $file + assert_success } diff --git a/test/2_docker_daemon_configuration.bats b/test/2_docker_daemon_configuration.bats new file mode 100644 index 0000000..776c500 --- /dev/null +++ b/test/2_docker_daemon_configuration.bats @@ -0,0 +1,102 @@ +#!/usr/bin/env bats + +load "test_helper/bats-support/load" +load "test_helper/bats-assert/load" +load "$BATS_TEST_DIRNAME/../helper_lib.sh" + +# 2.1 +@test "2.1 - Restrict network traffic between containers" { + result=$(get_docker_effective_command_line_args '--icc') + run grep "false" <<< "$result" + assert_success +} + +# 2.2 +@test "2.2 - Set the logging level" { + result=$(get_docker_effective_command_line_args '-l') + run grep 'debug' <<< "$result" + assert_failure +} + +# 2.3 +@test "2.3 - Allow Docker to make changes to iptables" { + result=$(get_docker_effective_command_line_args '--iptables') + run grep "false" <<< "$result" + assert_failure +} + +# 2.4 +@test "2.4 - Do not use insecure registries" { + result=$(get_docker_effective_command_line_args '--insecure-registry') + run grep "insecure-registry" <<< "$result" + assert_failure +} + +# 2.5 +@test "2.5 - Do not use the aufs storage driver" { + result=$(docker info 2>/dev/null) + run grep -e "^Storage Driver:\s*aufs\s*$" <<< "$result" + assert_failure +} + +# 2.6 +@test "2.6 - Configure TLS authentication for Docker daemon" { + result=$(get_docker_cumulative_command_line_args '-H') + run grep -vE '(unix|fd)://' <<< "$result" + if [ $status -eq 0 ]; then + result=$(get_command_line_args docker) + run $(grep "tlsverify" <<< "$result" | grep "tlskey") + assert_success + fi +} + +# 2.7 +@test "2.7 - Set default ulimit as appropriate" { + result=$(get_docker_effective_command_line_args '--default-ulimit') + run grep "default-ulimit" <<< "$result" + assert_success +} + +# 2.8 +@test "2.8 - Enable user namespace support" { + result=$(get_docker_effective_command_line_args '--userns-remap') + run grep "userns-remap" <<< "$result" + assert_success +} + +# 2.9 +@test "2.9 - Confirm default cgroup usage" { + result=$(get_docker_effective_command_line_args '--cgroup-parent') + run grep "cgroup-parent" <<< "$result" + if [ $status -eq 0 ]; then + refute_output_contains "docker" + fi +} + +# 2.10 +@test "2.10 - Do not change base device size until needed" { + result=$(get_docker_effective_command_line_args '--storage-opt') + run grep "dm.basesize" <<< "$result" + assert_failure +} + +# 2.11 +@test "2.11 - Use authorization plugin" { + result=$(get_docker_effective_command_line_args '--authorization-plugin') + run grep "authorization-plugin" <<< "$result" + assert_success +} + +# 2.12 +@test "2.12 - Configure centralized and remote logging" { + result=$(get_docker_effective_command_line_args '--log-driver') + run grep "log-driver" <<< "$result" + assert_success +} + +# 2.13 +@test "2.13 - Disable operations on legacy registry (v1)" { + result=$(get_docker_effective_command_line_args '--disable-legacy-registry') + run grep "disable-legacy-registry" <<< "$result" + assert_success +} From a91d99164f915cee0ff10041214c47cf07cdd2ec Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Fri, 13 May 2016 18:20:50 +0300 Subject: [PATCH 06/11] Implemented tests for CIS 3.x section --- README.md | 2 +- bats.Dockerfile | 34 +++ test/1_host_configuration.bats | 25 +- test/2_docker_daemon_configuration.bats | 2 +- test/3_docker_daemon_configuration_files.bats | 223 ++++++++++++++++++ 5 files changed, 274 insertions(+), 12 deletions(-) create mode 100644 bats.Dockerfile create mode 100644 test/3_docker_daemon_configuration_files.bats diff --git a/README.md b/README.md index 8689545..38ffd23 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ docker run -it --net host --pid host --cap-add audit_control \ Docker bench requires Docker 1.6.2 or later in order to run, since it depends on the `--label` to exclude the current container from being inspected. If you can't upgrade to 1.6.2, feel free to remove the `--label` flag or run the shell script locally (see below). -Additionally, there was a bug in Docker 1.6.0 that would not allow mounting `-v /dev:/dev`. If you are getting an error while accessing `resolv.conf`, please update your docker to 1.6.2. +Additionally, there was a bug in Docker 1.6.0 that would not allow mounting `-v /dev:/dev`. If you are getting an error while accessing `resolv.conf`, please update your docker to 1.6.2. Also note that the default image and `Dockerfile` uses `FROM: alpine` which doesn't contain `auditctl`, this will generate errors in section 1.8 to 1.18. Distribution specific Dockerfiles that fixes this issue are available in the [distros directory](https://github.com/docker/docker-bench-security/tree/master/distros). ## Building Docker Bench for Security diff --git a/bats.Dockerfile b/bats.Dockerfile new file mode 100644 index 0000000..376746a --- /dev/null +++ b/bats.Dockerfile @@ -0,0 +1,34 @@ +# REPOSITORY https://github.com/docker/docker-bench-securit +FROM alpine:3.3 + +MAINTAINER dockerbench.com +MAINTAINER Alexei Ledenev + +ENV VERSION 1.10.0 +ENV BATS_VERSION 0.4.0 + +WORKDIR /usr/bin + +RUN apk update && \ + apk upgrade && \ + apk --update add curl bash && \ + curl -sS https://get.docker.com/builds/Linux/x86_64/docker-$VERSION > docker-$VERSION && \ + curl -sS https://get.docker.com/builds/Linux/x86_64/docker-$VERSION.sha256 > docker-$VERSION.sha256 && \ + sha256sum -c docker-$VERSION.sha256 && \ + ln -s docker-$VERSION docker && \ + chmod u+x docker-$VERSION && \ + rm -rf /var/cache/apk/* + +RUN curl -o "/tmp/v${BATS_VERSION}.tar.gz" -L \ + "https://github.com/sstephenson/bats/archive/v${BATS_VERSION}.tar.gz" \ + && tar -x -z -f "/tmp/v${BATS_VERSION}.tar.gz" -C /tmp/ \ + && bash "/tmp/bats-${BATS_VERSION}/install.sh" /usr/local \ + && rm -rf /tmp/* + +RUN mkdir /docker-bench-security + +COPY . /docker-bench-security + +WORKDIR /docker-bench-security + +ENTRYPOINT ["/usr/local/bin/bats", "/docker-bench-security/test"] diff --git a/test/1_host_configuration.bats b/test/1_host_configuration.bats index dfa283d..62ad15b 100644 --- a/test/1_host_configuration.bats +++ b/test/1_host_configuration.bats @@ -21,8 +21,13 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" @test "1.4 - Remove all non-essential services from the host - Network" { # Check for listening network services. listening_services=$(netstat -na | grep -v tcp6 | grep -v unix | grep -c LISTEN) - refute [ "$listening_services" -eq 0 ] "1.4 - Failed to get listening services for check: $BATS_TEST_NAME" - refute [ "$listening_services" -gt 5 ] "Host listening on: $listening_services ports" + if [ "$listening_services" -eq 0 ]; then + fail "Failed to get listening services for check: $BATS_TEST_NAME" + else + if [ "$listening_services" -gt 5 ]; then + fail "Host listening on: $listening_services ports" + fi + fi } # 1.5 @@ -68,7 +73,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.8 @test "1.8 - Audit Docker files and directories - /var/lib/docker" { directory="/var/lib/docker" - refute [ -d "$directory" ] "'$directory' Directory not found" + assert [ -d "$directory" ] run command -v auditctl >/dev/null assert_success run auditctl -l | grep $directory @@ -78,7 +83,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.9 @test "1.9 - Audit Docker files and directories - /etc/docker" { directory="/etc/docker" - refute [ -d "$directory" ] "'$directory' Directory not found" + assert [ -d "$directory" ] run command -v auditctl assert_success run auditctl -l | grep $directory @@ -88,7 +93,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.10 @test "1.10 - Audit Docker files and directories - docker.service" { file="$(get_systemd_service_file docker.service)" - refute [ -f "$file" ] "'docker.service' file not found" + assert [ -f "$file" ] run command -v auditctl assert_success run auditctl -l | grep "$file" @@ -98,7 +103,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.11 @test "1.11 - Audit Docker files and directories - docker.socket" { file="$(get_systemd_service_file docker.socket)" - refute [ -e "$file" ] "'docker.socket' file not found" + assert [ -e "$file" ] run command -v auditctl assert_success run auditctl -l | grep "$file" @@ -108,7 +113,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.12 @test "1.12 - Audit Docker files and directories - /etc/default/docker" { file="/etc/default/docker" - refute [ -f "$file" ] "'$file' file not found" + assert [ -f "$file" ] run command -v auditctl assert_success run auditctl -l | grep $file @@ -118,7 +123,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.13 @test "1.13 - Audit Docker files and directories - /etc/docker/daemon.json" { file="/etc/docker/daemon.json" - refute [ -f "$file" ] "'$file' file not found" + assert [ -f "$file" ] run command -v auditctl assert_success run auditctl -l | grep $file @@ -128,7 +133,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.14 @test "1.14 - Audit Docker files and directories - /usr/bin/docker-containerd" { file="/usr/bin/docker-containerd" - refute [ -f "$file" ] "'$file' file not found" + assert [ -f "$file" ] run command -v auditctl assert_success run auditctl -l | grep $file @@ -138,7 +143,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 1.15 @test "1.15 - Audit Docker files and directories - /usr/bin/docker-runc" { file="/usr/bin/docker-runc" - refute [ -f "$file" ] "'$file' file not found" + assert [ -f "$file" ] run command -v auditctl assert_success run auditctl -l | grep $file diff --git a/test/2_docker_daemon_configuration.bats b/test/2_docker_daemon_configuration.bats index 776c500..de46c62 100644 --- a/test/2_docker_daemon_configuration.bats +++ b/test/2_docker_daemon_configuration.bats @@ -69,7 +69,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" result=$(get_docker_effective_command_line_args '--cgroup-parent') run grep "cgroup-parent" <<< "$result" if [ $status -eq 0 ]; then - refute_output_contains "docker" + assert_output_contains "docker" fi } diff --git a/test/3_docker_daemon_configuration_files.bats b/test/3_docker_daemon_configuration_files.bats new file mode 100644 index 0000000..de99861 --- /dev/null +++ b/test/3_docker_daemon_configuration_files.bats @@ -0,0 +1,223 @@ +#!/usr/bin/env bats + +load "test_helper/bats-support/load" +load "test_helper/bats-assert/load" +load "$BATS_TEST_DIRNAME/../helper_lib.sh" + +# 3.1 +@test "3.1 - Verify that docker.service file ownership is set to root:root" { + file="$(get_systemd_service_file docker.service)" + if [ -f "$file" ]; then + if [ "$(stat -c %u%g $file)" -ne 00 ]; then + fail "Wrong ownership for $file" + fi + fi +} + +# 3.2 +@test "3.2 - Verify that docker.service file permissions are set to 644" { + file="$(get_systemd_service_file docker.service)" + if [ -f "$file" ]; then + if [ "$(stat -c %a $file)" -ne 644 ]; then + fail "Wrong permissions for $file" + fi + fi +} + +# 3.3 +@test "3.3 - Verify that docker.socket file ownership is set to root:root" { + file="$(get_systemd_service_file docker.socket)" + if [ -f "$file" ]; then + if [ "$(stat -c %u%g $file)" -ne 00 ]; then + fail "Wrong ownership for $file" + fi + fi +} + +# 3.4 +@test "3.4 - Verify that docker.socket file permissions are set to 644" { + file="$(get_systemd_service_file docker.socket)" + if [ -f "$file" ]; then + if [ "$(stat -c %a $file)" -ne 644 ]; then + fail "Wrong permissions for $file" + fi + fi +} + +# 3.5 +@test "3.5 - Verify that /etc/docker directory ownership is set to root:root" { + directory="/etc/docker" + if [ -d "$directory" ]; then + if [ "$(stat -c %u%g $directory)" -ne 00 ]; then + fail "Wrong ownership for $directory" + fi + fi +} + +# 3.6 +@test "3.6 - Verify that /etc/docker directory permissions are set to 755 or 700" { + directory="/etc/docker" + if [ -d "$directory" ]; then + if [ "$(stat -c %a $directory)" -ne 755 -a "$(stat -c %a $directory)" -ne 700 ]; then + fail "Wrong permissions for $directory : $(stat -c %a $directory)" + fi + fi +} + +# 3.7 +@test "3.7 - Verify that registry certificate file ownership is set to root:root" { + directory="/etc/docker/certs.d/" + if [ -d "$directory" ]; then + fail=0 + owners=$(ls -lL $directory | grep ".crt" | awk '{print $3, $4}') + for p in $owners; do + printf "%s" "$p" | grep "root" >/dev/null 2>&1 + if [ $? -ne 0 ]; then + fail=1 + fi + done + if [ $fail -eq 1 ]; then + fail "Wrong ownership for $directory" + fi + fi +} + +# 3.8 +@test "3.8 - Verify that registry certificate file permissions are set to 444" { + directory="/etc/docker/certs.d/" + if [ -d "$directory" ]; then + fail=0 + perms=$(ls -lL $directory | grep ".crt" | awk '{print $1}') + for p in $perms; do + if [ "$p" != "-r--r--r--." -a "$p" = "-r--------." ]; then + fail=1 + fi + done + if [ $fail -eq 1 ]; then + fail "Wrong permissions for $directory" + fi + fi +} + +# 3.9 +@test "3.9 - Verify that TLS CA certificate file ownership is set to root:root" { + tlscacert=$(get_docker_effective_command_line_args '--tlscacert' | sed -n 's/.*tlscacert=\([^s]\)/\1/p' | sed 's/--/ --/g' | cut -d " " -f 1) + if [ -f "$tlscacert" ]; then + if [ "$(stat -c %u%g "$tlscacert")" -ne 00 ]; then + fail "Wrong ownership for $tlscacert : $(stat -c %u%g "$tlscacert")" + fi + fi +} + +# 3.10 +@test "3.10 - Verify that TLS CA certificate file permissions are set to 444" { + tlscacert=$(get_docker_effective_command_line_args '--tlscacert' | sed -n 's/.*tlscacert=\([^s]\)/\1/p' | sed 's/--/ --/g' | cut -d " " -f 1) + if [ -f "$tlscacert" ]; then + perms=$(ls -ld "$tlscacert" | awk '{print $1}') + if [ "$perms" != "-r--r--r--" ]; then + fail "Wrong permissions for $tlscacert" + fi + fi +} + +# 3.11 +@test "3.11 - Verify that Docker server certificate file ownership is set to root:root" { + tlscert=$(get_docker_effective_command_line_args '--tlscert' | sed -n 's/.*tlscert=\([^s]\)/\1/p' | sed 's/--/ --/g' | cut -d " " -f 1) + if [ -f "$tlscert" ]; then + if [ "$(stat -c %u%g "$tlscert")" -ne 00 ]; then + fail "Wrong ownership for $tlscert : $(stat -c %u%g "$tlscert")" + fi + fi +} + +# 3.12 +@test "3.12 - Verify that Docker server certificate file permissions are set to 444" { + tlscert=$(get_docker_effective_command_line_args '--tlscert' | sed -n 's/.*tlscert=\([^s]\)/\1/p' | sed 's/--/ --/g' | cut -d " " -f 1) + if [ -f "$tlscert" ]; then + perms=$(ls -ld "$tlscert" | awk '{print $1}') + if [ "$perms" != "-r--r--r--" ]; then + fail "Wrong permissions for $tlscert" + fi + fi +} + +# 3.13 +@test "3.13 - Verify that Docker server key file ownership is set to root:root" { + tlskey=$(get_docker_effective_command_line_args '--tlskey' | sed -n 's/.*tlskey=\([^s]\)/\1/p' | sed 's/--/ --/g' | cut -d " " -f 1) + if [ -f "$tlskey" ]; then + if [ "$(stat -c %u%g "$tlskey")" -ne 00 ]; then + fail "Wrong ownership for $tlskey : $(stat -c %u%g "$tlskey")" + fi + fi +} + +# 3.14 +@test "3.14 - Verify that Docker server key file permissions are set to 400" { + tlskey=$(get_docker_effective_command_line_args '--tlskey' | sed -n 's/.*tlskey=\([^s]\)/\1/p' | sed 's/--/ --/g' | cut -d " " -f 1) + if [ -f "$tlskey" ]; then + perms=$(ls -ld "$tlskey" | awk '{print $1}') + if [ "$perms" != "-r--------" ]; then + fail "Wrong permissions for $tlskey" + fi + fi +} + +# 3.15 +@test "3.15 - Verify that Docker socket file ownership is set to root:docker" { + file="/var/run/docker.sock" + if [ -S "$file" ]; then + if [ "$(stat -c %U:%G $file)" != 'root:docker' ]; then + fail "Wrong ownership for $file" + fi + fi +} + +# 3.16 +@test "3.16 - Verify that Docker socket file permissions are set to 660" { + file="/var/run/docker.sock" + if [ -S "$file" ]; then + if [ "$(stat -c %a $file)" -ne 660 ]; then + fail "Wrong permissions for $file" + fi + fi +} + +# 3.17 +@test "3.17 - Verify that daemon.json file ownership is set to root:root" { + file="/etc/docker/daemon.json" + if [ -f "$file" ]; then + if [ "$(stat -c %U:%G $file)" != 'root:root' ]; then + fail "Wrong ownership for $file" + fi + fi +} + +# 3.18 +@test "3.18 - Verify that daemon.json file permissions are set to 644" { + file="/etc/docker/daemon.json" + if [ -f "$file" ]; then + if [ "$(stat -c %a $file)" -ne 644 ]; then + fail "Wrong permissions for $file" + fi + fi +} + +# 3.19 +@test "3.19 - Verify that /etc/default/docker file ownership is set to root:root" { + file="/etc/default/docker" + if [ -f "$file" ]; then + if [ "$(stat -c %U:%G $file)" != 'root:root' ]; then + fail "Wrong ownership for $file" + fi + fi +} + +# 3.20 +@test "3.20 - Verify that /etc/default/docker file permissions are set to 644" { + file="/etc/default/docker" + if [ -f "$file" ]; then + if [ "$(stat -c %a $file)" -ne 644 ]; then + fail "Wrong permissions for $file" + fi + fi +} From 0e5cada34d61cd9cd2ac6c054e504ad32646ac4e Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Sun, 15 May 2016 18:01:08 +0300 Subject: [PATCH 07/11] run tests with run_test.sh; automatically generates tests per each running container, if needed --- bats.Dockerfile | 10 +- generate_tests.sh | 46 +++++++ run_tests.sh | 114 ++++++++++++++++++ test/1_host_configuration.bats | 66 ++++------ ...4_1_create_user_in_container.bats.template | 18 +++ test/4_container_images.bats | 9 ++ 6 files changed, 217 insertions(+), 46 deletions(-) create mode 100644 generate_tests.sh create mode 100755 run_tests.sh create mode 100644 test/4_1_create_user_in_container.bats.template create mode 100644 test/4_container_images.bats diff --git a/bats.Dockerfile b/bats.Dockerfile index 376746a..f681f12 100644 --- a/bats.Dockerfile +++ b/bats.Dockerfile @@ -7,11 +7,13 @@ MAINTAINER Alexei Ledenev ENV VERSION 1.10.0 ENV BATS_VERSION 0.4.0 +LABEL docker_bench_security=true + WORKDIR /usr/bin RUN apk update && \ apk upgrade && \ - apk --update add curl bash && \ + apk --update add curl bash ncurses ncurses-terminfo && \ curl -sS https://get.docker.com/builds/Linux/x86_64/docker-$VERSION > docker-$VERSION && \ curl -sS https://get.docker.com/builds/Linux/x86_64/docker-$VERSION.sha256 > docker-$VERSION.sha256 && \ sha256sum -c docker-$VERSION.sha256 && \ @@ -28,7 +30,11 @@ RUN curl -o "/tmp/v${BATS_VERSION}.tar.gz" -L \ RUN mkdir /docker-bench-security COPY . /docker-bench-security +RUN chmod +x /docker-bench-security/run_tests.sh WORKDIR /docker-bench-security -ENTRYPOINT ["/usr/local/bin/bats", "/docker-bench-security/test"] +VOLUME /var/docker-bench + +CMD ["-r"] +ENTRYPOINT ["./run_tests.sh"] diff --git a/generate_tests.sh b/generate_tests.sh new file mode 100644 index 0000000..b13d9b2 --- /dev/null +++ b/generate_tests.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +. ./helper_lib.sh + +TEST_SRC=./test +BENCH_ROOT=/var/docker-bench +TEST_ROOT=$BENCH_ROOT/test + +prepare_tests_directory() +{ + mkdir -p $BENCH_ROOT + if [ -d "$TEST_ROOT" ]; then + rm -rf $TEST_ROOT + fi + if [ ! -f "$BENCH_ROOT/helper_lib.sh" ]; then + cp helper_lib.sh $BENCH_ROOT + fi + cp -r $TEST_SRC $TEST_ROOT +} + +list_running_containers() { + # List all running containers + local containers=$(docker ps | sed '1d' | awk '{print $NF}') + # If there is a container with label docker_bench_security, memorize it: + local benchcont="nil" + for c in $containers; do + labels=$(docker inspect --format '{{ .Config.Labels }}' "$c") + contains "$labels" "docker_bench_security" && benchcont="$c" + done + # List all running containers except docker-bench (use names to improve readability in logs) + docker ps -aq --format="{{.Names}}" | grep -v "$benchcont" | tr "\n" " " +} + +generate_all_tests() { + # prepare test direcory: copy tests and templates + prepare_tests_directory + # generate tests from templates for running containers + local containers=$(list_running_containers) + ( cd $TEST_ROOT + for c in ${containers[@]}; do + for t in *.bats.template; do + sed -e "s/{{c}}/$c/g" "${t}" > "${t%.*.*}_${c}.bats" + done + done + ) +} diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..42afa39 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +. ./generate_tests.sh + +TERMINFO=/usr/share/terminfo + +TEST_RESULTS=$BENCH_ROOT/results + +# make result folder (inside VOLUME) +# mkdir -p $TEST_RESULTS +# generate all tests: copy host and daemon level tests and generate container level tests for running containers +# generate_all_tests +# run bats with all tests or passed tests +# bats $TEST_ROOT > + +#Set Script Name variable +SCRIPT="run_tests.sh" + +#Initialize variables to default values. +OPT_FORMAT="t" +OPT_OUTPUT=$TEST_RESULTS +OPT_RESULTS=1 + +#Set fonts for Help. +BOLD=`tput bold` +REV=`tput smso` +NORM=`tput sgr0` + +#Help function +HELP() { + echo -e \\n"Help documentation for ${BOLD}${SCRIPT}${NORM}"\\n + echo -e "Basic usage: ${BOLD}$SCRIPT [-c] [-p|-t] [-o path] [ ...]${NORM}"\\n + echo -e "Command line switches are optional. The following switches are recognized." + echo -e "${REV}-c${NORM} --Displays number of tests. No further functions are performed." + echo -e "${REV}-g${NORM} --Generates all CIS Bats tests without execution. No further functions are performed." + echo -e "${REV}-p${NORM} --Show results in pretty format." + echo -e "${REV}-t${NORM} --Show results in TAP format. This is the default format." + echo -e "${REV}-t${NORM} --Create test results files: ${BOLD}tests_.tap${NORM} in test result folder." + echo -e "${REV}-o${NORM} --Specify test result folder. Default to ${BOLD}$TEST_RESULTS${NORM}." + echo -e "${REV}-h${NORM} --Displays this help message. No further functions are performed."\\n + echo -e "Example: ${BOLD}$SCRIPT -t -o $TEST_RESULTS${NORM}"\\n + exit 1 +} + +#Check the number of arguments. If none are passed, print help and exit. +NUMARGS=$# +if [ $NUMARGS -eq 0 ]; then + HELP +fi + +### Start getopts code ### + +#Parse command line flags +while getopts o:rptcgh FLAG; do + case $FLAG in + o) # output test results into specified folder + OPT_OUTPUT=$OPTARG + ;; + p) # output test results in TAP format + OPT_FORMAT="p" + ;; + t) # output test results in pretty format + OPT_FORMAT="t" + ;; + r) # save test results into file + OPT_RESULTS=0 + ;; + c) # count tests + if [ -d "$TEST_ROOT" ]; then + echo -e "There are ${BOLD}$(bats $TEST_ROOT -c)${NORM} tests in ${BOLD}$TEST_ROOT${NORM}" + else + echo -e "No tests found, run ${BOLD}${SCRIPT}${NORM} with ${REV}-g${NORM} option first." + fi + exit 1 + ;; + g) # genetate all Bats tests: copy tests and generate tests (per container) from templates + generate_all_tests + exit 1 + ;; + h) #show help + HELP + ;; + \?) #unrecognized option - show help + echo -e \\n"Option -${BOLD}$OPTARG${NORM} not allowed." + HELP + ;; + esac +done + +shift $((OPTIND-1)) #This tells getopts to move on to the next argument. + +### End getopts code ### + +### Run Bats tests ### + +TESTS=$TEST_ROOT +if [ ! -d $TEST_ROOT ]; then # generate tests if needed + generate_all_tests +fi + +if [ $# -ne 0 ]; then # get tests from command line + TESTS=$* +fi + +if [ $OPT_RESULTS -eq 0 ]; then # run tests and [create test result file] + if [ ! -d "$OPT_OUTPUT" ]; then + mkdir -p "$OPT_OUTPUT" + fi + bats $TESTS -${OPT_FORMAT} > "${OPT_OUTPUT}/tests_$(date +%s).tap" +else + bats $TESTS -${OPT_FORMAT} +fi + +exit 0 diff --git a/test/1_host_configuration.bats b/test/1_host_configuration.bats index 62ad15b..97eee29 100644 --- a/test/1_host_configuration.bats +++ b/test/1_host_configuration.bats @@ -70,9 +70,8 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" assert_success } -# 1.8 -@test "1.8 - Audit Docker files and directories - /var/lib/docker" { - directory="/var/lib/docker" +test_audit_directory() { + local directory="$1" assert [ -d "$directory" ] run command -v auditctl >/dev/null assert_success @@ -80,72 +79,51 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" assert_success } -# 1.9 -@test "1.9 - Audit Docker files and directories - /etc/docker" { - directory="/etc/docker" - assert [ -d "$directory" ] +test_audit_file() { + file="$1" + assert [ -f "$file" ] run command -v auditctl assert_success - run auditctl -l | grep $directory + run auditctl -l | grep "$file" assert_success } +# 1.8 +@test "1.8 - Audit Docker files and directories - /var/lib/docker" { + test_audit_directory "/var/lib/docker" +} + +# 1.9 +@test "1.9 - Audit Docker files and directories - /etc/docker" { + test_audit_directory "/etc/docker" +} + # 1.10 @test "1.10 - Audit Docker files and directories - docker.service" { - file="$(get_systemd_service_file docker.service)" - assert [ -f "$file" ] - run command -v auditctl - assert_success - run auditctl -l | grep "$file" - assert_success + test_audit_file "$(get_systemd_service_file docker.service)" } # 1.11 @test "1.11 - Audit Docker files and directories - docker.socket" { - file="$(get_systemd_service_file docker.socket)" - assert [ -e "$file" ] - run command -v auditctl - assert_success - run auditctl -l | grep "$file" - assert_success + test_audit_file "$(get_systemd_service_file docker.socket)" } # 1.12 @test "1.12 - Audit Docker files and directories - /etc/default/docker" { - file="/etc/default/docker" - assert [ -f "$file" ] - run command -v auditctl - assert_success - run auditctl -l | grep $file - assert_success + test_audit_file "/etc/default/docker" } # 1.13 @test "1.13 - Audit Docker files and directories - /etc/docker/daemon.json" { - file="/etc/docker/daemon.json" - assert [ -f "$file" ] - run command -v auditctl - assert_success - run auditctl -l | grep $file - assert_success + test_audit_file "/etc/docker/daemon.json" } # 1.14 @test "1.14 - Audit Docker files and directories - /usr/bin/docker-containerd" { - file="/usr/bin/docker-containerd" - assert [ -f "$file" ] - run command -v auditctl - assert_success - run auditctl -l | grep $file - assert_success + test_audit_file "/usr/bin/docker-containerd" } # 1.15 @test "1.15 - Audit Docker files and directories - /usr/bin/docker-runc" { - file="/usr/bin/docker-runc" - assert [ -f "$file" ] - run command -v auditctl - assert_success - run auditctl -l | grep $file - assert_success + test_audit_file "/usr/bin/docker-runc" } diff --git a/test/4_1_create_user_in_container.bats.template b/test/4_1_create_user_in_container.bats.template new file mode 100644 index 0000000..ff67ab2 --- /dev/null +++ b/test/4_1_create_user_in_container.bats.template @@ -0,0 +1,18 @@ +#!/usr/bin/env bats + +load "test_helper/bats-support/load" +load "test_helper/bats-assert/load" +load "$BATS_TEST_DIRNAME/../helper_lib.sh" + + +# 4.1 +@test "4.1 - Create a user for the container {{c}}" { + local user=$(docker inspect --format 'User={{.Config.User}}' "{{c}}") + if [ "$user" = "User=" -o "$user" = "User=[]" -o "$user" = "User=" ]; then + # get PID 1 and check if it's running as root (uid=0) + local uid=$(docker exec {{c}} awk '/^Uid:/{print $2}' /proc/1/status) + if [ $uid -eq 0 ]; then + fail "Running as root: {{c}}" + fi + fi +} diff --git a/test/4_container_images.bats b/test/4_container_images.bats new file mode 100644 index 0000000..6f2de10 --- /dev/null +++ b/test/4_container_images.bats @@ -0,0 +1,9 @@ +#!/usr/bin/env bats + +load "test_helper/bats-support/load" +load "test_helper/bats-assert/load" + +# 4.5 +@test "4.5 - Enable Content trust for Docker" { + assert [ "x$DOCKER_CONTENT_TRUST" = "x1" ] +} From 28d3a4b4564ce66b880fd6e133b9e80831c68b55 Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Mon, 16 May 2016 17:44:58 +0300 Subject: [PATCH 08/11] fix/ignore non working xterm (due to /etc volume); download latest docker --- bats.Dockerfile | 30 ++++++++++++++---------------- run_tests.sh | 12 +++++++++--- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/bats.Dockerfile b/bats.Dockerfile index f681f12..af980b9 100644 --- a/bats.Dockerfile +++ b/bats.Dockerfile @@ -4,28 +4,26 @@ FROM alpine:3.3 MAINTAINER dockerbench.com MAINTAINER Alexei Ledenev -ENV VERSION 1.10.0 +ENV VERSION 1.11.1 ENV BATS_VERSION 0.4.0 LABEL docker_bench_security=true -WORKDIR /usr/bin +RUN apk --update add curl bash \ + && rm -rf /var/lib/apt/lists/* \ + && rm /var/cache/apk/* -RUN apk update && \ - apk upgrade && \ - apk --update add curl bash ncurses ncurses-terminfo && \ - curl -sS https://get.docker.com/builds/Linux/x86_64/docker-$VERSION > docker-$VERSION && \ - curl -sS https://get.docker.com/builds/Linux/x86_64/docker-$VERSION.sha256 > docker-$VERSION.sha256 && \ - sha256sum -c docker-$VERSION.sha256 && \ - ln -s docker-$VERSION docker && \ - chmod u+x docker-$VERSION && \ - rm -rf /var/cache/apk/* +RUN curl -o "/tmp/docker-$VERSION.tgz" -LS "https://get.docker.com/builds/Linux/x86_64/docker-$VERSION.tgz" && \ + curl -o "/tmp/docker-$VERSION.tgz.sha256" -LS "https://get.docker.com/builds/Linux/x86_64/docker-$VERSION.tgz.sha256" && \ + cd /tmp && sha256sum -c docker-$VERSION.tgz.sha256 && \ + tar -xvzf "/tmp/docker-$VERSION.tgz" -C /tmp/ && \ + chmod u+x /tmp/docker/docker && mv /tmp/docker/docker /usr/bin/ && \ + rm -rf /tmp/* -RUN curl -o "/tmp/v${BATS_VERSION}.tar.gz" -L \ - "https://github.com/sstephenson/bats/archive/v${BATS_VERSION}.tar.gz" \ - && tar -x -z -f "/tmp/v${BATS_VERSION}.tar.gz" -C /tmp/ \ - && bash "/tmp/bats-${BATS_VERSION}/install.sh" /usr/local \ - && rm -rf /tmp/* +RUN curl -o "/tmp/v${BATS_VERSION}.tar.gz" -LS "https://github.com/sstephenson/bats/archive/v${BATS_VERSION}.tar.gz" && \ + tar -xvzf "/tmp/v${BATS_VERSION}.tar.gz" -C /tmp/ && \ + bash "/tmp/bats-${BATS_VERSION}/install.sh" /usr/local && \ + rm -rf /tmp/* RUN mkdir /docker-bench-security diff --git a/run_tests.sh b/run_tests.sh index 42afa39..b1c4afd 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -22,9 +22,15 @@ OPT_OUTPUT=$TEST_RESULTS OPT_RESULTS=1 #Set fonts for Help. -BOLD=`tput bold` -REV=`tput smso` -NORM=`tput sgr0` +if [ -e "/usr/bin/tput" ]; then + BOLD=`tput bold` + REV=`tput smso` + NORM=`tput sgr0` +else + BOLD="" + REV="" + NORM="" +fi #Help function HELP() { From 100b33ceda65bb777abf57c8812882c40cfa944f Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Mon, 16 May 2016 18:05:06 +0300 Subject: [PATCH 09/11] update README.md file with info about running Bats tests --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index 38ffd23..057423c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,57 @@ Docker bench requires Docker 1.6.2 or later in order to run, since it depends on Additionally, there was a bug in Docker 1.6.0 that would not allow mounting `-v /dev:/dev`. If you are getting an error while accessing `resolv.conf`, please update your docker to 1.6.2. Also note that the default image and `Dockerfile` uses `FROM: alpine` which doesn't contain `auditctl`, this will generate errors in section 1.8 to 1.18. Distribution specific Dockerfiles that fixes this issue are available in the [distros directory](https://github.com/docker/docker-bench-security/tree/master/distros). +## Running Docker Bench Bats tests + +[Bats](https://github.com/sstephenson/bats) is a [TAP](http://testanything.org/)-compliant testing framework for Bash. It provides a simple way to verify that the UNIX programs you write behave as expected. + +All Docker Bench scipts are also available as Bats tests. Also container level (and image level) tests are automatically generated for all containers avaiable on host. It's possible to run all or only selected test(s), if you like. + +By default TAP test results are reported, but it's possible to produce a "pretty" printed output too. + +Use the following command to run Docker Bench Bats tests: + +``` +Help documentation for run_tests.sh + +Basic usage: run_tests.sh [-c] [-p|-t] [-o path] [ ...] + +Command line switches are optional. The following switches are recognized. +-c --Displays number of tests. No further functions are performed. +-g --Generates all CIS Bats tests without execution. No further functions are performed. +-p --Show results in pretty format. +-t --Show results in TAP format. This is the default format. +-t --Create test results files: tests_.tap in test result folder. +-o --Specify test result folder. Default to /var/docker-bench/results. +-h --Displays this help message. No further functions are performed. + +Example: run_tests.sh -t -o /var/docker-bench/results +``` + +You need to run `run_tests.sh` on Docker host as `root` user. + +### Running Docker Bench Bats tests from Docker image + +First, clone and compile your `docker-bench-tests` Docker image. + +```sh +git clone https://github.com/gaia-adm/docker-bench-security.git +cd docker-bench-security +docker build -t docker-bench-tests -f bats.Dockerfile . +``` + +Then run `docker-bench-tests` container (as bellow). Test results will be saved into `/var/docker-bench` folder in TAP format. Test results file is named accoring to the `test_.tap` pattern. + +```sh +docker run -it --net host --pid host --cap-add audit_control \ + -v /var/lib:/var/lib \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /usr/lib/systemd:/usr/lib/systemd \ + -v /var/docker-bench:/var/docker-bench + -v /etc:/etc --label docker_bench_security \ + docker-bench-tests +``` + ## Building Docker Bench for Security If you wish to build and run this container yourself, you can follow the following steps: From 1f856a743aa51459c95039cd9b0bbd40a580877f Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Tue, 17 May 2016 13:17:03 +0300 Subject: [PATCH 10/11] converted tests for section 5 and 6 --- .gitmodules | 6 - .../1_host_configuration.bats | 0 .../2_docker_daemon_configuration.bats | 0 .../3_docker_daemon_configuration_files.bats | 0 ...4_1_create_user_in_container.bats.template | 2 +- {test => bats_tests}/4_container_images.bats | 0 bats_tests/5_container_runtime.bats.template | 202 ++++++++++++++++++ bats_tests/6_docker_security_operations.bats | 36 ++++ generate_tests.sh | 4 +- test/test_helper/bats-assert | 1 - test/test_helper/bats-support | 1 - 11 files changed, 241 insertions(+), 11 deletions(-) delete mode 100644 .gitmodules rename {test => bats_tests}/1_host_configuration.bats (100%) rename {test => bats_tests}/2_docker_daemon_configuration.bats (100%) rename {test => bats_tests}/3_docker_daemon_configuration_files.bats (100%) rename {test => bats_tests}/4_1_create_user_in_container.bats.template (90%) rename {test => bats_tests}/4_container_images.bats (100%) create mode 100644 bats_tests/5_container_runtime.bats.template create mode 100644 bats_tests/6_docker_security_operations.bats delete mode 160000 test/test_helper/bats-assert delete mode 160000 test/test_helper/bats-support diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index dc270e5..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "test/test_helper/bats-support"] - path = test/test_helper/bats-support - url = https://github.com/ztombol/bats-support -[submodule "test/test_helper/bats-assert"] - path = test/test_helper/bats-assert - url = https://github.com/ztombol/bats-assert diff --git a/test/1_host_configuration.bats b/bats_tests/1_host_configuration.bats similarity index 100% rename from test/1_host_configuration.bats rename to bats_tests/1_host_configuration.bats diff --git a/test/2_docker_daemon_configuration.bats b/bats_tests/2_docker_daemon_configuration.bats similarity index 100% rename from test/2_docker_daemon_configuration.bats rename to bats_tests/2_docker_daemon_configuration.bats diff --git a/test/3_docker_daemon_configuration_files.bats b/bats_tests/3_docker_daemon_configuration_files.bats similarity index 100% rename from test/3_docker_daemon_configuration_files.bats rename to bats_tests/3_docker_daemon_configuration_files.bats diff --git a/test/4_1_create_user_in_container.bats.template b/bats_tests/4_1_create_user_in_container.bats.template similarity index 90% rename from test/4_1_create_user_in_container.bats.template rename to bats_tests/4_1_create_user_in_container.bats.template index ff67ab2..8e1fbaf 100644 --- a/test/4_1_create_user_in_container.bats.template +++ b/bats_tests/4_1_create_user_in_container.bats.template @@ -6,7 +6,7 @@ load "$BATS_TEST_DIRNAME/../helper_lib.sh" # 4.1 -@test "4.1 - Create a user for the container {{c}}" { +@test "4.1 - Create a user for the container: {{c}}" { local user=$(docker inspect --format 'User={{.Config.User}}' "{{c}}") if [ "$user" = "User=" -o "$user" = "User=[]" -o "$user" = "User=" ]; then # get PID 1 and check if it's running as root (uid=0) diff --git a/test/4_container_images.bats b/bats_tests/4_container_images.bats similarity index 100% rename from test/4_container_images.bats rename to bats_tests/4_container_images.bats diff --git a/bats_tests/5_container_runtime.bats.template b/bats_tests/5_container_runtime.bats.template new file mode 100644 index 0000000..3bbf13f --- /dev/null +++ b/bats_tests/5_container_runtime.bats.template @@ -0,0 +1,202 @@ +#!/usr/bin/env bats + +load "test_helper/bats-support/load" +load "test_helper/bats-assert/load" +load "$BATS_TEST_DIRNAME/../helper_lib.sh" + +# 5.1 +@test "5.1 - Verify AppArmor Profile, if applicable: {{c}}" { + policy=$(docker inspect --format 'AppArmorProfile={{ .AppArmorProfile }}' "{{c}}") + if [ "$policy" = "AppArmorProfile=" -o "$policy" = "AppArmorProfile=[]" -o "$policy" = "AppArmorProfile=" ]; then + fail "No AppArmorProfile Found: {{c}}" + fi +} + +# 5.2 +@test "5.2 - Verify SELinux security options, if applicable: {{c}}" { + policy=$(docker inspect --format 'SecurityOpt={{ .HostConfig.SecurityOpt }}' "{{c}}") + if [ "$policy" = "SecurityOpt=" -o "$policy" = "SecurityOpt=[]" -o "$policy" = "SecurityOpt=" ]; then + fail "No SecurityOptions Found: {{c}}" + fi +} + +# 5.3 +@test "5.3 - Restrict Linux Kernel Capabilities within containers: {{c}}" { + caps=$(docker inspect --format 'CapAdd={{ .HostConfig.CapAdd}}' "{{c}}") + if [ "$caps" != 'CapAdd=' -a "$caps" != 'CapAdd=[]' -a "$caps" != 'CapAdd=' -a "$caps" != 'CapAdd=' ]; then + fail "Capabilities added: $caps to {{c}}" + fi +} + +# 5.4 +@test "5.4 - Do not use privileged containers: {{c}}" { + privileged=$(docker inspect --format '{{ .HostConfig.Privileged }}' "{{c}}") + if [ "$privileged" = "true" ]; then + fail "Container running in Privileged mode: {{c}}" + fi +} + +# 5.5 +@test "5.5 - Do not mount sensitive host system directories on containers: {{c}}" { + # List of sensitive directories to test for. Script uses new-lines as a separator. + # Note the lack of identation. It needs it for the substring comparison. + sensitive_dirs=(/boot /dev /etc /lib /proc /sys /usr) + run docker inspect --format '{{ .VolumesRW }}' "{{c}}" + if [ $status -eq 0 ]; then + volumes=$(docker inspect --format '{{ .VolumesRW }}' "{{c}}") + else + volumes=$(docker inspect --format '{{ .Mounts }}' "{{c}}") + fi + # Go over each directory in sensitive dir and see if they exist in the volumes + for v in ${sensitive_dirs[@]}; do + run contains "$volumes" "$v" + if [ $status -eq 0 ]; then + fail "Sensitive directory $v mounted in: {{c}}" + fi + done +} + +# 5.7 +@test "5.7 - Do not map privileged ports within containers: {{c}}" { + # Port format is private port -> ip: public port + ports=$(docker port "{{c}}" | awk '{print $3}' | cut -d ':' -f2 | tr "\n" " ") + # iterate through port range (line delimited) + for port in $ports; do + if [ ! -z "$port" ] && [ "0$port" -lt 1024 ]; then + fail "Privileged Port in use: $port in {{c}}" + fi + done +} + +# 5.9 +@test "5.9 - Do not share the host's network namespace: {{c}}" { + mode=$(docker inspect --format 'NetworkMode={{ .HostConfig.NetworkMode }}' "{{c}}") + if [ "$mode" = "NetworkMode=host" ]; then + fail "Container running with networking mode 'host': {{c}}" + fi +} + +# 5.10 +@test "5.10 - Limit memory usage for container: {{c}}" { + run docker inspect --format '{{ .Config.Memory }}' "{{c}}" + if [ $status -eq 0 ]; then + memory=$(docker inspect --format '{{ .Config.Memory }}' "{{c}}") + else + memory=$(docker inspect --format '{{ .HostConfig.Memory }}' "{{c}}") + fi + if [ "$memory" = "0" ]; then + fail "Container running without memory restrictions: {{c}}" + fi +} + +# 5.11 +@test "5.11 - Set container CPU priority appropriately: {{c}}" { + run docker inspect --format '{{ .Config.CpuShares }}' "{{c}}" + if [ $status -eq 0 ]; then + shares=$(docker inspect --format '{{ .Config.CpuShares }}' "{{c}}") + else + shares=$(docker inspect --format '{{ .HostConfig.CpuShares }}' "{{c}}") + fi + if [ "$shares" = "0" ]; then + fail "Container running without CPU restrictions: {{c}}" + fi +} + +# 5.12 +@test "5.12 - Mount container's root filesystem as read only: {{c}}" { + read_status=$(docker inspect --format '{{ .HostConfig.ReadonlyRootfs }}' "{{c}}") + if [ "$read_status" = "false" ]; then + fail "Container running with root FS mounted R/W: {{c}}" + fi +} + +# 5.13 +@test "5.13 - Bind incoming container traffic to a specific host interface: {{c}}" { + for ip in $(docker port "{{c}}" | awk '{print $3}' | cut -d ':' -f1 | tr "\n" " "); do + if [ "$ip" = "0.0.0.0" ]; then + fail "Port being bound to wildcard IP: $ip in {{c}}" + fi + done +} + +# 5.14 +@test "5.14 - Set the 'on-failure' container restart policy to 5: {{c}}" { + policy=$(docker inspect --format MaximumRetryCount='{{ .HostConfig.RestartPolicy.MaximumRetryCount }}' "{{c}}") + if [ "$policy" != "MaximumRetryCount=5" ]; then + fail "MaximumRetryCount is not set to 5: {{c}}" + fi +} + +# 5.15 +@test "5.15 - Do not share the host's process namespace: {{c}}" { + mode=$(docker inspect --format 'PidMode={{.HostConfig.PidMode }}' "{{c}}") + if [ "$mode" = "PidMode=host" ]; then + fail "Host PID namespace being shared with: {{c}}" + fi +} + +# 5.16 +@test "5.16 - Do not share the host's IPC namespace: {{c}}" { + mode=$(docker inspect --format 'IpcMode={{.HostConfig.IpcMode }}' "{{c}}") + if [ "$mode" = "IpcMode=host" ]; then + fail "Host IPC namespace being shared with: {{c}}" + fi +} + +# 5.17 +@test "5.17 - Do not directly expose host devices to containers: {{c}}" { + devices=$(docker inspect --format 'Devices={{ .HostConfig.Devices }}' "{{c}}") + if [ "$devices" != "Devices=" -a "$devices" != "Devices=[]" -a "$devices" != "Devices=" ]; then + fail "Container has devices exposed directly: {{c}}" + fi +} + +# 5.18 +@test "5.18 - Override default ulimit at runtime only if needed: {{c}}" { + ulimits=$(docker inspect --format 'Ulimits={{ .HostConfig.Ulimits }}' "{{c}}") + if [ "$ulimits" = "Ulimits=" -o "$ulimits" = "Ulimits=[]" -o "$ulimits" = "Ulimits=" ]; then + fail "Container no default ulimit override: {{c}}" + fi +} + +# 5.19 +@test "5.19 - Do not set mount propagation mode to shared: {{c}}" { + mode=$(docker inspect --format 'Propagation={{range $mnt := .Mounts}} {{json $mnt.Propagation}} {{end}}' "{{c}}") + if [ "$mode" = "Propagation=shared" ]; then + fail "Mount propagation mode is shared: {{c}}" + fi +} + +# 5.20 +@test "5.20 - Do not share the host's UTS namespace: {{c}}" { + mode=$(docker inspect --format 'UTSMode={{.HostConfig.UTSMode }}' "{{c}}") + if [ "$mode" = "UTSMode=host" ]; then + fail "Host UTS namespace being shared with: {{c}}" + fi +} + +# 5.21 +@test "5.21 - Do not disable default seccomp profile: {{c}}" { + result=$(docker inspect --format 'SecurityOpt={{.HostConfig.SecurityOpt }}' "{{c}}") + run grep "seccomp:unconfined" <<< "$result" + if [ $status -eq 0 ]; then + fail "Default seccomp profile disabled: {{c}}" + fi +} + +# 5.24 +@test "5.24 - Confirm cgroup usage: {{c}}" { + mode=$(docker inspect --format 'CgroupParent={{.HostConfig.CgroupParent }}x' "{{c}}") + if [ "$mode" != "CgroupParent=x" ]; then + fail "Confirm cgroup usage: {{c}}" + fi +} + +# 5.25 +@test "5.25 - Restrict container from acquiring additional privileges: {{c}}" { + result=$(docker inspect --format 'SecurityOpt={{.HostConfig.SecurityOpt }}' "{{c}}") + run grep "no-new-privileges" <<< "$result" + if [ $status -ne 0 ]; then + fail "Privileges not restricted: {{c}}" + fi +} diff --git a/bats_tests/6_docker_security_operations.bats b/bats_tests/6_docker_security_operations.bats new file mode 100644 index 0000000..c4d9f43 --- /dev/null +++ b/bats_tests/6_docker_security_operations.bats @@ -0,0 +1,36 @@ +#!/usr/bin/env bats + +load "test_helper/bats-support/load" +load "test_helper/bats-assert/load" +load "$BATS_TEST_DIRNAME/../helper_lib.sh" + + +# 6.4 +@test "6.4 - Avoid image sprawl" { + images=$(docker images -q | sort -u | wc -l | awk '{print $1}') + active_images=0 + + for c in $(docker inspect -f "{{.Image}}" $(docker ps -qa)); do + if docker images --no-trunc -a | grep "$c" > /dev/null ; then + active_images=$(( active_images += 1 )) + fi + done + + if [ "$images" -gt 100 ]; then + fail "There are currently: $images images" + fi + + if [ "$active_images" -lt "$((images / 2))" ]; then + fail "Only $active_images out of $images are in use" + fi +} + +# 6.5 +@test "6.5 - Avoid container sprawl" { + total_containers=$(docker info 2>/dev/null | grep "Containers" | awk '{print $2}') + running_containers=$(docker ps -q | wc -l | awk '{print $1}') + diff="$((total_containers - running_containers))" + if [ "$diff" -gt 25 ]; then + fail "There are currently a total of $total_containers containers, with only $running_containers of them currently running" + fi +} diff --git a/generate_tests.sh b/generate_tests.sh index b13d9b2..908cbfa 100644 --- a/generate_tests.sh +++ b/generate_tests.sh @@ -2,9 +2,9 @@ . ./helper_lib.sh -TEST_SRC=./test +TEST_SRC=./bats_tests BENCH_ROOT=/var/docker-bench -TEST_ROOT=$BENCH_ROOT/test +TEST_ROOT=$BENCH_ROOT/bats_tests prepare_tests_directory() { diff --git a/test/test_helper/bats-assert b/test/test_helper/bats-assert deleted file mode 160000 index 9f88b42..0000000 --- a/test/test_helper/bats-assert +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9f88b4207da750093baabc4e3f41bf68f0dd3630 diff --git a/test/test_helper/bats-support b/test/test_helper/bats-support deleted file mode 160000 index d0a1318..0000000 --- a/test/test_helper/bats-support +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d0a131831c487a1f1141e76d3ab386c89642cdff From a888600cbb2c08e85ce279b335345e136d529f9b Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Tue, 17 May 2016 13:20:08 +0300 Subject: [PATCH 11/11] fix submodules path rename --- .gitmodules | 6 ++++++ bats_tests/test_helper/bats-assert | 1 + bats_tests/test_helper/bats-support | 1 + 3 files changed, 8 insertions(+) create mode 100644 .gitmodules create mode 160000 bats_tests/test_helper/bats-assert create mode 160000 bats_tests/test_helper/bats-support diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b18fe2f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "bats_tests/test_helper/bats-assert"] + path = bats_tests/test_helper/bats-assert + url = https://github.com/ztombol/bats-assert +[submodule "bats_tests/test_helper/bats-support"] + path = bats_tests/test_helper/bats-support + url = https://github.com/ztombol/bats-support diff --git a/bats_tests/test_helper/bats-assert b/bats_tests/test_helper/bats-assert new file mode 160000 index 0000000..9f88b42 --- /dev/null +++ b/bats_tests/test_helper/bats-assert @@ -0,0 +1 @@ +Subproject commit 9f88b4207da750093baabc4e3f41bf68f0dd3630 diff --git a/bats_tests/test_helper/bats-support b/bats_tests/test_helper/bats-support new file mode 160000 index 0000000..d0a1318 --- /dev/null +++ b/bats_tests/test_helper/bats-support @@ -0,0 +1 @@ +Subproject commit d0a131831c487a1f1141e76d3ab386c89642cdff