Skip to content

Commit

Permalink
add support for kubernetes validating admission webhook
Browse files Browse the repository at this point in the history
*add support for validating admission webhook
*unit tests for validating admission webhook
*refactor to move db logger into a separate dedicated package
*refactor validate handler to move specific functionality into webhook package
*fixing code smells and bugs in UI
*fixing html file bugs
*update documentation and fix documentation bullets
*serving the CSS locally instead of fetching from internet and go mod tidy
*fix: admission request is saved in db logs
*incorporate review comments
  • Loading branch information
kanchwala-yusuf authored Apr 12, 2021
1 parent 114222a commit dcfbd54
Show file tree
Hide file tree
Showing 52 changed files with 8,639 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ docs/_build/

.DS_Store

vendor/
vendor/
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
BUILD_FLAGS := -v -ldflags "-w -s"
ENV_SETTINGS := CGO_ENABLED=0
ENV_SETTINGS := CGO_ENABLED=1

BUILD_DIR = ./bin
BINARY_NAME = terrascan
Expand Down
7 changes: 6 additions & 1 deletion build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ FROM golang:alpine AS builder

ARG GOOS_VAL=linux
ARG GOARCH_VAL=amd64
ARG CGO_ENABLED_VAL=0
ARG CGO_ENABLED_VAL=1

WORKDIR $GOPATH/src/terrascan

# download go dependencies
COPY go.mod go.sum ./
RUN go mod download
RUN apk add -U build-base

# copy terrascan source
COPY . .
Expand All @@ -34,6 +35,10 @@ ENV PATH /go/bin:$PATH
# copy terrascan binary from build
COPY --from=builder /go/bin/terrascan /go/bin/terrascan

# Copy webhooks UI templates & assets
COPY ./pkg/http-server/templates /go/terrascan
COPY ./pkg/http-server/assets /go/terrascan/assets

EXPOSE 9010

ENTRYPOINT ["/go/bin/terrascan"]
Expand Down
104 changes: 104 additions & 0 deletions docs/getting-started/admission-controller-webhooks-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Using Terrascan as a Kubernetes Admission Controller

## Overview
Terrascan can be integrated with K8s [admissions webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/).
It can be used as one of the validating webhooks to be used and scan new configurations.

In this guide, we'll demonstrate how Terrascan can be configured to:
* Scan configuration changes policies when an object is being created or updated
* Allow / reject the request in case a violation is detected


## Installation Guide

### Create an instance
Your Terrascan instance has the following requirements for being able to scan K8s configurations.

