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