design/20250920.certificate-renewal-control.md
Author(s):
Status: Draft Date: 2025-09-20
Add a small, backward-compatible extension to the Certificate API that
allows users to:
The goal is to give cluster operators and application owners better
operational control over when certificate renewals happen (to avoid
renewal during business hours, maintenance windows, or restricted
network availability), while making the behavior explicit and
discoverable in status and safe-by-default.
Current cert-manager behavior: certificates are renewed automatically
based on duration and renewBefore. There are valid real-world
situations where users want to control when renewal attempts are
performed:
Providing a first-class API for these requirements improves transparency and reduces reliance on out-of-band tooling (cronjobs, custom controllers) to gate renewals.
Certificate that is easy to
reason about.status.conditions show why a renewal is
deferred or disabled.Certificate CRD renewalPolicy field with examples of windows
and recommended guidelines on how to configure windows.RenewalPolicy CRD
so that the policies can be shared across certificates.Add a new renewal block to CertificateSpec with two child fields:
policy and windows.
spec:
renewal:
# Type of policy to use for renewal.
# Default: RenewBefore
policy: RenewBefore # RenewBefore | EarliestWindow | Disabled
# Optional. If provided, renewal may only happen during one of the listed windows.
# If empty or omitted, renewals may occur at any time.
windows:
- cron: ["0 23 * * 1-5"] # Window is 11 pm - 5 am from Monday - Friday
duration: "6h"
timeZone: "America/Denver"
- cron: ["0 10 * * 6,0"] # Window is 10 am - 6 pm on Sat and Sunday
duration: "8h"
timeZone: "America/Denver"
renewal.policy (string, optional): when RenewBefore or EarliestWindow without windows,
cert-manager follows the existing behavior of using renewBefore
to renew the certificates.
windows are mentioned along with RenewBefore then cert-manager
will try to find the latest (forward) time which matches renewBefore. If there is no renewalTime in
the window, then the next renewalTime in the window would be returned. Add status on the certificates
when the renewalTime falls out of the window. Also, add status on the cert if the renewalTime is after
expiration date i.e. after notAfter.windows are mentioned along with EarliestWindow, then cert-manager will try to find a renewalTime
that is earliest (before) time which matches the window. This means, that if a renewBefore is ignored and
cert-manager will try to find a renewalTime earliest within a window. If, a renewalTime is outside of the compliant
window add it as a status on the cert object.Disabled cert won't be renewed.renewal.windows (array of RenewWindow, optional): defines
one or more allowed renewal windows. If omitted, renewal can happen
at any time (existing behavior).
cron: This defines a cron window which mentions the start time and the days when the renewal is allowed.duration: This determines the duration of the renewal.timezone: Timezone determines the timezone of the time. This must obey an IANA time zone listed in the link.Notes: - renewal.policy=RenewBefore and renewal.policy=EarliestWindow without windows would behave the same way as today
where they would try to get a renewalTime before renewBefore.
| renewal.policy | Windows defined? | RenewalTime Decision | Statuses Added to Certificate |
|---|---|---|---|
| RenewBefore | No | Same as existing behavior — choose renewBefore (i.e. NotAfter - X). | None (normal behavior). |
| RenewBefore | Yes | 1. Try to find the latest time within the allowed windows that is ≤ renewBefore. |
RenewBefore (i.e., respect renewBefore as today). | None (normal behavior). |
| EarliestWindow | Yes | Ignore renewBefore for window selection. Find the earliest allowed time within the configured windows (the earliest window slot). | Add appropriate conditions to the cert status. |
| Disabled | — | Certificate renewal is disabled (no renewal scheduling). | None — renewal not performed. |Tip: When renewal.windows are omitted, both RenewBefore and EarliestWindow fall back to the existing renew-before behavior, so the table's "No windows" rows reflect current behavior.
For the sake of uniformity, all windows defintions are going to be treated as UTC by the controller. renewal.windows definitions
allow IANA timezones for better configurations.
Cert-manager typically schedules a renewal event when the certificate's
NotAfter minus renewBefore is reached (or earlier, depending on
internal jitter and queueing). The controller reconciles Certificates
and triggers issuers.
desiredRenewalTime using existing logic
(expiry - renewBefore).renewal.policy == Disabled:
RenewalDisabled condition in status with a helpful
message and the observedGeneration when it was last observed.renewalPolicy.policy == RenewBefore:
renewBefore.
Find a renewalTime which fits in the window. If it doesn't fit the window
then add a status message; also add a message if the renewalTime compliant with
the window falls after expiration.windows is provided and renewalPolicy.policy == EarliestWindow:
renewalTime at the earliest which is compliant with the
window. If it is not compliant and it is past expiration, then post a status on the cert object.status.renewal.window.valid=false condition and do not
schedule renewals until corrected.Add the following renewal field to the status and also update some existing fields accordingly:
status:
renewalTime: "" # Update this according to the controller logic. Existing field.
lastFailureTime: "" # Again this exists and probably doesn't need to be touched.
renewal:
policy: RenewBefore
windows: # This will only be set if windows is set.
valid: "True"
# Every renewal check would add a condition and mark the cert as ready or not along with proper reasons
conditions: []
When a renewal is attempted or completed, existing issuance conditions
(e.g., Issuing, Ready) still apply.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: no-renewal-cert
spec:
secretName: no-renewal
dnsNames: ["test.example.com"]
renewal:
policy: Disabled
kubectl cert-manager renew (or similar manual
actions) should continue to work irrespective of disabled or
windows because those are meant to affect automatic renewal
only---unless the user explicitly requests that manual
operations be blocked (not proposed here).disabled explicit and require no special RBAC.RenewalConfigInvalid status to avoid silent misbehavior.kubectl hints in messages where appropriate
(e.g. To renew manually: kubectl cert-manager renew certificate/no-renewal-cert).renewal:
renewal early in reconcile.Disabled.EarliestWindow.start/end format using regex and invalid values
will be caught at runtime with RenewalConfigInvalid).Suggested metrics additions: -
certmanager_certificate_renewal_deferred_total{reason="outside_window"} -
certmanager_certificate_renewal_bypassed_total{reason="expiry_imminent"}
Document that operators should alert on many renewal_deferred events
for certificates approaching expiry.
We opted for per-Certificate windows for fine-grained control and simplicity of the API.
renewal behave exactly as today.renewal is opt-in.Standard unit and end-to-end tests will be used to verify new behaviour, as used by cert-manager currently.
Current end-to-end tests for Certificate resources will also give a good signal for renewal field.
renewal.policy=Disabled path.