ClusterTrustBundle: Enforce max size during validation

This commit is contained in:
Taahir Ahmed 2023-07-06 23:50:30 -07:00
parent 1ebe5774d0
commit 96e610ac18
3 changed files with 212 additions and 183 deletions

View File

@ -277,3 +277,6 @@ type ClusterTrustBundleList struct {
// Items is a collection of ClusterTrustBundle objects // Items is a collection of ClusterTrustBundle objects
Items []ClusterTrustBundle Items []ClusterTrustBundle
} }
// MaxTrustBundleSize is the maximimum size of a single trust bundle field.
const MaxTrustBundleSize = 1 * 1024 * 1024

View File

@ -508,6 +508,11 @@ func ValidateClusterTrustBundleUpdate(newBundle, oldBundle *certificates.Cluster
func validateTrustBundle(path *field.Path, in string) field.ErrorList { func validateTrustBundle(path *field.Path, in string) field.ErrorList {
var allErrors field.ErrorList var allErrors field.ErrorList
if len(in) > certificates.MaxTrustBundleSize {
allErrors = append(allErrors, field.TooLong(path, fmt.Sprintf("<value omitted, len %d>", len(in)), certificates.MaxTrustBundleSize))
return allErrors
}
blockDedupe := map[string][]int{} blockDedupe := map[string][]int{}
rest := []byte(in) rest := []byte(in)

View File

@ -1104,228 +1104,249 @@ func TestValidateClusterTrustBundle(t *testing.T) {
badBlockTypeBlock := string(mustMakePEMBlock("NOTACERTIFICATE", nil, goodCert1)) badBlockTypeBlock := string(mustMakePEMBlock("NOTACERTIFICATE", nil, goodCert1))
badNonParseableBlock := string(mustMakePEMBlock("CERTIFICATE", nil, []byte("this is not a certificate"))) badNonParseableBlock := string(mustMakePEMBlock("CERTIFICATE", nil, []byte("this is not a certificate")))
badTooBigBundle := ""
for i := 0; i < (core.MaxSecretSize/len(goodCert1Block))+1; i++ {
badTooBigBundle += goodCert1Block + "\n"
}
testCases := []struct { testCases := []struct {
description string description string
bundle *capi.ClusterTrustBundle bundle *capi.ClusterTrustBundle
opts ValidateClusterTrustBundleOptions opts ValidateClusterTrustBundleOptions
wantErrors field.ErrorList wantErrors field.ErrorList
}{{ }{
description: "valid, no signer name", {
bundle: &capi.ClusterTrustBundle{ description: "valid, no signer name",
ObjectMeta: metav1.ObjectMeta{ bundle: &capi.ClusterTrustBundle{
Name: "foo", ObjectMeta: metav1.ObjectMeta{
}, Name: "foo",
Spec: capi.ClusterTrustBundleSpec{ },
TrustBundle: goodCert1Block, Spec: capi.ClusterTrustBundleSpec{
TrustBundle: goodCert1Block,
},
}, },
}, },
}, { {
description: "invalid, no signer name, invalid name", description: "invalid, too big",
bundle: &capi.ClusterTrustBundle{ bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:bar:foo", Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: badTooBigBundle,
},
}, },
Spec: capi.ClusterTrustBundleSpec{ wantErrors: field.ErrorList{
TrustBundle: goodCert1Block, field.TooLong(field.NewPath("spec", "trustBundle"), fmt.Sprintf("<value omitted, len %d>", len(badTooBigBundle)), core.MaxSecretSize),
}, },
}, },
wantErrors: field.ErrorList{ {
field.Invalid(field.NewPath("metadata", "name"), "k8s.io:bar:foo", "ClusterTrustBundle without signer name must not have \":\" in its name"), description: "invalid, no signer name, invalid name",
}, bundle: &capi.ClusterTrustBundle{
}, { ObjectMeta: metav1.ObjectMeta{
description: "valid, with signer name", Name: "k8s.io:bar:foo",
bundle: &capi.ClusterTrustBundle{ },
ObjectMeta: metav1.ObjectMeta{ Spec: capi.ClusterTrustBundleSpec{
Name: "k8s.io:foo:bar", TrustBundle: goodCert1Block,
},
}, },
Spec: capi.ClusterTrustBundleSpec{ wantErrors: field.ErrorList{
SignerName: "k8s.io/foo", field.Invalid(field.NewPath("metadata", "name"), "k8s.io:bar:foo", "ClusterTrustBundle without signer name must not have \":\" in its name"),
TrustBundle: goodCert1Block,
}, },
}, }, {
}, { description: "valid, with signer name",
description: "invalid, with signer name, missing name prefix", bundle: &capi.ClusterTrustBundle{
bundle: &capi.ClusterTrustBundle{ ObjectMeta: metav1.ObjectMeta{
ObjectMeta: metav1.ObjectMeta{ Name: "k8s.io:foo:bar",
Name: "look-ma-no-prefix", },
Spec: capi.ClusterTrustBundleSpec{
SignerName: "k8s.io/foo",
TrustBundle: goodCert1Block,
},
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
SignerName: "k8s.io/foo", description: "invalid, with signer name, missing name prefix",
TrustBundle: goodCert1Block, bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "look-ma-no-prefix",
},
Spec: capi.ClusterTrustBundleSpec{
SignerName: "k8s.io/foo",
TrustBundle: goodCert1Block,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("metadata", "name"), "look-ma-no-prefix", "ClusterTrustBundle for signerName k8s.io/foo must be named with prefix k8s.io:foo:"),
field.Invalid(field.NewPath("metadata", "name"), "look-ma-no-prefix", "ClusterTrustBundle for signerName k8s.io/foo must be named with prefix k8s.io:foo:"),
},
}, {
description: "invalid, with signer name, empty name suffix",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
SignerName: "k8s.io/foo", description: "invalid, with signer name, empty name suffix",
TrustBundle: goodCert1Block, bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:",
},
Spec: capi.ClusterTrustBundleSpec{
SignerName: "k8s.io/foo",
TrustBundle: goodCert1Block,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
},
}, {
description: "invalid, with signer name, bad name suffix",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:123notvalidDNSSubdomain",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
SignerName: "k8s.io/foo", description: "invalid, with signer name, bad name suffix",
TrustBundle: goodCert1Block, bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:123notvalidDNSSubdomain",
},
Spec: capi.ClusterTrustBundleSpec{
SignerName: "k8s.io/foo",
TrustBundle: goodCert1Block,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:123notvalidDNSSubdomain", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:123notvalidDNSSubdomain", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
},
}, {
description: "valid, with signer name, with inter-block garbage",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:abc",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
SignerName: "k8s.io/foo", description: "valid, with signer name, with inter-block garbage",
TrustBundle: "garbage\n" + goodCert1Block + "\ngarbage\n" + goodCert2Block, bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:abc",
},
Spec: capi.ClusterTrustBundleSpec{
SignerName: "k8s.io/foo",
TrustBundle: "garbage\n" + goodCert1Block + "\ngarbage\n" + goodCert2Block,
},
}, },
}, }, {
}, { description: "invalid, no signer name, no trust anchors",
description: "invalid, no signer name, no trust anchors", bundle: &capi.ClusterTrustBundle{
bundle: &capi.ClusterTrustBundle{ ObjectMeta: metav1.ObjectMeta{
ObjectMeta: metav1.ObjectMeta{ Name: "foo",
Name: "foo", },
Spec: capi.ClusterTrustBundleSpec{},
}, },
Spec: capi.ClusterTrustBundleSpec{}, wantErrors: field.ErrorList{
}, field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
wantErrors: field.ErrorList{
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
},
}, {
description: "invalid, no trust anchors",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:abc",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
SignerName: "k8s.io/foo", description: "invalid, no trust anchors",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "k8s.io:foo:abc",
},
Spec: capi.ClusterTrustBundleSpec{
SignerName: "k8s.io/foo",
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
},
}, {
description: "invalid, bad signer name",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid:foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
SignerName: "invalid", description: "invalid, bad signer name",
TrustBundle: goodCert1Block, bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid:foo",
},
Spec: capi.ClusterTrustBundleSpec{
SignerName: "invalid",
TrustBundle: goodCert1Block,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "signerName"), "invalid", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
field.Invalid(field.NewPath("spec", "signerName"), "invalid", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
},
}, {
description: "invalid, no blocks",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
TrustBundle: "non block garbage", description: "invalid, no blocks",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: "non block garbage",
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
},
}, {
description: "invalid, bad block type",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
TrustBundle: goodCert1Block + "\n" + badBlockTypeBlock, description: "invalid, bad block type",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: goodCert1Block + "\n" + badBlockTypeBlock,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has bad block type: NOTACERTIFICATE"),
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has bad block type: NOTACERTIFICATE"),
},
}, {
description: "invalid, block with headers",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
TrustBundle: goodCert1Block + "\n" + badBlockHeadersBlock, description: "invalid, block with headers",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: goodCert1Block + "\n" + badBlockHeadersBlock,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has PEM block headers"),
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has PEM block headers"),
},
}, {
description: "invalid, cert is not a CA cert",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
TrustBundle: badNotCACertBlock, description: "invalid, cert is not a CA cert",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: badNotCACertBlock,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 0 does not have the CA bit set"),
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 0 does not have the CA bit set"),
},
}, {
description: "invalid, duplicated blocks",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
TrustBundle: goodCert1Block + "\n" + goodCert1AlternateBlock, description: "invalid, duplicated blocks",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: goodCert1Block + "\n" + goodCert1AlternateBlock,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "duplicate trust anchor (indices [0 1])"),
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "duplicate trust anchor (indices [0 1])"),
},
}, {
description: "invalid, non-certificate entry",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
TrustBundle: goodCert1Block + "\n" + badNonParseableBlock, description: "invalid, non-certificate entry",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: goodCert1Block + "\n" + badNonParseableBlock,
},
}, },
}, wantErrors: field.ErrorList{
wantErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 does not parse as X.509"),
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 does not parse as X.509"),
},
}, {
description: "allow any old garbage in the PEM field if we suppress parsing",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Spec: capi.ClusterTrustBundleSpec{ }, {
TrustBundle: "garbage", description: "allow any old garbage in the PEM field if we suppress parsing",
bundle: &capi.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: capi.ClusterTrustBundleSpec{
TrustBundle: "garbage",
},
}, },
}, opts: ValidateClusterTrustBundleOptions{
opts: ValidateClusterTrustBundleOptions{ SuppressBundleParsing: true,
SuppressBundleParsing: true, },
}, }}
}}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
gotErrors := ValidateClusterTrustBundle(tc.bundle, tc.opts) gotErrors := ValidateClusterTrustBundle(tc.bundle, tc.opts)