# Refinement Phase 5 — Security & config hygiene (deployment blockers)

> **Mission:** remove the committed secrets and dev-grade defaults that block any non-local deployment. None of
> this changes behavior, but two items are **live credential leaks sitting in git today**. Do the credential
> rotation (5.1) *now*, independently of everything else.
>
> **Track:** backend (config/security) · **Depends on:** nothing (can run in parallel with Phases 0–4) ·
> **Unlocks:** any real/shared-environment deployment
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**

## 1. Context

This phase **is** the already-written server audit's **post-phase-1**. It is reproduced here as a refinement
phase for sequencing; the full evidence (file/line for every item) and the exact fixes live in
**[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md) § post-phase-1**. Read that
section — it is the spec. This file only orders and frames it.

The findings, in short (all verified in code, dated 2026-07-10):

1. **Committed `sa` connection string** — a real remote SQL Server (public IP `87.107.152.16`, plaintext
   password) in `appsettings.json` **and** `appsettings.Development.json` (byte-identical), for both the app DB
   and the log DB. Anyone with repo access owns the database. Violates root `CLAUDE.md` agreement #6.
2. **Placeholder JWE signing/encryption + PII field-encryption keys** (`local-dev-…-change-me`), a ~7-day
   access-token lifetime (`ExpirationMinutes: 10000`), `RequireHttpsMetadata = false`, and stub
   `Issuer`/`Audience` (`MyWebsite`).
3. **Seeded `admin` / `qw123321`** full-admin user on every non-Testing boot.
4. **Auto-seeded active sandbox ZarinPal gateway** on every boot (a production DB would silently hold an active
   sandbox money gateway).
5. **Kestrel HTTP/2-only default** (breaks non-TLS HTTP/1.1 hops).
6. **Rate limiter not proxy-aware** (`RemoteIpAddress`, no `ForwardedHeaders`) → behind a proxy every client
   shares one bucket; plus the two payment webhooks use mismatched rate policies.

## 2. Required reading

- **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** § post-phase-1 (items
  1.1–1.6) — the authoritative, file/line-cited spec.
- **[../server/runtime-services.md](../server/runtime-services.md)** § 1–3 (SQL Server + reverse-proxy notes).
- [Phase 0](refinement-phase-0-bring-up.md) — it already introduced a secret-free local DB default; this phase
  finishes the rotation and the deployed-environment story.

## 3. Scope — deliver plan items 1.1–1.6

- **5.1 (do first, today) — rotate & remove the committed `sa` connection strings.** Assume compromised: rotate
  the password, create a least-privilege app login, move both connection strings to user-secrets (dev) / env
  vars (deploy), commit only a placeholder, and add a secret-scanning pre-commit hook. Consider history
  scrubbing (`git filter-repo`).
- **5.2 — replace the placeholder JWE + field-encryption keys** with per-environment secrets; set a sane
  access-token lifetime (≤ 60 min; refresh already exists); `RequireHttpsMetadata = true` outside Development;
  real `Issuer`/`Audience`. (Rotating the field key needs a re-encryption migration — do it before real PII
  exists.)
- **5.3 — environment-gate the seeded admin** (read bootstrap creds from config; Development-only; force a
  password change on first login).
- **5.4 — environment-gate the sandbox gateway seed** (Development/Testing only, or seed `is_active = false`).
- **5.5 — fix the Kestrel HTTP/2-only default** (`Http1AndHttp2`; give gRPC its own endpoint if kept).
- **5.6 — make rate limiting proxy-aware** (`ForwardedHeaders` trusting the known proxy; partition on the
  resolved client IP) and pick one deliberate webhook rate policy.

## 4. Mocks & seams

None. Config/security only.

## 5. Critical rules

- **5.1 is urgent and independent** — it doesn't wait for this phase's slot; the credential is leaking now.
- Rotating the field-encryption key **invalidates existing dev-DB ciphertext** — acceptable pre-launch, but do
  it before real PII exists and plan the re-encryption migration if any real data exists.
- Don't break local dev: Development keeps working defaults (via user-secrets/env), only the *committed* values
  become placeholders.

## 6. Definition of Done

- [ ] No working secret remains in any committed file (connection strings, JWE keys, field keys, admin
      password, gateway creds) — verified by a secret scan.
- [ ] Access-token lifetime sane; `RequireHttpsMetadata` gated by environment; real issuer/audience.
- [ ] Admin + sandbox-gateway seeds are Development-gated (or config-driven).
- [ ] Kestrel default is `Http1AndHttp2`; rate limiter honors forwarded headers; webhooks use one deliberate
      policy. `dotnet build`/`dotnet test` green.

## 7. How to test

- Fresh clone with no user-secrets → the app **fails fast with a clear "missing connection string" error**
  (not a silent connect to a leaked remote). With user-secrets/env set → boots normally.
- Confirm no `admin`/`qw123321` and no active sandbox gateway appear in a non-Development boot.
- Behind a reverse proxy, distinct client IPs get distinct rate-limit buckets.

## 8. Hand off & document

- Update `server/CLAUDE.md` (startup wiring / identity / rate-limiting) to reflect the config-driven secrets and
  the forwarded-headers middleware. Record the credential rotation in the security log / handoff. Save a memory
  note that the committed `sa` string was rotated and externalized.
