diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index 0b686c96..9c8bc524 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -17,7 +17,7 @@ services: volumes: - dnsconfig:/root/config/dnsconfig - sqldata:/root/data - - shared_certs:/etc/netmaker + - mosquitto_data:/etc/netmaker environment: SERVER_NAME: "broker.NETMAKER_BASE_DOMAIN" SERVER_HOST: "SERVER_PUBLIC_IP" @@ -39,7 +39,7 @@ services: VERBOSITY: "1" MANAGE_IPTABLES: "on" PORT_FORWARD_SERVICES: "dns" - MQ_ADMIN_PASSWORD: "MQ_ADMIN_PASSWORD" + MQ_ADMIN_PASSWORD: "REPLACE_MQ_ADMIN_PASSWORD" ports: - "51821-51830:51821-51830/udp" expose: @@ -115,7 +115,6 @@ services: NETMAKER_SERVER_HOST: "api.NETMAKER_BASE_DOMAIN" volumes: - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf - - /root/dynamic-security.json:/mosquitto/config/dynamic-security.json - /root/wait.sh:/mosquitto/config/wait.sh - mosquitto_data:/mosquitto/data - mosquitto_logs:/mosquitto/log @@ -130,7 +129,6 @@ services: - traefik.tcp.routers.mqtts.entrypoints=websecure volumes: traefik_certs: {} - shared_certs: {} sqldata: {} dnsconfig: {} mosquitto_data: {} diff --git a/controllers/node.go b/controllers/node.go index 0a88679c..052c2c8e 100644 --- a/controllers/node.go +++ b/controllers/node.go @@ -67,75 +67,94 @@ func authenticate(response http.ResponseWriter, request *http.Request) { decoderErr.Error()) logic.ReturnErrorResponse(response, request, errorResponse) return - } else { - errorResponse.Code = http.StatusBadRequest - if authRequest.ID == "" { - errorResponse.Message = "W1R3: ID can't be empty" - logger.Log(0, request.Header.Get("user"), errorResponse.Message) - logic.ReturnErrorResponse(response, request, errorResponse) - return - } else if authRequest.Password == "" { - errorResponse.Message = "W1R3: Password can't be empty" - logger.Log(0, request.Header.Get("user"), errorResponse.Message) - logic.ReturnErrorResponse(response, request, errorResponse) - return - } else { - var err error - result, err = logic.GetNodeByID(authRequest.ID) - - if err != nil { - errorResponse.Code = http.StatusBadRequest - errorResponse.Message = err.Error() - logger.Log(0, request.Header.Get("user"), - fmt.Sprintf("failed to get node info [%s]: %v", authRequest.ID, err)) - logic.ReturnErrorResponse(response, request, errorResponse) - return - } - - err = bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(authRequest.Password)) - if err != nil { - errorResponse.Code = http.StatusBadRequest - errorResponse.Message = err.Error() - logger.Log(0, request.Header.Get("user"), - "error validating user password: ", err.Error()) - logic.ReturnErrorResponse(response, request, errorResponse) - return - } else { - tokenString, err := logic.CreateJWT(authRequest.ID, authRequest.MacAddress, result.Network) - - if tokenString == "" { - errorResponse.Code = http.StatusBadRequest - errorResponse.Message = "Could not create Token" - logger.Log(0, request.Header.Get("user"), - fmt.Sprintf("%s: %v", errorResponse.Message, err)) - logic.ReturnErrorResponse(response, request, errorResponse) - return - } - - var successResponse = models.SuccessResponse{ - Code: http.StatusOK, - Message: "W1R3: Device " + authRequest.ID + " Authorized", - Response: models.SuccessfulLoginResponse{ - AuthToken: tokenString, - ID: authRequest.ID, - }, - } - successJSONResponse, jsonError := json.Marshal(successResponse) - - if jsonError != nil { - errorResponse.Code = http.StatusBadRequest - errorResponse.Message = err.Error() - logger.Log(0, request.Header.Get("user"), - "error marshalling resp: ", err.Error()) - logic.ReturnErrorResponse(response, request, errorResponse) - return - } - response.WriteHeader(http.StatusOK) - response.Header().Set("Content-Type", "application/json") - response.Write(successJSONResponse) - } - } } + errorResponse.Code = http.StatusBadRequest + if authRequest.ID == "" { + errorResponse.Message = "W1R3: ID can't be empty" + logger.Log(0, request.Header.Get("user"), errorResponse.Message) + logic.ReturnErrorResponse(response, request, errorResponse) + return + } else if authRequest.Password == "" { + errorResponse.Message = "W1R3: Password can't be empty" + logger.Log(0, request.Header.Get("user"), errorResponse.Message) + logic.ReturnErrorResponse(response, request, errorResponse) + return + } + var err error + result, err = logic.GetNodeByID(authRequest.ID) + if err != nil { + errorResponse.Code = http.StatusBadRequest + errorResponse.Message = err.Error() + logger.Log(0, request.Header.Get("user"), + fmt.Sprintf("failed to get node info [%s]: %v", authRequest.ID, err)) + logic.ReturnErrorResponse(response, request, errorResponse) + return + } + + err = bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(authRequest.Password)) + if err != nil { + errorResponse.Code = http.StatusBadRequest + errorResponse.Message = err.Error() + logger.Log(0, request.Header.Get("user"), + "error validating user password: ", err.Error()) + logic.ReturnErrorResponse(response, request, errorResponse) + return + } + event := mq.DynSecAction{ + ActionType: mq.CreateClient, + Payload: mq.MqDynsecPayload{ + Commands: []mq.MqDynSecCmd{ + { + Command: mq.CreateClientCmd, + Username: result.ID, + Password: authRequest.Password, + Textname: result.Name, + Roles: make([]mq.MqDynSecRole, 0), + Groups: make([]mq.MqDynSecGroup, 0), + }, + }, + }, + } + if err := mq.PublishEventToDynSecTopic(event); err != nil { + logger.Log(0, fmt.Sprintf("failed to send DynSec command [%s]: %v", + event.ActionType, err.Error())) + errorResponse.Code = http.StatusInternalServerError + errorResponse.Message = fmt.Sprintf("could not create mq client for node [%s]: %v", result.ID, err) + return + } + + tokenString, err := logic.CreateJWT(authRequest.ID, authRequest.MacAddress, result.Network) + if tokenString == "" { + errorResponse.Code = http.StatusBadRequest + errorResponse.Message = "Could not create Token" + logger.Log(0, request.Header.Get("user"), + fmt.Sprintf("%s: %v", errorResponse.Message, err)) + logic.ReturnErrorResponse(response, request, errorResponse) + return + } + + var successResponse = models.SuccessResponse{ + Code: http.StatusOK, + Message: "W1R3: Device " + authRequest.ID + " Authorized", + Response: models.SuccessfulLoginResponse{ + AuthToken: tokenString, + ID: authRequest.ID, + }, + } + successJSONResponse, jsonError := json.Marshal(successResponse) + + if jsonError != nil { + errorResponse.Code = http.StatusBadRequest + errorResponse.Message = err.Error() + logger.Log(0, request.Header.Get("user"), + "error marshalling resp: ", err.Error()) + logic.ReturnErrorResponse(response, request, errorResponse) + return + } + response.WriteHeader(http.StatusOK) + response.Header().Set("Content-Type", "application/json") + response.Write(successJSONResponse) + } // auth middleware for api calls from nodes where node is has not yet joined the server (register, join) diff --git a/docker/dynamic-security.json b/docker/dynamic-security.json deleted file mode 100755 index e93b1c01..00000000 --- a/docker/dynamic-security.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "clients": [{ - "username": "Netmaker-Admin", - "textName": "netmaker admin user", - "password": "T42rorlC/mAP+i19g/YqMlWShPpfo8F/nBz2ZQNRcjAnfczrgu4rIQam9z7T/87NBIHxqR1wMlCIvRN5JApHcw==", - "salt": "lHl24sEf+lJ/kFHk", - "iterations": 101, - "roles": [{ - "rolename": "admin" - }] - }, - { - "username": "netmaker-exporter", - "textName": "netmaker metrics exporter", - "password": "yl7HZglF4CvCxgjPLLIYc73LRtjEwp2/SAEQXeW5Ta1Dl4RoLN5/gjqiv8xmue+F9LfRk8KICkNbhSYuEfJ7ww==", - "salt": "veLl9eN02i+hKkyT", - "iterations": 101, - "roles": [] - }], - "roles": [{ - "rolename": "admin", - "acls": [{ - "acltype": "publishClientSend", - "topic": "$CONTROL/dynamic-security/#", - "allow": true - }, { - "acltype": "publishClientReceive", - "topic": "$CONTROL/dynamic-security/#", - "allow": true - }, { - "acltype": "subscribePattern", - "topic": "$CONTROL/dynamic-security/#", - "allow": true - }, { - "acltype": "publishClientReceive", - "topic": "$SYS/#", - "allow": true - }, { - "acltype": "subscribePattern", - "topic": "$SYS/#", - "allow": true - }, { - "acltype": "publishClientReceive", - "topic": "#", - "allow": true - }, { - "acltype": "subscribePattern", - "topic": "#", - "allow": true - }, { - "acltype": "unsubscribePattern", - "topic": "#", - "allow": true - }, - { - "acltype": "publishClientSend", - "topic": "#", - "allow": true - } - ] - }], - "defaultACLAccess": { - "publishClientSend": true, - "publishClientReceive": true, - "subscribe": true, - "unsubscribe": true - } -} diff --git a/docker/mosquitto.conf b/docker/mosquitto.conf index c09a6e3b..9131666b 100644 --- a/docker/mosquitto.conf +++ b/docker/mosquitto.conf @@ -4,5 +4,5 @@ allow_anonymous false listener 1883 allow_anonymous false plugin /usr/lib/mosquitto_dynamic_security.so -plugin_opt_config_file /mosquitto/config/dynamic-security.json +plugin_opt_config_file /mosquitto/data/dynamic-security.json diff --git a/docker/wait.sh b/docker/wait.sh index 7abe704e..cb438abc 100755 --- a/docker/wait.sh +++ b/docker/wait.sh @@ -1,6 +1,7 @@ -#!/bin/sh +#!/bin/ash wait_for_netmaker() { + echo "SERVER: ${NETMAKER_SERVER_HOST}" until curl --output /dev/null --silent --fail --head \ --location "${NETMAKER_SERVER_HOST}/api/server/health"; do echo "Waiting for netmaker server to startup" @@ -8,10 +9,15 @@ wait_for_netmaker() { done } -main() { - # wait for netmaker to startup - apk add curl - wait_for_netmaker +main(){ + # wait for netmaker to startup + apk add curl + wait_for_netmaker + echo "Starting MQ..." + # Run the main container command. + /docker-entrypoint.sh + /usr/sbin/mosquitto -c /mosquitto/config/mosquitto.conf + } main "${@}" \ No newline at end of file diff --git a/logic/accesskeys.go b/logic/accesskeys.go index 7bc75ea4..337693f9 100644 --- a/logic/accesskeys.go +++ b/logic/accesskeys.go @@ -222,6 +222,11 @@ func genKeyName() string { } func GenKey() string { + entropy, _ := rand.Int(rand.Reader, maxentropy) + return entropy.Text(16)[:16] +} + +func GenPassWord() string { entropy, _ := rand.Int(rand.Reader, maxentropy) return entropy.Text(62)[:64] } diff --git a/mq/dynsec.go b/mq/dynsec.go index ffa30908..ecdfd1d2 100644 --- a/mq/dynsec.go +++ b/mq/dynsec.go @@ -9,12 +9,88 @@ import ( "os" mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/gravitl/netmaker/functions" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" + "github.com/gravitl/netmaker/netclient/ncutils" "github.com/gravitl/netmaker/servercfg" "golang.org/x/crypto/pbkdf2" ) +var ( + dynamicSecurityFile = "dynamic-security.json" + dynConfig = dynJSON{ + Clients: []client{ + { + Username: "Netmaker-Admin", + TextName: "netmaker admin user", + Password: "", + Salt: "", + Iterations: 0, + Roles: []clientRole{ + { + Rolename: "admin", + }, + }, + }, + { + Username: "Netmaker-Server", + TextName: "netmaker server user", + Password: "", + Salt: "", + Iterations: 0, + Roles: []clientRole{}, + }, + { + Username: "netmaker-exporter", + TextName: "netmaker metrics exporter", + Password: "yl7HZglF4CvCxgjPLLIYc73LRtjEwp2/SAEQXeW5Ta1Dl4RoLN5/gjqiv8xmue+F9LfRk8KICkNbhSYuEfJ7ww==", + Salt: "veLl9eN02i+hKkyT", + Iterations: 0, + Roles: []clientRole{}, + }, + }, + Roles: []role{ + { + Rolename: "admin", + Acls: []Acl{ + { + AclType: "publishClientSend", + Topic: "$CONTROL/dynamic-security/#", + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "$CONTROL/dynamic-security/#", + Allow: true, + }, + { + AclType: "subscribePattern", + Topic: "$CONTROL/dynamic-security/#", + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "$SYS/#", + Allow: true, + }, + { + AclType: "subscribePattern", + Topic: "$SYS/#", + Allow: true, + }, + }, + }, + }, + DefaultAcl: defaultAccessAcl{ + PublishClientSend: true, + PublishClientReceive: true, + Subscribe: true, + Unsubscribe: true, + }, + } +) + const DynamicSecSubTopic = "$CONTROL/dynamic-security/#" const DynamicSecPubTopic = "$CONTROL/dynamic-security/v1" @@ -37,29 +113,32 @@ var ( ModifyClientCmd = "modifyClient" ) +type dynJSON struct { + Clients []client `json:"clients"` + Roles []role `json:"roles"` + DefaultAcl defaultAccessAcl `json:"defaultACLAccess"` +} + var ( mqAdminUserName string = "Netmaker-Admin" mqNetmakerServerUserName string = "Netmaker-Server" ) +type clientRole struct { + Rolename string `json:"rolename"` +} type client struct { - Username string `json:"username"` - TextName string `json:"textName"` - Password string `json:"password"` - Salt string `json:"salt"` - Iterations int `json:"iterations"` - Roles []struct { - Rolename string `json:"rolename"` - } `json:"roles"` + Username string `json:"username"` + TextName string `json:"textName"` + Password string `json:"password"` + Salt string `json:"salt"` + Iterations int `json:"iterations"` + Roles []clientRole `json:"roles"` } type role struct { Rolename string `json:"rolename"` - Acls []struct { - Acltype string `json:"acltype"` - Topic string `json:"topic"` - Allow bool `json:"allow"` - } `json:"acls"` + Acls []Acl `json:"acls"` } type defaultAccessAcl struct { @@ -120,34 +199,26 @@ func encodePasswordToPBKDF2(password string, salt string, iterations int, keyLen } func Configure() error { - file := "/root/dynamic-security.json" - b, err := os.ReadFile(file) - if err != nil { - return err - } - c := dynCnf{} - err = json.Unmarshal(b, &c) - if err != nil { - return err - } password := servercfg.GetMqAdminPassword() if password == "" { return errors.New("MQ admin password not provided") } - for i, cI := range c.Clients { + for i, cI := range dynConfig.Clients { if cI.Username == mqAdminUserName || cI.Username == mqNetmakerServerUserName { salt := logic.RandomString(12) hashed := encodePasswordToPBKDF2(password, salt, 101, 64) cI.Password = hashed + cI.Iterations = 101 cI.Salt = base64.StdEncoding.EncodeToString([]byte(salt)) - c.Clients[i] = cI + dynConfig.Clients[i] = cI } } - data, err := json.MarshalIndent(c, "", " ") + data, err := json.MarshalIndent(dynConfig, "", " ") if err != nil { return err } - return os.WriteFile(file, data, 0755) + path := functions.GetNetmakerPath() + ncutils.GetSeparator() + dynamicSecurityFile + return os.WriteFile(path, data, 0755) } func PublishEventToDynSecTopic(event DynSecAction) error { @@ -157,7 +228,7 @@ func PublishEventToDynSecTopic(event DynSecAction) error { return err } if token := mqAdminClient.Publish(DynamicSecPubTopic, 2, false, d); token.Error() != nil { - return err + return token.Error() } return nil } diff --git a/netclient/functions/join.go b/netclient/functions/join.go index ffc09e12..02482427 100644 --- a/netclient/functions/join.go +++ b/netclient/functions/join.go @@ -199,7 +199,7 @@ func JoinNetwork(cfg *config.ClientConfig, privateKey string) error { return err } if cfg.Node.Password == "" { - cfg.Node.Password = logic.GenKey() + cfg.Node.Password = logic.GenPassWord() } //check if ListenPort was set on command line if cfg.Node.ListenPort != 0 { diff --git a/netclient/functions/mqpublish.go b/netclient/functions/mqpublish.go index e49fd60c..46f641bc 100644 --- a/netclient/functions/mqpublish.go +++ b/netclient/functions/mqpublish.go @@ -115,7 +115,6 @@ func checkin(currentRun int) { config.Write(&nodeCfg, nodeCfg.Network) } Hello(&nodeCfg) - checkCertExpiry(&nodeCfg) if currentRun >= 5 && nodeCfg.Server.Is_EE { logger.Log(0, "collecting metrics for node", nodeCfg.Node.Name) publishMetrics(&nodeCfg) diff --git a/scripts/nm-quick.sh b/scripts/nm-quick.sh index 50bfeca7..e241108c 100755 --- a/scripts/nm-quick.sh +++ b/scripts/nm-quick.sh @@ -80,7 +80,7 @@ COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p') SERVER_PUBLIC_IP=$(curl -s ifconfig.me) MASTER_KEY=$(tr -dc A-Za-z0-9