Skip to content

Commit

Permalink
feat: automate changes from src.rego to template.yaml (open-policy-ag…
Browse files Browse the repository at this point in the history
…ent#166)

Signed-off-by: Ernest Wong <chuwon@microsoft.com>
  • Loading branch information
Ernest Wong committed Feb 5, 2022
1 parent 1d9a419 commit 844b471
Show file tree
Hide file tree
Showing 59 changed files with 1,460 additions and 137 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ on:
pull_request:
branches: [master]
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download prerequisites
run: |
mkdir -p $GITHUB_WORKSPACE/bin
echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
make gomplate
- name: Generate templates
run: |
make generate
git diff --exit-code || (echo "Please run 'make generate' to generate the templates" && exit 1)
build:
needs: generate
runs-on: ${{ matrix.os }}
strategy:
matrix:
Expand All @@ -21,6 +36,7 @@ jobs:
sh test.sh
build_test:
needs: generate
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ KUSTOMIZE_VERSION ?= 3.7.0
GATEKEEPER_VERSION ?= release-3.5
BATS_VERSION ?= 1.3.0
GATOR_VERSION ?= 3.7.0
GOMPLATE_VERSION ?= 3.10.0

integration-bootstrap:
# Download and install kind
Expand Down Expand Up @@ -42,3 +43,17 @@ test-gator-dockerized: __build-gator
.PHONY: build-gator
__build-gator:
docker build --build-arg GATOR_VERSION=$(GATOR_VERSION) -f build/gator/Dockerfile -t gator-container .

.PHONY: gomplate
gomplate:
curl -o ${GITHUB_WORKSPACE}/bin/gomplate -sSL https://github.com/hairyhenderson/gomplate/releases/download/v${GOMPLATE_VERSION}/gomplate_linux-amd64
chmod +x ${GITHUB_WORKSPACE}/bin/gomplate

.PHONY: generate
generate:
@for tmpl in $(shell find src -name 'constraint.tmpl'); do \
src_dir=$$(dirname $${tmpl}); \
lib_dir=library/$${src_dir#src/}; \
echo "Generating $${lib_dir}/template.yaml"; \
gomplate -f $${src_dir}/constraint.tmpl > $${lib_dir}/template.yaml; \
done
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ The gator CLI may be downloaded from the Gatekeeper

If you have a policy you would like to contribute, please submit a pull request.
Each new policy should contain:
* A constraint template with a `description` annotation and the parameter structure, if any, defined in `spec.crd.spec.validation.openAPIV3Schema`
* One or more sample constraints, each with an example of an allowed (`example_allowed.yaml`) and disallowed (`example_disallowed.yaml`) resource.
* The rego source, as `src.rego` and unit tests as `src_test.rego` in the corresponding subdirectory under `src/`
* A constraint template named `src/<policy-name>/constraint.tmpl` with a `description` annotation and the parameter structure, if any, defined in `spec.crd.spec.validation.openAPIV3Schema`. The template is rendered using [gomplate](https://docs.gomplate.ca/).
* One or more sample constraints, each with an example of an allowed (`example_allowed.yaml`) and disallowed (`example_disallowed.yaml`) resource under `library/<policy-name>/samples/<policy-name>`
* `kustomization.yaml` and `suite.yaml` under `library/<policy-name>`
* The rego source, as `src.rego` and unit tests as `src_test.rego` in the corresponding subdirectory under `src/<policy-name>`

### Development

* policy code and tests are maintained in `src/` folder and then manually copied into `library/`
* policy code and tests are maintained in `src/<policy-name>/src.rego` and `src/<policy-name>/src_test.rego`
* `make generate` will generate `library/<policy-name>/template.yaml` from `src/<policy-name>/src.rego` using [gomplate](https://docs.gomplate.ca/).
* run all tests with `./test.sh`
* run single test with `opa test src/<folder>/src.rego src/<folder>/src_test.rego --verbose`
* print results with `trace(sprintf("%v", [thing]))`
18 changes: 9 additions & 9 deletions library/general/automount-serviceaccount-token/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,30 @@ spec:
package k8sautomountserviceaccounttoken
violation[{"msg": msg}] {
obj := input.review.object
mountServiceAccountToken(obj.spec)
msg := sprintf("Automounting service account token is disallowed, pod: %v", [obj.metadata.name])
obj := input.review.object
mountServiceAccountToken(obj.spec)
msg := sprintf("Automounting service account token is disallowed, pod: %v", [obj.metadata.name])
}
mountServiceAccountToken(spec) {
spec.automountServiceAccountToken == true
spec.automountServiceAccountToken == true
}
# if there is no automountServiceAccountToken spec, check on volumeMount in containers. Service Account token is mounted on /var/run/secrets/kubernetes.io/serviceaccount
# https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#serviceaccount-admission-controller
mountServiceAccountToken(spec) {
not has_key(spec, "automountServiceAccountToken")
"/var/run/secrets/kubernetes.io/serviceaccount" == input_containers[_].volumeMounts[_].mountPath
not has_key(spec, "automountServiceAccountToken")
"/var/run/secrets/kubernetes.io/serviceaccount" == input_containers[_].volumeMounts[_].mountPath
}
input_containers[c] {
c := input.review.object.spec.containers[_]
c := input.review.object.spec.containers[_]
}
input_containers[c] {
c := input.review.object.spec.initContainers[_]
c := input.review.object.spec.initContainers[_]
}
has_key(x, k) {
_ = x[k]
_ = x[k]
}
20 changes: 11 additions & 9 deletions library/general/block-endpoint-edit-default-role/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ metadata:
ClusterRole which does not properly restrict access to editing Endpoints.
This ConstraintTemplate forbids the system:aggregate-to-edit ClusterRole
from granting permission to create/patch/update Endpoints.
ClusterRole/system:aggregate-to-edit should not allow
Endpoint edit permissions due to CVE-2021-25740, Endpoint & EndpointSlice
permissions allow cross-Namespace forwarding,
Expand All @@ -24,22 +24,24 @@ spec:
package k8sblockendpointeditdefaultrole
violation[{"msg": msg}] {
input.review.object.metadata.name == "system:aggregate-to-edit"
endpointRule(input.review.object.rules[_])
msg := "ClusterRole system:aggregate-to-edit should not allow endpoint edit permissions. For k8s version < 1.22, the Cluster Role should be annotated with rbac.authorization.kubernetes.io/autoupdate=false to prevent autoreconciliation back to default permissions for this role."
input.review.object.metadata.name == "system:aggregate-to-edit"
endpointRule(input.review.object.rules[_])
msg := "ClusterRole system:aggregate-to-edit should not allow endpoint edit permissions. For k8s version < 1.22, the Cluster Role should be annotated with rbac.authorization.kubernetes.io/autoupdate=false to prevent autoreconciliation back to default permissions for this role."
}
endpointRule(rule) {
"endpoints" == rule.resources[_]
hasEditVerb(rule.verbs)
"endpoints" == rule.resources[_]
hasEditVerb(rule.verbs)
}
hasEditVerb(verbs) {
"create" == verbs[_]
"create" == verbs[_]
}
hasEditVerb(verbs) {
"patch" == verbs[_]
"patch" == verbs[_]
}
hasEditVerb(verbs) {
"update" == verbs[_]
"update" == verbs[_]
}
1 change: 0 additions & 1 deletion library/general/containerresourceratios/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ spec:
The maximum allowed ratio of `resources.limits.cpu` to
`resources.requests.cpu` on a container. If not specified,
equal to `ratio`.
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
Expand Down
2 changes: 1 addition & 1 deletion library/general/disallowanonymous/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ spec:
type: object
properties:
allowedRoles:
description: >-
description: >-
The list of ClusterRoles and Roles that may be associated
with the `system:unauthenticated` group and `system:anonymous`
user.
Expand Down
20 changes: 10 additions & 10 deletions library/general/disallowedtags/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@ spec:
package k8sdisallowedtags
violation[{"msg": msg}] {
container := input_containers[_]
tags := [forbid | tag = input.parameters.tags[_] ; forbid = endswith(container.image, concat(":", ["", tag]))]
any(tags)
msg := sprintf("container <%v> uses a disallowed tag <%v>; disallowed tags are %v", [container.name, container.image, input.parameters.tags])
container := input_containers[_]
tags := [forbid | tag = input.parameters.tags[_] ; forbid = endswith(container.image, concat(":", ["", tag]))]
any(tags)
msg := sprintf("container <%v> uses a disallowed tag <%v>; disallowed tags are %v", [container.name, container.image, input.parameters.tags])
}
violation[{"msg": msg}] {
container := input_containers[_]
tag := [contains(container.image, ":")]
not all(tag)
msg := sprintf("container <%v> didn't specify an image tag <%v>", [container.name, container.image])
container := input_containers[_]
tag := [contains(container.image, ":")]
not all(tag)
msg := sprintf("container <%v> didn't specify an image tag <%v>", [container.name, container.image])
}
input_containers[c] {
c := input.review.object.spec.containers[_]
c := input.review.object.spec.containers[_]
}
input_containers[c] {
c := input.review.object.spec.initContainers[_]
c := input.review.object.spec.initContainers[_]
}
4 changes: 2 additions & 2 deletions library/general/imagedigests/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ spec:
openAPIV3Schema:
type: object
description: >-
Requires container images to contain a digest.
Requires container images to contain a digest.
https://kubernetes.io/docs/concepts/containers/images/
https://kubernetes.io/docs/concepts/containers/images/
properties:
exemptImages:
description: >-
Expand Down
18 changes: 9 additions & 9 deletions library/general/replicalimits/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@ spec:
deployment_name = input.review.object.metadata.name
violation[{"msg": msg}] {
spec := input.review.object.spec
not input_replica_limit(spec)
msg := sprintf("The provided number of replicas is not allowed for deployment: %v. Allowed ranges: %v", [deployment_name, input.parameters])
spec := input.review.object.spec
not input_replica_limit(spec)
msg := sprintf("The provided number of replicas is not allowed for deployment: %v. Allowed ranges: %v", [deployment_name, input.parameters])
}
input_replica_limit(spec) {
provided := input.review.object.spec.replicas
count(input.parameters.ranges) > 0
range := input.parameters.ranges[_]
value_within_range(range, provided)
provided := input.review.object.spec.replicas
count(input.parameters.ranges) > 0
range := input.parameters.ranges[_]
value_within_range(range, provided)
}
value_within_range(range, value) {
range.min_replicas <= value
range.max_replicas >= value
range.min_replicas <= value
range.max_replicas >= value
}
23 changes: 11 additions & 12 deletions library/general/requiredprobes/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,30 @@ spec:
package k8srequiredprobes
probe_type_set = probe_types {
probe_types := {type | type := input.parameters.probeTypes[_]}
probe_types := {type | type := input.parameters.probeTypes[_]}
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
probe := input.parameters.probes[_]
probe_is_missing(container, probe)
msg := get_violation_message(container, input.review, probe)
container := input.review.object.spec.containers[_]
probe := input.parameters.probes[_]
probe_is_missing(container, probe)
msg := get_violation_message(container, input.review, probe)
}
probe_is_missing(ctr, probe) = true {
not ctr[probe]
not ctr[probe]
}
probe_is_missing(ctr, probe) = true {
probe_field_empty(ctr, probe)
probe_field_empty(ctr, probe)
}
probe_field_empty(ctr, probe) = true {
probe_fields := {field | ctr[probe][field]}
diff_fields := probe_type_set - probe_fields
count(diff_fields) == count(probe_type_set)
probe_fields := {field | ctr[probe][field]}
diff_fields := probe_type_set - probe_fields
count(diff_fields) == count(probe_type_set)
}
get_violation_message(container, review, probe) = msg {
msg := sprintf("Container <%v> in your <%v> <%v> has no <%v>", [container.name, review.kind.kind, review.object.metadata.name, probe])
msg := sprintf("Container <%v> in your <%v> <%v> has no <%v>", [container.name, review.kind.kind, review.object.metadata.name, probe])
}
3 changes: 1 addition & 2 deletions library/general/uniqueingresshost/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,5 @@ spec:
re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion)
other.spec.rules[_].host == host
not identical(other, input.review)
msg := sprintf("Ingress host conflicts with an existing Ingress <%v>", [host])
msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host])
}
2 changes: 1 addition & 1 deletion library/general/uniqueserviceselector/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ metadata:
Selectors are considered the same if they have identical keys and values.
Selectors may share a key/value pair so long as there is at least one
distinct key/value pair between them.
https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service
spec:
crd:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ spec:
# has_field returns whether an object has a field
has_field(object, field) = true {
object[field]
}
}
libs:
- |
package lib.exempt_container
Expand Down
40 changes: 20 additions & 20 deletions library/pod-security-policy/fsgroup/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@ spec:
package k8spspfsgroup
violation[{"msg": msg, "details": {}}] {
spec := input.review.object.spec
not input_fsGroup_allowed(spec)
msg := sprintf("The provided pod spec fsGroup is not allowed, pod: %v. Allowed fsGroup: %v", [input.review.object.metadata.name, input.parameters])
spec := input.review.object.spec
not input_fsGroup_allowed(spec)
msg := sprintf("The provided pod spec fsGroup is not allowed, pod: %v. Allowed fsGroup: %v", [input.review.object.metadata.name, input.parameters])
}
input_fsGroup_allowed(spec) {
# RunAsAny - No range is required. Allows any fsGroup ID to be specified.
input.parameters.rule == "RunAsAny"
# RunAsAny - No range is required. Allows any fsGroup ID to be specified.
input.parameters.rule == "RunAsAny"
}
input_fsGroup_allowed(spec) {
# MustRunAs - Validates pod spec fsgroup against all ranges
input.parameters.rule == "MustRunAs"
fg := spec.securityContext.fsGroup
count(input.parameters.ranges) > 0
range := input.parameters.ranges[_]
value_within_range(range, fg)
# MustRunAs - Validates pod spec fsgroup against all ranges
input.parameters.rule == "MustRunAs"
fg := spec.securityContext.fsGroup
count(input.parameters.ranges) > 0
range := input.parameters.ranges[_]
value_within_range(range, fg)
}
input_fsGroup_allowed(spec) {
# MayRunAs - Validates pod spec fsgroup against all ranges or allow pod spec fsgroup to be left unset
Expand All @@ -74,18 +74,18 @@ spec:
not spec.securityContext.fsGroup
}
input_fsGroup_allowed(spec) {
# MayRunAs - Validates pod spec fsgroup against all ranges or allow pod spec fsgroup to be left unset
input.parameters.rule == "MayRunAs"
fg := spec.securityContext.fsGroup
count(input.parameters.ranges) > 0
range := input.parameters.ranges[_]
value_within_range(range, fg)
# MayRunAs - Validates pod spec fsgroup against all ranges or allow pod spec fsgroup to be left unset
input.parameters.rule == "MayRunAs"
fg := spec.securityContext.fsGroup
count(input.parameters.ranges) > 0
range := input.parameters.ranges[_]
value_within_range(range, fg)
}
value_within_range(range, value) {
range.min <= value
range.max >= value
range.min <= value
range.max >= value
}
# has_field returns whether an object has a field
has_field(object, field) = true {
object[field]
object[field]
}
6 changes: 3 additions & 3 deletions library/pod-security-policy/seccomp/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ spec:
}
input_container_allowed(metadata, container) {
profile := get_container_profile(metadata, container)
profile == input.parameters.allowedProfiles[_]
profile := get_container_profile(metadata, container)
profile == input.parameters.allowedProfiles[_]
}
get_container_profile(metadata, container) = profile {
value := metadata.annotations[key]
value := metadata.annotations[key]
startswith(key, "container.seccomp.security.alpha.kubernetes.io/")
[prefix, name] := split(key, "/")
name == container.name
Expand Down
Loading

0 comments on commit 844b471

Please sign in to comment.