PodSecurity: Initial webhook implementation

This commit is contained in:
Tim Allclair
2021-07-07 09:45:33 -07:00
parent a0d77e5763
commit 32783f7568
11 changed files with 862 additions and 54 deletions

View File

@@ -17,15 +17,33 @@ limitations under the License.
package auth
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"testing"
"time"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
apiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/component-base/featuregate"
featuregatetesting "k8s.io/component-base/featuregate/testing"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/test/integration/framework"
utiltest "k8s.io/kubernetes/test/utils"
podsecurityconfigloader "k8s.io/pod-security-admission/admission/api/load"
podsecurityserver "k8s.io/pod-security-admission/cmd/webhook/server"
podsecuritytest "k8s.io/pod-security-admission/test"
)
@@ -33,6 +51,7 @@ func TestPodSecurity(t *testing.T) {
// Enable all feature gates needed to allow all fields to be exercised
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ProcMountType, true)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WindowsHostProcessContainers, true)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AppArmor, true)()
// Ensure the PodSecurity feature is enabled
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodSecurity, true)()
// Start server
@@ -71,6 +90,43 @@ func TestPodSecurityGAOnly(t *testing.T) {
podsecuritytest.Run(t, opts)
}
func TestPodSecurityWebhook(t *testing.T) {
// Enable all feature gates needed to allow all fields to be exercised
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ProcMountType, true)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WindowsHostProcessContainers, true)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AppArmor, true)()
// The webhook should pass tests even when PodSecurity is disabled.
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodSecurity, false)()
// Start test API server.
capabilities.SetForTests(capabilities.Capabilities{AllowPrivileged: true})
testServer := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
"--anonymous-auth=false",
"--allow-privileged=true",
}, framework.SharedEtcd())
t.Cleanup(testServer.TearDownFn)
webhookAddr, err := startPodSecurityWebhook(t, testServer)
if err != nil {
t.Fatalf("Failed to start webhook server: %v", err)
}
if err := installWebhook(t, testServer.ClientConfig, webhookAddr); err != nil {
t.Fatalf("Failed to install webhook configuration: %v", err)
}
opts := podsecuritytest.Options{
ClientConfig: testServer.ClientConfig,
// Don't pass in feature-gate info, so all testcases run
// TODO
ExemptClient: nil,
ExemptNamespaces: []string{},
ExemptRuntimeClasses: []string{},
}
podsecuritytest.Run(t, opts)
}
func startPodSecurityServer(t *testing.T) *kubeapiservertesting.TestServer {
// ensure the global is set to allow privileged containers
capabilities.SetForTests(capabilities.Capabilities{AllowPrivileged: true})
@@ -84,3 +140,148 @@ func startPodSecurityServer(t *testing.T) *kubeapiservertesting.TestServer {
t.Cleanup(server.TearDownFn)
return server
}
func startPodSecurityWebhook(t *testing.T, testServer *kubeapiservertesting.TestServer) (addr string, err error) {
// listener, port, err := apiserver.CreateListener("tcp", "127.0.0.1:", net.ListenConfig{})
secureListener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
return "", err
}
insecureListener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
return "", err
}
cert, err := dynamiccertificates.NewStaticCertKeyContent("localhost", utiltest.LocalhostCert, utiltest.LocalhostKey)
if err != nil {
return "", err
}
defaultConfig, err := podsecurityconfigloader.LoadFromData(nil) // load the default
if err != nil {
return "", err
}
c := podsecurityserver.Config{
SecureServing: &apiserver.SecureServingInfo{
Listener: secureListener,
Cert: cert,
},
InsecureServing: &apiserver.DeprecatedInsecureServingInfo{
Listener: insecureListener,
},
KubeConfig: testServer.ClientConfig,
PodSecurityConfig: defaultConfig,
}
t.Logf("Starting webhook server...")
webhookServer, err := podsecurityserver.Setup(&c)
if err != nil {
return "", err
}
ctx, cancel := context.WithCancel(context.Background())
go webhookServer.Start(ctx)
t.Cleanup(cancel)
// Wait for server to be ready
t.Logf("Waiting for webhook server /readyz to be ok...")
readyz := (&url.URL{
Scheme: "http",
Host: c.InsecureServing.Listener.Addr().String(),
Path: "/readyz",
}).String()
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
resp, err := http.Get(readyz)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}); err != nil {
return "", err
}
return c.SecureServing.Listener.Addr().String(), nil
}
func installWebhook(t *testing.T, clientConfig *rest.Config, addr string) error {
client, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return fmt.Errorf("error creating client: %w", err)
}
fail := admissionregistrationv1.Fail
equivalent := admissionregistrationv1.Equivalent
none := admissionregistrationv1.SideEffectClassNone
endpoint := (&url.URL{
Scheme: "https",
Host: addr,
}).String()
// Installing Admission webhook to API server
_, err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "podsecurity-webhook.integration.test"},
Webhooks: []admissionregistrationv1.ValidatingWebhook{
{
Name: "podsecurity-webhook.integration.test",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
URL: &endpoint,
CABundle: utiltest.LocalhostCert,
},
Rules: []admissionregistrationv1.RuleWithOperations{
{
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"namespaces", "pods", "pods/ephemeralcontainers", "replicationcontrollers", "podtemplates"},
},
},
{
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"replicasets", "deployments", "statefulsets", "daemonsets"},
},
},
{
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"batch"},
APIVersions: []string{"v1"},
Resources: []string{"cronjobs", "jobs"},
},
},
},
FailurePolicy: &fail,
MatchPolicy: &equivalent,
AdmissionReviewVersions: []string{"v1"},
SideEffects: &none,
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
t.Logf("Waiting for webhook to be established...")
invalidNamespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "validation-fail",
Labels: map[string]string{
"pod-security.kubernetes.io/enforce": "invalid",
},
},
}
// Wait for the invalid namespace to be rejected.
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
_, err := client.CoreV1().Namespaces().Create(context.TODO(), invalidNamespace, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
if err != nil && apierrors.IsInvalid(err) {
return true, nil // An Invalid error indicates the webhook rejected the invalid level.
}
return false, nil
}); err != nil {
return err
}
return nil
}