Add an ACL list for users connecting to the tmux socket. Users may be forbidden

from attaching, forced to attach read-only, or allowed to attach read-write. A
new command, server-access, configures the list. tmux gets the user using
getpeereid(3) of the client socket. Users must still configure file system
permissions manually.
This commit is contained in:
Nicholas Marriott 2022-04-06 14:28:50 +01:00
parent 6e9a9d265e
commit d6306b634e
12 changed files with 445 additions and 14 deletions

View File

@ -123,6 +123,7 @@ dist_tmux_SOURCES = \
cmd-select-pane.c \ cmd-select-pane.c \
cmd-select-window.c \ cmd-select-window.c \
cmd-send-keys.c \ cmd-send-keys.c \
cmd-server-access.c \
cmd-set-buffer.c \ cmd-set-buffer.c \
cmd-set-environment.c \ cmd-set-environment.c \
cmd-set-option.c \ cmd-set-option.c \
@ -172,6 +173,7 @@ dist_tmux_SOURCES = \
screen-redraw.c \ screen-redraw.c \
screen-write.c \ screen-write.c \
screen.c \ screen.c \
server-acl.c \
server-client.c \ server-client.c \
server-fn.c \ server-fn.c \
server.c \ server.c \

View File

@ -361,6 +361,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags,
/* Send identify messages. */ /* Send identify messages. */
client_send_identify(ttynam, termname, caps, ncaps, cwd, feat); client_send_identify(ttynam, termname, caps, ncaps, cwd, feat);
tty_term_free_list(caps, ncaps); tty_term_free_list(caps, ncaps);
proc_flush_peer(client_peer);
/* Send first command. */ /* Send first command. */
if (msg == MSG_COMMAND) { if (msg == MSG_COMMAND) {

View File

@ -43,7 +43,7 @@ const struct cmd_entry cmd_attach_session_entry = {
/* -t is special */ /* -t is special */
.flags = CMD_STARTSERVER, .flags = CMD_STARTSERVER|CMD_READONLY,
.exec = cmd_attach_session_exec .exec = cmd_attach_session_exec
}; };
@ -69,6 +69,7 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag,
if (c == NULL) if (c == NULL)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
if (server_client_check_nested(c)) { if (server_client_check_nested(c)) {
cmdq_error(item, "sessions should be nested with care, " cmdq_error(item, "sessions should be nested with care, "
"unset $TMUX to force"); "unset $TMUX to force");

View File

@ -19,9 +19,11 @@
#include <sys/types.h> #include <sys/types.h>
#include <ctype.h> #include <ctype.h>
#include <pwd.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <unistd.h>
#include "tmux.h" #include "tmux.h"
@ -558,17 +560,31 @@ cmdq_add_message(struct cmdq_item *item)
{ {
struct client *c = item->client; struct client *c = item->client;
struct cmdq_state *state = item->state; struct cmdq_state *state = item->state;
const char *name, *key; const char *key;
char *tmp; char *tmp;
uid_t uid;
struct passwd *pw;
char *user = NULL;
tmp = cmd_print(item->cmd); tmp = cmd_print(item->cmd);
if (c != NULL) { if (c != NULL) {
name = c->name; uid = proc_get_peer_uid(c->peer);
if (uid != getuid()) {
if ((pw = getpwuid(uid)) != NULL)
xasprintf(&user, "[%s]", pw->pw_name);
else
user = xstrdup("[unknown]");
} else
user = xstrdup("");
if (c->session != NULL && state->event.key != KEYC_NONE) { if (c->session != NULL && state->event.key != KEYC_NONE) {
key = key_string_lookup_key(state->event.key, 0); key = key_string_lookup_key(state->event.key, 0);
server_add_message("%s key %s: %s", name, key, tmp); server_add_message("%s%s key %s: %s", c->name, user,
} else key, tmp);
server_add_message("%s command: %s", name, tmp); } else {
server_add_message("%s%s command: %s", c->name, user,
tmp);
}
free(user);
} else } else
server_add_message("command: %s", tmp); server_add_message("command: %s", tmp);
free(tmp); free(tmp);

147
cmd-server-access.c Normal file
View File

@ -0,0 +1,147 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2021 Dallas Lyons <dallasdlyons@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/stat.h>
#include <sys/types.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include "tmux.h"
/*
* Controls access to session.
*/
static enum cmd_retval cmd_server_access_exec(struct cmd *, struct cmdq_item *);
const struct cmd_entry cmd_server_access_entry = {
.name = "server-access",
.alias = NULL,
.args = { "adlrw", 0, 1, NULL },
.usage = "[-adlrw]" CMD_TARGET_PANE_USAGE " [user]",
.flags = CMD_CLIENT_CANFAIL,
.exec = cmd_server_access_exec
};
static enum cmd_retval
cmd_server_access_deny(struct cmdq_item *item, struct passwd *pw)
{
struct client *loop;
struct server_acl_user *user;
uid_t uid;
if ((user = server_acl_user_find(pw->pw_uid)) == NULL) {
cmdq_error(item, "user %s not found", pw->pw_name);
return (CMD_RETURN_ERROR);
}
TAILQ_FOREACH(loop, &clients, entry) {
uid = proc_get_peer_uid(loop->peer);
if (uid == server_acl_get_uid(user)) {
loop->exit_message = xstrdup("access not allowed");
loop->flags |= CLIENT_EXIT;
}
}
server_acl_user_deny(pw->pw_uid);
return (CMD_RETURN_NORMAL);
}
static enum cmd_retval
cmd_server_access_exec(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_target_client(item);
char *name;
struct passwd *pw = NULL;
if (args_has(args, 'l')) {
server_acl_display(item);
return (CMD_RETURN_NORMAL);
}
if (args_count(args) == 0) {
cmdq_error(item, "missing user arguement");
return (CMD_RETURN_ERROR);
}
name = format_single(item, args_string(args, 0), c, NULL, NULL, NULL);
if (*name != '\0')
pw = getpwnam(name);
if (pw == NULL) {
cmdq_error(item, "unknown user: %s", name);
return (CMD_RETURN_ERROR);
}
free(name);
if (pw->pw_uid == 0 || pw->pw_uid == getuid()) {
cmdq_error(item, "%s owns the server, can't change access",
pw->pw_name);
return (CMD_RETURN_ERROR);
}
if (args_has(args, 'a') && args_has(args, 'd')) {
cmdq_error(item, "-a and -d cannot be used together");
return (CMD_RETURN_ERROR);
}
if (args_has(args, 'w') && args_has(args, 'r')) {
cmdq_error(item, "-r and -w cannot be used together");
return (CMD_RETURN_ERROR);
}
if (args_has(args, 'd'))
return (cmd_server_access_deny(item, pw));
if (args_has(args, 'a')) {
if (server_acl_user_find(pw->pw_uid) != NULL) {
cmdq_error(item, "user %s is already added",
pw->pw_name);
return (CMD_RETURN_ERROR);
}
server_acl_user_allow(pw->pw_uid);
/* Do not return - allow -r or -w with -a. */
} else if (args_has(args, 'r') || args_has(args, 'w')) {
/* -r or -w implies -a if user does not exist. */
if (server_acl_user_find(pw->pw_uid) == NULL)
server_acl_user_allow(pw->pw_uid);
}
if (args_has(args, 'w')) {
if (server_acl_user_find(pw->pw_uid) == NULL) {
cmdq_error(item, "user %s not found", pw->pw_name);
return (CMD_RETURN_ERROR);
}
server_acl_user_allow_write(pw->pw_uid);
return (CMD_RETURN_NORMAL);
}
if (args_has(args, 'r')) {
if (server_acl_user_find(pw->pw_uid) == NULL) {
cmdq_error(item, "user %s not found", pw->pw_name);
return (CMD_RETURN_ERROR);
}
server_acl_user_deny_write(pw->pw_uid);
return (CMD_RETURN_NORMAL);
}
return (CMD_RETURN_NORMAL);
}

2
cmd.c
View File

@ -95,6 +95,7 @@ extern const struct cmd_entry cmd_select_pane_entry;
extern const struct cmd_entry cmd_select_window_entry; extern const struct cmd_entry cmd_select_window_entry;
extern const struct cmd_entry cmd_send_keys_entry; extern const struct cmd_entry cmd_send_keys_entry;
extern const struct cmd_entry cmd_send_prefix_entry; extern const struct cmd_entry cmd_send_prefix_entry;
extern const struct cmd_entry cmd_server_access_entry;
extern const struct cmd_entry cmd_set_buffer_entry; extern const struct cmd_entry cmd_set_buffer_entry;
extern const struct cmd_entry cmd_set_environment_entry; extern const struct cmd_entry cmd_set_environment_entry;
extern const struct cmd_entry cmd_set_hook_entry; extern const struct cmd_entry cmd_set_hook_entry;
@ -187,6 +188,7 @@ const struct cmd_entry *cmd_table[] = {
&cmd_select_window_entry, &cmd_select_window_entry,
&cmd_send_keys_entry, &cmd_send_keys_entry,
&cmd_send_prefix_entry, &cmd_send_prefix_entry,
&cmd_server_access_entry,
&cmd_set_buffer_entry, &cmd_set_buffer_entry,
&cmd_set_environment_entry, &cmd_set_environment_entry,
&cmd_set_hook_entry, &cmd_set_hook_entry,

6
proc.c
View File

@ -349,6 +349,12 @@ proc_kill_peer(struct tmuxpeer *peer)
peer->flags |= PEER_BAD; peer->flags |= PEER_BAD;
} }
void
proc_flush_peer(struct tmuxpeer *peer)
{
imsg_flush(&peer->ibuf);
}
void void
proc_toggle_log(struct tmuxproc *tp) proc_toggle_log(struct tmuxproc *tp)
{ {

182
server-acl.c Normal file
View File

@ -0,0 +1,182 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2021 Holland Schutte, Jayson Morberg
* Copyright (c) 2021 Dallas Lyons <dallasdlyons@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <ctype.h>
#include <pwd.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "tmux.h"
struct server_acl_user {
uid_t uid;
int flags;
#define SERVER_ACL_READONLY 0x1
RB_ENTRY(server_acl_user) entry;
};
static int
server_acl_cmp(struct server_acl_user *user1, struct server_acl_user *user2)
{
if (user1->uid < user2->uid)
return -1;
return user1->uid > user2->uid;
}
RB_HEAD(server_acl_entries, server_acl_user) server_acl_entries;
RB_GENERATE_STATIC(server_acl_entries, server_acl_user, entry, server_acl_cmp);
/* Initialize server_acl tree. */
void
server_acl_init(void)
{
RB_INIT(&server_acl_entries);
if (getuid() != 0)
server_acl_user_allow(0);
server_acl_user_allow(getuid());
}
/* Find user entry. */
struct server_acl_user*
server_acl_user_find(uid_t uid)
{
struct server_acl_user find = { .uid = uid };
return RB_FIND(server_acl_entries, &server_acl_entries, &find);
}
/* Display the tree. */
void
server_acl_display(struct cmdq_item *item)
{
struct server_acl_user *loop;
struct passwd *pw;
const char *name;
RB_FOREACH(loop, server_acl_entries, &server_acl_entries) {
if (loop->uid == 0)
continue;
if ((pw = getpwuid(loop->uid)) != NULL)
name = pw->pw_name;
else
name = "unknown";
if (loop->flags == SERVER_ACL_READONLY)
cmdq_print(item, "%s (R)", name);
else
cmdq_print(item, "%s (W)", name);
}
}
/* Allow a user. */
void
server_acl_user_allow(uid_t uid)
{
struct server_acl_user *user;
user = server_acl_user_find(uid);
if (user == NULL) {
user = xcalloc(1, sizeof *user);
user->uid = uid;
RB_INSERT(server_acl_entries, &server_acl_entries, user);
}
}
/* Deny a user (remove from the tree). */
void
server_acl_user_deny(uid_t uid)
{
struct server_acl_user *user;
user = server_acl_user_find(uid);
if (user != NULL) {
RB_REMOVE(server_acl_entries, &server_acl_entries, user);
free(user);
}
}
/* Allow this user write access. */
void
server_acl_user_allow_write(uid_t uid)
{
struct server_acl_user *user;
struct client *c;
user = server_acl_user_find(uid);
if (user == NULL)
return;
user->flags &= ~SERVER_ACL_READONLY;
TAILQ_FOREACH(c, &clients, entry) {
uid = proc_get_peer_uid(c->peer);
if (uid != (uid_t)-1 && uid == user->uid)
c->flags &= ~CLIENT_READONLY;
}
}
/* Deny this user write access. */
void
server_acl_user_deny_write(uid_t uid)
{
struct server_acl_user *user;
struct client *c;
user = server_acl_user_find(uid);
if (user == NULL)
return;
user->flags |= SERVER_ACL_READONLY;
TAILQ_FOREACH(c, &clients, entry) {
uid = proc_get_peer_uid(c->peer);
if (uid == user->uid && uid == user->uid)
c->flags |= CLIENT_READONLY;
}
}
/*
* Check if the client's UID exists in the ACL list and if so, set as read only
* if needed. Return false if the user does not exist.
*/
int
server_acl_join(struct client *c)
{
struct server_acl_user *user;
uid_t uid = proc_get_peer_uid(c->peer);
user = server_acl_user_find(uid);
if (user == NULL)
return (0);
if (user->flags & SERVER_ACL_READONLY)
c->flags |= CLIENT_READONLY;
return (1);
}
/* Get UID for user entry. */
uid_t
server_acl_get_uid(struct server_acl_user *user)
{
return (user->uid);
}

View File

@ -2772,6 +2772,14 @@ server_client_dispatch(struct imsg *imsg, void *arg)
} }
} }
/* Callback when command is not allowed. */
static enum cmd_retval
server_client_read_only(struct cmdq_item *item, __unused void *data)
{
cmdq_error(item, "client is read-only");
return (CMD_RETURN_ERROR);
}
/* Callback when command is done. */ /* Callback when command is done. */
static enum cmd_retval static enum cmd_retval
server_client_command_done(struct cmdq_item *item, __unused void *data) server_client_command_done(struct cmdq_item *item, __unused void *data)
@ -2796,6 +2804,7 @@ server_client_dispatch_command(struct client *c, struct imsg *imsg)
char **argv, *cause; char **argv, *cause;
struct cmd_parse_result *pr; struct cmd_parse_result *pr;
struct args_value *values; struct args_value *values;
struct cmdq_item *new_item;
if (c->flags & CLIENT_EXIT) if (c->flags & CLIENT_EXIT)
return; return;
@ -2834,7 +2843,12 @@ server_client_dispatch_command(struct client *c, struct imsg *imsg)
free(values); free(values);
cmd_free_argv(argc, argv); cmd_free_argv(argc, argv);
cmdq_append(c, cmdq_get_command(pr->cmdlist, NULL)); if ((c->flags & CLIENT_READONLY) &&
!cmd_list_all_have(pr->cmdlist, CMD_READONLY))
new_item = cmdq_get_callback(server_client_read_only, NULL);
else
new_item = cmdq_get_command(pr->cmdlist, NULL);
cmdq_append(c, new_item);
cmdq_append(c, cmdq_get_callback(server_client_command_done, NULL)); cmdq_append(c, cmdq_get_callback(server_client_command_done, NULL));
cmd_list_free(pr->cmdlist); cmd_list_free(pr->cmdlist);
@ -3072,9 +3086,11 @@ server_client_set_flags(struct client *c, const char *flags)
continue; continue;
log_debug("client %s set flag %s", c->name, next); log_debug("client %s set flag %s", c->name, next);
if (not) if (not) {
if (c->flags & CLIENT_READONLY)
flag &= ~CLIENT_READONLY;
c->flags &= ~flag; c->flags &= ~flag;
else } else
c->flags |= flag; c->flags |= flag;
if (flag == CLIENT_CONTROL_NOOUTPUT) if (flag == CLIENT_CONTROL_NOOUTPUT)
control_reset_offsets(c); control_reset_offsets(c);

View File

@ -245,6 +245,8 @@ server_start(struct tmuxproc *client, int flags, struct event_base *base,
evtimer_set(&server_ev_tidy, server_tidy_event, NULL); evtimer_set(&server_ev_tidy, server_tidy_event, NULL);
evtimer_add(&server_ev_tidy, &tv); evtimer_add(&server_ev_tidy, &tv);
server_acl_init();
server_add_accept(0); server_add_accept(0);
proc_loop(server_proc, server_loop); proc_loop(server_proc, server_loop);
@ -361,9 +363,10 @@ server_update_socket(void)
static void static void
server_accept(int fd, short events, __unused void *data) server_accept(int fd, short events, __unused void *data)
{ {
struct sockaddr_storage sa; struct sockaddr_storage sa;
socklen_t slen = sizeof sa; socklen_t slen = sizeof sa;
int newfd; int newfd;
struct client *c;
server_add_accept(0); server_add_accept(0);
if (!(events & EV_READ)) if (!(events & EV_READ))
@ -380,11 +383,16 @@ server_accept(int fd, short events, __unused void *data)
} }
fatal("accept failed"); fatal("accept failed");
} }
if (server_exit) { if (server_exit) {
close(newfd); close(newfd);
return; return;
} }
server_client_create(newfd); c = server_client_create(newfd);
if (!server_acl_join(c)) {
c->exit_message = xstrdup("access not allowed");
c->flags |= CLIENT_EXIT;
}
} }
/* /*

40
tmux.1
View File

@ -1488,6 +1488,44 @@ option.
.D1 Pq alias: Ic rename .D1 Pq alias: Ic rename
Rename the session to Rename the session to
.Ar new-name . .Ar new-name .
.It Xo Ic server-access
.Op Fl adlrw
.Op Ar user
.Xc
Change the access or read/write permission of
.Ar user .
The user running the
.Nm
server (its owner) and the root user cannot be changed and are always
permitted access.
.Pp
.Fl a
and
.Fl d
are used to give or revoke access for the specified user.
If the user is already attached, the
.Fl d
flag causes their clients to be detached.
.Pp
.Fl r
and
.Fl w
change the permissions for
.Ar user :
.Fl r
makes their clients read-only and
.Fl w
writable.
.Fl l
lists current access permissions.
.Pp
By default, the access list is empty and
.Nm
creates sockets with file system permissions preventing access by any user
other than the owner (and root).
These permissions must be changed manually.
Great care should be taken not to allow access to untrusted users even
read-only.
.Tg showmsgs .Tg showmsgs
.It Xo Ic show-messages .It Xo Ic show-messages
.Op Fl JT .Op Fl JT
@ -5072,7 +5110,7 @@ The following variables are available, where appropriate:
.It Li "client_name" Ta "" Ta "Name of client" .It Li "client_name" Ta "" Ta "Name of client"
.It Li "client_pid" Ta "" Ta "PID of client process" .It Li "client_pid" Ta "" Ta "PID of client process"
.It Li "client_prefix" Ta "" Ta "1 if prefix key has been pressed" .It Li "client_prefix" Ta "" Ta "1 if prefix key has been pressed"
.It Li "client_readonly" Ta "" Ta "1 if client is readonly" .It Li "client_readonly" Ta "" Ta "1 if client is read-only"
.It Li "client_session" Ta "" Ta "Name of the client's session" .It Li "client_session" Ta "" Ta "Name of the client's session"
.It Li "client_termfeatures" Ta "" Ta "Terminal features of client, if any" .It Li "client_termfeatures" Ta "" Ta "Terminal features of client, if any"
.It Li "client_termname" Ta "" Ta "Terminal name of client" .It Li "client_termname" Ta "" Ta "Terminal name of client"

12
tmux.h
View File

@ -2025,6 +2025,7 @@ struct tmuxpeer *proc_add_peer(struct tmuxproc *, int,
void (*)(struct imsg *, void *), void *); void (*)(struct imsg *, void *), void *);
void proc_remove_peer(struct tmuxpeer *); void proc_remove_peer(struct tmuxpeer *);
void proc_kill_peer(struct tmuxpeer *); void proc_kill_peer(struct tmuxpeer *);
void proc_flush_peer(struct tmuxpeer *);
void proc_toggle_log(struct tmuxproc *); void proc_toggle_log(struct tmuxproc *);
pid_t proc_fork_and_daemon(int *); pid_t proc_fork_and_daemon(int *);
uid_t proc_get_peer_uid(struct tmuxpeer *); uid_t proc_get_peer_uid(struct tmuxpeer *);
@ -3269,4 +3270,15 @@ struct window_pane *spawn_pane(struct spawn_context *, char **);
/* regsub.c */ /* regsub.c */
char *regsub(const char *, const char *, const char *, int); char *regsub(const char *, const char *, const char *, int);
/* server-acl.c */
void server_acl_init(void);
struct server_acl_user *server_acl_user_find(uid_t);
void server_acl_display(struct cmdq_item *);
void server_acl_user_allow(uid_t);
void server_acl_user_deny(uid_t);
void server_acl_user_allow_write(uid_t);
void server_acl_user_deny_write(uid_t);
int server_acl_join(struct client *);
uid_t server_acl_get_uid(struct server_acl_user *);
#endif /* TMUX_H */ #endif /* TMUX_H */