diff --git a/Makefile b/Makefile
index f6138eadb993f55c60dbeb5c78c9d37e96289819..dff0fd076e8e93a7fd56a3f904ce18fb9e2d390a 100644
--- a/Makefile
+++ b/Makefile
@@ -215,9 +215,6 @@ check_bin_var:
 check_server_var:
 	$(call check_defined, SERVER)
 
-check_serverip_var:
-	$(call check_defined, SERVER_IP)
-
 check_login_var:
 	$(call check_defined, LOGIN)
 
@@ -241,7 +238,7 @@ help_client:
 	@echo "└──────────────────────────────────────────────────────────────────────────────┘"
 	@echo "  run_nexush          Run nexush for Linux/amd64; require LOGIN variable"
 	@echo "                      Example: make run_nexush LOGIN=janedoe@nexus.org"
-	@echo "  run_nexus-exam      Run nexus-exam; require CERT and SERVER_IP variables"
+	@echo "  run_nexus-exam      Run nexus-exam; require CERT and SERVER variables"
 	@echo ""
 	@echo "┌──────────────────────────────────────────────────────────────────────────────┐"
 	@echo "│ VALIDATION tests                                                             │"
@@ -250,10 +247,9 @@ help_client:
 	@echo "                      Require LOGIN variable. Example:"
 	@echo ""
 	@echo "────────────────────────────────────────────────────────────────────────────────"
-	@echo "  CERT specifies the path to the public CA certificate."
-	@echo "  SERVER specifies the server ip address and port, separated by a colon,"
-	@echo "  for instance: SERVER=127.0.0.1:1077"
-	@echo "  SERVER_IP specifies the server ip address."
+	@echo "  CERT:   directory where the public CA certificate resides ($(CA_CERT_FILE))."
+	@echo "  SERVER: server ip address and port, separated by a colon,"
+	@echo "          for instance: SERVER=127.0.0.1:1077"
 
 copy_resources_client:
 	@mkdir -p $(RESOURCES_DIR_CLIENT)
@@ -306,8 +302,8 @@ clean_client:
 run_nexush: check_login_var $(BUILD_DIR_CLIENT)/nexush
 	$(BUILD_DIR_CLIENT)/nexush $(LOGIN)
 
-run_nexus-exam: check_serverip_var $(BUILD_DIR_CLIENT)/nexus-exam
-	@NEXUS_SERVER=$(SERVER_IP) NEXUS_CERT=$(CERT)/$(CA_CERT_FILE) $(BUILD_DIR_CLIENT)/nexus-exam
+run_nexus-exam: check_server_var $(BUILD_DIR_CLIENT)/nexus-exam
+	@NEXUS_SERVER=$(SERVER) NEXUS_CERT=$(CERT)/$(CA_CERT_FILE) $(BUILD_DIR_CLIENT)/nexus-exam
 
 tests: check_login_var tests/run_tests build_nexus-cli $(BUILD_DIR_CLIENT)/nexus-cli
 	@cd tests && ./run_tests $(BUILD_ABS_CLIENT)/nexus-cli $(LOGIN)
diff --git a/docs/rest_api.md b/docs/rest_api.md
index 9b5991e4ac13a3e71343444976242296f1cdab89..6b0f16d594b2f42527b5f74ee47347e372940866 100644
--- a/docs/rest_api.md
+++ b/docs/rest_api.md
@@ -25,35 +25,37 @@
 
 ### VM management
 
-| Route              | Description                                                      | Method | Input | Req. user cap.   | Op. | Req. VM access cap. | Output                                                          |
-|---                 |---                                                               |---     |---    |---               |---  |---                  |---                                                              |
-| `/vms`             | returns VMs that can be listed                                   | GET    | -     | `VM_LIST_ANY`    | OR  | `VM_LIST`           | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)     |
-| `/vms/{id}`        | returns the specified VM                                         | GET    | -     | `VM_LIST_ANY`    | OR  | `VM_LIST`           | [common.vm.VMNetworkSerialized](../src/common/vm/vm.go)         |
-| `/vms/start`       | returns VM creds info for VMs that can be started                | GET    | -     | `VM_START_ANY`   | OR  | `VM_START`          | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/attach`      | returns VM creds info for VMs that can be attached to            | GET    | -     | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/{id}/attach` | returns VM creds info for the specified VM                       | GET    | -     | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         | [common.vm.VMCredentialsSerialized](../src/common/vm/vm.go)     |
-| `/vms/stop`        | returns VM creds info for VMs that can be killed/shutdown        | GET    | -     | `VM_STOP_ANY`    | OR  | `VM_STOP`           | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/reboot`      | returns VM creds info for VMs that can be rebooted               | GET    | -     | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/edit`        | returns VM creds info for VMs that can be edited                 | GET    | -     | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/editaccess`  | returns VM creds info for VMs that can have their access changed | GET    | -     | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/del`         | returns VM creds info for VMs that can be deleted                | GET    | -     | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/exportdir`   | returns VM creds info for VMs that can have a dir downloaded     | GET    | -     | `VM_READFS_ANY`  | OR  | `VM_READFS`         | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
-| `/vms/importfiles` | returns VM creds info for VMs allowing files upload              | GET    | -     | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        | [\[\]common.vm.VMCredentialsSerialized](../src/common/vm/vm.go) |
+| Route                  | Description                                    | Method | Input | Req. user cap.   | Op. | Req. VM access cap. | Output                                                                |
+|---                     |---                                             |---     |---    |---               |---  |---                  |---                                                                    |
+| `/vms`                 | returns VMs that can be listed                 | GET    | -     | `VM_LIST_ANY`    | OR  | `VM_LIST`           | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/{id}`            | returns the specified VM                       | GET    | -     | `VM_LIST_ANY`    | OR  | `VM_LIST`           | [common.vm.VMNetworkSerialized](../src/common/vm/vm.go)               |
+| `/vms/start`           | returns VMs that can be started                | GET    | -     | `VM_START_ANY`   | OR  | `VM_START`          | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/attach`          | returns "attach creds" for attachable VMs      | GET    | -     | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         | [\[\]common.vm.VMAttachCredentialsSerialized](../src/common/vm/vm.go) |
+| `/vms/{id}/attach`     | returns "attach creds" for the specified VM    | GET    | -     | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         | [common.vm.VMAttachCredentialsSerialized](../src/common/vm/vm.go)     |
+| `/vms/stop`            | returns VMs that can be killed/shutdown        | GET    | -     | `VM_STOP_ANY`    | OR  | `VM_STOP`           | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/reboot`          | returns VMs that can be rebooted               | GET    | -     | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/edit`            | returns VMs that can be edited                 | GET    | -     | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/editaccess`      | returns VMs that can have their access changed | GET    | -     | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/del`             | returns VMs that can be deleted                | GET    | -     | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/exportdir`       | returns VMs that can have a dir downloaded     | GET    | -     | `VM_READFS_ANY`  | OR  | `VM_READFS`         | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
+| `/vms/importfiles`     | returns VMs that can have files upload to      | GET    | -     | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        | [\[\]common.vm.VMNetworkSerialized](../src/common/vm/vm.go)           |
 
-| Route                      | Description                  | Method | Input                                                         | Req. user cap.   | Op. | Req. VM access cap. | Output                                                  |
-|---                         |---                           |---     |---                                                            |---               |---  |---                  |---                                                      |
-| `/vms`                     | create a VM                  | POST   | [commmon.params.VMCreate](../src/common/params/vms.go)        | `VM_CREATE`      | -   |  -                  | [common.vm.VMNetworkSerialized](../src/common/vm/vm.go) |
-| `/vms/{id}`                | delete a VM                  | DELETE | -                                                             | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        | -                                                       |
-| `/vms/{id}`                | edit a VM                    | PUT    | [common.params.VMEdit](../src/common/params/vms.go)           | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           | [common.vm.VMNetworkSerialized](../src/common/vm/vm.go) |
-| `/vms/{id}/start`          | start a VM                   | PUT    | -                                                             | `VM_START_ANY`   | OR  | `VM_START`          | -                                                       |
-| `/vms/{id}/startwithcreds` | start a VM with credentials  | PUT    | [common.params.VMStartWithCreds](../src/common/params/vms.go) | `VM_START_ANY`   | OR  | `VM_START`          | -                                                       |
-| `/vms/{id}/stop`           | kill a VM                    | PUT    | -                                                             | `VM_STOP_ANY`    | OR  | `VM_STOP`           | -                                                       |
-| `/vms/{id}/reboot`         | reboot a VM                  | PUT    | -                                                             | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         | -                                                       |
-| `/vms/{id}/shutdown`       | gracefully shutdown a VM     | PUT    | -                                                             | `VM_STOP_ANY`    | OR  | `VM_STOP`           | -                                                       |
-| `/vms/{id}/access/{email}` | set VM access for a user     | PUT    | [common.params.VMAddAccess](../src/common/params/vms.go)      | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     | -                                                       |
-| `/vms/{id}/access/{email}` | del VM access for a user     | DELETE | -                                                             | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     | -                                                       |
-| `/vms/{id}/exportdir`      | download a VM's dir          | GET    | [common.params.VMExportDir](../src/common/params/vms.go)      | `VM_READFS_ANY`  | OR  | `VM_READFS`         | tar.gz archive                                          |
-| `/vms/{id}/importfiles`    | upload files into a VM's dir | POST   | multipart form: { "vmDir" (path), "file" (tar.gz archive) }   | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |                                                         |
+| Route                      | Description                         | Method | Input                                                         | Req. user cap.   | Op. | Req. VM access cap. | Output                                                           |
+|---                         |---                                  |---     |---                                                            |---               |---  |---                  |---                                                               |
+| `/vms`                     | create a VM                         | POST   | [commmon.params.VMCreate](../src/common/params/vms.go)        | `VM_CREATE`      | -   |  -                  | [common.vm.VMNetworkSerialized](../src/common/vm/vm.go)          |
+| `/vms/{id}`                | delete the VM                       | DELETE | -                                                             | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        | -                                                                |
+| `/vms/{id}`                | edit the VM                         | PUT    | [common.params.VMEdit](../src/common/params/vms.go)           | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           | [common.vm.VMNetworkSerialized](../src/common/vm/vm.go)          |
+| `/vms/{id}/start`          | start the VM                        | PUT    | -                                                             | `VM_START_ANY`   | OR  | `VM_START`          | -                                                                |
+| `/vms/{id}/startwithcreds` | start the VM with credentials       | PUT    | [common.params.VMStartWithCreds](../src/common/params/vms.go) | `VM_START_ANY`   | OR  | `VM_START`          | -                                                                |
+| `/vms/{id}/spicecreds`     | returns Spice creds for the VM      | POST   | [common.params.VMAttachCreds](../src/common/params/vms.go)    | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         | [common.vm.VMSpiceCredentialsSerialized](../src/common/vm/vm.go) |
+| `/vms/spicecreds`          | returns Spice creds for a VM        | POST   | [common.params.VMAttachCreds](../src/common/params/vms.go)    | `VM_ATTACH_ANY`  | OR  | -                   | [common.vm.VMSpiceCredentialsSerialized](../src/common/vm/vm.go) |
+| `/vms/{id}/stop`           | kill the VM                         | PUT    | -                                                             | `VM_STOP_ANY`    | OR  | `VM_STOP`           | -                                                                |
+| `/vms/{id}/reboot`         | reboot the VM                       | PUT    | -                                                             | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         | -                                                                |
+| `/vms/{id}/shutdown`       | gracefully shutdown the VM          | PUT    | -                                                             | `VM_STOP_ANY`    | OR  | `VM_STOP`           | -                                                                |
+| `/vms/{id}/access/{email}` | set the VM's VM access for the user | PUT    | [common.params.VMAddAccess](../src/common/params/vms.go)      | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     | -                                                                |
+| `/vms/{id}/access/{email}` | del the VM's VM access for the user | DELETE | -                                                             | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     | -                                                                |
+| `/vms/{id}/exportdir`      | download a dir from the VM          | GET    | [common.params.VMExportDir](../src/common/params/vms.go)      | `VM_READFS_ANY`  | OR  | `VM_READFS`         | tar.gz archive                                                   |
+| `/vms/{id}/importfiles`    | upload files into a dir in the VM   | POST   | multipart form: { "vmDir" (path), "file" (tar.gz archive) }   | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |                                                                  |
 
 ### Template management
 
