diff --git a/migration/tcp.c b/migration/tcp.c
index 2347d9d..68ecaa4 100644
--- a/migration/tcp.c
+++ b/migration/tcp.c
@@ -22,6 +22,8 @@
 #include "migration/migration.h"
 #include "migration/qemu-file.h"
 #include "io/channel-socket.h"
+#include "io/channel-tls.h"
+#include "crypto/tlscreds.h"
 
 //#define DEBUG_MIGRATION_TCP
 
@@ -33,6 +35,22 @@
     do { } while (0)
 #endif
 
+typedef struct {
+    MigrationState *s;
+    QCryptoTLSCreds *tlscreds;
+    char *hostname;
+} TCPConnectData;
+
+typedef struct {
+    MigrationState *s;
+    QCryptoTLSCreds *tlscreds;
+} TCPListenData;
+
+typedef struct {
+    MigrationState *s;
+    QIOChannel *ioc;
+} TCPConnectTLSData;
+
 
 static SocketAddress *tcp_build_address(const char *host_port, Error **errp)
 {
@@ -51,21 +69,174 @@ static SocketAddress *tcp_build_address(const char *host_port, Error **errp)
 }
 
 
+static void tcp_connect_data_free(gpointer opaque)
+{
+    TCPConnectData *data = opaque;
+    if (!data) {
+        return;
+    }
+    g_free(data->hostname);
+    object_unref(OBJECT(data->tlscreds));
+    g_free(data);
+}
+
+
+static void tcp_listen_data_free(gpointer opaque)
+{
+    TCPListenData *data = opaque;
+    if (!data) {
+        return;
+    }
+    object_unref(OBJECT(data->tlscreds));
+    g_free(data);
+}
+
+
+static void tcp_connect_tls_data_free(gpointer opaque)
+{
+    TCPConnectTLSData *data = opaque;
+    if (!data) {
+        return;
+    }
+    object_unref(OBJECT(data->ioc));
+    g_free(data);
+}
+
+
+static char *tcp_get_opt_str(const char *host_port,
+                             const char *key)
+{
+    const char *offset, *end;
+
+    offset = strstr(host_port, key);
+    if (!offset) {
+        return NULL;
+    }
+
+    offset += strlen(key);
+    if (offset[0] != '=') {
+        return NULL;
+    }
+    offset++;
+    end = strchr(offset, ',');
+    if (end) {
+        return g_strndup(offset, end - offset);
+    } else {
+        return g_strdup(offset);
+    }
+}
+
+
+static QCryptoTLSCreds *tcp_get_tls_creds(const char *host_port,
+                                          bool is_listen,
+                                          Error **errp)
+{
+    char *credname = NULL;
+    Object *creds;
+    QCryptoTLSCreds *ret;
+
+    credname = tcp_get_opt_str(host_port, "tls-creds");
+    if (!credname) {
+        return NULL;
+    }
+
+    creds = object_resolve_path_component(
+        object_get_objects_root(), credname);
+    if (!creds) {
+        error_setg(errp, "No TLS credentials with id '%s'",
+                   credname);
+        goto error;
+    }
+    ret = (QCryptoTLSCreds *)object_dynamic_cast(
+        creds, TYPE_QCRYPTO_TLS_CREDS);
+    if (!ret) {
+        error_setg(errp, "Object with id '%s' is not TLS credentials",
+                   credname);
+        goto error;
+    }
+    if (is_listen) {
+        if (ret->endpoint != QCRYPTO_TLS_CREDS_ENDPOINT_SERVER) {
+            error_setg(errp, "%s",
+                       "Expected TLS credentials for server endpoint");
+            goto error;
+        }
+    } else {
+        if (ret->endpoint != QCRYPTO_TLS_CREDS_ENDPOINT_CLIENT) {
+            error_setg(errp, "%s",
+                       "Expected TLS credentials for client endpoint");
+            goto error;
+        }
+    }
+
+    g_free(credname);
+    object_ref(OBJECT(ret));
+    return ret;
+
+ error:
+    g_free(credname);
+    return NULL;
+}
+
+
+static void tcp_outgoing_migration_tls(Object *src,
+                                       Error *err,
+                                       gpointer opaque)
+{
+    TCPConnectTLSData *data = opaque;
+    QIOChannel *ioc = QIO_CHANNEL(src);
+
+    if (err) {
+        DPRINTF("migrate connect TLS error: %s\n", error_get_pretty(err));
+        data->s->file = NULL;
+        migrate_fd_error(data->s);
+    } else {
+        DPRINTF("migrate connect success\n");
+        data->s->file = qemu_fopen_channel_output(ioc);
+        migrate_fd_connect(data->s);
+    }
+}
+
+
 static void tcp_outgoing_migration(Object *src,
                                    Error *err,
                                    gpointer opaque)
 {
-    MigrationState *s = opaque;
+    TCPConnectData *data = opaque;
     QIOChannel *sioc = QIO_CHANNEL(src);
 
     if (err) {
         DPRINTF("migrate connect error: %s\n", error_get_pretty(err));
-        s->file = NULL;
-        migrate_fd_error(s);
+        data->s->file = NULL;
+        migrate_fd_error(data->s);
     } else {
-        DPRINTF("migrate connect success\n");
-        s->file = qemu_fopen_channel_output(sioc);
-        migrate_fd_connect(s);
+        if (data->tlscreds) {
+            Error *local_err = NULL;
+            QIOChannelTLS *tioc = qio_channel_tls_new_client(
+                sioc, data->tlscreds, data->hostname,
+                &local_err);
+            if (!tioc) {
+                DPRINTF("migrate tls setup error: %s\n",
+                        error_get_pretty(local_err));
+                error_free(local_err);
+                data->s->file = NULL;
+                migrate_fd_error(data->s);
+            } else {
+                TCPConnectTLSData *tdata =
+                    g_new0(TCPConnectTLSData, 1);
+                DPRINTF("migrate connect tls handshake begin\n");
+                tdata->s = data->s;
+                tdata->ioc = sioc;
+                object_ref(OBJECT(sioc));
+                qio_channel_tls_handshake(tioc,
+                                          tcp_outgoing_migration_tls,
+                                          tdata,
+                                          tcp_connect_tls_data_free);
+            }
+        } else {
+            DPRINTF("migrate connect success\n");
+            data->s->file = qemu_fopen_channel_output(sioc);
+            migrate_fd_connect(data->s);
+        }
     }
     object_unref(src);
 }
