Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sanitize plugins configuration built from secrets #5495

Merged
merged 1 commit into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,11 @@ Adding a new version? You'll need three changes:
to Konnect.
[#5453](https://github.com/Kong/kubernetes-ingress-controller/pull/5453)
- Added `SanitizeKonnectConfigDumps` feature gate allowing to enable sanitizing
sensitive data in Konnect configuration dumps.
sensitive data in Konnect configuration dumps.
[#5489](https://github.com/Kong/kubernetes-ingress-controller/pull/5489)
- Kong Plugin's `config` field now is sanitized when it contains sensitive data
sourced from a Secret (i.e. `configFrom` or `configPatches` is used).
[#5495](https://github.com/Kong/kubernetes-ingress-controller/pull/5495)

### Fixed

Expand Down
4 changes: 3 additions & 1 deletion internal/dataplane/kongstate/kongstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ func (ks *KongState) SanitizedCopy() *KongState {
return
}(),
CACertificates: ks.CACertificates,
Plugins: ks.Plugins,
Plugins: lo.Map(ks.Plugins, func(p Plugin, _ int) Plugin {
return p.SanitizedCopy()
}),
Consumers: func() (res []Consumer) {
for _, v := range ks.Consumers {
res = append(res, *v.SanitizedCopy())
Expand Down
10 changes: 8 additions & 2 deletions internal/dataplane/kongstate/kongstate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ func TestKongState_SanitizedCopy(t *testing.T) {
Upstreams: []Upstream{{Upstream: kong.Upstream{ID: kong.String("1")}}},
Certificates: []Certificate{{Certificate: kong.Certificate{ID: kong.String("1"), Key: kong.String("secret")}}},
CACertificates: []kong.CACertificate{{ID: kong.String("1")}},
Plugins: []Plugin{{Plugin: kong.Plugin{ID: kong.String("1")}}},
Plugins: []Plugin{{
SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{WholeConfigIsSensitive: true},
Plugin: kong.Plugin{ID: kong.String("1"), Config: kong.Configuration{"secret": "secretValue"}},
}},
Consumers: []Consumer{{
KeyAuths: []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: kong.String("secret")}}},
}},
Expand All @@ -78,7 +81,10 @@ func TestKongState_SanitizedCopy(t *testing.T) {
Upstreams: []Upstream{{Upstream: kong.Upstream{ID: kong.String("1")}}},
Certificates: []Certificate{{Certificate: kong.Certificate{ID: kong.String("1"), Key: redactedString}}},
CACertificates: []kong.CACertificate{{ID: kong.String("1")}},
Plugins: []Plugin{{Plugin: kong.Plugin{ID: kong.String("1")}}},
Plugins: []Plugin{{
SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{WholeConfigIsSensitive: true},
Plugin: kong.Plugin{ID: kong.String("1"), Config: kong.Configuration{"secret": *redactedString}},
}},
Consumers: []Consumer{{
KeyAuths: []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: redactedString}}},
}},
Expand Down
126 changes: 124 additions & 2 deletions internal/dataplane/kongstate/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,122 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"

jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/kong/go-kong/kong"
"github.com/samber/lo"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"

"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
)

// Plugin represents a plugin Object in Kong.
type Plugin struct {
kong.Plugin
K8sParent client.Object
SensitiveFieldsMeta PluginSensitiveFieldsMetadata
}

func (p Plugin) DeepCopy() Plugin {
return Plugin{
Plugin: *p.Plugin.DeepCopy(),
K8sParent: p.K8sParent,
SensitiveFieldsMeta: p.SensitiveFieldsMeta,
}
}

