Skip to content

Commit

Permalink
Merge pull request kubevirt#8649 from acardace/prevent-postcopy
Browse files Browse the repository at this point in the history
Run VMs as 'restricted' PSA pods
  • Loading branch information
kubevirt-bot authored Dec 23, 2022
2 parents ebe24b8 + bd7e4d4 commit 0aec83c
Show file tree
Hide file tree
Showing 90 changed files with 1,815 additions and 1,303 deletions.
9 changes: 0 additions & 9 deletions automation/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,6 @@ else
export KUBEVIRT_PROVIDER=${TARGET}
fi

if [[ $TARGET =~ psa ]]; then
export KUBEVIRT_DEPLOY_CDI=false
if [[ -z $FEATURE_GATES ]]; then
export FEATURE_GATES="PSA"
else
export FEATURE_GATES="${FEATURE_GATES},PSA"
fi
fi

# Single-node single-replica test lanes need nfs csi to run sig-storage tests
if [[ $KUBEVIRT_NUM_NODES = "1" && $KUBEVIRT_INFRA_REPLICAS = "1" ]]; then
export KUBEVIRT_DEPLOY_NFS_CSI=true
Expand Down
3 changes: 3 additions & 0 deletions cmd/virt-launcher/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ xattrs(
"/usr/bin/virt-launcher": [
"cap_net_bind_service",
],
"/usr/bin/virt-launcher-monitor": [
"cap_net_bind_service",
],
},
tar = ":virt-launcher-tar",
)
Expand Down
9 changes: 7 additions & 2 deletions pkg/container-disk/container-disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ func generateContainerFromVolume(vmi *v1.VirtualMachineInstance, imageIDs map[st
log.Log.Object(vmi).Infof("arguments for container-disk \"%s\": --copy-path %s", name, copyPathArg)
}

noPrivilegeEscalation := false
nonRoot := true
var userId int64 = util.NonRootUID

Expand All @@ -337,8 +338,12 @@ func generateContainerFromVolume(vmi *v1.VirtualMachineInstance, imageIDs map[st
},
Resources: resources,
SecurityContext: &kubev1.SecurityContext{
RunAsUser: &userId,
RunAsNonRoot: &nonRoot,
RunAsUser: &userId,
RunAsNonRoot: &nonRoot,
AllowPrivilegeEscalation: &noPrivilegeEscalation,
Capabilities: &kubev1.Capabilities{
Drop: []kubev1.Capability{"ALL"},
},
},
}

Expand Down
21 changes: 21 additions & 0 deletions pkg/container-disk/container-disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,27 @@ var _ = Describe("ContainerDisk", func() {
Entry("image with registry and shasum and custom port and group", "myregistry.io:5000/mygroup/myimage@sha256:123534", "myregistry.io:5000/mygroup/myimage"),
)
})

Context("when generating the container", func() {
DescribeTable("when generating the container", func(testFunc func(*k8sv1.Container)) {
vmi := api.NewMinimalVMI("myvmi")
appendContainerDisk(vmi, "disk1")

pod := createMigrationSourcePod(vmi)
imageIDs, err := ExtractImageIDsFromSourcePod(vmi, pod)
Expect(err).ToNot(HaveOccurred())

newContainers := GenerateContainers(vmi, imageIDs, "a-name", "something")
testFunc(&newContainers[0])
},
Entry("AllowPrivilegeEscalation should be false", func(c *k8sv1.Container) {
Expect(*c.SecurityContext.AllowPrivilegeEscalation).To(BeFalse())
}),
Entry("all capabilities should be dropped", func(c *k8sv1.Container) {
Expect(c.SecurityContext.Capabilities.Drop).To(Equal([]k8sv1.Capability{"ALL"}))
}),
)
})
})
})