@@ -77,21 +248,56 @@ void tcp_start_outgoing_migration(MigrationState *s,
 {
     SocketAddress *saddr = tcp_build_address(host_port, errp);
     QIOChannelSocket *sioc;
+    Error *local_err = NULL;
+    QCryptoTLSCreds *creds;
+    TCPConnectData *data;
 
     if (!saddr) {
         return;
     }
 
+    creds = tcp_get_tls_creds(host_port, false, errp);
+    if (local_err) {
+        error_propagate(errp, local_err);
+        qapi_free_SocketAddress(saddr);
+        return;
+    }
+
+    data = g_new0(TCPConnectData, 1);
+    data->s = s;
+    if (creds) {
+        data->hostname = g_strdup(saddr->inet->host);
+        data->tlscreds = creds;
+    }
+
     sioc = qio_channel_socket_new();
     qio_channel_socket_connect_async(sioc,
                                      saddr,
                                      tcp_outgoing_migration,
-                                     s,
-                                     NULL);
+                                     data,
+                                     tcp_connect_data_free);
     qapi_free_SocketAddress(saddr);
 }
 
 
+static void tcp_incoming_migration_tls(Object *src,
+                                       Error *err,
+                                       gpointer opaque)
+{
+    QIOChannel *ioc = QIO_CHANNEL(src);
+
+    if (err) {
+        DPRINTF("migrate listen TLS error: %s\n", error_get_pretty(err));
+        object_unref(OBJECT(ioc));
+    } else {
+        DPRINTF("migrate listen success\n");
+        QEMUFile *f = qemu_fopen_channel_input(ioc);
+
+        process_incoming_migration(f);
+    }
+}
+
+
 static gboolean tcp_accept_incoming_migration(QIOChannel *ioc,
                                               GIOCondition condition,
                                               gpointer opaque)
