diff --git a/setup.py b/setup.py index c470d11..1ac2779 100755 --- a/setup.py +++ b/setup.py @@ -142,8 +142,8 @@ def ask_yes_no(prompt: str, default: bool = False) -> bool: def mask_secret(value: str) -> str: """Show first 8 chars of a secret, mask the rest.""" - if len(value) <= 8: - return value + if len(value) <= 12: + return "*" * len(value) return value[:8] + "..." @@ -221,6 +221,8 @@ def check_ram() -> None: ["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=5, ) + if result.returncode != 0: + return ram_bytes = int(result.stdout.strip()) else: with open("/proc/meminfo") as f: @@ -577,6 +579,7 @@ CONFIG_BACKUP_MAX_CONCURRENT=10 """ ENV_PROD.write_text(content) + ENV_PROD.chmod(0o600) ok(f"Wrote {ENV_PROD.name}") @@ -586,6 +589,7 @@ def write_init_sql_prod(config: dict) -> None: poll_pw = config["poller_user_password"] db = config["postgres_db"] + # Use dollar-quoting ($pw$...$pw$) to avoid SQL injection from passwords content = f"""\ -- Production database init — generated by setup.py -- Passwords match those in .env.prod @@ -593,7 +597,7 @@ def write_init_sql_prod(config: dict) -> None: DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_user') THEN - CREATE ROLE app_user WITH LOGIN PASSWORD '{app_pw}' NOSUPERUSER NOCREATEDB NOCREATEROLE; + CREATE ROLE app_user WITH LOGIN PASSWORD $pw${app_pw}$pw$ NOSUPERUSER NOCREATEDB NOCREATEROLE; END IF; END $$; @@ -604,7 +608,7 @@ GRANT USAGE ON SCHEMA public TO app_user; DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'poller_user') THEN - CREATE ROLE poller_user WITH LOGIN PASSWORD '{poll_pw}' NOSUPERUSER NOCREATEDB NOCREATEROLE BYPASSRLS; + CREATE ROLE poller_user WITH LOGIN PASSWORD $pw${poll_pw}$pw$ NOSUPERUSER NOCREATEDB NOCREATEROLE BYPASSRLS; END IF; END $$; @@ -614,6 +618,7 @@ GRANT USAGE ON SCHEMA public TO poller_user; """ INIT_SQL_PROD.write_text(content) + INIT_SQL_PROD.chmod(0o600) ok(f"Wrote {INIT_SQL_PROD.name}") @@ -673,10 +678,7 @@ def bootstrap_openbao(config: dict) -> bool: # Parse credentials from container logs info("Capturing OpenBao credentials from logs...") - result = subprocess.run( - ["docker", "compose", "-f", COMPOSE_BASE, "-f", COMPOSE_PROD, "logs", "openbao"], - capture_output=True, text=True, timeout=30, cwd=PROJECT_ROOT, - ) + result = run_compose("logs", "openbao", check=False, capture=True, timeout=30) logs = result.stdout + result.stderr unseal_match = re.search(r"BAO_UNSEAL_KEY=(\S+)", logs) @@ -693,6 +695,7 @@ def bootstrap_openbao(config: dict) -> bool: env_content = env_content.replace("BAO_UNSEAL_KEY=PLACEHOLDER_RUN_SETUP", f"BAO_UNSEAL_KEY={unseal_key}") ENV_PROD.write_text(env_content) + ENV_PROD.chmod(0o600) ok("OpenBao credentials captured and saved to .env.prod") info(f"OPENBAO_TOKEN={mask_secret(root_token)}") @@ -859,6 +862,10 @@ def main() -> int: # Phase 4: OpenBao bao_ok = bootstrap_openbao(config) + if not bao_ok: + if not ask_yes_no("Continue without OpenBao credentials? (stack will need manual fix)", default=False): + warn("Fix OpenBao credentials in .env.prod and re-run setup.py.") + return 1 # Phase 5: Build if not build_images():