csr: add expirationSeconds field to control cert lifetime
This change updates the CSR API to add a new, optional field called expirationSeconds. This field is a request to the signer for the maximum duration the client wishes the cert to have. The signer is free to ignore this request based on its own internal policy. The signers built-in to KCM will honor this field if it is not set to a value greater than --cluster-signing-duration. The minimum allowed value for this field is 600 seconds (ten minutes). This change will help enforce safer durations for certificates in the Kube ecosystem and will help related projects such as cert-manager with their migration to the Kube CSR API. Future enhancements may update the Kubelet to take advantage of this field when it is configured in a way that can tolerate shorter certificate lifespans with regular rotation. Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
@@ -17,9 +17,12 @@ limitations under the License.
|
||||
package fuzzer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
|
||||
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/util/certificate/csr"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
@@ -31,6 +34,7 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
c.FuzzNoCustom(obj) // fuzz self without calling this function again
|
||||
obj.Usages = []certificates.KeyUsage{certificates.UsageKeyEncipherment}
|
||||
obj.SignerName = "example.com/custom-sample-signer"
|
||||
obj.ExpirationSeconds = csr.DurationToExpirationSeconds(time.Hour + time.Minute + time.Second)
|
||||
},
|
||||
func(obj *certificates.CertificateSigningRequestCondition, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(obj) // fuzz self without calling this function again
|
||||
|
@@ -68,6 +68,30 @@ type CertificateSigningRequestSpec struct {
|
||||
// 6. Whether or not requests for CA certificates are allowed.
|
||||
SignerName string
|
||||
|
||||
// expirationSeconds is the requested duration of validity of the issued
|
||||
// certificate. The certificate signer may issue a certificate with a different
|
||||
// validity duration so a client must check the delta between the notBefore and
|
||||
// and notAfter fields in the issued certificate to determine the actual duration.
|
||||
//
|
||||
// The v1.22+ in-tree implementations of the well-known Kubernetes signers will
|
||||
// honor this field as long as the requested duration is not greater than the
|
||||
// maximum duration they will honor per the --cluster-signing-duration CLI
|
||||
// flag to the Kubernetes controller manager.
|
||||
//
|
||||
// Certificate signers may not honor this field for various reasons:
|
||||
//
|
||||
// 1. Old signer that is unaware of the field (such as the in-tree
|
||||
// implementations prior to v1.22)
|
||||
// 2. Signer whose configured maximum is shorter than the requested duration
|
||||
// 3. Signer whose configured minimum is longer than the requested duration
|
||||
//
|
||||
// The minimum valid value for expirationSeconds is 600, i.e. 10 minutes.
|
||||
//
|
||||
// As of v1.22, this field is beta and is controlled via the CSRDuration feature gate.
|
||||
//
|
||||
// +optional
|
||||
ExpirationSeconds *int32
|
||||
|
||||
// usages specifies a set of usage contexts the key will be
|
||||
// valid for.
|
||||
// See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||
|
@@ -205,6 +205,9 @@ func validateCertificateSigningRequest(csr *certificates.CertificateSigningReque
|
||||
} else {
|
||||
allErrs = append(allErrs, ValidateCertificateSigningRequestSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...)
|
||||
}
|
||||
if csr.Spec.ExpirationSeconds != nil && *csr.Spec.ExpirationSeconds < 600 {
|
||||
allErrs = append(allErrs, field.Invalid(specPath.Child("expirationSeconds"), *csr.Spec.ExpirationSeconds, "may not specify a duration less than 600 seconds (10 minutes)"))
|
||||
}
|
||||
allErrs = append(allErrs, validateConditions(field.NewPath("status", "conditions"), csr, opts)...)
|
||||
|
||||
if !opts.allowArbitraryCertificate {
|
||||
|
@@ -26,14 +26,17 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/client-go/util/certificate/csr"
|
||||
capi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
capiv1beta1 "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -262,6 +265,74 @@ func TestValidateCertificateSigningRequestCreate(t *testing.T) {
|
||||
},
|
||||
errs: field.ErrorList{},
|
||||
},
|
||||
"negative duration": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: validSignerName,
|
||||
ExpirationSeconds: pointer.Int32(-1),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("expirationSeconds"), int32(-1), "may not specify a duration less than 600 seconds (10 minutes)"),
|
||||
},
|
||||
},
|
||||
"zero duration": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: validSignerName,
|
||||
ExpirationSeconds: pointer.Int32(0),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("expirationSeconds"), int32(0), "may not specify a duration less than 600 seconds (10 minutes)"),
|
||||
},
|
||||
},
|
||||
"one duration": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: validSignerName,
|
||||
ExpirationSeconds: pointer.Int32(1),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("expirationSeconds"), int32(1), "may not specify a duration less than 600 seconds (10 minutes)"),
|
||||
},
|
||||
},
|
||||
"too short duration": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: validSignerName,
|
||||
ExpirationSeconds: csr.DurationToExpirationSeconds(time.Minute),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("expirationSeconds"), *csr.DurationToExpirationSeconds(time.Minute), "may not specify a duration less than 600 seconds (10 minutes)"),
|
||||
},
|
||||
},
|
||||
"valid duration": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: validSignerName,
|
||||
ExpirationSeconds: csr.DurationToExpirationSeconds(10 * time.Minute),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{},
|
||||
},
|
||||
"missing usages": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
|
Reference in New Issue
Block a user