fix(setup): address security and robustness issues

- Use dollar-quoting in generated SQL to prevent injection
- Set .env.prod and init-postgres-prod.sql to mode 0600
- Use run_compose for OpenBao log capture (consistent env-file)
- Prompt user before continuing if OpenBao bootstrap fails
- Improve mask_secret to fully mask short secrets
- Check sysctl return code before parsing RAM
This commit is contained in:
Jason Staack
2026-03-14 10:01:44 -05:00
parent 9123c6e6c0
commit 4757b93d9d

View File

@@ -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():