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/v1alpha1kind: CertificateRequestPolicymetadata:name: my-first-policyspec: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: falseconstraints:# The privateKey must be an RSA key of at least 4096 bits.privateKey:algorithm: RSAminSize: 4096selector:# Only applies to a CertificateRequest which has the following issuerRefissuerRef:name: "my-issuer"kind: "Issuer"group: "cert-manager.io"
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/v1Kind: CertificateRequestmetadata:name: my-requestnamespace: defaultspec:...username: kubernetes-admingroups:- system:masters- system:authenticated
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/v1kind: CertificateRequestmetadata:name: my-request-q9nw4namespace: defaultspec:...username: system:serviceaccount:cert-manager:cert-managergroups:- system:serviceaccounts- system:serviceaccounts:cert-manager- system:authenticateduid: bae0a607-0ef3-496d-bd57-99b5cae28188...
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"]
The names of the CertificateRequestPolicy resources for the Role are defined in the resourceNames field.
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:name: cert-manager-policy:my-first-policyrules:- apiGroups: ["policy.cert-manager.io"]resources: ["certificaterequestpolicies"]verbs: ["use"]resourceNames: ["my-first-policy", "add-more-policies-if-you-like"]
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/v1kind: ClusterRolemetadata:name: cert-manager-policy:my-fist-policyrules:- 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/v1kind: ClusterRoleBindingmetadata:name: cert-manager-policy:my-first-policyroleRef:# ClusterRole or Role _must_ be bound to a user for the policy to be considered.apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: cert-manager-policy:my-first-policysubjects:# The users who should be bound to the policies defined.- kind: Groupname: system:authenticatedapiGroup: rbac.authorization.k8s.io
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: ServiceAcountname: cert-managernamspace: jetstack-secure
ℹ️ 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
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
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.yamlapiVersion: cert-manager.io/v1kind: Certificatemetadata:name: denied-certificatespec:commonName: world.hellodnsNames:- "world.hello"- "example.world.hello"privateKey:algorithm: RSAsize: 2048issuerRef:name: my-issuerkind: Issuergroup: cert-manager.io
cmctl create certificaterequest denied-certificate --from-certificate-file cert-deny.yaml
Describing the created CertificateRequest should present that it has been Denied, and the reason why it was.
kubectl describe certificaterequest denied-certificate
...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]]
Now let's try a CertificateRequest which passes the policy created earlier:
apiVersion: cert-manager.io/v1kind: Certificatemetadata:name: approved-certificatespec:commonName: hello.worlddnsNames:- "hello.world"- "example.hello.world"privateKey:algorithm: RSAsize: 4096issuerRef:name: my-issuerkind: Issuergroup: cert-manager.io
cmctl create certificaterequest approved-certificate --from-certificate-file cert-approve.yaml
kubectl describe certificaterequest approved-certificate
...Type Reason Age From Message---- ------ ---- ---- -------Normal Approved 5s (x13 over 26s) policy.cert-manager.io Approved by CertificateRequestPolicy: "my-first-policy"