diff --git a/pkg/server/handlers/handlers_test.go b/pkg/server/handlers/handlers_test.go new file mode 100644 index 000000000000..99d9b4c23886 --- /dev/null +++ b/pkg/server/handlers/handlers_test.go @@ -0,0 +1,924 @@ +package handlers + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/golang/mock/gomock" + "github.com/k3s-io/k3s/pkg/authenticator" + "github.com/k3s-io/k3s/pkg/cli/cmds" + "github.com/k3s-io/k3s/pkg/daemons/config" + testutil "github.com/k3s-io/k3s/tests" + "github.com/k3s-io/k3s/tests/mock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + certutil "github.com/rancher/dynamiclistener/cert" + "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/user" +) + +func Test_UnitHandlers(t *testing.T) { + _, reporterConfig := GinkgoConfiguration() + reporterConfig.Verbose = testing.Verbose() + RegisterFailHandler(Fail) + RunSpecs(t, "Handlers Suite", reporterConfig) +} + +func init() { + logrus.SetLevel(logrus.DebugLevel) +} + +func withClientCert(req *http.Request, certFile string) { + bytes, err := os.ReadFile(certFile) + if err != nil { + panic(err) + } + certs, err := certutil.ParseCertsPEM(bytes) + if err != nil { + panic(err) + } + req.TLS = &tls.ConnectionState{ + PeerCertificates: certs, + } +} + +func withNewClientCert(req *http.Request, caCertFile, caKeyFile, signingKeyFile string, certConfig certutil.Config) { + caCerts, caKey, err := getCACertAndKey(caCertFile, caKeyFile) + if err != nil { + panic(err) + } + keyBytes, err := os.ReadFile(signingKeyFile) + if err != nil { + panic(err) + } + key, err := certutil.ParsePrivateKeyPEM(keyBytes) + if err != nil { + panic(err) + } + cert, err := certutil.NewSignedCert(certConfig, key.(crypto.Signer), caCerts[0], caKey) + if err != nil { + panic(err) + } + + req.TLS = &tls.ConnectionState{} + req.TLS.PeerCertificates = append(req.TLS.PeerCertificates, cert) + req.TLS.PeerCertificates = append(req.TLS.PeerCertificates, caCerts...) +} + +func withCertificateRequest(req *http.Request) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, key) + if err != nil { + panic(err) + } + req.Body = io.NopCloser(bytes.NewReader(csr)) +} + +var _ = Describe("Handlers", func() { + When("server is running", Ordered, ContinueOnFailure, func() { + ctx, cancel := context.WithCancel(context.Background()) + control := &config.Control{ + Token: "token", + AgentToken: "agent-token", + ServerNodeName: "k3s-server-1", + } + + BeforeAll(func() { + os.Setenv("NODE_NAME", control.ServerNodeName) + control.DataDir = GinkgoT().TempDir() + testutil.GenerateRuntime(control) + + // add dummy handler for tunnel/proxy CONNECT requests, since we're not + // setting up a whole remotedialer tunnel server here + control.Runtime.Tunnel = http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {}) + + // wire up mock controllers and cache stores + secretStore := &mock.SecretStore{} + nodeStore := &mock.NodeStore{} + nodeStore.Create(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: control.ServerNodeName}}) + nodeStore.Create(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "k3s-agent-1"}}) + + ctrl := gomock.NewController(GinkgoT()) + coreFactory := mock.NewCoreFactory(ctrl) + coreFactory.CoreMock.V1Mock.SecretMock.EXPECT().Cache().AnyTimes().Return(coreFactory.CoreMock.V1Mock.SecretCache) + coreFactory.CoreMock.V1Mock.SecretMock.EXPECT().Create(gomock.Any()).AnyTimes().DoAndReturn(secretStore.Create) + coreFactory.CoreMock.V1Mock.SecretCache.EXPECT().Get(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(secretStore.Get) + coreFactory.CoreMock.V1Mock.NodeMock.EXPECT().Cache().AnyTimes().Return(coreFactory.CoreMock.V1Mock.NodeCache) + coreFactory.CoreMock.V1Mock.NodeCache.EXPECT().Get(gomock.Any()).AnyTimes().DoAndReturn(nodeStore.Get) + control.Runtime.Core = coreFactory + + // add authenticator + auth, err := authenticator.FromArgs([]string{ + "--basic-auth-file=" + control.Runtime.PasswdFile, + "--client-ca-file=" + control.Runtime.ClientCA, + }) + Expect(err).ToNot(HaveOccurred()) + control.Runtime.Authenticator = auth + + // finally, bind request handlers + control.Runtime.Handler = NewHandler(ctx, control, &cmds.Server{}) + }) + + AfterAll(func() { + os.Unsetenv("NODE_NAME") + testutil.CleanupDataDir(control) + cancel() + }) + + type sub struct { + name string + prepare func(control *config.Control, req *http.Request) + match func(control *config.Control) types.GomegaMatcher + } + + genericFailures := []sub{ + { + name: "anonymous", + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusForbidden) + }, + }, { + name: "bad basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("server", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusUnauthorized) + }, + }, { + name: "valid cert but untrusted CA", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ServerCA, control.Runtime.ServerCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusUnauthorized) + }, + }, { + name: "valid cert but no RBAC", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:monitoring", + Organization: []string{user.MonitoringGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusForbidden) + }, + }, + } + + tests := []struct { + method string + path string + subs []sub + }{ + //** paths accessible with node cert or agent token, and specific headers ** + { + method: http.MethodGet, + path: "/v1-k3s/serving-kubelet.crt", + subs: append(genericFailures, + sub{ + name: "valid basic but missing headers", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusBadRequest) + }, + }, + sub{ + name: "valid cert but missing headers", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusBadRequest) + }, + }, + sub{ + name: "valid cert but wrong node name", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:k3s-agent-1", + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusBadRequest) + }, + }, + sub{ + name: "valid cert but nonexistent node", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", "nonexistent") + req.Header.Add("k3s-Node-Password", "password") + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:nonexistent", + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusUnauthorized) + }, + }, + sub{ + name: "valid basic legacy key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + sub{ + name: "valid cert legacy key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + sub{ + name: "valid basic different node", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", "k3s-agent-1") + req.Header.Add("k3s-Node-Password", "password") + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + sub{ + name: "valid basic bad node password", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", "k3s-agent-1") + req.Header.Add("k3s-Node-Password", "invalid-password") + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusForbidden), + ) + }, + }, + ), + }, { + method: http.MethodPost, + path: "/v1-k3s/serving-kubelet.crt", + subs: append(genericFailures, + sub{ + name: "valid basic client key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + withCertificateRequest(req) + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + sub{ + name: "valid cert client key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + withCertificateRequest(req) + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/client-kubelet.crt", + subs: append(genericFailures, + sub{ + name: "valid basic but missing headers", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusBadRequest) + }, + }, + sub{ + name: "valid cert but missing headers", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusBadRequest) + }, + }, + sub{ + name: "valid basic legacy key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + sub{ + name: "valid cert legacy key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + ), + }, { + method: http.MethodPost, + path: "/v1-k3s/client-kubelet.crt", + subs: append(genericFailures, + sub{ + name: "valid basic client key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + withCertificateRequest(req) + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + sub{ + name: "valid cert client key", + prepare: func(control *config.Control, req *http.Request) { + req.Header.Add("k3s-Node-Name", control.ServerNodeName) + req.Header.Add("k3s-Node-Password", "password") + withCertificateRequest(req) + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + ), + }, + //** paths accessible with node cert or agent token ** + { + method: http.MethodGet, + path: "/v1-k3s/client-kube-proxy.crt", + subs: append(genericFailures, + sub{ + name: "valid basic legacy key", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + sub{ + name: "valid cert legacy key", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + ), + }, { + method: http.MethodPost, + path: "/v1-k3s/client-kube-proxy.crt", + subs: append(genericFailures, + sub{ + name: "valid basic client key", + prepare: func(control *config.Control, req *http.Request) { + withCertificateRequest(req) + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + sub{ + name: "valid cert client key", + prepare: func(control *config.Control, req *http.Request) { + withCertificateRequest(req) + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/client-k3s-controller.crt", + subs: append(genericFailures, + sub{ + name: "valid basic legacy key", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + sub{ + name: "valid cert legacy key", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(ContainSubstring("PRIVATE KEY")), + ) + }, + }, + ), + }, { + method: http.MethodPost, + path: "/v1-k3s/client-k3s-controller.crt", + subs: append(genericFailures, + sub{ + name: "valid basic client key", + prepare: func(control *config.Control, req *http.Request) { + withCertificateRequest(req) + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + sub{ + name: "valid cert client key", + prepare: func(control *config.Control, req *http.Request) { + withCertificateRequest(req) + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(Not(ContainSubstring("PRIVATE KEY"))), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/client-ca.crt", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(control *config.Control) types.GomegaMatcher { + certs, _ := os.ReadFile(control.Runtime.ClientCA) + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(certs), + ) + }, + }, + sub{ + name: "valid cert", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + certs, _ := os.ReadFile(control.Runtime.ClientCA) + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(certs), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/server-ca.crt", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + certs, _ := os.ReadFile(control.Runtime.ServerCA) + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(certs), + ) + }, + }, + sub{ + name: "valid cert", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + certs, _ := os.ReadFile(control.Runtime.ServerCA) + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(certs), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/apiservers", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPHeaderWithValue("content-type", "application/json"), + ) + }, + }, + sub{ + name: "valid cert", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPHeaderWithValue("content-type", "application/json"), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/config", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPHeaderWithValue("content-type", "application/json"), + ) + }, + }, + sub{ + name: "valid cert", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPHeaderWithValue("content-type", "application/json"), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/readyz", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("node", control.AgentToken) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody("ok"), + ) + }, + }, + sub{ + name: "valid cert", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody("ok"), + ) + }, + }, + ), + }, + //** paths accessible with node cert ** + { + method: http.MethodGet, + path: "/v1-k3s/connect", + subs: append(genericFailures, + sub{ + name: "valid cert", + prepare: func(control *config.Control, req *http.Request) { + withNewClientCert(req, control.Runtime.ClientCA, control.Runtime.ClientCAKey, control.Runtime.ClientKubeletKey, certutil.Config{ + CommonName: "system:node:" + control.ServerNodeName, + Organization: []string{user.NodesGroup}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusOK) + }, + }, + ), + }, + //** paths accessible with server token ** + { + method: http.MethodGet, + path: "/v1-k3s/encrypt/status", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("server", control.Token) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusOK) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/encrypt/config", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("server", control.Token) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusMethodNotAllowed) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/cert/cacerts", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("server", control.Token) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusMethodNotAllowed) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/server-bootstrap", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("server", control.Token) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusBadRequest), + HaveHTTPBody(ContainSubstring("etcd disabled")), + ) + }, + }, + ), + }, { + method: http.MethodGet, + path: "/v1-k3s/token", + subs: append(genericFailures, + sub{ + name: "valid basic", + prepare: func(control *config.Control, req *http.Request) { + req.SetBasicAuth("server", control.Token) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusMethodNotAllowed) + }, + }, + ), + }, + //** paths accessible with apiserver cert ** + { + method: http.MethodConnect, + path: "/", + subs: append(genericFailures, + sub{ + name: "valid cert", + prepare: func(control *config.Control, req *http.Request) { + withClientCert(req, control.Runtime.ClientKubeAPICert) + }, + match: func(_ *config.Control) types.GomegaMatcher { + return HaveHTTPStatus(http.StatusOK) + }, + }, + ), + }, + //** paths accessible anonymously ** + { + method: http.MethodGet, + path: "/ping", + subs: []sub{ + { + name: "anonymous", + match: func(_ *config.Control) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody("pong"), + ) + }, + }, + }, + }, { + method: http.MethodGet, + path: "/cacerts", + subs: []sub{ + { + name: "anonymous", + match: func(control *config.Control) types.GomegaMatcher { + certs, _ := os.ReadFile(control.Runtime.ServerCA) + return And( + HaveHTTPStatus(http.StatusOK), + HaveHTTPBody(certs), + ) + }, + }, + }, + }, + } + + for _, tt := range tests { + Context(tt.method+" "+tt.path, func() { + for _, ss := range tt.subs { + It("handles "+ss.name+" request", func() { + req := httptest.NewRequest(tt.method, tt.path, nil) + + if ss.prepare != nil { + ss.prepare(control, req) + } + + resp := httptest.NewRecorder() + control.Runtime.Handler.ServeHTTP(resp, req) + By(fmt.Sprintf("Validating response: %s %s %s", resp.Result().Proto, resp.Result().Status, resp.Result().Header.Get("Content-Type"))) + + Expect(resp).To(ss.match(control)) + }) + } + }) + } + }) +}) diff --git a/tests/mock/core.go b/tests/mock/core.go index 71014dc94de8..26260ed73250 100644 --- a/tests/mock/core.go +++ b/tests/mock/core.go @@ -224,6 +224,16 @@ func (m *NodeStore) Create(node *v1.Node) (*v1.Node, error) { return node, nil } +func (m *NodeStore) Get(name string) (*v1.Node, error) { + if m.nodes == nil { + return nil, ErrorNotFound("node", name) + } + if node, ok := m.nodes[name]; ok { + return &node, nil + } + return nil, ErrorNotFound("node", name) +} + func (m *NodeStore) List(ls labels.Selector) ([]v1.Node, error) { nodes := []v1.Node{} if ls == nil {