diff --git a/pkg/admission/denypsalabel/admission.go b/pkg/admission/denypsalabel/admission.go new file mode 100644 index 000000000000..f8334c7ece49 --- /dev/null +++ b/pkg/admission/denypsalabel/admission.go @@ -0,0 +1,92 @@ +/* +Copyright 2020 The Kubernetes 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 denypsalabel + +import ( + "context" + "fmt" + "io" + "strings" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apiserver/pkg/admission" + "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/apis/core" +) + +const ( + // PluginName is the name of this admission controller plugin + PluginName = "DenyPSALabel" + labelPrefix = "pod-security.kubernetes.io/" +) + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + plugin := newPlugin() + return plugin, nil + }) +} + +// psaLabelDenialPlugin holds state for and implements the admission plugin. +type psaLabelDenialPlugin struct { + *admission.Handler +} + +var _ admission.Interface = &psaLabelDenialPlugin{} +var _ admission.ValidationInterface = &psaLabelDenialPlugin{} + +// newPlugin creates a new admission plugin. +func newPlugin() *psaLabelDenialPlugin { + return &psaLabelDenialPlugin{ + Handler: admission.NewHandler(admission.Create, admission.Update), + } +} + +// Validate ensures that applying PSA label to namespaces is denied +func (plug *psaLabelDenialPlugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { + if attr.GetResource().GroupResource() != core.Resource("namespaces") { + return nil + } + + if len(attr.GetSubresource()) != 0 { + return nil + } + + // if we can't convert then we don't handle this object so just return + newNS, ok := attr.GetObject().(*core.Namespace) + if !ok { + klog.V(3).Infof("Expected namespace resource, got: %v", attr.GetKind()) + return errors.NewInternalError(fmt.Errorf("Expected Namespace resource, got: %v", attr.GetKind())) + } + + if !isPSALabel(newNS) { + return nil + } + + klog.V(4).Infof("Denying use of label with %s prefix on Namespace %s", labelPrefix, newNS.Name) + return admission.NewForbidden(attr, fmt.Errorf("Use of label with %s prefix on Namespace is denied by admission control", labelPrefix)) +} + +func isPSALabel(newNS *core.Namespace) bool { + for labelName := range newNS.Labels { + if strings.HasPrefix(labelName, labelPrefix) { + return true + } + } + return false +} diff --git a/pkg/cli/cmds/server.go b/pkg/cli/cmds/server.go index cfc684f169f3..92cab3ccf444 100644 --- a/pkg/cli/cmds/server.go +++ b/pkg/cli/cmds/server.go @@ -109,6 +109,7 @@ type Server struct { EtcdS3Timeout time.Duration EtcdS3Insecure bool ServiceLBNamespace string + DenyPSALabel bool } var ( @@ -586,6 +587,12 @@ var ServerFlags = []cli.Flag{ Usage: "(flags) Customized flag for kube-cloud-controller-manager process", Value: &ServerConfig.ExtraCloudControllerArgs, }, + &cli.BoolFlag{ + Name: "deny-psa-label", + Usage: "(experimental) Deny use of pod-security.kubernetes.io labels on Namespaces", + Hidden: true, + Destination: &ServerConfig.DenyPSALabel, + }, } func NewServerCommand(action func(*cli.Context) error) cli.Command { diff --git a/pkg/cli/server/server.go b/pkg/cli/server/server.go index 698d04ec49e3..fd73c1a9bd25 100644 --- a/pkg/cli/server/server.go +++ b/pkg/cli/server/server.go @@ -180,6 +180,7 @@ func run(app *cli.Context, cfg *cmds.Server, leaderControllers server.CustomCont serverConfig.ControlConfig.SupervisorMetrics = cfg.SupervisorMetrics serverConfig.ControlConfig.VLevel = cmds.LogConfig.VLevel serverConfig.ControlConfig.VModule = cmds.LogConfig.VModule + serverConfig.ControlConfig.DenyPSALabel = cfg.DenyPSALabel if !cfg.EtcdDisableSnapshots || cfg.ClusterReset { serverConfig.ControlConfig.EtcdSnapshotCompress = cfg.EtcdSnapshotCompress diff --git a/pkg/daemons/config/types.go b/pkg/daemons/config/types.go index 93e354e1962c..57bc84ea17a9 100644 --- a/pkg/daemons/config/types.go +++ b/pkg/daemons/config/types.go @@ -245,6 +245,7 @@ type Control struct { ServerNodeName string VLevel int VModule string + DenyPSALabel bool BindAddress string SANs []string diff --git a/pkg/daemons/control/server.go b/pkg/daemons/control/server.go index 993bb2cfc591..c63a0bad3ef3 100644 --- a/pkg/daemons/control/server.go +++ b/pkg/daemons/control/server.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/k3s-io/k3s/pkg/admission/denypsalabel" "github.com/k3s-io/k3s/pkg/authenticator" "github.com/k3s-io/k3s/pkg/cluster" "github.com/k3s-io/k3s/pkg/daemons/config" @@ -20,8 +21,10 @@ import ( "github.com/sirupsen/logrus" authorizationv1 "k8s.io/api/authorization/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" logsapi "k8s.io/component-base/logs/api/v1" "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" + "k8s.io/kubernetes/pkg/kubeapiserver/options" "k8s.io/kubernetes/pkg/registry/core/node" // for client metric registration @@ -228,6 +231,13 @@ func apiServer(ctx context.Context, cfg *config.Control) error { args := config.GetArgs(argsMap, cfg.ExtraAPIArgs) + // Add extra admission plugins + if cfg.DenyPSALabel { + extraPlugins := make(map[string]func(*admission.Plugins)) + extraPlugins[denypsalabel.PluginName] = denypsalabel.Register + options.AdmissionPlugins = extraPlugins + } + logrus.Infof("Running kube-apiserver %s", config.ArgString(args)) return executor.APIServer(ctx, runtime.ETCDReady, args) diff --git a/tests/integration/startup/startup_int_test.go b/tests/integration/startup/startup_int_test.go index e0f23f916db3..b272eca4753c 100644 --- a/tests/integration/startup/startup_int_test.go +++ b/tests/integration/startup/startup_int_test.go @@ -365,7 +365,32 @@ var _ = Describe("startup tests", Ordered, func() { Expect(testutil.K3sCleanup(-1, "")).To(Succeed()) }) }) - + When("a server with a --deny-psa-label is created", func() { + It("is created with no arguments", func() { + var err error + startupServerArgs = []string{ + "--deny-psa-label", + "--kube-apiserver-arg", + "enable-admission-plugins=DenyPSALabel", + } + startupServer, err = testutil.K3sStartServer(startupServerArgs...) + Expect(err).ToNot(HaveOccurred()) + }) + It("has the default pods deployed", func() { + Eventually(func() error { + return testutil.K3sDefaultDeployments() + }, "120s", "5s").Should(Succeed()) + }) + It("change label of namespace", func() { + res, err := testutil.K3sCmd("kubectl label --dry-run=server --overwrite ns --all pod-security.kubernetes.io/enforce=baseline") + Expect(err).To(HaveOccurred()) + Expect(res).To(ContainSubstring("denying use of PSA label on namespace")) + }) + It("dies cleanly", func() { + Expect(testutil.K3sKillServer(startupServer)).To(Succeed()) + Expect(testutil.K3sCleanup(-1, "")).To(Succeed()) + }) + }) }) var failed bool