Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions bin/docker-compose
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,50 @@ SHELLHUB_POSTGRES_EXTERNAL=$(env_var SHELLHUB_POSTGRES_EXTERNAL false)
SHELLHUB_BILLING=$(env_var SHELLHUB_BILLING "")
EXTRA_COMPOSE_FILE=${EXTRA_COMPOSE_FILE:-$(env_var EXTRA_COMPOSE_FILE "")}

sbin_path="$PATH:/usr/sbin:/sbin"

daemon_is_rootless() {
docker info --format '{{.SecurityOptions}}' 2>/dev/null | grep -q 'name=rootless'
}

daemon_socket() {
docker_host=${DOCKER_HOST:-$(docker context inspect --format '{{.Endpoints.docker.Host}}' 2>/dev/null || true)}
case "$docker_host" in
unix://*) echo "${docker_host#unix://}" ;;
esac
}

rootlesskit_capabilities() {
rootlesskit=$(command -v rootlesskit 2>/dev/null || true)
PATH="$sbin_path" getcap "$rootlesskit" 2>/dev/null
}

lowest_published_port() {
http_port=$(env_var SHELLHUB_HTTP_PORT 80)
ssh_port=$(env_var SHELLHUB_SSH_PORT 22)
ssh_port=${ssh_port##*:}
[ "$ssh_port" -lt "$http_port" ] && echo "$ssh_port" || echo "$http_port"
Comment thread
gustavosbarreto marked this conversation as resolved.
}

if [ "$SHELLHUB_ENV" = "development" ] && daemon_is_rootless; then
socket=$(daemon_socket)
[ -z "$socket" ] || export SHELLHUB_DEV_DOCKER_SOCKET="$socket"

port=$(lowest_published_port)
unprivileged_start=$(PATH="$sbin_path" sysctl -n net.ipv4.ip_unprivileged_port_start 2>/dev/null || echo 1024)
if [ "$port" -lt "$unprivileged_start" ]; then
capabilities=$(rootlesskit_capabilities) && getcap_status=0 || getcap_status=$?
if [ "$getcap_status" -eq 127 ]; then
echo "⚠️ WARNING: getcap not found, cannot tell whether rootless Docker may publish port $port." >&2
elif ! echo "$capabilities" | grep -q 'cap_net_bind_service'; then
echo "🚫 ERROR: rootless Docker cannot publish port $port. Either grant rootlesskit the capability:" >&2
echo " sudo setcap cap_net_bind_service=ep \"\$(command -v rootlesskit)\" && systemctl --user restart docker" >&2
echo " or set SHELLHUB_HTTP_PORT and SHELLHUB_SSH_PORT to $unprivileged_start or above in .env.override." >&2
exit 1
fi
fi
fi

# COMPOSE_ENV_FILES — last file wins. Order chosen so that:
# - CE defaults are the base
# - enterprise defaults augment when edition is enterprise or cloud
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ services:
- ./pkg:/go/src/github.com/shellhub-io/shellhub/pkg
- ./openapi:/go/src/github.com/shellhub-io/shellhub/openapi
- ./tests:/go/src/github.com/shellhub-io/shellhub/tests
- /var/run/docker.sock:/var/run/docker.sock
- ${SHELLHUB_DEV_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
- ./.golangci.yaml:/.golangci.yaml
- ./.mockery.yaml:/go/src/github.com/shellhub-io/shellhub/.mockery.yaml
- ./go.sum:/go/src/github.com/shellhub-io/shellhub/go.sum
- ./install.sh:/go/src/github.com/shellhub-io/shellhub/install.sh
environment:
- TESTCONTAINERS_DOCKER_NETWORK=${SHELLHUB_NETWORK}
- TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=${SHELLHUB_DEV_DOCKER_SOCKET:-/var/run/docker.sock}
- SHELLHUB_EDITION=${SHELLHUB_EDITION}
- SHELLHUB_ENV=${SHELLHUB_ENV}
ui:
Expand Down
35 changes: 27 additions & 8 deletions tests/compose/helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,42 @@

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"

unset EXTRA_COMPOSE_FILE
unset EXTRA_COMPOSE_FILE DOCKER_HOST SHELLHUB_DEV_DOCKER_SOCKET

# Run the wrapper with the given override variables written to a tmpfile,
# while replacing `docker` in PATH with a stub that just echoes the env
# the wrapper exported.
# Usage: capture_with VAR1=value VAR2=value ...
capture_with() {
local stub_dir="$BATS_TEST_TMPDIR/stub"
if [ ! -x "$stub_dir/docker" ]; then
mkdir -p "$stub_dir"
cat > "$stub_dir/docker" <<'EOF'
#!/bin/sh
echo "COMPOSE_FILE=$COMPOSE_FILE"
echo "COMPOSE_ENV_FILES=$COMPOSE_ENV_FILES"
echo "COMPOSE_PROFILES=$COMPOSE_PROFILES"
case "$1" in
context)
echo "${STUB_DOCKER_ENDPOINT:-unix:///var/run/docker.sock}"
;;
info)
[ -z "${STUB_DOCKER_INFO_FAILS:-}" ] || exit 1
echo "[${STUB_DOCKER_SECURITY:-name=seccomp,profile=builtin name=cgroupns}]"
;;
*)
echo "COMPOSE_FILE=$COMPOSE_FILE"
echo "COMPOSE_ENV_FILES=$COMPOSE_ENV_FILES"
echo "COMPOSE_PROFILES=$COMPOSE_PROFILES"
echo "SHELLHUB_DEV_DOCKER_SOCKET=$SHELLHUB_DEV_DOCKER_SOCKET"
;;
esac
EOF
chmod +x "$stub_dir/docker"
cat > "$stub_dir/getcap" <<'EOF'
#!/bin/sh
[ -z "${STUB_NO_GETCAP:-}" ] || exit 127
[ -z "${STUB_GETCAP:-}" ] || echo "$1 $STUB_GETCAP"
EOF
cat > "$stub_dir/sysctl" <<'EOF'
#!/bin/sh
echo "${STUB_SYSCTL_UNPRIVILEGED_PORT_START:-1024}"
EOF
: > "$stub_dir/rootlesskit"
chmod +x "$stub_dir"/*
fi

local tmp
Expand Down
100 changes: 100 additions & 0 deletions tests/compose/wrapper.bats
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,103 @@ load helpers
out=$(capture_with SHELLHUB_POSTGRES_EXTERNAL=false)
[[ "$out" == *"docker-compose.postgres.yml"* ]]
}

@test "docker socket: a rootful daemon leaves the variable unset" {
export STUB_DOCKER_ENDPOINT=unix:///home/dev/.docker/run/docker.sock
out=$(capture_with SHELLHUB_ENV=development)
grep -qx 'SHELLHUB_DEV_DOCKER_SOCKET=' <<< "$out"
}

@test "docker socket: a rootless daemon resolves to the context's socket" {
export STUB_DOCKER_SECURITY="name=rootless" STUB_DOCKER_ENDPOINT=unix:///run/user/1000/docker.sock
out=$(capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=8080 SHELLHUB_SSH_PORT=2222)
[[ "$out" == *"SHELLHUB_DEV_DOCKER_SOCKET=/run/user/1000/docker.sock"* ]]
}

@test "docker socket: DOCKER_HOST with a unix socket wins over the context" {
export STUB_DOCKER_SECURITY="name=rootless" STUB_DOCKER_ENDPOINT=unix:///run/user/1000/docker.sock
out=$(DOCKER_HOST=unix:///tmp/other.sock capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=8080 SHELLHUB_SSH_PORT=2222)
[[ "$out" == *"SHELLHUB_DEV_DOCKER_SOCKET=/tmp/other.sock"* ]]
}

@test "docker socket: DOCKER_HOST with tcp leaves the variable unset" {
export STUB_DOCKER_SECURITY="name=rootless"
out=$(DOCKER_HOST=tcp://10.0.0.1:2375 capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=8080 SHELLHUB_SSH_PORT=2222)
grep -qx 'SHELLHUB_DEV_DOCKER_SOCKET=' <<< "$out"
}

@test "docker socket: not resolved outside development" {
export STUB_DOCKER_SECURITY="name=rootless" STUB_DOCKER_ENDPOINT=unix:///run/user/1000/docker.sock
out=$(capture_with)
grep -qx 'SHELLHUB_DEV_DOCKER_SOCKET=' <<< "$out"
}

@test "rootless guard: a privileged port without the capability aborts and names setcap" {
export STUB_DOCKER_SECURITY="name=rootless name=seccomp,profile=builtin"
run capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=80
[ "$status" -ne 0 ]
[[ "$output" == *"setcap cap_net_bind_service=ep"* ]]
}

@test "rootless guard: the ssh port counts too" {
export STUB_DOCKER_SECURITY="name=rootless"
run capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=8080 SHELLHUB_SSH_PORT=22
[ "$status" -ne 0 ]
[[ "$output" == *"port 22"* ]]
}

@test "rootless guard: the ssh port keeps its bind address out of the comparison" {
export STUB_DOCKER_SECURITY="name=rootless"
run capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=8080 SHELLHUB_SSH_PORT=127.0.0.1:22
[ "$status" -ne 0 ]
[[ "$output" == *"port 22"* ]]
}

@test "rootless guard: a missing getcap warns and lets the stack start" {
export STUB_DOCKER_SECURITY="name=rootless" STUB_NO_GETCAP=1
out=$(capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=80)
[[ "$out" == *"getcap not found"* ]]
[[ "$out" == *"COMPOSE_FILE="* ]]
}

@test "rootless guard: the capability on rootlesskit lets a privileged port through" {
export STUB_DOCKER_SECURITY="name=rootless" STUB_GETCAP="cap_net_bind_service=ep"
out=$(capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=80)
[[ "$out" == *"COMPOSE_FILE="* ]]
}

@test "rootless guard: a sysctl that frees the port lets it through" {
export STUB_DOCKER_SECURITY="name=rootless" STUB_SYSCTL_UNPRIVILEGED_PORT_START=0
out=$(capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=80 SHELLHUB_SSH_PORT=22)
[[ "$out" == *"COMPOSE_FILE="* ]]
}

@test "rootless guard: a sysctl above 1024 makes a higher port privileged too" {
export STUB_DOCKER_SECURITY="name=rootless" STUB_SYSCTL_UNPRIVILEGED_PORT_START=2048
run capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=1500 SHELLHUB_SSH_PORT=2222
[ "$status" -ne 0 ]
[[ "$output" == *"port 1500"* ]]
}

@test "rootless guard: unprivileged ports pass" {
export STUB_DOCKER_SECURITY="name=rootless"
out=$(capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=8080 SHELLHUB_SSH_PORT=2222)
[[ "$out" == *"COMPOSE_FILE="* ]]
}

@test "rootless guard: a rootful daemon ignores privileged ports" {
out=$(capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=80)
[[ "$out" == *"COMPOSE_FILE="* ]]
}

@test "rootless guard: only in development" {
export STUB_DOCKER_SECURITY="name=rootless"
out=$(capture_with SHELLHUB_HTTP_PORT=80)
[[ "$out" == *"COMPOSE_FILE="* ]]
}

@test "rootless guard: a failing docker info skips the guard" {
export STUB_DOCKER_INFO_FAILS=1
out=$(capture_with SHELLHUB_ENV=development SHELLHUB_HTTP_PORT=80)
[[ "$out" == *"COMPOSE_FILE="* ]]
}
Loading