diff --git a/CHANGELOG.md b/CHANGELOG.md index e10873c..6078ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 3.0.0 (unreleased) + +**Implemented enhancements:** + +- Update to CIS Docker Benchmark v1.8.0 with 118 controls, revised numbering, and removal of retired recommendations [\#59](https://github.com/dev-sec/cis-docker-benchmark/issues/59) +- Add configurable package, setuid/setgid, image-history, published-port, image-tag, and artifact-signature checks +- Extend package-index update checks to apk, yum, dnf, and zypper +- Update profile inputs, sample attributes, and migration documentation + +**Fixed bugs:** + +- Check running daemon arguments alongside its configuration file [\#27](https://github.com/dev-sec/cis-docker-benchmark/issues/27) +- Avoid matching image metadata as package update commands [\#80](https://github.com/dev-sec/cis-docker-benchmark/issues/80) +- Report container IDs and checked properties instead of full Docker inspect objects [\#76](https://github.com/dev-sec/cis-docker-benchmark/issues/76) + ## [2.1.4](https://github.com/dev-sec/cis-docker-benchmark/tree/2.1.4) (2023-05-02) [Full Changelog](https://github.com/dev-sec/cis-docker-benchmark/compare/2.1.3...2.1.4) diff --git a/README.md b/README.md index 92d2366..cd89cde 100644 --- a/README.md +++ b/README.md @@ -6,65 +6,66 @@ ## Description -This [InSpec](https://github.com/chef/inspec) compliance profile implement the [CIS Docker 1.13.0 Benchmark](https://downloads.cisecurity.org/) in an automated way to provide security best-practice tests around Docker daemon and containers in a production environment. +This [InSpec](https://github.com/chef/inspec) compliance profile implements the [CIS Docker Benchmark v1.8.0](https://www.cisecurity.org/benchmark/docker), published July 24, 2025. It contains 118 recommendations covering Linux hosts, the Docker daemon, container images and runtime, security operations, and Swarm. InSpec is an open-source run-time framework and rule language used to specify compliance, security, and policy requirements for testing any node in your infrastructure. ## Requirements -* at least [InSpec](http://inspec.io/) version 2.3.23 -* Docker 1.13+ +* InSpec 4.6.3 or later, or a compatible CINC Auditor release. +* Docker Engine on Linux. CIS v1.8.0 updates the benchmark for Docker 28.x. +* Access to the Docker CLI, daemon configuration, systemd units (where used), and audit rules on the target. Run with sufficient privileges, normally `--sudo`. +* The Docker context must point to the daemon on the audited host; a remote Docker context would mix daemon evidence with the wrong host files. -### Platform +### Assessment coverage -* Debian 8 -* Ubuntu 16.04 -* CentOS 7 +Controls check host configuration, daemon files, auditd rules, image build histories, and container runtimes. CIS's `Manual` classification does not disable executable checks. Organizational practices without verifiable host evidence still require review. + +Controls `docker-5.23` and `docker-5.24` search retained audit logs for Docker exec events. They require a running audit daemon and an execution rule for `docker_cli_path` under the `docker` audit key. The search is shared between both controls and validates that no unapproved privileged or root execs are recorded. + +Set policy inputs to your approved values. `container_capadd` and `seccomp_default_profile` retain their existing defaults; `default_ulimits` makes daemon limits configurable. `docker_min_version` defaults to 28.0.0, the benchmark's target major version. `swarm_node_cert_expiry_days` and `swarm_ca_rotation_days` default to 90 days. + +Container configuration checks include stopped containers. Process, published-port, and runtime health checks inspect running containers. Swarm controls apply only when the target is in Swarm mode, with manager-only checks skipped on workers. Optional files are skipped when absent. + +Package checks query dpkg, rpm, or apk inside running containers. Unavailable or failed package queries skip that container with instructions to review its image SBOM manually. Setuid/setgid checks read their root filesystems through `/proc` on the host; failed filesystem queries report an evidence error. Image-history checks detect known credential patterns and verification-bypass flags; they do not establish the absence of every possible secret or validate every installed package. Control 5.28 checks image-reference policy, without pulling images or asserting that a tag matches the current registry digest. + +Control 4.12 verifies each declared artifact's detached SHA-256 signature with OpenSSL and its approved RSA or EC public key. Supply target paths in `signed_artifacts`; an empty list skips verification with instructions to review signatures and provenance in the build pipeline. Docker content-trust settings are not a substitute for artifact signatures. + +### Migration from 2.x + +Control IDs now follow CIS v1.8.0 numbering: `host-1.1.1`, `docker-2.1`, through `docker-7.9`. Update control selections and waivers accordingly. Legacy benchmark selection and retired recommendations have been removed. + +`trusted_users` replaces `trusted_user` and checks every Docker group member against the approved list. Workload requirements and security baselines are supplied through policy inputs where the resulting settings are checked automatically. ## Attributes -We use a yml attribute file to steer the configuration, the following options are available: - -* `trusted_user: vagrant` - define trusted user to control Docker daemon. -* `authorization_plugin: authz-broker` - define authorization plugin to manage access to Docker daemon. -* `log_driver: syslog` - define preferable way to store logs. -* `log_opts: /syslog-address/` - define Docker daemon log-opts. -* `registry_cert_path: /etc/docker/certs.d` - directory contains various Docker registry directories. -* `registry_name: /etc/docker/certs.d/registry_hostname:port` - directory contain certificate certain Docker registry. -* `registry_ca_file: /etc/docker/certs.d/registry_hostname:port/ca.crt` - certificate file for a certain Docker registry certificate files. -* `container_user: vagrant` - define user within containers. -* `app_armor_profile: docker-default` - define apparmor profile for Docker containers. -* `selinux_profile: /label\:level\:s0-s0\:c1023/` - define SELinux profile for Docker containers. -* `container_capadd: null` - define needed capabilities for containers. example: `container_capadd: NET_ADMIN,SYS_ADMIN` -* `managable_container_number: 25` - keep number of containers on a host to a manageable total. -* `daemon_tlscacert : /etc/docker/ssl/ca.pem` - configure the certificate authority. -* `daemon_tlscert: /etc/docker/ssl/server_cert.pem` - configure the server certificate. -* `daemon_tlskey: /etc/docker/ssl/server_key.pem` - configure the server key. -* `swarm_mode: inactive` - configure the swarm mode. -* `swarm_max_manager_nodes: 3` - configure the maximum number of swarm leaders. -* `swarm_port: 2377` - configure the swarm port. -* `benchmark_version` - to execute also the old controls from previous benchmarks, e.g. set it to 1.12.0 to execute also the tests from cis-benchmark-1.12.0 (which is the default). - -These settings can be overridden using an attributes file (e.g. --attrs ). See [sample_attributes.yml](sample_attributes.yml) as an example. +Use [sample_attributes.yml](sample_attributes.yml) with `--input-file sample_attributes.yml` to override these inputs: + +* `trusted_users: []`: approved Docker group members; an empty list permits no members. +* `managable_container_number: 25`: maximum stopped containers retained on the host. +* `registry_cert_path: /etc/docker/certs.d`: registry certificate directory; all files below it are checked. +* `docker_daemon_config: /etc/docker/daemon.json`: effective daemon configuration file. +* `docker_daemon_path: /usr/bin/dockerd`: daemon executable to audit. +* `docker_cli_path: /usr/bin/docker`: Docker CLI client executable to audit. +* `docker_socket: /var/run/docker.sock`: Docker API Unix socket. +* `containerd_socket: /run/containerd/containerd.sock`: containerd gRPC socket. +* `authorization_plugin: authz-broker`: required daemon authorization plugin. +* `log_driver: syslog`: expected daemon log driver. +* `log_opts: syslog-address`: required remote logging option key. +* `swarm_mode: inactive`: expected Swarm state; set to `active` when Swarm is required. +* `swarm_max_manager_nodes: 3`: maximum approved number of Swarm managers. +* `swarm_port: 2377`: Swarm management port; discovery port 7946 is also checked. +* `swarm_node_cert_expiry_days: 90`: maximum Swarm node certificate lifetime in days. +* `swarm_ca_rotation_days: 90`: maximum age of the Swarm root CA certificate file in days. +* `approved_container_ports: []`: approved published container ports, such as `443/tcp`; empty permits none. +* `prohibited_packages`: list of high-risk / bloat packages prohibited in containers (`gcc`, `g++`, `gdb`, `tcpdump`, `wireshark`, `telnet`, `netcat`, `nc`). +* `whitelisted_suid_binaries`: approved setuid/setgid binaries inside containers. +* `image_max_age_days: 90`: maximum image age in days before rebuild is required. +* `allowed_unused_images_count: 5`: threshold of allowed unused/cached images before image sprawl is flagged. +* `allowed_unused_images: []`: explicit whitelist of approved unused image IDs or names (e.g. rollback images). +* `allowed_latest_tag_images: []`: images allowed to use unpinned `:latest` tags. +* `signed_artifacts: []`: artifacts with `path`, `signature`, and `public_key` paths on the target; see the sample attributes. +* `dockerfile_paths: []`: optional target Dockerfiles to inspect alongside image history. ## Usage @@ -85,7 +86,7 @@ inspec exec cis-docker-benchmark -t ssh://user@hostname -i /path/to/key inspec exec cis-docker-benchmark -t ssh://user@hostname -i /path/to/key --sudo # run profile on remote host via SSH with sudo and define attribute value -inspec exec cis-docker-benchmark --attrs sample_attributes.yml +inspec exec cis-docker-benchmark --input-file sample_attributes.yml # run profile direct from inspec supermarket inspec supermarket exec dev-sec/cis-docker-benchmark -t ssh://user@hostname --key-files private_key --sudo @@ -96,7 +97,7 @@ inspec supermarket exec dev-sec/cis-docker-benchmark -t ssh://user@hostname --ke In order to verify individual controls, just provide the control ids to InSpec: ```sh -inspec exec cis-docker-benchmark --controls 'cis-docker-benchmark-1.4 cis-docker-benchmark-1.5' +inspec exec cis-docker-benchmark --controls host-1.1.3 docker-2.15 ``` ## Contributors + Kudos diff --git a/controls/container_images.rb b/controls/container_images.rb index 9a9ac8d..b8c02ee 100644 --- a/controls/container_images.rb +++ b/controls/container_images.rb @@ -21,9 +21,6 @@ title 'Container Images and Build File' -# attributes -CONTAINER_USER = input('container_user') - # check if docker exists only_if('docker not found') do command('docker').exist? @@ -37,18 +34,18 @@ Rationale: It is a good practice to run the container as a non-root user, if possible. Though user namespace mapping is now available, if a user is already defined in the container image, the container is run as that user by default and specific user namespace remapping is not required.' tag 'docker' - tag 'cis-docker-1.12.0': '4.1' - tag 'cis-docker-1.13.0': '4.1' tag 'level:1' ref 'Having non-root privileges on the host and root inside the container', url: 'https://github.com/docker/docker/issues/2918' ref 'Support for user namespaces', url: 'https://github.com/docker/docker/pull/4572' ref 'Proposal: Support for user namespaces', url: 'https://github.com/docker/docker/issues/7906' ref 'Secure Engine', url: 'https://docs.docker.com/engine/security/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(Config User)) { should_not eq nil } - its(%w(Config User)) { should eq CONTAINER_USER } + only_if('No containers are present') { !docker.containers.ids.empty? } + docker.containers.ids.each do |id| + user = docker.object(id)['Config']['User'].to_s.split(':').first.to_s + describe user do + it { should_not be_empty } + it { should_not match(/\A(?:root|0+)\z/) } end end end @@ -61,8 +58,6 @@ Rationale: Official repositories are Docker images curated and optimized by the Docker community or the vendor. There could be other potentially unsafe public repositories. You should thus exercise a lot of caution when obtaining container images.' tag 'docker' - tag 'cis-docker-1.12.0': '4.2' - tag 'cis-docker-1.13.0': '4.2' tag 'level:1' ref 'Docker Image Insecurity', url: 'https://titanous.com/posts/docker-insecurity' ref 'Docker Hub', url: 'https://hub.docker.com/' @@ -75,6 +70,9 @@ describe os_env('DOCKER_CONTENT_TRUST') do its('content') { should eq '1' } end + describe 'docker-test' do + skip 'Review the origin and contents of base images; content trust alone does not establish publisher approval.' + end end control 'docker-4.3' do @@ -85,15 +83,23 @@ Rationale: Bloating containers with unnecessary software could possibly increase the attack surface of the container. This also voids the concept of minimal and slim down versions of container images. Hence, do not install anything else apart from what is truly needed for the purpose of the container.' tag 'docker' - tag 'cis-docker-1.12.0': '4.3' - tag 'cis-docker-1.13.0': '4.3' tag 'level:1' ref 'Get Started, Part 1: Orientation and setup', url: 'https://docs.docker.com/get-started/' ref 'Slimming down your Docker containers with Alpine Linux', url: 'http://www.livewyer.com/blog/2015/02/24/slimming-down-your-docker-containers-alpine-linux' ref 'busybox', url: 'https://github.com/progrium/busybox' - describe 'docker-test' do - skip 'Do not install unnecessary packages in the container' + only_if('No running containers are present') { !docker.containers.running?.ids.empty? } + prohibited = Array(input('prohibited_packages')) + docker.containers.running?.ids.each do |id| + packages = docker_helper.container_packages(id) + describe "Container #{id} prohibited packages" do + if packages.nil? + skip "Could not query packages in container #{id}. Verify shell and dpkg/rpm/apk availability and query permissions, or review the image SBOM against prohibited_packages manually." + else + subject { packages & prohibited } + it { should be_empty } + end + end end end @@ -105,16 +111,40 @@ Rationale: Vulnerabilities are loopholes/bugs that can be exploited and security patches are updates to resolve these vulnerabilities. We can use image vulnerability scanning tools to find any kind of vulnerabilities within the images and then check for available patches to mitigate these vulnerabilities. Patches update the system to the most recent code base. Being on the current code base is important because that\'s where vendors focus on fixing problems. Evaluate the security patches before applying and follow the patching best practices. Also, it would be better if, image vulnerability scanning tools could perform binary level analysis or hash based verification instead of just version string matching.' tag 'docker' - tag 'cis-docker-1.12.0': '4.4' - tag 'cis-docker-1.13.0': '4.4' tag 'level:1' ref 'Get Started, Part 1: Orientation and setup', url: 'https://docs.docker.com/get-started/' ref 'Docker Security Scan', url: ' https://docs.docker.com/docker-cloud/builds/image-scan/' ref 'Docker Security Scanning safeguards the container content lifecycle', url: 'https://blog.docker.com/2016/05/docker-security-scanning/' ref 'Dockerfile reference', url: 'https://docs.docker.com/engine/reference/builder/' + only_if('No images are present') { !docker.images.ids.empty? } + + max_age_days = input('image_max_age_days').to_i + max_age_seconds = max_age_days * 86_400 + + docker.images.ids.each do |id| + created_str = docker.object(id)['Created'] + created_time = begin + Time.parse(created_str).to_i + rescue ArgumentError, TypeError + nil + end + next unless created_time + + age_days = ((Time.now.to_i - created_time) / 86_400.0).round(1) + describe "Image #{id} age (#{age_days} days)" do + if max_age_seconds.positive? + subject { Time.now.to_i - created_time } + it { should be <= max_age_seconds } + else + subject { age_days } + it { should be >= 0 } + end + end + end + describe 'docker-test' do - skip 'Rebuild the images to include security patches' + skip 'Verify image vulnerability scan reports and patching SLAs in your CI/CD pipeline or registry; vulnerability scanning cannot be evaluated from host evidence alone.' end end @@ -126,8 +156,6 @@ Rationale: Content trust provides the ability to use digital signatures for data sent to and received from remote Docker registries. These signatures allow client-side verification of the integrity and publisher of specific image tags. This ensures provenance of container images.' tag 'docker' - tag 'cis-docker-1.12.0': '4.5' - tag 'cis-docker-1.13.0': '4.5' tag 'level:2' ref 'Content trust in Docker', url: 'https://docs.docker.com/engine/security/trust/content_trust/' ref 'Notary', url: 'https://docs.docker.com/engine/reference/commandline/cli/#notary' @@ -146,14 +174,15 @@ Rationale: One of the important security triads is availability. Adding HEALTHCHECK instruction to your container image ensures that the docker engine periodically checks the running container instances against that instruction to ensure that the instances are still working. Based on the reported health status, the docker engine could then exit non-working containers and instantiate new ones.' tag 'docker' - tag 'cis-docker-1.12.0': '4.6' - tag 'cis-docker-1.13.0': '4.6' tag 'level:1' ref 'Add support for user-defined healthchecks', url: 'https://github.com/moby/moby/pull/22719' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(Config Healthcheck)) { should_not eq nil } + only_if('No images are present') { !docker.images.ids.empty? } + docker.images.ids.each do |id| + healthcheck = Array(docker.object(id).dig('Config', 'Healthcheck', 'Test')) + describe healthcheck do + it { should_not be_empty } + it { should_not eq ['NONE'] } end end end @@ -166,15 +195,16 @@ Rationale: Adding the update instructions in a single line on the Dockerfile will cache the update layer. Thus, when you build any image later using the same instruction, previously cached update layer will be used. This could potentially deny any fresh updates to go in the later builds.' tag 'docker' - tag 'cis-docker-1.12.0': '4.7' - tag 'cis-docker-1.13.0': '4.7' tag 'level:1' ref 'Best practices for writing Dockerfiles', url: 'https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/' ref 'caching and apt-get update', url: 'https://github.com/moby/moby/issues/3313' + only_if('No images are present') { !docker.images.ids.empty? } docker.images.ids.each do |id| - describe command("docker history --no-trunc #{id}| grep -e 'update'") do - its('stdout') { should eq '' } + updates = docker_helper.standalone_updates(docker_helper.image_history(id)) + describe "Image #{id} standalone package index updates" do + subject { updates } + it { should be_empty } end end end @@ -187,8 +217,6 @@ Rationale: setuid and setgid permissions could be used for elevating privileges. While these permissions are at times legitimately needed, these could potentially be used in privilege escalation attacks. Thus, you should consider dropping these permissions for the packages which do not need them within the images.' tag 'docker' - tag 'cis-docker-1.12.0': '4.8' - tag 'cis-docker-1.13.0': '4.8' tag 'level:2' ref 'DevSec Linux Baseline', url: 'https://github.com/dev-sec/linux-baseline' ref 'Docker Security', url: 'http://www.oreilly.com/webops-perf/free/files/docker-security.pdf' @@ -196,8 +224,14 @@ ref 'setuid - set user identity', url: 'http://man7.org/linux/man-pages/man2/setuid.2.html' ref 'setgid - set group identity', url: 'http://man7.org/linux/man-pages/man2/setgid.2.html' - describe 'docker-test' do - skip 'Use DevSec Linux Baseline in Container' + only_if('No running containers are present') { !docker.containers.running?.ids.empty? } + whitelisted = Array(input('whitelisted_suid_binaries')) + docker.containers.running?.ids.each do |id| + suid_binaries = docker_helper.container_suid_sgid_binaries(id) + describe "Container #{id} unapproved setuid/setgid binaries" do + subject { suid_binaries - whitelisted } + it { should be_empty } + end end end @@ -209,14 +243,15 @@ Rationale: COPY instruction just copies the files from the local host machine to the container file system. ADD instruction potentially could retrieve files from remote URLs and perform operations such as unpacking. Thus, ADD instruction introduces risks such as adding malicious files from URLs without scanning and unpacking procedure vulnerabilities.' tag 'docker' - tag 'cis-docker-1.12.0': '4.9' - tag 'cis-docker-1.13.0': '4.9' tag 'level:1' ref 'Best practices for writing Dockerfiles', url: 'https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/' + only_if('No images are present') { !docker.images.ids.empty? } docker.images.ids.each do |id| - describe command("docker history --no-trunc #{id}| grep 'ADD'") do - its('stdout') { should eq '' } + additions = docker_helper.image_history(id).select { |instruction| instruction.match?(%r{\A(?:/\S+\s+-c\s+#\(nop\)\s+)?ADD\s}) } + describe "Image #{id} ADD instructions" do + subject { additions } + it { should be_empty } end end end @@ -229,15 +264,28 @@ Rationale: Dockerfiles could be backtracked easily by using native Docker commands such as docker history and various tools and utilities. Also, as a general practice, image publishers provide Dockerfiles to build the credibility for their images. Hence, the secrets within these Dockerfiles could be easily exposed and potentially be exploited.' tag 'docker' - tag 'cis-docker-1.12.0': '4.10' - tag 'cis-docker-1.13.0': '4.10' tag 'level:1' ref 'Secrets: write-up best practices, do\'s and don\'ts, roadmap', url: 'https://github.com/moby/moby/issues/13490' ref 'The Twelve-Factor App', url: 'https://12factor.net/config' ref 'Twitter\'s Vine Source code dump', url: 'https://avicoder.me/2016/07/22/Twitter-Vine-Source-code-dump/' - describe 'docker-test' do - skip 'Manually verify that you have not used secrets in images' + only_if('No images are present') { !docker.images.ids.empty? } + docker.images.ids.each do |id| + detected_secrets = docker_helper.image_secrets(docker_helper.image_history(id)) + describe "Image #{id} exposed secrets in build history" do + subject { detected_secrets } + it { should be_empty } + end + end + input('dockerfile_paths').each do |path| + describe file(path) do + it { should exist } + end + findings = docker_helper.image_secrets(file(path).content.to_s.lines) + describe "Dockerfile #{path} exposed secrets" do + subject { findings } + it { should be_empty } + end end end @@ -249,14 +297,58 @@ Rationale: Verifying authenticity of the packages is essential for building a secure container image. Tampered packages could potentially be malicious or have some known vulnerabilities that could be exploited.' tag 'docker' - tag 'cis-docker-1.13.0': '4.11' - tag 'level:1' + tag 'level:2' ref 'Docker Security', url: 'http://www.oreilly.com/webops-perf/free/files/docker-security.pdf' ref 'Dockerfile HTTPD', url: 'https://github.com/docker-library/httpd/blob/12bf8c8883340c98b3988a7bade8ef2d0d6dcf8a/2.4/Dockerfile' ref 'Dockerfile PHP Alpine', url: 'https://github.com/docker-library/php/blob/d8a4ccf4d620ec866d5b42335b699742df08c5f0/7.0/alpine/Dockerfile' ref 'Product Signing (GPG) Keys', url: 'https://access.redhat.com/security/team/key' - describe 'docker-test' do - skip 'Manually verify that you installed verified packages' + only_if('No images are present') { !docker.images.ids.empty? } + docker.images.ids.each do |id| + insecure_flags = docker_helper.insecure_package_flags(docker_helper.image_history(id)) + describe "Image #{id} unverified package installation instructions" do + subject { insecure_flags } + it { should be_empty } + end + end +end + +control 'docker-4.12' do + impact 1.0 + title 'Validate all signed artifacts' + desc 'Validate signatures on signed artifacts before using them in container images. + + Rationale: Signature verification helps establish that an artifact came from the expected publisher and has not been modified.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + artifacts = Array(input('signed_artifacts')) + if artifacts.empty? + describe 'docker-test' do + skip 'Verify artifact cryptographic signatures and provenance in your build pipeline prior to registry upload. (To automate local verification on this host, populate the signed_artifacts input).' + end + else + artifacts.each do |entry| + artifact = entry.transform_keys(&:to_s) + paths = %w(path signature public_key).map { |key| artifact[key].to_s } + describe "Signed artifact #{artifact['path']} verification paths" do + subject { paths } + it { should all(match(%r{\A/})) } + end + next unless paths.all? { |path| path.start_with?('/') } + + paths.each do |path| + describe file(path) do + it { should be_file } + end + end + payload, signature, public_key = paths.map { |path| Shellwords.escape(path) } + describe command("openssl dgst -sha256 -verify #{public_key} -signature #{signature} #{payload}") do + its('exit_status') { should eq 0 } + its('stdout.strip') { should eq 'Verified OK' } + end + end end end diff --git a/controls/container_runtime.rb b/controls/container_runtime.rb index dbea22e..65f1239 100644 --- a/controls/container_runtime.rb +++ b/controls/container_runtime.rb @@ -21,17 +21,28 @@ title 'Container Runtime' -# attributes -CONTAINER_CAPADD = input('container_capadd') -APP_ARMOR_PROFILE = input('app_armor_profile') -SELINUX_PROFILE = input('selinux_profile') - # check if docker exists only_if('docker not found') do command('docker').exist? end control 'docker-5.1' do + impact 1.0 + title 'Do not enable swarm mode, if not needed' + desc 'Do not enable swarm mode on a docker engine instance unless needed. + + Rationale: By default, a Docker engine instance will not listen on any network ports, with all communications with the client coming over the Unix socket. When Docker swarm mode is enabled on a docker engine instance, multiple network ports are opened on the system and made available to other systems on the network for the purposes of cluster management and node communications. Opening network ports on a system increase its attack surface and this should be avoided unless required.' + + tag 'docker' + tag 'level:1' + ref 'docker swarm init', url: 'https://docs.docker.com/engine/reference/commandline/swarm_init/' + + describe docker.info do + its('Swarm.LocalNodeState') { should eq input('swarm_mode') } + end +end + +control 'docker-5.2' do impact 1.0 title 'Verify AppArmor Profile, if applicable' desc 'AppArmor is an effective and easy-to-use Linux application security system. It is available on quite a few Linux distributions by default such as Debian and Ubuntu. @@ -39,23 +50,24 @@ Rationale: AppArmor protects the Linux OS and applications from various threats by enforcing security policy which is also known as AppArmor profile. You can create your own AppArmor profile for containers or use the Docker\'s default AppArmor profile. This would enforce security policies on the containers as defined in the profile.' tag 'docker' - tag 'cis-docker-1.12.0': '5.1' - tag 'cis-docker-1.13.0': '5.1' tag 'level:1' ref 'Docker Security', url: 'https://docs.docker.com/engine/security/security/' ref 'Secure Engine', url: 'https://docs.docker.com/engine/security/' ref 'AppArmor security profiles for Docker', url: 'https://docs.docker.com/engine/security/apparmor/' - only_if { %w(ubuntu debian).include? os[:name] } - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(['AppArmorProfile']) { should include(APP_ARMOR_PROFILE) } - its(['AppArmorProfile']) { should_not eq nil } + only_if('No containers are present') { !docker.containers.ids.empty? } + + only_if('AppArmor is not enabled on the host') { file('/sys/module/apparmor/parameters/enabled').content.to_s.strip == 'Y' } + docker.containers.ids.each do |id| + describe "Container #{id} AppArmorProfile" do + subject { docker.object(id)['AppArmorProfile'] } + it { should_not be_empty } + it { should_not eq 'unconfined' } end end end -control 'docker-5.2' do +control 'docker-5.3' do impact 1.0 title 'Verify SELinux security options, if applicable' desc 'SELinux is an effective and easy-to-use Linux application security system. It is available on quite a few Linux distributions by default such as Red Hat and Fedora. @@ -63,8 +75,6 @@ Rationale: SELinux provides a Mandatory Access Control (MAC) system that greatly augments the default Discretionary Access Control (DAC) model. You can thus add an extra layer of safety by enabling SELinux on your Linux host, if applicable.' tag 'docker' - tag 'cis-docker-1.12.0': '5.2' - tag 'cis-docker-1.13.0': '5.2' tag 'level:2' ref 'Docker Security', url: 'https://docs.docker.com/engine/security/security/' ref 'Secure Engine', url: 'https://docs.docker.com/engine/security/' @@ -73,20 +83,22 @@ ref 'Bug: selinux break docker user namespace', url: 'https://bugzilla.redhat.com/show_bug.cgi?id=1312665' ref 'Security-Enhanced Linux', url: 'https://docs-old.fedoraproject.org/en-US/Fedora/13/html/Security-Enhanced_Linux/' - only_if { %w(centos redhat).include? os[:name] } - describe json('/etc/docker/daemon.json') do - its(['selinux-enabled']) { should eq(true) } - end + only_if('No containers are present') { !docker.containers.ids.empty? } - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig SecurityOpt)) { should_not eq nil } - its(%w(HostConfig SecurityOpt)) { should include(SELINUX_PROFILE) } + only_if('SELinux is not enabled on the host') { file('/sys/fs/selinux/enforce').exist? } + docker.containers.ids.each do |id| + describe "Container #{id} ProcessLabel" do + subject { docker.object(id)['ProcessLabel'] } + it { should_not be_empty } + end + describe "Container #{id} MountLabel" do + subject { docker.object(id)['MountLabel'] } + it { should_not be_empty } end end end -control 'docker-5.3' do +control 'docker-5.4' do impact 1.0 title 'Restrict Linux Kernel Capabilities within containers' desc 'By default, Docker starts containers with a restricted set of Linux Kernel Capabilities. It means that any process may be granted the required capabilities instead of root access. Using Linux Kernel Capabilities, the processes do not have to run as root for almost all the specific areas where root privileges are usually needed. @@ -96,24 +108,30 @@ For example, capabilities such as below are usually not needed for container process: NET_ADMIN, SYS_ADMIN, SYS_MODULE' tag 'docker' - tag 'cis-docker-1.12.0': '5.3' - tag 'cis-docker-1.13.0': '5.3' tag 'level:1' ref 'Docker Security', url: 'https://docs.docker.com/engine/security/security/' ref 'Secure Engine', url: 'https://docs.docker.com/engine/security/' ref 'capabilities - overview of Linux capabilities', url: 'http://man7.org/linux/man-pages/man7/capabilities.7.html' ref 'Docker Security Book', url: 'http://www.oreilly.com/webops-perf/free/files/docker-security.pdf' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig CapDrop)) { should include(/all/) } - its(%w(HostConfig CapDrop)) { should_not eq nil } - its(%w(HostConfig CapAdd)) { should eq CONTAINER_CAPADD } + only_if('No containers are present') { !docker.containers.ids.empty? } + approved = input('container_capadd').split(',').map { |capability| capability.strip.upcase.delete_prefix('CAP_') }.reject(&:empty?) + docker.containers.ids.each do |id| + config = docker.object(id)['HostConfig'] + added = Array(config['CapAdd']).map { |capability| capability.upcase.delete_prefix('CAP_') } + dropped = Array(config['CapDrop']).map { |capability| capability.upcase.delete_prefix('CAP_') } + describe "Container #{id} unapproved added capabilities" do + subject { added - approved } + it { should be_empty } + end + describe "Container #{id} NET_RAW capability" do + subject { !approved.include?('NET_RAW') && (added.include?('NET_RAW') || (!dropped.include?('NET_RAW') && (added.include?('ALL') || !dropped.include?('ALL')))) } + it { should eq false } end end end -control 'docker-5.4' do +control 'docker-5.5' do impact 1.0 title 'Do not use privileged containers' desc 'Using the --privileged flag gives all Linux Kernel Capabilities to the container thus overwriting the --cap-add and --cap-drop flags. Ensure that it is not used. @@ -121,20 +139,21 @@ Rationale: The --privileged flag gives all capabilities to the container, and it also lifts all the limitations enforced by the device cgroup controller. In other words, the container can then do almost everything that the host can do. This flag exists to allow special use-cases, like running Docker within Docker.' tag 'docker' - tag 'cis-docker-1.12.0': '5.4' - tag 'cis-docker-1.13.0': '5.4' tag 'level:1' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig Privileged)) { should eq false } - its(%w(HostConfig Privileged)) { should_not eq true } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.Privileged" do + subject { docker.object(id).dig('HostConfig', 'Privileged') } + it { should eq false } + it { should_not eq true } end end end -control 'docker-5.5' do +control 'docker-5.6' do impact 1.0 title 'Do not mount sensitive host system directories on containers' desc 'Sensitive host system directories such as \'/, /boot, /dev, /etc, /lib, /proc, /sys, /usr\' should not be allowed to be mounted as container volumes especially in read-write mode. @@ -142,29 +161,21 @@ Rationale: If sensitive directories are mounted in read-write mode, it would be possible to make changes to files within those sensitive directories. The changes might bring down security implications or unwarranted changes that could put the Docker host in compromised state.' tag 'docker' - tag 'cis-docker-1.12.0': '5.5' - tag 'cis-docker-1.13.0': '5.5' tag 'level:1' ref 'Use volumes', url: 'https://docs.docker.com/engine/admin/volumes/volumes/' - docker.containers.running?.ids.each do |id| - info = docker.object(id) - info['Mounts'].each do |mounts| - describe mounts['Source'] do - it { should_not eq '/' } - it { should_not match(%r{/boot}) } - it { should_not match(%r{/dev}) } - it { should_not match(%r{/etc}) } - it { should_not match(%r{/lib}) } - it { should_not match(%r{/proc}) } - it { should_not match(%r{/sys}) } - it { should_not match(%r{/usr}) } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + docker.object(id)['Mounts'].each do |mount| + describe mount['Source'] do + it { should_not match(%r{\A/(?:$|(?:boot|dev|etc|lib|lib64|proc|sys|usr)(?:/|$))}) } end end end end -control 'docker-5.6' do +control 'docker-5.7' do impact 1.0 title 'Do not run ssh within containers' desc 'SSH server should not be running within the container. You should SSH into the Docker host, and use nsenter tool to enter a container from a remote host. @@ -178,20 +189,20 @@ It is possible to have shell access to a container without using SSH, the needlessly increasing the complexity of security management should be avoided.' tag 'docker' - tag 'cis-docker-1.12.0': '5.6' - tag 'cis-docker-1.13.0': '5.6' tag 'level:1' ref 'Why you don\'t need to run SSHd in your Docker containers', url: 'https://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/' + only_if('No running containers are present') { !docker.containers.running?.ids.empty? } + docker.containers.running?.ids.each do |id| - execute_command = "docker exec #{id} ps -e" - describe command(execute_command) do - its('stdout') { should_not match(/ssh/) } + describe command("docker top #{id} -eo comm") do + its('exit_status') { should eq 0 } + its('stdout') { should_not match(/^\s*sshd(?:\s|$)/) } end end end -control 'docker-5.7' do +control 'docker-5.8' do impact 1.0 title 'Do not map privileged ports within containers' desc 'The TCP/IP port numbers below 1024 are considered privileged ports. Normal users and processes are not allowed to use them for various security reasons. Docker allows a container port to be mapped to a privileged port. @@ -199,12 +210,12 @@ Rationale: By default, if the user does not specifically declare the container port to host port mapping, Docker automatically and correctly maps the container port to one available in 49153-65535 block on the host. But, Docker allows a container port to be mapped to a privileged port on the host if the user explicitly declared it. This is so because containers are executed with NET_BIND_SERVICE Linux kernel capability that does not restrict the privileged port mapping. The privileged ports receive and transmit various sensitive and privileged data. Allowing containers to use them can bring serious implications.' tag 'docker' - tag 'cis-docker-1.12.0': '5.7' - tag 'cis-docker-1.13.0': '5.7' tag 'level:1' ref 'Bind container ports to the host', url: 'https://docs.docker.com/engine/userguide/networking/default_network/binding/' ref 'Why putting SSH on another port than 22 is bad idea', url: 'https://www.adayinthelifeof.nl/2012/03/12/why-putting-ssh-on-another-port-than-22-is-bad-idea/' + only_if('No running containers are present') { !docker.containers.running?.ids.empty? } + docker.containers.running?.ids.each do |id| container_info = docker.object(id) next if container_info['NetworkSettings']['Ports'].nil? @@ -213,7 +224,7 @@ next if hosts.nil? hosts.each do |host| - describe host['HostPort'].to_i.between?(1, 1024) do + describe host['HostPort'].to_i.between?(1, 1023) do it { should eq false } end end @@ -221,7 +232,7 @@ end end -control 'docker-5.8' do +control 'docker-5.9' do impact 1.0 title 'Open only needed ports on container' desc 'Dockerfile for a container image defines the ports to be opened by default on a container instance. The list of ports may or may not be relevant to the application you are running within the container. @@ -229,13 +240,23 @@ Rationale: A container can be run just with the ports defined in the Dockerfile for its image or can be arbitrarily passed run time parameters to open a list of ports. Additionally, Overtime, Dockerfile may undergo various changes and the list of exposed ports may or may not be relevant to the application you are running within the container. Opening unneeded ports increase the attack surface of the container and the containerized application. As a recommended practice, do not open unneeded ports.' tag 'docker' - tag 'cis-docker-1.12.0': '5.8' - tag 'cis-docker-1.13.0': '5.8' tag 'level:1' ref 'Bind container ports to the host', url: 'https://docs.docker.com/engine/userguide/networking/default_network/binding/' + + only_if('No running containers are present') { !docker.containers.running?.ids.empty? } + approved_ports = Array(input('approved_container_ports')).map(&:to_s) + docker.containers.running?.ids.each do |id| + ports_hash = docker.object(id).dig('NetworkSettings', 'Ports') || {} + published = ports_hash.select { |_port, bindings| !bindings.nil? && !bindings.empty? } + unapproved = published.keys.reject { |port| approved_ports.include?(port) || approved_ports.include?(port.split('/').first) } + describe "Container #{id} unapproved published ports" do + subject { unapproved } + it { should be_empty } + end + end end -control 'docker-5.9' do +control 'docker-5.10' do impact 1.0 title 'Do not share the host\'s network namespace' desc 'The networking mode on a container when set to \'--net=host\', skips placing the container inside separate network stack. In essence, this choice tells Docker to not containerize the container\'s networking. This would network-wise mean that the container lives "outside" in the main Docker host and has full access to its network interfaces. @@ -243,20 +264,21 @@ Rationale: This is potentially dangerous. It allows the container process to open low-numbered ports like any other root process. It also allows the container to access network services like D-bus on the Docker host. Thus, a container process can potentially do unexpected things such as shutting down the Docker host. You should not use this option.' tag 'docker' - tag 'cis-docker-1.12.0': '5.9' - tag 'cis-docker-1.13.0': '5.9' tag 'level:1' ref 'Docker container networking', url: 'https://docs.docker.com/engine/userguide/networking/' ref 'Rebooting within docker container actually reboots the host', url: 'https://github.com/docker/docker/issues/6401' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig NetworkMode)) { should_not eq 'host' } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.NetworkMode" do + subject { docker.object(id).dig('HostConfig', 'NetworkMode') } + it { should_not eq 'host' } end end end -control 'docker-5.10' do +control 'docker-5.11' do impact 1.0 title 'Limit memory usage for container' desc 'By default, all containers on a Docker host share the resources equally. By using the resource management capabilities of Docker host, such as memory limit, you can control the amount of memory that a container may consume. @@ -264,21 +286,22 @@ Rationale: By default, container can use all of the memory on the host. You can use memory limit mechanism to prevent a denial of service arising from one container consuming all of the host’s resources such that other containers on the same host cannot perform their intended functions. Having no limit on memory can lead to issues where one container can easily make the whole system unstable and as a result unusable.' tag 'docker' - tag 'cis-docker-1.12.0': '5.10' - tag 'cis-docker-1.13.0': '5.10' tag 'level:1' ref 'Resource management in Docker', url: 'https://goldmann.pl/blog/2014/09/11/resource-management-in-docker/' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' ref 'Runtime metrics', url: 'https://docs.docker.com/engine/admin/runmetrics/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig Memory)) { should_not eq 0 } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.Memory" do + subject { docker.object(id).dig('HostConfig', 'Memory') } + it { should cmp > 0 } end end end -control 'docker-5.11' do +control 'docker-5.12' do impact 1.0 title 'Set container CPU priority appropriately' desc 'By default, all containers on a Docker host share the resources equally. By using the resource management capabilities of Docker host, such as CPU shares, you can control the host CPU resources that a container may consume. @@ -286,22 +309,23 @@ Rationale: By default, CPU time is divided between containers equally. If it is desired, to control the CPU time amongst the container instances, you can use CPU sharing feature. CPU sharing allows to prioritize one container over the other and forbids the lower priority container to claim CPU resources more often. This ensures that the high priority containers are served better.' tag 'docker' - tag 'cis-docker-1.12.0': '5.11' - tag 'cis-docker-1.13.0': '5.11' tag 'level:1' ref 'Resource management in Docker', url: 'https://goldmann.pl/blog/2014/09/11/resource-management-in-docker/' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' ref 'Runtime metrics', url: 'https://docs.docker.com/engine/admin/runmetrics/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig CpuShares)) { should_not eq 0 } - its(%w(HostConfig CpuShares)) { should_not eq 1024 } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.CpuShares" do + subject { docker.object(id).dig('HostConfig', 'CpuShares') } + it { should cmp > 0 } + it { should_not eq 1024 } end end end -control 'docker-5.12' do +control 'docker-5.13' do impact 1.0 title 'Mount container\'s root filesystem as read only' desc 'The container\'s root file system should be treated as a \'golden image\' and any writes to the root filesystem should be avoided. You should explicitly define a container volume for writing. @@ -314,19 +338,20 @@ Ability to use a purely volume based backup without backing up anything from theinstance' tag 'docker' - tag 'cis-docker-1.12.0': '5.12' - tag 'cis-docker-1.13.0': '5.12' tag 'level:1' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig ReadonlyRootfs)) { should eq true } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.ReadonlyRootfs" do + subject { docker.object(id).dig('HostConfig', 'ReadonlyRootfs') } + it { should eq true } end end end -control 'docker-5.13' do +control 'docker-5.14' do impact 1.0 title 'Bind incoming container traffic to a specific host interface' desc 'By default, Docker containers can make connections to the outside world, but the outside world cannot connect to containers. Each outgoing connection will appear to originate from one of the host machine\'s own IP addresses. Only allow container services to be contacted through a specific external interface on the host machine. @@ -334,11 +359,11 @@ Rationale: If you have multiple network interfaces on your host machine, the container can accept connections on the exposed ports on any network interface. This might not be desired and may not be secured. Many a times a particular interface is exposed externally and services such as intrusion detection, intrusion prevention, firewall, load balancing, etc. are run on those interfaces to screen incoming public traffic. Hence, you should not accept incoming connections on any interface. You should only allow incoming connections from a particular external interface.' tag 'docker' - tag 'cis-docker-1.12.0': '5.13' - tag 'cis-docker-1.13.0': '5.13' tag 'level:1' ref 'Docker container networking', url: 'https://docs.docker.com/engine/userguide/networking/' + only_if('No running containers are present') { !docker.containers.running?.ids.empty? } + docker.containers.running?.ids.each do |id| container_info = docker.object(id) next if container_info['NetworkSettings']['Ports'].nil? @@ -347,15 +372,18 @@ next if hosts.nil? hosts.each do |host| - describe host['HostIp'].to_i.between?(1, 1024) do + describe host['HostIp'] do it { should_not eq '0.0.0.0' } + it { should_not eq '::' } + it { should_not eq '' } + it { should_not eq nil } end end end end end -control 'docker-5.14' do +control 'docker-5.15' do impact 1.0 title 'Set the \'on-failure\' container restart policy to 5' desc 'Using the \'--restart\' flag in \'docker run\' command you can specify a restart policy for how a container should or should not be restarted on exit. You should choose the \'on-failure\' restart policy and limit the restart attempts to 5. @@ -363,25 +391,28 @@ Rationale: If you indefinitely keep trying to start the container, it could possibly lead to a denial of service on the host. It could be an easy way to do a distributed denial of service attack especially if you have many containers on the same host. Additionally, ignoring the exit status of the container and \'always\' attempting to restart the container leads to non-investigation of the root cause behind containers getting terminated. If a container gets terminated, you should investigate on the reason behind it instead of just attempting to restart it indefinitely. Thus, it is recommended to use \'on-failure\' restart policy and limit it to maximum of 5 restart attempts.' tag 'docker' - tag 'cis-docker-1.12.0': '5.14' - tag 'cis-docker-1.13.0': '5.14' tag 'level:1' ref 'Start containers automatically', url: 'https://docs.docker.com/engine/admin/start-containers-automatically/' - docker.containers.running?.ids.each do |id| + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + restart_policy = docker.object(id).dig('HostConfig', 'RestartPolicy') describe.one do - describe docker.object(id) do - its(%w(HostConfig RestartPolicy Name)) { should eq 'no' } + describe "Container #{id} HostConfig.RestartPolicy.Name" do + subject { restart_policy['Name'] } + it { should eq 'no' } end - describe docker.object(id) do - its(%w(HostConfig RestartPolicy Name)) { should eq 'on-failure' } - its(%w(HostConfig RestartPolicy MaximumRetryCount)) { should eq 5 } + describe "Container #{id} HostConfig.RestartPolicy" do + subject { restart_policy } + its('Name') { should eq 'on-failure' } + its('MaximumRetryCount') { should be_between(1, 5).inclusive } end end end end -control 'docker-5.15' do +control 'docker-5.16' do impact 1.0 title 'Do not share the host\'s process namespace' desc 'Process ID (PID) namespaces isolate the process ID number space, meaning that processes in different PID namespaces can have the same PID. This is process level isolation between containers and the host. @@ -389,20 +420,21 @@ Rationale: PID namespace provides separation of processes. The PID Namespace removes the view of the system processes, and allows process ids to be reused including PID 1. If the host\'s PID namespace is shared with the container, it would basically allow processes within the container to see all of the processes on the host system. This breaks the benefit of process level isolation between the host and the containers. Someone having access to the container can eventually know all the processes running on the host system and can even kill the host system processes from within the container. This can be catastrophic. Hence, do not share the host\'s process namespace with the containers.' tag 'docker' - tag 'cis-docker-1.12.0': '5.15' - tag 'cis-docker-1.13.0': '5.15' tag 'level:1' ref 'PID settings (–pid)', url: 'https://docs.docker.com/engine/reference/run/#pid-equivalent' ref 'pid_namespaces - overview of Linux PID namespaces', url: 'http://man7.org/linux/man-pages/man7/pid_namespaces.7.html' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig PidMode)) { should_not eq 'host' } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.PidMode" do + subject { docker.object(id).dig('HostConfig', 'PidMode') } + it { should_not eq 'host' } end end end -control 'docker-5.16' do +control 'docker-5.17' do impact 1.0 title 'Do not share the host\'s IPC namespace' desc 'IPC (POSIX/SysV IPC) namespace provides separation of named shared memory segments, semaphores and message queues. IPC namespace on the host thus should not be shared with the containers and should remain isolated. @@ -410,20 +442,21 @@ Rationale: IPC namespace provides separation of IPC between the host and containers. If the host\'s IPC namespace is shared with the container, it would basically allow processes within the container to see all of the IPC on the host system. This breaks the benefit of IPC level isolation between the host and the containers. Someone having access to the container can eventually manipulate the host IPC. This can be catastrophic. Hence, do not share the host\'s IPC namespace with the containers.' tag 'docker' - tag 'cis-docker-1.12.0': '5.16' - tag 'cis-docker-1.13.0': '5.16' tag 'level:1' ref 'IPC settings (–ipc)', url: 'https://docs.docker.com/engine/reference/run/#ipc-settings---ipc' ref 'namespaces - overview of Linux namespaces', url: 'http://man7.org/linux/man-pages/man7/namespaces.7.html' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig IpcMode)) { should_not eq 'host' } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.IpcMode" do + subject { docker.object(id).dig('HostConfig', 'IpcMode') } + it { should_not eq 'host' } end end end -control 'docker-5.17' do +control 'docker-5.18' do impact 1.0 title 'Do not directly expose host devices to containers' desc 'Host devices can be directly exposed to containers at runtime. Do not directly expose host devices to containers especially for containers that are not trusted. @@ -435,19 +468,20 @@ m - mknod allowed' tag 'docker' - tag 'cis-docker-1.12.0': '5.17' - tag 'cis-docker-1.13.0': '5.17' tag 'level:1' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig Devices)) { should be_empty } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.Devices" do + subject { docker.object(id).dig('HostConfig', 'Devices') } + it { should be_empty } end end end -control 'docker-5.18' do +control 'docker-5.19' do impact 1.0 title 'Override default ulimit at runtime only if needed' desc 'The default ulimit is set at the Docker daemon level. However, you may override the default ulimit setting, if needed, during container runtime. @@ -455,21 +489,35 @@ Rationale: ulimit provides control over the resources available to the shell and to processes started by it. Setting system resource limits judiciously saves you from many disasters such as a fork bomb. Sometimes, even friendly users and legitimate processes can overuse system resources and in-turn can make the system unusable. The default ulimit set at the Docker daemon level should be honored. If the default ulimit settings are not appropriate for a particular container instance, you may override them as an exception. But, do not make this a practice. If most of the container instances are overriding default ulimit settings, consider changing the default ulimit settings to something that is appropriate for your needs.' tag 'docker' - tag 'cis-docker-1.12.0': '5.18' - tag 'cis-docker-1.13.0': '5.18' tag 'level:1' ref 'docker run', url: 'https://docs.docker.com/engine/reference/commandline/run/' ref 'Command: man setrlimit' ref 'Docker Security Book', url: 'http://www.oreilly.com/webops-perf/free/files/docker-security.pdf' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig Ulimits)) { should eq nil } + only_if('No containers are present') { !docker.containers.ids.empty? } + docker.containers.ids.each do |id| + ulimits = Array(docker.object(id).dig('HostConfig', 'Ulimits')) + if ulimits.empty? + describe "Container #{id} runtime ulimits" do + subject { ulimits } + it { should be_empty } + end + else + ulimits.each do |limit| + describe "Container #{id} runtime ulimit #{limit['Name']} soft limit" do + subject { limit['Soft'].to_i } + it { should cmp >= 0 } + end + describe "Container #{id} runtime ulimit #{limit['Name']} hard limit" do + subject { limit['Hard'].to_i } + it { should cmp >= limit['Soft'].to_i } + end + end end end end -control 'docker-5.19' do +control 'docker-5.20' do impact 1.0 title 'Do not set mount propagation mode to shared' desc 'Mount propagation mode allows mounting volumes in shared, slave or private mode on a container. Do not use shared mount propagation mode until needed. @@ -477,22 +525,23 @@ Rationale: A shared mount is replicated at all mounts and the changes made at any mount point are propagated to all mounts. Mounting a volume in shared mode does not restrict any other container to mount and make changes to that volume. This might be catastrophic if the mounted volume is sensitive to changes. Do not set mount propagation mode to shared until needed.' tag 'docker' - tag 'cis-docker-1.12.0': '5.19' - tag 'cis-docker-1.13.0': '5.19' tag 'level:1' ref 'Capability to specify per volume mount propagation mode', url: 'https://github.com/docker/docker/pull/17034' ref 'Docker run reference', url: 'https://docs.docker.com/engine/reference/run/' ref 'Shared Subtrees', url: 'https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt' - docker.containers.running?.ids.each do |id| - raw = command("docker inspect --format '{{range $mnt := .Mounts}} {{json $mnt.Propagation}} {{end}}' #{id}").stdout - describe raw.delete("\n").delete('\"').delete(' ') do - it { should_not eq 'shared' } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + docker.object(id)['Mounts'].each do |mount| + describe mount['Propagation'] do + it { should_not be_in %w(shared rshared) } + end end end end -control 'docker-5.20' do +control 'docker-5.21' do impact 1.0 title 'Do not share the host\'s UTS namespace' desc 'UTS namespaces provide isolation of two system identifiers: the hostname and the NIS domain name. It is used for setting the hostname and the domain that is visible to running processes in that namespace. Processes running within containers do not typically require to know hostname and domain name. Hence, the namespace should not be shared with the host. @@ -500,20 +549,21 @@ Rationale: Sharing the UTS namespace with the host provides full permission to the container to change the hostname of the host. This is insecure and should not be allowed.' tag 'docker' - tag 'cis-docker-1.12.0': '5.20' - tag 'cis-docker-1.13.0': '5.20' tag 'level:1' ref 'Docker run reference', url: 'https://docs.docker.com/engine/reference/run/' ref 'namespaces - overview of Linux namespaces', url: ' http://man7.org/linux/man-pages/man7/namespaces.7.html' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig UTSMode)) { should_not eq 'host' } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.UTSMode" do + subject { docker.object(id).dig('HostConfig', 'UTSMode') } + it { should_not eq 'host' } end end end -control 'docker-5.21' do +control 'docker-5.22' do impact 1.0 title 'Do not disable default seccomp profile' desc 'Seccomp filtering provides a means for a process to specify a filter for incoming system calls. The default Docker seccomp profile disables 44 system calls, out of 313. It should not be disabled unless it hinders your container application usage. @@ -521,8 +571,6 @@ Rationale: A large number of system calls are exposed to every userland process with many of them going unused for the entire lifetime of the process. Most of the applications do not need all the system calls and thus benefit by having a reduced set of available system calls. The reduced set of system calls reduces the total kernel surface exposed to the application and thus improvises application security.' tag 'docker' - tag 'cis-docker-1.12.0': '5.21' - tag 'cis-docker-1.13.0': '5.21' tag 'level:1' ref 'New Docker Security Features and What They Mean: Seccomp Profiles', url: 'http://blog.aquasec.com/new-docker-security-features-and-what-they-mean-seccomp-profiles' ref 'Docker run reference', url: 'https://docs.docker.com/engine/reference/run/' @@ -531,15 +579,19 @@ ref 'SECure COMPuting with filters', url: 'https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt' ref 'Capability to specify per volume mount propagation mode', url: 'https://github.com/moby/moby/pull/17034' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig SecurityOpt)) { should include(/seccomp/) } - its(%w(HostConfig SecurityOpt)) { should_not include(/seccomp[=|:]unconfined/) } + only_if('No containers are present') { !docker.containers.ids.empty? } + + describe docker.info do + its('SecurityOptions') { should include(/name=seccomp(?:,|$)/) } + end + docker.containers.ids.each do |id| + describe Array(docker.object(id)['HostConfig']['SecurityOpt']) do + it { should_not include(/\Aseccomp[=:]unconfined\z/) } end end end -control 'docker-5.22' do +control 'docker-5.23' do impact 1.0 title 'Do not docker exec commands with privileged option' desc 'Do not docker exec with --privileged option. @@ -547,35 +599,61 @@ Rationale: Using --privileged option in docker exec gives extended Linux capabilities to the command. This could potentially be insecure and unsafe to do especially when you are running containers with dropped capabilities or with enhanced restrictions.' tag 'docker' - tag 'cis-docker-1.12.0': '5.22' - tag 'cis-docker-1.13.0': '5.22' tag 'level:2' ref 'docker exec', url: 'https://docs.docker.com/engine/reference/commandline/exec/' - describe command('ausearch --input-logs -k docker | grep exec | grep privileged').stdout do - it { should be_empty } + describe service('auditd') do + it { should be_running } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + end + describe docker_helper.docker_audit_rule?(command('auditctl -l').stdout, input('docker_cli_path')) do + it { should eq true } + end + audit = docker_helper.docker_exec_audit('privileged') + describe 'Docker exec audit search' do + subject { audit['error'] } + it { should be_nil } + end + if audit['error'].nil? + describe audit['events'] do + it { should be_empty } + end end end -control 'docker-5.23' do +control 'docker-5.24' do impact 1.0 - title 'Do not docker exec commands with user option' - desc 'Do not docker exec with --user option. - - Rationale: Using --user option in docker exec executes the command within the container as that user. This could potentially be insecure and unsafe to do especially when you are running containers with dropped capabilities or with enhanced restrictions. For example, suppose your container is running as tomcat user (or any other non-root user), it would be possible to run a command through docker exec as root with --user=root option. This could potentially be dangerous.' + title 'Do not docker exec commands with root user option' + desc 'Do not use docker exec to run commands as root (root or UID 0) within containers configured to run as a non-root user.' tag 'docker' - tag 'cis-docker-1.12.0': '5.23' - tag 'cis-docker-1.13.0': '5.23' tag 'level:2' ref 'docker exec', url: 'https://docs.docker.com/engine/reference/commandline/exec/' - describe command('ausearch --input-logs -k docker | grep exec | grep user').stdout do - it { should be_empty } + describe service('auditd') do + it { should be_running } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + end + describe docker_helper.docker_audit_rule?(command('auditctl -l').stdout, input('docker_cli_path')) do + it { should eq true } + end + audit = docker_helper.docker_exec_audit('user') + describe 'Docker exec audit search' do + subject { audit['error'] } + it { should be_nil } + end + if audit['error'].nil? + describe audit['events'] do + it { should be_empty } + end end end -control 'docker-5.24' do +control 'docker-5.25' do impact 1.0 title 'Confirm cgroup usage' desc 'It is possible to attach to a particular cgroup on container run. Confirming cgroup usage would ensure that containers are running under defined cgroups. @@ -583,20 +661,21 @@ Rationale: System administrators typically define cgroups under which containers are supposed to run. Even if cgroups are not explicitly defined by the system administrators, containers run under docker cgroup by default. At run-time, it is possible to attach to a different cgroup other than the one that was expected to be used. This usage should be monitored and confirmed. By attaching to a different cgroup than the one that is expected, excess permissions and resources might be granted to the container and thus, can prove to be unsafe.' tag 'docker' - tag 'cis-docker-1.12.0': '5.24' - tag 'cis-docker-1.13.0': '5.24' tag 'level:1' ref 'Specify custom cgroups', url: 'https://docs.docker.com/engine/reference/run/' ref 'Chapter 1. Introduction to Control Groups (Cgroups)', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/ch01.html' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig CgroupParent)) { should be_empty } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.CgroupParent" do + subject { docker.object(id).dig('HostConfig', 'CgroupParent') } + it { should be_empty } end end end -control 'docker-5.25' do +control 'docker-5.26' do impact 1.0 title 'Restrict container from acquiring additional privileges' desc 'Restrict the container from acquiring additional privileges via suid or sgid bits. @@ -604,8 +683,6 @@ Rationale: A process can set the no_new_priv bit in the kernel. It persists across fork, clone and execve. The no_new_priv bit ensures that the process or its children processes do not gain any additional privileges via suid or sgid bits. This way a lot of dangerous operations become a lot less dangerous because there is no possibility of subverting privileged binaries.' tag 'docker' - tag 'cis-docker-1.12.0': '5.25' - tag 'cis-docker-1.13.0': '5.25' tag 'level:1' ref 'BLOG: No New Privileges support in docker', url: 'https://github.com/projectatomic/atomic-site/issues/269' ref 'Add support for NoNewPrivileges in docker', url: 'https://github.com/moby/moby/pull/20727' @@ -613,14 +690,21 @@ ref 'System call filtering and no_new_privs', url: 'https://lwn.net/Articles/475678/' ref 'Add PR_{GET,SET}_NO_NEW_PRIVS to prevent execve from granting privs', url: 'https://lwn.net/Articles/475362/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its(%w(HostConfig SecurityOpt)) { should include(/no-new-privileges/) } + only_if('No containers are present') { !docker.containers.ids.empty? } + + daemon_default = docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('no-new-privileges', false) + docker.containers.ids.each do |id| + options = Array(docker.object(id)['HostConfig']['SecurityOpt']) + setting = options.find { |option| option.match?(/\Ano-new-privileges(?:[=:]|$)/) } + enabled = setting ? setting.match?(/\Ano-new-privileges(?:[=:]true)?\z/) : daemon_default + describe "Container #{id} no-new-privileges" do + subject { enabled } + it { should eq true } end end end -control 'docker-5.26' do +control 'docker-5.27' do impact 1.0 title 'Check container health at runtime' desc 'If the container image does not have an HEALTHCHECK instruction defined, use --health-cmd parameter at container runtime for checking container health. @@ -628,19 +712,19 @@ Rationale: One of the important security triads is availability. If the container image you are using does not have a pre-defined HEALTHCHECK instruction, use the --health-cmd parameter to check container health at runtime. Based on the reported health status, you could take necessary actions.' tag 'docker' - tag 'cis-docker-1.12.0': '5.26' - tag 'cis-docker-1.13.0': '5.26' tag 'level:1' ref 'Add support for user-defined healthchecks', url: 'https://github.com/moby/moby/pull/22719' + only_if('No running containers are present') { !docker.containers.running?.ids.empty? } + docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its('State.Health.Status') { should eq 'healthy' } + describe docker.object(id).dig('State', 'Health', 'Status') do + it { should eq 'healthy' } end end end -control 'docker-5.27' do +control 'docker-5.28' do impact 1.0 title 'Ensure docker commands always get the latest version of the image' desc 'Always ensure that you are using the latest version of the image within your repository and not the cached older versions. @@ -648,17 +732,27 @@ Rationale: Multiple docker commands such as docker pull, docker run, etc. are known to have an issue that by default, they extract the local copy of the image, if present, even though there is an updated version of the image with the "same tag" in the upstream repository. This could lead to using older and vulnerable images.' tag 'docker' - tag 'cis-docker-1.12.0': '5.27' - tag 'cis-docker-1.13.0': '5.27' tag 'level:1' ref 'Modifying trusted/untrusted pull behavior for create/run/build', url: 'https://github.com/moby/moby/pull/16609' - describe 'docker-test' do - skip 'Ensure docker commands always get the latest version of the image' + only_if('No containers are present') { !docker.containers.ids.empty? } + allowed_latest = Array(input('allowed_latest_tag_images')) + docker.containers.ids.each do |id| + image_ref = docker.object(id).dig('Config', 'Image').to_s + describe "Container #{id} image reference (#{image_ref})" do + it 'should not use the unpinned :latest tag in production' do + last_component = image_ref.split('/').last.to_s + implicit_latest = !last_component.include?(':') && !image_ref.include?('@') + expect(implicit_latest || last_component.end_with?(':latest')).to eq(false) unless allowed_latest.include?(image_ref) + end + it 'should specify a version tag or image digest' do + expect(image_ref.split('/').last.to_s).to match(/\A(?:[^:@]+:[a-zA-Z0-9_.-]+|[^@]+@sha256:[a-f0-9]{64})\z/) + end + end end end -control 'docker-5.28' do +control 'docker-5.29' do impact 1.0 title 'Use PIDs cgroup limit' desc 'Use --pids-limit flag at container runtime. @@ -666,21 +760,21 @@ Rationale: Attackers could launch a fork bomb with a single command inside the container. This fork bomb can crash the entire system and requires a restart of the host to make the system functional again. PIDs cgroup --pids-limit will prevent this kind of attacks by restricting the number of forks that can happen inside a container at a given time.' tag 'docker' - tag 'cis-docker-1.12.0': '5.28' - tag 'cis-docker-1.13.0': '5.28' tag 'level:1' ref 'Add PIDs cgroup support to Docker', url: 'https://github.com/moby/moby/pull/18697' ref 'docker run', url: 'https://docs.docker.com/engine/reference/commandline/run/' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its('HostConfig.PidsLimit') { should_not cmp 0 } - its('HostConfig.PidsLimit') { should_not cmp(-1) } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.PidsLimit" do + subject { docker.object(id).dig('HostConfig', 'PidsLimit') } + it { should cmp > 0 } end end end -control 'docker-5.29' do +control 'docker-5.30' do impact 1.0 title 'Do not use Docker\'s default bridge docker0' desc 'Do not use Docker\'s default bridge docker0. Use docker\'s user-defined networks for container networking. @@ -688,19 +782,26 @@ Rationale: Docker connects virtual interfaces created in the bridge mode to a common bridge called docker0. This default networking model is vulnerable to ARP spoofing and MAC flooding attacks since there is no filtering applied.' tag 'do cker' - tag 'cis-docker-1.12.0': '5.29' - tag 'cis-docker-1.13.0': '5.29' tag 'level:2' ref 'narwhal – secure Docker networking', url: 'https://github.com/nyantec/narwhal' ref 'Analysis of Docker Security', url: 'https://arxiv.org/pdf/1501.02967.pdf' ref 'Docker container networking', url: 'https://docs.docker.com/engine/userguide/networking/' - describe 'docker-test' do - skip 'Not implemented yet' + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} NetworkSettings.Networks" do + subject { docker.object(id).dig('NetworkSettings', 'Networks') } + it { should_not include 'bridge' } + end + describe "Container #{id} HostConfig.NetworkMode" do + subject { docker.object(id).dig('HostConfig', 'NetworkMode') } + it { should_not be_in %w(bridge default) } + end end end -control 'docker-5.30' do +control 'docker-5.31' do impact 1.0 title 'Do not share the host\'s user namespaces' desc 'Do not share the host\'s user namespaces with the containers. @@ -708,21 +809,22 @@ Rationale: User namespaces ensure that a root process inside the container will be mapped to a non-root process outside the container. Sharing the user namespaces of the host with the container thus does not isolate users on the host with users on the containers.' tag 'docker' - tag 'cis-docker-1.12.0': '5.30' - tag 'cis-docker-1.13.0': '5.30' tag 'level:1' ref 'docker run', url: 'https://docs.docker.com/engine/reference/commandline/run/' ref 'Rooting out Root: User namespaces in Docker', url: 'https://events.linuxfoundation.org/sites/events/files/slides/User%20Namespaces%20-%20ContainerCon%202015%20-%2016-9-final_0.pdf' ref 'Phase 1 implementation of user namespaces as a remapped container root', url: 'https://github.com/moby/moby/pull/12648' - docker.containers.running?.ids.each do |id| - describe docker.object(id) do - its('HostConfig.UsernsMode') { should eq '' } + only_if('No containers are present') { !docker.containers.ids.empty? } + + docker.containers.ids.each do |id| + describe "Container #{id} HostConfig.UsernsMode" do + subject { docker.object(id).dig('HostConfig', 'UsernsMode') } + it { should eq '' } end end end -control 'docker-5.31' do +control 'docker-5.32' do impact 1.0 title 'Do not mount the Docker socket inside any containers' desc 'The docker socket (docker.sock) should not be mounted inside a container. @@ -730,17 +832,23 @@ Rationale: If the docker socket is mounted inside a container it would allow processes running within the container to execute docker commands which effectively allows for full control of the host.' tag 'docker' - tag 'cis-docker-1.12.0': '5.31' - tag 'cis-docker-1.13.0': '5.31' tag 'level:1' ref 'The Dangers of Docker.sock', url: 'https://raesene.github.io/blog/2016/03/06/The-Dangers-Of-Docker.sock/' ref 'Docker-in-docker vs mounting /var/run/docker.sock', url: 'https://forums.docker.com/t/docker-in-docker-vs-mounting-var-run-docker-sock/9450/2' ref 'Is `-v /var/run/docker.sock:/var/run/docker.sock` a ticking time bomb', url: 'https://github.com/moby/moby/issues/21109' - docker.containers.running?.ids.each do |id| - docker.object(id).Mounts.each do |mount| - describe mount do - its('Source') { should_not include 'docker.sock' } + only_if('No containers are present') { !docker.containers.ids.empty? } + + socket_path = input('docker_socket') + docker.containers.ids.each do |id| + docker.object(id)['Mounts'].each do |mount| + source = mount['Source'].to_s.sub(%r{\A/var/run/}, '/run/').sub(%r{/$}, '') + socket = socket_path.sub(%r{\A/var/run/}, '/run/') + # A bind of the socket's parent directory exposes the socket as well. + exposed = source.empty? || socket == source || socket.start_with?(source + '/') || source.end_with?('/docker.sock') + describe "Container #{id} mount #{mount['Source']} exposes Docker socket" do + subject { exposed } + it { should eq false } end end end diff --git a/controls/docker_daemon_configuration.rb b/controls/docker_daemon_configuration.rb index 7ba2145..150023d 100644 --- a/controls/docker_daemon_configuration.rb +++ b/controls/docker_daemon_configuration.rb @@ -21,24 +21,29 @@ title 'Docker Daemon Configuration' -# attributes -DAEMON_TLSCACERT = input('daemon_tlscacert') -DAEMON_TLSCERT = input('daemon_tlscert') -DAEMON_TLSKEY = input('daemon_tlskey') -AUTHORIZATION_PLUGIN = input('authorization_plugin') -LOG_DRIVER = input('log_driver') -LOG_OPTS = input('log_opts') -SWARM_MODE = input('swarm_mode') -SWARM_MAX_MANAGER_NODES = input('swarm_max_manager_nodes') -SWARM_PORT = input('swarm_port') -SECCOMP_DEFAULT_PROFILE = input('seccomp_default_profile') - # check if docker exists only_if('docker not found') do command('docker').exist? end control 'docker-2.1' do + impact 1.0 + title 'Run the Docker daemon as a non-root user, if possible' + desc 'Run the Docker daemon and containers in rootless mode where the workload supports it. + + Rationale: Running the daemon without root privileges reduces the impact of a vulnerability in the daemon or container runtime. Review rootless mode limitations before enabling it.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + describe processes('dockerd') do + it { should exist } + its('users') { should_not include 'root' } + end +end + +control 'docker-2.2' do impact 1.0 title 'Restrict network traffic between containers' desc 'By default, all network traffic is allowed between containers on the same host. If not desired, restrict all the intercontainer communication. Link specific containers together that require inter communication. @@ -46,17 +51,16 @@ Rationale: By default, unrestricted network traffic is enabled between all containers on the same host. Thus, each container has the potential of reading all packets across the container network on the same host. This might lead to unintended and unwanted disclosure of information to other containers. Hence, restrict the inter container communication.' tag 'docker' - tag 'cis-docker-1.12.0': '2.1' - tag 'cis-docker-1.13.0': '2.1' tag 'level:1' ref 'Docker container networking', url: 'https://docs.docker.com/engine/userguide/networking/' - describe json('/etc/docker/daemon.json') do - its(['icc']) { should eq(false) } + describe command("docker network inspect bridge --format '{{index .Options \"com.docker.network.bridge.enable_icc\"}}'") do + its('exit_status') { should eq 0 } + its('stdout.strip') { should eq 'false' } end end -control 'docker-2.2' do +control 'docker-2.3' do impact 1.0 title 'Set the logging level' desc 'Set Docker daemon log level to \'info\'. @@ -64,17 +68,19 @@ Rationale: Setting up an appropriate log level, configures the Docker daemon to log events that you would want to review later. A ase log level of \'info\' and above would capture all logs except debug logs. Until and unless required, you should not run docker daemon at \'debug\' log level.' tag 'docker' - tag 'cis-docker-1.12.0': '2.2' - tag 'cis-docker-1.13.0': '2.2' tag 'level:1' ref 'Docker daemon', url: 'https://docs.docker.com/engine/reference/commandline/daemon/' - describe json('/etc/docker/daemon.json') do - its(['log-level']) { should eq('info') } + settings = docker_helper.daemon_configuration(input('docker_daemon_config')) + describe settings.fetch('log-level', 'info') do + it { should eq 'info' } + end + describe settings.fetch('debug', false) do + it { should eq false } end end -control 'docker-2.3' do +control 'docker-2.4' do impact 1.0 title 'Allow Docker to make changes to iptables' desc 'Iptables are used to set up, maintain, and inspect the tables of IP packet filter rules in the Linux kernel. Allow the Docker daemon to make changes to the iptables. @@ -82,17 +88,15 @@ Rationale: Docker will never make changes to your system iptables rules if you choose to do so. Docker server would automatically make the needed changes to iptables based on how you choose your networking options for the containers if it is allowed to do so. It is recommended to let Docker server make changes to iptables automatically to avoid networking misconfiguration that might hamper the communication between containers and to the outside world. Additionally, it would save you hassles of updating iptables every time you choose to run the containers or modify networking options.' tag 'docker' - tag 'cis-docker-1.12.0': '2.3' - tag 'cis-docker-1.13.0': '2.3' tag 'level:1' ref 'Understand container communication', url: 'https://docs.docker.com/engine/userguide/networking/default_network/container-communication/' - describe json('/etc/docker/daemon.json') do - its(['iptables']) { should eq(true) } + describe docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('iptables', true) do + it { should eq true } end end -control 'docker-2.4' do +control 'docker-2.5' do impact 1.0 title 'Do not use insecure registries' desc 'Docker considers a private registry either secure or insecure. By default, registries are considered secure. @@ -100,17 +104,15 @@ Rationale: A secure registry uses TLS. A copy of registry\'s CA certificate is placed on the Docker host at \'/etc/docker/certs.d//\' directory. An insecure registry is the one not having either valid registry certificate or is not using TLS. You should not be using any insecure registries in the production environment. Insecure registries can be tampered with leading to possible compromise to your production system. Additionally, If a registry is marked as insecure then \'docker pull\', \'docker push\', and \'docker search\' commands will not result in an error message and the user might be indefinitely working with insecure registries without ever being notified of potential danger.' tag 'docker' - tag 'cis-docker-1.12.0': '2.4' - tag 'cis-docker-1.13.0': '2.4' tag 'level:1' ref 'Insecure registry', url: 'https://docs.docker.com/registry/insecure/' - describe json('/etc/docker/daemon.json') do - its(['insecure-registries']) { should be_empty } + describe docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('insecure-registries', []) do + it { should be_empty } end end -control 'docker-2.5' do +control 'docker-2.6' do impact 1.0 title 'Do not use the aufs storage driver' desc 'Do not use \'aufs\' as storage driver for your Docker instance. @@ -118,20 +120,34 @@ Rationale: The \'aufs\' storage driver is the oldest storage driver. It is based on a Linux kernel patch-set that is unlikely to be merged into the main Linux kernel. \'aufs\' driver is also known to cause some serious kernel crashes. \'aufs\' just has legacy support from Docker. Most importantly, \'aufs\' is not a supported driver in many Linux distributions using latest Linux kernels.' tag 'docker' - tag 'cis-docker-1.12.0': '2.5' - tag 'cis-docker-1.13.0': '2.5' tag 'level:1' ref 'Docker daemon storage driver options', url: 'https://docs.docker.com/engine/reference/commandline/cli/#daemon-storage-driver-option' ref 'Switch from aufs to devicemapper', url: 'http://muehe.org/posts/switching-docker-from-aufs-to-devicemapper/' ref 'Deep dive into docker storage drivers', url: 'http://jpetazzo.github.io/assets/2015-03-05-deep-dive-into-docker-storage-drivers.html#1' ref 'Docker storage drivers', url: 'https://docs.docker.com/engine/userguide/storagedriver/' - describe json('/etc/docker/daemon.json') do - its(['storage-driver']) { should_not eq('aufs') } + describe docker.info do + its('Driver') { should_not eq 'aufs' } end end -control 'docker-2.6' do +control 'docker-2.7' do + impact 1.0 + title 'Do not use the devicemapper storage driver' + desc 'Do not use the devicemapper storage driver. + + Rationale: The devicemapper storage driver is deprecated and was removed in Docker Engine 25.0. Use a supported storage driver.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + describe docker.info do + its('Driver') { should_not eq 'devicemapper' } + end +end + +control 'docker-2.8' do impact 1.0 title 'Configure TLS authentication for Docker daemon' desc 'It is possible to make the Docker daemon to listen on a specific IP and port and any other Unix socket other than default Unix socket. Configure TLS authentication to restrict access to Docker daemon via IP and port. @@ -139,21 +155,27 @@ Rationale: By default, Docker daemon binds to a non-networked Unix socket and runs with \'root\' privileges. If you change the default docker daemon binding to a TCP port or any other Unix socket, anyone with access to that port or socket can have full access to Docker daemon and in turn to the host system. Hence, you should not bind the Docker daemon to another IP/port or a Unix socket. If you must expose the Docker daemon via a network socket, configure TLS authentication for the daemon and Docker Swarm APIs (if using). This would restrict the connections to your Docker daemon over the network to a limited number of clients who could successfully authenticate over TLS.' tag 'docker' - tag 'cis-docker-1.12.0': '2.6' - tag 'cis-docker-1.13.0': '2.6' tag 'level:1' ref 'Protect Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' - describe json('/etc/docker/daemon.json') do - its(['tls']) { should eq(true) } - its(['tlsverify']) { should eq(true) } - its(['tlscacert']) { should eq(DAEMON_TLSCACERT) } - its(['tlscert']) { should eq(DAEMON_TLSCERT) } - its(['tlskey']) { should eq(DAEMON_TLSKEY) } + settings = docker_helper.daemon_configuration(input('docker_daemon_config')) + only_if('No TCP daemon listener is configured') { Array(settings['hosts']).any? { |host| host.start_with?('tcp://') } } + describe settings do + its(['tlsverify']) { should eq true } + end + %w(tlscacert tlscert tlskey).each do |key| + describe settings[key].to_s do + it { should_not be_empty } + end + next unless settings[key] + + describe file(settings[key]) do + it { should be_file } + end end end -control 'docker-2.7' do +control 'docker-2.9' do impact 1.0 title 'Set default ulimit as appropriate' desc 'Set the default ulimit options as appropriate in your environment. @@ -161,18 +183,23 @@ Rationale: ulimit provides control over the resources available to the shell and to processes started by it. Setting system resource limits judiciously saves you from many disasters such as a fork bomb. Sometimes, even friendly users and legitimate processes can overuse system resources and in-turn can make the system unusable. Setting default ulimit for the Docker daemon would enforce the ulimit for all container instances. You would not need to setup ulimit for each container instance. However, the default ulimit can be overridden during container runtime, if needed. Hence, to control the system resources, define a default ulimit as needed in your environment.' tag 'docker' - tag 'cis-docker-1.12.0': '2.7' - tag 'cis-docker-1.13.0': '2.7' tag 'level:1' ref 'Docker daemon default ulimits', url: 'https://docs.docker.com/engine/reference/commandline/daemon/#default-ulimits' - describe json('/etc/docker/daemon.json') do - its(%w(default-ulimits nproc)) { should eq('1024:2408') } - its(%w(default-ulimits nofile)) { should eq('100': '200') } + limits = docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('default-ulimits', {}) + input('default_ulimits').each do |name, expected| + expected = expected.transform_keys(&:to_s) + describe limits.fetch(name.to_s, {}) do + its(['Soft']) { should eq expected['Soft'] } + its(['Hard']) { should eq expected['Hard'] } + end + end + describe input('default_ulimits') do + it { should_not be_empty } end end -control 'docker-2.8' do +control 'docker-2.10' do impact 1.0 title 'Enable user namespace support' desc 'Enable user namespace support in Docker daemon to utilize container user to host user re-mapping. This recommendation is beneficial where containers you are using do not have an explicit container user defined in the container image. If container images that you are using have a pre-defined non-root user, this recommendation may be skipped since this feature is still in its infancy and might give you unpredictable issues and complexities. @@ -180,28 +207,18 @@ Rationale: The Linux kernel user namespace support in Docker daemon provides additional security for the Docker host system. It allows a container to have a unique range of user and group IDs which are outside the traditional user and group range utilized by the host system. For example, the root user will have expected administrative privilege inside the container but can effectively be mapped to an unprivileged UID on the host system.' tag 'docker' - tag 'cis-docker-1.12.0': '2.8' - tag 'cis-docker-1.13.0': '2.8' tag 'level:2' ref 'User namespeces', url: 'http://man7.org/linux/man-pages/man7/user_namespaces.7.html' ref 'Docker daemon configuration', url: 'https://docs.docker.com/engine/reference/commandline/daemon/' ref 'Routing out root: user namespaces in docker', url: 'http://events.linuxfoundation.org/sites/events/files/slides/User%20Namespaces%20-%20ContainerCon%202015%20-%2016-9-final_0.pdf' ref 'Docker images vanish when using user namespaces ', url: 'https://github.com/docker/docker/issues/21050' - describe json('/etc/docker/daemon.json') do - its(['userns-remap']) { should eq('default') } - end - describe file('/etc/subuid') do - it { should exist } - it { should be_file } - end - describe file('/etc/subgid') do - it { should exist } - it { should be_file } + describe docker.info do + its('SecurityOptions') { should include(/name=(userns|rootless)(,|$)/) } end end -control 'docker-2.9' do +control 'docker-2.11' do impact 1.0 title 'Confirm default cgroup usage' desc 'The --cgroup-parent option allows you to set the default cgroup parent to use for all the containers. If there is no specific use case, this setting should be left at its default. @@ -209,17 +226,16 @@ Rationale: System administrators typically define cgroups under which containers are supposed to run. Even if cgroups are not explicitly defined by the system administrators, containers run under docker cgroup by default. It is possible to attach to a different cgroup other than that is the default. This usage should be monitored and confirmed. By attaching to a different cgroup than the one that is a default, it is possible to share resources unevenly and thus might starve the host for resources.' tag 'docker' - tag 'cis-docker-1.12.0': '2.9' - tag 'cis-docker-1.13.0': '2.9' tag 'level:2' ref 'Docker daemon configuration', url: 'https://docs.docker.com/engine/reference/commandline/daemon/' - describe json('/etc/docker/daemon.json') do - its(['cgroup-parent']) { should eq('docker') } + parent = docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('cgroup-parent', '') + describe parent do + it { should eq input('docker_cgroup_parent') } end end -control 'docker-2.10' do +control 'docker-2.12' do impact 1.0 title 'Do not change base device size until needed' desc 'In certain circumstances, you might need containers bigger than 10G in size. In these cases, carefully choose the base device size. @@ -227,17 +243,16 @@ Rationale: The base device size can be increased at daemon restart. Increasing the base device size allows all future images and containers to be of the new base device size. A user can use this option to expand the base device size however shrinking is not permitted. This value affects the system-wide “base” empty filesystem that may already be initialized and inherited by pulled images. Though the file system does not allot the increased size if it is empty, it will use more space for the empty case depending upon the device size. This may cause a denial of service by ending up in file system being over-allocated or full.' tag 'docker' - tag 'cis-docker-1.12.0': '2.10' - tag 'cis-docker-1.13.0': '2.10' tag 'level:2' ref 'Docker daemon storage driver options', url: 'https://docs.docker.com/engine/reference/commandline/daemon/#storage-driver-options' - describe json('/etc/docker/daemon.json') do - its(['storage-opts']) { should eq(['dm.basesize=10G']) } + options = docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('storage-opts', []) + describe options do + it { should_not include(/^dm\.basesize=/) } end end -control 'docker-2.11' do +control 'docker-2.13' do impact 1.0 title 'Use authorization plugin' desc 'Docker’s out-of-the-box authorization model is all or nothing. Any user with permission to access the Docker daemon can run any Docker client command. The same is true for callers using Docker’s remote API to contact the daemon. If you require greater access control, you can create authorization plugins and add them to your Docker daemon configuration. Using an authorization plugin, a Docker administrator can configure granular access policies for managing access to Docker daemon. @@ -245,20 +260,18 @@ Rationale: Docker’s out-of-the-box authorization model is all or nothing. Any user with permission to access the Docker daemon can run any Docker client command. The same is true for callers using Docker’s remote API to contact the daemon. If you require greater access control, you can create authorization plugins and add them to your Docker daemon configuration. Using an authorization plugin, a Docker administrator can configure granular access policies for managing access to Docker daemon.' tag 'docker' - tag 'cis-docker-1.12.0': '2.11' - tag 'cis-docker-1.13.0': '2.11' tag 'level:2' ref 'Access authorization', url: 'https://docs.docker.com/engine/reference/commandline/daemon/#access-authorization' ref 'Auhtorization plugins', url: 'https://docs.docker.com/engine/extend/plugins_authorization/' ref 'Twistlock authorization plugin', url: 'https://github.com/twistlock/authz' - describe json('/etc/docker/daemon.json') do - its(['authorization-plugins']) { should_not be_empty } - its(['authorization-plugins']) { should eq([AUTHORIZATION_PLUGIN]) } + describe docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('authorization-plugins', []) do + it { should_not be_empty } + it { should include input('authorization_plugin') } end end -control 'docker-2.12' do +control 'docker-2.14' do impact 1.0 title 'Configure centralized and remote logging' desc 'Docker now supports various log drivers. A preferable way to store logs is the one that supports centralized and remote logging. @@ -266,121 +279,51 @@ Ratonale: Centralized and remote logging ensures that all important log records are safe despite catastrophic events. Docker now supports various such logging drivers. Use the one that suits your environment the best.' tag 'docker' - tag 'cis-docker-1.12.0': '2.12' - tag 'cis-docker-1.13.0': '2.12' tag 'level:2' ref 'Logging overview', url: 'https://docs.docker.com/engine/admin/logging/overview/' - describe json('/etc/docker/daemon.json') do - its(['log-driver']) { should_not be_empty } - its(['log-driver']) { should eq(LOG_DRIVER) } - its(['log-opts']) { should include(LOG_OPTS) } + describe docker.info do + its('LoggingDriver') { should eq input('log_driver') } end -end - -control 'docker-2.13' do - impact 1.0 - title 'Disable operations on legacy registry (v1)' - desc 'The latest Docker registry is v2. All operations on the legacy registry version (v1) should be restricted. - - Rationale: Docker registry v2 brings in many performance and security improvements over v1. It supports container image provenance and other security features such as image signing and verification. Hence, operations on Docker legacy registry should be restricted.' - - tag 'docker' - tag 'cis-docker-1.12.0': '2.13' - tag 'cis-docker-1.13.0': '2.13' - tag 'level:1' - ref 'Docker daemon storage driver options', url: 'https://docs.docker.com/engine/reference/commandline/daemon/#storage-driver-options' - ref 'Proposal: Provenance step 1 - Transform images for validation and verification', url: 'https://github.com/docker/docker/issues/8093' - ref 'Proposal: JSON Registry API V2.1', url: 'https://github.com/docker/docker/issues/9015' - ref 'Registry next generation', url: 'https://github.com/docker/docker-registry/issues/612' - ref 'Docker Registry HTTP API V2', url: 'https://docs.docker.com/registry/spec/api/' - ref 'Creating Private Docker Registry 2.0 with Token Authentication Service', url: 'https://the.binbashtheory.com/creating-private-docker-registry-2-0-with-token-authentication-service/' - ref 'New Tool to Migrate From V1 Registry to Docker Trusted Registry or V2 Open Source Registry', url: 'https://blog.docker.com/2015/07/new-tool-v1-registry-docker-trusted-registry-v2-open-source/' - ref 'Docker Registry V2', url: 'https://www.slideshare.net/Docker/docker-registry-v2' - - describe json('/etc/docker/daemon.json') do - its(['disable-legacy-registry']) { should eq(true) } - end -end - -control 'docker-2.14' do - impact 1.0 - title 'Enable live restore' - desc 'The \'--live-restore\' enables full support of daemon-less containers in docker. It ensures that docker does not stop containers on shutdown or restore and properly reconnects to the container when restarted. - - Rationale: One of the important security triads is availability. Setting \'--live-restore\' flag in the docker daemon ensures that container execution is not interrupted when the docker daemon is not available. This also means that it is now easier to update and patch the docker daemon without execution downtime.' - - tag 'docker' - tag 'cis-docker-1.12.0': '2.14' - tag 'cis-docker-1.13.0': '2.14' - tag 'level:1' - ref 'Add --live-restore flag', url: 'https://github.com/docker/docker/pull/23213' - - describe json('/etc/docker/daemon.json') do - its(['live-restore']) { should eq(true) } + describe docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('log-opts', {}) do + it { should include input('log_opts') } end end control 'docker-2.15' do impact 1.0 - title 'Do not enable swarm mode, if not needed' - desc 'Do not enable swarm mode on a docker engine instance unless needed. + title 'Restrict containers from acquiring new privileges' + desc 'Enable no-new-privileges in the Docker daemon configuration. - Rationale: By default, a Docker engine instance will not listen on any network ports, with all communications with the client coming over the Unix socket. When Docker swarm mode is enabled on a docker engine instance, multiple network ports are opened on the system and made available to other systems on the network for the purposes of cluster management and node communications. Opening network ports on a system increase its attack surface and this should be avoided unless required.' + Rationale: Setting this option by default prevents container processes from gaining additional privileges through setuid or setgid executables.' tag 'docker' - tag 'cis-docker-1.12.0': '2.15' - tag 'cis-docker-1.13.0': '2.15' tag 'level:1' - ref 'docker swarm init', url: 'https://docs.docker.com/engine/reference/commandline/swarm_init/' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' - describe docker.info do - its('Swarm.LocalNodeState') { should eq SWARM_MODE } + describe docker_helper.daemon_configuration(input('docker_daemon_config')) do + its(['no-new-privileges']) { should eq true } end end control 'docker-2.16' do impact 1.0 - title 'Control the number of manager nodes in a swarm' - desc 'Ensure that the minimum number of required manager nodes is created in a swarm. + title 'Enable live restore' + desc 'The \'--live-restore\' enables full support of daemon-less containers in docker. It ensures that docker does not stop containers on shutdown or restore and properly reconnects to the container when restarted. - Rationale: Manager nodes within a swarm have control over the swarm and change its configuration modifying security parameters. Having excessive manager nodes could render the swarm more susceptible to compromise. If fault tolerance is not required in the manager nodes, a single node should be elected as a manager. If fault tolerance is required then the smallest practical odd number to achieve the appropriate level of tolerance should be configured.' + Rationale: One of the important security triads is availability. Setting \'--live-restore\' flag in the docker daemon ensures that container execution is not interrupted when the docker daemon is not available. This also means that it is now easier to update and patch the docker daemon without execution downtime.' tag 'docker' - tag 'cis-docker-1.12.0': '2.16' - tag 'cis-docker-1.13.0': '2.16' tag 'level:1' - ref 'Manage nodes in a swarm', url: 'https://docs.docker.com/engine/swarm/manage-nodes/' - ref 'Administer and maintain a swarm of Docker Engines', url: 'https://docs.docker.com/engine/swarm/admin_guide/' + ref 'Add --live-restore flag', url: 'https://github.com/docker/docker/pull/23213' - only_if { SWARM_MODE == 'active' } + only_if('Live restore is not supported for Swarm services') { docker.info.dig('Swarm', 'LocalNodeState') != 'active' } describe docker.info do - its('Swarm.Managers') { should cmp <= SWARM_MAX_MANAGER_NODES } + its('LiveRestoreEnabled') { should eq true } end end control 'docker-2.17' do - impact 1.0 - title 'Bind swarm services to a specific host interface' - desc 'By default, the docker swarm services will listen to all interfaces on the host, which may not be necessary for the operation of the swarm where the host has multiple network interfaces. - - Rationale: When a swarm is initialized the default value for the --listen-addr flag is 0.0.0.0\': \'2377 which means that the swarm services will listen on all interfaces on the host. If a host has multiple network interfaces this may be undesirable as it may expose the docker swarm services to networks which are not involved in the operation of the swarm. By passing a specific IP address to the --listen-addr, a specific network interface can be specified limiting this exposure.' - - tag 'docker' - tag 'cis-docker-1.12.0': '2.17' - tag 'cis-docker-1.13.0': '2.17' - tag 'level:1' - ref 'docker swarm init', url: 'https://docs.docker.com/engine/reference/commandline/swarm_init/' - ref 'Administer and maintain a swarm of Docker Engines', url: 'https://docs.docker.com/engine/swarm/admin_guide/' - - only_if { SWARM_MODE == 'active' } - describe port(SWARM_PORT) do - its('addresses') { should_not include '0.0.0.0' } - its('addresses') { should_not include '::' } - end -end - -control 'docker-2.18' do impact 1.0 title 'Disable Userland Proxy' desc 'The docker daemon starts a userland proxy service for port forwarding whenever a port is exposed. Where hairpin NAT is available, this service is generally superfluous to requirements and can be disabled. @@ -388,50 +331,18 @@ Rationale: Docker engine provides two mechanisms for forwarding ports from the host to containers, hairpin NAT, and a userland proxy. In most circumstances, the hairpin NAT mode is preferred as it improves performance and makes use of native Linux iptables functionality instead of an additional component. Where hairpin NAT is available, the userland proxy should be disabled on startup to reduce the attack surface of the installation.' tag 'docker' - tag 'cis-docker-1.12.0': '2.18' - tag 'cis-docker-1.13.0': '2.18' tag 'level:1' ref 'The docker-proxy', url: 'http://windsock.io/the-docker-proxy/' ref 'Disable Userland proxy by default', url: 'https://github.com/docker/docker/issues/14856' ref 'overlay networking with userland-proxy disabled prevents port exposure', url: 'https://github.com/moby/moby/issues/22741' ref 'Bind container ports to the host', url: 'https://docs.docker.com/engine/userguide/networking/default_network/binding/' - describe json('/etc/docker/daemon.json') do - its(['userland-proxy']) { should eq(false) } - end - describe processes('dockerd').commands do - it { should include 'userland-proxy=false' } - end -end - -control 'docker-2.19' do - impact 1.0 - title 'Encrypt data exchanged between containers on different nodes on the overlay network' - desc 'Encrypt data exchanged between containers on different nodes on the overlay network. - - Rationale: By default, data exchanged between containers on different nodes on the overlay network is not encrypted. This could potentially expose traffic between the container nodes.' - - tag 'docker' - tag 'cis-docker-1.13.0': '2.19' - tag 'level:1' - ref 'Docker swarm mode overlay network security model', url: 'https://docs.docker.com/engine/userguide/networking/overlay-security-model/' - ref 'Docker swarm container-container traffic not encrypted when inspecting externally with tcpdump', url: 'https://github.com/moby/moby/issues/24253' - - only_if { SWARM_MODE == 'active' } - if docker_helper.overlay_networks - docker_helper.overlay_networks.each do |k, _v| - describe docker_helper.overlay_networks[k] do - its(['encrypted']) { should_not eq(nil) } - end - end - else - describe 'Encrypted overlay networks' do - skip 'Cannot determine overlay networks' - end + describe docker_helper.daemon_configuration(input('docker_daemon_config')) do + its(['userland-proxy']) { should eq false } end end -control 'docker-2.20' do +control 'docker-2.18' do impact 1.0 title 'Apply a daemon-wide custom seccomp profile, if needed' desc 'You can choose to apply your custom seccomp profile at the daemon-wide level if needed and override Docker\'s default seccomp profile. @@ -439,17 +350,26 @@ Rationale: A large number of system calls are exposed to every userland process with many of them going unused for the entire lifetime of the process. Most of the applications do not need all the system calls and thus benefit by having a reduced set of available system calls. The reduced set of system calls reduces the total kernel surface exposed to the application and thus improvises application security. You could apply your own custom seccomp profile instead of Docker\'s default seccomp profile. Alternatively, if Docker\'s default profile is good for your environment, you can choose to ignore this recommendation.' tag 'docker' - tag 'cis-docker-1.13.0': '2.20' tag 'level:2' ref 'daemon: add a flag to override the default seccomp profile', url: 'https://github.com/moby/moby/pull/26276' - describe json('/etc/docker/daemon.json') do - its(['seccomp-profile']) { should_not eq(nil) } - its(['seccomp-profile']) { should eq(SECCOMP_DEFAULT_PROFILE) } + describe docker.info do + its('SecurityOptions') { should include(/name=seccomp(?:,|$)/) } + end + profile = docker_helper.daemon_configuration(input('docker_daemon_config')).fetch('seccomp-profile', 'default') + describe profile do + it { should eq input('seccomp_default_profile') } + it { should_not eq 'unconfined' } + end + unless %w(default unconfined).include?(profile) + describe file(profile) do + it { should exist } + it { should be_file } + end end end -control 'docker-2.21' do +control 'docker-2.19' do impact 1.0 title 'Avoid experimental features in production' desc 'Avoid experimental features in production. @@ -457,63 +377,12 @@ Rationale: Experimental is now a runtime docker daemon flag instead of a separate build. Passing --experimental as a runtime flag to the docker daemon, activates experimental features. Experimental is now considered a stable release, but with a couple of features which might not have tested and guaranteed API stability.' tag 'docker' - tag 'cis-docker-1.13.0': '2.21' tag 'level:1' ref 'Changing the definition of experimental', url: 'https://github.com/moby/moby/issues/26713' ref 'Make experimental a runtime flag', url: 'https://github.com/moby/moby/pull/27223' - describe command('docker version --format \'{{ .Server.Experimental }}\'').stdout.chomp do - it { should eq('false') } - end -end - -control 'docker-2.22' do - impact 1.0 - title 'Use Docker\'s secret management commands for managing secrets in a Swarm cluster' - desc 'Use Docker\'s in-built secret management command. - - Rationale: Docker has various commands for managing secrets in a Swarm cluster. This is the foundation for future secret support in Docker with potential improvements such as Windows support, different backing stores, etc.' - - tag 'docker' - tag 'cis-docker-1.13.0': '2.22' - tag 'level:2' - ref 'Secret Management', url: 'https://github.com/moby/moby/pull/27794' - - only_if { SWARM_MODE == 'active' } - describe command('docker secret ls -q').stdout.split("\n").length do - it { should be > 0 } + describe command("docker version --format '{{ .Server.Experimental }}'") do + its('exit_status') { should eq 0 } + its('stdout.strip') { should eq 'false' } end end - -control 'docker-2.23' do - impact 1.0 - title 'Run swarm manager in auto-lock mode' - desc 'Run Docker swarm manager in auto-lock mode. - - Rationale: When Docker restarts, both the TLS key used to encrypt communication among swarm nodes, and the key used to encrypt and decrypt Raft logs on disk, are loaded into each manager node\'s memory. You should protect the mutual TLS encryption key and the key used to encrypt and decrypt Raft logs at rest. This protection could be enabled by initializing swarm with --autolock flag. With --autolock enabled, when Docker restarts, you must unlock the swarm first, using a key encryption key generated by Docker when the swarm was initialized.' - - tag 'docker' - tag 'cis-docker-1.13.0': '2.23' - tag 'level:1' - ref 'Initialize a swarm with autolocking enabled', url: 'https://github.com/mistyhacks/docker.github.io/blob/af7dfdba8504f9b102fb31a78cd08a06c33a8975/engine/swarm/swarm_manager_locking.md' - - only_if { SWARM_MODE == 'active' } - describe command('docker swarm unlock-key -q').stdout.chomp.length do - it { should be > 0 } - end -end - -control 'docker-2.24' do - impact 1.0 - title 'Rotate swarm manager auto-lock key periodically' - desc 'Rotate swarm manager auto-lock key periodically. - - Rationale: Swarm manager auto-lock key is not automatically rotated. You should rotate them periodically as a best practice. - - Audit: Currently, there is no mechanism to find out when the key was last rotated on a swarm manager node. You should check with the system administrator if there is a key rotation record and the keys were rotated at a pre-defined frequency.' - - tag 'docker' - tag 'cis-docker-1.13.0': '2.24' - tag 'level:1' - ref 'Swarm Key rotation', url: 'https://github.com/mistyhacks/docker.github.io/blob/af7dfdba8504f9b102fb31a78cd08a06c33a8975/engine/swarm/swarm_manager_locking.md' -end diff --git a/controls/docker_daemon_configuration_files.rb b/controls/docker_daemon_configuration_files.rb index bcf291b..e026939 100644 --- a/controls/docker_daemon_configuration_files.rb +++ b/controls/docker_daemon_configuration_files.rb @@ -21,11 +21,6 @@ title 'Docker Daemon Configuration Files' -# attributes -REGISTRY_CERT_PATH = input('registry_cert_path') -REGISTRY_NAME = input('registry_name') -REGISTRY_CA_FILE = input('registry_ca_file') - # check if docker exists only_if('docker not found') do command('docker').exist? @@ -39,16 +34,18 @@ Rationale: \'docker.service\' file contains sensitive parameters that may alter the behavior of Docker daemon. Hence, it should be owned and group-owned by \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.1' - tag 'cis-docker-1.13.0': '3.1' tag 'level:1' ref 'Control and configure Docker with systemd', url: 'https://docs.docker.com/engine/admin/systemd/' - describe file(docker_helper.path) do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = docker_helper.path + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -60,21 +57,17 @@ Rationale: \'docker.service\' file contains sensitive parameters that may alter the behavior of Docker daemon. Hence, it should not be writable by any other user other than \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.2' - tag 'cis-docker-1.13.0': '3.2' tag 'level:1' ref 'Control and configure Docker with systemd', url: 'https://docs.docker.com/engine/admin/systemd/' - describe file(docker_helper.path) do - it { should exist } - it { should be_file } - it { should be_readable.by('owner') } - it { should be_writable.by('owner') } - it { should be_readable.by('group') } - it { should_not be_writable.by('group') } - it { should be_readable.by('other') } - it { should_not be_writable.by('other') } - it { should_not be_executable } + target = docker_helper.path + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + its('mode') { should satisfy('be 0644 or more restrictive') { |mode| (mode & (07777 ^ 0644)).zero? } } + end end end @@ -86,18 +79,20 @@ Rationale: \'docker.socket\' file contains sensitive parameters that may alter the behavior of Docker remote API. Hence, it should be owned and group-owned by \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.3' - tag 'cis-docker-1.13.0': '3.3' tag 'level:1' ref 'Dockerd', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' ref 'YungSang/fedora-atomic-packer', url: 'https://github.com/YungSang/fedora-atomic-packer/blob/master/oem/docker.socket' ref 'CentOS 7/RHEL 7 and docker containers on boot', url: 'https://daviddaeschler.com/2014/12/14/centos-7rhel-7-and-docker-containers-on-boot/' - describe file(docker_helper.socket) do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = docker_helper.socket + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -109,23 +104,19 @@ Rationale: \'docker.socket\' file contains sensitive parameters that may alter the behavior of Docker remote API. Hence, it should be writable only by \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.4' - tag 'cis-docker-1.13.0': '3.4' tag 'level:1' ref 'Dockerd', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' ref 'YungSang/fedora-atomic-packer', url: 'https://github.com/YungSang/fedora-atomic-packer/blob/master/oem/docker.socket' ref 'CentOS 7/RHEL 7 and docker containers on boot', url: 'https://daviddaeschler.com/2014/12/14/centos-7rhel-7-and-docker-containers-on-boot/' - describe file(docker_helper.socket) do - it { should exist } - it { should be_file } - it { should be_readable.by('owner') } - it { should be_writable.by('owner') } - it { should be_readable.by('group') } - it { should_not be_writable.by('group') } - it { should be_readable.by('other') } - it { should_not be_writable.by('other') } - it { should_not be_executable } + target = docker_helper.socket + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + its('mode') { should satisfy('be 0644 or more restrictive') { |mode| (mode & (07777 ^ 0644)).zero? } } + end end end @@ -137,16 +128,17 @@ Rationale: \'/etc/docker\' directory contains certificates and keys in addition to various sensitive files. Hence, it should be owned and group-owned by \'root\' to maintain the integrity of the directory.' tag 'docker' - tag 'cis-docker-1.12.0': '3.5' - tag 'cis-docker-1.13.0': '3.5' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' - describe file('/etc/docker') do - it { should exist } - it { should be_directory } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = '/etc/docker' + if target + describe file(target) do + it { should exist } + it { should be_directory } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -158,23 +150,16 @@ Rationale: \'/etc/docker\' directory contains certificates and keys in addition to various sensitive files. Hence, it should only be writable by \'root\' to maintain the integrity of the directory.' tag 'docker' - tag 'cis-docker-1.12.0': '3.6' - tag 'cis-docker-1.13.0': '3.6' tag 'level:1' ref 'Docker Security', url: 'https://docs.docker.com/engine/security/security/#conclusions' - describe file('/etc/docker') do - it { should exist } - it { should be_directory } - it { should be_readable.by('owner') } - it { should be_writable.by('owner') } - it { should be_executable.by('owner') } - it { should be_readable.by('group') } - it { should_not be_writable.by('group') } - it { should be_executable.by('group') } - it { should be_readable.by('other') } - it { should_not be_writable.by('other') } - it { should be_executable.by('other') } + target = '/etc/docker' + if target + describe file(target) do + it { should exist } + it { should be_directory } + its('mode') { should satisfy('be 0755 or more restrictive') { |mode| (mode & (07777 ^ 0755)).zero? } } + end end end @@ -186,32 +171,19 @@ Rationale: /etc/docker/certs.d/ directory contains Docker registry certificates. These certificate files must be owned and group-owned by \'root\' to maintain the integrity of the certificates.' tag 'docker' - tag 'cis-docker-1.12.0': '3.7' - tag 'cis-docker-1.13.0': '3.7' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(REGISTRY_CERT_PATH) do - it { should exist } - it { should be_directory } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } - end - - describe file(REGISTRY_NAME) do - it { should exist } - it { should be_directory } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } - end - - describe file(REGISTRY_CA_FILE) do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + certificates = docker_helper.registry_certificates(input('registry_cert_path')) + only_if('No registry certificates are installed') { !certificates.empty? } + certificates.each do |certificate| + describe file(certificate) do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -223,19 +195,18 @@ Rationale: /etc/docker/certs.d/ directory contains Docker registry certificates. These certificate files must have permissions of \'444\' to maintain the integrity of the certificates.' tag 'docker' - tag 'cis-docker-1.12.0': '3.8' - tag 'cis-docker-1.13.0': '3.8' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(REGISTRY_CA_FILE) do - it { should exist } - it { should be_file } - it { should be_readable } - it { should_not be_executable } - it { should_not be_writable } + certificates = docker_helper.registry_certificates(input('registry_cert_path')) + only_if('No registry certificates are installed') { !certificates.empty? } + certificates.each do |certificate| + describe file(certificate) do + it { should be_file } + its('mode') { should satisfy('be 0444 or more restrictive') { |mode| (mode & (07777 ^ 0444)).zero? } } + end end end @@ -247,18 +218,19 @@ Rationale: The TLS CA certificate file should be protected from any tampering. It is used to authenticate Docker server based on given CA certificate. Hence, it must be owned and group-owned by \'root\' to maintain the integrity of the CA certificate.' tag 'docker' - tag 'cis-docker-1.12.0': '3.9' - tag 'cis-docker-1.13.0': '3.9' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(json('/etc/docker/daemon.json').params['tlscacert']) do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = docker_helper.daemon_configuration(input('docker_daemon_config'))['tlscacert'] + only_if('TLS file is not configured') { target && !target.empty? } + if target && !target.empty? + describe file(target) do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -270,19 +242,18 @@ Rationale: The TLS CA certificate file should be protected from any tampering. It is used to authenticate Docker server based on given CA certificate. Hence, it must have permissions of \'444\' to maintain the integrity of the CA certificate.' tag 'docker' - tag 'cis-docker-1.12.0': '3.10' - tag 'cis-docker-1.13.0': '3.10' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(json('/etc/docker/daemon.json').params['tlscacert']) do - it { should exist } - it { should be_file } - it { should be_readable } - it { should_not be_executable } - it { should_not be_writable } + target = docker_helper.daemon_configuration(input('docker_daemon_config'))['tlscacert'] + only_if('TLS file is not configured') { target && !target.empty? } + if target && !target.empty? + describe file(target) do + it { should be_file } + its('mode') { should satisfy('be 0444 or more restrictive') { |mode| (mode & (07777 ^ 0444)).zero? } } + end end end @@ -294,18 +265,19 @@ Rationale: The Docker server certificate file should be protected from any tampering. It is used to authenticate Docker server based on the given server certificate. Hence, it must be owned and group-owned by \'root\' to maintain the integrity of the certificate.' tag 'docker' - tag 'cis-docker-1.12.0': '3.11' - tag 'cis-docker-1.13.0': '3.11' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(json('/etc/docker/daemon.json').params['tlscert']) do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = docker_helper.daemon_configuration(input('docker_daemon_config'))['tlscert'] + only_if('TLS file is not configured') { target && !target.empty? } + if target && !target.empty? + describe file(target) do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -317,19 +289,18 @@ Rationale: The Docker server certificate file should be protected from any tampering. It is used to authenticate Docker server based on the given server certificate. Hence, it must have permissions of \'444\' to maintain the integrity of the certificate.' tag 'docker' - tag 'cis-docker-1.12.0': '3.12' - tag 'cis-docker-1.13.0': '3.12' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(json('/etc/docker/daemon.json').params['tlscert']) do - it { should exist } - it { should be_file } - it { should be_readable } - it { should_not be_executable } - it { should_not be_writable } + target = docker_helper.daemon_configuration(input('docker_daemon_config'))['tlscert'] + only_if('TLS file is not configured') { target && !target.empty? } + if target && !target.empty? + describe file(target) do + it { should be_file } + its('mode') { should satisfy('be 0444 or more restrictive') { |mode| (mode & (07777 ^ 0444)).zero? } } + end end end @@ -341,42 +312,42 @@ Rationale: The Docker server certificate key file should be protected from any tampering or unneeded reads. It holds the private key for the Docker server certificate. Hence, it must be owned and group-owned by \'root\' to maintain the integrity of the Docker server certificate.' tag 'docker' - tag 'cis-docker-1.12.0': '3.13' - tag 'cis-docker-1.13.0': '3.13' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(json('/etc/docker/daemon.json').params['tlskey']) do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = docker_helper.daemon_configuration(input('docker_daemon_config'))['tlskey'] + only_if('TLS file is not configured') { target && !target.empty? } + if target && !target.empty? + describe file(target) do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end control 'docker-3.14' do impact 1.0 - title 'Verify that Docker server certificate key file permissions are set to 444 or more restrictive' + title 'Verify that Docker server certificate key file permissions are set to 400' desc 'Verify that the Docker server certificate key file (the file that is passed alongwith \'--tlskey\' parameter) has permissions of \'400\'. Rationale: The Docker server certificate key file should be protected from any tampering or unneeded reads. It holds the private key for the Docker server certificate. Hence, it must have permissions of \'400\' to maintain the integrity of the Docker server certificate.' tag 'docker' - tag 'cis-docker-1.12.0': '3.14' - tag 'cis-docker-1.13.0': '3.14' tag 'level:1' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'Verify repository client with certificates', url: 'https://docs.docker.com/engine/security/certificates/' ref 'Insecure Registry', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/' - describe file(json('/etc/docker/daemon.json').params['tlskey']) do - it { should exist } - it { should be_file } - it { should be_readable } - it { should_not be_executable } - it { should_not be_writable } + target = docker_helper.daemon_configuration(input('docker_daemon_config'))['tlskey'] + only_if('TLS file is not configured') { target && !target.empty? } + if target && !target.empty? + describe file(target) do + it { should be_file } + its('mode') { should cmp '0400' } + end end end @@ -388,17 +359,18 @@ Rationale: Docker daemon runs as \'root\'. The default Unix socket hence must be owned by \'root\'. If any other user or process owns this socket, then it might be possible for that non-privileged user or process to interact with Docker daemon. Also, such a non-privileged user or process might interact with containers. This is neither secure nor desired behavior. Additionally, the Docker installer creates a Unix group called \'docker\'. You can add users to this group, and then those users would be able to read and write to default Docker Unix socket. The membership to the \'docker\' group is tightly controlled by the system administrator. If any other group owns this socket, then it might be possible for members of that group to interact with Docker daemon. Also, such a group might not be as tightly controlled as the \'docker\' group. This is neither secure nor desired behavior. Hence, the default Docker Unix socket file must be owned by \'root\' and group-owned by \'docker\' to maintain the integrity of the socket file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.15' - tag 'cis-docker-1.13.0': '3.15' tag 'level:1' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/#daemon-socket-option' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' - describe file('/var/run/docker.sock') do - it { should exist } - it { should be_socket } - it { should be_owned_by 'root' } - it { should be_grouped_into 'docker' } + target = input('docker_socket') + if target + describe file(target) do + it { should exist } + it { should be_socket } + it { should be_owned_by 'root' } + it { should be_grouped_into 'docker' } + end end end @@ -410,24 +382,17 @@ Rationale: Only \'root\' and members of \'docker\' group should be allowed to read and write to default Docker Unix socket. Hence, the Docket socket file must have permissions of \'660\' or more restrictive.' tag 'docker' - tag 'cis-docker-1.12.0': '3.16' - tag 'cis-docker-1.13.0': '3.16' tag 'level:1' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/#daemon-socket-option' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' - describe file('/var/run/docker.sock') do - it { should exist } - it { should be_socket } - it { should be_readable.by('owner') } - it { should be_writable.by('owner') } - it { should_not be_executable.by('owner') } - it { should be_readable.by('group') } - it { should be_writable.by('group') } - it { should_not be_executable.by('group') } - it { should_not be_readable.by('other') } - it { should_not be_writable.by('other') } - it { should_not be_executable.by('other') } + target = input('docker_socket') + if target + describe file(target) do + it { should exist } + it { should be_socket } + its('mode') { should satisfy('be 0660 or more restrictive') { |mode| (mode & (07777 ^ 0660)).zero? } } + end end end @@ -439,16 +404,18 @@ Rationale: \'daemon.json\' file contains sensitive parameters that may alter the behavior of docker daemon. Hence, it should be owned and group-owned by \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.17' - tag 'cis-docker-1.13.0': '3.17' - tag 'level:1' + tag 'level:2' ref 'dockerd', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/#miscellaneous-options' - describe file('/etc/docker/daemon.json') do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = input('docker_daemon_config') + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -460,25 +427,19 @@ Rationale: \'daemon.json\' file contains sensitive parameters that may alter the behavior of docker daemon. Hence, it should be writable only by \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.18' - tag 'cis-docker-1.13.0': '3.18' - tag 'level:1' + tag 'level:2' ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/#daemon-socket-option' ref 'Protect the Docker daemon socket', url: 'https://docs.docker.com/engine/security/https/' ref 'dockerd', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/#miscellaneous-options' - describe file('/etc/docker/daemon.json') do - it { should exist } - it { should be_file } - it { should be_readable.by('owner') } - it { should be_writable.by('owner') } - it { should_not be_executable.by('owner') } - it { should be_readable.by('group') } - it { should_not be_writable.by('group') } - it { should_not be_executable.by('group') } - it { should be_readable.by('other') } - it { should_not be_writable.by('other') } - it { should_not be_executable.by('other') } + target = input('docker_daemon_config') + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + its('mode') { should satisfy('be 0644 or more restrictive') { |mode| (mode & (07777 ^ 0644)).zero? } } + end end end @@ -490,17 +451,18 @@ Rationale: \'/etc/default/docker\' file contains sensitive parameters that may alter the behavior of docker daemon. Hence, it should be owned and group-owned by \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.19' - tag 'cis-docker-1.13.0': '3.19' - tag 'level:1' + tag 'level:2' ref 'Configure and troubleshoot the Docker daemon', url: 'https://docs.docker.com/engine/admin/' - only_if { os[:family] != 'centos' } - describe file('/etc/default/docker') do - it { should exist } - it { should be_file } - it { should be_owned_by 'root' } - it { should be_grouped_into 'root' } + target = '/etc/default/docker' + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end end end @@ -512,23 +474,104 @@ Rationale: \'/etc/default/docker\' file contains sensitive parameters that may alter the behavior of docker daemon. Hence, it should be writable only by \'root\' to maintain the integrity of the file.' tag 'docker' - tag 'cis-docker-1.12.0': '3.20' - tag 'cis-docker-1.13.0': '3.20' - tag 'level:1' + tag 'level:2' ref 'Configure and troubleshoot the Docker daemon', url: 'https://docs.docker.com/engine/admin/' - only_if { os[:family] != 'centos' } - describe file('/etc/default/docker') do - it { should exist } - it { should be_file } - it { should be_readable.by('owner') } - it { should be_writable.by('owner') } - it { should_not be_executable.by('owner') } - it { should be_readable.by('group') } - it { should_not be_writable.by('group') } - it { should_not be_executable.by('group') } - it { should be_readable.by('other') } - it { should_not be_writable.by('other') } - it { should_not be_executable.by('other') } + target = '/etc/default/docker' + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + its('mode') { should satisfy('be 0644 or more restrictive') { |mode| (mode & (07777 ^ 0644)).zero? } } + end + end +end + +control 'docker-3.21' do + impact 1.0 + title 'Verify that /etc/sysconfig/docker file permissions are set to 644 or more restrictive' + desc 'Verify that the /etc/sysconfig/docker file has permissions of 644 or more restrictive, if applicable. + + Rationale: This file can contain Docker daemon startup options. Restricting write access prevents unauthorized changes to the daemon configuration.' + + tag 'docker' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + target = '/etc/sysconfig/docker' + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + its('mode') { should satisfy('be 0644 or more restrictive') { |mode| (mode & (07777 ^ 0644)).zero? } } + end + end +end + +control 'docker-3.22' do + impact 1.0 + title 'Verify that /etc/sysconfig/docker file ownership is set to root:root' + desc 'Verify that the /etc/sysconfig/docker file is owned by root and group-owned by root, if applicable. + + Rationale: This file can contain Docker daemon startup options. Root ownership prevents untrusted users from changing the daemon configuration.' + + tag 'docker' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + target = '/etc/sysconfig/docker' + only_if('File does not exist on this host') { target && file(target).exist? } + if target + describe file(target) do + it { should exist } + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end + end +end + +control 'docker-3.23' do + impact 1.0 + title 'Verify that containerd socket file ownership is set to root:root' + desc 'Verify that the containerd socket is owned by root and group-owned by root. + + Rationale: The containerd socket provides access to the container runtime. Restrict its ownership to prevent unauthorized control of containers and the host.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + target = input('containerd_socket') + if target + describe file(target) do + it { should exist } + it { should be_socket } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + end + end +end + +control 'docker-3.24' do + impact 1.0 + title 'Verify that containerd socket file permissions are set to 660 or more restrictive' + desc 'Verify that the containerd socket has permissions of 660 or more restrictive. + + Rationale: Access to the containerd socket allows control of the container runtime. Restrictive permissions prevent untrusted users from accessing this interface.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + target = input('containerd_socket') + if target + describe file(target) do + it { should exist } + it { should be_socket } + its('mode') { should satisfy('be 0660 or more restrictive') { |mode| (mode & (07777 ^ 0660)).zero? } } + end end end diff --git a/controls/docker_security_operations.rb b/controls/docker_security_operations.rb index 4b81e62..8c88ebb 100644 --- a/controls/docker_security_operations.rb +++ b/controls/docker_security_operations.rb @@ -27,70 +27,6 @@ end control 'docker-6.1' do - impact 1.0 - title 'Perform regular security audits of your host system and containers' - desc 'Perform regular security audits of your host system and containers to identify any mis-configurations or vulnerabilities that could expose your system to compromise. - - Rationale: Performing regular and dedicated security audits of your host systems and containers could provide deep security insights that you might not know in your daily course of business. The identified security weaknesses should be then mitigated and this overall improves security posture of your environment.' - - tag 'docker' - tag 'cis-docker-1.12.0': '6.1' - tag 'cis-docker-1.13.0': '6.1' - tag 'level:1' - ref 'IT security auditing: Best practices for conducting audits', url: 'http://searchsecurity.techtarget.com/IT-security-auditing-Best-practices-for-conducting-audits' - - describe 'docker-test' do - skip 'Perform regular security audits of your host system and containers' - end -end - -control 'docker-6.2' do - impact 1.0 - title 'Monitor Docker containers usage, performance and metering' - desc 'Containers might run services that are critical for your business. Monitoring their usage, performance and metering would be of paramount importance. - - Rationale: Tracking container usage, performance and having some sort of metering around them would be important as you embrace the containers to run critical services for your business. This would give you - - Capacity Management and Optimization - Performance Management - Comprehensive Visibility - - Such a deep visibility of container performance would help you ensure high availability of containers and minimum downtime.' - - tag 'docker' - tag 'cis-docker-1.12.0': '6.2' - tag 'cis-docker-1.13.0': '6.2' - tag 'level:1' - ref 'Runtime metrics', url: 'https://docs.docker.com/engine/admin/runmetrics/' - ref 'cAdvisor (Container Advisor)', url: 'https://github.com/google/cadvisor' - ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' - - describe 'docker-test' do - skip 'Monitor Docker containers usage, performance and metering' - end -end - -control 'docker-6.3' do - impact 1.0 - title 'Backup container data' - desc 'Take regular backups of your container data volumes. - - Rationale: Containers might run services that are critical for your business. Taking regular data backups would ensure that if there is ever any loss of data you would still have your data in backup. The loss of data could be devastating for your business.' - - tag 'docker' - tag 'cis-docker-1.12.0': '6.3' - tag 'cis-docker-1.13.0': '6.3' - tag 'level:1' - ref 'Backups and disaster recovery', url: 'https://docs.docker.com/datacenter/ucp/2.2/guides/admin/backups-and-disaster-recovery/' - ref 'How can I backup a Docker-container with its data-volumes?', url: 'https://stackoverflow.com/questions/26331651/how-can-i-backup-a-docker-container-with-its-data-volumes' - ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' - - describe 'docker-test' do - skip 'Backup container data' - end -end - -control 'host-6.4' do impact 1.0 title 'Avoid image sprawl' desc 'Do not keep a large number of container images on the same host. Use only tagged images as appropriate. @@ -98,8 +34,6 @@ Rationale: Tagged images are useful to fall back from "latest" to a specific version of an image in production. Images with unused or old tags may contain vulnerabilities that might be exploited, if instantiated. Additionally, if you fail to remove unused images from the system and there are various such redundant and unused images, the host filesystem may become full and could lead to denial of service.' tag 'host' - tag 'cis-docker-1.12.0': '6.4' - tag 'cis-docker-1.13.0': '6.4' tag 'level:1' ref 'Clean up unused Docker Containers and Images', url: 'http://craiccomputing.blogspot.de/2014/09/clean-up-unused-docker-containers-and.html' ref 'Command to remove all unused images', url: 'https://forums.docker.com/t/command-to-remove-all-unused-images/20/8' @@ -107,16 +41,22 @@ ref 'Use the Docker command line', url: 'https://docs.docker.com/engine/reference/commandline/cli/' ref 'Add support for referring to images by digest', url: 'https://github.com/moby/moby/pull/11109' - instantiated_images = command('docker ps -qa | xargs docker inspect -f \'{{.Image}}\'').stdout.split - all_images = command('docker images -q --no-trunc').stdout.split - diff = all_images - instantiated_images + only_if('No images are present') { !docker.images.ids.empty? } + instantiated = docker_helper.all_containers_image_ids + allowed_count = input('allowed_unused_images_count').to_i + allowed_images = Array(input('allowed_unused_images')) + + unused = docker.images.ids.reject do |img_id| + refs = [img_id] + Array(docker.object(img_id)['RepoTags']) + Array(docker.object(img_id)['RepoDigests']) + instantiated.include?(img_id) || !(refs & allowed_images).empty? + end - describe diff do - it { should be_empty } + describe unused.length do + it { should be <= allowed_count } end end -control 'host-6.5' do +control 'docker-6.2' do impact 1.0 title 'Avoid container sprawl' desc 'Do not keep a large number of containers on the same host. @@ -124,17 +64,14 @@ Rationale: The flexibility of containers makes it easy to run multiple instances of applications and indirectly leads to Docker images that exist at varying security patch levels. It also means that you are consuming host resources that otherwise could have been used for running \'useful\' containers. Having more than just the manageable number of containers on a particular host makes the situation vulnerable to mishandling, misconfiguration and fragmentation. Thus, avoid container sprawl and keep the number of containers on a host to a manageable total.' tag 'host' - tag 'cis-docker-1.12.0': '6.5' - tag 'cis-docker-1.13.0': '6.5' tag 'level:1' ref 'Security Risks and Benefits of Docker Application Containers', url: 'https://zeltser.com/security-risks-and-benefits-of-docker-application/' ref 'Docker networking: How Linux containers will change your network', url: 'http://searchsdn.techtarget.com/feature/Docker-networking-How-Linux-containers-will-change-your-network' - total_on_host = command('docker info').stdout.split[1].to_i - total_running = command('docker ps -q').stdout.split.length - diff = total_on_host - total_running + only_if('No containers are present') { !docker.containers.ids.empty? } - describe diff do - it { should be <= MANAGEABLE_CONTAINER_NUMBER } + stopped = docker.containers.ids - docker.containers.running?.ids + describe stopped.length do + it { should be <= input('managable_container_number') } end end diff --git a/controls/docker_swarm_configuration.rb b/controls/docker_swarm_configuration.rb new file mode 100644 index 0000000..855901d --- /dev/null +++ b/controls/docker_swarm_configuration.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +# Copyright:: 2016, Patrick Muench +# Copyright:: 2017, Christoph Hartmann +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# author: Christoph Hartmann +# author: Dominik Richter +# author: Patrick Muench + +title 'Docker Swarm Configuration' + +# check if docker exists +only_if('docker not found') do + command('docker').exist? +end + +only_if('Swarm mode is inactive') { docker.info.dig('Swarm', 'LocalNodeState') == 'active' } + +control 'docker-7.1' do + impact 1.0 + title 'Control the number of manager nodes in a swarm' + desc 'Ensure that the minimum number of required manager nodes is created in a swarm. + + Rationale: Manager nodes within a swarm have control over the swarm and change its configuration modifying security parameters. Having excessive manager nodes could render the swarm more susceptible to compromise. If fault tolerance is not required in the manager nodes, a single node should be elected as a manager. If fault tolerance is required then the smallest practical odd number to achieve the appropriate level of tolerance should be configured.' + + tag 'docker' + tag 'level:1' + ref 'Manage nodes in a swarm', url: 'https://docs.docker.com/engine/swarm/manage-nodes/' + ref 'Administer and maintain a swarm of Docker Engines', url: 'https://docs.docker.com/engine/swarm/admin_guide/' + + only_if('This node is not a Swarm manager') { docker.info.dig('Swarm', 'ControlAvailable') == true } + describe docker.info do + its('Swarm.Managers') { should cmp > 0 } + its('Swarm.Managers') { should cmp <= input('swarm_max_manager_nodes') } + end +end + +control 'docker-7.2' do + impact 1.0 + title 'Bind swarm services to a specific host interface' + desc 'By default, the docker swarm services will listen to all interfaces on the host, which may not be necessary for the operation of the swarm where the host has multiple network interfaces. + + Rationale: When a swarm is initialized the default value for the --listen-addr flag is 0.0.0.0\': \'2377 which means that the swarm services will listen on all interfaces on the host. If a host has multiple network interfaces this may be undesirable as it may expose the docker swarm services to networks which are not involved in the operation of the swarm. By passing a specific IP address to the --listen-addr, a specific network interface can be specified limiting this exposure.' + + tag 'docker' + tag 'level:1' + ref 'docker swarm init', url: 'https://docs.docker.com/engine/reference/commandline/swarm_init/' + ref 'Administer and maintain a swarm of Docker Engines', url: 'https://docs.docker.com/engine/swarm/admin_guide/' + + [input('swarm_port'), 7946].each do |swarm_port| + describe port(swarm_port) do + its('addresses') { should_not include '0.0.0.0' } + its('addresses') { should_not include '::' } + end + end +end + +control 'docker-7.3' do + impact 1.0 + title 'Encrypt data exchanged between containers on different nodes on the overlay network' + desc 'Encrypt data exchanged between containers on different nodes on the overlay network. + + Rationale: By default, data exchanged between containers on different nodes on the overlay network is not encrypted. This could potentially expose traffic between the container nodes.' + + tag 'docker' + tag 'level:1' + ref 'Docker swarm mode overlay network security model', url: 'https://docs.docker.com/engine/userguide/networking/overlay-security-model/' + ref 'Docker swarm container-container traffic not encrypted when inspecting externally with tcpdump', url: 'https://github.com/moby/moby/issues/24253' + + networks = docker_helper.overlay_networks + only_if('No overlay networks are present') { !networks.empty? } + networks.each do |id, options| + describe "Overlay network #{id} encryption" do + subject { options } + it { should include 'encrypted' } + end + end +end + +control 'docker-7.4' do + impact 1.0 + title 'Use Docker\'s secret management commands for managing secrets in a Swarm cluster' + desc 'Use Docker\'s in-built secret management command. + + Rationale: Docker has various commands for managing secrets in a Swarm cluster. This is the foundation for future secret support in Docker with potential improvements such as Windows support, different backing stores, etc.' + + tag 'docker' + tag 'level:1' + ref 'Secret Management', url: 'https://github.com/moby/moby/pull/27794' + + only_if('This node is not a Swarm manager') { docker.info.dig('Swarm', 'ControlAvailable') == true } + describe command('docker secret ls -q') do + its('exit_status') { should eq 0 } + its('stdout.strip') { should_not be_empty } + end +end + +control 'docker-7.5' do + impact 1.0 + title 'Run swarm manager in auto-lock mode' + desc 'Run Docker swarm manager in auto-lock mode. + + Rationale: When Docker restarts, both the TLS key used to encrypt communication among swarm nodes, and the key used to encrypt and decrypt Raft logs on disk, are loaded into each manager node\'s memory. You should protect the mutual TLS encryption key and the key used to encrypt and decrypt Raft logs at rest. This protection could be enabled by initializing swarm with --autolock flag. With --autolock enabled, when Docker restarts, you must unlock the swarm first, using a key encryption key generated by Docker when the swarm was initialized.' + + tag 'docker' + tag 'level:1' + ref 'Initialize a swarm with autolocking enabled', url: 'https://github.com/mistyhacks/docker.github.io/blob/af7dfdba8504f9b102fb31a78cd08a06c33a8975/engine/swarm/swarm_manager_locking.md' + + only_if('This node is not a Swarm manager') { docker.info.dig('Swarm', 'ControlAvailable') == true } + describe docker.info do + its('Swarm.Cluster.Spec.EncryptionConfig.AutoLockManagers') { should eq true } + end +end + +control 'docker-7.6' do + impact 1.0 + title 'Rotate swarm manager auto-lock key periodically' + desc 'Rotate swarm manager auto-lock key periodically. + + Rationale: Swarm manager auto-lock key is not automatically rotated. You should rotate them periodically as a best practice. + + Audit: Currently, there is no mechanism to find out when the key was last rotated on a swarm manager node. You should check with the system administrator if there is a key rotation record and the keys were rotated at a pre-defined frequency.' + + tag 'docker' + tag 'level:1' + ref 'Swarm Key rotation', url: 'https://github.com/mistyhacks/docker.github.io/blob/af7dfdba8504f9b102fb31a78cd08a06c33a8975/engine/swarm/swarm_manager_locking.md' + + describe 'docker-test' do + skip 'Manually verify the Swarm manager unlock key rotation process and evidence of periodic rotation.' + end +end + +control 'docker-7.7' do + impact 1.0 + title 'Rotate swarm node certificates as appropriate' + desc 'Rotate swarm node certificates in accordance with organizational policy. + + Rationale: Swarm uses TLS certificates to authenticate nodes. Regular rotation limits the period during which a compromised certificate can be used to impersonate a node.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + only_if('This node is not a Swarm manager') { docker.info.dig('Swarm', 'ControlAvailable') == true } + describe docker.info do + its('Swarm.Cluster.Spec.CAConfig.NodeCertExpiry') { should cmp > 0 } + its('Swarm.Cluster.Spec.CAConfig.NodeCertExpiry') { should cmp <= input('swarm_node_cert_expiry_days') * 86_400 * 1_000_000_000 } + end +end + +control 'docker-7.8' do + impact 1.0 + title 'Rotate swarm CA certificates as appropriate' + desc 'Rotate swarm CA certificates in accordance with organizational policy. + + Rationale: The swarm CA signs node certificates. Regular rotation limits the continued use of a compromised CA and certificates issued by it.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + only_if('This node is not a Swarm manager') { docker.info.dig('Swarm', 'ControlAvailable') == true } + + certificate = "#{docker.info['DockerRootDir']}/swarm/certificates/swarm-root-ca.crt" + describe input('swarm_ca_rotation_days') do + it { should be > 0 } + end + describe file(certificate) do + it { should exist } + its('mtime.to_i') { should be >= Time.now.to_i - input('swarm_ca_rotation_days') * 86_400 } + end +end + +control 'docker-7.9' do + impact 1.0 + title 'Separate management plane traffic from data plane traffic' + desc 'Use separate network interfaces for swarm management traffic and container data traffic. + + Rationale: Separating these traffic types limits exposure of the swarm management interface to application networks.' + + tag 'docker' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + only_if('Swarm mode is inactive') { docker.info.dig('Swarm', 'LocalNodeState') == 'active' } + + state = "#{docker.info['DockerRootDir']}/swarm/docker-state.json" + describe file(state) do + it { should exist } + end + if file(state).exist? + describe json(state) do + its(['DataPathAddr']) { should_not be_nil } + its(['DataPathAddr']) { should_not eq '' } + its(['DataPathAddr']) { should_not eq docker.info.dig('Swarm', 'NodeAddr') } + end + end +end diff --git a/controls/host_configuration.rb b/controls/host_configuration.rb index 0004389..116d4cf 100644 --- a/controls/host_configuration.rb +++ b/controls/host_configuration.rb @@ -21,16 +21,12 @@ title 'Host Configuration' -TRUSTED_USER = input('trusted_user') -MANAGEABLE_CONTAINER_NUMBER = input('managable_container_number') -BENCHMARK_VERSION = input('benchmark_version') - # check if docker exists only_if('docker not found') do command('docker').exist? end -control 'host-1.1' do +control 'host-1.1.1' do impact 1.0 title 'Create a separate partition for containers' desc 'All Docker containers and their data and metadata is stored under /var/lib/docker directory. By default, /var/lib/docker would be mounted under / or /var partitions based on availability. @@ -38,100 +34,15 @@ Rationale: Docker depends on /var/lib/docker as the default directory where all Docker related files, including the images, are stored. This directory might fill up fast and soon Docker and the host could become unusable. So, it is advisable to create a separate partition (logical volume) for storing Docker files.' tag 'host' - tag 'cis-docker-1.12.0': '1.1' - tag 'cis-docker-1.13.0': '1.1' tag 'level:1' ref 'Docker storage recommendation', url: 'http://www.projectatomic.io/docs/docker-storage-recommendation/' - describe mount('/var/lib/docker') do + describe mount(docker.info['DockerRootDir']) do it { should be_mounted } end end -control 'host-1.2' do - impact 1.0 - title 'Use the updated Linux Kernel' - desc 'Docker in daemon mode has specific kernel requirements. A 3.10 Linux kernel is the minimum requirement for Docker.' - - tag 'host' - tag 'cis-docker-1.12.0': '1.2' - tag 'level:1' - ref 'Check kernel dependencies', url: 'https://docs.docker.com/engine/installation/binaries/#check-kernel-dependencies' - ref 'Installation list', url: 'https://docs.docker.com/engine/installation/#installation-list' - - only_if { os.linux? } - kernel_version = command('uname -r | grep -o \'^\w\.\w*\.\w*\'').stdout - kernel_compare = Gem::Version.new('3.10') <= Gem::Version.new(kernel_version) - describe kernel_compare do - it { should eq true } - end - only_if { BENCHMARK_VERSION == '1.12.0' } -end - -control 'host-1.3' do - impact 1.0 - title 'Harden the container host' - desc 'Containers run on a Linux host. A container host can run one or more containers. It is of utmost importance to harden the host to mitigate host security misconfiguration. - - Rationale: You should follow infrastructure security best practices and harden your host OS. Keeping the host system hardened would ensure that the host vulnerabilities are mitigated. Not hardening the host system could lead to security exposures and breaches. You can use the dev-sec.io Hardening Framework for this task - - By default, host has factory settings. It is not hardened.' - - tag 'host' - tag 'cis-docker-1.12.0': '1.3' - tag 'cis-docker-1.13.0': '1.2' - tag 'level:1' - ref 'Dev-Sec Hardening Framework', url: 'http://dev-sec.io/' - ref 'Secure Engine', url: 'https://docs.docker.com/engine/security/' - ref 'Center of Internet Security Benchmarks', url: 'https://learn.cisecurity.org/benchmarks' - ref 'Grsecurity', url: 'https://grsecurity.net/' - ref 'Grsecurity Wiki', url: 'https://en.wikibooks.org/wiki/Grsecurity' - ref 'PAX Security', url: 'https://pax.grsecurity.net/' - ref 'PAX Security Wiki', url: 'https://en.wikipedia.org/wiki/PaX' - - describe 'docker-test' do - skip 'Harden the container host. Use the Dev-Sec Hardening Framework' - end -end - -control 'host-1.4' do - impact 1.0 - title 'Remove all non-essential services from the host' - desc 'Ensure that the host running the docker daemon is running only the essential services.' - - tag 'host' - tag 'cis-docker-1.12.0': '1.4' - tag 'level:1' - ref 'Containers & Docker: How Secure Are They?', url: 'https://blog.docker.com/2013/08/containers-docker-how-secure-are-they/' - ref 'Dev-Sec Hardening Framework', url: 'http://dev-sec.io/' - - describe 'docker-test' do - skip 'Remove all non-essential services from the host. Use the Dev-Sec Hardening Framework' - end -end - -control 'host-1.5' do - impact 1.0 - title 'Keep Docker up to date' - desc 'There are frequent releases for Docker software that address security vulnerabilities,product bugs and bring in new functionality. Keep a tab on these product updates and upgrade as frequently as when new security vulnerabilities are fixed or deemed correct for your organization. - - Rationale: By staying up to date on Docker updates, vulnerabilities in the Docker software can be mitigated. An educated attacker may exploit known vulnerabilities when attempting to attain access or elevate privileges. Not installing regular Docker updates may leave you ith running vulnerable Docker software. It might lead to elevation privileges, unauthorized access or other security breaches. Keep a track of new releases and update as necessary.' - - tag 'host' - tag 'cis-docker-1.12.0': '1.5' - tag 'cis-docker-1.13.0': '1.3' - tag 'level:1' - ref 'Docker installation', url: 'https://docs.docker.com/engine/installation/' - ref 'Docker releases', url: 'https://github.com/moby/moby/releases/tag/v17.03.2-ce' - ref 'About Docker EE', url: 'https://docs.docker.com/enterprise/' - - describe docker do - its('version.Client.Version') { should cmp >= '17.06' } - its('version.Server.Version') { should cmp >= '17.06' } - end -end - -control 'host-1.6' do +control 'host-1.1.2' do impact 1.0 title 'Only allow trusted users to control Docker daemon' desc 'The Docker daemon currently requires \'root\' privileges. A user added to the \'docker\' group gives him full \'root\' access rights. @@ -139,23 +50,20 @@ Rationale: Docker allows you to share a directory between the Docker host and a guest container without limiting the access rights of the container. This means that you can start a container and map the / directory on your host to the container. The container will then be able to alter your host file system without any restrictions. In simple terms, it means that you can attain elevated privileges with just being a member of the \'docker\' group and then starting a container with mapped / directory on the host.' tag 'host' - tag 'cis-docker-1.12.0': '1.6' - tag 'cis-docker-1.13.0': '1.4' tag 'level:1' ref 'Docker Engine Security', url: 'https://docs.docker.com/engine/security/' ref 'On Docker security: \'docker\' group considered harmful', url: 'https://www.zopyx.com/andreas-jung/contents/on-docker-security-docker-group-considered-harmful' ref 'Why we don\'t let non-root users run Docker in CentOS, Fedora, or RHEL', url: 'http://www.projectatomic.io/blog/2015/08/why-we-dont-let-non-root-users-run-docker-in-centos-fedora-or-rhel/' - describe group('docker') do - it { should exist } - end - - describe etc_group.where(group_name: 'docker') do - its('users') { should include TRUSTED_USER } + # An absent docker group grants no access. Existing members must all be trusted. + members = group('docker').exists? ? group('docker').members : [] + members = members.split(',') if members.is_a?(String) + describe members - input('trusted_users') do + it { should be_empty } end end -control 'host-1.7' do +control 'host-1.1.3' do impact 1.0 title 'Audit docker daemon' desc 'Audit all Docker daemon activities. @@ -163,23 +71,50 @@ Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker daemon as well. Docker daemon runs with \'root\' privileges. It is thus necessary to audit its activities and usage.' tag 'host' - tag 'cis-docker-1.12.0': '1.7' - tag 'cis-docker-1.13.0': '1.5' tag 'level:1' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' - only_if { os.linux? } - describe auditd do - its(:lines) { should include('-w /usr/bin/docker -p rwxa -k docker') } + audit_path = input('docker_daemon_path') + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end end describe service('auditd') do - it { should be_installed } it { should be_enabled } it { should be_running } end end -control 'host-1.8' do +control 'host-1.1.4' do + impact 1.0 + title 'Audit Docker files and directories - /run/containerd' + desc 'Audit /run/containerd, if applicable. + + Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker related files and directories. This directory contains containerd runtime state. Auditing changes helps identify unauthorized access or modification.' + + tag 'host' + tag 'level:1' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + audit_path = '/run/containerd' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.1.5' do impact 1.0 title 'Audit Docker files and directories - /var/lib/docker' desc 'Audit /var/lib/docker. @@ -187,18 +122,22 @@ Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /var/lib/docker is one such directory. It holds all the information about containers. It must be audited.' tag 'host' - tag 'cis-docker-1.12.0': '1.8' - tag 'cis-docker-1.13.0': '1.6' tag 'level:1' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' - only_if { os.linux? } - describe auditd do - its(:lines) { should include('-w /var/lib/docker -p rwxa -k docker') } + audit_path = docker.info['DockerRootDir'] + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end end end -control 'host-1.9' do +control 'host-1.1.6' do impact 1.0 title 'Audit Docker files and directories - /etc/docker' desc 'Audit /etc/docker. @@ -206,18 +145,22 @@ Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /etc/docker is one such directory. It holds various certificates and keys used for TLS communication between Docker daemon and Docker client. It must be audited.' tag 'host' - tag 'cis-docker-1.12.0': '1.9' - tag 'cis-docker-1.13.0': '1.7' tag 'level:1' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' - only_if { os.linux? } - describe auditd do - its(:lines) { should include('-w /etc/docker -p rwxa -k docker') } + audit_path = '/etc/docker' + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end end end -control 'host-1.10' do +control 'host-1.1.7' do impact 1.0 title 'Audit Docker files and directories - docker.service' desc 'Audit docker.service, if applicable. @@ -225,51 +168,71 @@ Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. docker.service is one such file. The docker.service file might be present if the daemon parameters have been changed by an administrator. It holds various parameters for Docker daemon. It must be audited, if applicable.' tag 'host' - tag 'cis-docker-1.12.0': '1.10' - tag 'cis-docker-1.13.0': '1.8' - tag 'level:1' + tag 'level:2' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' - only_if { os.linux? } - if docker_helper.path - rule = "-w #{docker_helper.path} -p rwxa -k docker" - describe auditd do - its(:lines) { should include(rule) } + audit_path = docker_helper.path + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } end - else - describe 'audit docker service' do - skip 'Cannot determine docker path' + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } end end end -control 'host-1.11' do +control 'host-1.1.8' do impact 1.0 - title 'Audit Docker files and directories - docker.socket' - desc 'Audit docker.socket, if applicable. + title 'Audit Docker files and directories - containerd.sock' + desc 'Audit containerd.sock, if applicable. - Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. docker.socket is one such file. It holds various parameters for Docker daemon socket. It must be audited, if applicable.' + Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker related files and directories. This socket provides access to the containerd API. Auditing changes helps identify unauthorized access or modification.' tag 'host' - tag 'cis-docker-1.12.0': '1.11' - tag 'cis-docker-1.13.0': '1.9' - tag 'level:1' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + audit_path = input('containerd_socket') + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.1.9' do + impact 1.0 + title 'Audit Docker files and directories - docker.sock' + desc 'Audit docker.sock, if applicable. + + Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. docker.sock is one such file. It holds various parameters for Docker daemon socket. It must be audited, if applicable.' + + tag 'host' + tag 'level:2' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' - only_if { os.linux? } - if docker_helper.socket - rule = "-w #{docker_helper.socket} -p rwxa -k docker" - describe auditd do - its(:lines) { should include(rule) } + audit_path = input('docker_socket') + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } end - else - describe 'audit docker service' do - skip 'Cannot determine docker socket' + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } end end end -control 'host-1.12' do +control 'host-1.1.10' do impact 1.0 title 'Audit Docker files and directories - /etc/default/docker' desc 'Audit /etc/default/docker , if applicable. @@ -277,18 +240,23 @@ Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /etc/default/docker is one such file. It holds various parameters for Docker daemon. It must be audited, if applicable.' tag 'host' - tag 'cis-docker-1.12.0': '1.12' - tag 'cis-docker-1.13.0': '1.10' - tag 'level:1' + tag 'level:2' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' - only_if { os.linux? } - describe auditd do - its(:lines) { should include('-w /etc/default/docker -p rwxa -k docker') } + audit_path = '/etc/default/docker' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end end end -control 'host-1.13' do +control 'host-1.1.11' do impact 1.0 title 'Audit Docker files and directories - /etc/docker/daemon.json' desc 'Audit /etc/docker/daemon.json, if applicable. @@ -296,57 +264,238 @@ Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /etc/docker/daemon.json is one such file. It holds various parameters for Docker daemon. It must be audited, if applicable.' tag 'host' - tag 'cis-docker-1.12.0': '1.13' - tag 'cis-docker-1.13.0': '1.11' - tag 'level:1' + tag 'level:2' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' ref 'Daemon configuration', url: 'https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file' - only_if { os.linux? } - describe auditd do - its(:lines) { should include('-w /etc/docker/daemon.json -p rwxa -k docker') } + audit_path = input('docker_daemon_config') + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end end end -control 'host-1.14' do +control 'host-1.1.12' do impact 1.0 - title 'Audit Docker files and directories - /usr/bin/docker-containerd' - desc 'Audit /usr/bin/docker-containerd, if applicable. + title 'Audit Docker files and directories - /etc/containerd/config.toml' + desc 'Audit /etc/containerd/config.toml, if applicable. - Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /usr/bin/docker-containerd is one such file. Docker now relies on containerd and runC to spawn containers. It must be audited, if applicable.' + Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker related files and directories. This file contains containerd configuration. Auditing changes helps identify unauthorized access or modification.' tag 'host' - tag 'cis-docker-1.12.0': '1.14' - tag 'cis-docker-1.13.0': '1.12' - tag 'level:1' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + audit_path = '/etc/containerd/config.toml' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.1.13' do + impact 1.0 + title 'Audit Docker files and directories - /etc/sysconfig/docker' + desc 'Audit /etc/sysconfig/docker, if applicable. + + Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker related files and directories. This file can contain Docker daemon startup options. Auditing changes helps identify unauthorized access or modification.' + + tag 'host' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + audit_path = '/etc/sysconfig/docker' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.1.14' do + impact 1.0 + title 'Audit Docker files and directories - /usr/bin/containerd' + desc 'Audit /usr/bin/containerd, if applicable. + + Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /usr/bin/containerd is one such file. Docker now relies on containerd and runC to spawn containers. It must be audited, if applicable.' + + tag 'host' + tag 'level:2' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' ref 'Containerd integration', url: 'https://github.com/docker/docker/pull/20662' ref 'Containerd tools', url: 'https://containerd.tools/' - only_if { os.linux? } - describe auditd do - its(:lines) { should include('-w /usr/bin/docker-containerd -p rwxa -k docker') } + audit_path = '/usr/bin/containerd' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end end end -control 'host-1.15' do +control 'host-1.1.15' do impact 1.0 - title 'Audit Docker files and directories - /usr/bin/docker-runc' - desc 'Audit /usr/bin/docker-runc, if applicable. + title 'Audit Docker files and directories - /usr/bin/containerd-shim' + desc 'Audit /usr/bin/containerd-shim, if applicable. - Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /usr/bin/docker-runc is one such file. Docker now relies on containerd and runC to spawn containers. It must be audited, if applicable.' + Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker related files and directories. This executable manages the lifecycle of container processes. Auditing changes helps identify unauthorized access or modification.' tag 'host' - tag 'cis-docker-1.12.0': '1.15' - tag 'cis-docker-1.13.0': '1.13' - tag 'level:1' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + audit_path = '/usr/bin/containerd-shim' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.1.16' do + impact 1.0 + title 'Audit Docker files and directories - /usr/bin/containerd-shim-runc-v1' + desc 'Audit /usr/bin/containerd-shim-runc-v1, if applicable. + + Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker related files and directories. This executable connects containerd to the runC runtime. Auditing changes helps identify unauthorized access or modification.' + + tag 'host' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + audit_path = '/usr/bin/containerd-shim-runc-v1' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.1.17' do + impact 1.0 + title 'Audit Docker files and directories - /usr/bin/containerd-shim-runc-v2' + desc 'Audit /usr/bin/containerd-shim-runc-v2, if applicable. + + Rationale: Apart from auditing your regular Linux file system and system calls, audit Docker related files and directories. This executable connects containerd to the runC runtime. Auditing changes helps identify unauthorized access or modification.' + + tag 'host' + tag 'level:2' + ref 'CIS Docker Benchmark v1.8.0', url: 'https://www.cisecurity.org/benchmark/docker' + + audit_path = '/usr/bin/containerd-shim-runc-v2' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.1.18' do + impact 1.0 + title 'Audit Docker files and directories - /usr/bin/runc' + desc 'Audit /usr/bin/runc, if applicable. + + Rationale: Apart from auditing your regular Linux file system and system calls, audit all Docker related files and directories. Docker daemon runs with \'root\' privileges. Its behavior depends on some key files and directories. /usr/bin/runc is one such file. Docker now relies on containerd and runC to spawn containers. It must be audited, if applicable.' + + tag 'host' + tag 'level:2' ref 'System auditing', url: 'https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html' ref 'Containerd integration', url: 'https://github.com/docker/docker/pull/20662' ref 'Containerd tools', url: 'https://containerd.tools/' ref 'Opencontainers runc repository', url: 'https://github.com/opencontainers/runc' - only_if { os.linux? } - describe auditd do - its(:lines) { should include('-w /usr/bin/docker-runc -p rwxa -k docker') } + audit_path = '/usr/bin/runc' + only_if('Audited file does not exist on this host') { audit_path && file(audit_path).exist? } + if audit_path + describe file(audit_path) do + it { should exist } + end + describe command('auditctl -l') do + its('exit_status') { should eq 0 } + its('stdout') { should match(/(?:-w\s+|-F\s+(?:path|dir)=)#{Regexp.escape(audit_path)}(?:\s|$)/) } + end + end +end + +control 'host-1.2.1' do + impact 1.0 + title 'Harden the container host' + desc 'Containers run on a Linux host. A container host can run one or more containers. It is of utmost importance to harden the host to mitigate host security misconfiguration. + + Rationale: You should follow infrastructure security best practices and harden your host OS. Keeping the host system hardened would ensure that the host vulnerabilities are mitigated. Not hardening the host system could lead to security exposures and breaches. You can use the dev-sec.io Hardening Framework for this task + + By default, host has factory settings. It is not hardened.' + + tag 'host' + tag 'level:1' + ref 'Dev-Sec Hardening Framework', url: 'http://dev-sec.io/' + ref 'Secure Engine', url: 'https://docs.docker.com/engine/security/' + ref 'Center of Internet Security Benchmarks', url: 'https://learn.cisecurity.org/benchmarks' + ref 'Grsecurity', url: 'https://grsecurity.net/' + ref 'Grsecurity Wiki', url: 'https://en.wikibooks.org/wiki/Grsecurity' + ref 'PAX Security', url: 'https://pax.grsecurity.net/' + ref 'PAX Security Wiki', url: 'https://en.wikipedia.org/wiki/PaX' + + describe 'docker-test' do + skip 'Harden the container host. Use the Dev-Sec Hardening Framework' + end +end + +control 'host-1.2.2' do + impact 1.0 + title 'Keep Docker up to date' + desc 'There are frequent releases for Docker software that address security vulnerabilities,product bugs and bring in new functionality. Keep a tab on these product updates and upgrade as frequently as when new security vulnerabilities are fixed or deemed correct for your organization. + + Rationale: By staying up to date on Docker updates, vulnerabilities in the Docker software can be mitigated. An educated attacker may exploit known vulnerabilities when attempting to attain access or elevate privileges. Not installing regular Docker updates may leave you ith running vulnerable Docker software. It might lead to elevation privileges, unauthorized access or other security breaches. Keep a track of new releases and update as necessary.' + + tag 'host' + tag 'level:1' + ref 'Docker installation', url: 'https://docs.docker.com/engine/installation/' + ref 'Docker releases', url: 'https://github.com/moby/moby/releases/tag/v17.03.2-ce' + ref 'About Docker EE', url: 'https://docs.docker.com/enterprise/' + + describe docker do + its('version.Client.Version') { should cmp >= input('docker_min_version') } + its('version.Server.Version') { should cmp >= input('docker_min_version') } + end + describe 'docker-test' do + skip 'Review vendor security advisories for the installed Docker version; the minimum version check does not establish vulnerability status.' end end diff --git a/inspec.yml b/inspec.yml index d76837e..5318456 100644 --- a/inspec.yml +++ b/inspec.yml @@ -5,107 +5,161 @@ maintainer: DevSec Hardening Framework Team copyright: DevSec Hardening Framework Team copyright_email: hello@dev-sec.io license: Apache-2.0 -summary: An InSpec Compliance Profile for the CIS Docker Benchmark -version: 2.1.4 -inspec_version: '>= 4.6.3' +summary: An InSpec Compliance Profile for CIS Docker Benchmark v1.8.0 +version: 3.0.0 +inspec_version: ">= 4.6.3" attributes: - - name: container_user - required: false - description: 'define user within containers.' - value: 'ubuntu' - type: string - - name: container_capadd - required: true - description: 'define needed capabilities for containers.' - type: string - value: NET_ADMIN,SYS_ADMIN - - name: app_armor_profile - required: false - description: 'define apparmor profile for Docker containers.' - value: 'docker-default' - type: string - - name: selinux_profile - required: false - description: 'define SELinux profile for Docker containers.' - value: label:level:s0-s0:c1023 - type: string - - name: trusted_user - required: false - description: 'define trusted user to control Docker daemon.' - value: vagrant - type: string - - name: managable_container_number - required: true - description: 'keep number of containers on a host to a manageable total.' - value: 25 - type: numeric - - name: benchmark_version - required: true - description: 'to execute also the old controls from previous benchmarks. to execute the controls, define the value as 1.12.0' - type: string - value: 1.12.0 - - name: registry_cert_path - required: true - description: 'directory contains various Docker registry directories.' - value: '/etc/docker/certs.d' - type: string - - name: registry_name - required: true - description: 'directory contain certificate certain Docker registry.' - value: '/etc/docker/certs.d/registry_hostname:port' - type: string - - name: registry_ca_file - required: false - description: 'directory contain certificate certain Docker registry.' - value: '/etc/docker/certs.d/registry_hostname:port/ca.crt' - type: string - - name: daemon_tlscacert - required: false - description: 'Trust certs signed only by this CA' - value: '/etc/docker/ssl/ca.pem' - type: string - - name: daemon_tlscert - required: false - description: 'Path to TLS certificate file' - value: '/etc/docker/ssl/server_cert.pem' - type: string - - name: daemon_tlskey - required: false - description: 'Path to TLS key file' - value: '/etc/docker/ssl/server_key.pem' - type: string - - name: authorization_plugin - required: false - description: 'define authorization plugin to manage access to Docker daemon.' - value: 'authz-broker' - type: string - - name: log_driver - required: false - description: 'define preferable way to store logs.' - value: 'syslog' - type: string - - name: log_opts - required: false - description: 'define Docker daemon log-opts.' - value: syslog-address - type: string - - name: swarm_mode - required: false - description: 'define the swarm mode, `active` or `inactive`' - value: inactive - type: string - - name: swarm_max_manager_nodes - required: false - description: 'number of manager nodes in a swarm' - value: 3 - type: numeric - - name: swarm_port - required: false - description: 'port of the swarm node' - value: 2377 - type: numeric - - name: seccomp_default_profile - required: false - description: 'define the default seccomp profile' - value: 'default' - type: string +- name: container_capadd + required: true + description: define needed capabilities for containers, separated by commas. + type: string + value: NET_ADMIN,SYS_ADMIN +- name: seccomp_default_profile + required: false + description: define the default seccomp profile. + value: default + type: string +- name: default_ulimits + description: approved daemon default ulimits; adjust these limits for the workload. + type: hash + value: + nproc: + Soft: 1024 + Hard: 2408 + nofile: + Soft: 100 + Hard: 200 +- name: docker_cgroup_parent + description: approved daemon cgroup parent; an empty string uses Docker's default. + type: string + value: "" +- name: docker_min_version + description: minimum approved Docker client and server version; review vendor security advisories separately. + type: string + value: "28.0.0" +- name: docker_cli_path + description: Docker CLI executable whose execution must be recorded under the docker audit key. + type: string + value: "/usr/bin/docker" +- name: swarm_node_cert_expiry_days + description: maximum Swarm node certificate lifetime in days. + type: numeric + value: 90 +- name: swarm_ca_rotation_days + description: maximum age of the Swarm root CA file in days; set from organizational policy. + type: numeric + value: 90 +- name: approved_container_ports + description: approved published container ports, such as 443/tcp; an empty list permits no published ports. + type: array + value: [] +- name: prohibited_packages + description: list of prohibited packages in containers. + type: array + value: + - gcc + - g++ + - gdb + - tcpdump + - wireshark + - telnet + - netcat + - nc +- name: whitelisted_suid_binaries + description: approved setuid and setgid binaries in containers. + type: array + value: + - /usr/bin/passwd + - /usr/bin/su + - /usr/bin/sudo + - /usr/bin/newgrp + - /usr/bin/chfn + - /usr/bin/chsh +- name: image_max_age_days + description: maximum acceptable age of images in days before rebuild is required. + type: numeric + value: 90 +- name: allowed_unused_images_count + description: allowable number of unused/cached images on host before image sprawl is flagged. + type: numeric + value: 5 +- name: allowed_unused_images + description: list of approved unused image IDs or names (e.g. rollback images, base images). + type: array + value: [] +- name: allowed_latest_tag_images + description: list of container images allowed to use the :latest tag without digest pinning. + type: array + value: [] +- name: signed_artifacts + description: artifacts to verify, each with absolute path, signature, and public_key paths on the target; signatures use SHA-256 and RSA or EC public keys. + type: array + value: [] +- name: dockerfile_paths + description: Dockerfiles on the target to check for embedded secrets in addition to image history. + type: array + value: [] +- name: managable_container_number + required: true + description: keep number of containers on a host to a manageable total. + value: 25 + type: numeric +- name: registry_cert_path + required: true + description: directory contains various Docker registry directories. + value: "/etc/docker/certs.d" + type: string +- name: authorization_plugin + required: false + description: define authorization plugin to manage access to Docker daemon. + value: authz-broker + type: string +- name: log_driver + required: false + description: define preferable way to store logs. + value: syslog + type: string +- name: log_opts + required: false + description: define Docker daemon log-opts. + value: syslog-address + type: string +- name: swarm_mode + required: false + description: define the swarm mode, `active` or `inactive` + value: inactive + type: string +- name: swarm_max_manager_nodes + required: false + description: number of manager nodes in a swarm + value: 3 + type: numeric +- name: swarm_port + required: false + description: port of the swarm node + value: 2377 + type: numeric +- name: trusted_users + description: Approved members of the docker group. All actual members must be in + this list. + type: array + value: [] +- name: docker_daemon_config + description: Docker daemon configuration file; set this to the effective --config-file + path when customized. + type: string + value: "/etc/docker/daemon.json" +- name: docker_daemon_path + description: Path to the dockerd executable for audit rules. + type: string + value: "/usr/bin/dockerd" +- name: docker_socket + description: Path to the Docker API Unix socket. + type: string + value: "/var/run/docker.sock" +- name: containerd_socket + description: Path to the containerd gRPC socket. + type: string + value: "/run/containerd/containerd.sock" +supports: +- platform-family: linux diff --git a/libraries/docker_helper.rb b/libraries/docker_helper.rb index 54bbb2a..695d343 100644 --- a/libraries/docker_helper.rb +++ b/libraries/docker_helper.rb @@ -18,6 +18,9 @@ # author: Dominik Richter # author: Patrick Muench +require 'json' +require 'shellwords' + class DockerHelper < Inspec.resource(1) name 'docker_helper' @@ -29,57 +32,303 @@ def path cmd = inspec.command('systemctl show -p FragmentPath docker.service') return if cmd.exit_status.to_i.nonzero? - # parse data - params = parse_systemd_values(cmd.stdout.chomp) - - # return the value - params['FragmentPath'] + value = cmd.stdout.lines.find { |line| line.start_with?('FragmentPath=') }.to_s.split('=', 2).last.to_s.strip + value.empty? ? nil : value end def socket cmd = inspec.command('systemctl show -p FragmentPath docker.socket') return if cmd.exit_status.to_i.nonzero? - # parse data - params = parse_systemd_values(cmd.stdout.chomp) - - # return the value - params['FragmentPath'] + value = cmd.stdout.lines.find { |line| line.start_with?('FragmentPath=') }.to_s.split('=', 2).last.to_s.strip + value.empty? ? nil : value end def overlay_networks - cmd = inspec.command('docker network ls -f driver=overlay -q') - return if cmd.exit_status.to_i.nonzero? + checked_output('docker network ls -f driver=overlay -q').split.to_h do |id| + network = JSON.parse(checked_output("docker network inspect #{Shellwords.escape(id)}")).fetch(0) + [id, network.fetch('Options', {}) || {}] + end + end + + # Read both daemon.json and startup flags, including Docker's secure defaults + # when an option is absent. Do not mistake failed evidence collection for a + # daemon with default settings. + def daemon_configuration(config_path = '/etc/docker/daemon.json') + commands = inspec.processes('dockerd').commands.uniq + candidates = commands.map { |command| parse_daemon_flags(command) } + candidates.select! { |flags| flags.fetch('config-file', '/etc/docker/daemon.json') == config_path } if candidates.length > 1 + candidates.uniq! + raise Inspec::Exceptions::ResourceFailed, 'Multiple Docker daemons match the configured path; select their config file explicitly' if candidates.length > 1 + + flags = candidates.first || {} + config = inspec.file(flags.delete('config-file') || config_path) + values = config.exist? ? JSON.parse(config.content) : {} + raise Inspec::Exceptions::ResourceFailed, 'daemon.json must contain an object' unless values.is_a?(Hash) + raise Inspec::Exceptions::ResourceFailed, 'Duplicate daemon settings in JSON and startup flags' unless (values.keys & flags.keys).empty? + + values.merge(flags) + rescue JSON::ParserError + raise Inspec::Exceptions::ResourceFailed, 'Invalid daemon.json' + end + + def registry_certificates(directory) + return [] unless inspec.file(directory).exist? + + checked_output("find #{Shellwords.escape(directory)} -type f -print0").split("\0") + end + + def image_history(id) + checked_output("docker history --no-trunc --format '{{json .CreatedBy}}' #{Shellwords.escape(id)}").lines.map { |line| JSON.parse(line) } + end + + def standalone_updates(history) + history.select do |instruction| + next false if instruction.include?('#(nop)') + + command = instruction.sub(/\ARUN(?: \|\d+)?\s+/, '') + command = command.sub(%r{\A(?:\w+=\S+\s+)*/\S+\s+-c\s+}, '') + actions = package_commands(command) + actions.each_index.any? do |index| + manager, action = actions[index] + action == 'update' && !actions.drop(index + 1).include?([manager, 'install']) + end + end + end + + def image_secrets(history) + secret_patterns = [ + /(?:password|passwd|pwd|secret|token|api_key|apikey|access_key)\s*[:=]\s*["']?[^\s"']{8,}/i, + /AKIA[0-9A-Z]{16}/, + /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/, + /\bghp_[0-9a-zA-Z]{36}\b/, + /\bglpat-[0-9a-zA-Z\-_]{20,}\b/, + /\bxox[baprs]-[0-9a-zA-Z]{10,48}\b/, + ] + history.each_index.filter_map do |index| + instruction = history[index].gsub(/\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*/, '') + "Instruction #{index + 1}: possible embedded secret (redacted)" if secret_patterns.any? { |pattern| instruction.match?(pattern) } + end + end + + def insecure_package_flags(history) + flags = %w(--allow-unauthenticated --allow-insecure-repositories --nogpgcheck --no-gpg-checks --allow-untrusted --no-check-certificate --insecure) + history.each_index.filter_map do |index| + instruction = history[index] + next false if instruction.include?('#(nop)') + + commands = shell_commands(instruction.sub(/\ARUN(?: \|\d+)?\s+/, '').sub(%r{\A(?:\w+=\S+\s+)*/\S+\s+-c\s+}, '')) + insecure = commands.any? do |args| + program = File.basename(args.first.to_s) + %w(apt apt-get apk yum dnf zypper curl wget rpm).include?(program) && + (args.any? { |arg| flags.include?(arg) || arg.match?(/\A--(?:nogpgcheck|allow-unauthenticated|allow-untrusted)=true\z/) } || (program == 'curl' && args.include?('-k'))) + end + piped_script = instruction.include?('|') && commands.any? { |args| %w(curl wget).include?(File.basename(args.first.to_s)) } && + commands.any? { |args| %w(sh bash).include?(File.basename(args.first.to_s)) } + "Instruction #{index + 1}: package verification bypass" if insecure || piped_script + end + end + + def all_containers_image_ids + inspec.docker.containers.ids.map do |id| + image = inspec.docker.object(id)['Image'] + raise Inspec::Exceptions::ResourceFailed, "Cannot determine image for container #{id}" if image.to_s.empty? + + image + end.uniq + end + + def container_suid_sgid_binaries(container_id) + pid = inspec.docker.object(container_id).dig('State', 'Pid').to_i + raise Inspec::Exceptions::ResourceFailed, "No running process for container #{container_id}" unless pid.positive? + + root = "/proc/#{pid}/root/" + checked_output("find #{root} -xdev -type f -perm /6000 -print0").split("\0").map { |path| '/' + path.delete_prefix(root) } + end + + def container_packages(container_id) + query = 'if command -v dpkg-query >/dev/null 2>&1; then dpkg-query -W -f=\'${Package} ${db:Status-Status}\\n\'; ' \ + 'elif command -v rpm >/dev/null 2>&1; then rpm -qa --qf \'%{NAME} installed\\n\'; ' \ + 'elif command -v apk >/dev/null 2>&1; then apk info; else exit 3; fi' + result = inspec.command("docker exec #{Shellwords.escape(container_id)} sh -c #{Shellwords.escape(query)}") + return unless result.exit_status == 0 + + result.stdout.lines.filter_map do |line| + name, status = line.split + name if status.nil? || status == 'installed' + end + end - # parse data from docker network ls - params = parse_systemd_values(cmd.stdout.chomp) + def docker_audit_rule?(rules, executable) + rules.lines.any? do |line| + line.match?(/(?:-w\s+|-F\s+path=)#{Regexp.escape(executable)}(?:\s|$)/) && + line.match?(/(?:-p\s+|-F\s+perm=)[rwa]*x[rwa]*(?:\s|$)/) && + line.match?(/(?:-k\s+|-F\s+key=)docker(?:\s|$)/) && + !line.match?(/\bnever\b/) + end + end - # parse data from docker network inspect output - params.each do |k, _v| - params[k] = parse_network_values(inspec.command("docker network inspect #{k}").stdout.delete('\"').delete(',').chomp) + # Search all retained logs. Both exec controls use the same cached command. + # Return event IDs, not command arguments which may contain secrets. + def docker_exec_audit(option) + result = (@docker_exec_audit_result ||= inspec.command('env LC_ALL=C ausearch --input-logs -k docker -m EXECVE --raw')) + no_matches = result.exit_status == 1 && result.stdout.strip.empty? && ['', ''].include?(result.stderr.strip) + return { 'error' => nil, 'events' => [] } if no_matches + unless result.exit_status == 0 && result.stderr.strip.empty? && !result.stdout.strip.empty? + return { 'error' => "ausearch failed (exit #{result.exit_status}); verify audit logs and read permissions", 'events' => [] } end - # return the value - params + events = result.stdout.lines.select { |line| line.match?(/\btype=EXECVE\b/) }.group_by do |line| + line[/\bmsg=audit\(([^)]+)\)/, 1] + end + return { 'error' => nil, 'events' => [] } if events.empty? + + matches = events.filter_map do |event, lines| + raise ArgumentError, 'Missing audit event ID' unless event + + record = lines.join(' ') + argc = record[/\bargc=(\d+)/, 1] + raise ArgumentError, "Missing argument count in audit event #{event}" unless argc && argc.to_i.positive? + + fields = record.scan(/\ba(\d+)(?:\[(\d+)\])?=("[^"]*"|[0-9A-Fa-f]+)(?=\s|$)/) + args = Array.new(argc.to_i) do |index| + parts = fields.select { |number, _, _| number.to_i == index }.sort_by { |_, part, _| part.to_i } + raise ArgumentError, "Incomplete arguments in audit event #{event}" if parts.empty? + + parts.map do |_, _, value| + if value.start_with?('"') + value[1...-1] + else + raise ArgumentError, "Invalid argument encoding in audit event #{event}" if value.length.odd? + + [value].pack('H*') + end + end.join + end + next unless File.basename(args.first.to_s) == 'docker' + + options = docker_exec_options(args.drop(1)) + next if options.nil? + + event if option == 'privileged' ? options['privileged'] == true : options['user'].to_s.split(':').first.to_s.match?(/\A(?:root|0+)\z/) + end + { 'error' => nil, 'events' => matches } + rescue ArgumentError => e + { 'error' => e.message, 'events' => [] } + end + + def checked_output(command) + result = inspec.command(command) + raise Inspec::Exceptions::ResourceFailed, "Evidence command failed: #{command}" unless result.exit_status == 0 + + result.stdout end private - # returns parsed params - def parse_systemd_values(stdout) - SimpleConfig.new( - stdout, - assignment_regex: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/, - multiple_values: false - ).params - end - - # returns parsed params - def parse_network_values(stdout) - SimpleConfig.new( - stdout, - assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/, - multiple_values: false - ).params + def package_commands(command) + shell_commands(command).filter_map do |args| + args.shift while args.first.to_s.match?(/\A\w+=/) + args.shift if args.first == 'sudo' + manager = File.basename(args.shift.to_s) + next unless %w(apt apt-get apk yum dnf zypper).include?(manager) + + while args.first.to_s.start_with?('-') + flag = args.shift + args.shift if %w(-o --option -c --config-file -t --target-release).include?(flag) + end + action = args.first + # yum/dnf update upgrades installed packages; makecache refreshes metadata. + refresh = { 'apt' => %w(update), 'apt-get' => %w(update), 'apk' => %w(update), + 'yum' => %w(makecache check-update), 'dnf' => %w(makecache check-update), 'zypper' => %w(refresh ref) } + install = { 'apk' => %w(add), 'zypper' => %w(install in) }.fetch(manager, %w(install)) + action = 'update' if refresh.fetch(manager).include?(action) + action = 'upgrade' if %w(yum dnf).include?(manager) && args.first == 'update' + action = 'install' if install.include?(args.first) + [manager, action] + end + end + + def shell_commands(command) + commands = [[]] + # Keep quoted text as one word, so echo/LABEL text is not treated as a command. + command.scan(/(?:[^\s\\'";&|()]+|\\.|"(?:\\.|[^"\\])*"|'[^']*')+|&&|\|\||[;&|()\n]/).each do |token| + if %w(&& || ; & | ( )).include?(token) || token == "\n" + commands << [] + else + commands.last << Shellwords.shellsplit(token).join + end + end + commands + end + + def docker_exec_options(args) + global_values = %w(--config --context -c --host -H --log-level -l --tlscacert --tlscert --tlskey) + while args.first.to_s.start_with?('-') + flag = args.shift + args.shift if global_values.include?(flag) + end + args.shift if args.first == 'container' + return unless args.shift == 'exec' + + options = {} + while args.first.to_s.start_with?('-') + flag = args.shift + break if flag == '--' + + case flag + when '--privileged', /\A--privileged=(?:1|t|T|true|TRUE|True)\z/ + options['privileged'] = true + when /\A--privileged=(?:0|f|F|false|FALSE|False)\z/ + options['privileged'] = false + when '--user', /\A-[dit]*u\z/ + options['user'] = args.shift + when /\A--user=(.*)/, /\A-[dit]*u=?(.+)/ + options['user'] = Regexp.last_match(1) + when '--env', '-e', '--env-file', '--workdir', '-w', '--detach-keys' + args.shift + end + end + options + end + + def parse_daemon_flags(command) + tokens = Shellwords.shellsplit(command).drop(1) + flags = {} + booleans = %w(debug experimental icc iptables tls tlsverify live-restore userland-proxy no-new-privileges) + arrays = { 'host' => 'hosts', 'insecure-registry' => 'insecure-registries', + 'authorization-plugin' => 'authorization-plugins', 'storage-opt' => 'storage-opts' } + values = %w(config-file log-level tlscacert tlscert tlskey cgroup-parent log-driver seccomp-profile) + until tokens.empty? + token = tokens.shift + token = token.sub(/\A-H=?/, '--host=') if token.start_with?('-H') && token != '-H' + token = token.sub(/\A-l=?/, '--log-level=') if token.start_with?('-l') && token != '-l' + token = '--host' if token == '-H' + token = token.sub(/\A-D/, '--debug') if token.start_with?('-D') + token = '--log-level' if token == '-l' + next unless token.start_with?('--') + + key, value = token.delete_prefix('--').split('=', 2) + next unless booleans.include?(key) || arrays.key?(key) || values.include?(key) || %w(log-opt default-ulimit).include?(key) + + value = booleans.include?(key) ? 'true' : tokens.shift if value.nil? + raise Inspec::Exceptions::ResourceFailed, "Missing daemon option value: #{key}" if value.nil? || value.start_with?('--') + + if arrays.key?(key) + (flags[arrays[key]] ||= []) << value + elsif key == 'log-opt' + name, setting = value.split('=', 2) + (flags['log-opts'] ||= {})[name] = setting + elsif key == 'default-ulimit' + name, limits = value.split('=', 2) + soft, hard = limits.to_s.split(':', 2).map { |limit| Integer(limit, 10) } + raise Inspec::Exceptions::ResourceFailed, 'Invalid default-ulimit value' unless soft + + (flags['default-ulimits'] ||= {})[name] = { 'Name' => name, 'Soft' => soft, 'Hard' => hard || soft } + else + flags[key] = booleans.include?(key) ? %w(true 1 t TRUE T True).include?(value) : value + end + end + flags end end diff --git a/sample_attributes.yml b/sample_attributes.yml index 2d42092..877b518 100644 --- a/sample_attributes.yml +++ b/sample_attributes.yml @@ -1,13 +1,54 @@ -trusted_user: vagrant +trusted_users: + - vagrant +container_capadd: NET_ADMIN,SYS_ADMIN +seccomp_default_profile: default +default_ulimits: + nproc: + Soft: 1024 + Hard: 2408 + nofile: + Soft: 100 + Hard: 200 +docker_cgroup_parent: "" +docker_min_version: "28.0.0" +docker_cli_path: /usr/bin/docker +swarm_node_cert_expiry_days: 90 +swarm_ca_rotation_days: 90 +approved_container_ports: [] +prohibited_packages: + - gcc + - g++ + - gdb + - tcpdump + - wireshark + - telnet + - netcat + - nc +whitelisted_suid_binaries: + - /usr/bin/passwd + - /usr/bin/su + - /usr/bin/sudo + - /usr/bin/newgrp + - /usr/bin/chfn + - /usr/bin/chsh +image_max_age_days: 90 +allowed_unused_images_count: 5 +allowed_unused_images: [] +allowed_latest_tag_images: [] +signed_artifacts: [] +# - path: /srv/build/application.tar.gz +# signature: /srv/build/application.tar.gz.sig +# public_key: /etc/docker/trusted-publisher.pem +dockerfile_paths: [] managable_container_number: 25 registry_cert_path: /etc/docker/certs.d -registry_name: /etc/docker/certs.d/registry_hostname:port -registry_ca_file: /etc/docker/certs.d/registry_hostname:port/ca.crt -container_user: vagrant -container_capadd: 'NET_ADMIN' +docker_daemon_config: /etc/docker/daemon.json +docker_daemon_path: /usr/bin/dockerd +docker_socket: /var/run/docker.sock +containerd_socket: /run/containerd/containerd.sock authorization_plugin: authz-broker log_driver: syslog -log_opts: /syslog-address/ -app_armor_profile: docker-default -selinux_profile: /label\:level\:s0-s0\:c1023/ -benchmark_version: 1.12.0 +log_opts: syslog-address +swarm_mode: inactive +swarm_max_manager_nodes: 3 +swarm_port: 2377