Efren Yevale Varela 3 недель назад
Родитель
Сommit
cf6c249fb8
5 измененных файлов с 43 добавлено и 24 удалено
  1. 10 8
      README.md
  2. 18 7
      RUNBOOK.md
  3. 10 6
      docs/CHANGELOG.md
  4. 1 0
      docs/DEPLOYMENT.md
  5. 4 3
      docs/OAUTH.md

+ 10 - 8
README.md

@@ -47,7 +47,9 @@ cargo build --locked --release
 
 
 The server reads configuration from `D_OAUTH_*` environment variables. Export a
 The server reads configuration from `D_OAUTH_*` environment variables. Export a
 development configuration (values here are placeholders; see
 development configuration (values here are placeholders; see
-[docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every key).
+[docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every key). The ports below
+are the local-dev defaults (Postgres `5432`, Redis `6379`); the shared test
+services use `5433`/`6380` (see Tests).
 
 
 ```bash
 ```bash
 export D_OAUTH_ISSUER=https://localhost:8080
 export D_OAUTH_ISSUER=https://localhost:8080
@@ -146,8 +148,9 @@ full grant behavior.
 ## Tests
 ## Tests
 
 
 The authoritative suite. The integration and end-to-end tests connect to the
 The authoritative suite. The integration and end-to-end tests connect to the
-shared Postgres and Redis from `compose.test.yaml` (Postgres on
-`127.0.0.1:5433`, Redis on `127.0.0.1:6380`), so start the services first and
+shared test Postgres and Redis from `compose.test.yaml` (test defaults:
+Postgres on `127.0.0.1:5433`, Redis on `127.0.0.1:6380` — not the dev
+`5432`/`6379`), so start the services first and
 tear them down with `down -v`:
 tear them down with `down -v`:
 
 
 ```bash
 ```bash
@@ -208,15 +211,14 @@ The container `HEALTHCHECK` uses the `healthcheck` subcommand, which probes the
 plaintext health listener without a shell or curl. See
 plaintext health listener without a shell or curl. See
 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
 
 
-For a local multi-replica proof, the deployment assets provide a compose file
-that runs three app replicas against one Postgres and one Redis. It is the local
+For a local multi-replica proof, `compose.yaml` runs three app replicas
+against one Postgres and one Redis. It is the local
 analogue of the cluster manifests, and proves that a token issued by one replica
 analogue of the cluster manifests, and proves that a token issued by one replica
-is introspectable via another. If your checkout does not include the compose
-file yet, it ships with the cluster manifests; see
+is introspectable via another; see
 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md):
 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md):
 
 
 ```bash
 ```bash
-docker compose up --scale app=3
+docker compose -f compose.yaml up --scale app=3
 ```
 ```
 
 
 Cluster deployment uses a separate migration Job, a PodDisruptionBudget, and
 Cluster deployment uses a separate migration Job, a PodDisruptionBudget, and

+ 18 - 7
RUNBOOK.md

@@ -115,10 +115,11 @@ correlation. No step below writes a secret to a log.
 
 
 ### Rotation order and commands
 ### Rotation order and commands
 
 
-- [ ] Confirm whether the key store leaked with the dump. The token pepper and
-      the TOTP encryption key live outside the database
+- [ ] Confirm whether the key store leaked with the dump. The token pepper, the
+      TOTP encryption key, and the PII field-encryption key live outside the database
       (`D_OAUTH_TOKEN_PEPPER` / `D_OAUTH_TOKEN_PEPPER_FILE`,
       (`D_OAUTH_TOKEN_PEPPER` / `D_OAUTH_TOKEN_PEPPER_FILE`,
-      `D_OAUTH_TOTP_ENCRYPTION_KEY` / `D_OAUTH_TOTP_ENCRYPTION_KEY_FILE`); if it
+      `D_OAUTH_TOTP_ENCRYPTION_KEY` / `D_OAUTH_TOTP_ENCRYPTION_KEY_FILE`,
+      `D_OAUTH_PII_ENCRYPTION_KEY` / `D_OAUTH_PII_ENCRYPTION_KEY_FILE`); if it
       is not provably untouched, assume it leaked and continue.
       is not provably untouched, assume it leaked and continue.
 - [ ] **1. Rotate the token pepper.** Point `D_OAUTH_TOKEN_PEPPER` (or
 - [ ] **1. Rotate the token pepper.** Point `D_OAUTH_TOKEN_PEPPER` (or
       `D_OAUTH_TOKEN_PEPPER_FILE`) at a new value, bump
       `D_OAUTH_TOKEN_PEPPER_FILE`) at a new value, bump
@@ -134,7 +135,15 @@ correlation. No step below writes a secret to a log.
 - [ ] Re-enroll every TOTP user before dropping the old key:
 - [ ] Re-enroll every TOTP user before dropping the old key:
       `d-oauth-server user enroll-totp --email <email>`
       `d-oauth-server user enroll-totp --email <email>`
       (prints the secret and `otpauth://` URI exactly once).
       (prints the secret and `otpauth://` URI exactly once).
