GUACAMOLE-632: Merge dynamic JPEG/WebP quality scaling.
diff --git a/Makefile.am b/Makefile.am
index e923376..91c8abe 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -52,6 +52,10 @@
 SUBDIRS += src/pulse
 endif
 
+if ENABLE_KUBERNETES
+SUBDIRS += src/protocols/kubernetes
+endif
+
 if ENABLE_RDP
 SUBDIRS += src/protocols/rdp
 endif
diff --git a/configure.ac b/configure.ac
index 6b20c97..d26db39 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1173,6 +1173,62 @@
 AC_SUBST(WEBP_LIBS)
 
 #
+# libwebsockets
+#
+
+have_libwebsockets=disabled
+WEBSOCKETS_LIBS=
+AC_ARG_WITH([websockets],
+            [AS_HELP_STRING([--with-websockets],
+                            [support WebSockets @<:@default=check@:>@])],
+            [],
+            [with_websockets=check])
+
+if test "x$with_websockets" != "xno"
+then
+    have_libwebsockets=yes
+    AC_CHECK_LIB([websockets],
+                 [lws_create_context],
+                 [WEBSOCKETS_LIBS="$WEBSOCKETS_LIBS -lwebsockets"],
+                 [AC_MSG_WARN([
+  --------------------------------------------
+   Unable to find libwebsockets.
+   Support for Kubernetes will be disabled.
+  --------------------------------------------])
+                 have_libwebsockets=no])
+fi
+
+# Check for client-specific closed event, which must be used in favor of the
+# generic closed event if libwebsockets is recent enough to provide this
+if test "x$with_websockets" != "xno"
+then
+    AC_CHECK_DECL([LWS_CALLBACK_CLIENT_CLOSED],
+        [AC_DEFINE([HAVE_LWS_CALLBACK_CLIENT_CLOSED],,
+                   [Whether LWS_CALLBACK_CLIENT_CLOSED is defined])],,
+        [#include <libwebsockets.h>])
+fi
+
+AM_CONDITIONAL([ENABLE_WEBSOCKETS],
+               [test "x${have_libwebsockets}"  = "xyes"])
+
+AC_SUBST(WEBSOCKETS_LIBS)
+
+#
+# Kubernetes
+#
+
+AC_ARG_ENABLE([kubernetes],
+              [AS_HELP_STRING([--disable-kubernetes],
+                              [do not build support for attaching to Kubernetes pods])],
+              [],
+              [enable_kubernetes=yes])
+
+AM_CONDITIONAL([ENABLE_KUBERNETES], [test "x${enable_kubernetes}"  = "xyes" \
+                                       -a "x${have_libwebsockets}" = "xyes" \
+                                       -a "x${have_ssl}"           = "xyes" \
+                                       -a "x${have_terminal}"      = "xyes"])
+
+#
 # guacd
 #
 
@@ -1230,6 +1286,7 @@
                  src/guaclog/Makefile
                  src/guaclog/man/guaclog.1
                  src/pulse/Makefile
+                 src/protocols/kubernetes/Makefile
                  src/protocols/rdp/Makefile
                  src/protocols/ssh/Makefile
                  src/protocols/telnet/Makefile
@@ -1240,10 +1297,11 @@
 # Protocol build status
 #
 
-AM_COND_IF([ENABLE_RDP],    [build_rdp=yes],    [build_rdp=no])
-AM_COND_IF([ENABLE_SSH],    [build_ssh=yes],    [build_ssh=no])
-AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no])
-AM_COND_IF([ENABLE_VNC],    [build_vnc=yes],    [build_vnc=no])
+AM_COND_IF([ENABLE_KUBERNETES], [build_kubernetes=yes], [build_kubernetes=no])
+AM_COND_IF([ENABLE_RDP],        [build_rdp=yes],        [build_rdp=no])
+AM_COND_IF([ENABLE_SSH],        [build_ssh=yes],        [build_ssh=no])
+AM_COND_IF([ENABLE_TELNET],     [build_telnet=yes],     [build_telnet=no])
+AM_COND_IF([ENABLE_VNC],        [build_vnc=yes],        [build_vnc=no])
 
 #
 # Service / tool build status
@@ -1287,15 +1345,17 @@
      libVNCServer ........ ${have_libvncserver}
      libvorbis ........... ${have_vorbis}
      libpulse ............ ${have_pulse}
+     libwebsockets ....... ${have_libwebsockets}
      libwebp ............. ${have_webp}
      wsock32 ............. ${have_winsock}
 
    Protocol support:
 
-      RDP ....... ${build_rdp}
-      SSH ....... ${build_ssh}
-      Telnet .... ${build_telnet}
-      VNC ....... ${build_vnc}
+      Kubernetes .... ${build_kubernetes}
+      RDP ........... ${build_rdp}
+      SSH ........... ${build_ssh}
+      Telnet ........ ${build_telnet}
+      VNC ........... ${build_vnc}
 
    Services / tools:
 
diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am
new file mode 100644
index 0000000..56db4d6
--- /dev/null
+++ b/src/protocols/kubernetes/Makefile.am
@@ -0,0 +1,64 @@
+#
+# 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.
+#
+
+AUTOMAKE_OPTIONS = foreign
+ACLOCAL_AMFLAGS = -I m4
+
+lib_LTLIBRARIES = libguac-client-kubernetes.la
+
+libguac_client_kubernetes_la_SOURCES = \
+    client.c                           \
+    clipboard.c                        \
+    input.c                            \
+    io.c                               \
+    pipe.c                             \
+    settings.c                         \
+    ssl.c                              \
+    kubernetes.c                       \
+    url.c                              \
+    user.c
+
+noinst_HEADERS = \
+    client.h     \
+    clipboard.h  \
+    input.h      \
+    io.h         \
+    pipe.h       \
+    settings.h   \
+    ssl.h        \
+    kubernetes.h \
+    url.h        \
+    user.h
+
+libguac_client_kubernetes_la_CFLAGS = \
+    -Werror -Wall -Iinclude           \
+    @LIBGUAC_INCLUDE@                 \
+    @TERMINAL_INCLUDE@
+
+libguac_client_kubernetes_la_LIBADD = \
+    @COMMON_LTLIB@                    \
+    @LIBGUAC_LTLIB@                   \
+    @TERMINAL_LTLIB@
+
+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
new file mode 100644
index 0000000..1a1eb3a
--- /dev/null
+++ b/src/protocols/kubernetes/client.c
@@ -0,0 +1,133 @@
+/*
+ * 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 "client.h"
+#include "common/clipboard.h"
+#include "kubernetes.h"
+#include "settings.h"
+#include "user.h"
+
+#include <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <langinfo.h>
+#include <locale.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+
+guac_client* guac_kubernetes_lws_current_client = NULL;
+
+/**
+ * Logging callback invoked by libwebsockets to log a single line of logging
+ * output. As libwebsockets messages are all generally low-level, the log
+ * level provided by libwebsockets is ignored here, with all messages logged
+ * instead at guacd's debug level.
+ *
+ * @param level
+ *     The libwebsockets log level associated with the log message. This value
+ *     is ignored by this implementation of the logging callback.
+ *
+ * @param line
+ *     The line of logging output to log.
+ */
+static void guac_kubernetes_log(int level, const char* line) {
+
+    char buffer[1024];
+
+    /* Drop log message if there's nowhere to log yet */
+    if (guac_kubernetes_lws_current_client == NULL)
+        return;
+
+    /* Trim length of line to fit buffer (plus null terminator) */
+    int length = strlen(line);
+    if (length > sizeof(buffer) - 1)
+        length = sizeof(buffer) - 1;
+
+    /* Copy as much of the received line as will fit in the buffer */
+    memcpy(buffer, line, length);
+
+    /* If the line ends with a newline character, trim the character */
+    if (length > 0 && buffer[length - 1] == '\n')
+        length--;
+
+    /* Null-terminate the trimmed string */
+    buffer[length] = '\0';
+
+    /* Log using guacd's own log facilities */
+    guac_client_log(guac_kubernetes_lws_current_client, GUAC_LOG_DEBUG,
+            "libwebsockets: %s", buffer);
+
+}
+
+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 */
+    lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO,
+            guac_kubernetes_log);
+
+    /* Set client args */
+    client->args = GUAC_KUBERNETES_CLIENT_ARGS;
+
+    /* Allocate client instance data */
+    guac_kubernetes_client* kubernetes_client = calloc(1, sizeof(guac_kubernetes_client));
+    client->data = kubernetes_client;
+
+    /* Init clipboard */
+    kubernetes_client->clipboard = guac_common_clipboard_alloc(GUAC_KUBERNETES_CLIPBOARD_MAX_LENGTH);
+
+    /* Set handlers */
+    client->join_handler = guac_kubernetes_user_join_handler;
+    client->free_handler = guac_kubernetes_client_free_handler;
+
+    /* Set locale and warn if not UTF-8 */
+    setlocale(LC_CTYPE, "");
+    if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) {
+        guac_client_log(client, GUAC_LOG_INFO,
+                "Current locale does not use UTF-8. Some characters may "
+                "not render correctly.");
+    }
+
+    /* Success */
+    return 0;
+
+}
+
+int guac_kubernetes_client_free_handler(guac_client* client) {
+
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Wait client thread to terminate */
+    pthread_join(kubernetes_client->client_thread, NULL);
+
+    /* Free settings */
+    if (kubernetes_client->settings != NULL)
+        guac_kubernetes_settings_free(kubernetes_client->settings);
+
+    guac_common_clipboard_free(kubernetes_client->clipboard);
+    free(kubernetes_client);
+    return 0;
+
+}
+
diff --git a/src/protocols/kubernetes/client.h b/src/protocols/kubernetes/client.h
new file mode 100644
index 0000000..ec4ba32
--- /dev/null
+++ b/src/protocols/kubernetes/client.h
@@ -0,0 +1,44 @@
+/*
+ * 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_CLIENT_H
+#define GUAC_KUBERNETES_CLIENT_H
+
+#include <guacamole/client.h>
+
+/**
+ * The maximum number of bytes to allow within the clipboard.
+ */
+#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.
+ */
+guac_client_free_handler guac_kubernetes_client_free_handler;
+
+#endif
+
diff --git a/src/protocols/kubernetes/clipboard.c b/src/protocols/kubernetes/clipboard.c
new file mode 100644
index 0000000..f168206
--- /dev/null
+++ b/src/protocols/kubernetes/clipboard.c
@@ -0,0 +1,65 @@
+/*
+ * 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 "clipboard.h"
+#include "common/clipboard.h"
+#include "kubernetes.h"
+
+#include <guacamole/client.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+
+int guac_kubernetes_clipboard_handler(guac_user* user, guac_stream* stream,
+        char* mimetype) {
+
+    guac_client* client = user->client;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Clear clipboard and prepare for new data */
+    guac_common_clipboard_reset(kubernetes_client->clipboard, mimetype);
+
+    /* Set handlers for clipboard stream */
+    stream->blob_handler = guac_kubernetes_clipboard_blob_handler;
+    stream->end_handler = guac_kubernetes_clipboard_end_handler;
+
+    return 0;
+}
+
+int guac_kubernetes_clipboard_blob_handler(guac_user* user,
+        guac_stream* stream, void* data, int length) {
+
+    guac_client* client = user->client;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Append new data */
+    guac_common_clipboard_append(kubernetes_client->clipboard, data, length);
+
+    return 0;
+}
+
+int guac_kubernetes_clipboard_end_handler(guac_user* user,
+        guac_stream* stream) {
+
+    /* Nothing to do - clipboard is implemented within client */
+
+    return 0;
+}
+
diff --git a/src/protocols/kubernetes/clipboard.h b/src/protocols/kubernetes/clipboard.h
new file mode 100644
index 0000000..87a393c
--- /dev/null
+++ b/src/protocols/kubernetes/clipboard.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_CLIPBOARD_H
+#define GUAC_KUBERNETES_CLIPBOARD_H
+
+#include <guacamole/user.h>
+
+/**
+ * Handler for inbound clipboard streams.
+ */
+guac_user_clipboard_handler guac_kubernetes_clipboard_handler;
+
+/**
+ * Handler for data received along clipboard streams.
+ */
+guac_user_blob_handler guac_kubernetes_clipboard_blob_handler;
+
+/**
+ * Handler for end-of-stream related to clipboard.
+ */
+guac_user_end_handler guac_kubernetes_clipboard_end_handler;
+
+#endif
+
diff --git a/src/protocols/kubernetes/input.c b/src/protocols/kubernetes/input.c
new file mode 100644
index 0000000..814578e
--- /dev/null
+++ b/src/protocols/kubernetes/input.c
@@ -0,0 +1,94 @@
+/*
+ * 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 "common/recording.h"
+#include "input.h"
+#include "kubernetes.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <guacamole/user.h>
+
+#include <stdlib.h>
+
+int guac_kubernetes_user_mouse_handler(guac_user* user,
+        int x, int y, int mask) {
+
+    guac_client* client = user->client;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Skip if terminal not yet ready */
+    guac_terminal* term = kubernetes_client->term;
+    if (term == NULL)
+        return 0;
+
+    /* Report mouse position within recording */
+    if (kubernetes_client->recording != NULL)
+        guac_common_recording_report_mouse(kubernetes_client->recording, x, y,
+                mask);
+
+    guac_terminal_send_mouse(term, user, x, y, mask);
+    return 0;
+
+}
+
+int guac_kubernetes_user_key_handler(guac_user* user, int keysym, int pressed) {
+
+    guac_client* client = user->client;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Report key state within recording */
+    if (kubernetes_client->recording != NULL)
+        guac_common_recording_report_key(kubernetes_client->recording,
+                keysym, pressed);
+
+    /* Skip if terminal not yet ready */
+    guac_terminal* term = kubernetes_client->term;
+    if (term == NULL)
+        return 0;
+
+    guac_terminal_send_key(term, keysym, pressed);
+    return 0;
+
+}
+
+int guac_kubernetes_user_size_handler(guac_user* user, int width, int height) {
+
+    /* Get terminal */
+    guac_client* client = user->client;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Skip if terminal not yet ready */
+    guac_terminal* terminal = kubernetes_client->term;
+    if (terminal == NULL)
+        return 0;
+
+    /* Resize terminal */
+    guac_terminal_resize(terminal, width, height);
+
+    /* Update Kubernetes terminal window size if connected */
+    guac_kubernetes_resize(client, terminal->term_height,
+            terminal->term_width);
+
+    return 0;
+}
+
diff --git a/src/protocols/kubernetes/input.h b/src/protocols/kubernetes/input.h
new file mode 100644
index 0000000..6f24cf2
--- /dev/null
+++ b/src/protocols/kubernetes/input.h
@@ -0,0 +1,44 @@
+/*
+ * 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_INPUT_H
+#define GUAC_KUBERNETES_INPUT_H
+
+#include <guacamole/user.h>
+
+/**
+ * Handler for key events. Required by libguac and called whenever key events
+ * are received.
+ */
+guac_user_key_handler guac_kubernetes_user_key_handler;
+
+/**
+ * Handler for mouse events. Required by libguac and called whenever mouse
+ * events are received.
+ */
+guac_user_mouse_handler guac_kubernetes_user_mouse_handler;
+
+/**
+ * Handler for size events. Required by libguac and called whenever the remote
+ * display (window) is resized.
+ */
+guac_user_size_handler guac_kubernetes_user_size_handler;
+
+#endif
+
diff --git a/src/protocols/kubernetes/io.c b/src/protocols/kubernetes/io.c
new file mode 100644
index 0000000..bfa37b1
--- /dev/null
+++ b/src/protocols/kubernetes/io.c
@@ -0,0 +1,143 @@
+/*
+ * 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 "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <string.h>
+
+void guac_kubernetes_receive_data(guac_client* client,
+        const char* buffer, size_t length) {
+
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Strip channel index from beginning of buffer */
+    int channel = *(buffer++);
+    length--;
+
+    switch (channel) {
+
+        /* Write STDOUT / STDERR directly to terminal as output */
+        case GUAC_KUBERNETES_CHANNEL_STDOUT:
+        case GUAC_KUBERNETES_CHANNEL_STDERR:
+            guac_terminal_write(kubernetes_client->term, buffer, length);
+            break;
+
+        /* Ignore data on other channels */
+        default:
+            guac_client_log(client, GUAC_LOG_DEBUG, "Received %i bytes along "
+                    "channel %i.", length, channel);
+
+    }
+
+}
+
+void guac_kubernetes_send_message(guac_client* client,
+        int channel, const char* data, int length) {
+
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    pthread_mutex_lock(&(kubernetes_client->outbound_message_lock));
+
+    /* Add message to buffer if space is available */
+    if (kubernetes_client->outbound_messages_waiting
+            < GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES) {
+
+        /* Calculate storage position of next message */
+        int index = (kubernetes_client->outbound_messages_top
+                  + kubernetes_client->outbound_messages_waiting)
+                  % GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES;
+
+        /* Obtain pointer to message slot at calculated position */
+        guac_kubernetes_message* message =
+            &(kubernetes_client->outbound_messages[index]);
+
+        /* Copy details of message into buffer */
+        message->channel = channel;
+        memcpy(message->data, data, length);
+        message->length = length;
+
+        /* One more message is now waiting */
+        kubernetes_client->outbound_messages_waiting++;
+
+        /* Notify libwebsockets that we need a callback to send pending
+         * messages */
+        lws_callback_on_writable(kubernetes_client->wsi);
+        lws_cancel_service(kubernetes_client->context);
+
+    }
+
+    /* Warn if data has to be dropped */
+    else
+        guac_client_log(client, GUAC_LOG_WARNING, "Send buffer could not be "
+                "flushed in time to handle additional data. Outbound "
+                "message dropped.");
+
+    pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock));
+
+}
+
+bool guac_kubernetes_write_pending_message(guac_client* client) {
+
+    bool messages_remain;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    pthread_mutex_lock(&(kubernetes_client->outbound_message_lock));
+
+    /* Send one message from top of buffer */
+    if (kubernetes_client->outbound_messages_waiting > 0) {
+
+        /* Obtain pointer to message at top */
+        int top = kubernetes_client->outbound_messages_top;
+        guac_kubernetes_message* message =
+            &(kubernetes_client->outbound_messages[top]);
+
+        /* Write message including channel index */
+        lws_write(kubernetes_client->wsi,
+                ((unsigned char*) message) + LWS_PRE,
+                message->length + 1, LWS_WRITE_BINARY);
+
+        /* Advance top to next message */
+        kubernetes_client->outbound_messages_top++;
+        kubernetes_client->outbound_messages_top %=
+            GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES;
+
+        /* One less message is waiting */
+        kubernetes_client->outbound_messages_waiting--;
+
+    }
+
+    /* Record whether messages remained at time of completion */
+    messages_remain = (kubernetes_client->outbound_messages_waiting > 0);
+
+    pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock));
+
+    return messages_remain;
+
+}
+
+
diff --git a/src/protocols/kubernetes/io.h b/src/protocols/kubernetes/io.h
new file mode 100644
index 0000000..40f2c69
--- /dev/null
+++ b/src/protocols/kubernetes/io.h
@@ -0,0 +1,144 @@
+/*
+ * 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_IO_H
+#define GUAC_KUBERNETES_IO_H
+
+#include <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+ * The maximum amount of data to include in any particular WebSocket message
+ * to Kubernetes. This excludes the storage space required for the channel
+ * index.
+ */
+#define GUAC_KUBERNETES_MAX_MESSAGE_SIZE 1024
+
+/**
+ * The index of the Kubernetes channel used for STDIN.
+ */
+#define GUAC_KUBERNETES_CHANNEL_STDIN 0
+
+/**
+ * The index of the Kubernetes channel used for STDOUT.
+ */
+#define GUAC_KUBERNETES_CHANNEL_STDOUT 1
+
+/**
+ * The index of the Kubernetes channel used for STDERR.
+ */
+#define GUAC_KUBERNETES_CHANNEL_STDERR 2
+
+/**
+ * The index of the Kubernetes channel used for terminal resize messages.
+ */
+#define GUAC_KUBERNETES_CHANNEL_RESIZE 4
+
+/**
+ * An outbound message to be received by Kubernetes over WebSocket.
+ */
+typedef struct guac_kubernetes_message {
+
+    /**
+     * lws_write() requires leading padding of LWS_PRE bytes to provide
+     * scratch space for WebSocket framing.
+     */
+    uint8_t _padding[LWS_PRE];
+
+    /**
+     * The index of the channel receiving the data, such as
+     * GUAC_KUBERNETES_CHANNEL_STDIN.
+     */
+    uint8_t channel;
+
+    /**
+     * The data that should be sent to Kubernetes (along with the channel
+     * index).
+     */
+    char data[GUAC_KUBERNETES_MAX_MESSAGE_SIZE];
+
+    /**
+     * The length of the data to be sent, excluding the channel index.
+     */
+    int length;
+
+} guac_kubernetes_message;
+
+
+/**
+ * Handles data received from Kubernetes over WebSocket, decoding the channel
+ * index of the received data and forwarding that data accordingly.
+ *
+ * @param client
+ *     The guac_client associated with the connection to Kubernetes.
+ *
+ * @param buffer
+ *     The data received from Kubernetes.
+ *
+ * @param length
+ *     The size of the data received from Kubernetes, in bytes.
+ */
+void guac_kubernetes_receive_data(guac_client* client,
+        const char* buffer, size_t length);
+
+/**
+ * Requests that the given data be sent along the given channel to the
+ * Kubernetes server when the WebSocket connection is next available for
+ * writing. If the WebSocket connection has not been available for writing for
+ * long enough that the outbound message buffer is full, the request to send
+ * this particular message will be dropped.
+ *
+ * @param client
+ *     The guac_client associated with the Kubernetes connection.
+ *
+ * @param channel
+ *     The Kubernetes channel on which to send the message,
+ *     such as GUAC_KUBERNETES_CHANNEL_STDIN.
+ *
+ * @param data
+ *     A buffer containing the data to send.
+ *
+ * @param length
+ *     The number of bytes to send.
+ */
+void guac_kubernetes_send_message(guac_client* client,
+        int channel, const char* data, int length);
+
+/**
+ * Writes the oldest pending message within the outbound message queue,
+ * as scheduled with guac_kubernetes_send_message(), removing that message
+ * from the queue. This function MAY NOT be invoked outside the libwebsockets
+ * event callback and MUST only be invoked in the context of a
+ * LWS_CALLBACK_CLIENT_WRITEABLE event. If no messages are pending, this
+ * function has no effect.
+ *
+ * @param client
+ *     The guac_client associated with the Kubernetes connection.
+ *
+ * @return
+ *     true if messages still remain to be written within the outbound message
+ *     queue, false otherwise.
+ */
+bool guac_kubernetes_write_pending_message(guac_client* client);
+
+#endif
+
diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c
new file mode 100644
index 0000000..f314c59
--- /dev/null
+++ b/src/protocols/kubernetes/kubernetes.c
@@ -0,0 +1,387 @@
+/*
+ * 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 "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"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <libwebsockets.h>
+
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+/**
+ * Callback invoked by libwebsockets for events related to a WebSocket being
+ * used for communicating with an attached Kubernetes pod.
+ *
+ * @param wsi
+ *     The libwebsockets handle for the WebSocket connection.
+ *
+ * @param reason
+ *     The reason (event) that this callback was invoked.
+ *
+ * @param user
+ *     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.
+ *
+ * @param length
+ *     An arbitrary, reason-specific length value.
+ *
+ * @return
+ *     An undocumented integer value related the success of handling the
+ *     event, or -1 if the WebSocket connection should be closed.
+ */
+static int guac_kubernetes_lws_callback(struct lws* wsi,
+        enum lws_callback_reasons reason, void* user,
+        void* in, size_t length) {
+
+    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,
+                    "Error connecting to Kubernetes server: %s",
+                    in != NULL ? (char*) in : "(no error description "
+                    "available)");
+            break;
+
+        /* Connected / logged in */
+        case LWS_CALLBACK_CLIENT_ESTABLISHED:
+            guac_client_log(client, GUAC_LOG_INFO,
+                    "Kubernetes connection successful.");
+
+            /* Schedule check for pending messages in case messages were added
+             * to the outbound message buffer prior to the connection being
+             * fully established */
+            lws_callback_on_writable(wsi);
+            break;
+
+        /* Data received via WebSocket */
+        case LWS_CALLBACK_CLIENT_RECEIVE:
+            guac_kubernetes_receive_data(client, (const char*) in, length);
+            break;
+
+        /* WebSocket is ready for writing */
+        case LWS_CALLBACK_CLIENT_WRITEABLE:
+
+            /* Send any pending messages, requesting another callback if
+             * yet more messages remain */
+            if (guac_kubernetes_write_pending_message(client))
+                lws_callback_on_writable(wsi);
+            break;
+
+#ifdef HAVE_LWS_CALLBACK_CLIENT_CLOSED
+        /* Connection closed (client-specific) */
+        case LWS_CALLBACK_CLIENT_CLOSED:
+#endif
+
+        /* Connection closed */
+        case LWS_CALLBACK_CLOSED:
+            guac_client_stop(client);
+            guac_client_log(client, GUAC_LOG_DEBUG, "WebSocket connection to "
+                    "Kubernetes server closed.");
+            break;
+
+        /* No other event types are applicable */
+        default:
+            break;
+
+    }
+
+    return lws_callback_http_dummy(wsi, reason, user, in, length);
+
+}
+
+/**
+ * List of all WebSocket protocols which should be declared as supported by
+ * libwebsockets during the initial WebSocket handshake, along with
+ * corresponding event-handling callbacks.
+ */
+struct lws_protocols guac_kubernetes_lws_protocols[] = {
+    {
+        .name = GUAC_KUBERNETES_LWS_PROTOCOL,
+        .callback = guac_kubernetes_lws_callback
+    },
+    { 0 }
+};
+
+/**
+ * Input thread, started by the main Kubernetes client thread. This thread
+ * continuously reads from the terminal's STDIN and transfers all read
+ * data to the Kubernetes connection.
+ *
+ * @param data
+ *     The current guac_client instance.
+ *
+ * @return
+ *     Always NULL.
+ */
+static void* guac_kubernetes_input_thread(void* data) {
+
+    guac_client* client = (guac_client*) data;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    char buffer[GUAC_KUBERNETES_MAX_MESSAGE_SIZE];
+    int bytes_read;
+
+    /* Write all data read */
+    while ((bytes_read = guac_terminal_read_stdin(kubernetes_client->term, buffer, sizeof(buffer))) > 0) {
+
+        /* Send received data to Kubernetes along STDIN channel */
+        guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_STDIN,
+                buffer, bytes_read);
+
+    }
+
+    return NULL;
+
+}
+
+void* guac_kubernetes_client_thread(void* data) {
+
+    guac_client* client = (guac_client*) data;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    guac_kubernetes_settings* settings = kubernetes_client->settings;
+
+    pthread_t input_thread;
+    char endpoint_path[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
+
+    /* Verify that the pod name was specified (it's always required) */
+    if (settings->kubernetes_pod == NULL) {
+        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+                "The name of the Kubernetes pod is a required parameter.");
+        goto fail;
+    }
+
+    /* Generate endpoint for attachment URL */
+    if (guac_kubernetes_endpoint_attach(endpoint_path, sizeof(endpoint_path),
+                settings->kubernetes_namespace,
+                settings->kubernetes_pod,
+                settings->kubernetes_container)) {
+        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+                "Unable to generate path for Kubernetes API endpoint: "
+                "Resulting path too long");
+        goto fail;
+    }
+
+    guac_client_log(client, GUAC_LOG_DEBUG, "The endpoint for attaching to "
+            "the requested Kubernetes pod is \"%s\".", endpoint_path);
+
+    /* Set up screen recording, if requested */
+    if (settings->recording_path != NULL) {
+        kubernetes_client->recording = guac_common_recording_create(client,
+                settings->recording_path,
+                settings->recording_name,
+                settings->create_recording_path,
+                !settings->recording_exclude_output,
+                !settings->recording_exclude_mouse,
+                settings->recording_include_keys);
+    }
+
+    /* Create terminal */
+    kubernetes_client->term = guac_terminal_create(client,
+            kubernetes_client->clipboard,
+            settings->max_scrollback, settings->font_name, settings->font_size,
+            settings->resolution, settings->width, settings->height,
+            settings->color_scheme, settings->backspace);
+
+    /* Fail if terminal init failed */
+    if (kubernetes_client->term == NULL) {
+        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+                "Terminal initialization failed");
+        goto fail;
+    }
+
+    /* Set up typescript, if requested */
+    if (settings->typescript_path != NULL) {
+        guac_terminal_create_typescript(kubernetes_client->term,
+                settings->typescript_path,
+                settings->typescript_name,
+                settings->create_typescript_path);
+    }
+
+    /* Init libwebsockets context creation parameters */
+    struct lws_context_creation_info context_info = {
+        .port = CONTEXT_PORT_NO_LISTEN, /* We are not a WebSocket server */
+        .uid = -1,
+        .gid = -1,
+        .protocols = guac_kubernetes_lws_protocols,
+        .user = client
+    };
+
+    /* Init WebSocket connection parameters which do not vary by Guacmaole
+     * connection parameters or creation of future libwebsockets objects */
+    struct lws_client_connect_info connection_info = {
+        .host = settings->hostname,
+        .address = settings->hostname,
+        .origin = settings->hostname,
+        .port = settings->port,
+        .protocol = GUAC_KUBERNETES_LWS_PROTOCOL,
+        .pwsi = &kubernetes_client->wsi,
+        .userdata = client
+    };
+
+    /* If requested, use an SSL/TLS connection for communication with
+     * 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) {
+        context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
+        connection_info.ssl_connection = LCCSCF_USE_SSL
+            | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
+    }
+
+    /* Create libwebsockets context */
+    kubernetes_client->context = lws_create_context(&context_info);
+    if (!kubernetes_client->context) {
+        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+                "Initialization of libwebsockets failed");
+        goto fail;
+    }
+
+    /* Generate path dynamically */
+    connection_info.context = kubernetes_client->context;
+    connection_info.path = endpoint_path;
+
+    /* Open WebSocket connection to Kubernetes */
+    kubernetes_client->wsi = lws_client_connect_via_info(&connection_info);
+    if (kubernetes_client->wsi == NULL) {
+        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+                "Connection via libwebsockets failed");
+        goto fail;
+    }
+
+    /* Init outbound message buffer */
+    pthread_mutex_init(&(kubernetes_client->outbound_message_lock), NULL);
+
+    /* Start input thread */
+    if (pthread_create(&(input_thread), NULL, guac_kubernetes_input_thread, (void*) client)) {
+        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread");
+        goto fail;
+    }
+
+    /* Force a redraw of the attached display (there will be no content
+     * otherwise, given the stream nature of attaching to a running
+     * container) */
+    guac_kubernetes_force_redraw(client);
+
+    /* As long as client is connected, continue polling libwebsockets */
+    while (client->state == GUAC_CLIENT_RUNNING) {
+
+        /* Cease polling libwebsockets if an error condition is signalled */
+        if (lws_service(kubernetes_client->context,
+                    GUAC_KUBERNETES_SERVICE_INTERVAL) < 0)
+            break;
+
+    }
+
+    /* Kill client and Wait for input thread to die */
+    guac_terminal_stop(kubernetes_client->term);
+    guac_client_stop(client);
+    pthread_join(input_thread, NULL);
+
+fail:
+
+    /* Kill and free terminal, if allocated */
+    if (kubernetes_client->term != NULL)
+        guac_terminal_free(kubernetes_client->term);
+
+    /* Clean up recording, if in progress */
+    if (kubernetes_client->recording != NULL)
+        guac_common_recording_free(kubernetes_client->recording);
+
+    /* Free WebSocket context if successfully allocated */
+    if (kubernetes_client->context != NULL)
+        lws_context_destroy(kubernetes_client->context);
+
+    guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended.");
+    return NULL;
+
+}
+
+void guac_kubernetes_resize(guac_client* client, int rows, int columns) {
+
+    char buffer[64];
+
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Send request only if different from last request */
+    if (kubernetes_client->rows != rows ||
+            kubernetes_client->columns != columns) {
+
+        kubernetes_client->rows = rows;
+        kubernetes_client->columns = columns;
+
+        /* Construct terminal resize message for Kubernetes */
+        int length = snprintf(buffer, sizeof(buffer),
+                "{\"Width\":%i,\"Height\":%i}", columns, rows);
+
+        /* Schedule message for sending */
+        guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_RESIZE,
+                buffer, length);
+
+    }
+
+}
+
+void guac_kubernetes_force_redraw(guac_client* client) {
+
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Get current terminal dimensions */
+    guac_terminal* term = kubernetes_client->term;
+    int rows = term->term_height;
+    int columns = term->term_width;
+
+    /* Force a redraw by increasing the terminal size by one character in
+     * each dimension and then resizing it back to normal (the same technique
+     * used by kubectl */
+    guac_kubernetes_resize(client, rows + 1, columns + 1);
+    guac_kubernetes_resize(client, rows, columns);
+
+}
+
diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h
new file mode 100644
index 0000000..c37ca4c
--- /dev/null
+++ b/src/protocols/kubernetes/kubernetes.h
@@ -0,0 +1,168 @@
+/*
+ * 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_H
+#define GUAC_KUBERNETES_H
+
+#include "common/clipboard.h"
+#include "common/recording.h"
+#include "io.h"
+#include "settings.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <pthread.h>
+
+/**
+ * The name of the WebSocket protocol specific to Kubernetes which should be
+ * sent to the Kubernetes server when attaching to a pod.
+ */
+#define GUAC_KUBERNETES_LWS_PROTOCOL "v4.channel.k8s.io"
+
+/**
+ * The maximum number of messages to allow within the outbound message buffer.
+ * If messages are sent despite the buffer being full, those messages will be
+ * dropped.
+ */
+#define GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES 8
+
+/**
+ * The maximum number of milliseconds to wait for a libwebsockets event to
+ * occur before entering another iteration of the libwebsockets event loop.
+ */
+#define GUAC_KUBERNETES_SERVICE_INTERVAL 1000
+
+/**
+ * Kubernetes-specific client data.
+ */
+typedef struct guac_kubernetes_client {
+
+    /**
+     * Kubernetes connection settings.
+     */
+    guac_kubernetes_settings* settings;
+
+    /**
+     * The libwebsockets context associated with the connected WebSocket.
+     */
+    struct lws_context* context;
+
+    /**
+     * The connected WebSocket.
+     */
+    struct lws* wsi;
+
+    /**
+     * Outbound message ring buffer for outbound WebSocket messages. As
+     * libwebsockets uses an event loop for all operations, outbound messages
+     * may be sent only in context of a particular event received via a
+     * callback. Until that event is received, pending data must accumulate in
+     * a buffer.
+     */
+    guac_kubernetes_message outbound_messages[GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES];
+
+    /**
+     * The number of messages currently waiting in the outbound message
+     * buffer.
+     */
+    int outbound_messages_waiting;
+
+    /**
+     * The index of the oldest entry in the outbound message buffer. Newer
+     * messages follow this entry.
+     */
+    int outbound_messages_top;
+
+    /**
+     * Lock which is acquired when the outbound message buffer is being read
+     * or manipulated.
+     */
+    pthread_mutex_t outbound_message_lock;
+
+    /**
+     * The Kubernetes client thread.
+     */
+    pthread_t client_thread;
+
+    /**
+     * The current clipboard contents.
+     */
+    guac_common_clipboard* clipboard;
+
+    /**
+     * The terminal which will render all output from the Kubernetes pod.
+     */
+    guac_terminal* term;
+
+    /**
+     * The number of rows last sent to Kubernetes in a terminal resize
+     * request.
+     */
+    int rows;
+
+    /**
+     * The number of columns last sent to Kubernetes in a terminal resize
+     * request.
+     */
+    int columns;
+
+    /**
+     * The in-progress session recording, or NULL if no recording is in
+     * progress.
+     */
+    guac_common_recording* recording;
+
+} guac_kubernetes_client;
+
+/**
+ * Main Kubernetes client thread, handling transfer of STDOUT/STDERR of an
+ * attached Kubernetes pod to STDOUT of the terminal.
+ */
+void* guac_kubernetes_client_thread(void* data);
+
+/**
+ * Sends a message to the Kubernetes server requesting that the terminal be
+ * resized to the given dimensions. This message may be queued until the
+ * underlying WebSocket connection is ready to send.
+ *
+ * @param client
+ *     The guac_client associated with the Kubernetes connection.
+ *
+ * @param rows
+ *     The new terminal size in rows.
+ *
+ * @param columns
+ *     The new terminal size in columns.
+ */
+void guac_kubernetes_resize(guac_client* client, int rows, int columns);
+
+/**
+ * Sends messages to the Kubernetes server such that the terminal is forced
+ * to redraw. This function should be invoked at the beginning of each
+ * session in order to restore expected display state.
+ *
+ * @param client
+ *     The guac_client associated with the Kubernetes connection.
+ */
+void guac_kubernetes_force_redraw(guac_client* client);
+
+#endif
+
diff --git a/src/protocols/kubernetes/pipe.c b/src/protocols/kubernetes/pipe.c
new file mode 100644
index 0000000..8f18530
--- /dev/null
+++ b/src/protocols/kubernetes/pipe.c
@@ -0,0 +1,52 @@
+/*
+ * 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 "terminal/terminal.h"
+#include "pipe.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/stream-types.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+
+#include <string.h>
+
+int guac_kubernetes_pipe_handler(guac_user* user, guac_stream* stream,
+        char* mimetype, char* name) {
+
+    guac_client* client = user->client;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Redirect STDIN if pipe has required name */
+    if (strcmp(name, GUAC_KUBERNETES_STDIN_PIPE_NAME) == 0) {
+        guac_terminal_send_stream(kubernetes_client->term, user, stream);
+        return 0;
+    }
+
+    /* No other inbound pipe streams are supported */
+    guac_protocol_send_ack(user->socket, stream, "No such input stream.",
+            GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND);
+    guac_socket_flush(user->socket);
+    return 0;
+
+}
+
diff --git a/src/protocols/kubernetes/pipe.h b/src/protocols/kubernetes/pipe.h
new file mode 100644
index 0000000..47565bf
--- /dev/null
+++ b/src/protocols/kubernetes/pipe.h
@@ -0,0 +1,40 @@
+/*
+ * 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_PIPE_H
+#define GUAC_KUBERNETES_PIPE_H
+
+#include <guacamole/user.h>
+
+/**
+ * The name reserved for the inbound pipe stream which forces the terminal
+ * emulator's STDIN to be received from the pipe.
+ */
+#define GUAC_KUBERNETES_STDIN_PIPE_NAME "STDIN"
+
+/**
+ * Handles an incoming stream from a Guacamole "pipe" instruction. If the pipe
+ * is named "STDIN", the the contents of the pipe stream are redirected to
+ * STDIN of the terminal emulator for as long as the pipe is open.
+ */
+guac_user_pipe_handler guac_kubernetes_pipe_handler;
+
+#endif
+
diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c
new file mode 100644
index 0000000..4f00a44
--- /dev/null
+++ b/src/protocols/kubernetes/settings.c
@@ -0,0 +1,403 @@
+/*
+ * 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 "settings.h"
+
+#include <guacamole/user.h>
+
+#include <stdlib.h>
+
+/* Client plugin arguments */
+const char* GUAC_KUBERNETES_CLIENT_ARGS[] = {
+    "hostname",
+    "port",
+    "namespace",
+    "pod",
+    "container",
+    "use-ssl",
+    "client-cert",
+    "client-key",
+    "ca-cert",
+    "ignore-cert",
+    "font-name",
+    "font-size",
+    "color-scheme",
+    "typescript-path",
+    "typescript-name",
+    "create-typescript-path",
+    "recording-path",
+    "recording-name",
+    "recording-exclude-output",
+    "recording-exclude-mouse",
+    "recording-include-keys",
+    "create-recording-path",
+    "read-only",
+    "backspace",
+    "scrollback",
+    NULL
+};
+
+enum KUBERNETES_ARGS_IDX {
+
+    /**
+     * The hostname to connect to. Required.
+     */
+    IDX_HOSTNAME,
+
+    /**
+     * The port to connect to. Optional.
+     */
+    IDX_PORT,
+
+    /**
+     * The name of the Kubernetes namespace of the pod containing the container
+     * being attached to. If omitted, the default namespace will be used.
+     */
+    IDX_NAMESPACE,
+
+    /**
+     * The name of the Kubernetes pod containing with the container being
+     * attached to. Required.
+     */
+    IDX_POD,
+
+    /**
+     * The name of the container to attach to. If omitted, the first container
+     * in the pod will be used.
+     */
+    IDX_CONTAINER,
+
+    /**
+     * Whether SSL/TLS should be used. If omitted, SSL/TLS will not be used.
+     */
+    IDX_USE_SSL,
+
+    /**
+     * 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,
+
+    /**
+     * 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,
+
+    /**
+     * 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,
+
+    /**
+     * Whether the certificate used by the Kubernetes server for SSL/TLS should
+     * be ignored if it cannot be validated.
+     */
+    IDX_IGNORE_CERT,
+
+    /**
+     * The name of the font to use within the terminal.
+     */
+    IDX_FONT_NAME,
+
+    /**
+     * The size of the font to use within the terminal, in points.
+     */
+    IDX_FONT_SIZE,
+
+    /**
+     * The color scheme to use, as a series of semicolon-separated color-value
+     * pairs: "background: <color>", "foreground: <color>", or
+     * "color<n>: <color>", where <n> is a number from 0 to 255, and <color> is
+     * "color<n>" or an X11 color code (e.g. "aqua" or "rgb:12/34/56").
+     * The color scheme can also be one of the special values: "black-white",
+     * "white-black", "gray-black", or "green-black".
+     */
+    IDX_COLOR_SCHEME,
+
+    /**
+     * The full absolute path to the directory in which typescripts should be
+     * written.
+     */
+    IDX_TYPESCRIPT_PATH,
+
+    /**
+     * The name that should be given to typescripts which are written in the
+     * given path. Each typescript will consist of two files: "NAME" and
+     * "NAME.timing".
+     */
+    IDX_TYPESCRIPT_NAME,
+
+    /**
+     * Whether the specified typescript path should automatically be created
+     * if it does not yet exist.
+     */
+    IDX_CREATE_TYPESCRIPT_PATH,
+
+    /**
+     * The full absolute path to the directory in which screen recordings
+     * should be written.
+     */
+    IDX_RECORDING_PATH,
+
+    /**
+     * The name that should be given to screen recordings which are written in
+     * the given path.
+     */
+    IDX_RECORDING_NAME,
+
+    /**
+     * Whether output which is broadcast to each connected client (graphics,
+     * streams, etc.) should NOT be included in the session recording. Output
+     * is included by default, as it is necessary for any recording which must
+     * later be viewable as video.
+     */
+    IDX_RECORDING_EXCLUDE_OUTPUT,
+
+    /**
+     * Whether changes to mouse state, such as position and buttons pressed or
+     * released, should NOT be included in the session recording. Mouse state
+     * is included by default, as it is necessary for the mouse cursor to be
+     * rendered in any resulting video.
+     */
+    IDX_RECORDING_EXCLUDE_MOUSE,
+
+    /**
+     * Whether keys pressed and released should be included in the session
+     * recording. Key events are NOT included by default within the recording,
+     * as doing so has privacy and security implications.  Including key events
+     * may be necessary in certain auditing contexts, but should only be done
+     * with caution. Key events can easily contain sensitive information, such
+     * as passwords, credit card numbers, etc.
+     */
+    IDX_RECORDING_INCLUDE_KEYS,
+
+    /**
+     * Whether the specified screen recording path should automatically be
+     * created if it does not yet exist.
+     */
+    IDX_CREATE_RECORDING_PATH,
+
+    /**
+     * "true" if this connection should be read-only (user input should be
+     * dropped), "false" or blank otherwise.
+     */
+    IDX_READ_ONLY,
+
+    /**
+     * ASCII code, as an integer to use for the backspace key, or 127
+     * if not specified.
+     */
+    IDX_BACKSPACE,
+
+    /**
+     * The maximum size of the scrollback buffer in rows.
+     */
+    IDX_SCROLLBACK,
+
+    KUBERNETES_ARGS_COUNT
+};
+
+guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user,
+        int argc, const char** argv) {
+
+    /* Validate arg count */
+    if (argc != KUBERNETES_ARGS_COUNT) {
+        guac_user_log(user, GUAC_LOG_WARNING, "Incorrect number of connection "
+                "parameters provided: expected %i, got %i.",
+                KUBERNETES_ARGS_COUNT, argc);
+        return NULL;
+    }
+
+    guac_kubernetes_settings* settings =
+        calloc(1, sizeof(guac_kubernetes_settings));
+
+    /* Read hostname */
+    settings->hostname =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_HOSTNAME, "");
+
+    /* Read port */
+    settings->port =
+        guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT);
+
+    /* Read Kubernetes namespace */
+    settings->kubernetes_namespace =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_NAMESPACE, GUAC_KUBERNETES_DEFAULT_NAMESPACE);
+
+    /* Read name of Kubernetes pod (required) */
+    settings->kubernetes_pod =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_POD, NULL);
+
+    /* Read container of pod (optional) */
+    settings->kubernetes_container =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_CONTAINER, NULL);
+
+    /* Parse whether SSL should be used */
+    settings->use_ssl =
+        guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_USE_SSL, false);
+
+    /* Read SSL/TLS connection details only if enabled */
+    if (settings->use_ssl) {
+
+        settings->client_cert =
+            guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS,
+                    argv, IDX_CLIENT_CERT, NULL);
+
+        settings->client_key =
+            guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS,
+                    argv, IDX_CLIENT_KEY, NULL);
+
+        settings->ca_cert =
+            guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS,
+                    argv, IDX_CA_CERT, NULL);
+
+        settings->ignore_cert =
+            guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS,
+                    argv, IDX_IGNORE_CERT, false);
+
+    }
+
+    /* Read-only mode */
+    settings->read_only =
+        guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_READ_ONLY, false);
+
+    /* Read maximum scrollback size */
+    settings->max_scrollback =
+        guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_SCROLLBACK, GUAC_KUBERNETES_DEFAULT_MAX_SCROLLBACK);
+
+    /* Read font name */
+    settings->font_name =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_FONT_NAME, GUAC_KUBERNETES_DEFAULT_FONT_NAME);
+
+    /* Read font size */
+    settings->font_size =
+        guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_FONT_SIZE, GUAC_KUBERNETES_DEFAULT_FONT_SIZE);
+
+    /* Copy requested color scheme */
+    settings->color_scheme =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_COLOR_SCHEME, "");
+
+    /* Pull width/height/resolution directly from user */
+    settings->width      = user->info.optimal_width;
+    settings->height     = user->info.optimal_height;
+    settings->resolution = user->info.optimal_resolution;
+
+    /* Read typescript path */
+    settings->typescript_path =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_TYPESCRIPT_PATH, NULL);
+
+    /* Read typescript name */
+    settings->typescript_name =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_TYPESCRIPT_NAME, GUAC_KUBERNETES_DEFAULT_TYPESCRIPT_NAME);
+
+    /* Parse path creation flag */
+    settings->create_typescript_path =
+        guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_CREATE_TYPESCRIPT_PATH, false);
+
+    /* Read recording path */
+    settings->recording_path =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_RECORDING_PATH, NULL);
+
+    /* Read recording name */
+    settings->recording_name =
+        guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_RECORDING_NAME, GUAC_KUBERNETES_DEFAULT_RECORDING_NAME);
+
+    /* Parse output exclusion flag */
+    settings->recording_exclude_output =
+        guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_RECORDING_EXCLUDE_OUTPUT, false);
+
+    /* Parse mouse exclusion flag */
+    settings->recording_exclude_mouse =
+        guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_RECORDING_EXCLUDE_MOUSE, false);
+
+    /* Parse key event inclusion flag */
+    settings->recording_include_keys =
+        guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_RECORDING_INCLUDE_KEYS, false);
+
+    /* Parse path creation flag */
+    settings->create_recording_path =
+        guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_CREATE_RECORDING_PATH, false);
+
+    /* Parse backspace key code */
+    settings->backspace =
+        guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
+                IDX_BACKSPACE, 127);
+
+    /* Parsing was successful */
+    return settings;
+
+}
+
+void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) {
+
+    /* Free network connection information */
+    free(settings->hostname);
+
+    /* Free Kubernetes pod/container details */
+    free(settings->kubernetes_namespace);
+    free(settings->kubernetes_pod);
+    free(settings->kubernetes_container);
+
+    /* Free SSL/TLS details */
+    free(settings->client_cert);
+    free(settings->client_key);
+    free(settings->ca_cert);
+
+    /* Free display preferences */
+    free(settings->font_name);
+    free(settings->color_scheme);
+
+    /* Free typescript settings */
+    free(settings->typescript_name);
+    free(settings->typescript_path);
+
+    /* Free screen recording settings */
+    free(settings->recording_name);
+    free(settings->recording_path);
+
+    /* Free overall structure */
+    free(settings);
+
+}
+
diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h
new file mode 100644
index 0000000..6267a18
--- /dev/null
+++ b/src/protocols/kubernetes/settings.h
@@ -0,0 +1,279 @@
+/*
+ * 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_SETTINGS_H
+#define GUAC_KUBERNETES_SETTINGS_H
+
+#include <guacamole/user.h>
+
+#include <stdbool.h>
+
+/**
+ * The name of the font to use for the terminal if no name is specified.
+ */
+#define GUAC_KUBERNETES_DEFAULT_FONT_NAME "monospace" 
+
+/**
+ * The size of the font to use for the terminal if no font size is specified,
+ * in points.
+ */
+#define GUAC_KUBERNETES_DEFAULT_FONT_SIZE 12
+
+/**
+ * The port to connect to when initiating any Kubernetes connection, if no
+ * other port is specified.
+ */
+#define GUAC_KUBERNETES_DEFAULT_PORT 8080
+
+/**
+ * The name of the Kubernetes namespace that should be used by default if no
+ * specific Kubernetes namespace is provided.
+ */
+#define GUAC_KUBERNETES_DEFAULT_NAMESPACE "default"
+
+/**
+ * The filename to use for the typescript, if not specified.
+ */
+#define GUAC_KUBERNETES_DEFAULT_TYPESCRIPT_NAME "typescript" 
+
+/**
+ * The filename to use for the screen recording, if not specified.
+ */
+#define GUAC_KUBERNETES_DEFAULT_RECORDING_NAME "recording"
+
+/**
+ * The default maximum scrollback size in rows.
+ */
+#define GUAC_KUBERNETES_DEFAULT_MAX_SCROLLBACK 1000
+
+/**
+ * Settings for the Kubernetes connection. The values for this structure are
+ * parsed from the arguments given during the Guacamole protocol handshake
+ * using the guac_kubernetes_parse_args() function.
+ */
+typedef struct guac_kubernetes_settings {
+
+    /**
+     * The hostname of the Kubernetes server to connect to.
+     */
+    char* hostname;
+
+    /**
+     * The port of the Kubernetes server to connect to.
+     */
+    int port;
+
+    /**
+     * The name of the Kubernetes namespace of the pod containing the container
+     * being attached to.
+     */
+    char* kubernetes_namespace;
+
+    /**
+     * The name of the Kubernetes pod containing with the container being
+     * attached to.
+     */
+    char* kubernetes_pod;
+
+    /**
+     * The name of the container to attach to, or NULL to arbitrarily attach to
+     * the first container in the pod.
+     */
+    char* kubernetes_container;
+
+    /**
+     * Whether SSL/TLS should be used.
+     */
+    bool use_ssl;
+
+    /**
+     * 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;
+
+    /**
+     * 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;
+
+    /**
+     * 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;
+
+    /**
+     * Whether the certificate used by the Kubernetes server for SSL/TLS should
+     * be ignored if it cannot be validated.
+     */
+    bool ignore_cert;
+
+    /**
+     * Whether this connection is read-only, and user input should be dropped.
+     */
+    bool read_only;
+
+    /**
+     * The maximum size of the scrollback buffer in rows.
+     */
+    int max_scrollback;
+
+    /**
+     * The name of the font to use for display rendering.
+     */
+    char* font_name;
+
+    /**
+     * The size of the font to use, in points.
+     */
+    int font_size;
+
+    /**
+     * The name of the color scheme to use.
+     */
+    char* color_scheme; 
+
+    /**
+     * The desired width of the terminal display, in pixels.
+     */
+    int width;
+
+    /**
+     * The desired height of the terminal display, in pixels.
+     */
+    int height;
+
+    /**
+     * The desired screen resolution, in DPI.
+     */
+    int resolution;
+
+    /**
+     * The path in which the typescript should be saved, if enabled. If no
+     * typescript should be saved, this will be NULL.
+     */
+    char* typescript_path;
+
+    /**
+     * The filename to use for the typescript, if enabled.
+     */
+    char* typescript_name;
+
+    /**
+     * Whether the typescript path should be automatically created if it does
+     * not already exist.
+     */
+    bool create_typescript_path;
+
+    /**
+     * The path in which the screen recording should be saved, if enabled. If
+     * no screen recording should be saved, this will be NULL.
+     */
+    char* recording_path;
+
+    /**
+     * The filename to use for the screen recording, if enabled.
+     */
+    char* recording_name;
+
+    /**
+     * Whether the screen recording path should be automatically created if it
+     * does not already exist.
+     */
+    bool create_recording_path;
+
+    /**
+     * Whether output which is broadcast to each connected client (graphics,
+     * streams, etc.) should NOT be included in the session recording. Output
+     * is included by default, as it is necessary for any recording which must
+     * later be viewable as video.
+     */
+    bool recording_exclude_output;
+
+    /**
+     * Whether changes to mouse state, such as position and buttons pressed or
+     * released, should NOT be included in the session recording. Mouse state
+     * is included by default, as it is necessary for the mouse cursor to be
+     * rendered in any resulting video.
+     */
+    bool recording_exclude_mouse;
+
+    /**
+     * Whether keys pressed and released should be included in the session
+     * recording. Key events are NOT included by default within the recording,
+     * as doing so has privacy and security implications.  Including key events
+     * may be necessary in certain auditing contexts, but should only be done
+     * with caution. Key events can easily contain sensitive information, such
+     * as passwords, credit card numbers, etc.
+     */
+    bool recording_include_keys;
+
+    /**
+     * The ASCII code, as an integer, that the Kubernetes client will use when
+     * the backspace key is pressed. By default, this is 127, ASCII delete, if
+     * not specified in the client settings.
+     */
+    int backspace;
+
+} guac_kubernetes_settings;
+
+/**
+ * Parses all given args, storing them in a newly-allocated settings object. If
+ * the args fail to parse, NULL is returned.
+ *
+ * @param user
+ *     The user who submitted the given arguments while joining the
+ *     connection.
+ *
+ * @param argc
+ *     The number of arguments within the argv array.
+ *
+ * @param argv
+ *     The values of all arguments provided by the user.
+ *
+ * @return
+ *     A newly-allocated settings object which must be freed with
+ *     guac_kubernetes_settings_free() when no longer needed. If the arguments
+ *     fail to parse, NULL is returned.
+ */
+guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user,
+        int argc, const char** argv);
+
+/**
+ * Frees the given guac_kubernetes_settings object, having been previously
+ * allocated via guac_kubernetes_parse_args().
+ *
+ * @param settings
+ *     The settings object to free.
+ */
+void guac_kubernetes_settings_free(guac_kubernetes_settings* settings);
+
+/**
+ * NULL-terminated array of accepted client args.
+ */
+extern const char* GUAC_KUBERNETES_CLIENT_ARGS[];
+
+#endif
+
diff --git a/src/protocols/kubernetes/ssl.c b/src/protocols/kubernetes/ssl.c
new file mode 100644
index 0000000..6ebafc6
--- /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 <guacamole/client.h>
+#include <openssl/asn1.h>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+#include <openssl/ssl.h>
+#include <openssl/x509v3.h>
+#include <openssl/x509_vfy.h>
+
+/**
+ * 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 0000000..cca02bd
--- /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 <openssl/ssl.h>
+
+/**
+ * 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
+
diff --git a/src/protocols/kubernetes/url.c b/src/protocols/kubernetes/url.c
new file mode 100644
index 0000000..78c116e
--- /dev/null
+++ b/src/protocols/kubernetes/url.c
@@ -0,0 +1,137 @@
+/*
+ * 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 "url.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * Returns whether the given character is a character that need not be
+ * escaped when included as part of a component of a URL.
+ *
+ * @param c
+ *     The character to test.
+ *
+ * @return
+ *     Zero if the character does not need to be escaped when included as
+ *     part of a component of a URL, non-zero otherwise.
+ */
+static int guac_kubernetes_is_url_safe(char c) {
+    return (c >= 'A' && c <= 'Z')
+        || (c >= 'a' && c <= 'z')
+        || (c >= '0' && c <= '9')
+        || strchr("-_.!~*'()", c) != NULL;
+}
+
+int guac_kubernetes_escape_url_component(char* output, int length,
+        const char* str) {
+
+    char* current = output;
+    while (*str != '\0') {
+
+        char c = *str;
+
+        /* Store alphanumeric characters verbatim */
+        if (guac_kubernetes_is_url_safe(c)) {
+
+            /* Verify space exists for single character */
+            if (length < 1)
+                return 1;
+
+            *(current++) = c;
+            length--;
+
+        }
+
+        /* Escape EVERYTHING else as hex */
+        else {
+
+            /* Verify space exists for hex-encoded character */
+            if (length < 4)
+                return 1;
+
+            snprintf(current, 4, "%%%02X", (int) c);
+
+            current += 3;
+            length -= 3;
+        }
+
+        /* Next character */
+        str++;
+
+    }
+
+    /* Verify space exists for null terminator */
+    if (length < 1)
+        return 1;
+
+    /* Append null terminator */
+    *current = '\0';
+    return 0;
+
+}
+
+int guac_kubernetes_endpoint_attach(char* buffer, int length,
+        const char* kubernetes_namespace, const char* kubernetes_pod,
+        const char* kubernetes_container) {
+
+    int written;
+
+    char escaped_namespace[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
+    char escaped_pod[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
+    char escaped_container[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
+
+    /* Escape Kubernetes namespace */
+    if (guac_kubernetes_escape_url_component(escaped_namespace,
+                sizeof(escaped_namespace), kubernetes_namespace))
+        return 1;
+
+    /* Escape name of Kubernetes pod */
+    if (guac_kubernetes_escape_url_component(escaped_pod,
+                sizeof(escaped_pod), kubernetes_pod))
+        return 1;
+
+    /* Generate attachment endpoint URL */
+    if (kubernetes_container != NULL) {
+
+        /* Escape container name */
+        if (guac_kubernetes_escape_url_component(escaped_container,
+                    sizeof(escaped_container), kubernetes_container))
+            return 1;
+
+        written = snprintf(buffer, length,
+                "/api/v1/namespaces/%s/pods/%s/attach"
+                "?container=%s&stdin=true&stdout=true&tty=true",
+                escaped_namespace, escaped_pod, escaped_container);
+    }
+    else {
+        written = snprintf(buffer, length,
+                "/api/v1/namespaces/%s/pods/%s/attach"
+                "?stdin=true&stdout=true&tty=true",
+                escaped_namespace, escaped_pod);
+    }
+
+    /* Endpoint URL was successfully generated if it was written to the given
+     * buffer without truncation */
+    return !(written < length - 1);
+
+}
+
diff --git a/src/protocols/kubernetes/url.h b/src/protocols/kubernetes/url.h
new file mode 100644
index 0000000..285baa2
--- /dev/null
+++ b/src/protocols/kubernetes/url.h
@@ -0,0 +1,85 @@
+/*
+ * 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_URL_H
+#define GUAC_KUBERNETES_URL_H
+
+/**
+ * The maximum number of characters allowed in the full path for any Kubernetes
+ * endpoint.
+ */
+#define GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH 1024
+
+/**
+ * Escapes the given string such that it can be included safely within a URL.
+ * This function duplicates the behavior of JavaScript's encodeURIComponent(),
+ * escaping all but the following characters: A-Z a-z 0-9 - _ . ! ~ * ' ( )
+ *
+ * @param output
+ *     The buffer which should receive the escaped string. This buffer may be
+ *     touched even if escaping is unsuccessful.
+ *
+ * @param length
+ *     The number of bytes available in the given output buffer.
+ *
+ * @param str
+ *     The string to escape.
+ *
+ * @return
+ *     Zero if the string was successfully escaped and written into the
+ *     provided output buffer without being truncated, including null
+ *     terminator, non-zero otherwise.
+ */
+int guac_kubernetes_escape_url_component(char* output, int length,
+        const char* str);
+
+/**
+ * Generates the full path to the Kubernetes API endpoint which handles
+ * attaching to running containers within specific pods. Values within the path
+ * will be URL-escaped as necessary.
+ *
+ * @param buffer
+ *     The buffer which should receive the endpoint path. This buffer may be
+ *     touched even if the endpoint path could not be generated.
+ *
+ * @param length
+ *     The number of bytes available in the given buffer.
+ *
+ * @param kubernetes_namespace
+ *     The name of the Kubernetes namespace of the pod containing the container
+ *     being attached to.
+ *
+ * @param kubernetes_pod
+ *     The name of the Kubernetes pod containing with the container being
+ *     attached to.
+ *
+ * @param kubernetes_container
+ *     The name of the container to attach to, or NULL to arbitrarily attach
+ *     to the first container in the pod.
+ *
+ * @return
+ *     Zero if the endpoint path was successfully written to the provided
+ *     buffer, non-zero if insufficient space exists within the buffer.
+ */
+int guac_kubernetes_endpoint_attach(char* buffer, int length,
+        const char* kubernetes_namespace, const char* kubernetes_pod,
+        const char* kubernetes_container);
+
+#endif
+
diff --git a/src/protocols/kubernetes/user.c b/src/protocols/kubernetes/user.c
new file mode 100644
index 0000000..f90260e
--- /dev/null
+++ b/src/protocols/kubernetes/user.c
@@ -0,0 +1,116 @@
+/*
+ * 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 "clipboard.h"
+#include "common/cursor.h"
+#include "input.h"
+#include "kubernetes.h"
+#include "pipe.h"
+#include "settings.h"
+#include "terminal/terminal.h"
+#include "user.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+
+#include <pthread.h>
+#include <stdlib.h>
+
+int guac_kubernetes_user_join_handler(guac_user* user, int argc, char** argv) {
+
+    guac_client* client = user->client;
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) client->data;
+
+    /* Parse provided arguments */
+    guac_kubernetes_settings* settings = guac_kubernetes_parse_args(user,
+            argc, (const char**) argv);
+
+    /* Fail if settings cannot be parsed */
+    if (settings == NULL) {
+        guac_user_log(user, GUAC_LOG_INFO,
+                "Badly formatted client arguments.");
+        return 1;
+    }
+
+    /* Store settings at user level */
+    user->data = settings;
+
+    /* Connect to Kubernetes if owner */
+    if (user->owner) {
+
+        /* Store owner's settings at client level */
+        kubernetes_client->settings = settings;
+
+        /* Start client thread */
+        if (pthread_create(&(kubernetes_client->client_thread), NULL,
+                    guac_kubernetes_client_thread, (void*) client)) {
+            guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+                    "Unable to start Kubernetes client thread");
+            return 1;
+        }
+
+    }
+
+    /* If not owner, synchronize with current display */
+    else {
+        guac_terminal_dup(kubernetes_client->term, user, user->socket);
+        guac_socket_flush(user->socket);
+    }
+
+    /* Only handle events if not read-only */
+    if (!settings->read_only) {
+
+        /* General mouse/keyboard/clipboard events */
+        user->key_handler       = guac_kubernetes_user_key_handler;
+        user->mouse_handler     = guac_kubernetes_user_mouse_handler;
+        user->clipboard_handler = guac_kubernetes_clipboard_handler;
+
+        /* STDIN redirection */
+        user->pipe_handler = guac_kubernetes_pipe_handler;
+
+        /* Display size change events */
+        user->size_handler = guac_kubernetes_user_size_handler;
+
+    }
+
+    return 0;
+
+}
+
+int guac_kubernetes_user_leave_handler(guac_user* user) {
+
+    guac_kubernetes_client* kubernetes_client =
+        (guac_kubernetes_client*) user->client->data;
+
+    /* Update shared cursor state */
+    guac_common_cursor_remove_user(kubernetes_client->term->cursor, user);
+
+    /* Free settings if not owner (owner settings will be freed with client) */
+    if (!user->owner) {
+        guac_kubernetes_settings* settings =
+            (guac_kubernetes_settings*) user->data;
+        guac_kubernetes_settings_free(settings);
+    }
+
+    return 0;
+}
+
diff --git a/src/protocols/kubernetes/user.h b/src/protocols/kubernetes/user.h
new file mode 100644
index 0000000..55d49fd
--- /dev/null
+++ b/src/protocols/kubernetes/user.h
@@ -0,0 +1,36 @@
+/*
+ * 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_USER_H
+#define GUAC_KUBERNETES_USER_H
+
+#include <guacamole/user.h>
+
+/**
+ * Handler for joining users.
+ */
+guac_user_join_handler guac_kubernetes_user_join_handler;
+
+/**
+ * Handler for leaving users.
+ */
+guac_user_leave_handler guac_kubernetes_user_leave_handler;
+
+#endif
+