func (p Plugin) SanitizedCopy() Plugin {
// We do not want to return an error if any of below fails - the best we can do
// is to return a plugin with wholly redacted config.
// Let's have a closure returning a plugin with wholly redacted config prepared.
whollySanitized := func() Plugin {
p := p.DeepCopy()
p.Config = sanitizeWholePluginConfig(p.Config)
return p
}

// If the whole config is sensitive, we need to redact the entire config.
if p.SensitiveFieldsMeta.WholeConfigIsSensitive {
return whollySanitized()
}

// If there are JSON paths, we need to redact them.
if len(p.SensitiveFieldsMeta.JSONPaths) > 0 {
var patchOperations []string
for _, path := range p.SensitiveFieldsMeta.JSONPaths {
// If the path is empty, we need to sanitize the whole config.
// An empty path means that the patch is on the root of the config.
if path == "" {
return whollySanitized()
}

patchOperations = append(patchOperations, fmt.Sprintf(
`{"op":"replace","path":"%s","value":"%s"}`,
path,
*redactedString,
))
}

// Decode the patch and apply it to the config.
// We need to marshal the config to JSON and then unmarshal it back to Configuration
// because the patch library works with bytes.
patch, err := jsonpatch.DecodePatch([]byte(fmt.Sprintf("[%s]", strings.Join(patchOperations, ","))))
if err != nil {
return whollySanitized()
czeslavo marked this conversation as resolved.
Show resolved Hide resolved
}
configB, err := json.Marshal(p.Config)
if err != nil {
return whollySanitized()
}
sanitizedConfigB, err := patch.Apply(configB)
if err != nil {
return whollySanitized()
}
sanitizedConfig := kong.Configuration{}
if err := json.Unmarshal(sanitizedConfigB, &sanitizedConfig); err != nil {
return whollySanitized()
}

sanitized := p.DeepCopy()
sanitized.Config = sanitizedConfig
return sanitized
}

// Nothing to sanitize.
return p
}

// sanitizeWholePluginConfig redacts the entire config of a plugin by replacing all of its
// values with a redacted string.
func sanitizeWholePluginConfig(config kong.Configuration) kong.Configuration {
sanitized := config.DeepCopy()
for k := range config {
sanitized[k] = *redactedString
}
return sanitized
}

// PluginSensitiveFieldsMetadata holds metadata about sensitive fields in a plugin's configuration.
// It can be used to sanitize them before exposing the configuration to the user (e.g. in debug dumps
// or in Konnect Admin API).
type PluginSensitiveFieldsMetadata struct {
// WholeConfigIsSensitive indicates that the entire configuration of the plugin is sensitive.
// If this is true, the configuration should be redacted entirely (each of its fields' values
// should be replaced with a redacted string).
WholeConfigIsSensitive bool

// JSONPaths holds a list of JSON paths to sensitive fields in the plugin's configuration.
// If this is not empty, the configuration should be redacted by replacing the values of the
// fields at these paths with a redacted string.
JSONPaths []string
}

// getKongPluginOrKongClusterPlugin fetches a KongPlugin or KongClusterPlugin (as fallback) from the store.
// If both are not found, an error is returned.
func getKongPluginOrKongClusterPlugin(s store.Storer, namespace, name string) (
Expand Down Expand Up @@ -77,6 +181,14 @@ func kongPluginFromK8SClusterPlugin(
}
}

// Prepare sensitive fields metadata for the plugin.
sensitiveFieldsMeta := PluginSensitiveFieldsMetadata{
JSONPaths: lo.Map(k8sPlugin.ConfigPatches, func(patch kongv1.NamespacedConfigPatch, _ int) string {
return patch.Path
}),
WholeConfigIsSensitive: k8sPlugin.ConfigFrom != nil,
}

return Plugin{
Plugin: plugin{
Name: k8sPlugin.PluginName,
Expand All @@ -89,7 +201,8 @@ func kongPluginFromK8SClusterPlugin(
Protocols: protocolsToStrings(k8sPlugin.Protocols),
Tags: util.GenerateTagsForObject(&k8sPlugin),
}.toKongPlugin(),
K8sParent: &k8sPlugin,
K8sParent: &k8sPlugin,
SensitiveFieldsMeta: sensitiveFieldsMeta,
}, nil
}

Expand Down Expand Up @@ -131,6 +244,14 @@ func kongPluginFromK8SPlugin(
}
}

// Prepare sensitive fields metadata for the plugin.
sensitiveFieldsMeta := PluginSensitiveFieldsMetadata{
JSONPaths: lo.Map(k8sPlugin.ConfigPatches, func(patch kongv1.ConfigPatch, _ int) string {
return patch.Path
}),
WholeConfigIsSensitive: k8sPlugin.ConfigFrom != nil,
}

return Plugin{
Plugin: plugin{
Name: k8sPlugin.PluginName,
Expand All @@ -143,7 +264,8 @@ func kongPluginFromK8SPlugin(
Protocols: protocolsToStrings(k8sPlugin.Protocols),
Tags: util.GenerateTagsForObject(&k8sPlugin),
}.toKongPlugin(),
K8sParent: &k8sPlugin,
K8sParent: &k8sPlugin,
SensitiveFieldsMeta: sensitiveFieldsMeta,
}, nil
}

Expand Down
Loading
Loading