Skip to content
This repository has been archived by the owner on Jun 29, 2022. It is now read-only.

Commit

Permalink
Don't mount default ServiceAccount in pods
Browse files Browse the repository at this point in the history
- 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 <kautilya@kinvolk.io>
  • Loading branch information
knrt10 committed Jul 20, 2020
1 parent 8d51baf commit e51053f
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 1 deletion.
5 changes: 5 additions & 0 deletions cli/cmd/cluster-apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
7 changes: 6 additions & 1 deletion cli/cmd/component-apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
54 changes: 54 additions & 0 deletions pkg/components/util/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package util

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions pkg/components/util/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
106 changes: 106 additions & 0 deletions test/components/patch_serviceaccount_test.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit e51053f

Please sign in to comment.