From 678d4bfb1e88268e0b2bd031ac83f45a2bb3f28b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sun, 21 Jun 2026 13:44:31 +0530 Subject: [PATCH] Harden Docker service credential setup --- docker-compose.yaml | 8 +-- docs/deployment/docker.mdx | 4 +- docs/deployment/update.mdx | 4 +- scripts/setup_local.ps1 | 7 +++ scripts/setup_local.sh | 7 +++ scripts/setup_remote.sh | 7 +++ scripts/start_docker.ps1 | 111 +++++++++++++++++++++++++++++++++++++ scripts/start_docker.sh | 92 ++++++++++++++++++++++++++++++ scripts/update_remote.sh | 24 +++++++- 9 files changed, 255 insertions(+), 9 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 610bc41f..134de93d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -47,8 +47,8 @@ services: container_name: minio command: server /data --console-address ":9001" environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin + MINIO_ROOT_USER: "${MINIO_ROOT_USER:-minioadmin}" + MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:-minioadmin}" MINIO_API_CORS_ALLOW_ORIGIN: "*" ports: - "127.0.0.1:9000:9000" # Bind to localhost explicitly @@ -154,8 +154,8 @@ services: # deployments behind HTTPS, set MINIO_PUBLIC_ENDPOINT in .env to # e.g. https://your-server.example.com (nginx proxies /voice-audio/). MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}" - MINIO_ACCESS_KEY: "minioadmin" - MINIO_SECRET_KEY: "minioadmin" + MINIO_ACCESS_KEY: "${MINIO_ROOT_USER:-minioadmin}" + MINIO_SECRET_KEY: "${MINIO_ROOT_PASSWORD:-minioadmin}" MINIO_BUCKET: "voice-audio" MINIO_SECURE: "false" diff --git a/docs/deployment/docker.mdx b/docs/deployment/docker.mdx index d5727108..5a804401 100644 --- a/docs/deployment/docker.mdx +++ b/docs/deployment/docker.mdx @@ -56,8 +56,8 @@ Invoke-WebRequest -OutFile start_docker.ps1 https://raw.githubusercontent.com/do This setup: - Downloads the latest docker-compose.yaml -- Creates `OSS_JWT_SECRET` and `REDIS_PASSWORD` in `.env` if they do not already exist -- Creates `POSTGRES_PASSWORD` for brand-new `.env` files; existing installs keep their original Postgres password +- Creates `OSS_JWT_SECRET`, `REDIS_PASSWORD`, and MinIO credentials in `.env` if they do not already exist +- Creates `POSTGRES_PASSWORD` for brand-new `.env` files and syncs retained local Postgres volumes to that value before startup - Prompts before running Docker Compose - Starts all required services including PostgreSQL, Redis, MinIO, API, and UI - Pulls the latest images automatically diff --git a/docs/deployment/update.mdx b/docs/deployment/update.mdx index b8eb414b..f598c610 100644 --- a/docs/deployment/update.mdx +++ b/docs/deployment/update.mdx @@ -66,7 +66,7 @@ The script overwrites `docker-compose.yaml` and the remote helper bundle (`remot ## Local deployment -For local Docker installs (the [Quick Start](/deployment/docker#quick-start) flow or `setup_local.sh` / `setup_local.ps1`), refresh `docker-compose.yaml` and the startup script, stop the stack, then run the startup script. The script preserves existing `.env` secrets, creates `REDIS_PASSWORD` if it is missing, and only creates `POSTGRES_PASSWORD` for brand-new `.env` files so existing Postgres volumes keep using their original password: +For local Docker installs (the [Quick Start](/deployment/docker#quick-start) flow or `setup_local.sh` / `setup_local.ps1`), refresh `docker-compose.yaml` and the startup script, stop the stack, then run the startup script. The script preserves existing `.env` secrets, creates `REDIS_PASSWORD` and MinIO credentials if they are missing, and only creates `POSTGRES_PASSWORD` for brand-new `.env` files. If a retained local Postgres volume already exists, it syncs the database user's password to `.env` before starting the full stack: ```bash macOS/Linux @@ -145,6 +145,6 @@ sudo docker compose --profile remote up -d If you update the `pipecat` submodule, you **must** run `git submodule update --init --recursive` before rebuilding, or the Docker build will not pick up `pipecat` changes. -If you maintain a fork with local customizations on top of upstream, merging conflicts in `docker-compose.yaml`, `remote_up.sh`, `scripts/run_dograh_init.sh`, `deploy/templates/*`, or `setup_remote.sh` is up to you — resolve them as you would any other git merge. Leave `OSS_JWT_SECRET`, `TURN_SECRET`, `POSTGRES_PASSWORD`, and `REDIS_PASSWORD` in `.env` unchanged across updates to preserve sessions, WebRTC auth, and service credentials. +If you maintain a fork with local customizations on top of upstream, merging conflicts in `docker-compose.yaml`, `remote_up.sh`, `scripts/run_dograh_init.sh`, `deploy/templates/*`, or `setup_remote.sh` is up to you — resolve them as you would any other git merge. Leave `OSS_JWT_SECRET`, `TURN_SECRET`, `POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `MINIO_ROOT_USER`, and `MINIO_ROOT_PASSWORD` in `.env` unchanged across updates to preserve sessions, WebRTC auth, and service credentials. The same migration warning above applies: rolling back across a schema change can leave the DB in a state the older API can't read. diff --git a/scripts/setup_local.ps1 b/scripts/setup_local.ps1 index ae594b72..4e54ec51 100644 --- a/scripts/setup_local.ps1 +++ b/scripts/setup_local.ps1 @@ -245,6 +245,8 @@ Write-Info "[2/$TotalSteps] Creating environment file..." $ossJwtSecret = New-HexSecret 32 $postgresPassword = New-HexSecret 32 $redisPassword = New-HexSecret 32 +$minioRootUser = "dograh$((New-HexSecret 6).Substring(0, 12))" +$minioRootPassword = New-HexSecret 32 $envLines = @( '# Container registry for Dograh images' @@ -263,6 +265,11 @@ $envLines = @( '# container.' "REDIS_PASSWORD=$redisPassword" '' + '# MinIO root credentials. Used by the MinIO container and the API''s' + '# MINIO_ACCESS_KEY / MINIO_SECRET_KEY.' + "MINIO_ROOT_USER=$minioRootUser" + "MINIO_ROOT_PASSWORD=$minioRootPassword" + '' '# Telemetry (set to false to disable)' "ENABLE_TELEMETRY=$EnableTelemetry" '' diff --git a/scripts/setup_local.sh b/scripts/setup_local.sh index 4c74b316..4c7fad5d 100755 --- a/scripts/setup_local.sh +++ b/scripts/setup_local.sh @@ -152,6 +152,8 @@ echo -e "${BLUE}[$ENV_STEP/$TOTAL_STEPS] Creating environment file...${NC}" OSS_JWT_SECRET=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -hex 32) REDIS_PASSWORD=$(openssl rand -hex 32) +MINIO_ROOT_USER="dograh$(openssl rand -hex 6)" +MINIO_ROOT_PASSWORD=$(openssl rand -hex 32) cat > .env << ENV_EOF # Container registry for Dograh images @@ -170,6 +172,11 @@ POSTGRES_PASSWORD=$POSTGRES_PASSWORD # container. REDIS_PASSWORD=$REDIS_PASSWORD +# MinIO root credentials. Used by the MinIO container and the API's +# MINIO_ACCESS_KEY / MINIO_SECRET_KEY. +MINIO_ROOT_USER=$MINIO_ROOT_USER +MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD + # Telemetry (set to false to disable) ENABLE_TELEMETRY=$ENABLE_TELEMETRY diff --git a/scripts/setup_remote.sh b/scripts/setup_remote.sh index b1a75d50..04a45adc 100755 --- a/scripts/setup_remote.sh +++ b/scripts/setup_remote.sh @@ -253,6 +253,8 @@ echo -e "${BLUE}[4/$TOTAL] Creating environment file...${NC}" OSS_JWT_SECRET=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -hex 32) REDIS_PASSWORD=$(openssl rand -hex 32) +MINIO_ROOT_USER="dograh$(openssl rand -hex 6)" +MINIO_ROOT_PASSWORD=$(openssl rand -hex 32) cat > .env << ENV_EOF # Remote deployments run with production signaling and HTTPS defaults @@ -288,6 +290,11 @@ POSTGRES_PASSWORD=$POSTGRES_PASSWORD # rotated by updating .env and recreating the redis container. REDIS_PASSWORD=$REDIS_PASSWORD +# MinIO root credentials. Used by the MinIO container and the API's +# MINIO_ACCESS_KEY / MINIO_SECRET_KEY. +MINIO_ROOT_USER=$MINIO_ROOT_USER +MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD + # Telemetry (set to false to disable) ENABLE_TELEMETRY=$ENABLE_TELEMETRY diff --git a/scripts/start_docker.ps1 b/scripts/start_docker.ps1 index 8fff43ba..22f78f6e 100644 --- a/scripts/start_docker.ps1 +++ b/scripts/start_docker.ps1 @@ -11,6 +11,10 @@ function New-HexSecret { return -join ($bytes | ForEach-Object { $_.ToString('x2') }) } +function New-MinioRootUser { + return "dograh$((New-HexSecret).Substring(0, 12))" +} + function Get-DotEnvValue { param( [string]$Path, @@ -60,6 +64,84 @@ function Set-DotEnvValue { [System.IO.File]::WriteAllLines((Join-Path (Get-Location) $Path), $lines, $Utf8NoBom) } +function Get-PostgresVolumeName { + try { + $configJson = docker compose config --format json 2>$null + if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrEmpty($configJson)) { + $config = $configJson | ConvertFrom-Json + $volumeName = $config.volumes.postgres_data.name + if (-not [string]::IsNullOrEmpty($volumeName)) { + return $volumeName + } + } + } catch { + # Fall back to Compose's default project-name convention below. + } + + $projectName = if ([string]::IsNullOrEmpty($env:COMPOSE_PROJECT_NAME)) { + (Split-Path -Leaf (Get-Location).Path).ToLowerInvariant() -replace '[^a-z0-9_-]', '' + } else { + $env:COMPOSE_PROJECT_NAME.ToLowerInvariant() -replace '[^a-z0-9_-]', '' + } + + return "${projectName}_postgres_data" +} + +function Test-DockerVolumeExists { + param([string]$Name) + + docker volume inspect $Name *> $null + return $LASTEXITCODE -eq 0 +} + +function Wait-PostgresReady { + for ($attempt = 0; $attempt -lt 20; $attempt++) { + docker compose exec -T postgres pg_isready -U postgres *> $null + if ($LASTEXITCODE -eq 0) { + return + } + Start-Sleep -Seconds 1 + } + + Write-Error 'Postgres did not become ready while syncing POSTGRES_PASSWORD.' + exit 1 +} + +function Sync-PostgresPassword { + param([string]$Password) + + if ([string]::IsNullOrEmpty($Password)) { + return + } + + $volumeName = Get-PostgresVolumeName + if ([string]::IsNullOrEmpty($volumeName) -or -not (Test-DockerVolumeExists $volumeName)) { + return + } + + Write-Host "Existing Postgres volume detected; syncing postgres password from $EnvFile." + $env:REGISTRY = $Registry + $env:ENABLE_TELEMETRY = $EnableTelemetry + docker compose up -d postgres + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + Wait-PostgresReady + + "ALTER USER postgres WITH PASSWORD :'dograh_password';" | docker compose exec -T postgres psql ` + -U postgres ` + -d postgres ` + -v 'ON_ERROR_STOP=1' ` + -v "dograh_password=$Password" > $null + if ($LASTEXITCODE -ne 0) { + Write-Error 'Failed to sync POSTGRES_PASSWORD with the existing Postgres volume.' + exit $LASTEXITCODE + } + + Write-Host 'Postgres password synced.' +} + if (-not (Test-Path 'docker-compose.yaml')) { Write-Error 'docker-compose.yaml not found. Download it first, then re-run this script.' exit 1 @@ -95,6 +177,34 @@ if ([string]::IsNullOrEmpty($existingRedisPassword)) { Write-Host "REDIS_PASSWORD is already set in $EnvFile." } +$existingMinioRootUser = Get-DotEnvValue -Path $EnvFile -Key 'MINIO_ROOT_USER' +if ([string]::IsNullOrEmpty($existingMinioRootUser)) { + $existingMinioAccessKey = Get-DotEnvValue -Path $EnvFile -Key 'MINIO_ACCESS_KEY' + if ([string]::IsNullOrEmpty($existingMinioAccessKey)) { + Set-DotEnvValue -Path $EnvFile -Key 'MINIO_ROOT_USER' -Value (New-MinioRootUser) + Write-Host "Created MINIO_ROOT_USER in $EnvFile." + } else { + Set-DotEnvValue -Path $EnvFile -Key 'MINIO_ROOT_USER' -Value $existingMinioAccessKey + Write-Host "Created MINIO_ROOT_USER in $EnvFile from existing MINIO_ACCESS_KEY." + } +} else { + Write-Host "MINIO_ROOT_USER is already set in $EnvFile." +} + +$existingMinioRootPassword = Get-DotEnvValue -Path $EnvFile -Key 'MINIO_ROOT_PASSWORD' +if ([string]::IsNullOrEmpty($existingMinioRootPassword)) { + $existingMinioSecretKey = Get-DotEnvValue -Path $EnvFile -Key 'MINIO_SECRET_KEY' + if ([string]::IsNullOrEmpty($existingMinioSecretKey)) { + Set-DotEnvValue -Path $EnvFile -Key 'MINIO_ROOT_PASSWORD' -Value (New-HexSecret) + Write-Host "Created MINIO_ROOT_PASSWORD in $EnvFile." + } else { + Set-DotEnvValue -Path $EnvFile -Key 'MINIO_ROOT_PASSWORD' -Value $existingMinioSecretKey + Write-Host "Created MINIO_ROOT_PASSWORD in $EnvFile from existing MINIO_SECRET_KEY." + } +} else { + Write-Host "MINIO_ROOT_PASSWORD is already set in $EnvFile." +} + Write-Host '' Write-Host "Docker registry: $Registry" Write-Host "Telemetry enabled: $EnableTelemetry" @@ -111,6 +221,7 @@ if ($answer -match '^[Nn]') { $env:REGISTRY = $Registry $env:ENABLE_TELEMETRY = $EnableTelemetry +Sync-PostgresPassword -Password (Get-DotEnvValue -Path $EnvFile -Key 'POSTGRES_PASSWORD') docker compose up --pull always if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE diff --git a/scripts/start_docker.sh b/scripts/start_docker.sh index 199f3b8d..56812f0a 100755 --- a/scripts/start_docker.sh +++ b/scripts/start_docker.sh @@ -26,6 +26,10 @@ generate_secret() { fail "Could not generate a secret. Install python3 or openssl, or set secrets manually in .env." } +generate_minio_root_user() { + printf 'dograh%s\n' "$(generate_secret | cut -c1-12)" +} + dotenv_value() { local key=$1 local line @@ -74,6 +78,63 @@ set_dotenv_value() { fi } +postgres_volume_name() { + local volume_name="" + local project_name="" + + if command -v python3 >/dev/null 2>&1; then + volume_name="$( + docker compose config --format json 2>/dev/null \ + | python3 -c 'import json, sys; print(json.load(sys.stdin).get("volumes", {}).get("postgres_data", {}).get("name", ""))' 2>/dev/null \ + || true + )" + if [[ -n "$volume_name" ]]; then + printf '%s\n' "$volume_name" + return + fi + fi + + project_name="${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}" + project_name="$(printf '%s' "$project_name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]//g')" + printf '%s_postgres_data\n' "$project_name" +} + +sync_postgres_password() { + local postgres_password=$1 + local volume_name="" + local postgres_ready=false + + [[ -n "$postgres_password" ]] || return + + volume_name="$(postgres_volume_name)" + if ! docker volume inspect "$volume_name" >/dev/null 2>&1; then + return + fi + + echo "Existing Postgres volume detected; syncing postgres password from $ENV_FILE." + REGISTRY="$REGISTRY" ENABLE_TELEMETRY="$ENABLE_TELEMETRY" docker compose up -d postgres + + for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do + if docker compose exec -T postgres pg_isready -U postgres >/dev/null 2>&1; then + postgres_ready=true + break + fi + sleep 1 + done + + if [[ "$postgres_ready" != "true" ]]; then + fail "Postgres did not become ready while syncing POSTGRES_PASSWORD." + fi + + printf '%s\n' "ALTER USER postgres WITH PASSWORD :'dograh_password';" \ + | docker compose exec -T postgres psql \ + -U postgres \ + -d postgres \ + -v ON_ERROR_STOP=1 \ + -v "dograh_password=$postgres_password" >/dev/null + echo "Postgres password synced." +} + [[ -f docker-compose.yaml ]] || fail "docker-compose.yaml not found. Download it first, then re-run this script." env_file_existed=false @@ -109,6 +170,34 @@ else echo "REDIS_PASSWORD is already set in $ENV_FILE." fi +existing_minio_root_user="$(dotenv_value MINIO_ROOT_USER || true)" +if [[ -z "$existing_minio_root_user" ]]; then + existing_minio_access_key="$(dotenv_value MINIO_ACCESS_KEY || true)" + if [[ -n "$existing_minio_access_key" ]]; then + set_dotenv_value MINIO_ROOT_USER "$existing_minio_access_key" + echo "Created MINIO_ROOT_USER in $ENV_FILE from existing MINIO_ACCESS_KEY." + else + set_dotenv_value MINIO_ROOT_USER "$(generate_minio_root_user)" + echo "Created MINIO_ROOT_USER in $ENV_FILE." + fi +else + echo "MINIO_ROOT_USER is already set in $ENV_FILE." +fi + +existing_minio_root_password="$(dotenv_value MINIO_ROOT_PASSWORD || true)" +if [[ -z "$existing_minio_root_password" ]]; then + existing_minio_secret_key="$(dotenv_value MINIO_SECRET_KEY || true)" + if [[ -n "$existing_minio_secret_key" ]]; then + set_dotenv_value MINIO_ROOT_PASSWORD "$existing_minio_secret_key" + echo "Created MINIO_ROOT_PASSWORD in $ENV_FILE from existing MINIO_SECRET_KEY." + else + set_dotenv_value MINIO_ROOT_PASSWORD "$(generate_secret)" + echo "Created MINIO_ROOT_PASSWORD in $ENV_FILE." + fi +else + echo "MINIO_ROOT_PASSWORD is already set in $ENV_FILE." +fi + echo "" echo "Docker registry: $REGISTRY" echo "Telemetry enabled: $ENABLE_TELEMETRY" @@ -130,4 +219,7 @@ case "$answer" in ;; esac +postgres_password="$(dotenv_value POSTGRES_PASSWORD || true)" +sync_postgres_password "$postgres_password" + REGISTRY="$REGISTRY" ENABLE_TELEMETRY="$ENABLE_TELEMETRY" docker compose up --pull always diff --git a/scripts/update_remote.sh b/scripts/update_remote.sh index 922f1f80..eef2dc75 100755 --- a/scripts/update_remote.sh +++ b/scripts/update_remote.sh @@ -44,7 +44,11 @@ generate_secret() { return fi - dograh_fail "Could not generate REDIS_PASSWORD. Install python3 or openssl, or set REDIS_PASSWORD manually in .env." + dograh_fail "Could not generate a secret. Install python3 or openssl, or set missing secrets manually in .env." +} + +generate_minio_root_user() { + printf 'dograh%s\n' "$(generate_secret | cut -c1-12)" } echo -e "${BLUE}" @@ -239,6 +243,24 @@ if [[ -z "${REDIS_PASSWORD:-}" ]]; then dograh_set_env_key .env REDIS_PASSWORD "$(generate_secret)" dograh_success "✓ REDIS_PASSWORD created in .env" fi +if [[ -z "${MINIO_ROOT_USER:-}" ]]; then + if [[ -n "${MINIO_ACCESS_KEY:-}" ]]; then + dograh_set_env_key .env MINIO_ROOT_USER "$MINIO_ACCESS_KEY" + dograh_success "✓ MINIO_ROOT_USER created in .env from existing MINIO_ACCESS_KEY" + else + dograh_set_env_key .env MINIO_ROOT_USER "$(generate_minio_root_user)" + dograh_success "✓ MINIO_ROOT_USER created in .env" + fi +fi +if [[ -z "${MINIO_ROOT_PASSWORD:-}" ]]; then + if [[ -n "${MINIO_SECRET_KEY:-}" ]]; then + dograh_set_env_key .env MINIO_ROOT_PASSWORD "$MINIO_SECRET_KEY" + dograh_success "✓ MINIO_ROOT_PASSWORD created in .env from existing MINIO_SECRET_KEY" + else + dograh_set_env_key .env MINIO_ROOT_PASSWORD "$(generate_secret)" + dograh_success "✓ MINIO_ROOT_PASSWORD created in .env" + fi +fi dograh_prepare_remote_install "$(pwd)" docker compose config -q dograh_success "✓ Remote init configuration validated"