-- [ ] **3. Rotate every confidential client secret.** Read the current metadata,
+- [ ] **3. Rotate the PII field-encryption key.** Point
+      `D_OAUTH_PII_ENCRYPTION_KEY` (or `D_OAUTH_PII_ENCRYPTION_KEY_FILE`) at a
+      new value, bump `D_OAUTH_PII_KEY_ID`, and roll the replicas with the old
+      key retained. Then, under a write freeze (app tier scaled to zero), run
+      `d-oauth-server pii rotate` to re-encrypt every `_enc` value and recompute
+      every blind index, verify with `d-oauth-server pii verify`, and only then
+      retire the old key. See
+      [docs/OPERATIONS.md](docs/OPERATIONS.md#pii-and-totp-key-rotation).
+- [ ] **4. Rotate every confidential client secret.** Read the current metadata,
       then re-submit it through RFC 7592 `PUT /register/{client_id}` to mint a
       then re-submit it through RFC 7592 `PUT /register/{client_id}` to mint a
       new secret and a new registration token:
       new secret and a new registration token:
 
 
@@ -151,15 +160,17 @@ correlation. No step below writes a secret to a log.
       The `PUT` response carries `client_secret` and
       The `PUT` response carries `client_secret` and
       `registration_access_token` exactly once. Store both before the process
       `registration_access_token` exactly once. Store both before the process
       exits; the previous registration token immediately answers `403`.
       exits; the previous registration token immediately answers `403`.
-- [ ] **4. Rotate every DCR registration token.** Every successful `PUT` above
+- [ ] **5. Rotate every DCR registration token.** Every successful `PUT` above
       already rotated the registration token. If you also need new
       already rotated the registration token. If you also need new
       registrations after the breach, issue a fresh single-use initial-access
       registrations after the breach, issue a fresh single-use initial-access
       token:
       token:
       `d-oauth-server initial-access-token issue --uses <n> --ttl <seconds>`
       `d-oauth-server initial-access-token issue --uses <n> --ttl <seconds>`
       (the raw token is printed once).
       (the raw token is printed once).
-- [ ] **5. Verify fail-closed behavior.** Readiness must report ready only with
+- [ ] **6. Verify fail-closed behavior.** Readiness must report ready only with
       both dependencies up:
       both dependencies up:
-      `d-oauth-server healthcheck --url http://127.0.0.1:8081/health/ready`.
+      `d-oauth-server healthcheck --url http://127.0.0.1:8081/health/ready`
+      (readiness; liveness is `/health/live` as in
+      [docs/OPERATIONS.md](docs/OPERATIONS.md)).
       A dependency outage during recovery must yield `503`, never a fail-open
       A dependency outage during recovery must yield `503`, never a fail-open
       success.
       success.
 
 

+ 10 - 6
docs/CHANGELOG.md

@@ -57,7 +57,7 @@ While the version stays below `1.0.0`, a minor bump may carry a breaking change.
   codecs); the text secret, URI, and one-time recovery codes are unchanged.
   codecs); the text secret, URI, and one-time recovery codes are unchanged.
 - A full-database-disclosure response procedure: `docs/SECURITY.md` gains the
 - A full-database-disclosure response procedure: `docs/SECURITY.md` gains the
   dump blast-radius table and the ordered rotation (token pepper, TOTP key,
   dump blast-radius table and the ordered rotation (token pepper, TOTP key,
-  client secrets, DCR registration tokens), and `RUNBOOK.md` gains the
+  PII field-encryption key, client secrets, DCR registration tokens), and `RUNBOOK.md` gains the
   matching operator checklist with concrete commands.
   matching operator checklist with concrete commands.
 - Cluster hardening in `deploy/k8s`: the token pepper and TOTP key mount as
 - Cluster hardening in `deploy/k8s`: the token pepper and TOTP key mount as
   `0400` tmpfs files under `/run/secrets` (read through
   `0400` tmpfs files under `/run/secrets` (read through
@@ -68,10 +68,12 @@ While the version stays below `1.0.0`, a minor bump may carry a breaking change.
   rejects one), and `automountServiceAccountToken` is `false`. Remote JAR
   rejects one), and `automountServiceAccountToken` is `false`. Remote JAR
   `request_uri` is documented as unsupported under the default-deny, with the
   `request_uri` is documented as unsupported under the default-deny, with the
   Cilium FQDN opt-in path in `docs/DEPLOYMENT.md`.
   Cilium FQDN opt-in path in `docs/DEPLOYMENT.md`.
-- `user disable` now purges all Redis state for the subject (sessions and their
+- `user disable` now purges all subject-scoped Redis state for the subject (sessions and their
   meta, codes, devices with their sidecars and mappings, grants via cascade
   meta, codes, devices with their sidecars and mappings, grants via cascade
   revoke, standalone tokens, and the lockout counter) before flipping
   revoke, standalone tokens, and the lockout counter) before flipping
