Configure approver-policy using CertificateRequestPolicy resources

Learn how to configure the approver-policy-enterprise component of TLS Protect for Kubernetes



The default cert-manager installation includes an approve-all policy approver. This approver adds an Approved condition to all the CertificateRequests that use the built-in cert-manager issuers.

The open-source cert-manager approver-policy project provides an replacement for this approve-all approver. It supports configuring simple policy rules using CertificateRequestPolicy resources.

The approver-policy-enterprise approver is our enterprise version of the open-source approver and includes two additional approver plugins. The Venafi plugin can be used to make policy decisions based on the policies defined in your Venafi Control Plane. The Rego plugin adds support for writing custom complex rules using the Rego language.

CertificateRequestPolicy resources

Multiple CertificateRequestPolicy resources can be applicable for a single CertificateRequest. If the CertificateRequest does not conform to at least one of these applicable policies, its approve condition is set to Denied and no certificate is issued based on the request. If the CertificateRequest conforms to all matching policies, the approve condition is set to Approved and issuance can continue. However, if there are no policies that match the CertificateRequest, the approver will not set any approve condition. This way, another approver can still set the approve condition. This way, there can be multiple concurrent approvers, each approving only the CertificateRequest that it is responsible for.

A CertificateRequestPolicy has four sections; allowed, constraints, selector, and plugins. You can read more about these in the Configuration section of the approver-policy Documentation.

Here is a basic CertificateRequestPolicy:

apiVersion: policy.cert-manager.io/v1alpha1
kind: CertificateRequestPolicy
metadata:
name: my-first-policy
spec:
allowed:
# The commonName field MUST be present and MUST have this value.
commonName:
value: "hello.world"
required: true
# The dnsNames field is optional, but if present the values MUST all match one of these.
dnsNames:
values: ["*.hello.world", "hello.world"]
required: false
constraints:
# The privateKey must be an RSA key of at least 4096 bits.
privateKey:
algorithm: RSA
minSize: 4096
selector:
# Only applies to a CertificateRequest which has the following issuerRef
issuerRef:
name: "my-issuer"
kind: "Issuer"
group: "cert-manager.io"
Copy to clipboard

This policy states that CertificateRequest resources that use the my-issuer Issuer:

  • MUST have commonName: hello.world.
  • MAY also include the domainNames field and if present the values must be hello.world (or a sub-domain of that).
  • MUST use an RSA private key with size at least 4096 bit.

Binding the CertificateRequestPolicy to CertificateRequest Resources

CertificateRequestPolicy resources are only used for the CertificateRequest resources that match its CertificateRequestPolicy.Selector section and that are bound via RBAC bindings.

Configuring CertificateRequestPolicy's Selector section

The CertificateRequestPolicy.Selector section can be set to {}, or contain a IssuerRef field and/ or a Namespace field.

A CertificateRequestPolicy will only be used to approver/ deny a CertificateRequest if both of these fields match the request's configuration, following the rules described here. Furthermore, the CertificateRequest has to be bound to the CertificateRequestPolicy via RBAC bindings.

Creating RBAC Bindings

Every CertificateRequest contains the following immutable identity fields:

  • username: the name of the user that created the CertificateRequest (a user account or a ServiceAccount).
  • groups: the group membership of the user at the moment when it was created.

These identity fields are managed by cert-manager and cannot be set by a user or ServiceAccount. Here's an example of a CertificateRequest which was created by the kubernetes-admin user:

apiGroup: cert-manager.io/v1
Kind: CertificateRequest
metadata:
name: my-request
namespace: default
spec:
...
username: kubernetes-admin
groups:
- system:masters
- system:authenticated
Copy to clipboard

And here's an example of a CertificateRequest which was created by cert-manager for a Certificate resource; notice that the username is that of cert-manager ServiceAccount:

apiVersion: cert-manager.io/v1
kind: CertificateRequest
metadata:
name: my-request-q9nw4
namespace: default
spec:
...
username: system:serviceaccount:cert-manager:cert-manager
groups:
- system:serviceaccounts
- system:serviceaccounts:cert-manager
- system:authenticated
uid: bae0a607-0ef3-496d-bd57-99b5cae28188
...
Copy to clipboard

We use Role and RoleBinding resources, and their Cluster equivalents (ClusterRoles, ClusterRoleBindings) to bind policies to users and ServiceAccounts.