@@ -99,6 +305,7 @@ static gboolean tcp_accept_incoming_migration(QIOChannel *ioc,
     QEMUFile *f;
     QIOChannelSocket *cioc;
     Error *err = NULL;
+    TCPListenData *data = opaque;
 
     cioc = qio_channel_socket_accept(QIO_CHANNEL_SOCKET(ioc),
                                      &err);
@@ -108,16 +315,38 @@ static gboolean tcp_accept_incoming_migration(QIOChannel *ioc,
         goto out;
     }
 
-    DPRINTF("accepted migration\n");
+    if (data->tlscreds) {
+        DPRINTF("Starting client TLS\n");
+        QIOChannelTLS *tioc = qio_channel_tls_new_server(
+            QIO_CHANNEL(cioc), data->tlscreds,
+            NULL, /* XXX pass ACL name */
+            &err);
+        object_unref(OBJECT(cioc));
+        if (!tioc) {
+            DPRINTF("migrate tls setup error: %s\n",
+                    error_get_pretty(err));
+            error_free(err);
+            goto out;
+        } else {
+            DPRINTF("migrate connect tls handshake begin\n");
+            qio_channel_tls_handshake(tioc,
+                                      tcp_incoming_migration_tls,
+                                      NULL,
+                                      NULL);
+        }
+    } else {
+        DPRINTF("accepted migration\n");
 
-    f = qemu_fopen_channel_input(QIO_CHANNEL(cioc));
-    object_unref(OBJECT(cioc));
+        f = qemu_fopen_channel_input(QIO_CHANNEL(cioc));
+        object_unref(OBJECT(cioc));
 
-    process_incoming_migration(f);
+        process_incoming_migration(f);
+    }
 
 out:
     /* Close listening socket as its no longer needed */
     qio_channel_close(ioc, NULL);
+    object_unref(OBJECT(ioc));
     return FALSE;
 }
 
@@ -126,23 +355,39 @@ void tcp_start_incoming_migration(const char *host_port, Error **errp)
 {
     SocketAddress *saddr = tcp_build_address(host_port, errp);
     QIOChannelSocket *listen_ioc;
+    TCPListenData *data;
+    Error *local_err = NULL;
+    QCryptoTLSCreds *creds;
 
     if (!saddr) {
         return;
     }
 
+    creds = tcp_get_tls_creds(host_port, true, &local_err);
+    if (local_err) {
+        error_propagate(errp, local_err);
+        qapi_free_SocketAddress(saddr);
+        return;
+    }
+
     listen_ioc = qio_channel_socket_new();
     if (qio_channel_socket_listen_sync(listen_ioc, saddr, errp) < 0) {
         object_unref(OBJECT(listen_ioc));
         qapi_free_SocketAddress(saddr);
+        object_unref(OBJECT(creds));
         return;
     }
 
+    data = g_new0(TCPListenData, 1);
+    if (creds) {
+        data->tlscreds = creds;
+    }
+
     qio_channel_add_watch(QIO_CHANNEL(listen_ioc),
                           G_IO_IN,
                           tcp_accept_incoming_migration,
-                          listen_ioc,
-                          (GDestroyNotify)object_unref);
+                          data,
+                          tcp_listen_data_free);
 
     qapi_free_SocketAddress(saddr);
 }
diff --git a/qemu-options.hx b/qemu-options.hx
index 6acd400..c1bf4a7 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -3299,7 +3299,7 @@ Set TB size.
 ETEXI
 
 DEF("incoming", HAS_ARG, QEMU_OPTION_incoming, \
-    "-incoming tcp:[host]:port[,to=maxport][,ipv4][,ipv6]\n" \
+    "-incoming tcp:[host]:port[,to=maxport][,ipv4][,ipv6][,tls-creds=ID]\n" \
     "-incoming rdma:host:port[,ipv4][,ipv6]\n" \
     "-incoming unix:socketpath\n" \
     "                prepare for incoming migration, listen on\n" \
@@ -3312,11 +3312,14 @@ DEF("incoming", HAS_ARG, QEMU_OPTION_incoming, \
     "                wait for the URI to be specified via migrate_incoming\n",
     QEMU_ARCH_ALL)
 STEXI
-@item -incoming tcp:[@var{host}]:@var{port}[,to=@var{maxport}][,ipv4][,ipv6]
+@item -incoming tcp:[@var{host}]:@var{port}[,to=@var{maxport}][,ipv4][,ipv6][,tls-creds=ID]
 @itemx -incoming rdma:@var{host}:@var{port}[,ipv4][,ipv6]
 @findex -incoming
 Prepare for incoming migration, listen on a given tcp port.
 
+If the @var{tls-creds} parameter is specified, it should refer to the ID
+of a TLS credentials object previously created with @var{-object}.
+
 @item -incoming unix:@var{socketpath}
 Prepare for incoming migration, listen on a given unix socket.
 
