Files
baya-monorepo/dev/post-phase/refinement/refinement-phase-5-security-hygiene.md
T
2026-07-10 20:59:47 +03:30

91 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 04) ·
> **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.11.6) — the authoritative, file/line-cited spec.
- **[../server/runtime-services.md](../server/runtime-services.md)** § 13 (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.11.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.