Skip to content

Commit eac357c

Browse files
authored
kvm: Secure KVM VNC Console Access Using the CA Framework (#7015)
This PR allows securing the console access through CloudStack to the virtual machines running on KVM. The secure access is achieved through the generated certificates for the CA Framework in CloudStack, that provides mutual TLS connections between agents. These certificates are used to also secure the connection between the console proxies and the VNC ports for VM console access. This feature is only supported on the KVM hypervisor Design Document: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Secure+KVM+VNC+connection+using+the+CA+framework
1 parent 61a7225 commit eac357c

31 files changed

+1823
-71
lines changed

agent/bindir/cloud-setup-agent.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ from cloudutils.configFileOps import configFileOps
2626
from cloudutils.globalEnv import globalEnv
2727
from cloudutils.networkConfig import networkConfig
2828
from cloudutils.syscfg import sysConfigFactory
29-
from cloudutils.serviceConfig import configureLibvirtConfig
29+
from cloudutils.serviceConfig import configureLibvirtConfig, configure_libvirt_tls
3030

3131
from optparse import OptionParser
3232

@@ -115,6 +115,7 @@ if __name__ == '__main__':
115115

116116
if not options.auto and options.secure:
117117
configureLibvirtConfig(True)
118+
configure_libvirt_tls(True)
118119
print("Libvirtd with TLS configured")
119120
sys.exit(0)
120121

python/lib/cloudutils/serviceConfig.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,23 @@ def restore(self):
587587
class securityPolicyConfigSUSE(securityPolicyConfigRedhat):
588588
pass
589589

590+
591+
def configure_libvirt_tls(tls_enabled=False, cfo=None):
592+
save = False
593+
if not cfo:
594+
cfo = configFileOps("/etc/libvirt/qemu.conf")
595+
save = True
596+
597+
if tls_enabled:
598+
cfo.addEntry("vnc_tls", "1")
599+
cfo.addEntry("vnc_tls_x509_verify", "1")
600+
cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"")
601+
else:
602+
cfo.addEntry("vnc_tls", "0")
603+
604+
if save:
605+
cfo.save()
606+
590607
def configureLibvirtConfig(tls_enabled = True, cfg = None):
591608
cfo = configFileOps("/etc/libvirt/libvirtd.conf", cfg)
592609
if tls_enabled:
@@ -639,6 +656,7 @@ def config(self):
639656
cfo.addEntry("user", "\"root\"")
640657
cfo.addEntry("group", "\"root\"")
641658
cfo.addEntry("vnc_listen", "\"0.0.0.0\"")
659+
configure_libvirt_tls(self.syscfg.env.secure, cfo)
642660
cfo.save()
643661

644662
self.syscfg.svo.stopService("libvirtd")
@@ -676,6 +694,7 @@ def config(self):
676694
cfo.addEntry("user", "\"root\"")
677695
cfo.addEntry("group", "\"root\"")
678696
cfo.addEntry("vnc_listen", "\"0.0.0.0\"")
697+
configure_libvirt_tls(self.syscfg.env.secure, cfo)
679698
cfo.save()
680699

681700
self.syscfg.svo.stopService("libvirtd")
@@ -729,6 +748,7 @@ def config(self):
729748
cfo.addEntry("security_driver", "\"none\"")
730749
cfo.addEntry("user", "\"root\"")
731750
cfo.addEntry("group", "\"root\"")
751+
configure_libvirt_tls(self.syscfg.env.secure, cfo)
732752
cfo.save()
733753

734754
if os.path.exists("/lib/systemd/system/libvirtd.service"):

scripts/util/keystore-cert-import

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ if [ -f "$LIBVIRTD_FILE" ]; then
114114
ln -sf /etc/cloudstack/agent/cloud.crt /etc/pki/libvirt/servercert.pem
115115
ln -sf /etc/cloudstack/agent/cloud.key /etc/pki/libvirt/private/clientkey.pem
116116
ln -sf /etc/cloudstack/agent/cloud.key /etc/pki/libvirt/private/serverkey.pem
117+
118+
# VNC TLS directory and certificates
119+
mkdir -p /etc/pki/libvirt-vnc
120+
ln -sf /etc/pki/CA/cacert.pem /etc/pki/libvirt-vnc/ca-cert.pem
121+
ln -sf /etc/pki/libvirt/servercert.pem /etc/pki/libvirt-vnc/server-cert.pem
122+
ln -sf /etc/pki/libvirt/private/serverkey.pem /etc/pki/libvirt-vnc/server-key.pem
117123
cloudstack-setup-agent -s > /dev/null
118124
fi
119125

server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class ConsoleProxyClientParam {
2222
private int clientHostPort;
2323
private String clientHostPassword;
2424
private String clientTag;
25+
private String clientDisplayName;
2526
private String ticket;
2627
private String locale;
2728
private String clientTunnelUrl;
@@ -85,6 +86,10 @@ public void setClientTag(String clientTag) {
8586
this.clientTag = clientTag;
8687
}
8788

89+
public String getClientDisplayName() { return this.clientDisplayName; }
90+
91+
public void setClientDisplayName(String clientDisplayName) { this.clientDisplayName = clientDisplayName; }
92+
8893
public String getTicket() {
8994
return ticket;
9095
}

server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.cloud.storage.GuestOSVO;
3434
import com.cloud.user.Account;
3535
import com.cloud.user.AccountManager;
36+
import com.cloud.uservm.UserVm;
3637
import com.cloud.utils.Pair;
3738
import com.cloud.utils.Ternary;
3839
import com.cloud.utils.component.ManagerBase;
@@ -285,11 +286,15 @@ private ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMach
285286
UserVmDetailVO details = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD);
286287

