diff --git a/compose/docker-compose.ee.yml b/compose/docker-compose.ee.yml index 0c4aba06..98466cd5 100644 --- a/compose/docker-compose.ee.yml +++ b/compose/docker-compose.ee.yml @@ -42,6 +42,7 @@ services: METRICS_EXPORTER: "on" LICENSE_KEY: "YOUR_LICENSE_KEY" NETMAKER_ACCOUNT_ID: "YOUR_ACCOUNT_ID" + MQ_ADMIN_PASSWORD: "REPLACE_MQ_ADMIN_PASSWORD" ports: - "51821-51830:51821-51830/udp" expose: @@ -112,21 +113,22 @@ services: depends_on: - netmaker restart: unless-stopped + command: ["/mosquitto/config/wait.sh"] + environment: + NETMAKER_SERVER_HOST: "api.NETMAKER_BASE_DOMAIN" volumes: - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf - - /root/mosquitto.passwords:/etc/mosquitto.passwords + - /root/wait.sh:/mosquitto/config/wait.sh - mosquitto_data:/mosquitto/data - mosquitto_logs:/mosquitto/log - - shared_certs:/mosquitto/certs expose: - "8883" labels: - traefik.enable=true - - traefik.tcp.routers.mqtts.rule=HostSNI(`broker.NETMAKER_BASE_DOMAIN`) - - traefik.tcp.routers.mqtts.tls.passthrough=true - - traefik.tcp.services.mqtts-svc.loadbalancer.server.port=8883 - - traefik.tcp.routers.mqtts.service=mqtts-svc - - traefik.tcp.routers.mqtts.entrypoints=websecure + - traefik.tcp.routers.mqtt.rule=HostSNI(`broker.NETMAKER_BASE_DOMAIN`) + - traefik.tcp.routers.mqtt.tls.certresolver=http + - traefik.tcp.services.mqtt.loadbalancer.server.port=8883 + - traefik.tcp.routers.mqtt.entrypoints=websecure prometheus: container_name: prometheus image: gravitl/netmaker-prometheus:latest diff --git a/controllers/network.go b/controllers/network.go index 6a5f3f99..6cb64d97 100644 --- a/controllers/network.go +++ b/controllers/network.go @@ -442,6 +442,21 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype)) return } + // Deletes the network role from MQ + event := mq.DynSecAction{ + Payload: mq.MqDynsecPayload{ + Commands: []mq.MqDynSecCmd{ + { + Command: mq.DeleteRoleCmd, + RoleName: network, + }, + }, + }, + } + if err := mq.PublishEventToDynSecTopic(event); err != nil { + logger.Log(0, fmt.Sprintf("failed to send DynSec command [%v]: %v", + event.Payload.Commands, err.Error())) + } logger.Log(1, r.Header.Get("user"), "deleted network", network) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode("success") @@ -488,6 +503,23 @@ func createNetwork(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } + // Create Role with acls for the network + event := mq.DynSecAction{ + Payload: mq.MqDynsecPayload{ + Commands: []mq.MqDynSecCmd{ + { + Command: mq.CreateRoleCmd, + RoleName: network.NetID, + Textname: "Network wide role with Acls for nodes", + Acls: mq.FetchNetworkAcls(network.NetID), + }, + }, + }, + } + if err := mq.PublishEventToDynSecTopic(event); err != nil { + logger.Log(0, fmt.Sprintf("failed to send DynSec command [%v]: %v", + event.Payload.Commands, err.Error())) + } if servercfg.IsClientMode() != "off" { _, err := logic.ServerJoin(&network) diff --git a/controllers/node.go b/controllers/node.go index c32b2d69..f46857b5 100644 --- a/controllers/node.go +++ b/controllers/node.go @@ -101,23 +101,45 @@ func authenticate(response http.ResponseWriter, request *http.Request) { return } event := mq.DynSecAction{ - ActionType: mq.CreateClient, Payload: mq.MqDynsecPayload{ Commands: []mq.MqDynSecCmd{ + + { + Command: mq.CreateRoleCmd, + RoleName: result.Network, + Textname: "Network wide role with Acls for nodes", + Acls: mq.FetchNetworkAcls(result.Network), + }, + + { + Command: mq.CreateRoleCmd, + RoleName: fmt.Sprintf("%s-%s", "Node", result.ID), + Acls: mq.FetchNodeAcls(result.ID), + Textname: "Role for node " + result.Name, + }, { Command: mq.CreateClientCmd, Username: result.ID, Password: authRequest.Password, Textname: result.Name, - Roles: make([]mq.MqDynSecRole, 0), - Groups: make([]mq.MqDynSecGroup, 0), + Roles: []mq.MqDynSecRole{ + { + Rolename: fmt.Sprintf("%s-%s", "Node", result.ID), + Priority: -1, + }, + { + Rolename: result.Network, + Priority: -1, + }, + }, + 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())) + logger.Log(0, fmt.Sprintf("failed to send DynSec command [%v]: %v", + event.Payload.Commands, err.Error())) errorResponse.Code = http.StatusInternalServerError errorResponse.Message = fmt.Sprintf("could not create mq client for node [%s]: %v", result.ID, err) return @@ -641,7 +663,6 @@ func createNode(w http.ResponseWriter, r *http.Request) { } // Delete Any Existing Client with this ID. event := mq.DynSecAction{ - ActionType: mq.DeleteClient, Payload: mq.MqDynsecPayload{ Commands: []mq.MqDynSecCmd{ { @@ -652,28 +673,42 @@ func createNode(w http.ResponseWriter, r *http.Request) { }, } if err := mq.PublishEventToDynSecTopic(event); err != nil { - logger.Log(0, fmt.Sprintf("failed to send DynSec command [%s]: %v", - event.ActionType, err.Error())) + logger.Log(0, fmt.Sprintf("failed to send DynSec command [%v]: %v", + event.Payload.Commands, err.Error())) } // Create client for this node in Mq event = mq.DynSecAction{ - ActionType: mq.CreateClient, Payload: mq.MqDynsecPayload{ Commands: []mq.MqDynSecCmd{ + { + Command: mq.CreateRoleCmd, + RoleName: fmt.Sprintf("%s-%s", "Node", node.ID), + Acls: mq.FetchNodeAcls(node.ID), + Textname: "Role for node " + node.Name, + }, { Command: mq.CreateClientCmd, Username: node.ID, Password: nodePassword, Textname: node.Name, - Roles: make([]mq.MqDynSecRole, 0), - Groups: make([]mq.MqDynSecGroup, 0), + Roles: []mq.MqDynSecRole{ + { + Rolename: fmt.Sprintf("%s-%s", "Node", node.ID), + Priority: -1, + }, + { + Rolename: node.Network, + Priority: -1, + }, + }, + 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())) + logger.Log(0, fmt.Sprintf("failed to send DynSec command [%v]: %v", + event.Payload.Commands, err.Error())) } response := models.NodeGet{ @@ -1013,9 +1048,12 @@ func deleteNode(w http.ResponseWriter, r *http.Request) { } event := mq.DynSecAction{ - ActionType: mq.DeleteClient, Payload: mq.MqDynsecPayload{ Commands: []mq.MqDynSecCmd{ + { + Command: mq.DeleteRoleCmd, + RoleName: fmt.Sprintf("%s-%s", "Node", nodeid), + }, { Command: mq.DeleteClientCmd, Username: nodeid, @@ -1024,8 +1062,8 @@ func deleteNode(w http.ResponseWriter, r *http.Request) { }, } if err := mq.PublishEventToDynSecTopic(event); err != nil { - logger.Log(0, fmt.Sprintf("failed to send DynSec command [%s]: %v", - event.ActionType, err.Error())) + logger.Log(0, fmt.Sprintf("failed to send DynSec command [%v]: %v", + event.Payload.Commands, err.Error())) } logic.ReturnSuccessResponse(w, r, nodeid+" deleted.") logger.Log(1, r.Header.Get("user"), "Deleted node", nodeid, "from network", params["network"]) diff --git a/docker/mosquitto-ee.conf b/docker/mosquitto-ee.conf deleted file mode 100644 index 6ee92ddc..00000000 --- a/docker/mosquitto-ee.conf +++ /dev/null @@ -1,16 +0,0 @@ -per_listener_settings true - -listener 8883 -allow_anonymous false -require_certificate true -use_identity_as_username true -cafile /mosquitto/certs/root.pem -certfile /mosquitto/certs/server.pem -keyfile /mosquitto/certs/server.key - -listener 1883 -allow_anonymous true - -listener 1884 -allow_anonymous false -password_file /etc/mosquitto.passwords diff --git a/docker/mosquitto.conf b/docker/mosquitto.conf index 924314e6..299f632f 100644 --- a/docker/mosquitto.conf +++ b/docker/mosquitto.conf @@ -7,4 +7,3 @@ allow_anonymous false plugin /usr/lib/mosquitto_dynamic_security.so plugin_opt_config_file /mosquitto/data/dynamic-security.json - diff --git a/docker/mosquitto.passwords b/docker/mosquitto.passwords deleted file mode 100644 index d6966cf3..00000000 --- a/docker/mosquitto.passwords +++ /dev/null @@ -1 +0,0 @@ -netmaker-exporter:$7$101$9kcXwXP+nUMh06gm$MND2YjtRSvcZTXjMn7xYKoqUFQxG6NOgqWmXIcxxxZksM9cA8732URQWOsPHqpGEvVF9mSVagM1MBEMIKwZm2A== diff --git a/mq/dynsec.go b/mq/dynsec.go index 8bb66f85..a0226805 100644 --- a/mq/dynsec.go +++ b/mq/dynsec.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "time" mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/gravitl/netmaker/functions" @@ -39,7 +40,11 @@ var ( Password: "", Salt: "", Iterations: 0, - Roles: []clientRole{}, + Roles: []clientRole{ + { + Rolename: "server", + }, + }, }, { Username: "netmaker-exporter", @@ -47,7 +52,11 @@ var ( Password: "yl7HZglF4CvCxgjPLLIYc73LRtjEwp2/SAEQXeW5Ta1Dl4RoLN5/gjqiv8xmue+F9LfRk8KICkNbhSYuEfJ7ww==", Salt: "veLl9eN02i+hKkyT", Iterations: 101, - Roles: []clientRole{}, + Roles: []clientRole{ + { + Rolename: "exporter", + }, + }, }, }, Roles: []role{ @@ -55,57 +64,123 @@ var ( Rolename: "admin", Acls: []Acl{ { - AclType: "publishClientSend", - Topic: "$CONTROL/dynamic-security/#", - Allow: true, + AclType: "publishClientSend", + Topic: "$CONTROL/dynamic-security/#", + Priority: -1, + Allow: true, }, + { + AclType: "publishClientReceive", + Topic: "$CONTROL/dynamic-security/#", + Priority: -1, + Allow: true, + }, + { + AclType: "subscribePattern", + Topic: "$CONTROL/dynamic-security/#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "$SYS/#", + Priority: -1, + Allow: true, + }, + { + AclType: "subscribePattern", + Topic: "$SYS/#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "#", + Priority: -1, + Allow: true, + }, + { + AclType: "subscribePattern", + Topic: "#", + Priority: -1, + Allow: true, + }, + { + AclType: "unsubscribePattern", + Topic: "#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientSend", + Topic: "#", + Priority: -1, + Allow: true, + }, + }, + }, + { + Rolename: "server", + Acls: []Acl{ + { + AclType: "publishClientSend", + Topic: "peers/#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientSend", + Topic: "update/#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientSend", + Topic: "metrics_exporter", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "ping/#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "update/#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "signal/#", + Priority: -1, + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: "metrics/#", + Priority: -1, + Allow: true, + }, + }, + }, + { + Rolename: "exporter", + Acls: []Acl{ { 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: "#", + Topic: "metrics_exporter", Allow: true, }, }, }, }, DefaultAcl: defaultAccessAcl{ - PublishClientSend: true, + PublishClientSend: false, PublishClientReceive: true, - Subscribe: true, + Subscribe: false, Unsubscribe: true, }, } @@ -114,18 +189,8 @@ var ( const DynamicSecSubTopic = "$CONTROL/dynamic-security/#" const DynamicSecPubTopic = "$CONTROL/dynamic-security/v1" -type DynSecActionType string - var mqAdminClient mqtt.Client -var ( - CreateClient DynSecActionType = "CREATE_CLIENT" - DisableClient DynSecActionType = "DISABLE_CLIENT" - EnableClient DynSecActionType = "ENABLE_CLIENT" - DeleteClient DynSecActionType = "DELETE_CLIENT" - ModifyClient DynSecActionType = "MODIFY_CLIENT" -) - var ( CreateClientCmd = "createClient" DisableClientCmd = "disableClient" @@ -133,6 +198,11 @@ var ( ModifyClientCmd = "modifyClient" ) +var ( + CreateRoleCmd = "createRole" + DeleteRoleCmd = "deleteRole" +) + type dynJSON struct { Clients []client `json:"clients"` Roles []role `json:"roles"` @@ -205,8 +275,7 @@ type MqDynSecCmd struct { } type DynSecAction struct { - ActionType DynSecActionType - Payload MqDynsecPayload + Payload MqDynsecPayload } type MqDynsecPayload struct { @@ -223,7 +292,6 @@ func Configure() error { if password == "" { return errors.New("MQ admin password not provided") } - fmt.Println("-----> PASSWORD: ", password) for i, cI := range dynConfig.Clients { if cI.Username == mqAdminUserName || cI.Username == mqNetmakerServerUserName { salt := logic.RandomString(12) @@ -248,10 +316,15 @@ func PublishEventToDynSecTopic(event DynSecAction) error { if err != nil { return err } - if token := mqAdminClient.Publish(DynamicSecPubTopic, 2, false, d); token.Error() != nil { - return token.Error() + var connecterr error + if token := mqAdminClient.Publish(DynamicSecPubTopic, 2, false, d); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil { + if token.Error() == nil { + connecterr = errors.New("connect timeout") + } else { + connecterr = token.Error() + } } - return nil + return connecterr } func watchDynSecTopic(client mqtt.Client, msg mqtt.Message) { diff --git a/mq/dynsec_helper.go b/mq/dynsec_helper.go new file mode 100644 index 00000000..2c8e1f07 --- /dev/null +++ b/mq/dynsec_helper.go @@ -0,0 +1,120 @@ +package mq + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/gravitl/netmaker/servercfg" +) + +type DynListCLientsCmdResp struct { + Responses []struct { + Command string `json:"command"` + Error string `json:"error"` + Data ListClientsData `json:"data"` + } `json:"responses"` +} + +type ListClientsData struct { + Clients []string `json:"clients"` + TotalCount int `json:"totalCount"` +} + +func GetAdminClient() (mqtt.Client, error) { + opts := mqtt.NewClientOptions() + setMqOptions(mqAdminUserName, servercfg.GetMqAdminPassword(), opts) + mqclient := mqtt.NewClient(opts) + var connecterr error + if token := mqclient.Connect(); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil { + if token.Error() == nil { + connecterr = errors.New("connect timeout") + } else { + connecterr = token.Error() + } + } + return mqclient, connecterr +} + +func ListClients(client mqtt.Client) (ListClientsData, error) { + respChan := make(chan mqtt.Message, 10) + defer close(respChan) + command := "listClients" + resp := ListClientsData{} + msg := MqDynsecPayload{ + Commands: []MqDynSecCmd{ + { + Command: command, + }, + }, + } + client.Subscribe("$CONTROL/dynamic-security/v1/response", 2, mqtt.MessageHandler(func(c mqtt.Client, m mqtt.Message) { + respChan <- m + })) + defer client.Unsubscribe() + d, _ := json.Marshal(msg) + token := client.Publish("$CONTROL/dynamic-security/v1", 2, true, d) + if !token.WaitTimeout(30) || token.Error() != nil { + var err error + if token.Error() == nil { + err = errors.New("connection timeout") + } else { + err = token.Error() + } + return resp, err + } + + for m := range respChan { + msg := DynListCLientsCmdResp{} + json.Unmarshal(m.Payload(), &msg) + for _, mI := range msg.Responses { + if mI.Command == command { + return mI.Data, nil + } + } + } + return resp, errors.New("resp not found") +} + +func FetchNetworkAcls(network string) []Acl { + return []Acl{ + { + AclType: "publishClientReceive", + Topic: fmt.Sprintf("update/%s/#", network), + Allow: true, + }, + { + AclType: "publishClientReceive", + Topic: fmt.Sprintf("peers/%s/#", network), + Allow: true, + }, + } +} + +func FetchNodeAcls(nodeID string) []Acl { + return []Acl{ + + { + AclType: "publishClientSend", + Topic: fmt.Sprintf("signal/%s", nodeID), + Allow: true, + }, + { + AclType: "publishClientSend", + Topic: fmt.Sprintf("update/%s", nodeID), + Allow: true, + }, + { + AclType: "publishClientSend", + Topic: fmt.Sprintf("ping/%s", nodeID), + Allow: true, + }, + { + AclType: "publishClientSend", + Topic: fmt.Sprintf("metrics/%s", nodeID), + Allow: true, + }, + } +}