security: run in rootless by default and drop unnecessary caps (#101)

Co-authored-by: yusing <yusing@6uo.me>
This commit is contained in:
Yuzerion 2025-04-29 08:42:30 +08:00 committed by GitHub
parent 1d22bcfed9
commit 33e400a17e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 209 additions and 98 deletions

View file

@ -4,6 +4,10 @@ TAG=latest
# set timezone to get correct log timestamp
TZ=ETC/UTC
# container uid and gid (must match the owner of mounted directories)
GODOXY_UID=1000
GODOXY_GID=1000
# API JWT Configuration (common)
# generate secret with `openssl rand -base64 32`
GODOXY_API_JWT_SECRET=
@ -44,9 +48,19 @@ GODOXY_API_PASSWORD=password
GODOXY_HTTP_ADDR=:80
GODOXY_HTTPS_ADDR=:443
# Enable HTTP3
GODOXY_HTTP3_ENABLED=true
# API listening address
GODOXY_API_ADDR=127.0.0.1:8888
# Metrics
GODOXY_METRICS_DISABLE_CPU=false
GODOXY_METRICS_DISABLE_MEMORY=false
GODOXY_METRICS_DISABLE_DISK=false
GODOXY_METRICS_DISABLE_NETWORK=false
GODOXY_METRICS_DISABLE_SENSORS=false
# Frontend listening port
GODOXY_FRONTEND_PORT=3000
@ -56,6 +70,7 @@ GODOXY_FRONTEND_ALIASES=godoxy
# Docker socket
# /var/run/podman/podman.sock for podman
DOCKER_SOCKET=/var/run/docker.sock
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
# Debug mode
GODOXY_DEBUG=false

View file

@ -6,6 +6,16 @@ HEALTHCHECK NONE
# trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap
ENV GOPATH=/root/go
WORKDIR /src
COPY go.mod go.sum ./
COPY agent ./agent
COPY internal/dnsproviders ./internal/dnsproviders
RUN go mod download -x
# Stage 2: builder
FROM deps AS builder
@ -17,12 +27,6 @@ COPY internal ./internal
COPY pkg ./pkg
COPY agent ./agent
# Only copy go.mod and go.sum initially for better caching
COPY go.mod go.sum /src/
ENV GOPATH=/root/go
RUN go mod download -x
ARG VERSION
ENV VERSION=${VERSION}
@ -31,9 +35,8 @@ ENV MAKE_ARGS=${MAKE_ARGS}
ENV GOCACHE=/root/.cache/go-build
ENV GOPATH=/root/go
RUN make ${MAKE_ARGS} build link-binary && \
mv bin /app/ && \
mkdir -p /app/error_pages /app/certs
RUN make ${MAKE_ARGS} docker=1 build
# Stage 3: Final image
FROM scratch
@ -45,10 +48,7 @@ LABEL proxy.exclude=1
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# copy binary
COPY --from=builder /app /app
# copy example config
COPY config.example.yml /app/config/config.yml
COPY --from=builder /app/run /app/run
# copy certs
COPY --from=builder /etc/ssl/certs /etc/ssl/certs

View file

@ -1,3 +1,4 @@
shell := /bin/sh
export VERSION ?= $(shell git describe --tags --abbrev=0)
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
export GOOS = linux
@ -59,20 +60,29 @@ else
SETCAP_CMD = sudo setcap
endif
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
ifeq ($(docker), 1)
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
endif
.PHONY: debug
test:
GODOXY_TEST=1 go test ./internal/...
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
get:
for dir in ${PWD} ${PWD}/agent; do cd $$dir && go get -u ./... && go mod tidy; done
build:
mkdir -p bin
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ${CMD_PATH}
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
$(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH}
${POST_BUILD}
run:
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
@ -82,7 +92,7 @@ debug:
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
mtrace:
bin/godoxy debug-ls-mtrace > mtrace.json
${BIN_PATH} debug-ls-mtrace > mtrace.json
rapid-crash:
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
@ -99,8 +109,5 @@ ci-test:
cloc:
cloc --not-match-f '_test.go$$' cmd internal pkg
link-binary:
ln -s /app/${NAME} bin/run
push-github:
git push origin $(shell git rev-parse --abbrev-ref HEAD)

View file

@ -1,17 +1,47 @@
---
services:
socket-proxy:
container_name: socket-proxy
image: lscr.io/linuxserver/socket-proxy:latest
environment:
- ALLOW_START=1
- ALLOW_STOP=1
- ALLOW_RESTARTS=1
- CONTAINERS=1
- EVENTS=1
- PING=1
- POST=1
- VERSION=1
volumes:
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
restart: unless-stopped
tmpfs:
- /run
ports:
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:2375"]
interval: 1m30s
timeout: 30s
retries: 5
start_period: 30s
frontend:
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
container_name: godoxy-frontend
restart: unless-stopped
network_mode: host # do not change this
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- all
depends_on:
- app
environment:
HOSTNAME: 127.0.0.1
PORT: ${GODOXY_FRONTEND_PORT:-3000}
# modify below to fit your needs
labels:
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
@ -29,11 +59,19 @@ services:
restart: always
network_mode: host # do not change this
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
security_opt:
- no-new-privileges:true
cap_drop:
- all
cap_add:
- NET_BIND_SERVICE
environment:
- DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}
volumes:
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
- ./config:/app/config
- ./logs:/app/logs
- ./error_pages:/app/error_pages
- ./error_pages:/app/error_pages:ro
- ./data:/app/data
# To use autocert, certs will be stored in "./certs".

