diff --git a/Dockerfile b/Dockerfile index 1ea7703..ffbff48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,7 @@ RUN apt-get update -y && \ curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin # build Go binary -# remove debugging information and compress binary -RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o kube-sentry main.go -#RUN upx --brute kube-sentry +RUN CGO_ENABLED=0 go build -o kube-sentry main.go # copy binary into smaller image FROM alpine:latest diff --git a/README.md b/README.md index 7c006c9..c9c32f7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# kube-sentry +kube-sentry + +>Defend against high-risk workloads and gain visibility into vulnerable containers running on Kubernetes + + +# Introduction kube-sentry is a validating admission webhook for Kubernetes that scans incoming container images for vulnerabilities, exports scan results to prometheus, and can prevent pods from being created based on user specified rules. @@ -8,13 +13,15 @@ kube-sentry is a validating admission webhook for Kubernetes that scans incoming kube-sentry can be installed with the included helm chart +![kube-sentry-demo](docs/demo/demo.gif) + ```bash -helm install kube-sentry -n kube-sentry --create-namespace . +helm install kube-sentry -n kube-sentry . --wait ``` ## Dependencies -kube-sentry requires a remote trivy server for scanning container images. This can be installed using the trivy helm chart https://github.com/aquasecurity/trivy/tree/main/helm/trivy. +kube-sentry requires a remote trivy server for scanning container images. By default, it is installed as a chart dependency. ## Configuration diff --git a/deploy/kube-sentry/Chart.lock b/deploy/kube-sentry/Chart.lock new file mode 100644 index 0000000..6c9fe86 --- /dev/null +++ b/deploy/kube-sentry/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: trivy + repository: https://aquasecurity.github.io/helm-charts/ + version: 0.4.17 +digest: sha256:c4d6c7d1dd063f88c1726e417d3cf005af1d3a95c3d45b0b4036e7d1cce90cab +generated: "2022-10-29T22:58:19.421851-05:00" diff --git a/deploy/kube-sentry/Chart.yaml b/deploy/kube-sentry/Chart.yaml index 5bcc6c4..1fff27f 100644 --- a/deploy/kube-sentry/Chart.yaml +++ b/deploy/kube-sentry/Chart.yaml @@ -1,6 +1,20 @@ apiVersion: v2 name: kube-sentry -description: A Helm chart for Kubernetes +description: Defend against high-risk workloads and gain visibility into vulnerable containers running on Kubernetes type: application -version: 0.1.0 -appVersion: "1.16.0" +version: 1.0.0 +appVersion: "1.0.0" +sources: + - https://github.com/tks98/kube-sentry +maintainers: + - name: Travis Smith + email: travis.kenneth.smith@gmail.com + url: https://github.com/tks98 + +dependencies: + - name: trivy + version: "0.4.17" + repository: https://aquasecurity.github.io/helm-charts/ + condition: trivy.enabled + tags: + - trivy \ No newline at end of file diff --git a/deploy/kube-sentry/charts/trivy-0.4.17.tgz b/deploy/kube-sentry/charts/trivy-0.4.17.tgz new file mode 100644 index 0000000..a79dfb1 Binary files /dev/null and b/deploy/kube-sentry/charts/trivy-0.4.17.tgz differ diff --git a/deploy/kube-sentry/templates/cert.yaml b/deploy/kube-sentry/templates/cert.yaml index 00da63b..8bc7238 100644 --- a/deploy/kube-sentry/templates/cert.yaml +++ b/deploy/kube-sentry/templates/cert.yaml @@ -1,24 +1,30 @@ +{{- if .Values.webhook.caBundle.certmanager.enabled }} apiVersion: cert-manager.io/v1 kind: Issuer metadata: - name: self-signer + name: {{ include "kube-sentry.fullname" . }} namespace: {{.Release.Namespace}} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "-3" + labels: + {{- include "kube-sentry.labels" . | nindent 4 }} spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: - name: kube-sentry-cert + name: {{ include "kube-sentry.fullname" . }}-cert namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install + labels: + {{- include "kube-sentry.labels" . | nindent 4 }} spec: - secretName: {{.Values.webhook.caBundle.certmanager.secretName}} + secretName: {{ include "kube-sentry.fullname" . }}-cert dnsNames: -{{- toYaml .Values.webhook.caBundle.certmanager.dnsNames | nindent 4 }} + - {{ include "kube-sentry.fullname" . }}.{{.Release.Namespace}}.svc issuerRef: - name: self-signer \ No newline at end of file + name: {{ include "kube-sentry.fullname" . }} +{{- end }} \ No newline at end of file diff --git a/deploy/kube-sentry/templates/deployment.yaml b/deploy/kube-sentry/templates/deployment.yaml index 9d187ec..21daf7a 100644 --- a/deploy/kube-sentry/templates/deployment.yaml +++ b/deploy/kube-sentry/templates/deployment.yaml @@ -57,7 +57,7 @@ spec: volumes: - name: webhook-certs secret: - secretName: kube-sentry-cert + secretName: {{ include "kube-sentry.fullname" . }}-cert {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/deploy/kube-sentry/templates/webhook.yaml b/deploy/kube-sentry/templates/validatingwebhookconfiguration.yaml similarity index 54% rename from deploy/kube-sentry/templates/webhook.yaml rename to deploy/kube-sentry/templates/validatingwebhookconfiguration.yaml index 5c60587..ec66132 100644 --- a/deploy/kube-sentry/templates/webhook.yaml +++ b/deploy/kube-sentry/templates/validatingwebhookconfiguration.yaml @@ -2,11 +2,15 @@ kind: ValidatingWebhookConfiguration apiVersion: admissionregistration.k8s.io/v1 metadata: name: {{ include "kube-sentry.fullname" . }} + labels: + {{- include "kube-sentry.labels" . | nindent 4 }} annotations: - helm.sh/hook: post-install -{{- toYaml .Values.webhook.caBundle.certmanager.annotations | nindent 4 }} + {{- if .Values.webhook.caBundle.certmanager.enabled }} + cert-manager.io/inject-ca-from: {{.Release.Namespace}}/{{ include "kube-sentry.fullname" . }}-cert # namespace/secretName + {{- end }} webhooks: - - name: kube-sentry.kube-sentry.svc + - name: {{ include "kube-sentry.fullname" . }}.{{.Release.Namespace}}.svc + failurePolicy: Ignore # still forward api requests when unexpected errors are returned (expected failures still block pod creation) clientConfig: {{- if not .Values.webhook.caBundle.certmanager.enabled }} caBundle: {{ .Values.webhook.caBundle.value }} @@ -20,6 +24,6 @@ webhooks: apiVersions: ["v1"] resources: ["pods"] operations: ["CREATE"] - scope: Namespaced + scope: "*" sideEffects: None admissionReviewVersions: ["v1"] \ No newline at end of file diff --git a/deploy/kube-sentry/values.yaml b/deploy/kube-sentry/values.yaml index 076bc30..f221c3d 100644 --- a/deploy/kube-sentry/values.yaml +++ b/deploy/kube-sentry/values.yaml @@ -1,23 +1,22 @@ replicaCount: 1 +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + # The admission webhook needs to communicate with the k8s api server over https # A caBundle needs to be created and specified ValidatingWebhookConfiguration # These certs also need to be mounted into kube-sentry so that we can utilize them webhook: + name: kube-sentry.test.svc caBundle: certmanager: # certmanager's ca-injector can be used to inject the caBundle into the ValidationWebhookConfiguration https://cert-manager.io/docs/concepts/ca-injector/ enabled: true - secretName: kube-sentry-cert - annotations: - cert-manager.io/inject-ca-from: kube-sentry/kube-sentry-cert # namespace/secretName - dnsNames: - - kube-sentry.kube-sentry.svc value: "" # if you are not using certmanager, put the caBundle value here image: repository: docker.io/tks98/kube-sentry pullPolicy: Always - # Overrides the image tag whose default is the chart appVersion. tag: "latest" args: tlsCertFile: "/etc/webhook/certs/tls.crt" @@ -25,7 +24,7 @@ image: insecure: "false" listenAddr: ":8080" metricsAddr: ":8081" - trivyAddr: "trivy.default:4954" + trivyAddr: "trivy.kube-sentry:4954" trivyScheme: "http" metricsLabels: "report_name, image_namespace, image_registry, image_repository, image_tag,image_digest, severity, vulnerability_id, vulnerable_resource_name, installed_resource_version, fixed_resource_version, vulnerability_title, vulnerability_link" sentryMode: "true" @@ -40,27 +39,14 @@ image: containerPort: 8081 protocol: TCP -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - serviceAccount: - # Specifies whether a service account should be created create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" podAnnotations: prometheus.io/path: / prometheus.io/port: '8081' prometheus.io/scrape: 'true' -podSecurityContext: {} - # fsGroup: 2000 - securityContext: capabilities: drop: @@ -73,18 +59,11 @@ service: type: ClusterIP port: 443 - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - autoscaling: - enabled: false \ No newline at end of file + enabled: false + +# enable and configure trivy server instance to be deployed with kube-sentry +# https://github.com/aquasecurity/trivy/blob/main/helm/trivy/values.yaml +trivy: + enabled: true + fullnameOverride: "trivy" diff --git a/docs/configuration/certificates.md b/docs/configuration/certificates.md index 1247563..14abf75 100644 --- a/docs/configuration/certificates.md +++ b/docs/configuration/certificates.md @@ -25,8 +25,6 @@ webhook: annotations: cert-manager.io/inject-ca-from: kube-sentry/kube-sentry-cert # namespace/secretName dnsNames: - - kube-sentry - - kube-sentry.kube-sentry - kube-sentry.kube-sentry.svc value: "" # if you are not using certmanager, put the PEM encoded caBundle here and set enabled to false ``` \ No newline at end of file diff --git a/docs/demo/demo.gif b/docs/demo/demo.gif new file mode 100644 index 0000000..13e3f3b Binary files /dev/null and b/docs/demo/demo.gif differ diff --git a/docs/demo/demo.mp4 b/docs/demo/demo.mp4 new file mode 100644 index 0000000..be1a121 Binary files /dev/null and b/docs/demo/demo.mp4 differ diff --git a/docs/demo/demo.tape b/docs/demo/demo.tape new file mode 100644 index 0000000..0dacb10 --- /dev/null +++ b/docs/demo/demo.tape @@ -0,0 +1,33 @@ +Output demo.mp4 +Output demo.gif + +Set FontSize 20 +Set Width 1200 +Set Height 600 + +Type "cd /Users/tks/Projects/kube-sentry/deploy/kube-sentry" +Enter + +Sleep 1s +Type "ls" +Enter +Sleep 1s + +Space +Type "helm install kube-sentry -n kube-sentry . --wait" +Enter +Sleep 20s + +Space +Type "clear" +Enter +Sleep 1s + +Type "kubectl get pods -n kube-sentry" +Enter +Sleep 5s + +Space +Type "kubectl apply -f https://k8s.io/examples/pods/simple-pod.yaml -n kube-sentry" +Enter +Sleep 10s diff --git a/docs/diagrams/logo/kube-sentry-logo-banner.png b/docs/diagrams/logo/kube-sentry-logo-banner.png new file mode 100644 index 0000000..13af512 Binary files /dev/null and b/docs/diagrams/logo/kube-sentry-logo-banner.png differ diff --git a/docs/diagrams/logo/kube-sentry-logo.png b/docs/diagrams/logo/kube-sentry-logo.png new file mode 100644 index 0000000..7175bef Binary files /dev/null and b/docs/diagrams/logo/kube-sentry-logo.png differ diff --git a/go.mod b/go.mod index 5d478f9..6712c18 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,10 @@ require ( ) require ( + bitbucket.org/creachadair/shell v0.0.7 // indirect github.com/aquasecurity/trivy-db v0.0.0-20220904090734-9dd4c7776a52 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bitfield/script v0.20.2 // indirect github.com/caarlos0/env/v6 v6.10.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/go-logr/logr v1.2.3 // indirect @@ -23,6 +25,8 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-containerregistry v0.11.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/itchyny/gojq v0.12.7 // indirect + github.com/itchyny/timefmt-go v0.1.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect diff --git a/go.sum b/go.sum index e84c699..c0862e6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= +bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -46,6 +48,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitfield/script v0.20.2 h1:4DexsRtBILVMEn3EZwHbtJdDqdk43sXI8gM3F04JXgs= +github.com/bitfield/script v0.20.2/go.mod h1:l3AZPVAtKQrL03bwh7nlNTUtgrgSWurpJSbtqspYrOA= github.com/caarlos0/env/v6 v6.10.0 h1:lA7sxiGArZ2KkiqpOQNf8ERBRWI+v8MWIH+eGjSN22I= github.com/caarlos0/env/v6 v6.10.0/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -122,6 +126,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-containerregistry v0.11.0 h1:Xt8x1adcREjFcmDoDK8OdOsjxu90PHkGuwNP8GiHMLM= github.com/google/go-containerregistry v0.11.0/go.mod h1:BBaYtsHPHA42uEgAvd/NejvAfPSlz281sJWqupjSxfk= @@ -143,6 +149,10 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/itchyny/gojq v0.12.7 h1:hYPTpeWfrJ1OT+2j6cvBScbhl0TkdwGM4bc66onUSOQ= +github.com/itchyny/gojq v0.12.7/go.mod h1:ZdvNHVlzPgUf8pgjnuDTmGfHA/21KoutQUJ3An/xNuw= +github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= +github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -164,6 +174,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -368,8 +380,10 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -536,6 +550,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 6559b8c..e1ca7dd 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -9,6 +8,7 @@ import ( kwhprometheus "github.com/slok/kubewebhook/v2/pkg/metrics/prometheus" kwhwebhook "github.com/slok/kubewebhook/v2/pkg/webhook" kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating" + "github.com/tks98/kube-sentry/pkg/config" "github.com/tks98/kube-sentry/pkg/logging" "github.com/tks98/kube-sentry/pkg/metrics" "github.com/tks98/kube-sentry/pkg/scanner" @@ -17,66 +17,15 @@ import ( "net/http" "os" "os/exec" - "strings" ) -// config stores the application's configuration state -type config struct { - certFile string - keyFile string - logLevel string - addr string - metricsAddr string - trivyAddr string - trivyScheme string - insecure bool - metricsLabels string - forbiddenCVEs string - numAllowedCVEs string - numCriticalCVEs string - sentryMode bool -} - -// initFlags() parses the application arguments and creates a config type -func initFlags() *config { - cfg := &config{} - - fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError) - fl.StringVar(&cfg.certFile, "tls-cert-file", "", "TLS certificate file") - fl.StringVar(&cfg.keyFile, "tls-key-file", "", "TLS key file") - fl.StringVar(&cfg.logLevel, "log-level", "info", "Specifies the logging level (info or debug") - fl.StringVar(&cfg.addr, "listen-addr", ":8080", "The address to start the server") - fl.StringVar(&cfg.metricsAddr, "metrics-addr", ":8081", "The address to start metrics server") - fl.StringVar(&cfg.trivyAddr, "trivy-addr", "http://127.0.0.1:4954", "The address of the remote trivy server") - fl.StringVar(&cfg.trivyScheme, "trivy-scheme", "http", "The scheme to reach trivy server (http or https") - fl.BoolVar(&cfg.insecure, "insecure", false, "Allow insecure server connections to trivy server when using TLS") - fl.StringVar(&cfg.metricsLabels, "metrics-labels", "", "Specifies the metrics labels to export. If not given, will export all") - fl.BoolVar(&cfg.sentryMode, "sentry-mode", false, "Enables or disables rejecting pods based on trivy scan results") - fl.StringVar(&cfg.forbiddenCVEs, "forbidden-cves", "", "Specifies which CVEs in images causes pod validation to fail") - fl.StringVar(&cfg.numCriticalCVEs, "num-critical-cves", "", "Specifies max number of critical CVEs pod images can have") - fl.StringVar(&cfg.numAllowedCVEs, "num-allowed-cves", "", "Specifies max number of CVEs pod images can have") - - _ = fl.Parse(os.Args[1:]) - - // clean up args - cfg.forbiddenCVEs = strings.Trim(cfg.forbiddenCVEs, "\"") - cfg.forbiddenCVEs = strings.Trim(cfg.forbiddenCVEs, "\n") - cfg.forbiddenCVEs = strings.Trim(cfg.forbiddenCVEs, "") - cfg.forbiddenCVEs = strings.Trim(cfg.forbiddenCVEs, " ") - cfg.metricsLabels = strings.Trim(cfg.metricsLabels, "\"") - cfg.metricsLabels = strings.Trim(cfg.metricsLabels, "\n") - cfg.metricsLabels = strings.Trim(cfg.metricsLabels, "") - cfg.metricsLabels = strings.Trim(cfg.metricsLabels, " ") - return cfg -} - func main() { - // parse config and clean args - cfg := initFlags() + // parse program arguments into config type + cfg := config.ParseFlags() // init logging and parse flags - logger, err := logging.NewLogger(cfg.logLevel) + logger, err := logging.NewLogger(cfg.LogLevel) if err != nil { fmt.Printf(err.Error()) os.Exit(1) @@ -90,7 +39,7 @@ func main() { } // create a new trivy scanner - trivyScanner, err := scanner.NewScanner(cfg.trivyAddr, cfg.insecure, logger, cfg.trivyScheme) + trivyScanner, err := scanner.NewScanner(cfg.TrivyAddr, cfg.Insecure, logger, cfg.TrivyScheme) if err != nil { logger.Errorf(err.Error()) os.Exit(1) @@ -99,8 +48,8 @@ func main() { // determine which criteria result in kube-sentry blocking pod creation var rejectionCriteria *webhook.RejectionCriteria - if cfg.sentryMode { - rejectionCriteria, err = webhook.InitRejectionCriteria(cfg.forbiddenCVEs, cfg.numCriticalCVEs, cfg.numAllowedCVEs) + if cfg.SentryMode { + rejectionCriteria, err = webhook.InitRejectionCriteria(cfg.ForbiddenCVEs, cfg.NumCriticalCVEs, cfg.NumAllowedCVEs) if err != nil { logger.Errorf(err.Error()) os.Exit(1) @@ -115,7 +64,7 @@ func main() { } // create webhook - config := kwhvalidating.WebhookConfig{ + webhookConfig := kwhvalidating.WebhookConfig{ ID: "imageScanner", Obj: &v1.Pod{}, Validator: scannerWebhook, @@ -123,7 +72,7 @@ func main() { } // register the webhook - wh, err := kwhvalidating.NewWebhook(config) + wh, err := kwhvalidating.NewWebhook(webhookConfig) if err != nil { logger.Errorf("error creating webhook: %s", err) os.Exit(1) @@ -138,14 +87,14 @@ func main() { } // register the vulnerabilityInfo collector for exporting scan results - metrics.RegisterVulnerabilityCollector(reg, cfg.metricsLabels) + metrics.RegisterVulnerabilityCollector(reg, cfg.MetricsLabels) errCh := make(chan error) // serve the webhook go func() { - logger.Infof("Listening on %s", cfg.addr) - err = http.ListenAndServeTLS(cfg.addr, cfg.certFile, cfg.keyFile, kwhhttp.MustHandlerFor(kwhhttp.HandlerConfig{ + logger.Infof("Listening on %s", cfg.Addr) + err = http.ListenAndServeTLS(cfg.Addr, cfg.CertFile, cfg.KeyFile, kwhhttp.MustHandlerFor(kwhhttp.HandlerConfig{ Webhook: kwhwebhook.NewMeasuredWebhook(metricsRec, wh), Logger: logger, })) @@ -155,10 +104,10 @@ func main() { errCh <- nil }() - // serve metrics. + // serve metrics go func() { - logger.Infof("Listening metrics on %s", cfg.metricsAddr) - err = http.ListenAndServe(cfg.metricsAddr, promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) + logger.Infof("Listening metrics on %s", cfg.MetricsAddr) + err = http.ListenAndServe(cfg.MetricsAddr, promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) if err != nil { errCh <- fmt.Errorf("error serving webhook metrics: %w", err) } diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..eced269 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,57 @@ +package config + +import ( + "flag" + "os" + "strings" +) + +// config stores the application's configuration state +type config struct { + CertFile string + KeyFile string + LogLevel string + Addr string + MetricsAddr string + TrivyAddr string + TrivyScheme string + Insecure bool + MetricsLabels string + ForbiddenCVEs string + NumAllowedCVEs string + NumCriticalCVEs string + SentryMode bool +} + +// ParseFlags parses the application arguments and creates a config type +func ParseFlags() *config { + cfg := &config{} + + fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + fl.StringVar(&cfg.CertFile, "tls-cert-file", "", "TLS certificate file") + fl.StringVar(&cfg.KeyFile, "tls-key-file", "", "TLS key file") + fl.StringVar(&cfg.LogLevel, "log-level", "info", "Specifies the logging level (info or debug") + fl.StringVar(&cfg.Addr, "listen-addr", ":8080", "The address to start the server") + fl.StringVar(&cfg.MetricsAddr, "metrics-addr", ":8081", "The address to start metrics server") + fl.StringVar(&cfg.TrivyAddr, "trivy-addr", "http://127.0.0.1:4954", "The address of the remote trivy server") + fl.StringVar(&cfg.TrivyScheme, "trivy-scheme", "http", "The scheme to reach trivy server (http or https") + fl.BoolVar(&cfg.Insecure, "insecure", false, "Allow insecure connections to container registries") + fl.StringVar(&cfg.MetricsLabels, "metrics-labels", "", "Specifies the metrics labels to export. If not given, will export all") + fl.BoolVar(&cfg.SentryMode, "sentry-mode", false, "Enables or disables rejecting pods based on trivy scan results") + fl.StringVar(&cfg.ForbiddenCVEs, "forbidden-cves", "", "Specifies which CVEs in images causes pod validation to fail") + fl.StringVar(&cfg.NumCriticalCVEs, "num-critical-cves", "", "Specifies max number of critical CVEs pod images can have") + fl.StringVar(&cfg.NumAllowedCVEs, "num-allowed-cves", "", "Specifies max number of CVEs pod images can have") + + _ = fl.Parse(os.Args[1:]) + + // clean up args + cfg.ForbiddenCVEs = strings.Trim(cfg.ForbiddenCVEs, "\"") + cfg.ForbiddenCVEs = strings.Trim(cfg.ForbiddenCVEs, "\n") + cfg.ForbiddenCVEs = strings.Trim(cfg.ForbiddenCVEs, "") + cfg.ForbiddenCVEs = strings.Trim(cfg.ForbiddenCVEs, " ") + cfg.MetricsLabels = strings.Trim(cfg.MetricsLabels, "\"") + cfg.MetricsLabels = strings.Trim(cfg.MetricsLabels, "\n") + cfg.MetricsLabels = strings.Trim(cfg.MetricsLabels, "") + cfg.MetricsLabels = strings.Trim(cfg.MetricsLabels, " ") + return cfg +} diff --git a/pkg/exec/exec.go b/pkg/exec/exec.go new file mode 100644 index 0000000..1cc5886 --- /dev/null +++ b/pkg/exec/exec.go @@ -0,0 +1,49 @@ +package exec + +import ( + "bytes" + "fmt" + "os/exec" + "syscall" +) + +const defaultFailedCode = 1 + +func RunCommand(name string, args ...string) (string, error) { + + var stdout, stderr string + var exitCode int + + var outbuf, errbuf bytes.Buffer + cmd := exec.Command(name, args...) + cmd.Stdout = &outbuf + cmd.Stderr = &errbuf + + err := cmd.Run() + stdout = outbuf.String() + stderr = errbuf.String() + + if err != nil { + // attempt to get the error code from the failed program + if exitError, ok := err.(*exec.ExitError); ok { + ws := exitError.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + } else { + exitCode = defaultFailedCode + if stderr == "" { + stderr = err.Error() + } + } + } else { + // command executed successfully, exit code should be 0 in this case + ws := cmd.ProcessState.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + } + + // return results of the command + if exitCode == 0 { + return stdout, nil // command executed successfully + } else { + return stdout, fmt.Errorf(stderr + stdout) // there was a problem + } +} diff --git a/pkg/metrics/register.go b/pkg/metrics/register.go index d709ba9..2715e88 100644 --- a/pkg/metrics/register.go +++ b/pkg/metrics/register.go @@ -1,7 +1,6 @@ package metrics import ( - "fmt" "github.com/prometheus/client_golang/prometheus" "strings" ) @@ -71,7 +70,6 @@ func GetMetricsLabels(l string) []VulnerabilityLabel { Scope: kind, }) } - fmt.Printf("Labels: %v\n", vulnerabilityLabels) return vulnerabilityLabels } diff --git a/pkg/scanner/trivy.go b/pkg/scanner/trivy.go index 730040e..bcc350e 100644 --- a/pkg/scanner/trivy.go +++ b/pkg/scanner/trivy.go @@ -6,9 +6,9 @@ import ( "github.com/aquasecurity/trivy/pkg/types" parser "github.com/novln/docker-parser" kwhlog "github.com/slok/kubewebhook/v2/pkg/log" + "github.com/tks98/kube-sentry/pkg/exec" "github.com/tks98/kube-sentry/pkg/metrics" v1 "k8s.io/api/core/v1" - "os/exec" ) // Scanner represents a trivy scanner @@ -54,7 +54,6 @@ func (s *Scanner) ScanImages(pod *v1.Pod) ([]*types.Report, error) { } reports = append(reports, report) - } s.Logger.Debugf("images were scanned") @@ -76,16 +75,15 @@ func (s *Scanner) sendScanRequest(image string) (*types.Report, error) { s.Logger.Debugf("sending scan request for image %s", image) s.Logger.Debugf("%s:%v", command, args) - out, err := exec.Command(command, args...).Output() + out, err := exec.RunCommand(command, args...) if err != nil { - s.Logger.Errorf("error running trivy %s", err.Error()) return nil, err } s.Logger.Debugf("image %s has been scanned", image) var report types.Report - err = json.Unmarshal(out, &report) + err = json.Unmarshal([]byte(out), &report) if err != nil { return nil, err } diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index eafb735..a5b6933 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -50,13 +50,21 @@ func (is *ImageScanner) Validate(_ context.Context, _ *kwhmodel.AdmissionReview, return nil, fmt.Errorf("not a pod") } - is.Logger.Infof("pod %s is valid", pod.Name) - // scan container images and export results results, err := is.Scanner.ScanImages(pod) if err != nil { + var warnings []string + err = fmt.Errorf("kube-sentry error, allowing api request: %s", err.Error()) is.Logger.Errorf(err.Error()) - return nil, err + + // return valid response since we do not want to block api requests if there is a problem with kube-sentry + // attach warning so user knows there was a problem + warnings = append(warnings, err.Error()) + is.Logger.Errorf("%v", warnings) + return &kwhvalidating.ValidatorResult{ + Valid: true, + Warnings: warnings, + }, err } is.Logger.Infof("%s images have been scanned", pod.Name) @@ -80,7 +88,7 @@ func (is *ImageScanner) getValidatorResult(results []*types.Report) *kwhvalidati return allowed } - is.Logger.Debugf("Checking if report passes validation") + is.Logger.Debugf("checking if report passes validation") // check if total number of CVEs is over allowed value, if enabled if is.RejectionCriteria.NumAllowedCVEs != nil { @@ -96,7 +104,7 @@ func (is *ImageScanner) getValidatorResult(results []*types.Report) *kwhvalidati if total > is.RejectionCriteria.NumAllowedCVEs.AllowedCVEs { is.Logger.Debugf("too many CVEs") - rulesViolated = append(rulesViolated, "pod container images contain too many total vulnerabilities ") + rulesViolated = append(rulesViolated, "pod container images contain too many total vulnerabilities") } } @@ -133,7 +141,7 @@ func (is *ImageScanner) getValidatorResult(results []*types.Report) *kwhvalidati for _, vuln := range result.Vulnerabilities { if slices.Contains(is.RejectionCriteria.ForbiddenCVEs.CVEs, vuln.VulnerabilityID) { is.Logger.Infof("forbidden CVE found %s", vuln.VulnerabilityID) - msg := fmt.Sprintf("pod container image %s contains forbidden CVE %s", result.Target, vuln.VulnerabilityID) + msg := fmt.Sprintf("pod container image %s contains forbidden CVE: %s", result.Target, vuln.VulnerabilityID) rulesViolated = append(rulesViolated, msg) }