From e64c0298f27961bcf7466030c28be6ee796ab680 Mon Sep 17 00:00:00 2001 From: Erik Wilson Date: Tue, 23 Apr 2019 10:53:06 -0700 Subject: [PATCH] Add cert per-node password authentication --- pkg/agent/config/config.go | 30 ++++++++++++++--- pkg/server/router.go | 68 ++++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/pkg/agent/config/config.go b/pkg/agent/config/config.go index 1c0ba40144..4acf6d6d5b 100644 --- a/pkg/agent/config/config.go +++ b/pkg/agent/config/config.go @@ -3,7 +3,9 @@ package config import ( "bufio" "context" + cryptorand "crypto/rand" "crypto/tls" + "encoding/hex" "encoding/pem" "fmt" "io/ioutil" @@ -56,7 +58,7 @@ func Request(path string, info *clientaccess.Info, requester HTTPRequester) ([]b return requester(u.String(), clientaccess.GetHTTPClient(info.CACerts), username, password) } -func getNodeNamedCrt(nodeName string) HTTPRequester { +func getNodeNamedCrt(nodeName, nodePasswordFile string) HTTPRequester { return func(u string, client *http.Client, username, password string) ([]byte, error) { req, err := http.NewRequest(http.MethodGet, u, nil) if err != nil { @@ -68,6 +70,12 @@ func getNodeNamedCrt(nodeName string) HTTPRequester { } req.Header.Set("K3s-Node-Name", nodeName) + nodePassword, err := ensureNodePassword(nodePasswordFile) + if err != nil { + return nil, err + } + req.Header.Set("K3s-Node-Password", hex.EncodeToString(nodePassword)) + resp, err := client.Do(req) if err != nil { return nil, err @@ -82,8 +90,20 @@ func getNodeNamedCrt(nodeName string) HTTPRequester { } } -func getNodeCert(nodeName, nodeCertFile, nodeKeyFile string, info *clientaccess.Info) (*tls.Certificate, error) { - nodeCert, err := Request("/v1-k3s/node.crt", info, getNodeNamedCrt(nodeName)) +func ensureNodePassword(nodePasswordFile string) ([]byte, error) { + if _, err := os.Stat(nodePasswordFile); err == nil { + return ioutil.ReadFile(nodePasswordFile) + } + password := make([]byte, 16, 16) + _, err := cryptorand.Read(password) + if err != nil { + return nil, err + } + return password, ioutil.WriteFile(nodePasswordFile, password, 0600) +} + +func getNodeCert(nodeName, nodeCertFile, nodeKeyFile, nodePasswordFile string, info *clientaccess.Info) (*tls.Certificate, error) { + nodeCert, err := Request("/v1-k3s/node.crt", info, getNodeNamedCrt(nodeName, nodePasswordFile)) if err != nil { return nil, err } @@ -233,7 +253,9 @@ func get(envInfo *cmds.Agent) (*config.Node, error) { nodeCertFile := filepath.Join(envInfo.DataDir, "token-node.crt") nodeKeyFile := filepath.Join(envInfo.DataDir, "token-node.key") - nodeCert, err := getNodeCert(nodeName, nodeCertFile, nodeKeyFile, info) + nodePasswordFile := filepath.Join(envInfo.DataDir, "node-password.bin") + + nodeCert, err := getNodeCert(nodeName, nodeCertFile, nodeKeyFile, nodePasswordFile, info) if err != nil { return nil, err } diff --git a/pkg/server/router.go b/pkg/server/router.go index 7665e78693..84c3a739aa 100644 --- a/pkg/server/router.go +++ b/pkg/server/router.go @@ -1,13 +1,18 @@ package server import ( + "bufio" "crypto/rsa" "crypto/x509" + "errors" + "fmt" "io/ioutil" "net" "net/http" + "os" "path/filepath" "strconv" + "strings" "github.com/gorilla/mux" "github.com/rancher/k3s/pkg/daemons/config" @@ -67,10 +72,21 @@ func nodeCrt(server *config.Control) http.Handler { return } - var nodeName string nodeNames := req.Header["K3s-Node-Name"] - if len(nodeNames) == 1 { - nodeName = nodeNames[0] + if len(nodeNames) != 1 || nodeNames[0] == "" { + sendError(errors.New("node name not set"), resp) + return + } + + nodePasswords := req.Header["K3s-Node-Password"] + if len(nodePasswords) != 1 || nodePasswords[0] == "" { + sendError(errors.New("node password not set"), resp) + return + } + + if err := ensureNodePassword(server.Runtime.PasswdFile, nodeNames[0], nodePasswords[0]); err != nil { + sendError(err, resp, http.StatusForbidden) + return } nodeKey, err := ioutil.ReadFile(server.Runtime.NodeKey) @@ -119,7 +135,7 @@ func nodeCrt(server *config.Control) http.Handler { CommonName: "kubernetes", Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, AltNames: certutil.AltNames{ - DNSNames: []string{"kubernetes.default.svc", "kubernetes.default", "kubernetes", "localhost", nodeName}, + DNSNames: []string{"kubernetes.default.svc", "kubernetes.default", "kubernetes", "localhost", nodeNames[0]}, IPs: []net.IP{apiServerServiceIP, net.ParseIP("127.0.0.1")}, }, } @@ -190,8 +206,48 @@ func serveStatic(urlPrefix, staticDir string) http.Handler { return http.StripPrefix(urlPrefix, http.FileServer(http.Dir(staticDir))) } -func sendError(err error, resp http.ResponseWriter) { +func sendError(err error, resp http.ResponseWriter, status ...int) { + code := http.StatusInternalServerError + if len(status) == 1 { + code = status[0] + } + logrus.Error(err) - resp.WriteHeader(http.StatusInternalServerError) + resp.WriteHeader(code) resp.Write([]byte(err.Error())) } + +func ensureNodePassword(passwdFile, nodeName, passwd string) error { + f, err := os.Open(passwdFile) + if err != nil { + return err + } + defer f.Close() + user := strings.ToLower("node:" + nodeName) + + buf := &strings.Builder{} + scan := bufio.NewScanner(f) + for scan.Scan() { + line := scan.Text() + parts := strings.Split(line, ",") + if len(parts) < 4 { + continue + } + if parts[1] == user { + if parts[0] == passwd { + return nil + } + return fmt.Errorf("Node password validation failed for [%s]", nodeName) + } + buf.WriteString(line) + buf.WriteString("\n") + } + buf.WriteString(fmt.Sprintf("%s,%s,%s,system:masters\n", passwd, user, user)) + + if scan.Err() != nil { + return scan.Err() + } + + f.Close() + return ioutil.WriteFile(passwdFile, []byte(buf.String()), 0600) +}