View file

@ -2,6 +2,33 @@
set -e # Exit on error
check_cmd() {
not_available=()
for cmd in "$@"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
not_available+=("$cmd")
fi
done
if [ "${#not_available[@]}" -gt 0 ]; then
echo "Error: ${not_available[*]} unavailable, please install it first"
exit 1
fi
}
check_cmd openssl docker
# quit if running user is root
if [ "$EUID" -eq 0 ]; then
echo "Error: Please do not run this script as root"
exit 1
fi
# check if user has docker permission
if [ -z "$(docker ps >/dev/null 2>&1)" ]; then
echo "Error: User $USER does not have permission to run docker, please add it to docker group"
exit 1
fi
# Detect download tool
if command -v curl >/dev/null 2>&1; then
DOWNLOAD_TOOL="curl"
@ -10,13 +37,8 @@ elif command -v wget >/dev/null 2>&1; then
DOWNLOAD_TOOL="wget"
DOWNLOAD_CMD="wget -qO"
else
read -p "Neither curl nor wget is installed, install curl? (y/n): " INSTALL
if [ "$INSTALL" == "y" ]; then
install_pkg "curl"
else
echo "Error: Neither curl nor wget is installed. Please install one of them and try again."
exit 1
fi
echo "Error: Neither curl nor wget is installed. Please install one of them and try again."
exit 1
fi
echo "Using ${DOWNLOAD_TOOL} for downloads"
@ -36,43 +58,12 @@ COMPOSE_FILE_NAME="compose.yml"
COMPOSE_EXAMPLE_FILE_NAME="compose.example.yml"
CONFIG_FILE_NAME="config.yml"
CONFIG_EXAMPLE_FILE_NAME="config.example.yml"
CONFIG_FILE_PATH="${CONFIG_BASE_PATH}/${CONFIG_FILE_NAME}"
REQUIRED_DIRECTORIES=("config" "logs" "error_pages" "data" "certs")
echo "Setting up GoDoxy"
echo "Branch: ${BRANCH}"
install_pkg() {
# detect package manager
if command -v apt >/dev/null 2>&1; then
apt install -y "$1"
elif command -v yum >/dev/null 2>&1; then
yum install -y "$1"
elif command -v pacman >/dev/null 2>&1; then
pacman -S --noconfirm "$1"
else
echo "Error: No supported package manager found"
exit 1
fi
}
check_pkg() {
local cmd="$1"
local pkg="$2"
if ! command -v "$cmd" >/dev/null 2>&1; then
# check if user is root
if [ "$EUID" -ne 0 ]; then
echo "Error: $pkg is not installed and you are not running as root. Please install it and try again."
exit 1
fi
read -p "$pkg is not installed, install it? (y/n): " INSTALL
if [ "$INSTALL" == "y" ]; then
install_pkg "$pkg"
else
echo "Error: $pkg is not installed. Please install it and try again."
exit 1
fi
fi
}
# Function to check if file/directory exists
has_file_or_dir() {
[ -e "$1" ]
@ -133,6 +124,32 @@ ask_while_empty() {
eval "$var_name=\"$value\""
}
ask_multiple_choice() {
local var_name="$1"
local prompt="$2"
shift 2
local choices=("$@")
local n_choices="${#choices[@]}"
local value=""
local valid=0
while [ $valid -eq 0 ]; do
echo -e "$prompt"
for i in "${!choices[@]}"; do
echo "$((i + 1)). ${choices[$i]}"
done
read -p "Enter your choice: " value
if [ -z "$value" ]; then
echo "Error: $var_name cannot be empty, please try again"
fi
if [ "$value" -gt "$n_choices" ] || [ "$value" -lt 1 ]; then
echo "Error: invalid choice, please try again"
else
valid=1
fi
done
eval "$var_name=\"${choices[$((value - 1))]}\""
}
get_timezone() {
if [ -f /etc/timezone ]; then
TIMEZONE=$(cat /etc/timezone)
@ -142,38 +159,45 @@ get_timezone() {
elif command -v timedatectl >/dev/null 2>&1; then
TIMEZONE=$(timedatectl status | grep "Time zone" | awk '{print $3}')
if [ -n "$TIMEZONE" ]; then
echo "$TIMEZONE"
echo "Detected timezone: $TIMEZONE"
fi
else
echo "Warning: could not detect timezone, you may set it manually in ${DOT_ENV_PATH} to have correct time in logs"
fi
}
check_pkg "openssl" "openssl"
check_pkg "docker" "docker-ce"
setenv() {
local key="$1"
local value="$2"
# uncomment line if it is commented
sed -i "/^# *${key}=/s/^# *//" "$DOT_ENV_PATH"
sed -i "s|${key}=.*|${key}=\"${value}\"|" "$DOT_ENV_PATH"
echo "${key}=${value}"
}
# Setup required configurations
# 1. Config base directory
mkdir_if_not_exists "$CONFIG_BASE_PATH"
# 1. Setup required directories
for dir in "${REQUIRED_DIRECTORIES[@]}"; do
mkdir_if_not_exists "$dir"
done
# 2. .env file
fetch_file "$DOT_ENV_EXAMPLE_PATH" "$DOT_ENV_PATH"
# set random JWT secret
JWT_SECRET=$(openssl rand -base64 32)
sed -i "s|GODOXY_API_JWT_SECRET=.*|GODOXY_API_JWT_SECRET=${JWT_SECRET}|" "$DOT_ENV_PATH"
setenv "GODOXY_API_JWT_SECRET" "$(openssl rand -base64 32)"
# set timezone
get_timezone
if [ -n "$TIMEZONE" ]; then
sed -i "s|TZ=.*|TZ=${TIMEZONE}|" "$DOT_ENV_PATH"
setenv "TZ" "$TIMEZONE"
fi
# 3. docker-compose.yml
fetch_file "$COMPOSE_EXAMPLE_FILE_NAME" "$COMPOSE_FILE_NAME"
# 4. config.yml
fetch_file "$CONFIG_EXAMPLE_FILE_NAME" "${CONFIG_BASE_PATH}/${CONFIG_FILE_NAME}"
fetch_file "$CONFIG_EXAMPLE_FILE_NAME" "$CONFIG_FILE_PATH"
# 5. setup authentication
@ -182,44 +206,71 @@ echo "Setting up login user"
ask_while_empty "Enter login username: " LOGIN_USERNAME
ask_while_empty "Enter login password: " LOGIN_PASSWORD
echo "Setting up login user \"$LOGIN_USERNAME\" with password \"$LOGIN_PASSWORD\""
sed -i "s|GODOXY_API_USERNAME=.*|GODOXY_API_USERNAME=${LOGIN_USERNAME}|" "$DOT_ENV_PATH"
sed -i "s|GODOXY_API_PASSWORD=.*|GODOXY_API_PASSWORD=${LOGIN_PASSWORD}|" "$DOT_ENV_PATH"
setenv "GODOXY_API_USERNAME" "$LOGIN_USERNAME"
setenv "GODOXY_API_PASSWORD" "$LOGIN_PASSWORD"
# 6. setup autocert
# ask if want to enable autocert
echo "Setting up autocert for SSL certificate"
ask_while_empty "Do you want to enable autocert? (y/n): " ENABLE_AUTOCERT
# quit if not using autocert
if [ "$ENABLE_AUTOCERT" == "y" ]; then
# ask for domain
echo "Setting up autocert"
ask_while_empty "Enter domain (e.g. example.com): " DOMAIN
skip=false
# ask for email
ask_while_empty "Enter email for Let's Encrypt: " EMAIL
# ask if using cloudflare
ask_while_empty "Is cloudflare the current DNS nameserver? (y/n): " USE_CLOUDFLARE
# select dns provider
ask_multiple_choice DNS_PROVIDER "Select DNS provider:" \
"Cloudflare" \
"CloudDNS" \
"DuckDNS" \
"Other"
# ask for cloudflare api key
if [ "$USE_CLOUDFLARE" = "y" ]; then
ask_while_empty "Enter cloudflare zone api key: " CLOUDFLARE_API_KEY
cat <<EOF >>"$CONFIG_BASE_PATH/$CONFIG_FILE_NAME"
autocert:
provider: cloudflare
email: $EMAIL
domains:
- "*.${DOMAIN}"
- "${DOMAIN}"
options:
auth_token: "$CLOUDFLARE_API_KEY"
EOF
# ask for dns provider credentials
if [ "$DNS_PROVIDER" == "Cloudflare" ]; then
provider="cloudflare"
read -p "Enter cloudflare zone api key: " auth_token
options=("auth_token: \"$auth_token\"")
elif [ "$DNS_PROVIDER" == "CloudDNS" ]; then
provider="clouddns"
read -p "Enter clouddns client_id: " client_id
read -p "Enter clouddns email: " email
read -p "Enter clouddns password: " password
options=(
"client_id: \"$client_id\""
"email: \"$email\""
"password: \"$password\""
)
elif [ "$DNS_PROVIDER" == "DuckDNS" ]; then
provider="duckdns"
read -p "Enter duckdns token: " token
options=("token: \"$token\"")
else
echo "Not using cloudflare, skipping autocert setup"
echo "Please refer to ${WIKI_URL}/Supported-DNS-01-Providers for more information"
echo "Please check Wiki for other DNS providers: ${WIKI_URL}/Supported-DNS%E2%80%9001-Providers"
echo "Skipping autocert setup"
skip=true
fi
if [ "$skip" == false ]; then
autocert_config="
autocert:
provider: \"${provider}\"
email: \"${EMAIL}\"
domains:
- \"*.${BASE_DOMAIN}\"
- \"${BASE_DOMAIN}\"
options:
"
for option in "${options[@]}"; do
autocert_config+=" ${option}\n"
done
autocert_config+="\n"
echo -e "${autocert_config}$(<"$CONFIG_FILE_PATH")" >"$CONFIG_FILE_PATH"
fi
fi
# 7. set uid and gid
setenv "GODOXY_UID" "$(id -u)"
setenv "GODOXY_GID" "$(id -g)"
echo "Setup finished"