Skip to content

Commit

Permalink
Rename constraint to policy
Browse files Browse the repository at this point in the history
  • Loading branch information
swoehrl-mw committed Jun 2, 2022
1 parent 26fb503 commit ae0d88f
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 337 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
TAG=${GITHUB_REF//refs\/tags\/v}
VERSION=${GITHUB_REF//refs\/tags\/v}
sed -i 's/version = "0.1.0"/version = "'${VERSION}'"/' Cargo.toml
docker build . -t $IMAGE_ID:$TAG
docker push $IMAGE_ID:$TAG
Expand Down
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
>
> -- Based loosely on Monty Python and the Holy Grail
Bridgekeeper helps you to enforce policies in your kubernetes cluster by providing a simple declarative way to define constraints using the python programming language. Whenever resources are created or updated matching constraints will be evaluated and if a constraint is violated the resource will be rejected.
Bridgekeeper helps you to enforce policies in your kubernetes cluster by providing a simple declarative way to define policies using the python programming language. Whenever resources are created or updated matching policies will be evaluated and if a policy is violated the resource will be rejected.

From a technical perspective bridgekeeper acts as a kubernetes [admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/). For every admission request that it gets it will check all registered constraints if any match the resource of the request and if yes the constraint will be evaluated and based on the result the admission request will be allowed or rejected.
From a technical perspective bridgekeeper acts as a kubernetes [admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/). For every admission request that it gets it will check all registered policies if any match the resource of the request and if yes the policy will be evaluated and based on the result the admission request will be allowed or rejected.

Bridgekeeper is very similar to and heavily inspired by [OPA gatekeeper](https://github.com/open-policy-agent/gatekeeper). It was born out of the idea to make gatekeeper simpler and use python instead of rego (because we love python) as the policy language.

**This service is work-in-progress and should not yet be used in production setups. Use at your own risk.**
Bridgekeeper is similar to and inspired by [OPA gatekeeper](https://github.com/open-policy-agent/gatekeeper). It was born out of the idea to make gatekeeper simpler and use python instead of rego (because we love python) as the policy language.

## User Guide

Expand Down Expand Up @@ -56,15 +54,15 @@ bridgekeeper:
interval: 600
```
### Writing constraints
### Writing policies
Bridgekeeper uses a custom resource called Constraint to manage policies. A constraint consists of a target that describes what kubernetes resources are to be validated by the constraint and a rule script written in python.
Bridgekeeper uses a custom resource called Policy to manage rules. A policy consists of a target that describes what kubernetes resources are to be validated by the policy and a rule script written in python.
Below is a minimal example:
```yaml
apiVersion: bridgekeeper.maibornwolff.de/v1alpha1
kind: Constraint
kind: Policy
metadata:
name: foobar
spec:
Expand All @@ -78,10 +76,10 @@ spec:
return False, "You don't want to deploy anything today"
```
The constraint spec has the following fields:
The policy spec has the following fields:
* `audit`: If set to true and bridgekeeper is started with the audit feature enabled this constraint will be checked during audit runs (see also next section).
* `enforce`: If set to false a constraint violation will be logged but the request will still be allowed. Defaults to true. Can be used to safely test constraints.
* `audit`: If set to true and bridgekeeper is started with the audit feature enabled this policy will be checked during audit runs (see also next section).
* `enforce`: If set to false a policy violation will be logged but the request will still be allowed. Defaults to true. Can be used to safely test policies.
* `target.matches`: A list of one or more match parameters consisting of `apiGroup` and `kind`. Wildcards can be used as `"*"`, if the resource has no API group (e.g. namespaces) use an empty string `""`.
* `target.namespaces`: An optional list of strings, if specified only resources from one of these namespaces are matched, only this or `target.excludedNamespaces` can be specified, not both
* `target.excludedNamespaces`: An optional list of strings, if specified resources from one of these namespaces are not matched, only this or `target.namespaces` can be specified, not both
Expand All @@ -99,13 +97,13 @@ If the code returns a mutated object bridekeeper will calculate the diff between

You can use the entire python feature set and standard library in your script (so stuff like `import re` is possible). Using threads, accessing the filesystem or using the network (e.g. via sockets) should not be done and might be prohibited in the future.

You can find a more useful example under [example/constraint.yaml](example/constraint.yaml) that denies deployments that use a docker image with a `latest` tag. Try it out using the following steps:
You can find a more useful example under [example/policy.yaml](example/policy.yaml) that denies deployments that use a docker image with a `latest` tag. Try it out using the following steps:

1. `kubectl apply -f example/constraint.yaml` to create the constraint
1. `kubectl apply -f example/policy.yaml` to create the policy
2. `kubectl apply -f example/deployment-error.yaml` will yield an error because the docker image is referenced using the `latest` tag
3. `kubectl apply -f example/deployment-ok.yaml` will be accepted because the docker image tag uses a specific version

Constraints are not namespaced and apply to the entire cluster unless `target.namespaces` or `target.excludedNamespaces` are used to filter namespaces. To completely exclude a namespace from bridgekeeper label it with `bridgekeeper/ignore`. If you use the helm chart to install bridgekeeper you can set the option `bridgekeeper.ignoreNamespaces` to a list of namespaces that should be labled and it will be done during initial install (by default it will label the `kube-*` namespaces).
Policies are not namespaced and apply to the entire cluster unless `target.namespaces` or `target.excludedNamespaces` are used to filter namespaces. To completely exclude a namespace from bridgekeeper label it with `bridgekeeper/ignore`. If you use the helm chart to install bridgekeeper you can set the option `bridgekeeper.ignoreNamespaces` to a list of namespaces that should be labled and it will be done during initial install (by default it will label the `kube-*` namespaces).

### Mutations

Expand All @@ -115,17 +113,19 @@ Mutations are implemented very lightweight in bridgekeeper. Instead of generatin

To mutate the object the validate function must return a tuple of boolean, string, object/dict. If the object is rejected the mutations are ignored. If you do not want to return a reason just return `None`.

An example that adds a label to each deployment can be found under [example/mutate-add-label.yaml](example/mutate-add-label.yaml)

### Auditing

Bridgekeeper has an audit feature that periodically checks if any existing objects violate constraints. This is useful to check objects that were created before the constraint was installed.
Bridgekeeper has an audit feature that periodically checks if any existing objects violate policies. This is useful to check objects that were created before the policy was installed.

To enable the audit feature launch bridgekeeper with the `--audit` flag. The audit interval is by default 10 minutes and can be changed with `--audit-interval <seconds>`. If installed using helm audit can be enabled by setting `bridgekeeper.audit.enable` to `true`.
To enable the audit feature launch bridgekeeper with the `--audit` flag. The audit interval is by default to 10 minutes and can be changed with `--audit-interval <seconds>`. If installed using helm audit can be enabled by setting `bridgekeeper.audit.enable` to `true` and the interval can be set with `bridgekeeper.audit.interval`.

To include a constraint in the audit run set the `spec.audit` field to `yes`. The namespace include/exclude lists of the constraints and the `bridgekeeper/ignore` label on namespaces is honored during audit runs. The results of the run will be stored in the status of the constraint with a list of objects that violate the constraint and the provided reason if any.
To include a policy in the audit run set the `spec.audit` field to `true`. The namespace include/exclude lists of the policies and the `bridgekeeper/ignore` label on namespaces are honored during audit runs. The results of the run will be stored in the status of the policy with a list of objects that violate the policy and the provided reason if any.

A single audit run can also be launched locally: `bridgekeeper audit`. This will connect to the kubernetes cluster defined by the currently active kubernetes context (as kubectl would use it), read in all existing constraints and perform an audit run. All objects that violate a constraint are printed on the console as output, this can be disabled by providing `--silent`. By adding the `--status` flag the constraint status will also be updated with the violations.
A single audit run can also be launched locally: `bridgekeeper audit`. This will connect to the kubernetes cluster defined by the currently active kubernetes context (as kubectl would use it), read in all existing policies and perform an audit run. All objects that violate a policy are printed on the console as output, this can be disabled by providing `--silent`. By adding the `--status` flag the policy status will also be updated with the violations.

Note: Currently the audit feature only works correctly if you launch bridgekeeper as a single instance (in helm `replicaCount: 1`).
Note: Currently the audit feature only works correctly if you launch bridgekeeper as a single instance (in helm set `replicaCount: 1`).

Any mutations returned by the rules are ignored in audit mode.

Expand All @@ -152,8 +152,8 @@ After you are finished, run `cargo run -- cleanup --local` to delete the webook.

### Development cycle

As long as you do not change the schema of the constraints you can just recompile and restart the server without having to reinstall any of the other stuff (certificate, webhook, constraints).
If you change the schema of the CRD (via `src/crd.rs`) you need to regenerate the CRD yaml by running `cargo run -- gencrd`.
As long as you do not change the schema of the policies you can just recompile and restart the server without having to reinstall any of the other stuff (certificate, webhook, policies).
If you change the schema of the CRD (via `src/crd.rs`) you need to regenerate the CRD yaml in the helm chart by running `cargo run -- gencrd`.

### Test cluster deployment

Expand Down
12 changes: 6 additions & 6 deletions charts/bridgekeeper/templates/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: constraints.bridgekeeper.maibornwolff.de
name: policies.bridgekeeper.maibornwolff.de
spec:
group: bridgekeeper.maibornwolff.de
names:
categories: []
kind: Constraint
plural: constraints
kind: Policy
plural: policies
shortNames: []
singular: constraint
singular: policy
scope: Cluster
versions:
- additionalPrinterColumns: []
name: v1alpha1
schema:
openAPIV3Schema:
description: "Auto-generated derived type for ConstraintSpec via `CustomResource`"
description: "Auto-generated derived type for PolicySpec via `CustomResource`"
properties:
spec:
properties:
Expand Down Expand Up @@ -92,7 +92,7 @@ spec:
type: object
required:
- spec
title: Constraint
title: Policy
type: object
served: true
storage: true
Expand Down
6 changes: 3 additions & 3 deletions charts/bridgekeeper/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ rules:
verbs:
- patch
- update
# During audit runs bridgekeeper needs to update the constraint status with the audit results
# During audit runs bridgekeeper needs to update the policy status with the audit results
- apiGroups:
- "bridgekeeper.maibornwolff.de"
resources:
- constraints
- constraints/status
- policies
- policies/status
verbs:
- patch
- update
Expand Down
2 changes: 1 addition & 1 deletion example/mutate-add-label.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
apiVersion: bridgekeeper.maibornwolff.de/v1alpha1
kind: Constraint
kind: Policy
metadata:
name: deployment-add-label
spec:
Expand Down
2 changes: 1 addition & 1 deletion example/constraint.yaml → example/policy.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
apiVersion: bridgekeeper.maibornwolff.de/v1alpha1
kind: Constraint
kind: Policy
metadata:
name: no-latest-tag
spec:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: bridgekeeper-constraint-validation
name: bridgekeeper-policy-validation
webhooks:
- name: constraint-validation.bridgekeeper.k8s
- name: policy-validation.bridgekeeper.k8s
namespaceSelector:
matchExpressions:
- key: bridgekeeper/ignore
operator: DoesNotExist
clientConfig:
url: "https://<host>/validate-constraint"
url: "https://<host>/validate-policy"
caBundle: "<cadata>"
matchPolicy: Equivalent
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["bridgekeeper.maibornwolff.de"]
apiVersions: ["*"]
resources: ["constraints"]
resources: ["policies"]
failurePolicy: <failure_policy>
admissionReviewVersions: ["v1"]
sideEffects: None
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: bridgekeeper-constraint-validation
name: bridgekeeper-policy-validation
webhooks:
- name: constraint-validation.bridgekeeper.k8s
- name: policy-validation.bridgekeeper.k8s
namespaceSelector:
matchExpressions:
- key: bridgekeeper/ignore
Expand All @@ -12,14 +12,14 @@ webhooks:
service:
name: bridgekeeper-webhook
namespace: "<namespace>"
path: "/validate-constraint"
path: "/validate-policy"
caBundle: "<cadata>"
matchPolicy: Equivalent
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["bridgekeeper.maibornwolff.de"]
apiVersions: ["*"]
resources: ["constraints"]
resources: ["policies"]
failurePolicy: <failure_policy>
admissionReviewVersions: ["v1"]
sideEffects: None
Expand Down
24 changes: 12 additions & 12 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::crd::Constraint;
use crate::evaluator::{ConstraintEvaluatorRef, EvaluationResult};
use crate::crd::Policy;
use crate::evaluator::{PolicyEvaluatorRef, EvaluationResult};
use crate::util::cert::CertKeyPair;
use kube::{
api::DynamicObject,
Expand Down Expand Up @@ -38,7 +38,7 @@ async fn health() -> &'static str {
#[rocket::post("/mutate", data = "<data>")]
async fn admission_mutate(
data: Json<AdmissionReview<DynamicObject>>,
evaluator: &State<ConstraintEvaluatorRef>,
evaluator: &State<PolicyEvaluatorRef>,
) -> Result<Json<AdmissionReview<DynamicObject>>, ApiError> {
HTTP_REQUEST_COUNTER.with_label_values(&["/mutate"]).inc();
let admission_review = data.0;
Expand All @@ -54,7 +54,7 @@ async fn admission_mutate(
reason,
warnings,
patch,
} = evaluator.evaluate_constraints(admission_request);
} = evaluator.evaluate_policies(admission_request);
response.allowed = allowed;
if !warnings.is_empty() {
response.warnings = Some(warnings);
Expand All @@ -76,13 +76,13 @@ async fn admission_mutate(
Ok(Json(review))
}

#[rocket::post("/validate-constraint", data = "<data>")]
async fn validate_constraint(
data: Json<AdmissionReview<Constraint>>,
evaluator: &State<ConstraintEvaluatorRef>,
#[rocket::post("/validate-policy", data = "<data>")]
async fn validate_policy(
data: Json<AdmissionReview<Policy>>,
evaluator: &State<PolicyEvaluatorRef>,
) -> Result<Json<AdmissionReview<DynamicObject>>, ApiError> {
HTTP_REQUEST_COUNTER
.with_label_values(&["/validate_constraint"])
.with_label_values(&["/validate-policy"])
.inc();
let admission_review = data.0;
let admission_request = admission_review.try_into().map_err(|err| {
Expand All @@ -92,7 +92,7 @@ async fn validate_constraint(

let evaluator = evaluator.lock().expect("lock failed. Cannot continue");

let (allowed, reason) = evaluator.validate_constraint(&admission_request);
let (allowed, reason) = evaluator.validate_policy(&admission_request);
response.allowed = allowed;
if !allowed {
response.result.message = reason;
Expand Down Expand Up @@ -127,7 +127,7 @@ async fn metrics() -> Result<(ContentType, String), ApiError> {
))
}

pub async fn server(cert: CertKeyPair, evaluator: ConstraintEvaluatorRef) {
pub async fn server(cert: CertKeyPair, evaluator: PolicyEvaluatorRef) {
let config = Config {
address: "0.0.0.0".parse().unwrap(),
port: 8081,
Expand All @@ -143,7 +143,7 @@ pub async fn server(cert: CertKeyPair, evaluator: ConstraintEvaluatorRef) {
.manage(evaluator)
.mount(
"/",
rocket::routes![admission_mutate, validate_constraint, health, metrics],
rocket::routes![admission_mutate, validate_policy, health, metrics],
)
.launch()
.await
Expand Down
Loading

0 comments on commit ae0d88f

Please sign in to comment.