1. Be accessible via HTTPS. Make sure your cloud firewall is configured to allow this.
2. Have a valid SSL certificate for the served domain name. To do that, choose one of our suggested methods:
- Use a subdomain of your choosing (e.g dev-terrascan-k8s.accurics.com) and create a valid certificate for this subdomain through your SSL certificate provider. [Let's Encrypt](https://letsencrypt.org/) is a free, simple to use certificate authority you can use.
- Use a reverse-proxy to serve SSL requests; for example, use Cloudflare Flexible to get a certificate by a trusted-CA to your [self-signed certificate](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs).
- Generate a self-signed certificate and have your K8s cluster trust it. To add a trusted CA to ca-pemstore, as demonstrated in [paraspatidar's blog post](https://medium.com/@paraspatidar/add-ssl-tls-certificate-or-pem-file-to-kubernetes-pod-s-trusted-root-ca-store-7bed5cd683d).
3. Use the Terrascan docker as demonstrated in this document, or run it from the sources.

### Run Terrascan webhook service
Run Terrascan docker image in your server using the following command:
```bash
sudo docker run -p 443:9443 -v <DATA_PATH>:/data -u root -e K8S_WEBHOOK_API_KEY=<API_KEY> accurics/terrascan server --cert-path /data/cert.pem --key-path /data/key.pem -c /data/config.toml
```
`<API_KEY>` is a key used for authentication between your K8s environment and the Terrascan server. Generate your preferred key and use it here.

`<DATA_PATH>` is a directory path in your server where both the certificate and the private key .pem files are stored.
In addition, this directory is used to save the webhook logs. (An SQLite file)

You can specify a config file that specifies which policies to use in the scan and which violations should lead to rejection.

A config file example: ```config.toml```
```bash
[severity]
level = "medium"
[rules]
skip-rules = [
"accurics.kubernetes.IAM.107"
]

[k8s-deny-rules]
denied-categories = [
"Network Ports Security"
]
denied-severity = "high"
```

You can specify the following configurations:
* **scan-rules** - one or more rules to scan
* **skip-rules** - one or more rules to skip while scanning
* **severity** - the minimal level of severity of the policies to be scanned
* **category** - the list of type of categories of the policies to be scanned


* **k8s-deny-rules** - specify the rules that should cause a rejection of the admission request
* **denied-categories** - one or more policy categories that are not allowed in the detected violations
* **denied-severity** - the minimal level of severity that should cause a rejection

### Configure K8s to send webhooks
Configure a new ```ValidatingWebhookConfiguration``` in your Kubernetes environment and specify your Terrascan server endpoint.

Example:
```bash
cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: my.validation.example.check
webhooks:
- name: my.validation.example.check
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- pods
- services
failurePolicy: Fail
clientConfig:
url: https://<SERVER_ADDRESS>/v1/k8s/webhooks/<API_KEY>/scan
sideEffects: None
admissionReviewVersions: ["v1"]
EOF
```
* You can modify the `rules` that trigger the webhook according to your preferences.
* Update the ```clientConfig``` URL with your terrascan server address and the API key you generated before.
### Test your settings
Try to run a new pod / service. For example:
``` Bash
kubectl run mynginx --image=nginx
```
Go to ```https://<SERVER_ADDRESS>/k8s/webhooks/<API_KEY>/logs``` and verify your request is logged.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/iancoleman/strcase v0.1.3
github.com/itchyny/gojq v0.12.1
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-sqlite3 v1.12.0
github.com/mitchellh/go-homedir v1.1.0
github.com/onsi/ginkgo v1.12.1
github.com/onsi/gomega v1.10.5
Expand All @@ -36,5 +37,7 @@ require (
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
helm.sh/helm/v3 v3.4.0
honnef.co/go/tools v0.1.3 // indirect
k8s.io/api v0.19.2
k8s.io/apimachinery v0.19.2
sigs.k8s.io/kustomize/api v0.8.5
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do=
github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
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/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import (
var (
// LogLevel Logging level (debug, info, warn, error, panic, fatal)
LogLevel string

// LogType Logging output type (console, json)
LogType string

// OutputType Violation output type (human, json, yaml, xml)
OutputType string

// ConfigFile Config file path
ConfigFile string
)
Expand Down
16 changes: 15 additions & 1 deletion pkg/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ import (
"github.com/spf13/cobra"
)

var (
// Port at which API server will listen
port string

// CertFile Certificate file path, required in order to enable secure HTTP server
certFile string

// PrivateKeyFile Private key file path, required in order to enable secure HTTP server
privateKeyFile string
)

var serverCmd = &cobra.Command{
Use: "server",
Short: "Run Terrascan as an API server",
Expand All @@ -35,9 +46,12 @@ Run Terrascan as an API server that inspects incoming IaC (Infrastructure-as-Cod
}

func server(cmd *cobra.Command, args []string) {
httpserver.Start()
httpserver.Start(port, ConfigFile, certFile, privateKeyFile)
}

func init() {
serverCmd.Flags().StringVarP(&privateKeyFile, "key-path", "", "", "private key file path")
serverCmd.Flags().StringVarP(&certFile, "cert-path", "", "", "certificate file path")
serverCmd.Flags().StringVarP(&port, "port", "p", httpserver.GatewayDefaultPort, "server port")
RegisterCommand(rootCmd, serverCmd)
}
5 changes: 5 additions & 0 deletions pkg/config/config-reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,8 @@ func (r TerrascanConfigReader) getCategory() Category {
func (r TerrascanConfigReader) getSeverity() Severity {
return r.config.Severity
}

// GetK8sDenyRules will return the k8s deny rules specified in the terrascan config file
func (r TerrascanConfigReader) GetK8sDenyRules() K8sDenyRules {
return r.config.K8sDenyRules
}
7 changes: 7 additions & 0 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type TerrascanConfig struct {
Rules `toml:"rules,omitempty"`
Category `toml:"category,omitempty"`
Severity `toml:"severity,omitempty"`
K8sDenyRules `toml:"k8s-deny-rules,omitempty"`
}

// Category defines the categories of violations that you want to be reported
Expand Down Expand Up @@ -61,3 +62,9 @@ type Rules struct {
ScanRules []string `toml:"scan-rules,omitempty"`
SkipRules []string `toml:"skip-rules,omitempty"`
}

// K8sDenyRules deny rules in the terrascan config file
type K8sDenyRules struct {
DeniedSeverity string `toml:"denied-severity,omitempty"`
Categories []string `toml:"denied-categories,omitempty"`
}
7 changes: 7 additions & 0 deletions pkg/http-server/assets/bootstrap.min.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pkg/http-server/assets/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions pkg/http-server/assets/jsonTree.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* JSON Tree Viewer
* http://github.com/summerstyle/jsonTreeViewer
*
* Copyright 2017 Vera Lobacheva (http://iamvera.com)
* Released under the MIT license (LICENSE.txt)
*/

/* Background for the tree. May use for <body> element */
.jsontree_bg {
background: #FFF;
}

/* Styles for the container of the tree (e.g. fonts, margins etc.) */
.jsontree_tree {
/*margin-left: 30px;*/
font-family: 'PT Mono', monospace;
font-size: 14px;
}

/* Styles for a list of child nodes */
.jsontree_child-nodes {
display: none;
margin-left: 35px;
margin-bottom: 5px;
line-height: 2;
}
.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes {
display: block;
}

/* Styles for labels */
.jsontree_label-wrapper {
float: left;
margin-right: 8px;
}
.jsontree_label {
font-weight: normal;
vertical-align: top;
color: #000;
position: relative;
padding: 1px;
border-radius: 4px;
cursor: default;
}
.jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label {
background: #fff2aa;
}

/* Styles for values */
.jsontree_value-wrapper {
display: block;
/*overflow: hidden;*/
}
.jsontree_node_complex > .jsontree_value-wrapper {
overflow: inherit;
}
.jsontree_value {
vertical-align: top;
display: inline;
}
.jsontree_value_null {
color: #777;
font-weight: bold;
}
.jsontree_value_string {
color: #025900;
font-weight: bold;
}
.jsontree_value_number {
color: #000E59;
font-weight: bold;
}
.jsontree_value_boolean {
color: #600100;
font-weight: bold;
}

/* Styles for active elements */
.jsontree_expand-button {
position: absolute;
top: 3px;
left: -15px;
display: block;
width: 11px;
height: 11px;
background-image: url('icons.svg');
}
.jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button {
background-position: 0 -11px;
}
.jsontree_show-more {
cursor: pointer;
}
.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
display: none;
}
.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button,
.jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
display: none !important;
}
.jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label {
cursor: pointer;
}
.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label {
cursor: default !important;
}
Loading

0 comments on commit dcfbd54

Please sign in to comment.