From e71bd675b59f50b08df16648afdeec77fa8bd787 Mon Sep 17 00:00:00 2001 From: Nicolas Viennot Date: Thu, 7 Nov 2019 08:10:06 -0500 Subject: [PATCH] Allow tmate daemon to specify a list of authorized SSH keys --- Makefile.am | 1 + client.c | 24 +++-- cmd-set-option.c | 6 ++ server-client.c | 67 ++++++++++-- server.c | 5 +- tmate-auth-keys.c | 225 +++++++++++++++++++++++++++++++++++++++++ tmate-daemon-decoder.c | 36 +++++-- tmate-daemon-encoder.c | 2 +- tmate-ssh-server.c | 28 +++-- tmate-websocket.c | 15 ++- tmate.h | 10 +- tmux.h | 15 ++- 12 files changed, 386 insertions(+), 48 deletions(-) create mode 100644 tmate-auth-keys.c diff --git a/Makefile.am b/Makefile.am index 00700122..37fde7ed 100644 --- a/Makefile.am +++ b/Makefile.am @@ -175,6 +175,7 @@ dist_tmate_ssh_server_SOURCES = \ signal.c \ status.c \ style.c \ + tmate-auth-keys.c \ tmate-daemon-decoder.c \ tmate-daemon-encoder.c \ tmate-daemon-legacy.c \ diff --git a/client.c b/client.c index 45a84f62..f3df59ed 100644 --- a/client.c +++ b/client.c @@ -401,21 +401,25 @@ client_send_identify(const char *ttynam, const char *cwd) int fd, flags = client_flags; pid_t pid; - proc_send(client_peer, MSG_IDENTIFY_FLAGS, -1, &flags, sizeof flags); - #ifdef TMATE_SLAVE - proc_send(client_peer, MSG_IDENTIFY_TMATE_IP_ADDRESS, -1, - tmate_session->ssh_client.ip_address, - strlen(tmate_session->ssh_client.ip_address) + 1); - - proc_send(client_peer, MSG_IDENTIFY_TMATE_PUBKEY, -1, - tmate_session->ssh_client.pubkey, - strlen(tmate_session->ssh_client.pubkey) + 1); + if (tmate_session->ssh_client.pubkey) { + proc_send(client_peer, MSG_IDENTIFY_TMATE_AUTH_PUBKEY, -1, + tmate_session->ssh_client.pubkey, + strlen(tmate_session->ssh_client.pubkey) + 1); + } else { + proc_send(client_peer, MSG_IDENTIFY_TMATE_AUTH_NONE, -1, NULL, 0); + } proc_send(client_peer, MSG_IDENTIFY_TMATE_READONLY, -1, &tmate_session->readonly, 1); + + proc_send(client_peer, MSG_IDENTIFY_TMATE_IP_ADDRESS, -1, + tmate_session->ssh_client.ip_address, + strlen(tmate_session->ssh_client.ip_address) + 1); #endif + proc_send(client_peer, MSG_IDENTIFY_FLAGS, -1, &flags, sizeof flags); + if ((s = getenv("TERM")) == NULL) s = ""; proc_send(client_peer, MSG_IDENTIFY_TERM, -1, s, strlen(s) + 1); @@ -658,6 +662,8 @@ client_dispatch_wait(struct imsg *imsg, const char *shellcmd) case MSG_EXITED: proc_exit(client_proc); break; + case MSG_TMATE_AUTH_STATUS: + break; } } diff --git a/cmd-set-option.c b/cmd-set-option.c index 83caad40..d0855ff2 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -22,6 +22,7 @@ #include #include "tmux.h" +#include "tmate.h" /* * Set an option. @@ -113,6 +114,11 @@ cmd_set_option_exec(struct cmd *self, struct cmd_q *cmdq) else valstr = args->argv[1]; +#ifdef TMATE_SLAVE + if (!args_has(args, 'u')) + tmate_hook_set_option(optstr, valstr); +#endif + /* Is this a user option? */ if (*optstr == '@') return (cmd_set_option_user(self, cmdq, optstr, valstr)); diff --git a/server-client.c b/server-client.c index 6fa173dd..b0c323d2 100644 --- a/server-client.c +++ b/server-client.c @@ -266,7 +266,9 @@ server_client_lost(struct client *c) server_client_unref(c); +#ifndef TMATE_SLAVE server_add_accept(0); /* may be more file descriptors now */ +#endif recalculate_sizes(); server_check_unattached(); @@ -1057,6 +1059,11 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (c->flags & CLIENT_DEAD) return; +#ifdef TMATE_SLAVE + if (c->flags & CLIENT_EXIT) + return; +#endif + if (imsg == NULL) { server_client_lost(c); return; @@ -1065,6 +1072,24 @@ server_client_dispatch(struct imsg *imsg, void *arg) data = imsg->data; datalen = imsg->hdr.len - IMSG_HEADER_SIZE; +#ifdef TMATE_SLAVE + switch (imsg->hdr.type) { + case MSG_IDENTIFY_TMATE_IP_ADDRESS: + case MSG_IDENTIFY_TMATE_AUTH_NONE: + case MSG_IDENTIFY_TMATE_AUTH_PUBKEY: + case MSG_IDENTIFY_TMATE_READONLY: + server_client_dispatch_identify(c, imsg); + return; + } + + if (!(c->flags & CLIENT_TMATE_AUTHENTICATED)) { + control_write(c, "Authentication needed"); + tmate_warn("Dropping unauthenticated client"); + c->flags |= CLIENT_EXIT; + return; + } +#endif + switch (imsg->hdr.type) { case MSG_IDENTIFY_FLAGS: case MSG_IDENTIFY_TERM: @@ -1074,13 +1099,18 @@ server_client_dispatch(struct imsg *imsg, void *arg) case MSG_IDENTIFY_ENVIRON: case MSG_IDENTIFY_CLIENTPID: case MSG_IDENTIFY_DONE: -#ifdef TMATE_SLAVE - case MSG_IDENTIFY_TMATE_IP_ADDRESS: - case MSG_IDENTIFY_TMATE_PUBKEY: - case MSG_IDENTIFY_TMATE_READONLY: -#endif server_client_dispatch_identify(c, imsg); - break; + return; + } + +#ifdef TMATE_SLAVE + if (!(c->flags & CLIENT_IDENTIFIED)) { + tmate_warn("dropping unidentified client message: %d", imsg->hdr.type); + return; + } +#endif + + switch (imsg->hdr.type) { case MSG_COMMAND: server_client_dispatch_command(c, imsg); break; @@ -1205,6 +1235,15 @@ error: c->flags |= CLIENT_EXIT; } +static void handle_tmate_auth(struct client *c) +{ + bool allow = tmate_allow_auth(c->pubkey); + if (allow) + c->flags |= CLIENT_TMATE_AUTHENTICATED; + + proc_send(c->peer, MSG_TMATE_AUTH_STATUS, -1, &allow, sizeof(allow)); +} + /* Handle identify message. */ void server_client_dispatch_identify(struct client *c, struct imsg *imsg) @@ -1275,11 +1314,19 @@ server_client_dispatch_identify(struct client *c, struct imsg *imsg) fatalx("bad MSG_IDENTIFY_TMATE_IP_ADDRESS string"); c->ip_address = xstrdup(data); break; - case MSG_IDENTIFY_TMATE_PUBKEY: + + case MSG_IDENTIFY_TMATE_AUTH_NONE: + assert(!c->pubkey); + handle_tmate_auth(c); + break; + + case MSG_IDENTIFY_TMATE_AUTH_PUBKEY: if (datalen == 0 || data[datalen - 1] != '\0') fatalx("bad MSG_IDENTIFY_TMATE_PUBKEY string"); c->pubkey = xstrdup(data); + handle_tmate_auth(c); break; + case MSG_IDENTIFY_TMATE_READONLY: if (datalen != 1) fatalx("bad MSG_IDENTIFY_TMATE_READONLY size"); @@ -1370,6 +1417,9 @@ server_client_push_stdout(struct client *c) struct msg_stdout_data data; size_t sent, left; + if (!(c->flags & CLIENT_TMATE_AUTHENTICATED)) + return; + left = EVBUFFER_LENGTH(c->stdout_data); while (left != 0) { sent = left; @@ -1411,6 +1461,9 @@ server_client_push_stderr(struct client *c) struct msg_stderr_data data; size_t sent, left; + if (!(c->flags & CLIENT_TMATE_AUTHENTICATED)) + return; + if (c->stderr_data == c->stdout_data) { server_client_push_stdout(c); return; diff --git a/server.c b/server.c index b2452d49..765521a0 100644 --- a/server.c +++ b/server.c @@ -190,9 +190,8 @@ server_start(struct event_base *base, int lockfd, char *lockfile) #ifndef TMATE_SLAVE status_prompt_load_history(); -#endif - server_add_accept(0); +#endif proc_loop(server_proc, server_loop); #ifndef TMATE_SLAVE @@ -379,6 +378,7 @@ server_signal(int sig) case SIGCHLD: server_child_signal(); break; +#ifndef TMATE_SLAVE case SIGUSR1: event_del(&server_ev_accept); fd = server_create_socket(); @@ -389,6 +389,7 @@ server_signal(int sig) } server_add_accept(0); break; +#endif } } diff --git a/tmate-auth-keys.c b/tmate-auth-keys.c new file mode 100644 index 00000000..4c9b4e91 --- /dev/null +++ b/tmate-auth-keys.c @@ -0,0 +1,225 @@ +#include +#include + +#include "tmate.h" + +static void reset_and_enable_authorized_keys(void) +{ + ssh_key *keys = tmate_session->authorized_keys; + if (keys) { + for (ssh_key *k = keys; *k; k++) + ssh_key_free(*k); + free(keys); + } + + keys = xreallocarray(NULL, sizeof(ssh_key), 1); + keys[0] = NULL; + + tmate_session->authorized_keys = keys; +} + +static ssh_key import_ssh_pubkey64(const char *_keystr) +{ + /* key is formatted as "type base64_key" */ + + char * const keystr = xstrdup(_keystr); + char *s = keystr; + ssh_key ret = NULL; + + char *key_type = strsep(&s, " "); + char *key_content = strsep(&s, " "); + + if (!key_content) + goto out; + + enum ssh_keytypes_e type = ssh_key_type_from_name(key_type); + if (type == SSH_KEYTYPE_UNKNOWN) + goto out; + + if (ssh_pki_import_pubkey_base64(key_content, type, &ret) != SSH_OK) { + ret = NULL; + goto out; + } +out: + free(keystr); + return ret; +} + +int get_num_authorized_keys(ssh_key *keys) +{ + if (!keys) + return 0; + + int count = 0; + for (ssh_key *k = keys; *k; k++) + count++; + return count; +} + +static void append_authorized_key(const char *keystr) +{ + if (!tmate_session->authorized_keys) + reset_and_enable_authorized_keys(); + + ssh_key pkey = import_ssh_pubkey64(keystr); + if (!pkey) + return; + + ssh_key *keys = tmate_session->authorized_keys; + int count = get_num_authorized_keys(keys); + keys = xreallocarray(keys, sizeof(ssh_key), count+2); + tmate_session->authorized_keys = keys; + + keys[count++] = pkey; + keys[count] = NULL; +} + +static void tmate_set(char *key, char *value) +{ + if (!strcmp(key, "authorized_keys")) + append_authorized_key(value); +} + +void tmate_hook_set_option(const char *name, const char *val) +{ + if (!strcmp(name, "tmate-authorized-keys")) { + reset_and_enable_authorized_keys(); + } else if (!strcmp(name, "tmate-set")) { + char *key_value = xstrdup(val); + char *s = key_value; + + char *key = strsep(&s, "="); + char *value = s; + if (value) + tmate_set(key, value); + + free(key_value); + } +} + +bool tmate_allow_auth(const char *pubkey) +{ + /* + * Note that we don't accept connections on the tmux socket until we + * get the tmate ready message. + */ + if (!tmate_session->authorized_keys) + return true; + + if (!pubkey) + return false; + + ssh_key client_pkey = import_ssh_pubkey64(pubkey); + if (!client_pkey) + return false; + + bool ret = false; + for (ssh_key *k = tmate_session->authorized_keys; *k; k++) { + if (!ssh_key_cmp(client_pkey, *k, SSH_KEY_CMP_PUBLIC)) { + ret = true; + break; + } + } + + ssh_key_free(client_pkey); + + return ret; +} + +static int write_all(int fd, const char *buf, size_t len) +{ + for (size_t i = 0; i < len;) { + size_t ret = write(fd, buf+i, len-i); + if (ret <= 0) + return -1; + i += ret; + } + return 0; +} + +static int read_all(int fd, char *buf, size_t len) +{ + for (size_t i = 0; i < len;) { + size_t ret = read(fd, buf+i, len-i); + if (ret <= 0) + return -1; + i += ret; + } + + return 0; +} + +/* + * The following is executed in the context of the SSH server + */ +bool would_tmate_session_allow_auth(const char *token, const char *pubkey) +{ + /* + * The existance of this function is a bit unpleasant: + * In order to have the right SSH public key from the SSH client, + * we need to ask the tmate session for a match. Denying the key + * to the SSH client will make it cycle through its keys. + * We briefly connect to the session to get an answer. + * + * Note that the client will get reauthenticated later (see + * server-client.c when identifying the client). + */ + int sock_fd = -1; + int ret = true; + + if (tmate_validated_session_token(token) < 0) + goto out; + + char *sock_path = get_socket_path(token); + + struct sockaddr_un sa; + memset(&sa, 0, sizeof(sa)); + sa.sun_family = AF_UNIX; + size_t size = strlcpy(sa.sun_path, sock_path, sizeof(sa.sun_path)); + free(sock_path); + if (size >= sizeof sa.sun_path) + goto out; + + sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock_fd < 0) + goto out; + + if (connect(sock_fd, (struct sockaddr *)&sa, sizeof sa) == -1) + goto out; + + struct imsg_hdr hdr = { + .type = pubkey ? MSG_IDENTIFY_TMATE_AUTH_PUBKEY : + MSG_IDENTIFY_TMATE_AUTH_NONE, + .len = IMSG_HEADER_SIZE + (pubkey ? strlen(pubkey)+1 : 0), + .flags = 0, + .peerid = PROTOCOL_VERSION, + .pid = -1, + }; + + if (write_all(sock_fd, (void*)&hdr, sizeof(hdr)) < 0) + goto out; + + if (pubkey) { + if (write_all(sock_fd, pubkey, strlen(pubkey)+1) < 0) + goto out; + } + + struct { + struct imsg_hdr hdr; + bool allow; + } __packed recv_msg; + + if (read_all(sock_fd, (void*)&recv_msg, sizeof(recv_msg)) < 0) + goto out; + + if (recv_msg.hdr.type == MSG_TMATE_AUTH_STATUS && + recv_msg.hdr.len == sizeof(recv_msg)) + ret = recv_msg.allow; + + tmate_info("(preauth) allow=%d", ret); + +out: + if (sock_fd != -1) + close(sock_fd); + return ret; +} diff --git a/tmate-daemon-decoder.c b/tmate-daemon-decoder.c index c984b710..b4094300 100644 --- a/tmate-daemon-decoder.c +++ b/tmate-daemon-decoder.c @@ -6,15 +6,29 @@ char *tmate_left_status, *tmate_right_status; +static void tmate_ready(struct tmate_session *session, + __unused struct tmate_unpacker *uk) +{ + /* This message is also used by the websocket server */ + + /* + * We only start accepting connections once the host is ready, this + * way we have the authorized keys loaded correctly + */ + + if (session->authorized_keys) { + int count = get_num_authorized_keys(session->authorized_keys); + tmate_info("Restricting ssh access, num_keys=%d", count); + } + server_add_accept(0); +} + static void tmate_header(struct tmate_session *session, struct tmate_unpacker *uk) { char *ssh_conn_str; session->client_protocol_version = unpack_int(uk); - if (session->client_protocol_version <= 4) { - session->daemon_encoder.mpac_version = 4; - } if (session->client_protocol_version >= 3) { session->client_version = unpack_string(uk); @@ -22,6 +36,15 @@ static void tmate_header(struct tmate_session *session, session->client_version = xstrdup("1.8.5"); } + if (session->client_protocol_version < 6) { + /* older clients don't send a ready message */ + tmate_ready(session, NULL); + } + + if (session->client_protocol_version < 5) { + session->daemon_encoder.mpac_version = 4; + } + tmate_notice("Daemon header: client version: %s, protocol version: %d", session->client_version, session->client_protocol_version); @@ -45,13 +68,6 @@ static void tmate_header(struct tmate_session *session, tmate_send_client_ready(); } -static void tmate_ready(__unused struct tmate_session *session, - __unused struct tmate_unpacker *uk) -{ - /* used by the websocket */ -} - - extern u_int next_window_pane_id; static void tmate_sync_window_panes(struct window *w, diff --git a/tmate-daemon-encoder.c b/tmate-daemon-encoder.c index 2d528825..4baf6985 100644 --- a/tmate-daemon-encoder.c +++ b/tmate-daemon-encoder.c @@ -1,7 +1,7 @@ #include "tmate.h" #include "tmate-protocol.h" -#define pack(what, ...) _pack(&tmate_session->daemon_encoder, what, __VA_ARGS__) +#define pack(what, ...) _pack(&tmate_session->daemon_encoder, what, ##__VA_ARGS__) static void __tmate_notify(const char *msg) { diff --git a/tmate-ssh-server.c b/tmate-ssh-server.c index 294f16c4..6624ba70 100644 --- a/tmate-ssh-server.c +++ b/tmate-ssh-server.c @@ -127,7 +127,7 @@ static ssh_channel channel_open_request_cb(ssh_session session, void *userdata) static int auth_pubkey_cb(__unused ssh_session session, const char *user, - struct ssh_key_struct *pubkey, + ssh_key pubkey, char signature_state, void *userdata) { struct tmate_ssh_client *client = userdata; @@ -135,9 +135,24 @@ static int auth_pubkey_cb(__unused ssh_session session, switch (signature_state) { case SSH_PUBLICKEY_STATE_VALID: client->username = xstrdup(user); - if (ssh_pki_export_pubkey_base64(pubkey, &client->pubkey) != SSH_OK) + + const char *key_type = ssh_key_type_to_char(ssh_key_type(pubkey)); + + char *b64_key; + if (ssh_pki_export_pubkey_base64(pubkey, &b64_key) != SSH_OK) tmate_fatal("error getting public key"); + char *pubkey64; + xasprintf(&pubkey64, "%s %s", key_type, b64_key); + free(b64_key); + + if (!would_tmate_session_allow_auth(user, pubkey64)) { + free(pubkey64); + return SSH_AUTH_DENIED; + } + + client->pubkey = pubkey64; + return SSH_AUTH_SUCCESS; case SSH_PUBLICKEY_STATE_NONE: return SSH_AUTH_SUCCESS; @@ -146,14 +161,15 @@ static int auth_pubkey_cb(__unused ssh_session session, } } -static int auth_none_cb(ssh_session session, const char *user, void *userdata) +static int auth_none_cb(__unused ssh_session session, const char *user, void *userdata) { - (void)session; - struct tmate_ssh_client *client = userdata; + if (!would_tmate_session_allow_auth(user, NULL)) + return SSH_AUTH_DENIED; + client->username = xstrdup(user); - client->pubkey = xstrdup("none"); + client->pubkey = NULL; return SSH_AUTH_SUCCESS; } diff --git a/tmate-websocket.c b/tmate-websocket.c index 53a1993e..eed4145b 100644 --- a/tmate-websocket.c +++ b/tmate-websocket.c @@ -16,7 +16,14 @@ #define CONTROL_PROTOCOL_VERSION 2 -#define pack(what, ...) _pack(&tmate_session->websocket_encoder, what, __VA_ARGS__) +#define pack(what, ...) _pack(&tmate_session->websocket_encoder, what, ##__VA_ARGS__) + +#define pack_string_or_nil(str) ({ \ + if (str) \ + pack(string, str); \ + else \ + pack(nil); \ +}) static void ctl_daemon_fwd_msg(__unused struct tmate_session *session, struct tmate_unpacker *uk) @@ -205,7 +212,7 @@ void tmate_websocket_exec(struct tmate_session *session, const char *command) pack(int, TMATE_CTL_EXEC); pack(string, client->username); pack(string, client->ip_address); - pack(string, client->pubkey); + pack_string_or_nil(client->pubkey); pack(string, command); } @@ -223,7 +230,7 @@ void tmate_notify_client_join(__unused struct tmate_session *session, pack(int, TMATE_CTL_CLIENT_JOIN); pack(int, c->id); pack(string, c->ip_address); - pack(string, c->pubkey); + pack_string_or_nil(c->pubkey); pack(boolean, c->readonly); } @@ -270,7 +277,7 @@ void tmate_send_websocket_header(struct tmate_session *session) pack(int, TMATE_CTL_HEADER); pack(int, CONTROL_PROTOCOL_VERSION); pack(string, session->ssh_client.ip_address); - pack(string, session->ssh_client.pubkey); + pack_string_or_nil(session->ssh_client.pubkey); pack(string, session->session_token); pack(string, session->session_token_ro); diff --git a/tmate.h b/tmate.h index c3ca0ef2..b70053f9 100644 --- a/tmate.h +++ b/tmate.h @@ -33,6 +33,12 @@ extern void printflike(2, 3) tmate_log(int level, const char *msg, ...); exit(1); \ }) +/* tmate-auth-keys.c */ +extern void tmate_hook_set_option(const char *name, const char *val); +extern bool tmate_allow_auth(const char *pubkey); +extern bool would_tmate_session_allow_auth(const char *token, const char *pubkey); +extern int get_num_authorized_keys(ssh_key *keys); + /* tmate-msgpack.c */ typedef void tmate_encoder_write_cb(void *userdata, struct evbuffer *buffer); @@ -57,7 +63,7 @@ extern void msgpack_pack_boolean(msgpack_packer *pk, bool value); extern int _msgpack_pack_object(msgpack_packer *pk, msgpack_object d); #define msgpack_pack_object _msgpack_pack_object -#define _pack(enc, what, ...) msgpack_pack_##what(&(enc)->pk, __VA_ARGS__) +#define _pack(enc, what, ...) msgpack_pack_##what(&(enc)->pk, ##__VA_ARGS__) struct tmate_unpacker; struct tmate_decoder; @@ -222,6 +228,8 @@ struct tmate_session { int tmux_socket_fd; /* only for role deamon */ + ssh_key *authorized_keys; /* array with NULL as last element */ + const char *session_token; const char *session_token_ro; const char *obfuscated_session_token; /* for logging purposes */ diff --git a/tmux.h b/tmux.h index 4fd1bbb0..1821a3c6 100644 --- a/tmux.h +++ b/tmux.h @@ -414,13 +414,6 @@ enum msgtype { MSG_IDENTIFY_CLIENTPID, MSG_IDENTIFY_CWD, -#ifdef TMATE_SLAVE - /* Next time put this after TMATE_LATENCY */ - MSG_IDENTIFY_TMATE_IP_ADDRESS, - MSG_IDENTIFY_TMATE_PUBKEY, - MSG_IDENTIFY_TMATE_READONLY, -#endif - MSG_COMMAND = 200, MSG_DETACH, MSG_DETACHKILL, @@ -440,7 +433,12 @@ enum msgtype { MSG_WAKEUP, #ifdef TMATE_SLAVE - MSG_LATENCY = 300 + MSG_LATENCY = 300, + MSG_IDENTIFY_TMATE_IP_ADDRESS, + MSG_IDENTIFY_TMATE_AUTH_NONE, + MSG_IDENTIFY_TMATE_AUTH_PUBKEY, + MSG_IDENTIFY_TMATE_READONLY, + MSG_TMATE_AUTH_STATUS, #endif }; @@ -1307,6 +1305,7 @@ struct client { #define CLIENT_STATUSFORCE 0x80000 #ifdef TMATE_SLAVE #define CLIENT_TMATE_NOTIFIED_JOIN 0x10000000 +#define CLIENT_TMATE_AUTHENTICATED 0x20000000 #endif int flags; struct key_table *keytable;