fix deploy: preserve all credentials, only rotate Gitea token
Some checks failed
ci/woodpecker/push/ci Pipeline is running
ci/woodpecker/push/infra-ci Pipeline failed
ci/woodpecker/push/deploy Pipeline failed

Stop rotating PostgreSQL passwords — services read them from env
vars at startup and CI can't safely restart infrastructure.
Preserve ALL current .env values; only create a fresh Gitea API
token (stateless, no restart needed). This prevents the cascade
of auth failures that was killing the stack on every deploy.
This commit is contained in:
kert
2026-03-23 13:10:42 -04:00
parent 1727e1d59b
commit c9ab766e1f
2 changed files with 21 additions and 43 deletions

View File

@@ -29,7 +29,6 @@ from api.auth.provision import (
_retry, _retry,
derive_all, derive_all,
provision_gitea, provision_gitea,
provision_postgres,
provision_woodpecker, provision_woodpecker,
write_env, write_env,
) )
@@ -145,41 +144,21 @@ def deploy(
k, _, v = line.partition("=") k, _, v = line.partition("=")
current_env[k.strip()] = v.strip() current_env[k.strip()] = v.strip()
# Preserve BOOTSTRAP-tier credentials from current .env. # Preserve ALL current credentials from .env.
# These were set during initial bootstrap and must NOT be rotated # Credential rotation is dangerous — changing a password without
# (RustFS stores IAM on disk; changing env vars crashes it). # also restarting the service that reads it causes auth failures.
from api.auth.manifest import CREDENTIALS, Provisioner, Tier # We only overwrite values that were successfully rotated in Phase 1.
for var in current_env:
bootstrap_vars = { if var in values:
c.env_var
for c in CREDENTIALS
if c.tier is Tier.BOOTSTRAP and c.provisioner is not Provisioner.SKIP
}
for var in bootstrap_vars:
if var in current_env:
values[var] = current_env[var] values[var] = current_env[var]
log.info("Preserved %d bootstrap-tier credentials from .env", len(bootstrap_vars)) log.info("Preserved %d credentials from current .env", len(current_env))
# Use current GITEA_TOKEN (derived one is just a hash placeholder) # ── Phase 1: Rotate Gitea token ─────────────────────────────
if current_env.get("GITEA_TOKEN"): # Only rotate the Gitea API token — it's stateless and doesn't
values["GITEA_TOKEN"] = current_env["GITEA_TOKEN"] # require a service restart. DB passwords are NOT rotated because
log.info("Using current GITEA_TOKEN from .env") # services read them from env vars at startup and we can't safely
# restart infrastructure services from CI.
# ── Phase 1: Rotate backends FIRST ──────────────────────── result.postgres = True # preserved from .env, no rotation needed
# PostgreSQL — authenticate with CURRENT superuser password,
# then ALTER ROLE to the NEW derived passwords
pg_auth_pw = current_env.get(
"POSTGRES_PASSWORD", values.get("POSTGRES_PASSWORD", "")
)
try:
_retry(lambda: provision_postgres(values, superuser_pw=pg_auth_pw), "postgres")
result.postgres = True
log.info("Phase 1: PostgreSQL passwords rotated")
except Exception as e:
result.errors.append(("postgres", e))
log.error("Phase 1: PostgreSQL rotation failed: %s", e)
# Gitea
try: try:
token = _retry(lambda: provision_gitea(values), "gitea") token = _retry(lambda: provision_gitea(values), "gitea")
values["GITEA_TOKEN"] = token values["GITEA_TOKEN"] = token

View File

@@ -108,7 +108,7 @@ class TestDeploy:
env.write_text("OLD_KEY=keep\n") env.write_text("OLD_KEY=keep\n")
with ( with (
patch("api.auth.deploy.provision_postgres"), # provision_postgres no longer called
patch("api.auth.deploy.provision_gitea", return_value="new-tok"), patch("api.auth.deploy.provision_gitea", return_value="new-tok"),
patch("api.auth.deploy.provision_woodpecker"), patch("api.auth.deploy.provision_woodpecker"),
patch("api.auth.deploy.subprocess.run"), patch("api.auth.deploy.subprocess.run"),
@@ -131,7 +131,7 @@ class TestDeploy:
env.write_text("SAFE=original\n") env.write_text("SAFE=original\n")
with ( with (
patch("api.auth.deploy.provision_postgres"), # provision_postgres no longer called
patch("api.auth.deploy.provision_gitea", return_value="tok"), patch("api.auth.deploy.provision_gitea", return_value="tok"),
patch("api.auth.deploy.provision_woodpecker"), patch("api.auth.deploy.provision_woodpecker"),
patch("api.auth.deploy.subprocess.run"), patch("api.auth.deploy.subprocess.run"),
@@ -148,16 +148,15 @@ class TestDeploy:
content = env.read_text() content = env.read_text()
assert "SAFE=original" in content assert "SAFE=original" in content
def test_partial_backend_failure_still_deploys(self, tmp_path: Path): def test_gitea_failure_still_deploys(self, tmp_path: Path):
env = tmp_path / ".env" env = tmp_path / ".env"
env.write_text("") env.write_text("GITEA_TOKEN=old\n")
with ( with (
patch( patch(
"api.auth.deploy.provision_postgres", "api.auth.deploy.provision_gitea",
side_effect=Exception("pg down"), side_effect=Exception("gitea down"),
), ),
patch("api.auth.deploy.provision_gitea", return_value="tok"),
patch("api.auth.deploy.provision_woodpecker"), patch("api.auth.deploy.provision_woodpecker"),
patch("api.auth.deploy.subprocess.run"), patch("api.auth.deploy.subprocess.run"),
patch("api.auth.deploy.verify_all", return_value=[]), patch("api.auth.deploy.verify_all", return_value=[]),
@@ -165,6 +164,6 @@ class TestDeploy:
): ):
result = deploy(ROOT, COMMIT, env, compose_dir=tmp_path) result = deploy(ROOT, COMMIT, env, compose_dir=tmp_path)
assert not result.postgres assert not result.gitea
assert result.gitea assert result.postgres # preserved, not rotated
assert result.env_written assert result.env_written