diff --git a/configure.ac b/configure.ac index df36eabe..d26db39f 100644 --- a/configure.ac +++ b/configure.ac @@ -1225,6 +1225,7 @@ AC_ARG_ENABLE([kubernetes], AM_CONDITIONAL([ENABLE_KUBERNETES], [test "x${enable_kubernetes}" = "xyes" \ -a "x${have_libwebsockets}" = "xyes" \ + -a "x${have_ssl}" = "xyes" \ -a "x${have_terminal}" = "xyes"]) # diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am index e818ff72..56db4d64 100644 --- a/src/protocols/kubernetes/Makefile.am +++ b/src/protocols/kubernetes/Makefile.am @@ -29,6 +29,7 @@ libguac_client_kubernetes_la_SOURCES = \ io.c \ pipe.c \ settings.c \ + ssl.c \ kubernetes.c \ url.c \ user.c @@ -40,6 +41,7 @@ noinst_HEADERS = \ io.h \ pipe.h \ settings.h \ + ssl.h \ kubernetes.h \ url.h \ user.h @@ -57,5 +59,6 @@ libguac_client_kubernetes_la_LIBADD = \ libguac_client_kubernetes_la_LDFLAGS = \ -version-info 0:0:0 \ @PTHREAD_LIBS@ \ + @SSL_LIBS@ \ @WEBSOCKETS_LIBS@ diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c index 58f4728a..331e03d7 100644 --- a/src/protocols/kubernetes/client.c +++ b/src/protocols/kubernetes/client.c @@ -32,12 +32,7 @@ #include #include -/** - * Static reference to the guac_client associated with the active Kubernetes - * connection. As guacd guarantees that each main client connection is - * isolated within its own process, this is safe. - */ -static guac_client* guac_kubernetes_lws_log_client = NULL; +guac_client* guac_kubernetes_lws_current_client = NULL; /** * Logging callback invoked by libwebsockets to log a single line of logging @@ -53,15 +48,18 @@ static guac_client* guac_kubernetes_lws_log_client = NULL; * The line of logging output to log. */ static void guac_kubernetes_log(int level, const char* line) { - if (guac_kubernetes_lws_log_client != NULL) - guac_client_log(guac_kubernetes_lws_log_client, GUAC_LOG_DEBUG, + if (guac_kubernetes_lws_current_client != NULL) + guac_client_log(guac_kubernetes_lws_current_client, GUAC_LOG_DEBUG, "libwebsockets: %s", line); } int guac_client_init(guac_client* client) { + /* Ensure reference to main guac_client remains available in all + * libwebsockets contexts */ + guac_kubernetes_lws_current_client = client; + /* Redirect libwebsockets logging */ - guac_kubernetes_lws_log_client = client; lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO, guac_kubernetes_log); diff --git a/src/protocols/kubernetes/client.h b/src/protocols/kubernetes/client.h index 0b847da4..ec4ba326 100644 --- a/src/protocols/kubernetes/client.h +++ b/src/protocols/kubernetes/client.h @@ -27,6 +27,13 @@ */ #define GUAC_KUBERNETES_CLIPBOARD_MAX_LENGTH 262144 +/** + * Static reference to the guac_client associated with the active Kubernetes + * connection. While libwebsockets provides some means of storing and + * retrieving custom data in some structures, this is not always available. + */ +extern guac_client* guac_kubernetes_lws_current_client; + /** * Free handler. Required by libguac and called when the guac_client is * disconnected and must be cleaned up. diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 4e7928ed..f314c597 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -18,9 +18,11 @@ */ #include "config.h" +#include "client.h" #include "common/recording.h" #include "io.h" #include "kubernetes.h" +#include "ssl.h" #include "terminal/terminal.h" #include "url.h" @@ -43,8 +45,9 @@ * The reason (event) that this callback was invoked. * * @param user - * Arbitrary data assocated with the WebSocket session. This will always - * be a pointer to the guac_client instance. + * Arbitrary data assocated with the WebSocket session. In some cases, + * this is actually event-specific data (such as the + * LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERT event). * * @param in * A pointer to arbitrary, reason-specific data. @@ -60,14 +63,19 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t length) { - /* Request connection closure if client is stopped (note that the user - * pointer passed by libwebsockets may be NULL for some events) */ - guac_client* client = (guac_client*) user; - if (client != NULL && client->state != GUAC_CLIENT_RUNNING) + guac_client* client = guac_kubernetes_lws_current_client; + + /* Do not handle any further events if connection is closing */ + if (client->state != GUAC_CLIENT_RUNNING) return lws_callback_http_dummy(wsi, reason, user, in, length); switch (reason) { + /* Complete initialization of SSL */ + case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS: + guac_kubernetes_init_ssl(client, (SSL_CTX*) user); + break; + /* Failed to connect */ case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, @@ -256,29 +264,13 @@ void* guac_kubernetes_client_thread(void* data) { }; /* If requested, use an SSL/TLS connection for communication with - * Kubernetes */ + * Kubernetes. Note that we disable hostname checks here because we + * do our own validation - libwebsockets does not validate properly if + * IP addresses are used. */ if (settings->use_ssl) { - - /* Enable use of SSL/TLS */ context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; - connection_info.ssl_connection = LCCSCF_USE_SSL; - - /* Bypass certificate checks if requested */ - if (settings->ignore_cert) { - connection_info.ssl_connection |= - LCCSCF_ALLOW_SELFSIGNED - | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK - | LCCSCF_ALLOW_EXPIRED; - } - - /* Otherwise use the given CA certificate to validate (if any) */ - else - context_info.client_ssl_ca_filepath = settings->ca_cert_file; - - /* Certificate and key file for SSL/TLS client auth */ - context_info.client_ssl_cert_filepath = settings->client_cert_file; - context_info.client_ssl_private_key_filepath = settings->client_key_file; - + connection_info.ssl_connection = LCCSCF_USE_SSL + | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; } /* Create libwebsockets context */ diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c index 122a8584..4f00a445 100644 --- a/src/protocols/kubernetes/settings.c +++ b/src/protocols/kubernetes/settings.c @@ -31,9 +31,9 @@ const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { "pod", "container", "use-ssl", - "client-cert-file", - "client-key-file", - "ca-cert-file", + "client-cert", + "client-key", + "ca-cert", "ignore-cert", "font-name", "font-size", @@ -89,24 +89,26 @@ enum KUBERNETES_ARGS_IDX { IDX_USE_SSL, /** - * The filename of the certificate to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The certificate to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - IDX_CLIENT_CERT_FILE, + IDX_CLIENT_CERT, /** - * The filename of the key to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The key to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - IDX_CLIENT_KEY_FILE, + IDX_CLIENT_KEY, /** - * The filename of the certificate of the certificate authority that signed - * the certificate of the Kubernetes server. + * The certificate of the certificate authority that signed the certificate + * of the Kubernetes server, in PEM format. If omitted. verification of + * the Kubernetes server certificate will use the systemwide certificate + * authorities. */ - IDX_CA_CERT_FILE, + IDX_CA_CERT, /** * Whether the certificate used by the Kubernetes server for SSL/TLS should @@ -264,17 +266,17 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, /* Read SSL/TLS connection details only if enabled */ if (settings->use_ssl) { - settings->client_cert_file = + settings->client_cert = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, - argv, IDX_CLIENT_CERT_FILE, NULL); + argv, IDX_CLIENT_CERT, NULL); - settings->client_key_file = + settings->client_key = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, - argv, IDX_CLIENT_KEY_FILE, NULL); + argv, IDX_CLIENT_KEY, NULL); - settings->ca_cert_file = + settings->ca_cert = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, - argv, IDX_CA_CERT_FILE, NULL); + argv, IDX_CA_CERT, NULL); settings->ignore_cert = guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, @@ -378,9 +380,9 @@ void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) { free(settings->kubernetes_container); /* Free SSL/TLS details */ - free(settings->client_cert_file); - free(settings->client_key_file); - free(settings->ca_cert_file); + free(settings->client_cert); + free(settings->client_key); + free(settings->ca_cert); /* Free display preferences */ free(settings->font_name); diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index a86d14a5..6267a18b 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -103,24 +103,26 @@ typedef struct guac_kubernetes_settings { bool use_ssl; /** - * The filename of the certificate to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The certificate to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - char* client_cert_file; + char* client_cert; /** - * The filename of the key to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The key to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - char* client_key_file; + char* client_key; /** - * The filename of the certificate of the certificate authority that signed - * the certificate of the Kubernetes server. + * The certificate of the certificate authority that signed the certificate + * of the Kubernetes server, in PEM format. If omitted. verification of + * the Kubernetes server certificate will use the systemwide certificate + * authorities. */ - char* ca_cert_file; + char* ca_cert; /** * Whether the certificate used by the Kubernetes server for SSL/TLS should diff --git a/src/protocols/kubernetes/ssl.c b/src/protocols/kubernetes/ssl.c new file mode 100644 index 00000000..6ebafc61 --- /dev/null +++ b/src/protocols/kubernetes/ssl.c @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "kubernetes.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include + +/** + * Tests whether the given hostname is, in fact, an IP address. + * + * @param hostname + * The hostname to test. + * + * @return + * Non-zero if the given hostname is an IP address, zero otherwise. + */ +static int guac_kubernetes_is_address(const char* hostname) { + + /* Attempt to interpret the hostname as an IP address */ + ASN1_OCTET_STRING* ip = a2i_IPADDRESS(hostname); + + /* If unsuccessful, the hostname is not an IP address */ + if (ip == NULL) + return 0; + + /* Converted hostname must be freed */ + ASN1_OCTET_STRING_free(ip); + return 1; + +} + +/** + * Parses the given PEM certificate, returning a new OpenSSL X509 structure + * representing that certificate. + * + * @param pem + * The PEM certificate. + * + * @return + * An X509 structure representing the given certificate, or NULL if the + * certificate was unreadable. + */ +static X509* guac_kubernetes_read_cert(char* pem) { + + /* Prepare a BIO which provides access to the in-memory CA cert */ + BIO* bio = BIO_new_mem_buf(pem, -1); + if (bio == NULL) + return NULL; + + /* Read the CA cert as PEM */ + X509* certificate = PEM_read_bio_X509(bio, NULL, NULL, NULL); + if (certificate == NULL) { + BIO_free(bio); + return NULL; + } + + return certificate; + +} + +/** + * Parses the given PEM private key, returning a new OpenSSL EVP_PKEY structure + * representing that key. + * + * @param pem + * The PEM private key. + * + * @return + * An EVP_KEY representing the given private key, or NULL if the private + * key was unreadable. + */ +static EVP_PKEY* guac_kubernetes_read_key(char* pem) { + + /* Prepare a BIO which provides access to the in-memory key */ + BIO* bio = BIO_new_mem_buf(pem, -1); + if (bio == NULL) + return NULL; + + /* Read the private key as PEM */ + EVP_PKEY* key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + if (key == NULL) { + BIO_free(bio); + return NULL; + } + + return key; + +} + +void guac_kubernetes_init_ssl(guac_client* client, SSL_CTX* context) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + guac_kubernetes_settings* settings = kubernetes_client->settings; + + /* Bypass certificate checks if requested */ + if (settings->ignore_cert) + SSL_CTX_set_verify(context, SSL_VERIFY_NONE, NULL); + + /* Otherwise use the given CA certificate to validate (if any) */ + else if (settings->ca_cert != NULL) { + + /* Read CA certificate from configuration data */ + X509* ca_cert = guac_kubernetes_read_cert(settings->ca_cert); + if (ca_cert == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided CA certificate is unreadable"); + return; + } + + /* Add certificate to CA store */ + X509_STORE* ca_store = SSL_CTX_get_cert_store(context); + if (!X509_STORE_add_cert(ca_store, ca_cert)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to add CA certificate to certificate store of " + "SSL context"); + return; + } + + } + + /* Certificate for SSL/TLS client auth */ + if (settings->client_cert != NULL) { + + /* Read client certificate from configuration data */ + X509* client_cert = guac_kubernetes_read_cert(settings->client_cert); + if (client_cert == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided client certificate is unreadable"); + return; + } + + /* Use parsed certificate for authentication */ + if (!SSL_CTX_use_certificate(context, client_cert)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Client certificate could not be used for SSL/TLS " + "client authentication"); + return; + } + + } + + /* Private key for SSL/TLS client auth */ + if (settings->client_key != NULL) { + + /* Read client private key from configuration data */ + EVP_PKEY* client_key = guac_kubernetes_read_key(settings->client_key); + if (client_key == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided client private key is unreadable"); + return; + } + + /* Use parsed key for authentication */ + if (!SSL_CTX_use_PrivateKey(context, client_key)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Client private key could not be used for SSL/TLS " + "client authentication"); + return; + } + + } + + /* Enable hostname checking */ + X509_VERIFY_PARAM *param = SSL_CTX_get0_param(context); + X509_VERIFY_PARAM_set_hostflags(param, + X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + + /* Validate properly depending on whether hostname is an IP address */ + if (guac_kubernetes_is_address(settings->hostname)) { + if (!X509_VERIFY_PARAM_set1_ip_asc(param, settings->hostname)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Server IP address validation could not be enabled"); + return; + } + } + else { + if (!X509_VERIFY_PARAM_set1_host(param, settings->hostname, 0)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Server hostname validation could not be enabled"); + return; + } + } + +} + diff --git a/src/protocols/kubernetes/ssl.h b/src/protocols/kubernetes/ssl.h new file mode 100644 index 00000000..cca02bdb --- /dev/null +++ b/src/protocols/kubernetes/ssl.h @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef GUAC_KUBERNETES_SSL_H +#define GUAC_KUBERNETES_SSL_H + +#include "settings.h" + +#include + +/** + * Initializes the given SSL/TLS context using the configuration parameters + * associated with the given guac_client, setting up hostname/address + * validation and client authentication. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param context + * The SSL_CTX in use by libwebsockets. + */ +void guac_kubernetes_init_ssl(guac_client* client, SSL_CTX* context); + +#endif +