mirror of
https://github.com/sligter/LandPPT.git
synced 2026-08-28 15:10:58 +08:00
fix(docker): migrate volumes before non-root startup
This commit is contained in:
+5
-3
@@ -169,12 +169,14 @@ WORKDIR /app
|
||||
COPY run.py ./
|
||||
COPY src/ ./src/
|
||||
COPY template_examples/ ./template_examples/
|
||||
COPY docker-healthcheck.sh docker-entrypoint.sh /usr/local/bin/
|
||||
COPY docker-healthcheck.sh docker-entrypoint.sh docker-permissions-init.sh /usr/local/bin/
|
||||
COPY .env.example ./.env
|
||||
|
||||
# Create directories and set permissions in one layer
|
||||
RUN sed -i 's/\r$//' /usr/local/bin/docker-healthcheck.sh /usr/local/bin/docker-entrypoint.sh && \
|
||||
chmod +x /usr/local/bin/docker-healthcheck.sh /usr/local/bin/docker-entrypoint.sh && \
|
||||
RUN sed -i 's/\r$//' /usr/local/bin/docker-healthcheck.sh /usr/local/bin/docker-entrypoint.sh /usr/local/bin/docker-permissions-init.sh && \
|
||||
chmod +x /usr/local/bin/docker-healthcheck.sh \
|
||||
/usr/local/bin/docker-entrypoint.sh \
|
||||
/usr/local/bin/docker-permissions-init.sh && \
|
||||
mkdir -p temp/ai_responses_cache temp/style_genes_cache temp/summeryanyfile_cache temp/templates_cache \
|
||||
research_reports lib/Linux lib/MacOS lib/Windows uploads data && \
|
||||
chown -R landppt:landppt /app /home/landppt && \
|
||||
|
||||
@@ -7,6 +7,7 @@ x-landppt-build: &landppt-build
|
||||
|
||||
x-landppt-volumes: &landppt-volumes
|
||||
- ./:/app
|
||||
- ${LANDPPT_ENV_FILE:-./.env}:/app/.env
|
||||
- landppt_data:/app/data
|
||||
- landppt_uploads:/app/uploads
|
||||
- landppt_reports:/app/research_reports
|
||||
@@ -51,6 +52,8 @@ x-landppt-env: &landppt-env
|
||||
S3_PRESIGNED_URL_EXPIRES_SECONDS: ${S3_PRESIGNED_URL_EXPIRES_SECONDS:-3600}
|
||||
|
||||
x-landppt-depends-on: &landppt-depends-on
|
||||
permissions-init:
|
||||
condition: service_completed_successfully
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
@@ -61,6 +64,37 @@ x-landppt-depends-on: &landppt-depends-on
|
||||
condition: service_completed_successfully
|
||||
|
||||
services:
|
||||
permissions-init:
|
||||
build: *landppt-build
|
||||
image: ${LANDPPT_DEV_IMAGE:-landppt-dev:latest}
|
||||
user: "0:0"
|
||||
entrypoint: ["/usr/local/bin/docker-permissions-init.sh"]
|
||||
environment:
|
||||
LANDPPT_UID: "10001"
|
||||
LANDPPT_GID: "10001"
|
||||
volumes:
|
||||
- ${LANDPPT_ENV_FILE:-./.env}:/mnt/landppt/env/.env
|
||||
- landppt_data:/mnt/landppt/data
|
||||
- landppt_uploads:/mnt/landppt/uploads
|
||||
- landppt_reports:/mnt/landppt/reports
|
||||
- landppt_cache:/mnt/landppt/cache
|
||||
- landppt_lib:/mnt/landppt/lib
|
||||
network_mode: "none"
|
||||
read_only: true
|
||||
restart: "no"
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- FOWNER
|
||||
- DAC_OVERRIDE
|
||||
- SETUID
|
||||
- SETGID
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
landppt:
|
||||
build: *landppt-build
|
||||
image: ${LANDPPT_DEV_IMAGE:-landppt-dev:latest}
|
||||
|
||||
+33
-1
@@ -1,5 +1,5 @@
|
||||
x-landppt-volumes: &landppt-volumes
|
||||
- ./.env:/app/.env
|
||||
- ${LANDPPT_ENV_FILE:-./.env}:/app/.env
|
||||
- landppt_data:/app/data
|
||||
- landppt_uploads:/app/uploads
|
||||
- landppt_reports:/app/research_reports
|
||||
@@ -44,6 +44,8 @@ x-landppt-env: &landppt-env
|
||||
S3_PRESIGNED_URL_EXPIRES_SECONDS: ${S3_PRESIGNED_URL_EXPIRES_SECONDS:-3600}
|
||||
|
||||
x-landppt-depends-on: &landppt-depends-on
|
||||
permissions-init:
|
||||
condition: service_completed_successfully
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
@@ -54,6 +56,36 @@ x-landppt-depends-on: &landppt-depends-on
|
||||
condition: service_completed_successfully
|
||||
|
||||
services:
|
||||
permissions-init:
|
||||
image: ${LANDPPT_IMAGE:-bradleylzh/landppt:latest}
|
||||
user: "0:0"
|
||||
entrypoint: ["/usr/local/bin/docker-permissions-init.sh"]
|
||||
environment:
|
||||
LANDPPT_UID: "10001"
|
||||
LANDPPT_GID: "10001"
|
||||
volumes:
|
||||
- ${LANDPPT_ENV_FILE:-./.env}:/mnt/landppt/env/.env
|
||||
- landppt_data:/mnt/landppt/data
|
||||
- landppt_uploads:/mnt/landppt/uploads
|
||||
- landppt_reports:/mnt/landppt/reports
|
||||
- landppt_cache:/mnt/landppt/cache
|
||||
- landppt_lib:/mnt/landppt/lib
|
||||
network_mode: "none"
|
||||
read_only: true
|
||||
restart: "no"
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- FOWNER
|
||||
- DAC_OVERRIDE
|
||||
- SETUID
|
||||
- SETGID
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
landppt:
|
||||
image: ${LANDPPT_IMAGE:-bradleylzh/landppt:latest}
|
||||
shm_size: ${LANDPPT_SHM_SIZE:-4gb}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
TARGET_UID="${LANDPPT_UID:-10001}"
|
||||
TARGET_GID="${LANDPPT_GID:-10001}"
|
||||
MOUNT_ROOT="/mnt/landppt"
|
||||
MARKER_NAME=".landppt-permissions-v1"
|
||||
|
||||
log() {
|
||||
printf '[permissions-init] %s\n' "$1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[permissions-init] WARNING: %s\n' "$1" >&2
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[permissions-init] ERROR: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
validate_access() {
|
||||
target_path="$1"
|
||||
target_kind="$2"
|
||||
|
||||
if ! /opt/venv/bin/python - "$TARGET_UID" "$TARGET_GID" "$target_path" "$target_kind" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
uid = int(sys.argv[1])
|
||||
gid = int(sys.argv[2])
|
||||
path = sys.argv[3]
|
||||
kind = sys.argv[4]
|
||||
|
||||
os.setgroups([])
|
||||
os.setgid(gid)
|
||||
os.setuid(uid)
|
||||
|
||||
if kind == "file":
|
||||
descriptor = os.open(path, os.O_WRONLY | os.O_APPEND)
|
||||
os.close(descriptor)
|
||||
else:
|
||||
probe = os.path.join(path, f".landppt-write-test-{os.getpid()}")
|
||||
descriptor = os.open(probe, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
os.close(descriptor)
|
||||
os.unlink(probe)
|
||||
PY
|
||||
then
|
||||
fail "UID ${TARGET_UID} cannot write ${target_path}"
|
||||
fi
|
||||
}
|
||||
|
||||
migrate_volume() {
|
||||
volume_path="$1"
|
||||
marker_path="${volume_path}/${MARKER_NAME}"
|
||||
|
||||
[ -d "$volume_path" ] || fail "Volume path is missing: ${volume_path}"
|
||||
|
||||
if [ ! -f "$marker_path" ]; then
|
||||
log "Migrating ${volume_path} to ${TARGET_UID}:${TARGET_GID}"
|
||||
chown -R "${TARGET_UID}:${TARGET_GID}" "$volume_path"
|
||||
chmod -R u+rwX "$volume_path"
|
||||
validate_access "$volume_path" directory
|
||||
: > "$marker_path"
|
||||
chown "${TARGET_UID}:${TARGET_GID}" "$marker_path"
|
||||
chmod 600 "$marker_path"
|
||||
else
|
||||
log "Migration marker found for ${volume_path}; validating"
|
||||
validate_access "$volume_path" directory
|
||||
fi
|
||||
}
|
||||
|
||||
configure_env_file() {
|
||||
env_path="${MOUNT_ROOT}/env/.env"
|
||||
|
||||
[ -e "$env_path" ] || fail "Mounted .env is missing: ${env_path}"
|
||||
[ -f "$env_path" ] || fail "Mounted .env is not a regular file: ${env_path}"
|
||||
|
||||
if ! chgrp "$TARGET_GID" "$env_path" 2>/dev/null; then
|
||||
warn "Could not change .env group; checking effective access"
|
||||
fi
|
||||
if ! chmod g+rw,o-rwx "$env_path" 2>/dev/null; then
|
||||
warn "Could not change .env mode; checking effective access"
|
||||
fi
|
||||
|
||||
validate_access "$env_path" file
|
||||
}
|
||||
|
||||
main() {
|
||||
[ "$(id -u)" -eq 0 ] || fail "Permission migration must run as root"
|
||||
|
||||
configure_env_file
|
||||
for volume_name in data uploads reports cache lib; do
|
||||
migrate_volume "${MOUNT_ROOT}/${volume_name}"
|
||||
done
|
||||
|
||||
log "Permission migration complete"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -42,3 +42,61 @@ def test_entrypoint_checks_identity_and_never_repairs_permissions():
|
||||
assert "chmod " not in entrypoint
|
||||
assert "chown " not in entrypoint
|
||||
assert 'cp "/app/.env"' not in entrypoint
|
||||
|
||||
|
||||
def compose_init_service(compose_text: str) -> str:
|
||||
return compose_text.split("\n permissions-init:\n", 1)[1].split(
|
||||
"\n landppt:\n", 1
|
||||
)[0]
|
||||
|
||||
|
||||
def compose_dependency_anchor(compose_text: str) -> str:
|
||||
return compose_text.split(
|
||||
"x-landppt-depends-on: &landppt-depends-on", 1
|
||||
)[1].split("\n\nservices:", 1)[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("compose_path", ["docker-compose.yml", "docker-compose-dev.yaml"])
|
||||
def test_compose_migrates_permissions_before_app_start(compose_path: str):
|
||||
compose_text = read_repo_file(compose_path)
|
||||
init_service = compose_init_service(compose_text)
|
||||
dependencies = compose_dependency_anchor(compose_text)
|
||||
|
||||
assert 'user: "0:0"' in init_service
|
||||
assert 'entrypoint: ["/usr/local/bin/docker-permissions-init.sh"]' in init_service
|
||||
assert 'network_mode: "none"' in init_service
|
||||
assert "read_only: true" in init_service
|
||||
assert 'restart: "no"' in init_service
|
||||
assert "- ALL" in init_service
|
||||
for capability in ("CHOWN", "FOWNER", "DAC_OVERRIDE", "SETUID", "SETGID"):
|
||||
assert f"- {capability}" in init_service
|
||||
assert "no-new-privileges:true" in init_service
|
||||
|
||||
for mount in (
|
||||
"${LANDPPT_ENV_FILE:-./.env}:/mnt/landppt/env/.env",
|
||||
"landppt_data:/mnt/landppt/data",
|
||||
"landppt_uploads:/mnt/landppt/uploads",
|
||||
"landppt_reports:/mnt/landppt/reports",
|
||||
"landppt_cache:/mnt/landppt/cache",
|
||||
"landppt_lib:/mnt/landppt/lib",
|
||||
):
|
||||
assert mount in init_service
|
||||
|
||||
assert "/app" not in init_service
|
||||
assert "${LANDPPT_ENV_FILE:-./.env}:/app/.env" in compose_text
|
||||
assert "permissions-init:" in dependencies
|
||||
assert "condition: service_completed_successfully" in dependencies
|
||||
|
||||
|
||||
def test_permission_migration_script_is_idempotent_and_validates_as_target_user():
|
||||
script = read_repo_file("docker-permissions-init.sh")
|
||||
dockerfile = read_repo_file("Dockerfile")
|
||||
|
||||
assert ".landppt-permissions-v1" in script
|
||||
assert 'LANDPPT_UID:-10001' in script
|
||||
assert 'LANDPPT_GID:-10001' in script
|
||||
assert "os.setgid(gid)" in script
|
||||
assert "os.setuid(uid)" in script
|
||||
assert 'chown -R "${TARGET_UID}:${TARGET_GID}"' in script
|
||||
assert "chmod -R u+rwX" in script
|
||||
assert "docker-permissions-init.sh /usr/local/bin/" in dockerfile
|
||||
|
||||
Reference in New Issue
Block a user