From e51053f5a8ee22b945d67de433ecc4957d48a604 Mon Sep 17 00:00:00 2001 From: knrt10 Date: Tue, 7 Jul 2020 17:15:28 +0530 Subject: [PATCH] Don't mount default ServiceAccount in pods - Add security to components and default namespace by preventing user to mount default ServiceAccount to their pods. - Add test for components. Fixes #669 Signed-off-by: knrt10 --- cli/cmd/cluster-apply.go | 5 + cli/cmd/component-apply.go | 7 +- pkg/components/util/install.go | 54 ++++++++++ pkg/components/util/types.go | 7 ++ test/components/patch_serviceaccount_test.go | 106 +++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 test/components/patch_serviceaccount_test.go diff --git a/cli/cmd/cluster-apply.go b/cli/cmd/cluster-apply.go index f410cdb18..fd08e3b27 100644 --- a/cli/cmd/cluster-apply.go +++ b/cli/cmd/cluster-apply.go @@ -22,6 +22,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/kinvolk/lokomotive/pkg/components/util" "github.com/kinvolk/lokomotive/pkg/install" "github.com/kinvolk/lokomotive/pkg/k8sutil" "github.com/kinvolk/lokomotive/pkg/lokomotive" @@ -107,6 +108,10 @@ func runClusterApply(cmd *cobra.Command, args []string) { } } + if err := util.DisableAutomountServiceAccountToken("default", kubeconfigPath); err != nil { + ctxLogger.Fatalf("Applying patch to default service account failed: %v", err) + } + if skipComponents { return } diff --git a/cli/cmd/component-apply.go b/cli/cmd/component-apply.go index 441a33fa4..8caa99a10 100644 --- a/cli/cmd/component-apply.go +++ b/cli/cmd/component-apply.go @@ -84,7 +84,12 @@ func applyComponents(lokoConfig *config.Config, kubeconfig string, componentName return diags } - if err := util.InstallComponent(component, kubeconfig); err != nil { + if err = util.InstallComponent(component, kubeconfig); err != nil { + return err + } + + ns := component.Metadata().Namespace + if err = util.DisableAutomountServiceAccountToken(ns, kubeconfig); err != nil { return err } diff --git a/pkg/components/util/install.go b/pkg/components/util/install.go index 11cd93cc8..6be57850c 100644 --- a/pkg/components/util/install.go +++ b/pkg/components/util/install.go @@ -16,6 +16,7 @@ package util import ( "context" + "encoding/json" "fmt" "io/ioutil" @@ -29,6 +30,7 @@ import ( "github.com/kinvolk/lokomotive/pkg/components" "github.com/kinvolk/lokomotive/pkg/k8sutil" + types "k8s.io/apimachinery/pkg/types" ) func ensureNamespaceExists(name string, kubeconfigPath string) error { @@ -59,6 +61,58 @@ func ensureNamespaceExists(name string, kubeconfigPath string) error { return nil } +// DisableAutomountServiceAccountToken updates default Service Account to not mount it in pods by default. +func DisableAutomountServiceAccountToken(ns, kubeconfigPath string) error { + if ns == "" { + return fmt.Errorf("namespace name can't be empty") + } + + kubeconfig, err := ioutil.ReadFile(kubeconfigPath) // #nosec G304 + if err != nil { + return fmt.Errorf("reading kubeconfig file: %w", err) + } + + cs, err := k8sutil.NewClientset(kubeconfig) + if err != nil { + return fmt.Errorf("creating clientset: %w", err) + } + + payload := []patchUInt32Value{{ + Op: "add", + Path: "/automountServiceAccountToken", + Value: false, + }} + + payloadBytes, _ := json.Marshal(payload) + + _, err = cs.CoreV1().ServiceAccounts(ns).Get(context.TODO(), "default", metav1.GetOptions{}) + + // This is fix for CI failing sometimes for `openebs-operator` + if err != nil && err.Error() == `serviceaccounts "default" not found` { + automountServiceAccountToken := false + _, err = cs.CoreV1().ServiceAccounts(ns).Create(context.TODO(), &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + AutomountServiceAccountToken: &automountServiceAccountToken, + }, metav1.CreateOptions{}) + + if err != nil { + return err + } + + return nil + } + + //nolint:lll + _, err = cs.CoreV1().ServiceAccounts(ns).Patch(context.TODO(), "default", types.JSONPatchType, payloadBytes, metav1.PatchOptions{}) + if err != nil { + return err + } + + return nil +} + // InstallComponent installs given component using given kubeconfig as a Helm release using a Helm client. func InstallComponent(c components.Component, kubeconfig string) error { name := c.Metadata().Name diff --git a/pkg/components/util/types.go b/pkg/components/util/types.go index bfb201341..009e6e2f4 100644 --- a/pkg/components/util/types.go +++ b/pkg/components/util/types.go @@ -78,3 +78,10 @@ func (n *NodeSelector) Render() (string, error) { return string(b), nil } + +// patchUInt32Value specifies a patch operation for a uint32. +type patchUInt32Value struct { + Op string `json:"op"` + Path string `json:"path"` + Value bool `json:"value"` +} diff --git a/test/components/patch_serviceaccount_test.go b/test/components/patch_serviceaccount_test.go new file mode 100644 index 000000000..52a56ce31 --- /dev/null +++ b/test/components/patch_serviceaccount_test.go @@ -0,0 +1,106 @@ +// Copyright 2020 The Lokomotive 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. + +// +build aks aws aws_edge packet +// +build e2e + +//nolint +package components + +import ( + "context" + "fmt" + "regexp" + "testing" + "time" + + _ "github.com/kinvolk/lokomotive/pkg/components/flatcar-linux-update-operator" + testutil "github.com/kinvolk/lokomotive/test/components/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" +) + +const ( + retryInterval = time.Second * 5 + timeout = time.Minute * 5 + contextTimeout = 10 +) + +type componentTestCase struct { + namespace string +} + +func TestDisableAutomountServiceAccountToken(t *testing.T) { + client := testutil.CreateKubeClient(t) + ns, _ := client.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + + var componentTestCases []componentTestCase + + for _, val := range ns.Items { + if !isSpecialNamespace(val.Name) { + componentTestCases = append(componentTestCases, componentTestCase{ + namespace: val.Name, + }) + } + } + + for _, tc := range componentTestCases { + tc := tc + t.Run(tc.namespace, func(t *testing.T) { + t.Parallel() + + if err := wait.PollImmediate( + retryInterval, timeout, checkDefaultServiceAccountPatch(client, tc), + ); err != nil { + t.Fatalf("%v", err) + } + }) + } +} + +func checkDefaultServiceAccountPatch(client kubernetes.Interface, tc componentTestCase) wait.ConditionFunc { + return func() (done bool, err error) { + ctx, cancel := context.WithTimeout(context.Background(), contextTimeout*time.Second) + defer cancel() + + sa, err := client.CoreV1().ServiceAccounts(tc.namespace).Get(ctx, "default", metav1.GetOptions{}) + if err != nil { + return false, fmt.Errorf("error getting service account: %v", err) + } + + automountServiceAccountToken := *sa.AutomountServiceAccountToken + + if automountServiceAccountToken != false { + //nolint:lll + return false, fmt.Errorf("service account for namespace %q was not patched. Expected %v got %v", tc.namespace, false, automountServiceAccountToken) + } + + return true, nil + } +} + +func isSpecialNamespace(ns string) bool { + if ns == "kube-system" || ns == "kube-public" || ns == "kube-node-lease" { + return true + } + + // check for metadata-access-test by calico + matched, _ := regexp.Match(`metadata-access-test-.*`, []byte(ns)) + if matched { + return true + } + + return false +}