From 77c5f32b8ce31395f0a0bbe23454dafff2b93bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Burzy=C5=84ski?= Date: Tue, 18 Oct 2022 12:27:50 +0200 Subject: [PATCH] feat: Skip synchronisation of expired CA certificates (#3063) CA certificate expiration validation is introduced. Once it fails, synchronization to the data plane is skipped (analogically to the other already existing validations against CA certificate secrets). It also adds logging of affected plugins for better context. --- CHANGELOG.md | 3 + internal/dataplane/kong_client.go | 10 +- internal/dataplane/parser/parser.go | 59 +----- internal/dataplane/parser/parser_test.go | 178 ++++++++---------- .../dataplane/parser/translate_secrets.go | 108 +++++++++++ .../parser/translate_secrets_test.go | 40 ++++ 6 files changed, 239 insertions(+), 159 deletions(-) create mode 100644 internal/dataplane/parser/translate_secrets.go create mode 100644 internal/dataplane/parser/translate_secrets_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b975e9270..49d80c609e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,9 @@ Adding a new version? You'll need three changes: - Added `--cache-sync-timeout` flag allowing to change the default controllers' cache synchronisation timeout. [#3013](https://github.com/Kong/kubernetes-ingress-controller/pull/3013) +- Secrets validation introduced: CA certificates won't be synchronized + to Kong if the certificate is expired. + [#3063](https://github.com/Kong/kubernetes-ingress-controller/pull/3063) ### Fixed diff --git a/internal/dataplane/kong_client.go b/internal/dataplane/kong_client.go index 2d786b80b3..a73561799b 100644 --- a/internal/dataplane/kong_client.go +++ b/internal/dataplane/kong_client.go @@ -311,13 +311,9 @@ func (c *KongClient) Update(ctx context.Context) error { } // parse the Kubernetes objects from the storer into Kong configuration - kongstate, err := p.Build() - if err != nil { - c.prometheusMetrics.TranslationCount.With(prometheus.Labels{ - metrics.SuccessKey: metrics.SuccessFalse, - }).Inc() - return err - } + kongstate := p.Build() + // todo: does it still make sense to report TranslationCount when Build no longer returns an error? + // https://github.com/Kong/kubernetes-ingress-controller/issues/1892 c.prometheusMetrics.TranslationCount.With(prometheus.Labels{ metrics.SuccessKey: metrics.SuccessTrue, }).Inc() diff --git a/internal/dataplane/parser/parser.go b/internal/dataplane/parser/parser.go index 74bda121b9..d6c617aca9 100644 --- a/internal/dataplane/parser/parser.go +++ b/internal/dataplane/parser/parser.go @@ -3,8 +3,6 @@ package parser import ( "bytes" "crypto/tls" - "crypto/x509" - "encoding/pem" "fmt" "reflect" "sort" @@ -73,8 +71,7 @@ func NewParser( // Build creates a Kong configuration from Ingress and Custom resources // defined in Kubernetes. -// It throws an error if there is an error returned from client-go. -func (p *Parser) Build() (*kongstate.KongState, error) { +func (p *Parser) Build() *kongstate.KongState { // parse and merge all rules together from all Kubernetes API sources ingressRules := mergeIngressRules( p.ingressRulesFromIngressV1beta1(), @@ -121,14 +118,9 @@ func (p *Parser) Build() (*kongstate.KongState, error) { result.Certificates = mergeCerts(p.logger, ingressCerts, gatewayCerts) // populate CA certificates in Kong - var err error - caCertSecrets, err := p.storer.ListCACerts() - if err != nil { - return nil, err - } - result.CACertificates = toCACerts(p.logger, caCertSecrets) + result.CACertificates = getCACerts(p.logger, p.storer, result.Plugins) - return &result, nil + return &result } // ----------------------------------------------------------------------------- @@ -186,51 +178,6 @@ func (p *Parser) EnableRegexPathPrefix() { // Parser - Private Methods // ----------------------------------------------------------------------------- -func toCACerts(log logrus.FieldLogger, caCertSecrets []*corev1.Secret) []kong.CACertificate { - var caCerts []kong.CACertificate - for _, certSecret := range caCertSecrets { - secretName := certSecret.Namespace + "/" + certSecret.Name - - idbytes, idExists := certSecret.Data["id"] - log = log.WithFields(logrus.Fields{ - "secret_name": secretName, - "secret_namespace": certSecret.Namespace, - }) - if !idExists { - log.Errorf("invalid CA certificate: missing 'id' field in data") - continue - } - - caCertbytes, certExists := certSecret.Data["cert"] - if !certExists { - log.Errorf("invalid CA certificate: missing 'cert' field in data") - continue - } - - pemBlock, _ := pem.Decode(caCertbytes) - if pemBlock == nil { - log.Errorf("invalid CA certificate: invalid PEM block") - continue - } - x509Cert, err := x509.ParseCertificate(pemBlock.Bytes) - if err != nil { - log.WithError(err).Errorf("invalid CA certificate: failed to parse certificate") - continue - } - if !x509Cert.IsCA { - log.WithError(err).Errorf("invalid CA certificate: certificate is missing the 'CA' basic constraint") - continue - } - - caCerts = append(caCerts, kong.CACertificate{ - ID: kong.String(string(idbytes)), - Cert: kong.String(string(caCertbytes)), - }) - } - - return caCerts -} - func knativeIngressToNetworkingTLS(tls []knative.IngressTLS) []netv1beta1.IngressTLS { var result []netv1beta1.IngressTLS diff --git a/internal/dataplane/parser/parser_test.go b/internal/dataplane/parser/parser_test.go index 3e389d5810..09648ea256 100644 --- a/internal/dataplane/parser/parser_test.go +++ b/internal/dataplane/parser/parser_test.go @@ -236,6 +236,23 @@ T1I8lwssIgbA3XubokI+IMkLDEpCQ27niWXOZL5y2M3xyutd6PPjmEEmoHMkOrZL etlxzx2CCoUDXKkYW2gZKEozwBZ+eBgUj8WB5g/8jGDAI0qzYnfAgiahjGwlEUtP hJiPG/YFADw0m5b/8OMCZ6AXNhxjdweHniDxY2HE734Nwm9mG/7UbkdvhR05tqFh G4KCViLH0cXt/TgW1sYB2o9Z +-----END CERTIFICATE-----` + expiredCACert = `-----BEGIN CERTIFICATE----- +MIICwTCCAamgAwIBAgIUHGUzUWvHJHrREvIZIcORiFUvze4wDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAwwFSGVsbG8wHhcNMjAwNTA4MjExODA1WhcNMjAwNjA3MjEx +ODA1WjAQMQ4wDAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANCMMBngjuTvqts8ZXtZhqdr181QH/NmytW1KlyqZd6ppXUer+i0OWhP +1nAyHsBPJljKAFLd8l1EioPFkN78/wJFDJrHOtfniIQPVLdS2cnNQ72dLyQH6smH +JQDV8ePBQ2GdRP6s61+Da8eoaW6nSLtmEUhxvyteboqwmi2CtUtAfuiU1m5sOdpS +z+L4D08CE+SFIT4MGD3gxNdg7lccWCHIfk54VRSdGDKEVwed8OQvxD0TdpHY+ym5 +nJ4JSkhiS9XIodnxR3AZ6rIPRqk+MQ4LGTjX2EbM0/Yg4qvnZ7m4fcpK2goDZIVL +EF8F+ka1RaAYWTsXI1BAkJbb3kdo/yUCAwEAAaMTMBEwDwYDVR0TBAgwBgEB/wIB +ADANBgkqhkiG9w0BAQsFAAOCAQEAVvB/PeVZpeQ7q2IQQQpADtTd8+22Ma3jNZQD +EkWGZEQLkRws4EJNCCIvkApzpx1GqRcLLL9lbV+iCSiIdlR5W9HtK07VZ318gpsG +aTMNrP9/2XWTBzdHWaeZKmRKB04H4z7V2Dl58D+wxjdqNWsMIHeqqPNKGamk/q8k +YFNqNwisRxMhU6qPOpOj5Swl2jLTuVMAeGWBWmPGU2MUoaJb8sc2Vix9KXcyDZIr +eidkzkqSrjNzI0yJ2gdCDRS4/Rw9iV3B3SRMs0mJMLBDrsowhNfLAd8I3NHzLwps +dZFcvZcT/p717K3hlFVdjGnKIgKcG7aYji/XRR87HKnc+cJMCw== -----END CERTIFICATE-----` ) @@ -264,8 +281,7 @@ func TestGlobalPlugin(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Plugins), "expected one plugin to be rendered") @@ -446,7 +462,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { store, err := store.NewFakeStore(objects) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() + state := p.Build() assert.Nil(err) assert.NotNil(state) assert.Equal(3, len(state.Plugins), @@ -551,7 +567,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { store, err := store.NewFakeStore(objects) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() + state := p.Build() assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Plugins), @@ -653,8 +669,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { store, err := store.NewFakeStore(objects) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(0, len(state.Plugins), "expected no plugins to be rendered") @@ -705,8 +720,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { store, err := store.NewFakeStore(objects) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) for _, testcase := range references { config, err := kongstate.SecretToConfiguration(store, *testcase, "default") @@ -804,8 +818,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { store, err := store.NewFakeStore(objects) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(0, len(state.Plugins), "expected no plugins to be rendered") @@ -839,8 +852,7 @@ func TestCACertificate(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.CACertificates)) @@ -890,8 +902,7 @@ func TestCACertificate(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(2, len(state.CACertificates)) @@ -946,6 +957,22 @@ func TestCACertificate(t *testing.T) { "cert": []byte(caCert2), }, }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "non-default", + Labels: map[string]string{ + "konghq.com/ca-cert": "true", + }, + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + }, + }, + Data: map[string][]byte{ + "id": []byte("670c28aa-e784-43c1-8ec7-ae7f4ce40189"), + "cert": []byte(expiredCACert), + }, + }, } store, err := store.NewFakeStore(store.FakeObjects{ @@ -953,8 +980,7 @@ func TestCACertificate(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.CACertificates)) @@ -1037,8 +1063,7 @@ func TestServiceClientCertificate(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Certificates), "expected one certificates to be rendered") @@ -1105,8 +1130,7 @@ func TestServiceClientCertificate(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(0, len(state.Certificates), "expected no certificates to be rendered") @@ -1166,8 +1190,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1246,8 +1269,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1327,8 +1349,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1409,8 +1430,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1490,8 +1510,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1571,8 +1590,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1652,8 +1670,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1733,8 +1750,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -1814,8 +1830,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one service to be rendered") @@ -1893,8 +1908,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one service to be rendered") @@ -1972,8 +1986,7 @@ func TestKongRouteAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) kongTrue := kong.Bool(true) @@ -2033,8 +2046,7 @@ func TestKongProcessClasslessIngress(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -2084,8 +2096,7 @@ func TestKongProcessClasslessIngress(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(0, len(state.Services), @@ -2162,8 +2173,7 @@ func TestKnativeIngressAndPlugins(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one knative service") @@ -2241,8 +2251,7 @@ func TestKnativeIngressAndPlugins(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one knative service") @@ -2311,8 +2320,7 @@ func TestKnativeIngressAndPlugins(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one knative service") @@ -2397,8 +2405,7 @@ func TestKnativeIngressAndPlugins(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -2508,8 +2515,7 @@ func TestKongServiceAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -2591,8 +2597,7 @@ func TestKongServiceAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -2680,8 +2685,7 @@ func TestKongServiceAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -2750,8 +2754,7 @@ func TestDefaultBackend(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one service to be rendered") @@ -2819,8 +2822,7 @@ func TestDefaultBackend(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(0, len(state.Certificates), "expected no certificates to be rendered") @@ -2885,8 +2887,7 @@ func TestParserSecret(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(0, len(state.Certificates), "expected no certificates to be rendered with empty secret") @@ -2967,8 +2968,7 @@ func TestParserSecret(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Certificates), "certificates are de-duplicated") @@ -3094,8 +3094,7 @@ func TestParserSecret(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Certificates), "certificates are de-duplicated") @@ -3176,8 +3175,7 @@ func TestParserSecret(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Certificates), "SNIs are de-duplicated") @@ -3259,8 +3257,7 @@ func TestParserSNI(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), @@ -3324,8 +3321,7 @@ func TestParserSNI(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), @@ -3384,8 +3380,7 @@ func TestParserHostAliases(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), @@ -3437,8 +3432,7 @@ func TestParserHostAliases(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), @@ -3491,8 +3485,7 @@ func TestParserHostAliases(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), @@ -3581,8 +3574,7 @@ func TestPluginAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Plugins), "expected no plugins to be rendered with missing plugin") @@ -3679,8 +3671,7 @@ func TestPluginAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Plugins), "expected no plugins to be rendered with missing plugin") @@ -3750,8 +3741,7 @@ func TestPluginAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Plugins), "expected no plugins to be rendered with missing plugin") @@ -3797,8 +3787,7 @@ func TestPluginAnnotations(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(0, len(state.Plugins), "expected no plugins to be rendered with missing plugin") @@ -4594,8 +4583,7 @@ func TestPickPort(t *testing.T) { assert.NoError(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.NoError(err) + state := p.Build() assert.Equal(tt.wantTarget, *state.Upstreams[0].Targets[0].Target.Target) }) @@ -4708,8 +4696,7 @@ func TestCertificate(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(3, len(state.Certificates)) // foo.com with cert should be fixed @@ -4801,8 +4788,7 @@ func TestCertificate(t *testing.T) { }) assert.Nil(err) p := NewParser(logrus.New(), store) - state, err := p.Build() - assert.Nil(err) + state := p.Build() assert.NotNil(state) assert.Equal(1, len(state.Certificates)) assert.Equal(state.Certificates[0], fooCertificate) diff --git a/internal/dataplane/parser/translate_secrets.go b/internal/dataplane/parser/translate_secrets.go new file mode 100644 index 0000000000..7121a34e49 --- /dev/null +++ b/internal/dataplane/parser/translate_secrets.go @@ -0,0 +1,108 @@ +package parser + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "time" + + "github.com/kong/go-kong/kong" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" + "github.com/kong/kubernetes-ingress-controller/v2/internal/store" +) + +// getCACerts translates CA certificates Secrets to kong.CACertificates. It ensures every certificate's structure and +// validity. In case of violation of any validation rule, a secret gets skipped in a result and error message is logged +// with affected plugins for context. +func getCACerts(log logrus.FieldLogger, storer store.Storer, plugins []kongstate.Plugin) []kong.CACertificate { + caCertSecrets, err := storer.ListCACerts() + if err != nil { + log.WithError(err).Error("failed to list CA certs") + return nil + } + + var caCerts []kong.CACertificate + for _, certSecret := range caCertSecrets { + log := log.WithFields(logrus.Fields{ + "secret_name": certSecret.Name, + "secret_namespace": certSecret.Namespace, + }) + + idBytes, ok := certSecret.Data["id"] + if !ok { + log.Error("skipping synchronisation, invalid CA certificate: missing 'id' field in data") + continue + } + secretID := string(idBytes) + + caCert, err := toKongCACertificate(certSecret, secretID) + if err != nil { + logWithAffectedPlugins(log, plugins, secretID).WithError(err). + Error("skipping synchronisation, invalid CA certificate") + continue + } + + caCerts = append(caCerts, caCert) + } + + return caCerts +} + +func toKongCACertificate(certSecret *corev1.Secret, secretID string) (kong.CACertificate, error) { + caCertbytes, certExists := certSecret.Data["cert"] + if !certExists { + return kong.CACertificate{}, errors.New("missing 'cert' field in data") + } + pemBlock, _ := pem.Decode(caCertbytes) + if pemBlock == nil { + return kong.CACertificate{}, errors.New("invalid PEM block") + } + x509Cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return kong.CACertificate{}, errors.New("failed to parse certificate") + } + if !x509Cert.IsCA { + return kong.CACertificate{}, errors.New("certificate is missing the 'CA' basic constraint") + } + if time.Now().After(x509Cert.NotAfter) { + return kong.CACertificate{}, errors.New("expired") + } + + return kong.CACertificate{ + ID: kong.String(secretID), + Cert: kong.String(string(caCertbytes)), + }, nil +} + +func logWithAffectedPlugins(log logrus.FieldLogger, plugins []kongstate.Plugin, secretID string) logrus.FieldLogger { + affectedPlugins := getPluginsAssociatedWithCACertSecret(plugins, secretID) + return log.WithField("affected_plugins", affectedPlugins) +} + +func getPluginsAssociatedWithCACertSecret(plugins []kongstate.Plugin, secretID string) []string { + refersToSecret := func(pluginConfig map[string]interface{}) bool { + caCertReferences, ok := pluginConfig["ca_certificates"].([]string) + if !ok { + return false + } + + for _, reference := range caCertReferences { + if reference == secretID { + return true + } + } + return false + } + + var affectedPlugins []string + for _, p := range plugins { + if refersToSecret(p.Config) && p.Name != nil { + affectedPlugins = append(affectedPlugins, *p.Name) + } + } + + return affectedPlugins +} diff --git a/internal/dataplane/parser/translate_secrets_test.go b/internal/dataplane/parser/translate_secrets_test.go new file mode 100644 index 0000000000..f3b03e3d56 --- /dev/null +++ b/internal/dataplane/parser/translate_secrets_test.go @@ -0,0 +1,40 @@ +package parser + +import ( + "testing" + + "github.com/kong/go-kong/kong" + "github.com/stretchr/testify/require" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" +) + +func TestGetPluginsAssociatedWithCACertSecret(t *testing.T) { + secretID := "8a3753e0-093b-43d9-9d39-27985c987d92" //nolint:gosec + plugins := []kongstate.Plugin{ + { + Plugin: kong.Plugin{ + Name: kong.String("associated-plugin"), + Config: map[string]interface{}{ + "ca_certificates": []string{secretID}, + }, + }, + }, + { + Plugin: kong.Plugin{ + Name: kong.String("another-associated-plugin"), + Config: map[string]interface{}{ + "ca_certificates": []string{secretID}, + }, + }, + }, + { + Plugin: kong.Plugin{ + Name: kong.String("non-associated-plugin"), + }, + }, + } + + associatedPlugins := getPluginsAssociatedWithCACertSecret(plugins, secretID) + require.ElementsMatch(t, []string{"associated-plugin", "another-associated-plugin"}, associatedPlugins) +}