-  `users.disabled`; a Redis failure aborts before the flag changes, so the
+  `users.disabled`; records without a subject (PAR, pending, DPoP and
+  `private_key_jwt` replay markers, rotation map, TOTP replay, rate-limit
+  counters) are untouched. A Redis failure aborts before the flag changes, so the
   command fails closed.
   command fails closed.
 - `POST /revoke` authenticates mTLS-registered clients (RFC 8705 §2) with the
 - `POST /revoke` authenticates mTLS-registered clients (RFC 8705 §2) with the
   verified peer chain injected by the TLS accept loop, exactly as the token and
   verified peer chain injected by the TLS accept loop, exactly as the token and
@@ -86,9 +88,11 @@ While the version stays below `1.0.0`, a minor bump may carry a breaking change.
   previous key. `pii_dual_write` (default `false`, encrypt-only) turns on
   previous key. `pii_dual_write` (default `false`, encrypt-only) turns on
   dual-write for the rolling-upgrade window; readiness fails closed while the
   dual-write for the rolling-upgrade window; readiness fails closed while the
   switch is off and any blanked plaintext PII column still holds a value.
   switch is off and any blanked plaintext PII column still holds a value.
-- TOTP secrets now encrypt under HKDF-SHA256 (info `totp-gcm-v2`) instead of
-  bare `SHA256(totp_key)`. New blobs carry format version 2; version-1 blobs
-  keep decrypting through a legacy fallback, so no re-enrollment is needed.
+- TOTP secrets now use a per-row envelope (blob version 3): a random DEK
+  wrapped by AES-256-GCM under HKDF-SHA256 (info `totp-kek-v1`), so
+  `totp rewrap` re-wraps the DEK without re-encrypting the secret. Versions 1
+  (bare `SHA256(totp_key)`) and 2 (HKDF info `totp-gcm-v2`) keep decrypting
+  through a legacy fallback, so no re-enrollment is needed.
 
 
 - `GET /.well-known/oauth-authorization-server` now carries an explicit
 - `GET /.well-known/oauth-authorization-server` now carries an explicit
   `Cache-Control: public, max-age=3600` instead of relying on browser
   `Cache-Control: public, max-age=3600` instead of relying on browser

+ 1 - 0
docs/DEPLOYMENT.md

@@ -371,6 +371,7 @@ manifest; verify the cluster CNI applies it.
 
 
 - Egress admits DNS to the `kube-system` namespace on port 53 (UDP and TCP) and
 - Egress admits DNS to the `kube-system` namespace on port 53 (UDP and TCP) and
   Postgres (5432) and Redis (6379) scoped to the namespace that hosts them —
   Postgres (5432) and Redis (6379) scoped to the namespace that hosts them —
+  cluster-service defaults, not the shared-test `5433`/`6380` —
   `d-oauth` in the reference deployment, matching the ConfigMap DSNs
   `d-oauth` in the reference deployment, matching the ConfigMap DSNs
   (`*.d-oauth.svc.cluster.local`). It carries no `ipBlock: 0.0.0.0/0` peer, so
   (`*.d-oauth.svc.cluster.local`). It carries no `ipBlock: 0.0.0.0/0` peer, so
   neither port is an exfiltration channel to an arbitrary internet host;
   neither port is an exfiltration channel to an arbitrary internet host;

+ 4 - 3
docs/OAUTH.md

@@ -281,9 +281,10 @@ on every use.
 `private_key_jwt` is client authentication only. It produces no confirmation and
 `private_key_jwt` is client authentication only. It produces no confirmation and
 never satisfies sender constraint. For token exchange, the proof must match the
 never satisfies sender constraint. For token exchange, the proof must match the
 subject token's confirmation. See [SECURITY.md](SECURITY.md) for the full matrix
 subject token's confirmation. See [SECURITY.md](SECURITY.md) for the full matrix
-and the confirmation propagation rule. It accepts `RS256`, `PS256`, `ES256`, and
-`EdDSA` only (`none` and HMAC are rejected); `exp` may lie at most 300 s past
-`now` plus 30 s clock skew, and the single-use `jti` marker lives for the
+and the confirmation propagation rule. `private_key_jwt` accepts `RS256`, `PS256`, `ES256`, and
+`EdDSA` only (`none` and HMAC are rejected); DPoP proofs accept `ES256`, `EdDSA`, and `RS256` only (no `PS256`).
+`exp` may lie at most 300 s past `now` with 30 s clock-skew leeway on each side
+(360 s bound), and the single-use `jti` marker lives for the
 assertion's remaining validity, clamped to `[1 s, 360 s]`.
 assertion's remaining validity, clamped to `[1 s, 360 s]`.
 
 
 ## Metadata
 ## Metadata