diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2905dcec..146f2498 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,21 @@
# x.x.x (next release)
+
+# 1.0.0
+## New Features
+* Added support for custom checks using JSON Schema
+* Added support for arbitrary controllers, rather than a pre-configured set
+ * removed support for `controllers_to_scan` in config
* Added the ability to exempt a particular controller from a particular check.
-* Breaking changes in the config format.
-* Added support for finding the Owners, this will allow Polaris to work with types of Controllers it doesn't even know about.
+* Docker image now includes the default config
+
+## Breaking Changes
+* Breaking changes in both input and output formats. See [Examples](/examples) for examples of the new formats.
+ * removed config-level configuration for checks like max/min memory settings
+ * changed severity `error` to `danger`
+* Breaking changes [to the CLI](/docs/usage.md#cli-options)
+ * CLI flag `--set-exit-code-on-error` is now `--set-exit-code-on-danger`
+ * Flags `--version`, `--dashboard`, `--webhook`, and `--audit` are now arguments
+ * Port flags are now just `--port`
# 0.6.0
* Fixed webhook support in Kubernetes 1.16
diff --git a/README.md b/README.md
index 8cd590da..da39b3db 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
-

+
[![Version][version-image]][version-link] [![CircleCI][circleci-image]][circleci-link] [![Go Report Card][goreport-image]][goreport-link]
-[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.6.0&color=239922
+[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=1.0.0&color=239922
[version-link]: https://github.com/FairwindsOps/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/FairwindsOps/polaris
@@ -43,7 +43,7 @@ With the port forwarding in place, you can open http://localhost:8080 in your br
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes workloads as well as a roadmap for what can be improved. The dashboard provides a cluster wide overview as well as breaking out results by category, namespace, and workload.
-
+
Our default standards in Polaris are rather high, so don’t be surprised if your score is lower than you might expect. A key goal for Polaris was to set a high standard and aim for great configuration by default. If the defaults we’ve included are too strict, it’s easy to adjust the configuration as part of the deployment configuration to better suit your workloads.
diff --git a/deploy/dashboard.yaml b/deploy/dashboard.yaml
index 7650aa10..b3a3f1b8 100644
--- a/deploy/dashboard.yaml
+++ b/deploy/dashboard.yaml
@@ -5,183 +5,43 @@ kind: Namespace
metadata:
name: polaris
---
-# Source: polaris/templates/configmap.yaml
+# Source: polaris/templates/rbac.yaml
apiVersion: v1
-kind: ConfigMap
+kind: ServiceAccount
metadata:
name: polaris
namespace: polaris
labels:
app: polaris
-data:
- config.yaml: |-
- checks:
- # resources
- cpuRequestsMissing: warning
- cpuLimitsMissing: warning
- memoryRequestsMissing: warning
- memoryLimitsMissing: warning
- # images
- tagNotSpecified: danger
- pullPolicyNotAlways: ignore
- # healthChecks
- readinessProbeMissing: warning
- livenessProbeMissing: warning
- # networking
- hostNetworkSet: warning
- hostPortSet: warning
- # security
- hostIPCSet: danger
- hostPIDSet: danger
- notReadOnlyRootFilesystem: warning
- privilegeEscalationAllowed: danger
- runAsRootAllowed: warning
- runAsPrivileged: danger
- dangerousCapabilities: danger
- insecureCapabilities: warning
- controllersToScan:
- - Deployments
- - StatefulSets
- - DaemonSets
- - CronJobs
- - Jobs
- - ReplicationControllers
- exemptions:
- - controllerNames:
- - dns-controller
- - datadog-datadog
- - kube-flannel-ds
- - kube2iam
- - aws-iam-authenticator
- - datadog
- - kube2iam
- rules:
- - hostNetworkSet
- - controllerNames:
- - aws-iam-authenticator
- - aws-cluster-autoscaler
- - kube-state-metrics
- - dns-controller
- - external-dns
- - dnsmasq
- - autoscaler
- - kubernetes-dashboard
- - install-cni
- - kube2iam
- rules:
- - readinessProbeMissing
- - livenessProbeMissing
- - controllerNames:
- - aws-iam-authenticator
- - nginx-ingress-controller
- - nginx-ingress-default-backend
- - aws-cluster-autoscaler
- - kube-state-metrics
- - dns-controller
- - external-dns
- - kubedns
- - dnsmasq
- - autoscaler
- - tiller
- - kube2iam
- rules:
- - runAsRootAllowed
- - controllerNames:
- - aws-iam-authenticator
- - nginx-ingress-controller
- - nginx-ingress-default-backend
- - aws-cluster-autoscaler
- - kube-state-metrics
- - dns-controller
- - external-dns
- - kubedns
- - dnsmasq
- - autoscaler
- - tiller
- - kube2iam
- rules:
- - notReadOnlyRootFilesystem
- - controllerNames:
- - cert-manager
- - dns-controller
- - kubedns
- - dnsmasq
- - autoscaler
- - insights-agent-goldilocks-vpa-install
- rules:
- - cpuRequestsMissing
- - cpuLimitsMissing
- - memoryRequestsMissing
- - memoryLimitsMissing
- - controllerNames:
- - kube2iam
- - kube-flannel-ds
- rules:
- - runAsPrivileged
- - controllerNames:
- - kube-hunter
- rules:
- - hostPIDSet
- - controllerNames:
- - polaris
- - kube-hunter
- - goldilocks
- - insights-agent-goldilocks-vpa-install
- rules:
- - notReadOnlyRootFilesystem
- - controllerNames:
- - insights-agent-goldilocks-controller
- rules:
- - livenessProbeMissing
- - readinessProbeMissing
- - controllerNames:
- - insights-agent-goldilocks-vpa-install
- - kube-hunter
- rules:
- - runAsRootAllowed
-
---
-# Source: polaris/templates/dashboard.rbac.yaml
-apiVersion: v1
-kind: ServiceAccount
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
metadata:
- name: polaris-dashboard
- namespace: polaris
+ name: polaris-view
labels:
app: polaris
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: view
+subjects:
+ - kind: ServiceAccount
+ name: polaris
+ namespace: polaris
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
- name: polaris-dashboard
+ name: polaris
labels:
app: polaris
rules:
- - apiGroups:
- - 'apps'
- - 'extensions'
- resources:
- - 'deployments'
- - 'statefulsets'
- - 'daemonsets'
- verbs:
- - 'get'
- - 'list'
- - apiGroups:
- - 'batch'
- resources:
- - 'jobs'
- - 'cronjobs'
- verbs:
- - 'get'
- - 'list'
+ # required by controller-runtime code doing a cluster wide lookup
+ # when it seems namespace would suffice
- apiGroups:
- ''
resources:
- 'nodes'
- - 'namespaces'
- - 'pods'
- - 'replicationcontrollers'
verbs:
- 'get'
- 'list'
@@ -189,17 +49,18 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
- name: polaris-dashboard
+ name: polaris
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
- name: polaris-dashboard
+ name: polaris
subjects:
- kind: ServiceAccount
- name: polaris-dashboard
+ name: polaris
namespace: polaris
+
---
# Source: polaris/templates/dashboard.service.yaml
apiVersion: v1
@@ -212,7 +73,7 @@ metadata:
annotations:
spec:
ports:
- - name: dashboard
+ - name: http-dashboard
port: 80
protocol: TCP
targetPort: 8080
@@ -238,23 +99,15 @@ spec:
component: dashboard
template:
metadata:
- annotations:
- checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501'
labels:
app: polaris
component: dashboard
spec:
- volumes:
- - name: config
- configMap:
- name: polaris
containers:
- command:
- polaris
- - --dashboard
- - --config
- - /opt/app/config.yaml
- image: 'quay.io/fairwinds/polaris:0.6'
+ - dashboard
+ image: 'quay.io/fairwinds/polaris:1'
imagePullPolicy: 'Always'
name: dashboard
ports:
@@ -286,12 +139,7 @@ spec:
capabilities:
drop:
- ALL
- volumeMounts:
- - name: config
- mountPath: /opt/app/config.yaml
- subPath: config.yaml
- readOnly: true
- serviceAccountName: polaris-dashboard
+ serviceAccountName: polaris
nodeSelector:
tolerations:
---
@@ -300,6 +148,10 @@ spec:
---
# Source: polaris/templates/audit.rbac.yaml
+---
+# Source: polaris/templates/configmap.yaml
+
+
---
# Source: polaris/templates/ingress.yaml
diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml
index ccee1cda..7003f0d6 100644
--- a/deploy/webhook.yaml
+++ b/deploy/webhook.yaml
@@ -18,187 +18,71 @@ type: Opaque
stringData:
cert.pem: ''
---
-# Source: polaris/templates/configmap.yaml
+# Source: polaris/templates/rbac.yaml
apiVersion: v1
-kind: ConfigMap
+kind: ServiceAccount
metadata:
name: polaris
namespace: polaris
labels:
app: polaris
-data:
- config.yaml: |-
- checks:
- # resources
- cpuRequestsMissing: warning
- cpuLimitsMissing: warning
- memoryRequestsMissing: warning
- memoryLimitsMissing: warning
- # images
- tagNotSpecified: danger
- pullPolicyNotAlways: ignore
- # healthChecks
- readinessProbeMissing: warning
- livenessProbeMissing: warning
- # networking
- hostNetworkSet: warning
- hostPortSet: warning
- # security
- hostIPCSet: danger
- hostPIDSet: danger
- notReadOnlyRootFilesystem: warning
- privilegeEscalationAllowed: danger
- runAsRootAllowed: warning
- runAsPrivileged: danger
- dangerousCapabilities: danger
- insecureCapabilities: warning
- controllersToScan:
- - Deployments
- - StatefulSets
- - DaemonSets
- - CronJobs
- - Jobs
- - ReplicationControllers
- exemptions:
- - controllerNames:
- - dns-controller
- - datadog-datadog
- - kube-flannel-ds
- - kube2iam
- - aws-iam-authenticator
- - datadog
- - kube2iam
- rules:
- - hostNetworkSet
- - controllerNames:
- - aws-iam-authenticator
- - aws-cluster-autoscaler
- - kube-state-metrics
- - dns-controller
- - external-dns
- - dnsmasq
- - autoscaler
- - kubernetes-dashboard
- - install-cni
- - kube2iam
- rules:
- - readinessProbeMissing
- - livenessProbeMissing
- - controllerNames:
- - aws-iam-authenticator
- - nginx-ingress-controller
- - nginx-ingress-default-backend
- - aws-cluster-autoscaler
- - kube-state-metrics
- - dns-controller
- - external-dns
- - kubedns
- - dnsmasq
- - autoscaler
- - tiller
- - kube2iam
- rules:
- - runAsRootAllowed
- - controllerNames:
- - aws-iam-authenticator
- - nginx-ingress-controller
- - nginx-ingress-default-backend
- - aws-cluster-autoscaler
- - kube-state-metrics
- - dns-controller
- - external-dns
- - kubedns
- - dnsmasq
- - autoscaler
- - tiller
- - kube2iam
- rules:
- - notReadOnlyRootFilesystem
- - controllerNames:
- - cert-manager
- - dns-controller
- - kubedns
- - dnsmasq
- - autoscaler
- - insights-agent-goldilocks-vpa-install
- rules:
- - cpuRequestsMissing
- - cpuLimitsMissing
- - memoryRequestsMissing
- - memoryLimitsMissing
- - controllerNames:
- - kube2iam
- - kube-flannel-ds
- rules:
- - runAsPrivileged
- - controllerNames:
- - kube-hunter
- rules:
- - hostPIDSet
- - controllerNames:
- - polaris
- - kube-hunter
- - goldilocks
- - insights-agent-goldilocks-vpa-install
- rules:
- - notReadOnlyRootFilesystem
- - controllerNames:
- - insights-agent-goldilocks-controller
- rules:
- - livenessProbeMissing
- - readinessProbeMissing
- - controllerNames:
- - insights-agent-goldilocks-vpa-install
- - kube-hunter
- rules:
- - runAsRootAllowed
-
---
-# Source: polaris/templates/webhook.rbac.yaml
-apiVersion: v1
-kind: ServiceAccount
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
metadata:
- name: polaris-webhook
- namespace: polaris
+ name: polaris-view
labels:
app: polaris
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: view
+subjects:
+ - kind: ServiceAccount
+ name: polaris
+ namespace: polaris
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
+metadata:
+ name: polaris
+ labels:
+ app: polaris
+rules:
+ # required by controller-runtime code doing a cluster wide lookup
+ # when it seems namespace would suffice
+ - apiGroups:
+ - ''
+ resources:
+ - 'nodes'
+ verbs:
+ - 'get'
+ - 'list'
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
+metadata:
+ name: polaris
+ labels:
+ app: polaris
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: polaris
+subjects:
+ - kind: ServiceAccount
+ name: polaris
+ namespace: polaris
+
+---
+# Source: polaris/templates/webhook.rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRole
metadata:
name: polaris-webhook
labels:
app: polaris
rules:
- # auditor rules rbac
- - apiGroups:
- - 'apps'
- - 'extensions'
- resources:
- - 'deployments'
- - 'statefulsets'
- - 'daemonsets'
- verbs:
- - 'get'
- - 'list'
- - apiGroups:
- - 'batch'
- resources:
- - 'jobs'
- - 'cronjobs'
- verbs:
- - 'get'
- - 'list'
- - apiGroups:
- - ''
- resources:
- - 'nodes'
- - 'namespaces'
- - 'pods'
- - 'replicationcontrollers'
- verbs:
- - 'get'
- - 'list'
# required by controller-runtime code doing a cluster wide lookup
# when it seems namespace would suffice
- apiGroups:
@@ -229,7 +113,7 @@ roleRef:
name: polaris-webhook
subjects:
- kind: ServiceAccount
- name: polaris-webhook
+ name: polaris
namespace: polaris
---
apiVersion: rbac.authorization.k8s.io/v1beta1
@@ -262,7 +146,7 @@ roleRef:
name: polaris-webhook
subjects:
- kind: ServiceAccount
- name: polaris-webhook
+ name: polaris
namespace: polaris
---
# Source: polaris/templates/webhook.service.yaml
@@ -301,8 +185,6 @@ spec:
component: webhook
template:
metadata:
- annotations:
- checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501'
labels:
app: polaris
component: webhook
@@ -311,10 +193,8 @@ spec:
- name: webhook
command:
- polaris
- - --webhook
- - --config
- - /opt/app/config.yaml
- image: 'quay.io/fairwinds/polaris:0.6'
+ - webhook
+ image: 'quay.io/fairwinds/polaris:1'
imagePullPolicy: 'Always'
ports:
- containerPort: 9876
@@ -353,23 +233,16 @@ spec:
drop:
- ALL
volumeMounts:
- - name: config
- mountPath: /opt/app/config.yaml
- subPath: config.yaml
- readOnly: true
- name: secret
mountPath: /opt/cert/
readOnly: true
- name: cr-logs
mountPath: /tmp/
readOnly: false
- serviceAccountName: polaris-webhook
+ serviceAccountName: polaris
nodeSelector:
tolerations:
volumes:
- - name: config
- configMap:
- name: polaris
- name: secret
secret:
secretName: polaris-webhook
@@ -382,10 +255,11 @@ spec:
# Source: polaris/templates/audit.rbac.yaml
---
-# Source: polaris/templates/dashboard.deployment.yaml
+# Source: polaris/templates/configmap.yaml
+
---
-# Source: polaris/templates/dashboard.rbac.yaml
+# Source: polaris/templates/dashboard.deployment.yaml
---
# Source: polaris/templates/dashboard.service.yaml
diff --git a/docs/check-documentation/resources.md b/docs/check-documentation/resources.md
index 9eb2cf26..60e21d70 100644
--- a/docs/check-documentation/resources.md
+++ b/docs/check-documentation/resources.md
@@ -13,17 +13,6 @@ key | default | description
`resources.cpuLimitsMissing` | `danger` | Fails when `resources.limits.cpu` attribute is not configured.
`resources.memoryLimitsMissing` | `danger` | Fails when `resources.limits.memory` attribute is not configured.
-## Range Checks
-
-Polaris can also verify that those values fall within a certain range. These checks are not enabled by default, and as such do not have default values. The `cpuRequestRanges`, `cpuLimitRanges`, `memoryRequestRanges`, and `memoryLimitRanges` all support the following attributes:
-
-key | description
-----|------------
-`warning.below` | Warn when resource is below this value (or not defined)
-`warning.above` | Warn when resource is above this value
-`danger.below` | Error when resource is below this value (or not defined)
-`danger.above` | Error when resource is above this value
-
## Background
Configuring resource requests and limits for containers running in Kubernetes is an important best practice to follow. Setting appropriate resource requests will ensure that all your applications have sufficient compute resources. Setting appropriate resource limits will ensure that your applications do not consume too many resources.
diff --git a/docs/check-documentation/security-capabilities.md b/docs/check-documentation/security-capabilities.md
deleted file mode 100644
index 52fdefa4..00000000
--- a/docs/check-documentation/security-capabilities.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Security Capabilities
-
-Polaris supports a number of checks to ensure pods are running with a limited set of capabilities. Under `security.capabilities`, there are `danger` and `warning` sections indicating the severity of failures for the following checks.
-
-key | default | description
-----|---------|------------
-`security.capabilities.danger.ifAnyAdded` | [`SYS_ADMIN`, `NET_ADMIN`, `ALL`] | Fails when any of the listed capabilities have been added.
-`security.capabilities.danger.ifAnyAddedBeyond` | `nil` | Fails when any capabilities have been added beyond the specified list.
-`security.capabilities.danger.ifAnyNotDropped` | `nil` | Fails when any of the listed capabilities have not been dropped.
-`security.capabilities.warning.ifAnyAdded` | `nil` | Fails when any of the listed capabilities have been added.
-`security.capabilities.warning.ifAnyAddedBeyond` | [`CHOWN`, `DAC_OVERRIDE`, `FSETID`, `FOWNER`, `MKNOD`, `NET_RAW`, `SETGID`, `SETUID`, `SETFCAP`, `SETPCAP`, `NET_BIND_SERVICE`, `SYS_CHROOT`, `KILL`,`AUDIT_WRITE`] | Fails when any capabilities have been added beyond the specified list.
-`security.capabilities.warning.ifAnyNotDropped` | `nil` | Fails when any of the listed capabilities have not been dropped.
-
-## Background
-
-Linux Capabilities allow you to specify privileges for a process at a granular level. The [default list of capabilities](https://github.com/moby/moby/blob/master/oci/defaults.go#L15) included with a container are already fairly minimal, but often can be further restricted.
-
-With Kubernetes configuration, these capabilities can be added or removed by adjusting `securityContext.capabilities`.
-
-## Further Reading
-
-- [Kubernetes Docs: Set capabilities for a Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container)
-- [Linux Programmer's Manual: Capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html)
diff --git a/docs/check-documentation/security.md b/docs/check-documentation/security.md
index c0ba2d02..7b049e34 100644
--- a/docs/check-documentation/security.md
+++ b/docs/check-documentation/security.md
@@ -10,10 +10,8 @@ key | default | description
`security.privilegeEscalationAllowed` | `danger` | Fails when `securityContext.allowPrivilegeEscalation` is true.
`security.runAsRootAllowed` | `danger` | Fails when `securityContext.runAsNonRoot` is not true.
`security.runAsPrivileged` | `danger` | Fails when `securityContext.privileged` is true.
-
-## Security Capabilities
-
-Additional validations are available to ensure pods are running with a limited set of capabilities. More information is available in our [Security Capabilities documentation](security-capabilities.md).
+`security.insecureCapabilities` | `warning` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](/checks/insecureCapabilities.yaml)
+`security.dangerousCapabilities` | `danger` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](/checks/dangerousCapabilities.yaml)
## Background
@@ -25,3 +23,5 @@ Much of this configuration can be found in the `securityContext` attribute for b
- [Kubernetes Docs: Configure a Security Context for a Pod or Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
- [KubeCon 2018 Keynote: Running with Scissors](https://www.youtube.com/watch?v=ltrV-Qmh3oY)
- [Kubernetes Security Book](https://kubernetes-security.info/)
+- [Kubernetes Docs: Set capabilities for a Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container)
+- [Linux Programmer's Manual: Capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html)
diff --git a/docs/usage.md b/docs/usage.md
index 89bca010..ec746376 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -60,6 +60,10 @@ strings like `1000m` and `1G`. You can see an example in [the extended config](/
There are additional examples in the [checks folder](/checks).
### Exemptions
+Sometimes a workload really does need to do things that Polaris considers insecure. For instance,
+many of the `kube-system` workloads need to run as root, or need access to the host network. In these
+cases, we can add **exemptions** to allow the workload to pass Polaris checks.
+
Exemptions can be added two ways: by annotating a controller, or editing the Polaris config.
To exempt a controller from all checks via annotations, use the annotation `polaris.fairwinds.com/exempt=true`, e.g.
diff --git a/img/dashboard-screenshot.png b/img/dashboard-screenshot.png
new file mode 100644
index 00000000..4ddc6e77
Binary files /dev/null and b/img/dashboard-screenshot.png differ
diff --git a/polaris-logo.png b/img/polaris-logo.png
similarity index 100%
rename from polaris-logo.png
rename to img/polaris-logo.png
diff --git a/main.go b/main.go
index 76f160d0..7c8322e9 100644
--- a/main.go
+++ b/main.go
@@ -20,11 +20,9 @@ import (
const (
// Version represents the current release version of Polaris
- Version = "0.6.0"
-
+ Version = "1.0.0"
)
func main() {
cmd.Execute(Version)
}
-
diff --git a/scripts/generate-deployment-files.sh b/scripts/generate-deployment-files.sh
index 252f412d..ab49a130 100755
--- a/scripts/generate-deployment-files.sh
+++ b/scripts/generate-deployment-files.sh
@@ -5,7 +5,6 @@ set -eo pipefail
helm template $CHARTS_DIR/stable/polaris/ \
--name polaris --namespace polaris \
--set templateOnly=true \
- --set config="$(cat ./examples/config.yaml)" \
> deploy/dashboard.yaml
helm template $CHARTS_DIR/stable/polaris/ \
@@ -13,5 +12,4 @@ helm template $CHARTS_DIR/stable/polaris/ \
--set templateOnly=true \
--set webhook.enable=true \
--set dashboard.enable=false \
- --set config="$(cat ./examples/config.yaml)" \
> deploy/webhook.yaml