Gatekeeper Policy Authoring
Ratify can be deployed behind admission controllers such as Gatekeeper. When deployed in this manner Ratify will provide the results of all verifiers back to Gatekeeper so that policies can be authored in rego.
Rego policies utilizing external data are authored in exactly the same way as traditional rego policies, but need to know the structure of the response from the external data provider in order to take advantage of it.
Rego References
These are some helpful references for working with rego:
- Rego Policy Language
- Rego Policy Reference
- Rego Built-in Functions
- Gatekeeper External Data
- Gatekeeper Policy Debugging
- Rego Playground
The rego playground is a great way to test out policies against responses from Ratify. Use the steps from the Gatekeeper debugging reference to print the response from Ratify. This can then be used as the input for rego playground. In rego playground you can replace the external_data call with the input:
package ratify
# You would use the below when running the rego using external_data
remote_data := response {
images := [img | img = input.review.object.spec.containers[_].image]
response := external_data({"provider": "ratify-provider", "keys": images})
}
# Use this to mimic the behavior of external_data within rego playground
remote_data := response {
response := input
}
Ratify Response in Rego
Let's assume we have a registry with the following image and associated artifacts:
localhost:5000/net-monitor:v1
- sbom
- notation signature
- cosign signature
We will try to deploy the following pod spec:
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: net-monitor
image: localhost:5000/net-monitor:v1
- name: missing-image
image: localhost:5000/missing-image:v1
Note that one of the images listed in the pod spec does not actually exist in the registry. This is to give an example of something that might generate an error message in one of the response fields.
Given the following rego snippet:
package ratify
# Get data from Ratify
remote_data := response {
images := [img | img = input.review.object.spec.containers[_].image]
response := external_data({"provider": "ratify-provider", "keys": images})
}
The remote_data
document contents would look something like this (NOTE: this
is a sample response to explain the structure of the response data and the
actual values of fields may differ):
{
"errors": [
["localhost:5000/missing-image:v1", "The subject did not exist"]
],
"responses": [
[
"localhost:5000/net-monitor:v1",
{
"isSuccess": true,
"verifierReports": [
{
"artifactType": "application/x.example.sbom.v0",
"isSuccess": true,
"message": "SBOM verification success. The contents is good.",
"name": "sbom",
"subject": "localhost:5000/net-monitor:v1"
},
{
"artifactType": "application/vnd.cncf.notary.signature",
"extensions": {
"Issuer": "CN=localhost:5000,O=Ratify,L=Seattle,ST=Washington,C=US",
"SN": "CN=localhost:5000,O=Ratify,L=Seattle,ST=Washington,C=US"
},
"isSuccess": true,
"message": "signature verification success",
"name": "notation",
"subject": "localhost:5000/net-monitor:v1"
},
{
"artifactType": "application/vnd.dev.cosign.artifact.sig.v1+json",
"isSuccess": true,
"message": "cosign verification success. valid signatures found",
"name": "cosign",
"subject": "localhost:5000/net-monitor:v1"
}
]
}
]
],
"status_code": 200,
"system_error": ""
}
Most of the response structure is dictated by the Gatekeeper external data ProviderResponse interface. This structure includes:
errors
: An array of 0-N elements where N is typically the number of subjects from the external data request. This array contains nested arrays where each nested array contains the subject as the first element and an error message as the second element.responses
: An array of 0-N elements where N is typically the number of subjects from the external data request. This array contains nested arrays where each nested array contains the subject as the first element and a value as the second element. The type of the value is dictated by the external data provider.status_code
: The HTTP status code returned from the request to the external data provider.system_error
: A system level error that can either be returned by the external data provider or result from some communication issue with the external data provider.
An array of each
VerifyResult
for a subject is included in the response
field. This allows the policy to be entirely
authored in the rego using all the results from the individual verifiers.
Each verifier produces a VerifierResult
with the following potential fields:
type VerifierResult struct {
Subject string `json:"subject,omitempty"`
IsSuccess bool `json:"isSuccess"`
Name string `json:"name,omitempty"`
Message string `json:"message,omitempty"`
Extensions interface{} `json:"extensions,omitempty"`
NestedResults []VerifierResult `json:"nestedResults,omitempty"`
ArtifactType string `json:"artifactType,omitempty"`
}
The most commonly used fields are:
Name
: The name of the verifierIsSuccess
: The result of the verifierMessage
: A message about the success or failure of the verifierExtensions
: Additional metadata added to the result by the verifier
The Extensions
field allows the verifiers to include additional metadata as
part of the response to use for policy evaluation (for example, the notation
verifier includes the Issuer and SubjectName from the certificate as an extension field).
Currently, the best way to determine the extension fields for a verifier is to
check the source for that verifier.
When Ratify is verifying against a notation signature, there might be ambiguity in the verifierReport from notation verifier. Notation verifier introduces a trust policy component in the rc.1 release. Users could control the verification level and scopes applied to specified artifact. There could be 2 scenarios needed to pay attention:
- If users forgot to set up scopes for some repositories/artifacts, then the verification result from notation verifier will be an error saying the policy is missing.
- If users set the verification level in trust policy to any value except
strict
, then the notation verification result might be a success even though some underlying validation failed. Check the verification level provided in notation for more information: Signature Verification Level
Sample Rego Policy
Let's look at a Ratify policy from the library in more detail. Below is the
rego from the notationissuervalidation
policy:
package notationissuervalidation
# Get data from Ratify
remote_data := response {
images := [img | img = input.review.object.spec.containers[_].image]
response := external_data({"provider": "ratify-provider", "keys": images})
}
# Base Gatekeeper violation
violation[{"msg": msg}] {
general_violation[{"result": msg}]
}
# Check if there are any system errors
general_violation[{"result": result}] {
err := remote_data.system_error
err != ""
result := sprintf("System error calling external data provider: %s", [err])
}
# Check if there are errors for any of the images
general_violation[{"result": result}] {
count(remote_data.errors) > 0
result := sprintf("Error validating one or more images: %s", remote_data.errors)
}
# Check if the success criteria is true
general_violation[{"result": result}] {
subject_validation := remote_data.responses[_]
subject_validation[1].isSuccess == false
result := sprintf("Subject failed verification: %s", [subject_validation[0]])
}
# Check that signature result for Issuer exists
general_violation[{"result": result}] {
subject_results := remote_data.responses[_]
subject_result := subject_results[1]
notation_results := [res | subject_result.verifierReports[i].name == "notation"; res := subject_result.verifierReports[i]]
issuer_results := [res | notation_results[i].extensions.Issuer == input.parameters.issuer; res := notation_results[i]]
count(issuer_results) == 0
result := sprintf("Subject %s has no signatures for certificate with Issuer: %s", [subject_results[0], input.parameters.issuer])
}
# Check for valid signature
general_violation[{"result": result}] {
subject_results := remote_data.responses[_]
subject_result := subject_results[1]
notation_results := [res | subject_result.verifierReports[i].name == "notation"; res := subject_result.verifierReports[i]]
notation_result := notation_results[_]
notation_result.isSuccess == false
notation_result.extensions.Issuer == input.parameters.issuer
result = sprintf("Subject %s failed signature validation: %s", [subject_results[0], notation_result.message])
}
There are a handful of rules defined here that serve different purposes.
The first rule creates a document called remote_data
by making a call to the
Ratify external data provider. This document is then utilized in other rules to
evaluate the results.
The violation
rule is a Gatekeeper construct and is used by Gatekeeper to
determine rule violations. If a message is not present after evaluating the rego
the admission request will pass. From this rule we call all the different types
of general_violations
that we have defined.
Finally, there are a collection of general_violation
rules. Since these rules
all share the same name and signature they are iteratively building a set of
messages that will be bubbled up to the main violation
rule. Each of them is
checking for different criteria that would indicate a violation.
The first general_violation
is satisfied when there is a system_error
field
under the remote_data
document and when the field is not an empty string. When
the body of the rule is satisfied it will generate a result defined by the
sprintf
statement. This result is then appended to the msg
in the
violation
rule and will be returned to the user.
Each subsequent general_violation
is checking for different criteria that
would indicate a violation using standard rego.
When building custom policies the top four rules can be reused as they are used
to handle data fetching and general error cases. Custom general_violation
rules can then be added to handle specific violations.