脆弱性スキャナのTrivyが設定ミスも検出できるようになったらしいので、少し触ってみました。
Dockerfileのスキャンツール
hadolint
github.com
hadolintはDockerfileのスキャナで、ポリシーはDL(hadolint由来)とSC(ShellCheck由来)で構成されています。
前者は、Dockerfile best practicesをベースにしていてDockerfile固有の記述をスキャンしてくれます。後者は、脆弱なシェルコマンドを検出してくれます。
ポリシーの例
Rule | Default Severity | Description |
---|---|---|
DL3000 | Error | Use absolute WORKDIR. |
DL3001 | Info | For some bash commands it makes no sense running them in a Docker container like ssh, vim, shutdown, service, ps, free, top, kill, mount, ifconfig. |
DL3002 | Warning | Last user should not be root. |
DL3003 | Warning | Use WORKDIR to switch to a directory. |
DL3004 | Error | Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce root. |
DL3005 | Error | Do not use apt-get dist-upgrade. |
DL3006 | Warning | Always tag the version of an image explicitly. |
DL3007 | Warning | Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag. |
DL3008 | Warning | Pin versions in apt-get install. |
DL3009 | Info | Delete the apt-get lists after installing something. |
DL3010 | Info | Use ADD for extracting archives into an image. |
DL3011 | Error | Valid UNIX ports range from 0 to 65535. |
DL3012 | Error | Multiple HEALTHCHECK instructions. |
DL3013 | Warning | Pin versions in pip. |
DL3014 | Warning | Use the -y switch. |
SC1000 | $ is not used specially and should therefore be escaped. | |
SC1001 | This \c will be a regular 'c' in this context. | |
SC1007 | Remove space after = if trying to assign a value (or for empty string, use var='' ...). | |
SC1010 | Use semicolon or linefeed before done (or quote to make it literal). | |
SC1018 | This is a unicode non-breaking space. Delete it and retype as space. | |
SC1035 | You need a space here | |
SC1045 | It's not foo &; bar, just foo & bar. | |
SC1065 | Trying to declare parameters? Don't. Use () and refer to params as $1, $2 etc. | |
SC1066 | Don't use $ on the left side of assignments. | |
SC1068 | Don't put spaces around the = in assignments. | |
SC1077 | For command expansion, the tick should slant left (` vs ´). |
Trivy
aquasecurity.github.io
TrivyはVulnerability ScanningとMisconfigurationの二つの機能があります。
Vulnerability Scanning機能は、コンテナイメージのみならず、ホストOSのファイルシステム、Gitリポジトリも対象としています。
仕組みとしては、apt/yum/rpm/dpkg等のOSパッケージマネージャやgem/pip等の言語パッケージマネージャから依存関係を抽出し、脆弱性データベースと照合しているようです。
Misconfiguration機能は、最近追加された機能でDockerfile、Kubernetes、TerraformといったIaC(Infrastructure as Code)のスキャン機能です。
DockerfileとKubernetesのポリシーはAppshieldリポジトリで管理されていて、hadolint、kubesec、tfsecといったスキャンツールのノウハウが統合されているようです。今回は、新機能のMisconfiguration機能に焦点を当てます。
https://github.com/aquasecurity/appshield
・https://github.com/hadolint/hadolint
・https://github.com/Checkmarx/kics
・https://github.com/controlplaneio/kubesec
・https://github.com/aquasecurity/tfsec
・https://kubernetes.io/docs/concepts/security/pod-security-standards/
・https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
インストール
OS:Ubuntu 21.04
Trivy
wget https://github.com/aquasecurity/trivy/releases/download/v0.19.2/trivy_0.19.2_Linux-64bit.deb sudo dpkg -i trivy_0.19.2_Linux-64bit.deb
hadolint
git clone https://github.com/hadolint/hadolint cd hadolint sudo apt install haskell-stack stack install nano ~/.profile PATH="$PATH:/home/user/.local/bin" source ~/.profile
使い方
#Trivy:IaCファイルのディレクトリを指定 trivy config [YOUR_IaC_DIRECTORY] #hadolint:Dockerfileを指定 hadolint Dockerfile
比較
検証1
#スキャン対象のDockerfile $ cat Dockerfile FROM busybox WORKDIR usr/src/app
$ trivy conf -f json $(pwd) 2021-08-05T16:33:46.620+0900 INFO Detected config files: 1 2021-08-05T16:33:46.620+0900 WARN DEPRECATED: the current JSON schema is deprecated, check https://github.com/aquasecurity/trivy/discussions/1050 for more information. [ { "Target": "Dockerfile", "Class": "config", "Type": "dockerfile", "MisconfSummary": { "Successes": 20, "Failures": 3, "Exceptions": 0 }, "Misconfigurations": [ { "Type": "Dockerfile Security Check", "ID": "DS001", "Title": "':latest' tag used", "Description": "When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.", "Message": "Specify a tag in the 'FROM' statement for image 'busybox'", "Namespace": "appshield.dockerfile.DS001", "Query": "data.appshield.dockerfile.DS001.deny", "Resolution": "Add a tag to the image in the 'FROM' statement", "Severity": "MEDIUM", "PrimaryURL": "https://avd.aquasec.com/appshield/ds001", "References": [ "https://avd.aquasec.com/appshield/ds001" ], "Status": "FAIL", "Layer": { "DiffID": "sha256:358203d0551db478185a6443612240b7fc2a6e0cf692bdcaf576ebca134958e1" } }, { "Type": "Dockerfile Security Check", "ID": "DS002", "Title": "root user", "Description": "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", "Message": "Specify at least 1 USER command in Dockerfile with non-root user as argument", "Namespace": "appshield.dockerfile.DS002", "Query": "data.appshield.dockerfile.DS002.deny", "Resolution": "Add 'USER \u003cnon root user name\u003e' line to the Dockerfile", "Severity": "HIGH", "PrimaryURL": "https://avd.aquasec.com/appshield/ds002", "References": [ "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", "https://avd.aquasec.com/appshield/ds002" ], "Status": "FAIL", "Layer": { "DiffID": "sha256:358203d0551db478185a6443612240b7fc2a6e0cf692bdcaf576ebca134958e1" } }, { "Type": "Dockerfile Security Check", "ID": "DS009", "Title": "WORKDIR path not absolute", "Description": "For clarity and reliability, you should always use absolute paths for your WORKDIR.", "Message": "WORKDIR path 'usr/src/app' should be absolute", "Namespace": "appshield.dockerfile.DS009", "Query": "data.appshield.dockerfile.DS009.deny", "Resolution": "Use absolute paths for your WORKDIR", "Severity": "HIGH", "PrimaryURL": "https://avd.aquasec.com/appshield/ds009", "References": [ "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#workdir", "https://avd.aquasec.com/appshield/ds009" ], "Status": "FAIL", "Layer": { "DiffID": "sha256:358203d0551db478185a6443612240b7fc2a6e0cf692bdcaf576ebca134958e1" } } ] } ]
$ hadolint Dockerfile Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly Dockerfile:2 DL3000 error: Use absolute WORKDIR
Trivyでは下記すべてが検出されましたがhadolintは2が漏れています。
1.latestタグ(バージャン管理が困難)
2.コンテナ内のrootユーザ(コンテナ内の過剰な権限)
3.WORKDIRの絶対パス(意図しないパスの参照)
検証2
$ cat Dockerfile FROM python:3.4 ADD requirements.txt /usr/src/app/
$ trivy conf -f json $(pwd) 2021-08-05T16:50:05.497+0900 INFO Detected config files: 1 2021-08-05T16:50:05.497+0900 WARN DEPRECATED: the current JSON schema is deprecated, check https://github.com/aquasecurity/trivy/discussions/1050 for more information. [ { "Target": "Dockerfile", "Class": "config", "Type": "dockerfile", "MisconfSummary": { "Successes": 21, "Failures": 2, "Exceptions": 0 }, "Misconfigurations": [ { "Type": "Dockerfile Security Check", "ID": "DS002", "Title": "root user", "Description": "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", "Message": "Specify at least 1 USER command in Dockerfile with non-root user as argument", "Namespace": "appshield.dockerfile.DS002", "Query": "data.appshield.dockerfile.DS002.deny", "Resolution": "Add 'USER \u003cnon root user name\u003e' line to the Dockerfile", "Severity": "HIGH", "PrimaryURL": "https://avd.aquasec.com/appshield/ds002", "References": [ "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", "https://avd.aquasec.com/appshield/ds002" ], "Status": "FAIL", "Layer": { "DiffID": "sha256:16165dee25682bccd2a150512b6971582a57652bfd051590a7eac075e4da8d84" } }, { "Type": "Dockerfile Security Check", "ID": "DS005", "Title": "ADD instead of COPY", "Description": "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", "Message": "Consider using 'COPY requirements.txt /usr/src/app/' command instead of 'ADD requirements.txt /usr/src/app/'", "Namespace": "appshield.dockerfile.DS005", "Query": "data.appshield.dockerfile.DS005.deny", "Resolution": "Use COPY instead of ADD", "Severity": "LOW", "PrimaryURL": "https://avd.aquasec.com/appshield/ds005", "References": [ "https://docs.docker.com/engine/reference/builder/#add", "https://avd.aquasec.com/appshield/ds005" ], "Status": "FAIL", "Layer": { "DiffID": "sha256:16165dee25682bccd2a150512b6971582a57652bfd051590a7eac075e4da8d84" } } ] } ]
$ hadolint Dockerfile Dockerfile:2 DL3020 error: Use COPY instead of ADD for files and folders
ADDの使用については両方のツールで拾えています。
検証3
$ cat Dockerfile FROM debian RUN wget http://google.com RUN curl http://bing.com
$ trivy conf -f json $(pwd) ... { "Type": "Dockerfile Security Check", "ID": "DS014", "Title": "RUN using 'wget' and 'curl'", "Description": "Avoid using both 'wget' and 'curl' since these tools have the same effect.", "Message": "Shouldn't use both curl and wget", "Namespace": "appshield.dockerfile.DS014", "Query": "data.appshield.dockerfile.DS014.deny", "Resolution": "Pick one util, either 'wget' or 'curl'", "Severity": "LOW", "PrimaryURL": "https://avd.aquasec.com/appshield/ds014", "References": [ "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run", "https://avd.aquasec.com/appshield/ds014" ], "Status": "FAIL", "Layer": { "DiffID": "sha256:b33501c59c91430addd5b3e3ecb960911c78c4cc04662b926b5303dc30440262" } } ] } ]
$ hadolint Dockerfile Dockerfile:2 DL3047 info: Avoid use of wget without progress bar. Use `wget --progress=dot:giga`.Or consider using `-q` or `-nv` (shorthands for `--quiet` or `--no-verbose`). Dockerfile:3 DL4001 warning: Either use Wget or Curl but not both Dockerfile:3 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
検証4
$ cat Dockerfile FROM debian:10 RUN $greeting="Hello World" RUN ls -l $(getfilename)
$ trivy conf -f json $(pwd) "Misconfigurations": [ { "Type": "Dockerfile Security Check", "ID": "DS002", "Title": "root user", "Description": "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", "Message": "Specify at least 1 USER command in Dockerfile with non-root user as argument", "Namespace": "appshield.dockerfile.DS002", "Query": "data.appshield.dockerfile.DS002.deny", "Resolution": "Add 'USER \u003cnon root user name\u003e' line to the Dockerfile", "Severity": "HIGH", "PrimaryURL": "https://avd.aquasec.com/appshield/ds002", "References": [ "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", "https://avd.aquasec.com/appshield/ds002" ], "Status": "FAIL", "Layer": { "DiffID": "sha256:5776f76fe0c52302603355ee575cd5be70d7b0891ab85903c3de9516f49ff027" } } ] } ]
$ hadolint Dockerfile Dockerfile:2 SC1066 error: Don't use $ on the left side of assignments. Dockerfile:3 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation. Dockerfile:3 SC2046 warning: Quote this to prevent word splitting.
ShellCheck関連はhadolintだけ検出しています。
まとめ
Dockerfile固有の設定はTrivyでもhadolint並みに拾えています。ShellCheckの機能は未実装なので、hadolintの上位互換ではなさそうです。
ポリシーが多少異なっているので、両方使う選択肢もありな気がします。
ただ、カバー範囲や将来性ではTrivyに期待です。
TrivyのMisconfigurationポリシはポリシ検査(Policy as Code)に特化したRego言語をサポートしているため、KubernetesやCICDと親和性が高そうです。
Rego言語は多少癖がありますが、IaC(Infrastructure as Code)セキュリティの感心が高まれば存在感が増してくるのではないかと思います。
Open Policy Agent | Policy Language
DS002 | Vulnerability Database | Aqua Security
package appshield.dockerfile.DS002 import data.lib.docker __rego_metadata__ := { "id": "DS002", "title": "root user", "version": "v1.0.0", "severity": "HIGH", "type": "Dockerfile Security Check", "description": "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", "recommended_actions": "Add 'USER' line to the Dockerfile", "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", } __rego_input__ := { "combine": false, "selector": [{"type": "dockerfile"}], } # get_user returns all the usernames from # the USER command. get_user[username] { user := docker.user[_] username := user.Value[_] } # fail_user_count is true if there is no USER command. fail_user_count { count(get_user) < 1 } # fail_last_user_root is true if the last USER command # value is "root" fail_last_user_root { user := cast_array(get_user) len := count(get_user) user[minus(len, 1)] == "root" } deny[msg] { fail_user_count msg = "Specify at least 1 USER command in Dockerfile with non-root user as argument" } deny[res] { fail_last_user_root res := "Last USER command in Dockerfile should not be 'root'" }