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
5 changes: 5 additions & 0 deletions .github/workflows/container-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ jobs:
run: ./test-saml "$WEBLATE_TEST"
env:
WEBLATE_TEST: ${{ inputs.test }}
- name: Verify Django secret permissions
working-directory: docker-compose
run: ../tests/test-runtime-secrets "$WEBLATE_TEST"
env:
WEBLATE_TEST: ${{ inputs.test }}
- name: Test commands
working-directory: docker-compose
run: ./test-commands
Expand Down
75 changes: 65 additions & 10 deletions start
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ runserver_has_web_service() {
[[ -z $weblate_service || $weblate_service == "web" ]]
}

service_runs_migrations() {
local weblate_service=${WEBLATE_SERVICE:-}
[[ -z $weblate_service || $weblate_service == "celery-beat" ]]
}

EARLY_NGINX_PID_FILE=/run/nginx-early.pid
EARLY_NGINX_CONF_FILE=/tmp/nginx/nginx-early.conf

Expand Down Expand Up @@ -119,11 +124,56 @@ prepare_ssh_keys() {
}

ensure_django_secret() {
if [[ ! -s /app/data/secret ]]; then
echo "Generating Django secret..."
# https://github.com/django/django/blob/1.10.2/django/utils/crypto.py#L54-L56
/app/venv/bin/python -c "from django.utils.crypto import get_random_string; print(get_random_string(50))" > /app/data/secret
fi
local secret_file=/app/data/secret
local secret_tmp

(
umask 077
exec 9> /app/data/.django-secret.lock
flock -x 9

if [[ ! -s $secret_file ]]; then
echo "Generating Django secret..."
secret_tmp=$(mktemp /app/data/.django-secret.XXXXXX)
trap 'rm -f "$secret_tmp"' EXIT
# https://github.com/django/django/blob/1.10.2/django/utils/crypto.py#L54-L56
/app/venv/bin/python -c "from django.utils.crypto import get_random_string; print(get_random_string(50))" > "$secret_tmp"
if [[ ! -s $secret_tmp ]]; then
echo "Failed to generate a non-empty Django secret." >&2
exit 1
fi
chmod 600 "$secret_tmp"
mv -f -- "$secret_tmp" "$secret_file"
secret_tmp=
trap - EXIT
fi

# Repair secrets created by older container versions or copied into the volume.
chmod 600 "$secret_file"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Repair readable secrets without requiring file ownership

When an existing nonempty secret is readable but owned by root or by a previous arbitrary runtime UID, the data directory can still be writable while this chmod is forbidden. Under set -e, upgrading the container then aborts startup even though the previous entrypoint could use the secret; this also contradicts the stated goal of repairing secrets copied into the volume. Preserve the value by rewriting it through an owned temporary file and atomic rename rather than requiring ownership of the existing inode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be owned by correct user, there is runtime check for that in Weblate AFAIK.

)
}

django_secret_ready() {
[[ -s /app/data/secret && -r /app/data/secret ]] || return 1
[[ $(stat -c %a /app/data/secret 2> /dev/null) == "600" ]]
}

wait_for_django_secret() {
local max_retries=30
local timeout=0

until django_secret_ready; do
if ((timeout >= max_retries)); then
echo "Django secret was not created by the migration service within $max_retries seconds." >&2
echo "Ensure exactly one container runs migrations and that /app/data is shared with the same uid." >&2
exit 1
fi
if ((timeout == 0)); then
echo "Waiting for the migration service to create the Django secret..."
fi
sleep 1
((timeout += 1))
done
}

ensure_saml_certificate() {
Expand All @@ -146,7 +196,11 @@ ensure_saml_certificate() {
prepare_runtime_files() {
prepare_ssh_keys
ensure_data_volume_writable
ensure_django_secret
if service_runs_migrations; then
ensure_django_secret
else
wait_for_django_secret
fi
ensure_saml_certificate
}

Expand Down Expand Up @@ -333,7 +387,11 @@ check_postgres_version() {
}

prepare_supervisor_services() {
DO_MIGRATE=1
if service_runs_migrations; then
DO_MIGRATE=1
else
DO_MIGRATE=0
fi
SUPERVISOR_CONF=/run/supervisor.conf.d/

# Select which services to run.
Expand All @@ -342,9 +400,6 @@ prepare_supervisor_services() {
rm -f "$SUPERVISOR_CONF"/*

if [[ -n $WEBLATE_SERVICE ]]; then
if [[ $WEBLATE_SERVICE != "celery-beat" ]]; then
DO_MIGRATE=0
fi
ln -s "/etc/supervisor/conf.d/$WEBLATE_SERVICE.conf" "$SUPERVISOR_CONF"
return 0
fi
Expand Down
42 changes: 42 additions & 0 deletions tests/test-runtime-secrets
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/sh
set -eu

fail() {
echo "$1" >&2
exit 1
}

secret_mode() {
docker compose exec -T weblate stat -c %a /app/data/secret
}

secret_checksum() {
docker compose exec -T weblate sha256sum /app/data/secret
}

MODE=$(secret_mode)
[ "$MODE" = "600" ] || fail "Django secret has mode $MODE instead of 600"

CHECKSUM=$(secret_checksum)
docker compose exec -T weblate chmod 0644 /app/data/secret

if [ "$1" = "split" ]; then
MIGRATION_SERVICE=weblate-celery-beat
else
MIGRATION_SERVICE=weblate
fi

# Run the entrypoint as the migration owner to exercise legacy permission repair.
docker compose run --rm --no-deps "$MIGRATION_SERVICE" list_versions > /dev/null

MODE=$(secret_mode)
[ "$MODE" = "600" ] || fail "Django secret mode was not repaired: $MODE"
[ "$(secret_checksum)" = "$CHECKSUM" ] || fail "Django secret changed while repairing permissions"

if [ "$1" = "split" ]; then
GENERATIONS=$(docker compose logs --no-color 2>&1 | grep -c "Generating Django secret..." || true)
[ "$GENERATIONS" -eq 1 ] || fail "Expected one Django secret generation, found $GENERATIONS"
elif [ "$1" = "saml" ]; then
MODE=$(docker compose exec -T weblate stat -c %a /app/data/ssl/saml.key)
[ "$MODE" = "600" ] || fail "SAML private key has mode $MODE instead of 600"
fi
Loading