├── Makefile ├── README.md ├── docker-cmd.c ├── docker-jailsh.c ├── docker-mkjail ├── docker-reaper.c ├── docker-rmjail ├── shared.c └── shared.h /Makefile: -------------------------------------------------------------------------------- 1 | # You might need/want to change the installation path, and the default shell. 2 | DOCKER_PATH = $(shell which docker) 3 | TARGET_PATH = /usr/local/bin 4 | SHELL = /bin/bash 5 | 6 | CC = gcc 7 | CFLAGS = -O -Wall -ansi -pedantic -std=c99 -DDOCKER_PATH=\"$(DOCKER_PATH)\" -I. 8 | LDFLAGS = -lutil 9 | TARGETS = docker-cmd docker-jailsh docker-reaper 10 | 11 | %.o: %.c 12 | $(CC) $(CFLAGS) -c -o $@ $< 13 | 14 | %: %.c 15 | $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) 16 | 17 | all: $(TARGETS) 18 | 19 | install: $(TARGETS) 20 | install -m 755 -o 0 -g 0 $(TARGETS) docker-mkjail docker-rmjail $(TARGET_PATH) 21 | 22 | clean: 23 | -$(RM) $(TARGETS) *.o 24 | 25 | docker-cmd: shared.o 26 | 27 | docker-reaper: docker-reaper.c 28 | $(CC) $(CFLAGS) -o $@ $^ 29 | 30 | docker-jailsh: docker-jailsh.c shared.o 31 | $(CC) $(CFLAGS) -o $@ $^ -DSHELL=\"$(SHELL)\" 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | docker-cmd 2 | ========== 3 | 4 | The docker-cmd project provides an alternative to docker-bash and 5 | dockersh (based on nsenter), that does the right thing and allocates 6 | a new pty from within the target container, so that programs such as 7 | tmux and screen works within the session in question. 8 | 9 | Usage: docker-cmd CONTAINER USER CMD ARGS... 10 | 11 | If no username is provided, it will be set to root. If no command 12 | is provided, /bin/bash is used. 13 | 14 | The docker-mkjail and docker-rmjail tools are provided in order to 15 | simplify the process of setting up a jail for SSH users (for example). 16 | For more information about this, look at the "Setting up a Docker jail" 17 | section. 18 | 19 | Installation 20 | ============ 21 | 22 | To download, build and install the docker-cmd tools, run: 23 | ``` 24 | git clone https://github.com/clevcode/docker-cmd.git 25 | cd docker-cmd 26 | make clean all 27 | sudo make install 28 | ``` 29 | 30 | Basic usage 31 | =========== 32 | 33 | To use docker-cmd as a replacement for docker-bash or docker-enter, 34 | simply use it like this: 35 | ``` 36 | # To enter the container with name CONTAINER as root and run /bin/bash 37 | sudo docker-cmd CONTAINER 38 | 39 | # To run echo Hello World from within CONTAINER as user nobody 40 | sudo docker-cmd CONTAINER nobody run echo Hello World 41 | ``` 42 | The docker-reaper command can be used as the init-process in containers 43 | that do not require any daemons (and note that running an SSH daemon from 44 | within the container is quite unnecessary, when you can simply use 45 | docker-cmd instead). Example: 46 | ``` 47 | je@seth:~$ docker run -h test --name test -v $(which docker-reaper):/sbin/reaper:ro \ 48 | -d ubuntu:trusty /sbin/reaper 49 | 20096f6b9871052f54687632d6cf12d9671bbf6a2d9792b2f7eac787b8d013f3 50 | je@seth:~$ tty 51 | /dev/pts/22 52 | je@seth:~$ sudo docker-cmd test 53 | root@test:~# ps auxw 54 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND 55 | root 1 0.0 0.0 4192 356 ? Ss 13:05 0:00 /sbin/reaper 56 | root 8 0.0 0.0 18168 1876 pts/0 Ss 13:05 0:00 /bin/bash 57 | root 18 0.0 0.0 15568 1124 pts/0 R+ 13:05 0:00 ps auxw 58 | root@test:~# tty 59 | /dev/pts/0 60 | ``` 61 | As you can see, unlike most other solutions, a new pty will be allocated 62 | within the target container so that commands such as tmux and screen work 63 | fine. With docker-bash and docker-enter, you would see "not a tty" as the 64 | output from the tty-command, since the pty connected to the session is 65 | located outside the Docker container namespace. 66 | 67 | Setting up a Docker jail 68 | ======================== 69 | 70 | To create a jail for the user with username "luser", configure sudo to 71 | allow entering the jail, and change the login shell for the user so that 72 | the jail is automatically entered when logging in, run: 73 | ``` 74 | sudo docker-mkjail luser 75 | ``` 76 | If you want to disable the jail and restore the default login shell for "luser", run: 77 | ``` 78 | sudo docker-rmjail luser 79 | ``` 80 | To enter the jail for a user as root, to make further customizations, run: 81 | ``` 82 | sudo docker-cmd jail_luser 83 | ``` 84 | The first time you create a jail, it will create a new base image. This will 85 | take some time. The next time you create a jail for a user, this image will 86 | be reused. If you want to remove a jail base image, in order to create a new 87 | one, you should run (assuming the default base image name "jail" is used) the 88 | following to remove the existing one first: 89 | ``` 90 | sudo docker rm jail 91 | sudo docker rmi jail 92 | ``` 93 | 94 | Notes 95 | ===== 96 | 97 | You might want to customize the docker-mkjail script, if you want to 98 | use another Docker base image or change the commands that are executed 99 | to set it up. By default, it will set up a jail container based on 100 | Ubuntu (Trusty), and disable all SUID/SGID programs within the jail in 101 | order to decrease the chances of performing a privilege escalation 102 | attack. Another thing you might want to do is add more shared folders, 103 | by adding -v /path/on/host:/path/in/guest:rw to the docker run line. 104 | They will by default only have access to their own home directory. 105 | 106 | Note that a kernel vulnerability could obviously be abused in order to 107 | break out of the Docker container regardless. The only way to be fully 108 | secure is to not keep any sensitive data/services on the same host as 109 | you are letting people log in to, even when using this. 110 | 111 | The reaper-daemon (docker-reaper) is used as the init-process in jail 112 | containers, and simply handles the reaping of zombie processes. When 113 | using docker-cmd to put users in jails, there is usually no need for 114 | daemon processes running from within the jail container, and thus a 115 | real init-process is not necessary. It would be possible to simply run 116 | /bin/bash here, of course, but besides being a waste of CPU cycles and 117 | RAM it would also result in zombie processes not being reaped. 118 | 119 | Future enhancements 120 | =================== 121 | 122 | Since this was developed primarily with the intention of quickly getting 123 | a working solution for myself and the specific use-case I had in mind, 124 | it is not very configurable. If anyone wants to take the time and add 125 | some getopt()-handling, and possibly a simple configuration file parser, 126 | it would be appreciated. If not, I'll probably add this when I have some 127 | more time to spare. 128 | 129 | Things that should be configurable includes the namespaces to clone, what 130 | uid/gid to use, what environment variables to allow, the working directory, 131 | whether to allocate a tty or not (at the moment, it will check if stdin is 132 | connected to a tty to determine whether to allocate one), and so on. 133 | 134 | Another feature that I would like to have is for the username parameter 135 | to be interpreted as a username within the container. At the moment, the 136 | UID and GID is determined by looking up the username on the host. 137 | 138 | Security 139 | ======== 140 | 141 | While I have tried hard to minimize the risk of this being abused, I make 142 | no guarantees about the security of this solution. Since my primary area of 143 | expertise is within the offensive side of IT-security, rather than the 144 | defensive, I know how futile it might be to make any such guarantees. 145 | There will always be factors beyond my control, such as kernel vulnerabilities. 146 | That being said, I believe I have done my part in minimizing the possible 147 | attack vectors. :) 148 | 149 | Copyright (C) Joel Eriksson 2014 150 | -------------------------------------------------------------------------------- /docker-cmd.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Joel Eriksson 2014 3 | */ 4 | 5 | #define _GNU_SOURCE 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "shared.h" 25 | 26 | int clone_namespace(pid_t pid, int nstype) 27 | { 28 | char *type = NULL; 29 | char fname[32]; 30 | int fd; 31 | int n; 32 | 33 | switch (nstype) { 34 | case CLONE_NEWIPC: type = "ipc"; break; 35 | case CLONE_NEWUTS: type = "uts"; break; 36 | case CLONE_NEWNET: type = "net"; break; 37 | case CLONE_NEWPID: type = "pid"; break; 38 | case CLONE_NEWNS: type = "mnt"; break; 39 | case CLONE_NEWUSER: type = "user"; break; 40 | } 41 | 42 | if (type == NULL) { 43 | fprintf(stderr, "clone_namespace: Invalid nstype (%d)\n", nstype); 44 | return -1; 45 | } 46 | 47 | n = snprintf(fname, sizeof(fname), "/proc/%u/ns/%s", pid, type); 48 | if (n < 0 || n >= sizeof(fname)) { 49 | fprintf(stderr, "clone_namespace: snprintf() failed\n"); 50 | return -1; 51 | } 52 | 53 | if ((fd = open(fname, O_RDONLY)) == -1) { 54 | fprintf(stderr, "clone_namespace(%d, %s): open: %s\n", pid, type, strerror(errno)); 55 | return -1; 56 | } 57 | 58 | if (setns(fd, nstype) == -1) { 59 | fprintf(stderr, "clone_namespace(%d, %s): setns: %s\n", pid, type, strerror(errno)); 60 | return -1; 61 | } 62 | 63 | close(fd); 64 | 65 | return 0; 66 | } 67 | 68 | ssize_t writen(int fd, const char *buf, size_t len) 69 | { 70 | const char *data = buf; 71 | size_t left = len; 72 | ssize_t n; 73 | 74 | while (left > 0) { 75 | if ((n = write(fd, data, left)) == -1) { 76 | if (errno == EAGAIN || errno == EINTR) 77 | continue; 78 | perror("writen: write"); 79 | return -1; 80 | } 81 | data += n; 82 | left -= n; 83 | } 84 | 85 | return (ssize_t) len; 86 | } 87 | 88 | int proxy_fds(int fd1, int fd2) 89 | { 90 | fd_set rfd, wfd, xfd; 91 | char buf[65536]; 92 | ssize_t n; 93 | int max; 94 | 95 | max = fd1 < fd2 ? fd2 : fd1; 96 | 97 | for (;;) { 98 | FD_ZERO(&rfd); 99 | FD_ZERO(&wfd); 100 | FD_ZERO(&xfd); 101 | 102 | FD_SET(fd1, &rfd); 103 | FD_SET(fd2, &rfd); 104 | 105 | if (select(max+1, &rfd, &wfd, &xfd, NULL) == -1) { 106 | if (errno == EINTR || errno == EAGAIN) 107 | continue; 108 | perror("proxy_fds: select"); 109 | return -1; 110 | } 111 | 112 | if (FD_ISSET(fd1, &rfd)) { 113 | n = read(fd1, buf, sizeof(buf)); 114 | if (n == 0) 115 | return 0; 116 | if (n == -1) { 117 | if (errno != EAGAIN && errno != EINTR) { 118 | if (errno == EIO) 119 | return 0; 120 | perror("proxy_fds: read"); 121 | return -1; 122 | } 123 | } else { 124 | if (writen(fd2, buf, n) == -1) { 125 | perror("proxy_fds: write"); 126 | return -1; 127 | } 128 | } 129 | } 130 | 131 | if (FD_ISSET(fd2, &rfd)) { 132 | n = read(fd2, buf, sizeof(buf)); 133 | if (n == 0) 134 | return 0; 135 | if (n == -1) { 136 | if (errno != EAGAIN && errno != EINTR) { 137 | if (errno == EIO) 138 | return 0; 139 | perror("proxy_fds: read"); 140 | return -1; 141 | } 142 | } else { 143 | if (writen(fd1, buf, n) == -1) { 144 | perror("proxy_fds: write"); 145 | return -1; 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | static struct termios org_termios; 153 | static int tty_saved; 154 | 155 | int tty_save() 156 | { 157 | if (tcgetattr(STDIN_FILENO, &org_termios) == -1) { 158 | perror("tty_save: tcgetattr"); 159 | return -1; 160 | } 161 | 162 | tty_saved = 1; 163 | 164 | return 0; 165 | } 166 | 167 | int tty_restore() 168 | { 169 | if (tty_saved) { 170 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &org_termios) == -1) { 171 | perror("tty_restore: tcsetattr"); 172 | return -1; 173 | } 174 | } 175 | 176 | return 0; 177 | } 178 | 179 | int tty_raw() 180 | { 181 | struct termios org_termios; 182 | struct termios raw; 183 | 184 | if (! tty_saved) 185 | if (tty_save() == -1) 186 | return -1; 187 | 188 | raw = org_termios; 189 | 190 | raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 191 | raw.c_oflag &= ~(OPOST); 192 | raw.c_cflag |= (CS8); 193 | raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 194 | raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; 195 | 196 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { 197 | perror("tty_raw: tcsetattr"); 198 | return -1; 199 | } 200 | 201 | return 0; 202 | } 203 | 204 | int get_window_size(int tty_fd, struct winsize *w) 205 | { 206 | if (ioctl(tty_fd, TIOCGWINSZ, w) == -1) { 207 | perror("get_window_size: ioctl"); 208 | return -1; 209 | } 210 | 211 | return 0; 212 | } 213 | 214 | int set_window_size(int tty_fd, struct winsize *w) 215 | { 216 | if (ioctl(tty_fd, TIOCSWINSZ, w) == -1) { 217 | perror("set_window_size: ioctl"); 218 | return -1; 219 | } 220 | 221 | return 0; 222 | } 223 | 224 | int clone_window_size(int src_tty, int dst_tty) 225 | { 226 | struct winsize w; 227 | 228 | if (get_window_size(src_tty, &w) == -1) 229 | return -1; 230 | 231 | if (set_window_size(dst_tty, &w) == -1) 232 | return -1; 233 | 234 | return 0; 235 | } 236 | 237 | int pty_fd; 238 | 239 | void window_size_changed(int sig) 240 | { 241 | clone_window_size(STDIN_FILENO, pty_fd); 242 | } 243 | 244 | /* 245 | * We don't want to use popen() here, since that will also invoke a shell. 246 | * No reason to increase the attack surface more than necessary. 247 | */ 248 | FILE *execve_pipe(const char *fname, const char **argv, const char **envp) 249 | { 250 | int fds[2]; 251 | pid_t pid; 252 | 253 | if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) { 254 | perror("execve_pipe: socketpair"); 255 | return NULL; 256 | } 257 | 258 | if ((pid = vfork()) == -1) { 259 | perror("execve_pipe: vfork"); 260 | close(fds[0]); 261 | close(fds[1]); 262 | return NULL; 263 | } 264 | 265 | if (pid == 0) { 266 | close(fds[0]); 267 | dup2(fds[1], 0); 268 | dup2(fds[1], 1); 269 | close(fds[1]); 270 | close_files(); /* just in case, prevent fd leaks */ 271 | execve(fname, (char * const*) argv, (char * const*) envp); 272 | _exit(1); 273 | } 274 | 275 | close(fds[1]); 276 | return fdopen(fds[0], "r+"); 277 | } 278 | 279 | pid_t get_docker_container_pid(const char *name) 280 | { 281 | const char *fname = DOCKER_PATH; 282 | const char *argp[] = { 283 | "docker", 284 | "inspect", 285 | "--format", 286 | "{{.State.Pid}}", 287 | name, 288 | NULL 289 | }; 290 | const char *envp[] = { 291 | "PATH=/bin:/usr/bin:/sbin:/usr/sbin", 292 | NULL 293 | }; 294 | pid_t pid; 295 | FILE *fp; 296 | 297 | if ((fp = execve_pipe(fname, argp, envp)) == NULL) 298 | return (pid_t) -1; 299 | 300 | errno = 0; 301 | if (fscanf(fp, "%d", &pid) != 1) { 302 | if (errno != 0) 303 | perror("get_docker_container_pid: fscanf"); 304 | else 305 | fprintf(stderr, "get_docker_container_pid: Could not find a PID for container '%s'\n", name); 306 | return (pid_t) -1; 307 | } 308 | 309 | fclose(fp); 310 | 311 | return pid; 312 | } 313 | 314 | int main(int argc, char **argv) 315 | { 316 | char *def_argp[] = { "/bin/bash", NULL }; 317 | int set_supplemental_groups = 0; 318 | const char *username = "root"; 319 | char **argp = def_argp; 320 | struct passwd *pw; 321 | int status; 322 | int is_tty; 323 | pid_t pid; 324 | 325 | if (argc < 2) { 326 | fprintf(stderr, "Usage: %s CONTAINER [USERNAME] [CMD...]\n", argv[0]); 327 | return 1; 328 | } 329 | 330 | if ((pid = get_docker_container_pid(argv[1])) == (pid_t) -1) 331 | return 2; 332 | 333 | if (argc > 2) 334 | username = argv[2]; 335 | 336 | if (argc > 3) 337 | argp = &argv[3]; 338 | 339 | is_tty = isatty(0); 340 | 341 | if (set_reasonably_secure_env(username) == -1) 342 | return 3; 343 | 344 | if ((pw = getpwnam(username)) == NULL) { 345 | fprintf(stderr, "No such user: %s\n", username); 346 | return 4; 347 | } 348 | 349 | /* 350 | * When docker adds support for CLONE_NEWUSER, that should be added as well. 351 | */ 352 | if (clone_namespace(pid, CLONE_NEWIPC) == -1 353 | || clone_namespace(pid, CLONE_NEWUTS) == -1 354 | || clone_namespace(pid, CLONE_NEWNET) == -1 355 | || clone_namespace(pid, CLONE_NEWPID) == -1 356 | || clone_namespace(pid, CLONE_NEWNS) == -1) 357 | return 5; 358 | 359 | if (setregid(pw->pw_gid, pw->pw_gid) == -1) { 360 | perror("setregid"); 361 | return 6; 362 | } 363 | 364 | if (set_supplemental_groups) { 365 | if (initgroups(argv[2], pw->pw_gid) == -1) { 366 | perror("initgroups"); 367 | return 7; 368 | } 369 | } else { 370 | if (setgroups(1, &pw->pw_gid) == -1) { 371 | perror("setgroups"); 372 | return 7; 373 | } 374 | } 375 | 376 | if (setreuid(pw->pw_uid, pw->pw_uid) == -1) { 377 | perror("setreuid"); 378 | return 8; 379 | } 380 | 381 | if (chdir(pw->pw_dir) == -1) { 382 | perror("chdir"); 383 | return 9; 384 | } 385 | 386 | if (is_tty) 387 | pid = forkpty(&pty_fd, NULL, NULL, NULL); 388 | else 389 | pid = fork(); 390 | 391 | if (pid == -1) { 392 | perror("fork"); 393 | return 10; 394 | } 395 | 396 | if (pid == 0) { 397 | close_files(); /* just in case, prevent fd leaks */ 398 | execvp(argp[0], argp); 399 | perror("execvp"); 400 | return -1; 401 | } 402 | 403 | if (is_tty) { 404 | struct sigaction sa; 405 | sigemptyset(&sa.sa_mask); 406 | sa.sa_flags = 0; 407 | sa.sa_handler = window_size_changed; 408 | if (sigaction(SIGWINCH, &sa, NULL) == -1) { 409 | perror("sigaction"); 410 | return 11; 411 | } 412 | clone_window_size(STDIN_FILENO, pty_fd); 413 | tty_raw(); 414 | proxy_fds(STDIN_FILENO, pty_fd); 415 | tty_restore(); 416 | } 417 | 418 | if (waitpid(pid, &status, 0) == -1) { 419 | perror("waitpid"); 420 | return 12; 421 | } 422 | 423 | return WEXITSTATUS(status); 424 | } 425 | -------------------------------------------------------------------------------- /docker-jailsh.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Joel Eriksson 2014 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "shared.h" 13 | 14 | #define MAX_ARGS 32768 15 | 16 | int main(int argc, char **argv) 17 | { 18 | char jailname[128]; 19 | char username[128]; 20 | char *argp[MAX_ARGS] = { 21 | "sudo", 22 | "docker-cmd", 23 | jailname, 24 | username, 25 | NULL 26 | }; 27 | struct passwd *pw; 28 | int n; 29 | 30 | if ((pw = getpwuid(getuid())) == NULL) { 31 | perror("getpwuid"); 32 | return 1; 33 | } 34 | 35 | if (set_reasonably_secure_env(pw->pw_name) == -1) 36 | return 2; 37 | 38 | n = snprintf(jailname, sizeof(jailname), "jail_%s", pw->pw_name); 39 | if (n < 0 || n >= sizeof(jailname)) { 40 | fprintf(stderr, "snprintf() failed\n"); 41 | return 3; 42 | } 43 | 44 | n = snprintf(username, sizeof(username), "%s", pw->pw_name); 45 | if (n < 0 || n >= sizeof(username)) { 46 | fprintf(stderr, "snprintf() failed\n"); 47 | return 4; 48 | } 49 | 50 | if (argc > 1) { 51 | int i = 4; 52 | argp[i++] = SHELL; 53 | while (--argc > 0 && i < (sizeof(argp)/sizeof(argp[0]))-1) 54 | argp[i++] = *++argv; 55 | argp[i] = NULL; 56 | } 57 | 58 | close_files(); /* just in case, prevent fd leaks */ 59 | execvp(argp[0], argp); 60 | perror("execvp"); 61 | return 5; 62 | } 63 | -------------------------------------------------------------------------------- /docker-mkjail: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Creates a jail-container for the user specified on the command line. Uses 4 | # the ubuntu image by default. Customize this to your needs. In particular, 5 | # the default base image you want to use for your jail, and the commands to 6 | # be executed in the jail after it has been created.. 7 | # 8 | # In my case, I remove the SUID/SGID-bit from all SUID and SGID-binaries, 9 | # since I want to minimize the risk of privilege escalation and breaking out 10 | # of the Docker container. Of course, with a kernel vulnerability, it would 11 | # still be game over... 12 | # 13 | # Copyright (C) Joel Eriksson 2014 14 | # 15 | 16 | if [ $# -lt 1 ]; then 17 | echo "Usage: $0 USER [HOSTNAME] [JAILNAME] [IMAGE] [SHELL]" >&2 18 | exit 1 19 | fi 20 | 21 | if [ $(id -u) -ne 0 ]; then 22 | echo "This command must be run as root." >&2 23 | exit 2 24 | fi 25 | 26 | # The name of the base jail container 27 | JAILNAME=jail 28 | 29 | REAPER=$(which docker-reaper) 30 | 31 | if [ "$REAPER" = "" ]; then 32 | echo "The docker-reaper binary was not found." >&2 33 | echo "Have you run 'sudo make install' yet?" >&2 34 | exit 3 35 | fi 36 | 37 | # The image to use when building the initial jail container 38 | JAIL_IMAGE=ubuntu:trusty 39 | 40 | # The hostname to use for the jail 41 | JAIL_HOSTNAME=jail 42 | 43 | # The shell to assign to the jailed user, within the jail 44 | JAIL_SHELL=/bin/bash 45 | 46 | if [ $# -ge 2 ]; then 47 | JAIL_HOSTNAME="$2" 48 | fi 49 | 50 | if [ $# -ge 3 ]; then 51 | JAIL_IMAGE="$3" 52 | fi 53 | 54 | if [ $# -ge 4 ]; then 55 | JAILNAME="$4" 56 | fi 57 | 58 | if [ $# -ge 5 ]; then 59 | JAIL_SHELL="$5" 60 | fi 61 | 62 | usr="$1" 63 | 64 | psw=$(grep "^$usr:" /etc/passwd) 65 | 66 | if [ "$psw" = "" ]; then 67 | echo "No such user: $usr" >&2 68 | exit 4 69 | fi 70 | 71 | uid=$(echo "$psw" | cut -d: -f3) 72 | gid=$(echo "$psw" | cut -d: -f4) 73 | dir=$(echo "$psw" | cut -d: -f6) 74 | grp=$(awk -F: "\$3 == $gid { print \$1 }" < /etc/group) 75 | 76 | if [ "$uid" = "0" ]; then 77 | echo "Setting up a jail for a user with root-privileges is meaningless. Don't." >&2 78 | exit 5 79 | fi 80 | 81 | if [ "$dir" = "/" ]; then 82 | echo "The home directory for the user you want to create a jail for is /." >&2 83 | echo "That would be a pretty bad idea..." >&2 84 | exit 6 85 | fi 86 | 87 | docker inspect "$JAILNAME" >/dev/null 2>&1 88 | 89 | if [ $? -ne 0 ]; then 90 | echo "Could not find jail container, creating..." >&2 91 | docker run --name="$JAILNAME" -h "$JAIL_HOSTNAME" -v "$REAPER:/sbin/reaper:ro" -d "$JAIL_IMAGE" /sbin/reaper 92 | docker-cmd "$JAILNAME" root apt-get -y update 93 | docker-cmd "$JAILNAME" root apt-get -y upgrade 94 | docker-cmd "$JAILNAME" root apt-get -y dist-update 95 | # For scp and sftp to work, these must be installed within the jail 96 | docker-cmd "$JAILNAME" root apt-get -y install openssh-client openssh-server 97 | # Install a bunch of other useful packages, customize these any way you like 98 | docker-cmd "$JAILNAME" root apt-get -y install python python-doc man-db manpages manpages-dev tmux 99 | docker-cmd "$JAILNAME" root apt-get -y install python-examples binutils binfmt-support cpp 100 | docker-cmd "$JAILNAME" root apt-get -y install tmux ctags vim vim-doc vim-scripts vim-nox 101 | # Disable all SUID/SGID binaries. Do this after all packages have been installed. 102 | docker-cmd "$JAILNAME" root sh -c "find / -type f '(' -perm -4000 -o -perm -2000 ')' -exec chmod ug-s {} \\; 2>/dev/null" 103 | docker stop -t 1 "$JAILNAME" >/dev/null 104 | docker commit "$JAILNAME" "$JAILNAME" 105 | fi 106 | 107 | docker inspect "${JAILNAME}_$usr" >/dev/null 2>&1 108 | 109 | if [ $? -eq 0 ]; then 110 | echo "Jail for user already exists, removing..." >&2 111 | docker stop -t 1 "${JAILNAME}_$usr" >/dev/null 112 | docker rm "${JAILNAME}_$usr" >/dev/null 113 | fi 114 | 115 | echo "Creating jail for user $usr..." >&2 116 | docker run --name="${JAILNAME}_${usr}" -h "$JAIL_HOSTNAME" -v "$REAPER:/sbin/reaper:ro" -v "$dir:$dir:rw" -d "$JAILNAME" >/dev/null 117 | 118 | if [ $? -ne 0 ]; then 119 | echo "Docker failed to create jail. :(" >&2 120 | exit 7 121 | fi 122 | 123 | docker-cmd "${JAILNAME}_${usr}" root groupadd -g "$gid" "$grp" 124 | docker-cmd "${JAILNAME}_${usr}" root useradd -s "$JAIL_SHELL" -u "$uid" -g "$gid" -d "$dir" "$usr" 125 | 126 | cat > /etc/sudoers.d/jail_$usr << EOF 127 | $usr ALL=(root) NOPASSWD: $(which docker-cmd) ${JAILNAME}_$usr $usr 128 | $usr ALL=(root) NOPASSWD: $(which docker-cmd) ${JAILNAME}_$usr $usr * 129 | EOF 130 | usermod -s $(which docker-jailsh) $usr 131 | 132 | exit 0 133 | -------------------------------------------------------------------------------- /docker-reaper.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Joel Eriksson 2014 3 | */ 4 | 5 | #define _POSIX_SOURCE 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | void on_sigchld(int sig) 14 | { 15 | int rc; 16 | 17 | do { 18 | if ((rc = waitpid(-1, 0, WNOHANG)) == -1 && errno != ECHILD) 19 | perror("waitpid"); 20 | } while (rc > 0); 21 | } 22 | 23 | int main(void) 24 | { 25 | struct sigaction sa; 26 | 27 | memset(&sa, 0, sizeof(sa)); 28 | sigemptyset(&sa.sa_mask); 29 | sa.sa_handler = on_sigchld; 30 | 31 | if (sigaction(SIGCHLD, &sa, NULL) == -1) { 32 | perror("sigaction"); 33 | return 1; 34 | } 35 | 36 | for (;;) 37 | pause(); 38 | 39 | /* will not be reached */ 40 | return 1; 41 | } 42 | -------------------------------------------------------------------------------- /docker-rmjail: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (C) Joel Eriksson 2014 4 | # 5 | 6 | if [ $# -lt 1 ]; then 7 | echo "Usage: $0 USER [JAILNAME]" >&2 8 | exit 1 9 | fi 10 | 11 | if [ $(id -u) -ne 0 ]; then 12 | echo "This command must be run as root." >&2 13 | exit 2 14 | fi 15 | 16 | JAILNAME=jail 17 | USERNAME="$1" 18 | 19 | if [ $# -ge 2 ]; then 20 | JAILNAME="$2" 21 | fi 22 | 23 | docker inspect "${JAILNAME}_${USERNAME}" >/dev/null 2>&1 24 | 25 | if [ $? -ne 0 ]; then 26 | echo "The ${JAILNAME}_${USERNAME} jail does not exist." >&2 27 | exit 3 28 | fi 29 | 30 | echo "Removing jail for ${USERNAME}..." >&2 31 | 32 | docker stop -t 1 ${JAILNAME}_${USERNAME} >/dev/null 33 | docker rm ${JAILNAME}_${USERNAME} >/dev/null 34 | rm -f /etc/sudoers.d/${JAILNAME}_${USERNAME} 35 | usermod -s "" ${USERNAME} 36 | 37 | exit 0 38 | -------------------------------------------------------------------------------- /shared.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Joel Eriksson 2014 3 | */ 4 | 5 | #define _GNU_SOURCE 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | int set_reasonably_secure_env(const char *username) 17 | { 18 | static char home_buf[PATH_MAX+6], term_buf[64], path_buf[128], *term_str; 19 | struct passwd *pw; 20 | int n; 21 | 22 | errno = 0; 23 | if ((pw = getpwnam(username)) == NULL) { 24 | if (errno != 0) 25 | perror("set_reasonably_secure_env: getpwnam"); 26 | else 27 | fprintf(stderr, "set_reasonably_secure_env: getpwnam: User not found\n"); 28 | _exit(1); /* rather not give the calling function a chance to mess this up */ 29 | } 30 | 31 | if (clearenv() == -1) { 32 | perror("set_reasonably_secure_env: clearenv"); 33 | _exit(1); /* rather not give the calling function a chance to mess this up */ 34 | } 35 | 36 | n = snprintf(home_buf, sizeof(home_buf), "HOME=%s", pw->pw_dir); 37 | if (n < 0 || n >= sizeof(home_buf)) { 38 | fprintf(stderr, "set_reasonably_secure_env: snprintf() failed\n"); 39 | _exit(1); /* rather not give the calling function a chance to mess this up */ 40 | } 41 | 42 | if (pw->pw_uid == 0) 43 | n = snprintf(path_buf, sizeof(path_buf), "PATH=/bin:/usr/bin:/sbin:/usr/sbin"); 44 | else 45 | n = snprintf(path_buf, sizeof(path_buf), "PATH=/bin:/usr/bin"); 46 | 47 | if (n < 0 || n >= sizeof(path_buf)) { 48 | fprintf(stderr, "set_reasonably_secure_env: snprintf() failed\n"); 49 | _exit(1); /* rather not give the calling function a chance to mess this up */ 50 | } 51 | 52 | if ((term_str = getenv("TERM")) == NULL) 53 | term_str = "xterm"; 54 | 55 | if (! strncmp(term_str, "() {", 4)) { 56 | fprintf(stderr, "set_reasonably_secure_env: Possible CVE-2014-6271 exploit attempt\n"); 57 | _exit(1); /* rather not give the calling function a chance to mess this up */ 58 | } 59 | 60 | snprintf(term_buf, sizeof(term_buf), "TERM=%s", term_str); 61 | 62 | if (n < 0 || n >= sizeof(term_buf)) { 63 | fprintf(stderr, "set_reasonably_secure_env: snprintf() failed\n"); 64 | _exit(1); /* rather not give the calling function a chance to mess this up */ 65 | } 66 | 67 | putenv(home_buf); 68 | putenv(path_buf); 69 | putenv(term_buf); 70 | 71 | /* 72 | * Might want to add a few more whitelisted environment variables, 73 | * such as LANG / LC_*. But I'd rather be safe than sorry, so until 74 | * configuration options / configuration files have been added to 75 | * specify this, we keep it strict. 76 | */ 77 | 78 | return 0; 79 | } 80 | 81 | int close_files() 82 | { 83 | struct rlimit rlim; 84 | rlim_t fd; 85 | 86 | if (getrlimit(RLIMIT_NOFILE, &rlim) == -1) { 87 | perror("close_files: getrlimit"); 88 | _exit(1); /* rather not give the calling function a chance to mess this up */ 89 | } 90 | 91 | for (fd = 3; fd < rlim.rlim_cur; fd++) 92 | if (close(fd) == -1 && errno != EBADF) { 93 | perror("close_files: close"); 94 | _exit(1); /* rather not give the calling function a chance to mess this up */ 95 | } 96 | 97 | return 0; 98 | } 99 | -------------------------------------------------------------------------------- /shared.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Joel Eriksson 2014 3 | */ 4 | 5 | #ifndef __DOCKER_CMD_SHARED_H 6 | #define __DOCKER_CMD_SHARED_H 7 | 8 | int set_reasonably_secure_env(const char *username); 9 | int close_files(); 10 | 11 | #endif /* ! __DOCKER_CMD_SHARED_H */ 12 | --------------------------------------------------------------------------------