diff --git a/src/client/cmdUser/userUpdatePwd.go b/src/client/cmdUser/userUpdatePwd.go
index 172cac8db6be53e2db859881ad2b868cbf2fe006..d6b151edce9139432debc13f6149a1b4ca0cecdd 100644
--- a/src/client/cmdUser/userUpdatePwd.go
+++ b/src/client/cmdUser/userUpdatePwd.go
@@ -69,7 +69,7 @@ func (cmd *UpdatePwd)Run(args []string) int {
         return 1
     } else {
         if resp.IsSuccess() {
-            u.Println(resp)
+            u.Println("Password changed successfully.")
             return 0
         } else {
             u.PrintlnErr("Error: "+resp.Status()+": "+resp.String())
diff --git a/src/client/cmdVM/attachHelper.go b/src/client/cmdVM/attachHelper.go
new file mode 100644
index 0000000000000000000000000000000000000000..645400a9dded24142e74cd55bb12a2343a269468
--- /dev/null
+++ b/src/client/cmdVM/attachHelper.go
@@ -0,0 +1,71 @@
+package cmdVM
+
+import (
+	"fmt"
+	"sync"
+    "errors"
+    "encoding/json"
+    "nexus-common/vm"
+	"nexus-common/params"
+	"nexus-client/exec"
+    u "nexus-client/utils"
+    g "nexus-client/globals"
+    "github.com/go-playground/validator/v10"
+)
+
+// The boolean indicates whether attaching is blocking (synchronous) or non-blocking (asynchronous).
+func AttachToVMs(vms []vm.VMAttachCredentialsSerialized, synchronous bool) (int, error) {
+	statusCode := 0
+
+    hostname := g.GetInstance().Hostname
+    cert := g.GetInstance().PubCert
+
+    client := g.GetInstance().Client
+    host := g.GetInstance().Host
+
+	var wg sync.WaitGroup
+
+	if synchronous {
+		// Use wait groups to wait until all viewers threads have completed.
+		wg.Add(len(vms))
+	}
+
+    for _, v := range(vms) {
+        uuid := v.ID.String()
+        p := &params.VMAttachCreds{ Pwd: v.Pwd }
+        resp, err := client.R().SetBody(p).Post(host+"/vms/"+uuid+"/spicecreds")
+        if err != nil {
+			return 1, err
+        }
+
+        if resp.IsSuccess() {
+            var creds vm.VMSpiceCredentialsSerialized
+            if err := json.Unmarshal(resp.Body(), &creds); err != nil {
+				return 1, err
+            }
+            if err := validator.New(validator.WithRequiredStructEnabled()).Struct(creds); err != nil {
+                return 1, err
+            }
+
+            go func(v vm.VMAttachCredentialsSerialized, creds vm.VMSpiceCredentialsSerialized) {
+                stdoutStderr, err := exec.RunRemoteViewer(hostname, cert, v.Name, creds.SpicePort, creds.SpicePwd, false)
+                if err != nil {
+                    u.PrintlnErr("Failed attaching to VM ", v.ID, ": ", fmt.Sprintf("%s", stdoutStderr))
+                    statusCode |= 1
+                }
+				if synchronous {
+					wg.Done()
+				}
+            } (v, creds)
+
+        } else {
+			return 1, errors.New("Error: "+resp.Status()+": "+resp.String())
+        }
+    }
+
+	if synchronous {
+		wg.Wait()
+	}
+
+    return statusCode, nil
+}
diff --git a/src/client/cmdVM/helper.go b/src/client/cmdVM/helper.go
index c8e7bd77d05ecb6533916b7108ccbbb43c0f2738..8ff69a37b911c72a100782bd25d52726425846b8 100644
--- a/src/client/cmdVM/helper.go
+++ b/src/client/cmdVM/helper.go
@@ -118,7 +118,7 @@ func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized,
 // Regular expression examples:
 //   "."   -> matches everything
 //   "bla" -> matches any VM name containing "bla"
-func getFilteredVMCredentials(route string, patterns []string) ([]vm.VMCredentialsSerialized, error) {
+func getFilteredVMCredentials(route string, patterns []string) ([]vm.VMAttachCredentialsSerialized, error) {
     if len(patterns) < 1 {
         return nil, errors.New("At least one ID or regex must be specified")
     }
@@ -143,7 +143,7 @@ func getFilteredVMCredentials(route string, patterns []string) ([]vm.VMCredentia
         return nil, err
     }
 
-    vmsList := []vm.VMCredentialsSerialized{}
+    vmsList := []vm.VMAttachCredentialsSerialized{}
 
     if resp.IsSuccess() {
         vms, err := deserializeVMsCredentials(resp)
@@ -203,8 +203,8 @@ func deserializeVM(resp *resty.Response) (*vm.VMNetworkSerialized, error) {
 }
 
 // Deserialize a list of VM credentials from an http response.
-func deserializeVMsCredentials(resp *resty.Response) ([]vm.VMCredentialsSerialized, error) {
-    vmsCreds := []vm.VMCredentialsSerialized{}
+func deserializeVMsCredentials(resp *resty.Response) ([]vm.VMAttachCredentialsSerialized, error) {
+    vmsCreds := []vm.VMAttachCredentialsSerialized{}
     if err := json.Unmarshal(resp.Body(), &vmsCreds); err != nil {
         return nil, err
     }
@@ -212,8 +212,8 @@ func deserializeVMsCredentials(resp *resty.Response) ([]vm.VMCredentialsSerializ
 }
 
 // Deserialize a VM credentials from an http response.
-func deserializeVMCredentials(resp *resty.Response) (*vm.VMCredentialsSerialized, error) {
-    var vmCreds vm.VMCredentialsSerialized
+func deserializeVMCredentials(resp *resty.Response) (*vm.VMAttachCredentialsSerialized, error) {
+    var vmCreds vm.VMAttachCredentialsSerialized
     if err := json.Unmarshal(resp.Body(), &vmCreds); err != nil {
         return nil, err
     }
diff --git a/src/client/cmdVM/vmAttachAsync.go b/src/client/cmdVM/vmAttachAsync.go
index 7fd0f28538c4fbafe7286decdaf61c4b26398b33..d9959b165918e70dee96c577558467a7705814fd 100644
--- a/src/client/cmdVM/vmAttachAsync.go
+++ b/src/client/cmdVM/vmAttachAsync.go
@@ -1,11 +1,8 @@
 package cmdVM
 
 import (
-    "fmt"
-    "nexus-common/vm"
     "nexus-client/exec"
     u "nexus-client/utils"
-    g "nexus-client/globals"
 )
 
 type AttachAsync struct {
@@ -33,37 +30,27 @@ func (cmd *AttachAsync)Run(args []string) int {
         return 1
     }
 
-    hostname := g.GetInstance().Hostname
-    cert := g.GetInstance().PubCert
-
     argc := len(args)
     if argc < 1 {
         cmd.PrintUsage()
         return 1
     }
 
-    vms, err := getFilteredVMCredentials("/vms/attach", args)
+    creds, err := getFilteredVMCredentials("/vms/attach", args)
     if err != nil {
         u.PrintlnErr(err.Error())
         return 1
     }
 
-    if len(vms) == 0 {
+    if len(creds) == 0 {
         u.PrintlnErr("Error: VM(s) not found.")
         return 1
     }
 
-    statusCode := 0
-
-    for _, v := range(vms) {
-        go func(v vm.VMCredentialsSerialized) {
-            stdoutStderr, err := exec.RunRemoteViewer(hostname, cert, v.Name, v.Port, v.Pwd, false)
-            if err != nil {
-                u.PrintlnErr("Failed attaching to VM ", v.ID, ": ", fmt.Sprintf("%s", stdoutStderr))
-                statusCode |= 1
-            }
-        } (v)
-    }
+	statusCode, err := AttachToVMs(creds, true)
+	if err != nil {
+		u.PrintlnErr(err)
+	}
 
     return statusCode
 }
diff --git a/src/client/cmdVM/vmAttachAsyncSingle.go b/src/client/cmdVM/vmAttachAsyncSingle.go
index 65e71c3352c0a51a842fd2a4f4e6d35650e12a07..ff235105f3d6fd8dd37b6d907d8a088ee083e7fe 100644
--- a/src/client/cmdVM/vmAttachAsyncSingle.go
+++ b/src/client/cmdVM/vmAttachAsyncSingle.go
@@ -1,9 +1,8 @@
 package cmdVM
 
 import (
-    "fmt"
-    "nexus-common/vm"
     "nexus-client/exec"
+    "nexus-common/vm"
     u "nexus-client/utils"
     g "nexus-client/globals"
 )
@@ -18,7 +17,7 @@ func (cmd *AttachAsyncSingle)GetName() string {
 
 func (cmd *AttachAsyncSingle)GetDesc() []string {
     return []string{
-        "Attach to a VM.",
+        "Attaches to a VM in order to use its desktop environment.",
         "If not the VM's owner: requires VM_ATTACH VM access capability or VM_ATTACH_ANY user capability."}
 }
 
@@ -37,17 +36,15 @@ func (cmd *AttachAsyncSingle)Run(args []string) int {
         return 1
     }
 
-    hostname := g.GetInstance().Hostname
-    cert := g.GetInstance().PubCert
-    client := g.GetInstance().Client
-    host := g.GetInstance().Host
-
     argc := len(args)
-    if argc < 1 {
+    if argc != 1 {
         cmd.PrintUsage()
         return 1
     }
 
+    client := g.GetInstance().Client
+    host := g.GetInstance().Host
+
     vmID := args[0]
     resp, err := client.R().Get(host+"/vms/"+vmID+"/attach")
     if err != nil {
@@ -55,18 +52,16 @@ func (cmd *AttachAsyncSingle)Run(args []string) int {
         return 1
     } else {
         if resp.IsSuccess() {
-            myvm, err := deserializeVMCredentials(resp)
+            creds, err := deserializeVMCredentials(resp)
             if err != nil {
                 u.PrintlnErr("Failed retrieving server's response: "+err.Error())
                 return 1
             }
-
-            go func(v vm.VMCredentialsSerialized) {
-                stdoutStderr, err := exec.RunRemoteViewer(hostname, cert, v.Name, v.Port, v.Pwd, false)
-                if err != nil {
-                    u.PrintlnErr("Failed attaching to VM ", v.ID, ": ", fmt.Sprintf("%s", stdoutStderr))
-                }
-            } (*myvm)
+            statusCode, err := AttachToVMs([]vm.VMAttachCredentialsSerialized{*creds}, false)
+            if err != nil {
+                u.PrintlnErr(err)
+            }
+            return statusCode
         } else {
             u.PrintlnErr("Failed retrieving VM credentials for VM \""+vmID+"\": "+resp.Status()+": "+resp.String())
             return 1
diff --git a/src/client/cmdVM/vmAttachSync.go b/src/client/cmdVM/vmAttachSync.go
index b8244870b4265d585d2f0d6c88d86c2612dd2c4b..b6dd9f5821ec430191665aa1f5801ecf9ec8d279 100644
--- a/src/client/cmdVM/vmAttachSync.go
+++ b/src/client/cmdVM/vmAttachSync.go
@@ -1,12 +1,8 @@
 package cmdVM
 
 import (
-    "fmt"
-    "sync"
     "nexus-client/exec"
-    "nexus-common/vm"
     u "nexus-client/utils"
-    g "nexus-client/globals"
 )
 
 type AttachSync struct {
@@ -20,7 +16,7 @@ func (cmd *AttachSync)GetName() string {
 func (cmd *AttachSync)GetDesc() []string {
     return []string{
         "Attaches to one or more VMs in order to use their desktop environment.",
-        "If not the VM's owner: requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
+        "If not the VM's owner: requires VM_ATTACH VM access capability or VM_ATTACH_ANY user capability."}
 }
 
 func (cmd *AttachSync)PrintUsage() {
@@ -34,44 +30,27 @@ func (cmd *AttachSync)Run(args []string) int {
         return 1
     }
 
-    hostname := g.GetInstance().Hostname
-    cert := g.GetInstance().PubCert
-
     argc := len(args)
     if argc < 1 {
         cmd.PrintUsage()
         return 1
     }
 
-    vms, err := getFilteredVMCredentials("/vms/attach", args)
+    creds, err := getFilteredVMCredentials("/vms/attach", args)
     if err != nil {
         u.PrintlnErr(err.Error())
         return 1
     }
 
-    if len(vms) == 0 {
+    if len(creds) == 0 {
         u.PrintlnErr("Error: VM(s) not found.")
         return 1
     }
 
-    // Use wait groups to wait until all viewers threads have completed.
-    var wg sync.WaitGroup
-    wg.Add(len(vms))
-
-    statusCode := 0
-
-    for _, v := range(vms) {
-        go func(v vm.VMCredentialsSerialized) {
-            stdoutStderr, err := exec.RunRemoteViewer(hostname, cert, v.Name, v.Port, v.Pwd, false)
-            if err != nil {
-                u.PrintlnErr("Failed attaching to VM ", v.ID, ": ", fmt.Sprintf("%s", stdoutStderr))
-                statusCode |= 1
-            }
-            wg.Done()
-        } (v)
-    }
-
-    wg.Wait()
+	statusCode, err := AttachToVMs(creds, true)
+	if err != nil {
+		u.PrintlnErr(err)
+	}
 
     return statusCode
 }
diff --git a/src/client/cmdVM/vmCreds2csv.go b/src/client/cmdVM/vmCreds2csv.go
index 3033e487b420fb14c4d09e41ea6f76748e4bdba9..b164f3fa1866d63af9ae5d6203c61cf95191da7f 100644
--- a/src/client/cmdVM/vmCreds2csv.go
+++ b/src/client/cmdVM/vmCreds2csv.go
@@ -3,7 +3,6 @@ package cmdVM
 import (
     "os"
     "fmt"
-    "strconv"
     u "nexus-client/utils"
     g "nexus-client/globals"
 )
@@ -19,7 +18,7 @@ func (cmd *Creds2csv)GetName() string {
 func (cmd *Creds2csv)GetDesc() []string {
     return []string{
         "Creates a CSV file with the credentials required to attach to running VMs.",
-        "The written CSV file contains 4 columns: VM ID;VM name;port;password",
+        "The written CSV file contains 3 columns: VM ID;VM name;password",
         "If not the VM's owner: requires VM_ATTACH VM access capability or VM_ATTACH_ANY user capability."}
 }
 
@@ -42,13 +41,13 @@ func (cmd *Creds2csv)Run(args []string) int {
 
     csvFile := args[argc-1]
 
-    vms, err := getFilteredVMCredentials("/vms/attach", args[:argc-1])
+    credsList, err := getFilteredVMCredentials("/vms/attach", args[:argc-1])
     if err != nil {
         u.PrintlnErr("Error: "+err.Error())
         return 1
     }
 
-    if len(vms) == 0 {
+    if len(credsList) == 0 {
         u.PrintlnErr("Error: VM(s) not found.")
         return 1
     }
@@ -62,9 +61,9 @@ func (cmd *Creds2csv)Run(args []string) int {
 
     defer f.Close()
 
-    for _, vm := range vms {
+    for _, creds := range credsList {
         sep := string(g.CsvFieldSeparator)
-        _, err := fmt.Fprintln(f, vm.ID.String()+sep+vm.Name+sep+strconv.Itoa(vm.Port)+sep+vm.Pwd)
+        _, err := fmt.Fprintln(f, creds.ID.String()+sep+creds.Name+sep+creds.Pwd)
         if err != nil {
             u.PrintlnErr("Error: "+err.Error())
             return 1
diff --git a/src/client/cmdVM/vmCreds2pdf.go b/src/client/cmdVM/vmCreds2pdf.go
index cba3c23a9dbecfa20ef38b34eab275b086affc7d..303491069057e4ed61dbe4c1ea5211e38e4ade11 100644
--- a/src/client/cmdVM/vmCreds2pdf.go
+++ b/src/client/cmdVM/vmCreds2pdf.go
@@ -1,7 +1,6 @@
 package cmdVM
 
 import (
-    "strconv"
     u "nexus-client/utils"
     "github.com/go-pdf/fpdf"
 )
@@ -43,14 +42,14 @@ func (cmd *Creds2pdf)Run(args []string) int {
 
     pdfFile := args[argc-1]
 
-    vms, err := getFilteredVMCredentials("/vms/attach", args[:argc-1])
+    credsList, err := getFilteredVMCredentials("/vms/attach", args[:argc-1])
     if err != nil {
         u.PrintlnErr("Error: "+err.Error())
         return 1
     }
 
-    if len(vms) == 0 {
-        u.PrintlnErr("Error: VM(s) not found (possible causes: no VM by that ID/regex, VM(s) not started, no right to list requested VM(s)).")
+    if len(credsList) == 0 {
+        u.PrintlnErr("Error: VM(s) not found (possible causes: no VM by that ID/regex, VM(s) not started, missing capabilities).")
         return 1
     }
 
@@ -59,7 +58,7 @@ func (cmd *Creds2pdf)Run(args []string) int {
     const rightMargin = 0.
     // Max number of chars allowed in the VM's name given the used font and size.
     const maxChar = 70
-    columnWidth := [...]float64 {156.,14.,23.}
+    columnWidth := [...]float64 {156.,30.}
     const rowHeight = 15.
     const fontSize = 12.
     const cellBorder = "1"  // full cell border; use "" for no border
@@ -76,16 +75,15 @@ func (cmd *Creds2pdf)Run(args []string) int {
     // Read a specific monospace bold font known to support UTF8 encoding
     pdf.AddUTF8FontFromBytes("cmuntb", "b", []byte(embeddedCMUTypewriterBold))
 
-    for _, vm := range vms {
-        if len(vm.Name) > maxChar {
-            vm.Name = vm.Name[0:maxChar-1]
-            vm.Name = vm.Name+"…"
+    for _, creds := range credsList {
+        if len(creds.Name) > maxChar {
+            creds.Name = creds.Name[0:maxChar-1]
+            creds.Name = creds.Name+"…"
         }
         pdf.SetX(leftMargin)
         pdf.SetFont("cmuntb", "b", fontSize)   // "b" means bold; use "" for normal
-        pdf.CellFormat(columnWidth[0], rowHeight, vm.Name, cellBorder, nextPos, align, bkgd, link, url)
-        pdf.CellFormat(columnWidth[1], rowHeight, strconv.Itoa(vm.Port), cellBorder, nextPos, "C", bkgd, link, url)
-        pdf.CellFormat(columnWidth[2], rowHeight, vm.Pwd, cellBorder, nextPos, "C", bkgd, link, url)
+        pdf.CellFormat(columnWidth[0], rowHeight, creds.Name, cellBorder, nextPos, align, bkgd, link, url)
+        pdf.CellFormat(columnWidth[1], rowHeight, creds.Pwd, cellBorder, nextPos, "C", bkgd, link, url)
         pdf.Ln(-1)  // line break; -1 means move by the height of the last printed cell
     }
 
diff --git a/src/client/cmdVM/vmStartWithCreds.go b/src/client/cmdVM/vmStartWithCreds.go
index 79ead12e4c992320dd558b69a31bae2b8d0cfa2f..5d2d758a27ceaa677cec376eaa5fcc2a2904a34c 100644
--- a/src/client/cmdVM/vmStartWithCreds.go
+++ b/src/client/cmdVM/vmStartWithCreds.go
@@ -2,7 +2,6 @@ package cmdVM
 
 import (
     "errors"
-    "strconv"
     "nexus-common/params"
     u "nexus-client/utils"
     g "nexus-client/globals"
@@ -29,32 +28,27 @@ func (cmd *StartWithCreds)PrintUsage() {
     u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
     u.PrintlnErr("USAGE: "+cmd.GetName()+" file.csv")
     u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
-    const usage string = `file.csv    4-column CSV file defining the VMs to start and their credentials.
-            Content of the 4 columns: VM ID;VM name;port;password.`
+    const usage string = `file.csv    3-column CSV file defining the VMs to start and their credentials.
+            Content of the 3 columns: VM ID;VM name;password.`
     u.PrintlnErr(usage)
 }
 
-func (cmd *StartWithCreds)parseCSVFile(csvFile string) ([]string, []string, []string, error) {
+func (cmd *StartWithCreds)parseCSVFile(csvFile string) ([]string, []string, error) {
     // Column 0: VM IDs
     vmIDs, err := u.ReadCSVColumn(csvFile, 0)
     if err != nil {
-        return nil, nil, nil, err
+        return nil, nil, err
     }
-    // Column 2: ports
-    ports, err := u.ReadCSVColumn(csvFile, 2)
+    // Column 2: passwords
+    pwds, err := u.ReadCSVColumn(csvFile, 2)
     if err != nil {
-        return nil, nil, nil, err
-    }
-    // Column 3: passwords
-    pwds, err := u.ReadCSVColumn(csvFile, 3)
-    if err != nil {
-        return nil, nil, nil, err
+        return nil, nil, err
     }
     count := len(vmIDs)
-    if (len(ports) != count) || (len(pwds) != count) {
-        return nil, nil, nil, errors.New("Invalid CSV file: all columns must have the same number of entries!")
+    if (len(pwds) != count) {
+        return nil, nil, errors.New("Invalid CSV file: all columns must have the same number of entries!")
     }
-    return vmIDs, ports, pwds, nil
+    return vmIDs, pwds, nil
 }
 
 func (cmd *StartWithCreds)Run(args []string) int {
@@ -67,7 +61,7 @@ func (cmd *StartWithCreds)Run(args []string) int {
         return 1
     }
 
-    vmIDs, ports, pwds, err := cmd.parseCSVFile(args[0])
+    vmIDs, pwds, err := cmd.parseCSVFile(args[0])
     if err != nil {
         u.PrintlnErr(err.Error())
         return 1
@@ -78,11 +72,6 @@ func (cmd *StartWithCreds)Run(args []string) int {
     vmArgs := &params.VMStartWithCreds {}
 
     for i, vmID := range(vmIDs) {
-        port, err := strconv.Atoi(ports[i])
-        if err != nil {
-            u.PrintlnErr("Line ",i,": invalid port number")
-        }
-        vmArgs.Port = port
         vmArgs.Pwd = pwds[i]
         resp, err := client.R().SetBody(vmArgs).Put(host+"/vms/"+vmID+"/startwithcreds")
         if err != nil {
diff --git a/src/client/nexus-exam/go.mod b/src/client/nexus-exam/go.mod
index 350be8af3c0be8c06029ee3efa23fbd0ec3a5d0b..62db63cccdd8c1281463da007694ff5aae077c12 100644
--- a/src/client/nexus-exam/go.mod
+++ b/src/client/nexus-exam/go.mod
@@ -2,14 +2,42 @@ module nexus-exam
 
 go 1.18
 
+replace nexus-common/caps => ../../common/caps
+
+replace nexus-common/params => ../../common/params
+
+replace nexus-common/vm => ../../common/vm
+
+replace nexus-common/template => ../../common/template
+
+replace nexus-client/version => ../version
+
+replace nexus-client/globals => ../globals
+
+replace nexus-client/defaults => ../defaults
+
 replace nexus-common/utils => ../../common/utils
 
 replace nexus-client/utils => ../utils
 
-replace nexus-client/globals => ../globals
+replace nexus-client/cmd => ../cmd
+
+replace nexus-client/cmdMisc => ../cmdMisc
+
+replace nexus-client/cmdLogin => ../cmdLogin
+
+replace nexus-client/cmdToken => ../cmdToken
+
+replace nexus-client/cmdTemplate => ../cmdTemplate
+
+replace nexus-client/cmdUser => ../cmdUser
+
+replace nexus-client/cmdVM => ../cmdVM
 
 replace nexus-client/exec => ../exec
 
+replace nexus-client/cmdVersion => ../cmdVersion
+
 require fyne.io/fyne/v2 v2.2.1
 
 require (
@@ -25,7 +53,7 @@ require (
 	github.com/go-resty/resty/v2 v2.7.0 // indirect
 	github.com/godbus/dbus/v5 v5.1.0 // indirect
 	github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
-	github.com/google/uuid v1.1.2 // indirect
+	github.com/google/uuid v1.3.0 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
 	github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -38,11 +66,19 @@ require (
 	golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect
 	golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
 	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
+	golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
+	nexus-client/cmd v0.0.0-00010101000000-000000000000 // indirect
+	nexus-client/cmdLogin v0.0.0-00010101000000-000000000000 // indirect
+	nexus-client/cmdVersion v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/exec v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/globals v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/utils v0.0.0-00010101000000-000000000000 // indirect
+	nexus-client/version v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/caps v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/params v0.0.0-00010101000000-000000000000 // indirect
 	nexus-common/utils v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/vm v0.0.0-00010101000000-000000000000 // indirect
 )
diff --git a/src/client/nexus-exam/go.sum b/src/client/nexus-exam/go.sum
index 61c507975706de65a2618b06686e0d43fc92582b..72dd0f6b977ef46655d1bfa8edb6f788d2771bd1 100644
--- a/src/client/nexus-exam/go.sum
+++ b/src/client/nexus-exam/go.sum
@@ -163,6 +163,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -457,6 +459,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/src/client/nexus-exam/nexus-exam.go b/src/client/nexus-exam/nexus-exam.go
index 1cbf2904dd7af3973485b16190de03fce2ffee4b..579996a55a6314f9c6d1b08dfec2adb590769c96 100644
--- a/src/client/nexus-exam/nexus-exam.go
+++ b/src/client/nexus-exam/nexus-exam.go
@@ -2,16 +2,20 @@ package main
 
 import (
     "os"
-    "errors"
-    "strconv"
+    // "errors"
+    "strings"
+    // "strconv"
     "nexus-common/utils"
     "nexus-client/exec"
     u "nexus-client/utils"
     g "nexus-client/globals"
+    "nexus-client/cmdVersion"
+    // "nexus-client/cmdLogin"
     "fyne.io/fyne/v2/app"
     "fyne.io/fyne/v2/theme"
     "fyne.io/fyne/v2/widget"
     "fyne.io/fyne/v2/container"
+    "github.com/go-resty/resty/v2"
 )
 
 const (
@@ -24,11 +28,11 @@ func run() int {
         return 1
     }
 
-    server, found := os.LookupEnv(g.ENV_NEXUS_SERVER)
+    serverEnvVar, found := os.LookupEnv(g.ENV_NEXUS_SERVER)
     if !found {
         u.PrintlnErr("Environment variable \""+g.ENV_NEXUS_SERVER+"\" must be set!")
-        u.PrintlnErr("It defines the nexus server to connect to.")
-        u.PrintlnErr("Example: export "+g.ENV_NEXUS_SERVER+"=192.168.1.42")
+        u.PrintlnErr("It defines the nexus server to connect to along the port number.")
+        u.PrintlnErr("Example: export "+g.ENV_NEXUS_SERVER+"=192.168.1.42:1077")
         return 1
     }
 
@@ -45,36 +49,47 @@ func run() int {
         return 1
     }
 
+    parts := strings.Split(serverEnvVar, ":")
+    hostname := parts[0]
+
+    client := resty.New()
+    client.SetRootCertificate(certPath)
+    host := "https://"+serverEnvVar
+
+    g.Init(hostname, host, certPath, client)
+
+    // Checks the client version is compatible with the server's API.
+    if !cmdVersion.CheckServerCompatibility("nexus-exam") {
+        return 1
+    }
+
+    // Logins and obtains a JWT token.
+    // email := ""
+    // pwd := ""
+
+    // token, err := cmdLogin.GetToken(email, pwd)
+    // u.PrintlnErr("")
+    // if err != nil {
+    //     u.PrintlnErr("Error: "+err.Error())
+    //     return 1
+    // }
+
     app := app.New()
     app.Settings().SetTheme(theme.LightTheme())
 
     win := app.NewWindow(windowTitle)
 
-    portEntry := widget.NewEntry()
-    portEntry.Validator = func(val string) error {
-        port, err := strconv.Atoi(val)
-        if err != nil {
-            return errors.New("Please enter an integer")
-        }
-        if port < 1024 || port > 65535 {
-            return errors.New("Please enter a value within [1024,65535]")
-        }
-        return nil
-    }
-
     pwdEntry := widget.NewEntry()
     pwdEntry.Password = true
 
-    label := widget.NewLabel("Enter port and password to connect to your VM:")
+    label := widget.NewLabel("Enter password to connect to your VM:")
 
     form := &widget.Form{
         Items: []*widget.FormItem {
-            {Text: "Port", Widget: portEntry},
-            { Text: "Password", Widget: pwdEntry},
+            {Text: "Password", Widget: pwdEntry},
         },
         OnSubmit: func() {
-            port, _ := strconv.Atoi(portEntry.Text)
-            exec.RunRemoteViewer(server, certPath, windowTitle, port, pwdEntry.Text, true)
+            // exec.RunRemoteViewer(server, certPath, windowTitle, port, pwdEntry.Text, true)
         },
         SubmitText: "Connect",
     }
diff --git a/src/client/version/version.go b/src/client/version/version.go
index f406b364bdec025db6d2c29b5eaed18fc66f35af..2b4b5abd8935ff089a7a2b38873c47cb6ae4dc69 100644
--- a/src/client/version/version.go
+++ b/src/client/version/version.go
@@ -6,8 +6,8 @@ import (
 
 const (
     major  = 1
-    minor  = 10
-    bugfix = 1
+    minor  = 11
+    bugfix = 0
 )
 
 var version params.Version = params.NewVersion(major, minor, bugfix)
diff --git a/src/common/params/vms.go b/src/common/params/vms.go
index 0991271f088b287c2bacfec5da3e1ce57de9c941..dc2a44329142f61575166173bfa1fa1b1ff0a035 100644
--- a/src/common/params/vms.go
+++ b/src/common/params/vms.go
@@ -16,8 +16,11 @@ type VMCreate struct {
 }
 
 type VMStartWithCreds struct {
-	Port int              `json:"port"       validate:"required,gte=1025,lte=65535"`
-	Pwd string            `json:"pwd"        validate:"required,min=8,max=64"`
+	Pwd string            `json:"pwd"        validate:"required,min=10,max=64"`
+}
+
+type VMAttachCreds struct {
+	Pwd string            `json:"pwd"        validate:"required,min=10,max=64"`
 }
 
 type VMEdit struct {
diff --git a/src/common/utils/go.sum b/src/common/utils/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..c4a57955c6e0d8c358a48f375daf966e42220935
--- /dev/null
+++ b/src/common/utils/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
diff --git a/src/common/utils/utils.go b/src/common/utils/utils.go
index b607ad28db86b3794ef2df4ef702fc993d83d885..6675ab7cc814c94c4de5ce7cae99161bf98ee996 100644
--- a/src/common/utils/utils.go
+++ b/src/common/utils/utils.go
@@ -2,13 +2,11 @@ package utils
 
 import (
     "os"
+    "errors"
 )
 
 // Returns true if the specified file exists, false otherwise.
 func FileExists(filename string) bool {
-    info, err := os.Stat(filename)
-    if os.IsNotExist(err) {
-       return false
-    }
-    return !info.IsDir()
+    _, err := os.Stat(filename)
+	return !errors.Is(err, os.ErrNotExist)
 }
diff --git a/src/common/vm/vm.go b/src/common/vm/vm.go
index 696ca02a0f7605c213af2c3978b13b641ba3155c..ecdb82fd608623d9b1bd40a01ce28635d3b630b9 100644
--- a/src/common/vm/vm.go
+++ b/src/common/vm/vm.go
@@ -40,12 +40,15 @@ type (
         StartTime time.Time    `json:"startTime"`
     }
 
-    // VM id, name and credentials to be serialized over the network.
-    VMCredentialsSerialized struct {
+    VMSpiceCredentialsSerialized struct {
+        SpicePort int         `json:"spicePort"  validate:"required,gte=1025,lte=65535"`
+        SpicePwd string       `json:"spicePwd"   validate:"required,min=16,max=64"`
+    }
+
+    VMAttachCredentialsSerialized struct {
         ID uuid.UUID          `json:"id"         validate:"required"`
         Name string           `json:"name"       validate:"required,min=1,max=256"`
-        Port int              `json:"port"       validate:"required,gte=1025,lte=65535"`
-        Pwd string            `json:"pwd"        validate:"required,min=1,max=64"`
+        Pwd string            `json:"pwd"        validate:"required,min=8,max=64"`
     }
 
     VMState string  // see stateXXX below
diff --git a/src/server/config/config.go b/src/server/config/config.go
index 23fdbef97b997122a5fbbcf69a627e0134bdcbd0..23a31f1ca1b9f88c48f903525adcd752e4e40e90 100644
--- a/src/server/config/config.go
+++ b/src/server/config/config.go
@@ -40,10 +40,15 @@ type Config struct {
     // Unix permissions when creating directories: when creating templates and VMs and for tmp directory.
     MkDirPerm os.FileMode
 
-    VMPwdLength int
-    VMPwdDigitCount int
-    VMPwdSymbolCount int
-    VMPwdRepeatChars bool
+    VMSpicePwdLength int
+    VMSpicePwdDigitCount int
+    VMSpicePwdSymbolCount int
+    VMSpicePwdRepeatChars bool
+
+    VMAttachPwdLength int
+    VMAttachPwdDigitCount int
+    VMAttachPwdSymbolCount int
+    VMAttachPwdRepeatChars bool
 
     UsersFile string
     DataDir string
@@ -76,12 +81,20 @@ func GetInstance() *Config {
         }
     
         sanityChecks(config)
-    
+
         config.MkDirPerm = 0750
-        config.VMPwdLength = 8
-        config.VMPwdDigitCount = 4
-        config.VMPwdSymbolCount = 0
-        config.VMPwdRepeatChars = false
+
+        // Spice password
+        config.VMSpicePwdLength = 32
+        config.VMSpicePwdDigitCount = 10
+        config.VMSpicePwdSymbolCount = 0
+        config.VMSpicePwdRepeatChars = true
+
+        // VM attach password
+        config.VMAttachPwdLength = 10
+        config.VMAttachPwdDigitCount = 4
+        config.VMAttachPwdSymbolCount = 0
+        config.VMAttachPwdRepeatChars = true
         
         config.UsersFile = filepath.Join(configDir, "/users.json")
         config.DataDir = dataDir
diff --git a/src/server/router/router.go b/src/server/router/router.go
index 445e1514afb1c4d63bddd29cba91268be2776d42..fc66077aaa84179bd325ad6ed926b023bbddc711 100644
--- a/src/server/router/router.go
+++ b/src/server/router/router.go
@@ -91,6 +91,8 @@ func (router *Router)Start(port int) {
     vmsGroup.PUT("/:id", router.vms.EditVM)
     vmsGroup.PUT("/:id/start", router.vms.StartVM)
     vmsGroup.PUT("/:id/startwithcreds", router.vms.StartVMWithCreds)
+    vmsGroup.POST("/:id/spicecreds", router.vms.VMSpiceCreds)
+    vmsGroup.POST("/spicecreds", router.vms.AnyVMSpiceCreds)
     vmsGroup.PUT("/:id/stop", router.vms.KillVM)
     vmsGroup.PUT("/:id/shutdown", router.vms.ShutdownVM)
     vmsGroup.PUT("/:id/reboot", router.vms.RebootVM)
diff --git a/src/server/router/routerTemplates.go b/src/server/router/routerTemplates.go
index f98ce7c7e9c1bda0dac96ca70e551d119a909c1e..1b8d15e79c926e9a6fdba475176462e6ef10f14b 100644
--- a/src/server/router/routerTemplates.go
+++ b/src/server/router/routerTemplates.go
@@ -236,7 +236,7 @@ func (r *RouterTemplates)DeleteTemplate(c echo.Context) error {
         if err := r.tpl.DeleteTemplate(tplID, r.vms); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
-        return c.JSON(http.StatusOK, "")
+        return c.NoContent(http.StatusOK)
     } else if user.HasCapability(caps.CAP_TPL_DESTROY) {
         template, err := r.tpl.GetTemplate(tplID)
         if err != nil {
@@ -247,7 +247,7 @@ func (r *RouterTemplates)DeleteTemplate(c echo.Context) error {
             if err := r.tpl.DeleteTemplate(tplID, r.vms); err != nil {
                 return echo.NewHTTPError(http.StatusNotFound, err.Error())
             }
-            return c.JSON(http.StatusOK, "")
+            return c.NoContent(http.StatusOK)
         }
     }
 
diff --git a/src/server/router/routerUsers.go b/src/server/router/routerUsers.go
index 5858fb046e9925cf5aa99a67cc133dd7679436a2..361a1de3e5a303848bc95d6a86db81d68d042534 100644
--- a/src/server/router/routerUsers.go
+++ b/src/server/router/routerUsers.go
@@ -76,7 +76,7 @@ func (r *RouterUsers)DeleteUserByEmail(c echo.Context) error {
         return echo.NewHTTPError(http.StatusNotFound, err.Error())
     }
 
-    return c.JSON(http.StatusOK, "")
+    return c.NoContent(http.StatusOK)
 }
 
 // Creates a user.
@@ -113,7 +113,7 @@ func (r *RouterUsers)CreateUser(c echo.Context) error {
         return echo.NewHTTPError(http.StatusBadRequest, err.Error())
     }
 
-    return c.JSON(http.StatusCreated, "")
+    return c.NoContent(http.StatusCreated)
 }
 
 // Update a user's capabilities.
@@ -155,7 +155,7 @@ func (r *RouterUsers)SetUserCaps(c echo.Context) error {
         return echo.NewHTTPError(http.StatusBadRequest, err.Error())
     }
 
-    return c.JSON(http.StatusOK, "")
+    return c.NoContent(http.StatusOK)
 }
 
 // Update the logged user's password.
@@ -186,7 +186,7 @@ func (r *RouterUsers)SetUserPwd(c echo.Context) error {
         return echo.NewHTTPError(http.StatusBadRequest, err.Error())
     }
 
-    return c.JSON(http.StatusOK, "")
+    return c.NoContent(http.StatusOK)
 }
 
 // Unlock a user.
@@ -212,7 +212,7 @@ func (r *RouterUsers)UnlockUser(c echo.Context) error {
         return echo.NewHTTPError(http.StatusBadRequest, err.Error())
     }
 
-    return c.JSON(http.StatusOK, "")
+    return c.NoContent(http.StatusOK)
 }
 
 // Reset a user's password.
diff --git a/src/server/router/routerVMs.go b/src/server/router/routerVMs.go
index ba358b0fe89e5228b8a4a795ee0b76fcf6836e9b..29fbd142af7508ec2be0c591bcea8a13f669f179 100644
--- a/src/server/router/routerVMs.go
+++ b/src/server/router/routerVMs.go
@@ -84,18 +84,18 @@ func (r *RouterVMs)GetListableVM(c echo.Context) error {
     }
 }
 
-// Returns VMs credentials for VMs that are running and that can be attached to.
+// Returns VMs Spice credentials for VMs that are running and that can be attached to.
 // Requires to be the VM's owner, or either capability:
 // User cap: CAP_VM_ATTACH_ANY: returns VMs credentials for all running VMs.
 // VM access cap: CAP_VM_ATTACH: returns VMs credentials for all running VMs with this cap for the logged user.
 // curl --cacert ca.pem -X GET https://localhost:1077/vms/attach -H "Authorization: Bearer <AccessToken>"
 func (r *RouterVMs)GetAttachableVMs(c echo.Context) error {
-    return r.getFilteredVMCredentials(c, caps.CAP_VM_ATTACH_ANY, caps.CAP_VM_ATTACH, func(vm *vms.VM) bool {
+    return r.getFilteredVMAttachCredentials(c, caps.CAP_VM_ATTACH_ANY, caps.CAP_VM_ATTACH, func(vm *vms.VM) bool {
         return vm.IsRunning()
     })
 }
 
-// Returns VM credentials for a VM specified by its UUID.
+// Returns VM Spice credentials for a VM specified by its UUID.
 // Requires to be the VM's owner, or either capability:
 // User cap: CAP_VM_ATTACH_ANY: returns VM credentials for the VM.
 // VM access cap: CAP_VM_ATTACH: returns VM credentials for the VM with this cap for the logged user.
@@ -123,16 +123,16 @@ func (r *RouterVMs)GetAttachableVM(c echo.Context) error {
 
     // If user has CAP_VM_ATTACH_ANY capability, returns the VM.
     if user.HasCapability(caps.CAP_VM_ATTACH_ANY) {
-        return c.JSONPretty(http.StatusOK, vm.SerializeCredentials(), "    ")
+        return c.JSONPretty(http.StatusOK, vm.SerializeAttachCredentials(), "    ")
     } else {
         if vm.IsOwner(user.Email) {
-            return c.JSONPretty(http.StatusOK, vm.SerializeCredentials(), "    ")
+            return c.JSONPretty(http.StatusOK, vm.SerializeAttachCredentials(), "    ")
         } else {
             capabilities, exists := vm.GetAccess()[user.Email]
             if exists {
                 _, found := capabilities[caps.CAP_VM_ATTACH]
                 if found {
-                    return c.JSONPretty(http.StatusOK, vm.SerializeCredentials(), "    ")
+                    return c.JSONPretty(http.StatusOK, vm.SerializeAttachCredentials(), "    ")
                 } else {
                     return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
                 }
@@ -143,6 +143,93 @@ func (r *RouterVMs)GetAttachableVM(c echo.Context) error {
     }
 }
 
+// Returns the Spice credentials for the specific VM and specific VM attach password.
+// Requires to be the VM's owner, or either capability:
+// User cap: CAP_VM_ATTACH_ANY: returns VM credentials for the VM.
+// VM access cap: CAP_VM_ATTACH: returns VM credentials for the VM with this cap for the logged user.
+// curl --cacert ca.pem -X POST https://localhost:1077/vms/62ae8791-c108-4235-a7d6-074e9b6a9017/spicecreds -H 'Content-Type: application/json' -d '{"pwd":"46L8drgZ5Dx"}' -H "Authorization: Bearer <AccessToken>"
+func (r *RouterVMs)VMSpiceCreds(c echo.Context) error {
+    // Retrieves logged user from context.
+    user, err := getLoggedUser(r.users, c)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
+    }
+
+    // Retrieves the VM based on its UUID.
+    id, err := uuid.Parse(c.Param("id"))
+    if err != nil {
+        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+    }
+    vm, err := r.vms.GetVM(id)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusNotFound, err.Error())
+    }
+
+    if !vm.IsRunning() {
+        return echo.NewHTTPError(http.StatusBadRequest, errors.New("VM is not running"))
+    }
+
+    // Deserializes and validates client's parameters.
+    p := new(params.VMAttachCreds)
+    if err := decodeJson(c, &p); err != nil {
+        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+    }
+
+    // Does the received password match the "VM Attach" password stored in the VM?
+    if p.Pwd != vm.Run.AttachPwd {
+        return echo.NewHTTPError(http.StatusUnauthorized, errors.New("Access denied"))
+    }
+
+    // If user has CAP_VM_ATTACH_ANY capability, returns the VM.
+    if user.HasCapability(caps.CAP_VM_ATTACH_ANY) {
+        return c.JSONPretty(http.StatusOK, vm.SerializeSpiceCredentials(), "    ")
+    } else {
+        if vm.IsOwner(user.Email) {
+            return c.JSONPretty(http.StatusOK, vm.SerializeSpiceCredentials(), "    ")
+        } else {
+            capabilities, exists := vm.GetAccess()[user.Email]
+            if exists {
+                _, found := capabilities[caps.CAP_VM_ATTACH]
+                if found {
+                    return c.JSONPretty(http.StatusOK, vm.SerializeSpiceCredentials(), "    ")
+                } else {
+                    return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
+                }
+            } else {
+                return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
+            }
+        }
+    }
+}
+
+// Returns the Spice credentials for the VM matching the specific VM attach password.
+// Requires the CAP_VM_ATTACH_ANY user capability:
+// curl --cacert ca.pem -X POST https://localhost:1077/vms/spicecreds -H 'Content-Type: application/json' -d '{"pwd":"46L8drgZ5Dx"}' -H "Authorization: Bearer <AccessToken>"
+func (r *RouterVMs)AnyVMSpiceCreds(c echo.Context) error {
+    // Retrieves logged user from context.
+    user, err := getLoggedUser(r.users, c)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
+    }
+
+    if user.HasCapability(caps.CAP_VM_ATTACH_ANY) {
+        return echo.NewHTTPError(http.StatusUnauthorized, errors.New("Access denied"))
+    }
+
+    // Deserializes and validates client's parameters.
+    p := new(params.VMAttachCreds)
+    if err := decodeJson(c, &p); err != nil {
+        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+    }
+
+    vm, err := r.vms.GetVMByPwd(p.Pwd)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusNotFound, err.Error())
+    }
+
+    return c.JSONPretty(http.StatusOK, vm.SerializeSpiceCredentials(), "    ")
+}
+
 // Returns VMs that are stopped and that can be deleted.
 // Requires to be the VM's owner, or either capability:
 // User cap: CAP_VM_DESTROY_ANY: returns all VMs.
@@ -311,7 +398,7 @@ func (r *RouterVMs)DeleteVM(c echo.Context) error {
         if err := r.vms.DeleteVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
-        return c.JSON(http.StatusOK, "")
+        return c.NoContent(http.StatusOK)
     })
 }
 
@@ -322,19 +409,20 @@ func (r *RouterVMs)DeleteVM(c echo.Context) error {
 // curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/start -H "Authorization: Bearer <AccessToken>"
 func (r *RouterVMs)StartVM(c echo.Context) error {
     return r.applyOnFilteredVMs(c, caps.CAP_VM_START_ANY, caps.CAP_VM_START, func(c echo.Context, vm *vms.VM) error {
-        _, _, err := r.vms.StartVM(vm.GetID())
+        // "" means a unique password to attach to the VM must be generated
+        err := r.vms.StartVM(vm.GetID(), "")
         if err != nil {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
-        return c.JSON(http.StatusOK, "")
+        return c.NoContent(http.StatusOK)
     })
 }
 
-// Starts a VM based on its ID using the specified credentials (port and password).
+// Starts a VM based on its ID using the specified credentials (attach password).
 // Requires to be the VM's owner, or either capability:
 // User cap: CAP_VM_START_ANY: any VM can be started.
 // VM access cap: CAP_VM_START: any of the VMs with this cap for the logged user can be started.
-// curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/startwithcreds -H "Authorization: Bearer <AccessToken>"
+// curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/startwithcreds -H 'Content-Type: application/json' -d '{"pwd":"46L8drgZ5Dx"}' -H "Authorization: Bearer <AccessToken>"
 func (r *RouterVMs)StartVMWithCreds(c echo.Context) error {
     return r.applyOnFilteredVMs(c, caps.CAP_VM_START_ANY, caps.CAP_VM_START, func(c echo.Context, vm *vms.VM) error {
         // Deserializes and validates client's parameters.
@@ -343,10 +431,10 @@ func (r *RouterVMs)StartVMWithCreds(c echo.Context) error {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
 
-        if err := r.vms.StartVMWithCreds(vm.GetID(), p.Port, true, p.Pwd); err != nil {
+        if err := r.vms.StartVM(vm.GetID(), p.Pwd); err != nil {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
-        return c.JSON(http.StatusOK, "")
+        return c.NoContent(http.StatusOK)
     })
 }
 
@@ -360,7 +448,7 @@ func (r *RouterVMs)KillVM(c echo.Context) error {
         if err := r.vms.KillVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
-        return c.JSON(http.StatusOK, "")
+        return c.NoContent(http.StatusOK)
     })
 }
 
@@ -374,7 +462,7 @@ func (r *RouterVMs)ShutdownVM(c echo.Context) error {
         if err := r.vms.ShutdownVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
-        return c.JSON(http.StatusOK, "")
+        return c.NoContent(http.StatusOK)
     })
 }
 
@@ -388,7 +476,7 @@ func (r *RouterVMs)RebootVM(c echo.Context) error {
         if err := r.vms.RebootVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
-        return c.JSON(http.StatusOK, "")
+        return c.NoContent(http.StatusOK)
     })
 }
 
@@ -451,7 +539,7 @@ func (r *RouterVMs)SetVMAccessForUser(c echo.Context) error {
         return echo.NewHTTPError(http.StatusBadRequest, err.Error())
     }
 
-    return c.JSON(http.StatusOK, "")
+    return c.NoContent(http.StatusOK)
 }
 
 // Delete a VM Access for a given user.
@@ -480,7 +568,7 @@ func (r *RouterVMs)DeleteVMAccessForUser(c echo.Context) error {
         return echo.NewHTTPError(http.StatusBadRequest, err.Error())
     }
 
-    return c.JSON(http.StatusOK, "")
+    return c.NoContent(http.StatusOK)
 }
 
 // Exports a VM's directory into a compressed archive.
@@ -559,7 +647,7 @@ func (r *RouterVMs)ImportFilesToVM(c echo.Context) error {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
 
-        return c.JSON(http.StatusCreated, "")
+        return c.NoContent(http.StatusCreated)
     })
 }
 
@@ -599,12 +687,7 @@ func (r *RouterVMs)getFilteredVMs(c echo.Context, userCapabilityAny, vmAccessCap
     }
 }
 
-// Helper function that returns a list of serialized VM credentials if either condition is true:
-// - the logged user has the userCapabilityAny capability.
-// - the logged user is the VM's owner.
-// - the VM access for the logged user matches the vmAccessCapability capability.
-// Additionally, VMs for which the cond function returns false are filtered out.
-func (r *RouterVMs)getFilteredVMCredentials(c echo.Context, userCapabilityAny, vmAccessCapability string, cond vms.VMKeeperFn) error {
+func (r *RouterVMs)getFilteredVMAttachCredentials(c echo.Context, userCapabilityAny, vmAccessCapability string, cond vms.VMKeeperFn) error {
     // Retrieves logged user from context.
     user, err := getLoggedUser(r.users, c)
     if err != nil {
@@ -614,12 +697,12 @@ func (r *RouterVMs)getFilteredVMCredentials(c echo.Context, userCapabilityAny, v
     // If user has the XX_ANY capability, returns all VMs.
     if user.HasCapability(userCapabilityAny) {
         // Returns all VMs that pass the condition.
-        return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMCredentials(cond), "    ")
+        return c.JSONPretty(http.StatusOK, r.vms.GetVMAttachCredentialsSerialized(cond), "    ")
     } else {
         // Returns all VMs:
         // - owned by the user
         // - for which the user has the specified capability (vmAccessCapability) in the VM's access
-        return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMCredentials(func(vm *vms.VM) bool {
+        return c.JSONPretty(http.StatusOK, r.vms.GetVMAttachCredentialsSerialized(func(vm *vms.VM) bool {
             if vm.IsOwner(user.Email) {
                 return cond(vm)
             } else {
diff --git a/src/server/version/version.go b/src/server/version/version.go
index f406b364bdec025db6d2c29b5eaed18fc66f35af..2b4b5abd8935ff089a7a2b38873c47cb6ae4dc69 100644
--- a/src/server/version/version.go
+++ b/src/server/version/version.go
@@ -6,8 +6,8 @@ import (
 
 const (
     major  = 1
-    minor  = 10
-    bugfix = 1
+    minor  = 11
+    bugfix = 0
 )
 
 var version params.Version = params.NewVersion(major, minor, bugfix)
diff --git a/src/server/vms/vm.go b/src/server/vms/vm.go
index 681e35db4e4dca7ea2273168d7abec51c1a1b2ac..5f9f00b2c3904b9d19473d5c99e3042ceff5a47f 100644
--- a/src/server/vms/vm.go
+++ b/src/server/vms/vm.go
@@ -26,15 +26,16 @@ type (
         dir string         // VM directory
         qgaSock string     // QEMU Guest Agent (QGA) UNIX socket
         Run runStates
-        DiskBusy bool      // When true, the disk image must not be modified (e.g. write access)
+        DiskBusy bool      // When true, the disk image MUST NOT be modified (e.g. write access)
         mutex *sync.Mutex
     }
 
     runStates struct {
         State vmc.VMState
-        Pid int
-        Port int
-        Pwd string
+        Pid int           // QEMU process ID
+        SpicePort int     // Port for Spice access
+        SpicePwd string   // Password for Spice access
+        AttachPwd string  // Password required to attach to this VM
         StartTime time.Time
     }
 
@@ -117,12 +118,18 @@ func (vm *VM)Serialize() vmc.VMNetworkSerialized {
 
 // Returns a serialized version of the VM with basic fields and credentials.
 // Concurrency: safe
-func (vm *VM)SerializeCredentials() vmc.VMCredentialsSerialized {
-    return vmc.VMCredentialsSerialized {
+func (vm *VM)SerializeAttachCredentials() vmc.VMAttachCredentialsSerialized {
+    return vmc.VMAttachCredentialsSerialized {
         ID: vm.v.ID,
         Name: vm.v.Name,
-        Port: vm.Run.Port,
-        Pwd: vm.Run.Pwd,
+        Pwd: vm.Run.AttachPwd,
+    }
+}
+
+func (vm *VM)SerializeSpiceCredentials() vmc.VMSpiceCredentialsSerialized {
+    return vmc.VMSpiceCredentialsSerialized {
+        SpicePort: vm.Run.SpicePort,
+        SpicePwd: vm.Run.SpicePwd,
     }
 }
 
@@ -180,7 +187,7 @@ func newEmptyVM() *VM {
         },
         dir: "",
         qgaSock: "",
-        Run: runStates { State: vmc.StateStopped, Pid: 0, Port: 0, Pwd: "" },
+        Run: runStates { State: vmc.StateStopped, Pid: 0, SpicePort: 0, SpicePwd: "" },
         DiskBusy: false,
         mutex: new(sync.Mutex),
     }
diff --git a/src/server/vms/vms.go b/src/server/vms/vms.go
index 00b7c99caa4acc96cdffb3f289feaa2d717c7539..94c1d2d06c691fca94b170ad15a11850317757ca 100644
--- a/src/server/vms/vms.go
+++ b/src/server/vms/vms.go
@@ -8,6 +8,7 @@ import (
     "time"
     "errors"
     "syscall"
+    osexec "os/exec"
     "io/ioutil"
     "path/filepath"
     "encoding/base64"
@@ -27,11 +28,14 @@ type (
     VMKeeperFn func(vm *VM) bool
 
     VMs struct {
-        m map[string]*VM
-        dir string             // Base directory where VMs are stored
-        rwlock *sync.RWMutex   // RWlock to ensure the VMs' map (m) coherency
-        usedPorts [65536]bool  // Ports used by VMs for spice
-        usedRAM int            // RAM used by running VMs (in MB)
+        m map[string]*VM        // Map of VM IDs to VMs
+        pwdToVM map[string]uuid.UUID  // Map of VM attach passwords to VM IDs
+        mutexPwdToVM *sync.Mutex
+        dir string              // Base directory where VMs are stored
+        rwlock *sync.RWMutex    // RWlock to ensure the VMs' map (m) coherency
+        usedPorts [65536]bool   // Ports used by VMs for spice
+        mutexUsedPorts *sync.Mutex
+        usedRAM int             // RAM used by running VMs (in MB)
     }
 )
 
@@ -48,10 +52,10 @@ func GetVMsInstance() *VMs {
 
 // Creates all VMs from their files on disk.
 // REMARK: path is the root directory where VMs reside.
-// Concurrency: unsafe
+// Concurrency: unsafe!
 func InitVMs() error {
     vmsDir := conf.VMsDir
-    vms = &VMs { m: make(map[string]*VM), dir: vmsDir, rwlock: new(sync.RWMutex), usedRAM: 0 }
+    vms = &VMs { m: make(map[string]*VM), pwdToVM: make(map[string]uuid.UUID), mutexPwdToVM: new(sync.Mutex), dir: vmsDir, rwlock: new(sync.RWMutex), mutexUsedPorts: new(sync.Mutex), usedRAM: 0 }
     for i, _ := range vms.usedPorts {
         vms.usedPorts[i] = false
     }
@@ -122,16 +126,16 @@ func (vms *VMs)GetNetworkSerializedVMs(keepFn VMKeeperFn) []vmc.VMNetworkSeriali
     return list
 }
 
-// Returns the list of serialized VM credentials for which VMKeeperFn is true.
+// Returns the list of serialized "VM attach" credentials for which VMKeeperFn is true.
 // Concurrency: safe
-func (vms *VMs)GetNetworkSerializedVMCredentials(keepFn VMKeeperFn) []vmc.VMCredentialsSerialized {
+func (vms *VMs)GetVMAttachCredentialsSerialized(keepFn VMKeeperFn) []vmc.VMAttachCredentialsSerialized {
     vms.rwlock.RLock()
 
-    list := []vmc.VMCredentialsSerialized{}
+    list := []vmc.VMAttachCredentialsSerialized{}
     for _, vm := range vms.m {
         vm.mutex.Lock()
         if keepFn(vm) {
-            list = append(list, vm.SerializeCredentials())
+            list = append(list, vm.SerializeAttachCredentials())
         }
         vm.mutex.Unlock()
     }
@@ -152,8 +156,20 @@ func (vms *VMs)GetVM(vmID uuid.UUID) (*VM, error) {
     return vms.getVMUnsafe(vmID)
 }
 
+// Returns the running VM matching a "access password".
+// Concurrency: safe
+func (vms *VMs)GetVMByPwd(pwd string) (*VM, error) {
+    vms.rwlock.RLock()
+    defer vms.rwlock.RUnlock()
+    vmID, exists := vms.pwdToVM[pwd]
+    if !exists {
+        return nil, errors.New("VM not found")
+    }
+    return vms.getVMUnsafe(vmID)
+}
+
 // Returns a VM by its ID. Concurrency-unsafe version.
-// Concurrency: unsafe
+// Concurrency: unsafe!
 func (vms *VMs)getVMUnsafe(vmID uuid.UUID) (*VM, error) {
     vm, exists := vms.m[vmID.String()]
     if !exists {
@@ -215,43 +231,75 @@ func (vms *VMs)AddVM(vm *VM) error {
     return nil
 }
 
-// Starts a VM by its ID, using the specified port and password.
-// If checkPort is true, a check is performed on the specified port and if it is already
-// in use, the function fails and returns a corresponding error.
+// Prepares a VM to be started.
+// If attachPwd is empty, then a random unique password is generated, otherwise the password
+// in argument is used.
 // Concurrency: safe
-func (vms *VMs)StartVMWithCreds(vmID uuid.UUID, port int, checkPort bool, pwd string) error {
+func (vms *VMs)prepareStartVM(vmID uuid.UUID, attachPwd string) (*osexec.Cmd, *VM, string, int, int, error) {
     prefix := "Failed starting VM: "
+    var err error
+    prepareCompleted := false;
+
+    // Randomly generates a unique password used to attach to the VM
+    if attachPwd == "" {
+        attachPwd, err = vms.allocateAttachPwd(vmID)
+        if err != nil {
+            msg := prefix+vmID.String()+": attach password generation error: "+err.Error()
+            log.Error(msg)
+            return nil, nil, "", 0, 0, errors.New(msg)
+        }
+    }
+
+    // Function that frees the allocated "VM attach" password.
+    freeAttachPwdFn := func(attachPwd string) {
+        if !prepareCompleted { vms.freeAttachPwd(attachPwd) }
+    }
+    defer freeAttachPwdFn(attachPwd)
+
+    // Randomly generates a password for Spice
+    spicePwd, err := utils.CustomPwd.Generate(conf.VMSpicePwdLength, conf.VMSpicePwdDigitCount, conf.VMSpicePwdSymbolCount, false, conf.VMSpicePwdRepeatChars)
+    if err != nil {
+        msg := prefix+vmID.String()+": Spice password generation error: "+err.Error()
+        log.Error(msg)
+        return nil, nil, "", 0, 0, errors.New(msg)
+    }
+
+    // Allocates a free random port for Spice
+    spicePort, err := vms.allocateSpiceRandomPort()
+    if err != nil {
+        msg := prefix+vmID.String()+": "+err.Error()
+        log.Error(msg)
+        return nil, nil, "", 0, 0, errors.New(msg)
+    }
+
+    // Function that frees the port allocated for Spice.
+    freeSpiceRandomPortFn := func(spicePort int) {
+        if !prepareCompleted { vms.freeSpiceRandomPort(spicePort) }
+    }
+    defer freeSpiceRandomPortFn(spicePort)
+
+    totalRAM, availRAM, err := utils.GetRAM()
+    if err != nil {
+        return nil, nil, "", 0, 0, errors.New(prefix+"failed obtaining memory info: "+err.Error())
+    }
 
     vms.rwlock.Lock()
     defer vms.rwlock.Unlock()
 
     vm, err := vms.getVMUnsafe(vmID)
     if err != nil {
-        return err
+        return nil, nil, "", 0, 0, err
     }
 
     vm.mutex.Lock()
     defer vm.mutex.Unlock()
 
     if vm.IsRunning() {
-        return errors.New(prefix+"already running")
+        return nil, nil, "", 0, 0, errors.New(prefix+"already running")
     }
 
     if vm.IsDiskBusy() {
-        return errors.New(prefix+"disk in use (busy)")
-    }
-
-    if checkPort {
-        if vms.usedPorts[port] {
-            return errors.New(prefix+"port already in use")
-        } else if !utils.IsPortAvailable(port) {
-            return errors.New(prefix+"port not available")
-        }
-    }
-
-    totalRAM, availRAM, err := utils.GetRAM()
-    if err != nil {
-        return errors.New(prefix+"failed obtaining memory info: "+err.Error())
+        return nil, nil, "", 0, 0, errors.New(prefix+"disk in use (busy)")
     }
 
     // We estimate ~30% of RAM saving thanks to KSM (due to page sharing across VMs).
@@ -263,95 +311,91 @@ func (vms *VMs)StartVMWithCreds(vmID uuid.UUID, port int, checkPort bool, pwd st
     // otherwise, refuses to run it in order to avoid RAM saturation.
     if availRAM - vms.usedRAM <= int(math.Round(float64(totalRAM)*(1.-conf.Limits.RamUsageLimit))) {
         vms.usedRAM -= estimatedVmRAM
-        return errors.New(prefix+"insufficient free RAM")
+        return nil, nil, "", 0, 0, errors.New(prefix+"insufficient free RAM")
     }
 
-    // Function that executes the VM in QEMU using the specified spice port and password.
-    runQemuFn := func(vm *VM, port int, pwd, pwdFile string, endofExecFn endOfExecCallback) error {
-        certsDir := conf.CertsDir
-        cmd, err := exec.NewQemuSystem(vm.qgaSock, vm.v.Cpus, vm.v.Ram, string(vm.v.Nic), vm.v.UsbDevs, filepath.Join(vm.dir, vmDiskFile), port, pwdFile, certsDir)
-        if err != nil {
-            log.Error(prefix+"filepath join error: "+err.Error())
-            return err
-        }
-
-        if err := cmd.Start(); err != nil {
-            log.Error(prefix+vm.v.ID.String()+": exec.Start error: "+err.Error())
-            log.Error("Failed cmd: "+cmd.String())
-            return err
-        }
-
-        vm.Run = runStates { State: vmc.StateRunning, Pid: cmd.Process.Pid, Port: port, Pwd: pwd, StartTime: time.Now() }
-        vm.DiskBusy = true
-
-        // Go routine that executes cmd.Wait() which is a blocking call (hence the go routine).
-        // From here on, there are 2 flows of execution!
-        go func() {
-            if err := cmd.Wait(); err != nil {
-                log.Error(prefix+vm.v.ID.String()+": exec.Wait error: "+err.Error())
-                log.Error("Failed cmd: "+cmd.String())
-            }
-            endofExecFn(vm)
-        } ()
-
-        return nil
+    // Writes the password in Base64 in a secret file inside the VM's directory.
+    pwdBase64 := base64.StdEncoding.EncodeToString([]byte(spicePwd))
+    content := []byte(pwdBase64)
+    spicePwdFile := filepath.Join(vm.dir, vmSecretFile)
+    if err := ioutil.WriteFile(spicePwdFile, content, 0600); err != nil {
+        msg := prefix+"error creating secret file: "+err.Error()
+        log.Error(msg)
+        return nil, nil, "", 0, 0, errors.New(msg)
     }
 
     // Function that deletes a VM's secret file.
     removeSecretFileFn := func(vm *VM) {
-        pwdFile := filepath.Join(vm.dir, vmSecretFile)
-        os.Remove(pwdFile)
-    }
-
-    // Function that starts a VM on the given port with the given password.
-    // endofExecFn is called once the VM's execution is over.
-    startFn := func(vm *VM, port int, pwd string, endofExecFn endOfExecCallback) error {
-        // Writes the password in Base64 in a secret file inside the VM's directory.
-        pwdBase64 := base64.StdEncoding.EncodeToString([]byte(pwd))
-        content := []byte(pwdBase64)
-        pwdFile := filepath.Join(vm.dir, vmSecretFile)
-        if err := ioutil.WriteFile(pwdFile, content, 0600); err != nil {
-            msg := prefix+"error creating secret file: "+err.Error()
-            log.Error(msg)
-            return errors.New(msg)
+        if !prepareCompleted {
+            spicePwdFile := filepath.Join(vm.dir, vmSecretFile)
+            os.Remove(spicePwdFile)
         }
-
-        if err = runQemuFn(vm, port, pwd, pwdFile, endofExecFn); err != nil {
-            removeSecretFileFn(vm)
-            os.Remove(vm.qgaSock)  // If QEMU fails it's likely the Guest Agent file it created is still there.
-            return errors.New(prefix+"error running QEMU: "+err.Error())
-        }
-
-        return nil
     }
+    defer removeSecretFileFn(vm)
 
-    // Function that resets the VM's states, frees its port and deletes its secret file.
-    // Called once the VM's execution is over.
-    endofExecFn := func(vm *VM) {
-        vms.rwlock.Lock()
-        vms.usedPorts[port] = false
-        vms.usedRAM -= estimatedVmRAM
-        vms.rwlock.Unlock()
-        vm.mutex.Lock()
-        removeSecretFileFn(vm)
-        // Resets VM states and disk busy flag.
-        zeroTime := time.Time{}
-        vm.Run = runStates { State: vmc.StateStopped, Pid: 0, Port: 0, Pwd: "", StartTime: zeroTime }
-        vm.DiskBusy = false
-        vm.mutex.Unlock()
+    // Executes the QEMU process.
+    certsDir := conf.CertsDir
+    cmd, err := exec.NewQemuSystem(vm.qgaSock, vm.v.Cpus, vm.v.Ram, string(vm.v.Nic), vm.v.UsbDevs, filepath.Join(vm.dir, vmDiskFile), spicePort, spicePwdFile, certsDir)
+    if err != nil {
+        os.Remove(vm.qgaSock)  // If QEMU fails it's likely the Guest Agent file it created is still there.
+        log.Error(prefix+"filepath join error: "+err.Error())
+        return nil, nil, "", 0, 0, err
+    }
+    if err := cmd.Start(); err != nil {
+        os.Remove(vm.qgaSock)  // If QEMU fails it's likely the Guest Agent file it created is still there.
+        log.Error(prefix+vm.v.ID.String()+": exec.Start error: "+err.Error())
+        log.Error("Failed cmd: "+cmd.String())
+        return nil, nil, "", 0, 0, err
     }
 
-    vms.usedPorts[port] = true
+    // Updates the VM's running states.
+    vm.Run = runStates { State: vmc.StateRunning, Pid: cmd.Process.Pid, SpicePort: spicePort, SpicePwd: spicePwd, AttachPwd: attachPwd, StartTime: time.Now() }
+    vm.DiskBusy = true
+
+    prepareCompleted = true
+
+    return cmd, vm, attachPwd, spicePort, estimatedVmRAM, nil
+}
+
+// Starts a VM by its ID.
+// If attachPwd is empty, then a random unique password is generated, otherwise the password
+// in argument is used.
+// Concurrency: safe
+func (vms *VMs)StartVM(vmID uuid.UUID, attachPwd string) error {
+    prefix := "Failed starting VM[2]: "
 
-    if err = startFn(vm, port, pwd, endofExecFn); err != nil {
-        vms.usedPorts[port] = false
+    cmd, vm, attachPwd, spicePort, estimatedVmRAM, err := vms.prepareStartVM(vmID, attachPwd)
+    if err != nil {
         return err
+    } else {
+        go func() {
+            if err := cmd.Wait(); err != nil {
+                log.Error(prefix+vm.v.ID.String()+": exec.Wait error: "+err.Error())
+                log.Error("Failed cmd: "+cmd.String())
+            }
+
+            // Resets the VM's running states, frees its port, free its attach pwd and deletes its secret file.
+            // Executed only when the VM's execution is over, i.e. when the QEMU process terminates (both normal and aborted terminations).
+            vms.freeAttachPwd(attachPwd)
+            vms.freeSpiceRandomPort(spicePort)
+            vms.rwlock.Lock()
+            vms.usedRAM -= estimatedVmRAM
+            vms.rwlock.Unlock()
+            vm.mutex.Lock()
+            spicePwdFile := filepath.Join(vm.dir, vmSecretFile)
+            os.Remove(spicePwdFile)
+            // Resets VM states and disk busy flag.
+            zeroTime := time.Time{}
+            vm.Run = runStates { State: vmc.StateStopped, Pid: 0, SpicePort: 0, SpicePwd: "", AttachPwd: "", StartTime: zeroTime }
+            vm.DiskBusy = false
+            vm.mutex.Unlock()
+        } ()
     }
 
     return nil
 }
 
-// Allocates and returns a free port, randomly chosen between [VMSpiceMinPort,VMSpiceMaxPort].
+// Allocates and returns a free port for Spice, randomly chosen between [VMSpiceMinPort,VMSpiceMaxPort].
 // When the number of free port becomes very small,
 // randomly picking a port is very time consuming (many attempts).
 // TODO: A better approach would be to do this instead:
@@ -359,9 +403,9 @@ func (vms *VMs)StartVMWithCreds(vmID uuid.UUID, port int, checkPort bool, pwd st
 // Otherwise: pick the first available one either from the beginning or the end (randomly).
 // REMARK: this function updates the vms map.
 // Concurrency: safe
-func (vms *VMs)allocateFreeRandomPort() (int, error) {
-    vms.rwlock.Lock()
-    defer vms.rwlock.Unlock()
+func (vms *VMs)allocateSpiceRandomPort() (int, error) {
+    vms.mutexUsedPorts.Lock()
+    defer vms.mutexUsedPorts.Unlock()
 
     minPort := conf.Core.VMSpiceMinPort
     maxPort := conf.Core.VMSpiceMaxPort
@@ -390,30 +434,41 @@ func (vms *VMs)allocateFreeRandomPort() (int, error) {
     }
 }
 
-// Starts a VM by its ID using randomly generated port number and password.
-// Returns the port on which the VM is running and the access password.
+// Frees a port previously allocated with allocateSpiceRandomPort().
 // Concurrency: safe
-func (vms *VMs)StartVM(vmID uuid.UUID) (int, string, error) {
-    port, err := vms.allocateFreeRandomPort()
-    if err != nil {
-        msg := "Failed starting VM "+vmID.String()+": "+err.Error()
-        log.Error(msg)
-        return -1, "", errors.New(msg)
-    }
+func (vms *VMs)freeSpiceRandomPort(port int) {
+    vms.mutexUsedPorts.Lock()
+    defer vms.mutexUsedPorts.Unlock()
+    vms.usedPorts[port] = false
+}
 
-    // Randomly generates a 8 characters long password with 4 digits, 0 symbols,
-    // allowing upper and lower case letters, disallowing repeat characters.
-    pwd, err := utils.CustomPwd.Generate(conf.VMPwdLength, conf.VMPwdDigitCount, conf.VMPwdSymbolCount, false, conf.VMPwdRepeatChars)
-    if err != nil {
-        msg := "Failed starting VM "+vmID.String()+": password generation error: "+err.Error()
-        log.Error(msg)
-        return -1, "", errors.New(msg)
-    }
+// Randomly generates a unique password as credentials to attach to a VM.
+// Returns the generated password.
+// Concurrency: safe
+func (vms *VMs)allocateAttachPwd(vmID uuid.UUID) (string, error) {
+    vms.mutexPwdToVM.Lock()
+    defer vms.mutexPwdToVM.Unlock()
 
-    if err = vms.StartVMWithCreds(vmID, port, false, pwd); err != nil {
-        return -1, "", err
+    for {
+        attachPwd, err := utils.CustomPwd.Generate(conf.VMAttachPwdLength, conf.VMAttachPwdDigitCount, conf.VMAttachPwdSymbolCount, false, conf.VMAttachPwdRepeatChars)
+        if err != nil {
+            return "", err
+        }
+        _, exists := vms.pwdToVM[attachPwd]
+        if exists {
+            continue
+        }
+        vms.pwdToVM[attachPwd] = vmID
+        return attachPwd, nil
     }
-    return port, pwd, nil
+}
+
+// Frees a password that was previously allocated with allocateAttachPwd().
+// Concurrency: safe
+func (vms *VMs)freeAttachPwd(attachPwd string) {
+    vms.mutexPwdToVM.Lock()
+    defer vms.mutexPwdToVM.Unlock()
+    delete(vms.pwdToVM, attachPwd)
 }
 
 // Kills a VM by its ID.