Expand Down
24 changes: 24 additions & 0 deletions pkg/psa/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["psa.go"],
importpath = "kubevirt.io/kubevirt/pkg/psa",
visibility = ["//visibility:public"],
deps = [
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = ["psa_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/github.com/onsi/ginkgo/v2:go_default_library",
"//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)
44 changes: 44 additions & 0 deletions pkg/psa/psa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2022 Red Hat, Inc.
*
*/

package psa

import (
"fmt"

k8sv1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
)

const PSALabel = "pod-security.kubernetes.io/enforce"

func IsNamespacePrivilegedWithStore(namespaceStore cache.Store, namespace string) (bool, error) {
obj, exists, err := namespaceStore.GetByKey(namespace)
if err != nil {
return false, fmt.Errorf("failed to get namespace, %w", err)
}
if !exists {
return false, fmt.Errorf("namespace %s not observed, %w", namespace, err)
}
return IsNamespacePrivileged(obj.(*k8sv1.Namespace)), nil
}

func IsNamespacePrivileged(namespace *k8sv1.Namespace) bool {
enforceLevel, labelExist := namespace.Labels[PSALabel]
return labelExist && enforceLevel == "privileged"
}
40 changes: 40 additions & 0 deletions pkg/psa/psa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package psa

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
k8sv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ = Describe("PSA", func() {
var (
privilegedNamespace *k8sv1.Namespace
restrictedNamespace *k8sv1.Namespace
)

BeforeEach(func() {
privilegedNamespace = newNamespace("privileged")
restrictedNamespace = newNamespace("restricted")
})

Context("should report correct PSA level", func() {
DescribeTable("when inspecting namespace", func(namespace *k8sv1.Namespace, privileged bool) {
Expect(IsNamespacePrivileged(namespace)).To(Equal(privileged))
},
Entry("privileged", privilegedNamespace, true),
Entry("restricted", restrictedNamespace, false),
Entry("with no label", &k8sv1.Namespace{}, false),
)
})
})

func newNamespace(level string) *k8sv1.Namespace {
return &k8sv1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
PSALabel: level,
},
},
}
}
2 changes: 1 addition & 1 deletion pkg/virt-api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ func (app *virtAPIApp) registerValidatingWebhooks(informers *webhooks.Informers)
validating_webhook.ServePodEvictionInterceptor(w, r, app.clusterConfig, app.virtCli)
})
http.HandleFunc(components.MigrationPolicyCreateValidatePath, func(w http.ResponseWriter, r *http.Request) {
validating_webhook.ServeMigrationPolicies(w, r, app.virtCli)
validating_webhook.ServeMigrationPolicies(w, r, app.clusterConfig, app.virtCli)
})
http.HandleFunc(components.VMCloneCreateValidatePath, func(w http.ResponseWriter, r *http.Request) {
validating_webhook.ServeVirtualMachineClones(w, r, app.clusterConfig, app.virtCli)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ go_library(
"//pkg/hooks:go_default_library",
"//pkg/instancetype:go_default_library",
"//pkg/network/link:go_default_library",
"//pkg/psa:go_default_library",
"//pkg/storage/snapshot:go_default_library",
"//pkg/storage/types:go_default_library",
"//pkg/util/hardware:go_default_library",
Expand Down Expand Up @@ -94,6 +95,7 @@ go_test(
deps = [
"//pkg/hooks:go_default_library",
"//pkg/instancetype:go_default_library",
"//pkg/psa:go_default_library",
"//pkg/testutils:go_default_library",
"//pkg/util/webhooks:go_default_library",
"//pkg/virt-api/webhooks:go_default_library",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
package admitters

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

virtconfig "kubevirt.io/kubevirt/pkg/virt-config"

k8sfield "k8s.io/apimachinery/pkg/util/validation/field"

"kubevirt.io/api/migrations"
Expand All @@ -34,18 +37,21 @@ import (

"kubevirt.io/client-go/kubecli"

"kubevirt.io/kubevirt/pkg/psa"
webhookutils "kubevirt.io/kubevirt/pkg/util/webhooks"
)

// MigrationPolicyAdmitter validates VirtualMachineSnapshots
type MigrationPolicyAdmitter struct {
Client kubecli.KubevirtClient
ClusterConfig *virtconfig.ClusterConfig
Client kubecli.KubevirtClient
}

// NewMigrationPolicyAdmitter creates a MigrationPolicyAdmitter
func NewMigrationPolicyAdmitter(client kubecli.KubevirtClient) *MigrationPolicyAdmitter {
func NewMigrationPolicyAdmitter(clusterConfig *virtconfig.ClusterConfig, client kubecli.KubevirtClient) *MigrationPolicyAdmitter {
return &MigrationPolicyAdmitter{
Client: client,
ClusterConfig: clusterConfig,
Client: client,
}
}

Expand Down Expand Up @@ -91,6 +97,21 @@ func (admitter *MigrationPolicyAdmitter) Admit(ar *admissionv1.AdmissionReview)
}
}

if spec.AllowPostCopy != nil && *spec.AllowPostCopy {
namespace, err := admitter.Client.CoreV1().Namespaces().Get(context.Background(), policy.Namespace, metav1.GetOptions{})
if err != nil {
return webhookutils.ToAdmissionResponseError(err)
}

if !admitter.ClusterConfig.PSASeccompAllowsUserfaultfd() && !psa.IsNamespacePrivileged(namespace) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: "PostCopy is not allowed if the namespace is unprivileged",
Field: sourceField.Child("allowPostCopy").String(),
})
}
}

if len(causes) > 0 {
return webhookutils.ToAdmissionResponse(causes)
}
Expand Down
Loading

0 comments on commit 0aec83c

Please sign in to comment.