All policy Roles and ClusterRoles share the same fields except for the name of the CertificateRequestPolicy the role is targeting, namely:

# These fields do not change.
- apiGroups: ["policy.cert-manager.io"]
resources: ["certificaterequestpolicies"]
verbs: ["use"]
Copy to clipboard

The names of the CertificateRequestPolicy resources for the Role are defined in the resourceNames field.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cert-manager-policy:my-first-policy
rules:
- apiGroups: ["policy.cert-manager.io"]
resources: ["certificaterequestpolicies"]
verbs: ["use"]
resourceNames: ["my-first-policy", "add-more-policies-if-you-like"]
Copy to clipboard

In the following example, we are binding the policy to the system:authenticated group, meaning the request will match all users or ServiceAccounts who create CertificateRequests.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cert-manager-policy:my-fist-policy
rules:
- apiGroups: ["policy.cert-manager.io"]
resources: ["certificaterequestpolicies"]
verbs: ["use"]
# Name of the CertificateRequestPolicies to be used.
resourceNames: ["my-first-policy"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cert-manager-policy:my-first-policy
roleRef:
# ClusterRole or Role _must_ be bound to a user for the policy to be considered.
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cert-manager-policy:my-first-policy
subjects:
# The users who should be bound to the policies defined.
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
Copy to clipboard

In the case of users creating Certificates, the actor who is making the actual CertificateRequest resources is the cert-manager ServiceAccount. For these cases we need to bind to the ServiceAccount instead:

- kind: ServiceAcount
name: cert-manager
namspace: jetstack-secure
Copy to clipboard

ℹ️ Change the namespace value if cert-manager has been installed in a different namespace. e.g. cert-manager or openshift-operators.

Testing Bindings

We can check whether a user, group, or ServiceAccount is able use a CertificateRequestPolicy by using the following kubectl command. We recommend keeping this command handy to check your policy is setup as you would expect as you go.

kubectl auth can-i -n $NAMESPACE --as $USER use certificaterequestpolicies/$POLICY_NAME
Copy to clipboard

In our case, we would expect this command to always return OK since we bound the policy to the cluster scope to all authenticated identities.

kubectl auth can-i -n sandbox --as alice use certificaterequestpolicies/my-first-policy
Copy to clipboard

No Binding vs Denied CertificateRequests

If a CertificateRequest does not match any CertificateRequestPolicy resource, its approved condition will remain unset. This is different from a situation in which one or more CertificateRequestPolicy resources are applicable for a CertificateRequest but one of them denies the request. In the second case an approval condition will be added, but its status will be Denied.

Creating Certificates

Now we have a basic policy in place, we can observe certificates being approved and denied. To test this, we are going to be creating CertificateRequest resources using local Certificate template files via the cmctl CLI tool.

# cert-deny.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: denied-certificate
spec:
commonName: world.hello
dnsNames:
- "world.hello"
- "example.world.hello"
privateKey:
algorithm: RSA
size: 2048
issuerRef:
name: my-issuer
kind: Issuer
group: cert-manager.io
Copy to clipboard
cmctl create certificaterequest denied-certificate --from-certificate-file cert-deny.yaml
Copy to clipboard

Describing the created CertificateRequest should present that it has been Denied, and the reason why it was.

kubectl describe certificaterequest denied-certificate
Copy to clipboard
...
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Denied 14s policy.cert-manager.io No policy approved this request: [my-first-policy: [spec.allowed.commonName.value: Invalid value: "world.hello": hello.world, spec.allowed.dnsNames.values: Invalid value: []string{"world.hello", "example.world.hello"}: *.hello.world, hello.world]]
Copy to clipboard

Now let's try a CertificateRequest which passes the policy created earlier:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: approved-certificate
spec:
commonName: hello.world
dnsNames:
- "hello.world"
- "example.hello.world"
privateKey:
algorithm: RSA
size: 4096
issuerRef:
name: my-issuer
kind: Issuer
group: cert-manager.io
Copy to clipboard
cmctl create certificaterequest approved-certificate --from-certificate-file cert-approve.yaml
Copy to clipboard
kubectl describe certificaterequest approved-certificate
Copy to clipboard
...
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Approved 5s (x13 over 26s) policy.cert-manager.io Approved by CertificateRequestPolicy: "my-first-policy"
Copy to clipboard

On this page