diff --git a/.goreleaser.yml b/.goreleaser.yml index 7cb5822..2ec2684 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -9,10 +9,11 @@ builds: # Archive customization archive: format: tar.gz -fpm: +nfpm: vendor: Aqua Security description: "The Kubernetes Bench for Security is a Go application that checks whether Kubernetes is deployed according to security best practices" license: Apache-2.0 + homepage: https://github.com/aquasecurity/kube-bench formats: - deb - rpm diff --git a/.travis.yml b/.travis.yml index 9528ceb..16d33a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,11 @@ --- language: go +sudo: required + +services: + - docker + notifications: email: false @@ -16,6 +21,10 @@ install: script: - go test ./... + - docker build --tag kube-bench . + - docker run -v `pwd`:/host kube-bench install + - test -d cfg + - test -f kube-bench after_success: - test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash diff --git a/Dockerfile b/Dockerfile index 10f1676..0a0fbad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,22 @@ -FROM golang:1.9 -WORKDIR /kube-bench -RUN go get github.com/aquasecurity/kube-bench +FROM golang:1.9 AS build +WORKDIR /go/src/github.com/aquasecurity/kube-bench/ +ADD glide.lock glide.yaml ./ +RUN go get github.com/Masterminds/glide && glide install +ADD main.go . +ADD check/ check/ +ADD cmd/ cmd/ +RUN CGO_ENABLED=0 go install -a -ldflags '-w' -FROM alpine:latest -WORKDIR / -COPY --from=0 /go/bin/kube-bench /kube-bench -COPY --from=0 /go/src/github.com/aquasecurity/kube-bench/cfg /cfg -COPY --from=0 /go/src/github.com/aquasecurity/kube-bench/entrypoint.sh /entrypoint.sh -ENTRYPOINT /entrypoint.sh +FROM alpine:3.7 AS run +WORKDIR /opt/kube-bench/ +# add GNU ps for -C, -o cmd, and --no-headers support +# https://github.com/aquasecurity/kube-bench/issues/109 +RUN apk --no-cache add procps +COPY --from=build /go/bin/kube-bench /usr/local/bin/kube-bench +ADD entrypoint.sh . +ADD cfg/ cfg/ +ENTRYPOINT ["./entrypoint.sh"] +CMD ["install"] # Build-time metadata as defined at http://label-schema.org ARG BUILD_DATE diff --git a/README.md b/README.md index 1c1369d..43e0e02 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![Docker image](https://images.microbadger.com/badges/image/aquasec/kube-bench.svg)](https://microbadger.com/images/aquasec/kube-bench "Get your own image badge on microbadger.com") [![Source commit](https://images.microbadger.com/badges/commit/aquasec/kube-bench.svg)](https://microbadger.com/images/aquasec/kube-bench) -# kube-bench +kube-bench logo -The Kubernetes Bench for Security is a Go application that checks whether Kubernetes is deployed securely by running the checks documented in the CIS Kubernetes Benchmark. +kube-bench is a Go application that checks whether Kubernetes is deployed securely by running the checks documented in the CIS Kubernetes Benchmark. Tests are configured with YAML files, making this tool easy to update as test specifications evolve. @@ -17,12 +17,50 @@ kube-bench supports the tests for multiple versions of Kubernetes (1.6, 1.7 and ## Installation -You can either install kube-bench through a dedicated container, or compile it from source: +You can choose to +* run kube-bench from inside a container (sharing PID namespace with the host) +* run a container that installs kube-bench on the host, and then run kube-bench directly on the host +* install the latest binaries from the [Releases page](https://github.com/aquasecurity/kube-bench/releases), +* compile it from source. -1. Container installation: -Run ```docker run --rm -v `pwd`:/host aquasec/kube-bench:latest```. This will copy the kube-bench binary and configuration to you host. You can then run ```./kube-bench ```. +### Running inside a container + +You can avoid installing kube-bench on the host by running it inside a container using the host PID namespace. + +``` +docker run --pid=host aquasec/kube-bench:latest +``` + +You can even use your own configs by mounting them over the default ones in `/opt/kube-bench/cfg/` + +``` +docker run --pid=host -v path/to/my-config.yaml:/opt/kube-bench/cfg/config.yaml aquasec/kube-bench:latest +``` + +### Running in a kubernetes cluster +Run the master check + +``` +kubectl run --rm -i -t kube-bench-master --image=aquasec/kube-bench:latest --restart=Never --overrides="{ \"apiVersion\": \"v1\", \"spec\": { \"hostPID\": true, \"nodeSelector\": { \"kubernetes.io/role\": \"master\" }, \"tolerations\": [ { \"key\": \"node-role.kubernetes.io/master\", \"operator\": \"Exists\", \"effect\": \"NoSchedule\" } ] } }" -- master --version 1.8 +``` + +Run the node check + +``` +kubectl run --rm -i -t kube-bench-node --image=aquasec/kube-bench:latest --restart=Never --overrides="{ \"apiVersion\": \"v1\", \"spec\": { \"hostPID\": true } }" -- node --version 1.8 +``` + +### Installing from a container + +This command copies the kube-bench binary and configuration files to your host from the Docker container: +``` +docker run --rm -v `pwd`:/host aquasec/kube-bench:latest install +``` + +You can then run `./kube-bench `. + +### Installing from sources -2. Install from sources: If Go is installed on the target machines, you can simply clone this repository and run as follows (assuming your [$GOPATH is set](https://github.com/golang/go/wiki/GOPATH)): ```go get github.com/aquasecurity/kube-bench @@ -30,25 +68,13 @@ go get github.com/Masterminds/glide cd $GOPATH/src/github.com/aquasecurity/kube-bench $GOPATH/bin/glide install go build -o kube-bench . -./kube-bench -``` -## Usage -```./kube-bench [command]``` +# See all supported options +./kube-bench --help + +# Run the all checks on a master node +./kube-bench master -``` -Available Commands: - federated Run benchmark checks for a Kubernetes federated deployment. - help Help about any command - master Run benchmark checks for a Kubernetes master node. - node Run benchmark checks for a Kubernetes node. - -Flags: - -c, --check string A comma-delimited list of checks to run as specified in CIS document. Example --check="1.1.1,1.1.2" - --config string config file (default is ./cfg/config.yaml) - -g, --group string Run all the checks under this comma-delimited list of groups. Example --group="1.1" - --json Prints the results as JSON - -v, --verbose verbose output (default false) ``` ## Configuration diff --git a/cfg/1.8/master.yaml b/cfg/1.8/master.yaml index 170c89a..7fb9dfa 100644 --- a/cfg/1.8/master.yaml +++ b/cfg/1.8/master.yaml @@ -418,7 +418,7 @@ groups: - id: 1.1.26 text: "Ensure that the --etcd-certfile and --etcd-keyfile arguments are set as - appropriate (Scored" + appropriate (Scored)" audit: "ps -ef | grep $apiserverbin | grep -v grep" tests: bin_op: and @@ -610,7 +610,7 @@ groups: remediation: | Edit the API server pod specification file $apiserverconf and set the below parameter as appropriate and if needed. For example, - --request-timeout=300 + --request-timeout=300s scored: true - id: 1.2 @@ -666,7 +666,7 @@ groups: scored: true - id: 1.3.3 - text: "Ensure that the --use-service-account-credentials argument is set" + text: "Ensure that the --use-service-account-credentials argument is set (Scored)" audit: "ps -ef | grep $controllermanagerbin | grep -v grep" tests: test_items: diff --git a/cmd/common.go b/cmd/common.go index 15cb237..9e4cfef 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -17,6 +17,7 @@ package cmd import ( "fmt" "io/ioutil" + "path/filepath" "github.com/aquasecurity/kube-bench/check" "github.com/golang/glog" @@ -46,10 +47,22 @@ func runChecks(t check.NodeType) { nodetype = "federated" } - ver := getKubeVersion() - path := fmt.Sprintf("%s/%s", cfgDir, ver) + var ver string + if kubeVersion != "" { + ver = kubeVersion + } else { + ver = getKubeVersion() + } + + switch ver { + case "1.9", "1.10": + continueWithError(nil, fmt.Sprintf("No CIS spec for %s - using tests from CIS 1.2.0 spec for Kubernetes 1.8\n", ver)) + ver = "1.8" + } + + path := filepath.Join(cfgDir, ver) + def := filepath.Join(path, file) - def := fmt.Sprintf("%s/%s", path, file) in, err := ioutil.ReadFile(def) if err != nil { exitWithError(fmt.Errorf("error opening %s controls file: %v", t, err)) @@ -124,41 +137,48 @@ func colorPrint(state check.State, s string) { // prettyPrint outputs the results to stdout in human-readable format func prettyPrint(r *check.Controls, summary check.Summary) { - colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text)) - for _, g := range r.Groups { - colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text)) - for _, c := range g.Checks { - colorPrint(c.State, fmt.Sprintf("%s %s\n", c.ID, c.Text)) + // Print check results. + if !noResults { + colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text)) + for _, g := range r.Groups { + colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text)) + for _, c := range g.Checks { + colorPrint(c.State, fmt.Sprintf("%s %s\n", c.ID, c.Text)) + } } - } - fmt.Println() + fmt.Println() + } // Print remediations. - if summary.Fail > 0 || summary.Warn > 0 { - colors[check.WARN].Printf("== Remediations ==\n") - for _, g := range r.Groups { - for _, c := range g.Checks { - if c.State != check.PASS { - fmt.Printf("%s %s\n", c.ID, c.Remediation) + if !noRemediations { + if summary.Fail > 0 || summary.Warn > 0 { + colors[check.WARN].Printf("== Remediations ==\n") + for _, g := range r.Groups { + for _, c := range g.Checks { + if c.State != check.PASS { + fmt.Printf("%s %s\n", c.ID, c.Remediation) + } } } + fmt.Println() } - fmt.Println() } // Print summary setting output color to highest severity. - var res check.State - if summary.Fail > 0 { - res = check.FAIL - } else if summary.Warn > 0 { - res = check.WARN - } else { - res = check.PASS - } + if !noSummary { + var res check.State + if summary.Fail > 0 { + res = check.FAIL + } else if summary.Warn > 0 { + res = check.WARN + } else { + res = check.PASS + } - colors[res].Printf("== Summary ==\n") - fmt.Printf("%d checks PASS\n%d checks FAIL\n%d checks WARN\n", - summary.Pass, summary.Fail, summary.Warn, - ) + colors[res].Printf("== Summary ==\n") + fmt.Printf("%d checks PASS\n%d checks FAIL\n%d checks WARN\n", + summary.Pass, summary.Fail, summary.Warn, + ) + } } diff --git a/cmd/root.go b/cmd/root.go index 76d871a..a41ea61 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,9 +26,10 @@ import ( var ( envVarsPrefix = "KUBE_BENCH" - cfgDir = "./cfg" defaultKubeVersion = "1.6" + kubeVersion string cfgFile string + cfgDir string jsonFmt bool pgSQL bool checkList string @@ -36,13 +37,16 @@ var ( masterFile string nodeFile string federatedFile string + noResults bool + noSummary bool + noRemediations bool ) // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ Use: os.Args[0], Short: "Run CIS Benchmarks checks against a Kubernetes deployment", - Long: `This tool runs the CIS Kubernetes 1.6 Benchmark v1.0.0 checks.`, + Long: `This tool runs the CIS Kubernetes Benchmark (http://www.cisecurity.org/benchmark/kubernetes/)`, } // Execute adds all child commands to the root command sets flags appropriately. @@ -60,8 +64,13 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) + // Output control + RootCmd.PersistentFlags().BoolVar(&noResults, "noresults", false, "Disable printing of results section") + RootCmd.PersistentFlags().BoolVar(&noSummary, "nosummary", false, "Disable printing of summary section") + RootCmd.PersistentFlags().BoolVar(&noRemediations, "noremediations", false, "Disable printing of remediations section") RootCmd.PersistentFlags().BoolVar(&jsonFmt, "json", false, "Prints the results as JSON") RootCmd.PersistentFlags().BoolVar(&pgSQL, "pgsql", false, "Save the results to PostgreSQL") + RootCmd.PersistentFlags().StringVarP( &checkList, "check", @@ -77,6 +86,8 @@ func init() { `Run all the checks under this comma-delimited list of groups. Example --group="1.1"`, ) RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./cfg/config.yaml)") + RootCmd.PersistentFlags().StringVarP(&cfgDir, "config-dir", "D", "./cfg/", "config directory") + RootCmd.PersistentFlags().StringVar(&kubeVersion, "version", "", "Manually specify Kubernetes version, automatically detected if unset") goflag.CommandLine.VisitAll(func(goflag *goflag.Flag) { RootCmd.PersistentFlags().AddGoFlag(goflag) diff --git a/cmd/util.go b/cmd/util.go index 4f0c658..ab78945 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -215,10 +215,19 @@ func multiWordReplace(s string, subname string, sub string) string { func getKubeVersion() string { // These executables might not be on the user's path. _, err := exec.LookPath("kubectl") + if err != nil { - exitWithError(fmt.Errorf("kubernetes version check failed: %v", err)) + _, err = exec.LookPath("kubelet") + if err != nil { + exitWithError(fmt.Errorf("Version check failed: need kubectl or kubelet binaries to get kubernetes version.\nAlternately, you can specify the version with --version")) + } + return getKubeVersionFromKubelet() } + return getKubeVersionFromKubectl() +} + +func getKubeVersionFromKubectl() string { cmd := exec.Command("kubectl", "version", "--short") out, err := cmd.CombinedOutput() if err != nil { @@ -228,6 +237,17 @@ func getKubeVersion() string { return getVersionFromKubectlOutput(string(out)) } +func getKubeVersionFromKubelet() string { + cmd := exec.Command("kubelet", "--version") + out, err := cmd.CombinedOutput() + + if err != nil { + continueWithError(fmt.Errorf("%s", out), "") + } + + return getVersionFromKubeletOutput(string(out)) +} + func getVersionFromKubectlOutput(s string) string { serverVersionRe := regexp.MustCompile(`Server Version: v(\d+.\d+)`) subs := serverVersionRe.FindStringSubmatch(s) @@ -238,6 +258,16 @@ func getVersionFromKubectlOutput(s string) string { return subs[1] } +func getVersionFromKubeletOutput(s string) string { + serverVersionRe := regexp.MustCompile(`Kubernetes v(\d+.\d+)`) + subs := serverVersionRe.FindStringSubmatch(s) + if len(subs) < 2 { + printlnWarn(fmt.Sprintf("Unable to get kubelet version, using default version: %s", defaultKubeVersion)) + return defaultKubeVersion + } + return subs[1] +} + func makeSubstitutions(s string, ext string, m map[string]string) string { for k, v := range m { subst := "$" + k + ext diff --git a/entrypoint.sh b/entrypoint.sh index ad28fbf..b06f083 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,14 +1,19 @@ -#!/bin/sh -if [ -d /host ]; then - mkdir -p /host/cfg/ - yes | cp -rf /cfg/* /host/cfg/ - yes | cp -rf /kube-bench /host/ - echo "===============================================" - echo "kube-bench is now installed on your host " - echo "Run ./kube-bench to perform a security check " - echo "===============================================" +#!/bin/sh -e +if [ "$1" == "install" ]; then + if [ -d /host ]; then + mkdir -p /host/cfg/ + yes | cp -rf cfg/* /host/cfg/ + yes | cp -rf /usr/local/bin/kube-bench /host/ + echo "===============================================" + echo "kube-bench is now installed on your host " + echo "Run ./kube-bench to perform a security check " + echo "===============================================" + else + echo "Usage:" + echo " install: docker run --rm -v \`pwd\`:/host aquasec/kube-bench install" + echo " run: docker run --rm --pid=host aquasec/kube-bench [command]" + exit + fi else - echo "Usage:" - echo " docker run --rm -v \`pwd\`:/host aquasec/kube-bench" - exit + exec kube-bench "$@" fi diff --git a/hooks/build b/hooks/build old mode 100644 new mode 100755 diff --git a/images/kube-bench.png b/images/kube-bench.png new file mode 100644 index 0000000..c135396 Binary files /dev/null and b/images/kube-bench.png differ diff --git a/images/kube-bench.svg b/images/kube-bench.svg new file mode 100644 index 0000000..ba64a9e --- /dev/null +++ b/images/kube-bench.svg @@ -0,0 +1,121 @@ + +image/svg+xml \ No newline at end of file