287288
String tag = vm.getUuid();
289+
String displayName = vm.getHostName();
290+
if (vm instanceof UserVm) {
291+
displayName = ((UserVm) vm).getDisplayName();
292+
}
288293

289294
String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag, sessionUuid);
290295
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword());
291296
ConsoleProxyClientParam param = generateConsoleProxyClientParam(parsedHostInfo, port, sid, tag, ticket,
292-
sessionUuid, addr, extraSecurityToken, vm, hostVo, details, portInfo, host);
297+
sessionUuid, addr, extraSecurityToken, vm, hostVo, details, portInfo, host, displayName);
293298
String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param);
294299
int vncPort = consoleProxyManager.getVncPort();
295300

@@ -353,12 +358,14 @@ private ConsoleProxyClientParam generateConsoleProxyClientParam(Ternary<String,
353358
String sessionUuid, String addr,
354359
String extraSecurityToken, VirtualMachine vm,
355360
HostVO hostVo, UserVmDetailVO details,
356-
Pair<String, Integer> portInfo, String host) {
361+
Pair<String, Integer> portInfo, String host,
362+
String displayName) {
357363
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
358364
param.setClientHostAddress(parsedHostInfo.first());
359365
param.setClientHostPort(port);
360366
param.setClientHostPassword(sid);
361367
param.setClientTag(tag);
368+
param.setClientDisplayName(displayName);
362369
param.setTicket(ticket);
363370
param.setSessionUuid(sessionUuid);
364371
param.setSourceIP(addr);

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException
7676
String portStr = queryMap.get("port");
7777
String sid = queryMap.get("sid");
7878
String tag = queryMap.get("tag");
79+
String displayName = queryMap.get("displayname");
7980
String ticket = queryMap.get("ticket");
8081
String ajaxSessionIdStr = queryMap.get("sess");
8182
String eventStr = queryMap.get("event");
@@ -129,6 +130,7 @@ private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException
129130
param.setClientHostPort(port);
130131
param.setClientHostPassword(sid);
131132
param.setClientTag(tag);
133+
param.setClientDisplayName(displayName);
132134
param.setTicket(ticket);
133135
param.setClientTunnelUrl(console_url);
134136
param.setClientTunnelSession(console_host_session);

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException
6969
String portStr = queryMap.get("port");
7070
String sid = queryMap.get("sid");
7171
String tag = queryMap.get("tag");
72+
String displayName = queryMap.get("displayname");
7273
String ticket = queryMap.get("ticket");
7374
String keyStr = queryMap.get("key");
7475
String console_url = queryMap.get("consoleurl");
@@ -113,6 +114,7 @@ private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException
113114
param.setClientHostPort(port);
114115
param.setClientHostPassword(sid);
115116
param.setClientTag(tag);
117+
param.setClientDisplayName(displayName);
116118
param.setTicket(ticket);
117119
param.setClientTunnelUrl(console_url);
118120
param.setClientTunnelSession(console_host_session);

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class ConsoleProxyClientParam {
2626
private int clientHostPort;
2727
private String clientHostPassword;
2828
private String clientTag;
29+
private String clientDisplayName;
2930
private String ticket;
3031

3132
private String clientTunnelUrl;
@@ -89,6 +90,10 @@ public void setClientTag(String clientTag) {
8990
this.clientTag = clientTag;
9091
}
9192

93+
public String getClientDisplayName() { return this.clientDisplayName; }
94+
95+
public void setClientDisplayName(String clientDisplayName) { this.clientDisplayName = clientDisplayName; }
96+
9297
public String getTicket() {
9398
return ticket;
9499
}

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ public static Map<String, String> getQueryMap(String query) {
7171
} else {
7272
s_logger.error("decode token. tag info is not found!");
7373
}
74+
if (param.getClientDisplayName() != null) {
75+
if (s_logger.isDebugEnabled()) {
76+
s_logger.debug("decode token. displayname: " + param.getClientDisplayName());
77+
}
78+
map.put("displayname", param.getClientDisplayName());
79+
} else {
80+
s_logger.error("decode token. displayname info is not found!");
81+
}
7482
if (param.getClientHostPassword() != null) {
7583
map.put("sid", param.getClientHostPassword());
7684
} else {

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.eclipse.jetty.websocket.api.Session;
3030
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
3131
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
32+
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
3233
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
3334
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
3435
import org.eclipse.jetty.websocket.api.extensions.Frame;
@@ -79,6 +80,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce
7980
String sid = queryMap.get("sid");
8081
String tag = queryMap.get("tag");
8182
String ticket = queryMap.get("ticket");
83+
String displayName = queryMap.get("displayname");
8284
String ajaxSessionIdStr = queryMap.get("sess");
8385
String console_url = queryMap.get("consoleurl");
8486
String console_host_session = queryMap.get("sessionref");
@@ -126,6 +128,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce
126128
param.setClientHostPassword(sid);
127129
param.setClientTag(tag);
128130
param.setTicket(ticket);
131+
param.setClientDisplayName(displayName);
129132
param.setClientTunnelUrl(console_url);
130133
param.setClientTunnelSession(console_host_session);
131134
param.setLocale(vm_locale);
@@ -174,4 +177,9 @@ public void onClose(Session session, int statusCode, String reason) throws IOExc
174177
public void onFrame(Frame f) throws IOException {
175178
viewer.sendClientFrame(f);
176179
}
180+
181+
@OnWebSocketError
182+
public void onError(Throwable cause) {
183+
s_logger.error("Error on websocket", cause);
184+
}
177185
}

0 commit comments

Comments
 (0)