From af119898a95992fa53c4574af57b7ac13873f787 Mon Sep 17 00:00:00 2001 From: ssuresh Date: Mon, 4 Mar 2024 18:11:47 -0500 Subject: [PATCH] Enforce FIPS compatible Sprig functions for template rendering (#2708) * Add fipsonly sprig support * Add tests for fipsonly sprig * Update usages of sprig * Update docs about rendering templates * Update docs/templates.rst Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com> * Update pkg/ksprig/fipsonly_sprig.go Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com> * Update pkg/ksprig/fipsonly_sprig_test.go Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com> * Rename error and error field * Convert tests to check.v1 format --------- Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com> --- docs/templates.rst | 4 + pkg/function/wait.go | 4 +- pkg/ksprig/fipsonly_sprig.go | 80 ++++++++++++++++++++ pkg/ksprig/fipsonly_sprig_test.go | 121 ++++++++++++++++++++++++++++++ pkg/kube/unstructured_test.go | 6 +- pkg/param/param_test.go | 4 +- pkg/param/render.go | 4 +- 7 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 pkg/ksprig/fipsonly_sprig.go create mode 100644 pkg/ksprig/fipsonly_sprig_test.go diff --git a/docs/templates.rst b/docs/templates.rst index 68352868ca..7d9e9bc0d4 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -55,6 +55,10 @@ standard go template functions, Kanister imports all the `sprig } return ras, nil +.. note:: Kanister will error during template rendering if FIPS non-compliant + sprig functions are used. The unsupported functions include: ``bcrypt``, + ``derivePassword``, ``htpasswd``, and ``genPrivateKey`` for key type ``dsa``. + Objects ======= diff --git a/pkg/function/wait.go b/pkg/function/wait.go index 3d291db7dd..c02c3394bb 100644 --- a/pkg/function/wait.go +++ b/pkg/function/wait.go @@ -22,7 +22,6 @@ import ( "text/template" "time" - "github.com/Masterminds/sprig" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -33,6 +32,7 @@ import ( crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1" "github.com/kanisterio/kanister/pkg/field" "github.com/kanisterio/kanister/pkg/jsonpath" + "github.com/kanisterio/kanister/pkg/ksprig" "github.com/kanisterio/kanister/pkg/kube" "github.com/kanisterio/kanister/pkg/log" "github.com/kanisterio/kanister/pkg/param" @@ -189,7 +189,7 @@ func evaluateWaitCondition(ctx context.Context, dynCli dynamic.Interface, cond C return false, err } log.Debug().Print(fmt.Sprintf("Resolved jsonpath: %s", rcondition)) - t, err := template.New("config").Option("missingkey=zero").Funcs(sprig.TxtFuncMap()).Parse(rcondition) + t, err := template.New("config").Option("missingkey=zero").Funcs(ksprig.TxtFuncMap()).Parse(rcondition) if err != nil { return false, errors.WithStack(err) } diff --git a/pkg/ksprig/fipsonly_sprig.go b/pkg/ksprig/fipsonly_sprig.go new file mode 100644 index 0000000000..a6b4a5d31c --- /dev/null +++ b/pkg/ksprig/fipsonly_sprig.go @@ -0,0 +1,80 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ksprig + +import ( + "fmt" + "html/template" + + "github.com/Masterminds/sprig" +) + +// TxtFuncMap provides a FIPS compliant version of sprig.TxtFuncMap(). +// Usage of a FIPS non-compatible function from the function map will result in an error. +func TxtFuncMap() template.FuncMap { + return replaceNonCompliantFuncs(sprig.TxtFuncMap()) +} + +func replaceNonCompliantFuncs(m map[string]interface{}) map[string]interface{} { + for name, fn := range fipsNonCompliantFuncs { + if _, ok := m[name]; ok { + m[name] = fn + } + } + return m +} + +// fipsNonCompliantFuncs is a map of sprig function name to its replacement function. +// Functions identified for Sprig v3.2.3. +var fipsNonCompliantFuncs = map[string]interface{}{ + "bcrypt": func(input string) (string, error) { + return "", NewUnsupportedSprigUsageErr("bcrypt") + }, + + "derivePassword": func(counter uint32, password_type, password, user, site string) (string, error) { + return "", NewUnsupportedSprigUsageErr("derivePassword") + }, + + "genPrivateKey": func(typ string) (string, error) { + switch typ { + case "rsa", "ecdsa", "ed25519": + fn, ok := sprig.TxtFuncMap()["genPrivateKey"].(func(string) string) + if !ok { + return "", NewUnsupportedSprigUsageErr("genPrivateKey") + } + return fn(typ), nil + } + return "", NewUnsupportedSprigUsageErr(fmt.Sprintf("genPrivateKey for %s", typ)) + }, + + "htpasswd": func(username string, password string) (string, error) { + return "", NewUnsupportedSprigUsageErr("htpasswd") + }, +} + +// NewUnsupportedSprigUsageErr returns an UnsupportedSprigUsageErr. +func NewUnsupportedSprigUsageErr(usage string) UnsupportedSprigUsageErr { + return UnsupportedSprigUsageErr{Usage: usage} +} + +// UnsupportedSprigUsageErr indicates a FIPS non-compatible sprig usage. +type UnsupportedSprigUsageErr struct { + Usage string +} + +// Error returns an error string indicating the unsupported function. +func (err UnsupportedSprigUsageErr) Error() string { + return fmt.Sprintf("sprig usage of '%s' is not allowed by kanister as it is not FIPS compatible", err.Usage) +} diff --git a/pkg/ksprig/fipsonly_sprig_test.go b/pkg/ksprig/fipsonly_sprig_test.go new file mode 100644 index 0000000000..2de1e33905 --- /dev/null +++ b/pkg/ksprig/fipsonly_sprig_test.go @@ -0,0 +1,121 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ksprig_test + +import ( + "errors" + "strings" + "testing" + "text/template" + + . "gopkg.in/check.v1" + + "github.com/kanisterio/kanister/pkg/ksprig" +) + +type FipsOnlySprigSuite struct{} + +var _ = Suite(&FipsOnlySprigSuite{}) + +func TestFipsOnlySprigSuite(t *testing.T) { TestingT(t) } + +func (f *FipsOnlySprigSuite) TestUnsupportedTxtFuncMapUsage(c *C) { + funcMap := ksprig.TxtFuncMap() + + testCases := []struct { + function string + templateText string + usageErr string + }{ + { + function: "bcrypt", + templateText: "{{bcrypt \"password\"}}", + usageErr: "bcrypt", + }, + { + function: "derivePassword", + templateText: "{{derivePassword 1 \"long\" \"password\" \"user\" \"example.com\"}}", + usageErr: "derivePassword", + }, + { + function: "genPrivateKey", + templateText: "{{genPrivateKey \"dsa\"}}", + usageErr: "genPrivateKey for dsa", + }, + { + function: "htpasswd", + templateText: "{{htpasswd \"username\" \"password\"}}", + usageErr: "htpasswd", + }, + } + + for _, tc := range testCases { + if _, ok := funcMap[tc.function]; !ok { + c.Logf("Skipping test of %s since the tested sprig version does not support it", tc.function) + continue + } + c.Logf("Testing %s", tc.function) + + temp, err := template.New("test").Funcs(funcMap).Parse(tc.templateText) + c.Assert(err, IsNil) + + err = temp.Execute(nil, "") + + var sprigErr ksprig.UnsupportedSprigUsageErr + c.Assert(errors.As(err, &sprigErr), Equals, true) + c.Assert(sprigErr.Usage, Equals, tc.usageErr) + } +} + +func (f *FipsOnlySprigSuite) TestSupportedTxtFuncMapUsage(c *C) { + funcMap := ksprig.TxtFuncMap() + + testCases := []struct { + description string + function string + templateText string + }{ + // The supported usages are not limited to these test cases + { + description: "genPrivateKey for rsa key", + function: "genPrivateKey", + templateText: "{{genPrivateKey \"rsa\"}}", + }, + { + description: "genPrivateKey for ecdsa key", + function: "genPrivateKey", + templateText: "{{genPrivateKey \"ecdsa\"}}", + }, + { + description: "genPrivateKey for ed25519 key", + function: "genPrivateKey", + templateText: "{{genPrivateKey \"ed25519\"}}", + }, + } + + for _, tc := range testCases { + if _, ok := funcMap[tc.function]; !ok { + c.Logf("Skipping test of %s since the tested sprig version does not support it", tc.function) + continue + } + c.Logf("Testing %s", tc.description) + + temp, err := template.New("test").Funcs(funcMap).Parse(tc.templateText) + c.Assert(err, IsNil) + + err = temp.Execute(&strings.Builder{}, "") + c.Assert(err, IsNil) + } +} diff --git a/pkg/kube/unstructured_test.go b/pkg/kube/unstructured_test.go index 560c587246..5b935f3ac2 100644 --- a/pkg/kube/unstructured_test.go +++ b/pkg/kube/unstructured_test.go @@ -20,9 +20,9 @@ import ( "text/template" . "gopkg.in/check.v1" - - "github.com/Masterminds/sprig" "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/kanisterio/kanister/pkg/ksprig" ) type UnstructuredSuite struct{} @@ -52,7 +52,7 @@ func (s *UnstructuredSuite) TestFetch(c *C) { {"{{ .Unstructured.metadata.name }}"}, {"{{ .Unstructured.spec.clusterIP }}"}, } { - t, err := template.New("config").Option("missingkey=error").Funcs(sprig.TxtFuncMap()).Parse(tc.arg) + t, err := template.New("config").Option("missingkey=error").Funcs(ksprig.TxtFuncMap()).Parse(tc.arg) c.Assert(err, IsNil) err = t.Execute(buf, tp) c.Assert(err, IsNil) diff --git a/pkg/param/param_test.go b/pkg/param/param_test.go index 61e2dc4736..b21e05effa 100644 --- a/pkg/param/param_test.go +++ b/pkg/param/param_test.go @@ -23,7 +23,6 @@ import ( "text/template" "time" - "github.com/Masterminds/sprig" . "gopkg.in/check.v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -38,6 +37,7 @@ import ( crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1" crfake "github.com/kanisterio/kanister/pkg/client/clientset/versioned/fake" + "github.com/kanisterio/kanister/pkg/ksprig" "github.com/kanisterio/kanister/pkg/kube" osapps "github.com/openshift/api/apps/v1" osversioned "github.com/openshift/client-go/apps/clientset/versioned" @@ -772,7 +772,7 @@ func (s *ParamsSuite) TestRenderingPhaseParams(c *C) { "bar", }, } { - t, err := template.New("config").Option("missingkey=error").Funcs(sprig.TxtFuncMap()).Parse(tc.arg) + t, err := template.New("config").Option("missingkey=error").Funcs(ksprig.TxtFuncMap()).Parse(tc.arg) c.Assert(err, IsNil) buf := bytes.NewBuffer(nil) err = t.Execute(buf, tp) diff --git a/pkg/param/render.go b/pkg/param/render.go index 367211b300..1fde104cc7 100644 --- a/pkg/param/render.go +++ b/pkg/param/render.go @@ -21,10 +21,10 @@ import ( "strings" "text/template" - "github.com/Masterminds/sprig" "github.com/pkg/errors" crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1" + "github.com/kanisterio/kanister/pkg/ksprig" ) const ( @@ -119,7 +119,7 @@ func RenderArtifacts(arts map[string]crv1alpha1.Artifact, tp TemplateParams) (ma } func renderStringArg(arg string, tp TemplateParams) (string, error) { - t, err := template.New("config").Option("missingkey=error").Funcs(sprig.TxtFuncMap()).Parse(arg) + t, err := template.New("config").Option("missingkey=error").Funcs(ksprig.TxtFuncMap()).Parse(arg) if err != nil { return "", errors.WithStack(err) }