diff --git a/client/cmdVM/vmAttachSync.go b/client/cmdVM/vmAttachSync.go
new file mode 100644
index 0000000000000000000000000000000000000000..193fc4f49df37680b8fcef92b94f49bc85418a8a
--- /dev/null
+++ b/client/cmdVM/vmAttachSync.go
@@ -0,0 +1,63 @@
+package cmdVM
+
+import (
+	"sync"
+
+	u "gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/client/utils"
+	nc "gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/nexusclient"
+)
+
+type AttachSync struct {
+	Name string
+}
+
+func (cmd *AttachSync) GetName() string {
+	return cmd.Name
+}
+
+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_ATTACH VM access capability or VM_ATTACH_ANY user capability."}
+}
+
+func (cmd *AttachSync) PrintUsage() {
+	printRegexUsage(cmd)
+	printRegexUsageDetails()
+}
+
+func (cmd *AttachSync) Run(nc *nc.NexusClient, args []string) int {
+	argc := len(args)
+	if argc < 1 {
+		cmd.PrintUsage()
+		return 1
+	}
+
+	credsList, err := getFilteredAttachVMsCreds(nc, args)
+	if err != nil {
+		u.PrintlnErr(err)
+		return 1
+	}
+
+	if len(credsList) == 0 {
+		u.PrintlnErr("No matching VMs.")
+	}
+
+	// Use wait groups to wait until all viewers threads have completed.
+	var wg sync.WaitGroup
+	wg.Add(len(credsList))
+
+	for _, creds := range credsList {
+		go func() {
+			err := nc.VMAttachFromCreds(creds, false)
+			if err != nil {
+				u.PrintlnErr("Failed attach to VM: " + err.Error())
+			}
+			wg.Done()
+		}()
+	}
+
+	wg.Wait()
+
+	return 0
+}
diff --git a/client/nexus-cli/nexus-cli.go b/client/nexus-cli/nexus-cli.go
index 3c33d28b3cd61557a2cec4e5bfa6b5de1cbd3cd1..2ba93cf86b81c48c338ecd86085fcbd2a8c3b993 100644
--- a/client/nexus-cli/nexus-cli.go
+++ b/client/nexus-cli/nexus-cli.go
@@ -49,7 +49,7 @@ var cmdList = []cmd.Command{
 
 	&cmdMisc.HelpHeader{Name: "!═════╡ VM commands ╞═════════════════════════════════════════════════════════════════"},
 	&cmdVM.AddAccess{Name: "vmaddaccess"},
-	&cmdVM.Attach{Name: "vmattach"},
+	&cmdVM.AttachSync{Name: "vmattach"},
 	// &cmdVM.AttachSingle{Name: "vmattachsingle"},   // for testing the route only
 	// &cmdVM.AttachFromPwd{Name: "vmattachfrompwd"},    // for testing the route only
 	&cmdVM.Create{Name: "vmcreate"},
@@ -119,7 +119,7 @@ func run() int {
 
 	found, cmd := cmd.Match(cmdName, cmdList)
 	if found {
-		exitFn = func() { nc.Close() }
+		exitFn = func() { nc.Cleanup() }
 
 		// This thread acts as a signal handler for SIGINT or SIGTERM.
 		// When one of these signals is received, the temporary certificate file is deleted.
diff --git a/client/nexus-exam/nexus-exam.go b/client/nexus-exam/nexus-exam.go
index 62283c7592fa2adedda249631b64947bf6f831d7..1e2b026ca747e974895c011934f29d4cb42c92c2 100644
--- a/client/nexus-exam/nexus-exam.go
+++ b/client/nexus-exam/nexus-exam.go
@@ -48,8 +48,8 @@ func exit(code int) {
 	os.Exit(code)
 }
 
-func attachVM(parent fyne.Window, certificateFile string, pwd string) {
-	err := client.VMAttachFromPwd(pwd, certificateFile, true)
+func attachVM(parent fyne.Window, pwd string) {
+	err := client.VMAttachFromPwd(pwd, true)
 	if err != nil {
 		errorPopup(parent, "Failed attaching to VM: "+err.Error())
 		return
@@ -102,7 +102,6 @@ func refreshToken(parent fyne.Window) {
 	for {
 		if isServerUp {
 			if !isAuthenticated {
-				// Logins and obtains a JWT token.
 				u.Println(time.Now(), ": initiate authentication")
 				_, err = client.Authenticate(nexus_exam_user, nexus_exam_pwd)
 				if err != nil {
@@ -118,7 +117,7 @@ func refreshToken(parent fyne.Window) {
 				}
 			} else {
 				u.Println(time.Now(), ": attempt to refresh token")
-				client.RefreshAuthToken()
+				client.RefreshToken()
 				if err != nil {
 					statusLabel.SetText("ERROR: failed to refresh token")
 					u.PrintlnErr(time.Now(), ": failed to refresh token: "+err.Error())
@@ -164,11 +163,7 @@ func run() int {
 		abortWindow(err.Error())
 	}
 
-	certFile, err := client.GetVMAttachCertificate()
-	if err != nil {
-		abortWindow(err.Error())
-	}
-	exitFn = func() { os.Remove(certFile) }
+	exitFn = func() { client.Cleanup() }
 
 	// This thread acts as a signal handler for SIGINT or SIGTERM.
 	// When one of these signals is received, the temporary certificate file is deleted.
@@ -212,7 +207,7 @@ func run() int {
 			{Text: "Password", Widget: pwdEntry},
 		},
 		OnSubmit: func() {
-			attachVM(win, certFile, pwdEntry.Text)
+			attachVM(win, pwdEntry.Text)
 		},
 		SubmitText: "Connect",
 	}
diff --git a/client/nexush/nexush.go b/client/nexush/nexush.go
index 5adbdf96870a64381f9eb6cd138c76043c4a0034..2f4755e8c3aa0ae948c6d9a6f2b0ef8aee4fa7b4 100644
--- a/client/nexush/nexush.go
+++ b/client/nexush/nexush.go
@@ -138,7 +138,7 @@ func run() int {
 		return 1
 	}
 
-	exitFn = func() { nc.Close() }
+	exitFn = func() { nc.Cleanup() }
 
 	// This thread acts as a signal handler for SIGINT or SIGTERM.
 	// When one of these signals is received, the temporary certificate file is deleted.
diff --git a/libclient/client.go b/libclient/client.go
index adfa41fd04e092b2e64cfeb3a8b57b366ff31275..0ffbd6fdad878de651bdc700c0b9974cf3a858d5 100644
--- a/libclient/client.go
+++ b/libclient/client.go
@@ -48,7 +48,7 @@ func New(host string) (*NexusClient, error) {
 // Authenticate a user.
 // curl --cacert ca-cert.pem -X POST https://localhost:8000/login -H 'Content-Type: application/json' -d '{"email": "johndoe@nexus.org", "pwd":"pipomolo"}'
 // Returns an JWT token if authentication succeeded or an error if it failed.
-// IMPORTANT: caller MUST call the Close() function before exiting!
+// IMPORTANT: caller MUST call the Cleanup() function before exiting!
 func (nc *NexusClient) Authenticate(user, pwd string) (string, error) {
 	loginArgs := &params.Login{user, pwd}
 	resp, err := nc.client.R().SetBody(loginArgs).Post("/login")
@@ -76,7 +76,7 @@ func (nc *NexusClient) Authenticate(user, pwd string) (string, error) {
 
 // "Authenticate" by using an already established connection using a previously received token.
 // Useful when wanting to access the API without authenticating.
-// IMPORTANT: caller MUST call the Close() function before exiting!
+// IMPORTANT: caller MUST call the Cleanup() function before exiting!
 func (nc *NexusClient) AuthenticateWithToken(token string) error {
 	nc.client.SetAuthToken(token)
 	// Download the CA certificate required by Spice clients
@@ -88,9 +88,9 @@ func (nc *NexusClient) AuthenticateWithToken(token string) error {
 	return nil
 }
 
-// Releases/deletes resources allocated by Authenticate and AuthenticateWithToken.
+// Cleanup resources allocated by Authenticate and AuthenticateWithToken.
 // IMPORTANT: caller MUST call this function before exiting!
-func (nc *NexusClient) Close() {
+func (nc *NexusClient) Cleanup() {
 	if nc.certFile != "" {
 		os.Remove(nc.certFile)
 	}
@@ -135,6 +135,8 @@ func (nc *NexusClient) getVMAttachCertFile() string {
 // The certificate file is saved in the OS' temporary directory.
 // Its name is randomly generated and garanteed to be unique.
 func (nc *NexusClient) downloadVMAttachCert() (string, error) {
+	nc.Cleanup()
+
 	uniqPostfix, err := uuid.NewRandom()
 	if err != nil {
 		return "", errors.New("Failed saving CA certificate, code 1:\n" + err.Error())