fastapi-m8 3.0.0__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.codacy.yml +4 -0
- fastapi_m8-3.2.0/.dockerignore +20 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.env.example +34 -2
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.github/workflows/PiPy.yml +25 -0
- fastapi_m8-3.2.0/.github/workflows/ci.yml +54 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.gitignore +3 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/CHANGELOG.md +95 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/PKG-INFO +70 -9
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/README.md +68 -7
- fastapi_m8-3.2.0/SECURITY.md +50 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/__init__.py +13 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_app.py +34 -9
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_compat.py +14 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_deps.py +7 -6
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_events.py +37 -27
- fastapi_m8-3.2.0/fastapi_m8/_internal_auth.py +238 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_revocation.py +83 -9
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_version.py +1 -1
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/config.py +58 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/pyproject.toml +2 -2
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_app.py +197 -12
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_deps.py +44 -1
- fastapi_m8-3.2.0/tests/test_event_signing_gate.py +50 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_events.py +42 -39
- fastapi_m8-3.2.0/tests/test_internal_auth.py +285 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_revocation.py +174 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.gitattributes +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.github/FUNDING.yml +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.github/dependabot.yml +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.github/workflows/CI.yaml +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/.pydocstyle +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/LICENSE +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_async_stub.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_engine.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/_health.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/scripts/__init__.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/scripts/docker_start.sh +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/fastapi_m8/scripts/pre_start.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/__init__.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/conftest.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_app_extra.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_async_stub.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_compat.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_config.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_config_file_secrets.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_engine.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_health.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_host_header_routing.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_meta.py +0 -0
- {fastapi_m8-3.0.0 → fastapi_m8-3.2.0}/tests/test_pre_start.py +0 -0
|
@@ -23,3 +23,7 @@ exclude_paths:
|
|
|
23
23
|
- "*.egg-info/**"
|
|
24
24
|
- ".pytest_cache/**"
|
|
25
25
|
- "**/__pycache__/**"
|
|
26
|
+
# SECURITY.md is kept markdownlint-clean by hand; excluded here because
|
|
27
|
+
# Codacy's prose analyzer misclassifies a security policy doc (flags standard
|
|
28
|
+
# security acronyms such as CA/TTL/JTI as "undefined" on every revision).
|
|
29
|
+
- "SECURITY.md"
|
|
@@ -68,8 +68,13 @@ TOKEN_AUDIENCE=my-service
|
|
|
68
68
|
# is required at boot unless signing is disabled. Must match the value set on fa-auth.
|
|
69
69
|
# DEV-ONLY placeholder — replace in staging/production.
|
|
70
70
|
EVENT_SIGNING_KEY=DEV-ONLY-do-not-use-event-signing-key-Aa1!
|
|
71
|
-
#
|
|
72
|
-
#
|
|
71
|
+
# The two rollout flags below feed the shared check_config_health gate that
|
|
72
|
+
# create_app auto-runs at startup (item 7.x.1). Under STRICT_PRODUCTION_MODE
|
|
73
|
+
# both misconfigurations are FATAL (the app refuses to boot); EVENT_SIGNING_
|
|
74
|
+
# ACCEPT_UNSIGNED is also fatal under ENVIRONMENT=production. Leave both at the
|
|
75
|
+
# secure defaults (signing on, unsigned rejected) for any real deployment.
|
|
76
|
+
# EVENT_SIGNING_ENABLED=false # disable signing entirely (opt-out) — fatal under strict
|
|
77
|
+
# EVENT_SIGNING_ACCEPT_UNSIGNED=false # transitional: accept signed AND unsigned — fatal in prod/strict
|
|
73
78
|
|
|
74
79
|
# ── Auth event stream (fa-auth SSE bridge) ────────────────────────────────────
|
|
75
80
|
# Optional: start AuthEventStreamClient in lifespan for best-effort cache
|
|
@@ -85,7 +90,25 @@ EVENT_SIGNING_KEY=DEV-ONLY-do-not-use-event-signing-key-Aa1!
|
|
|
85
90
|
# PRIVATE_API_SECRET=change-me-shared-internal-secret
|
|
86
91
|
|
|
87
92
|
# Revocation failure mode: fail_closed (default, secure) or fail_open (availability).
|
|
93
|
+
# A fail_open opt-out is logged loudly and counted (revocation_check_failures_total
|
|
94
|
+
# {mode="fail_open"}) on every swallowed failure — item 5.5.
|
|
88
95
|
# ACCESS_REVOCATION_FAILURE_MODE=fail_closed
|
|
96
|
+
# Positive validation cache for JTI checks: 0 (default) = always call fa-auth;
|
|
97
|
+
# >0 caches active=True for N seconds (stream events evict). e.g. 30.
|
|
98
|
+
# REVOCATION_CACHE_TTL_SECONDS=0
|
|
99
|
+
|
|
100
|
+
# ── Per-consumer internal auth (item 9.1) ────────────────────────────────────
|
|
101
|
+
# Unset (default) = legacy mode: PRIVATE_API_SECRET is sent as X-Internal-Token,
|
|
102
|
+
# matching fa-auth's fallback when it has no PRIVATE_API_CONSUMERS registry.
|
|
103
|
+
# Set INTERNAL_CLIENT_ID to this service's registered consumer id to switch to
|
|
104
|
+
# the per-consumer model — PRIVATE_API_SECRET then carries THIS consumer's
|
|
105
|
+
# bootstrap secret and fa-auth gates each private route by granted scope.
|
|
106
|
+
# INTERNAL_CLIENT_ID=my-service
|
|
107
|
+
# Opt into exchanging the bootstrap credential for short-TTL Bearer service
|
|
108
|
+
# tokens at {issuer}/private/v1/service-token (rotation comes from the short TTL):
|
|
109
|
+
# SERVICE_TOKEN_EXCHANGE_ENABLED=false
|
|
110
|
+
# SERVICE_TOKEN_SCOPES=introspection # comma/space list; default introspection
|
|
111
|
+
# SERVICE_TOKEN_REFRESH_LEEWAY_SECONDS=30 # refresh this many seconds before exp
|
|
89
112
|
|
|
90
113
|
# ── Database ──────────────────────────────────────────────────────────────────
|
|
91
114
|
SELECTED_DB=Mysql # Mysql | Postgres
|
|
@@ -103,6 +126,15 @@ REDIS_USER=redisuser
|
|
|
103
126
|
REDIS_PASSWORD=change-me
|
|
104
127
|
REDIS_SSL=false
|
|
105
128
|
|
|
129
|
+
# ── Health detail gating (item 9.3) ──────────────────────────────────────────
|
|
130
|
+
# Dedicated credential for the deep-/health detail body gate (X-Internal-Token).
|
|
131
|
+
# Unset (default) = detail body never shown — only {"status": "ok/fail"} is
|
|
132
|
+
# returned to unauthenticated callers (fail-closed). Set to a long-lived secret
|
|
133
|
+
# and present it as X-Internal-Token to receive the full per-check breakdown.
|
|
134
|
+
# MUST NOT equal PRIVATE_API_SECRET — a fatal startup error is raised on reuse.
|
|
135
|
+
# Supports _FILE mount: set HEALTH_DETAIL_CREDENTIAL_FILE=/run/secrets/health_cred.txt
|
|
136
|
+
# HEALTH_DETAIL_CREDENTIAL=change-me-health-detail-credential
|
|
137
|
+
|
|
106
138
|
# ── Observability ─────────────────────────────────────────────────────────────
|
|
107
139
|
METRICS_ENABLED=false
|
|
108
140
|
# METRICS_GROUPS=traffic,performance,reliability,health,auth
|
|
@@ -54,3 +54,28 @@ jobs:
|
|
|
54
54
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
55
55
|
with:
|
|
56
56
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
57
|
+
|
|
58
|
+
sbom:
|
|
59
|
+
name: Generate & attach SBOM
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
needs:
|
|
62
|
+
- release-build
|
|
63
|
+
permissions:
|
|
64
|
+
contents: write
|
|
65
|
+
id-token: write
|
|
66
|
+
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
69
|
+
|
|
70
|
+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
71
|
+
with:
|
|
72
|
+
python-version: "3.x"
|
|
73
|
+
|
|
74
|
+
- name: Install package
|
|
75
|
+
run: pip install -e ".[all]"
|
|
76
|
+
|
|
77
|
+
- uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
|
|
78
|
+
with:
|
|
79
|
+
artifact-name: fastapi-m8-${{ github.event.release.tag_name }}.sbom.spdx.json
|
|
80
|
+
format: spdx-json
|
|
81
|
+
upload-release-assets: true
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Test & lint
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install -e ".[dev]"
|
|
26
|
+
|
|
27
|
+
- name: Lint (ruff)
|
|
28
|
+
run: ruff check .
|
|
29
|
+
|
|
30
|
+
- name: Type check (mypy)
|
|
31
|
+
run: mypy fastapi_m8
|
|
32
|
+
|
|
33
|
+
- name: Security scan (bandit)
|
|
34
|
+
run: bandit -r fastapi_m8 -x tests
|
|
35
|
+
|
|
36
|
+
- name: Test (pytest)
|
|
37
|
+
run: pytest
|
|
38
|
+
|
|
39
|
+
- name: Dependency audit (pip-audit)
|
|
40
|
+
run: pip-audit
|
|
41
|
+
|
|
42
|
+
secret-scan:
|
|
43
|
+
name: Secret scan (gitleaks)
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
48
|
+
with:
|
|
49
|
+
fetch-depth: 0
|
|
50
|
+
|
|
51
|
+
- uses: gitleaks/gitleaks-action@1938557f6a58837331b99822ab17b8e536e7bef9 # v2.3.0
|
|
52
|
+
env:
|
|
53
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
54
|
+
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
|
@@ -5,6 +5,101 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning:
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [3.2.0] — 2026-06-28 · ungated `/health` constant liveness body (item 9.4 Design B)
|
|
9
|
+
|
|
10
|
+
> **MINOR — additive, backward-compatible.** The only behaviour change is in the ungated
|
|
11
|
+
> `/health` response: callers without a valid `HEALTH_DETAIL_CREDENTIAL` now always
|
|
12
|
+
> receive `200 {"status":"ok"}` instead of the real aggregate status. Callers that
|
|
13
|
+
> already present the credential are unaffected — they still get the real status, per-check
|
|
14
|
+
> detail, and the 503 HTTP code when checks fail. `auth-sdk-m8` floor unchanged at
|
|
15
|
+
> `>=2.1.0,<3.0.0`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Ungated `/health` body is now a constant liveness response (item 9.4 Design B).**
|
|
20
|
+
Callers without a valid `HEALTH_DETAIL_CREDENTIAL` (or `detail_public=True`) always
|
|
21
|
+
receive `200 {"status":"ok"}`, regardless of the real aggregate health. Previously the
|
|
22
|
+
ungated branch returned `{"status": overall.value}`, which leaked `degraded` — a
|
|
23
|
+
public attack-timing oracle that signals when fail-open degradation is active.
|
|
24
|
+
|
|
25
|
+
Health checks are **not run** for ungated requests (pure liveness, no dependency
|
|
26
|
+
probing); caching is unaffected for credentialed callers. Credentialed callers
|
|
27
|
+
(valid `X-Internal-Token` matching `HEALTH_DETAIL_CREDENTIAL`, or `detail_public=True`)
|
|
28
|
+
continue to receive the real aggregate status, the per-check body, and the correct
|
|
29
|
+
HTTP status code (200 or 503).
|
|
30
|
+
|
|
31
|
+
- **README `Health Detail Gating` section updated** to document the constant ungated
|
|
32
|
+
body and the Design B rationale; the defaults-by-layer table row for
|
|
33
|
+
`HEALTH_DETAIL_CREDENTIAL` updated accordingly.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [3.1.0] — 2026-06-24/25 · consumer-side post-1.0 remediation (9.1 / 5.5 / 7.x.1 / 9.2 / 10.1) + event-stream 9.1 follow-on
|
|
38
|
+
|
|
39
|
+
> **MINOR — additive, backward-compatible.** Default behaviour is unchanged: a consumer
|
|
40
|
+
> with none of the new settings set keeps the legacy single `PRIVATE_API_SECRET`
|
|
41
|
+
> (`X-Internal-Token`) posture and the existing fail-closed revocation path. The post-1.0
|
|
42
|
+
> breaking removals (retiring the shared `PRIVATE_API_SECRET` model) remain **deferred** —
|
|
43
|
+
> `fastapi-m8 3.0.0` / `auth-sdk-m8 2.0.1` are not yet consumed by any live issuer/consumer
|
|
44
|
+
> stack, so this remediation wave folds into a minor rather than forcing a new major.
|
|
45
|
+
> Requires `auth-sdk-m8 >= 2.1.0, < 3.0.0` (floor raised from 2.0.1 by the event-stream
|
|
46
|
+
> follow-on item — `auth_provider` on `AuthEventStreamClient` landed in SDK 2.1.0).
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Per-consumer credentials & short-TTL service tokens — consumer side (item 9.1).** New
|
|
51
|
+
`fastapi_m8._internal_auth` with `build_internal_auth(settings)` selecting one of three
|
|
52
|
+
modes purely by config:
|
|
53
|
+
- **legacy** (`INTERNAL_CLIENT_ID` unset) — single `X-Internal-Token`, as before;
|
|
54
|
+
- **bootstrap** (`INTERNAL_CLIENT_ID` set) — per-consumer `X-Internal-Client` +
|
|
55
|
+
`X-Internal-Token` on every private call;
|
|
56
|
+
- **service token** (`+ SERVICE_TOKEN_EXCHANGE_ENABLED`) — exchange the bootstrap
|
|
57
|
+
credential at `{issuer}/private/v1/service-token` for a short-TTL `Authorization:
|
|
58
|
+
Bearer` token, refreshed before `exp` and re-exchanged once on a `401`.
|
|
59
|
+
|
|
60
|
+
`RemoteRevocationClient` now attaches the provider's headers per request (with a single
|
|
61
|
+
401 re-exchange retry) instead of baking `X-Internal-Token` in at construction. New
|
|
62
|
+
public exports: `build_internal_auth`, `InternalAuthProvider`,
|
|
63
|
+
`ServiceTokenInternalAuth`, `derive_service_token_url`. New settings:
|
|
64
|
+
`INTERNAL_CLIENT_ID`, `SERVICE_TOKEN_EXCHANGE_ENABLED`, `SERVICE_TOKEN_SCOPES`,
|
|
65
|
+
`SERVICE_TOKEN_REFRESH_LEEWAY_SECONDS`.
|
|
66
|
+
- **Revocation degradation observability (item 5.5).** New
|
|
67
|
+
`revocation_check_failures_total{mode}` counter and a loud `security.revocation_fail_open`
|
|
68
|
+
log line: a `fail_open` opt-out is now surfaced as a conscious availability-over-safety
|
|
69
|
+
decision, never silent. Consumer-side degradation matrix tested end-to-end
|
|
70
|
+
(`fail_closed` → 503, `fail_open` → token accepted) through `get_current_user`.
|
|
71
|
+
- **`SECURITY.md` (item 9.2).** New consumer-scoped security doc cross-referencing
|
|
72
|
+
`auth-sdk-m8`'s canonical "Service identity and mTLS" pattern for multi-host deployments,
|
|
73
|
+
plus a vulnerability-reporting section.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **`build_event_stream_client` routes through `build_internal_auth` (item 9.1 event-stream
|
|
78
|
+
follow-on, 2026-06-25).** The SSE stream now authenticates via the same
|
|
79
|
+
`InternalAuthProvider` the revocation client uses, selected purely by config (legacy /
|
|
80
|
+
bootstrap / service-token). The factory always passes `auth_provider` to
|
|
81
|
+
`AuthEventStreamClient`, which owns the provider lifecycle and closes it on `stop()`.
|
|
82
|
+
Consumer settings are unchanged; the only external requirement change is the SDK floor
|
|
83
|
+
bump to `>=2.1.0`.
|
|
84
|
+
- **`auth-sdk-m8` floor raised to `>=2.1.0`** (was `>=2.0.1`). `2.1.0` ships the
|
|
85
|
+
`auth_provider` parameter on `AuthEventStreamClient` and the framework-agnostic
|
|
86
|
+
`auth_sdk_m8.security.internal_auth` module. The `<3.0.0` ceiling is unchanged.
|
|
87
|
+
- **README (item 10.1).** New *Per-consumer internal auth* subsection and a *Defaults by
|
|
88
|
+
layer (consumer)* table documenting the consumer column (auto-run config-health,
|
|
89
|
+
`ALLOWED_HOSTS` inheritance, `METRICS_SCRAPE_CREDENTIAL`, `_FILE` inheritance,
|
|
90
|
+
`ACCESS_REVOCATION_FAILURE_MODE`). Event-stream section updated to note the shared
|
|
91
|
+
credential provider.
|
|
92
|
+
- **Event-signing rollout flags surfaced for consumers (item 7.x.1).** `.env.example`
|
|
93
|
+
documents `EVENT_SIGNING_ENABLED` / `EVENT_SIGNING_ACCEPT_UNSIGNED` and their strict
|
|
94
|
+
config-health gate (fatal under `STRICT_PRODUCTION_MODE`; `ACCEPT_UNSIGNED` also fatal
|
|
95
|
+
under `ENVIRONMENT=production`). The gate is auto-run via `create_app`; a consumer-side
|
|
96
|
+
test asserts it fires. `.env.example` also gains the new 9.1 vars and
|
|
97
|
+
`REVOCATION_CACHE_TTL_SECONDS`.
|
|
98
|
+
- `COMPAT_MATRIX` `3.1` entry updated to `auth-sdk-m8 >=2.1.0,<3.0.0` (reflects the
|
|
99
|
+
event-stream `auth_provider` requirement).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
8
103
|
## [3.0.0] — 2026-06-23 · auth-sdk-m8 2.0.0 alignment — single-mount `/ping` + SDK major floor
|
|
9
104
|
|
|
10
105
|
> **MAJOR.** Two independent breaking changes, either of which alone forces this bump:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-m8
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: FastAPI application framework for m8 consumer microservices.
|
|
5
5
|
Author-email: Eli Serra <e.serra173@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -216,7 +216,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
216
216
|
Classifier: Topic :: Software Development :: Libraries
|
|
217
217
|
Requires-Python: >=3.11
|
|
218
218
|
Requires-Dist: anyio>=4.0
|
|
219
|
-
Requires-Dist: auth-sdk-m8[config,events,fastapi,observability,security]<3.0.0,>=2.0
|
|
219
|
+
Requires-Dist: auth-sdk-m8[config,events,fastapi,observability,security]<3.0.0,>=2.1.0
|
|
220
220
|
Requires-Dist: fastapi>=0.136.3
|
|
221
221
|
Requires-Dist: httpx>=0.27.0
|
|
222
222
|
Requires-Dist: packaging>=24.0
|
|
@@ -613,10 +613,29 @@ Required only when `TOKEN_MODE=stateful` and `AUTH_SERVICE_ROLE=consumer`.
|
|
|
613
613
|
| Variable | Required | Default | Description |
|
|
614
614
|
|---|---|---|---|
|
|
615
615
|
| `INTROSPECTION_URL` | Yes | — | `POST` endpoint on auth service for JTI revocation checks, e.g. `http://auth_user_service:8000/user/private/v1/jti-status` |
|
|
616
|
-
| `PRIVATE_API_SECRET` | Yes | — |
|
|
617
|
-
| `ACCESS_REVOCATION_FAILURE_MODE` | No | `fail_closed` | `fail_closed` (default, secure — reject tokens when the check is unverifiable) or `fail_open` (accept on network/HTTP error). |
|
|
616
|
+
| `PRIVATE_API_SECRET` | Yes | — | The credential for private calls. In **legacy** mode it is the single shared secret sent as `X-Internal-Token` (must match auth service); in **per-consumer** mode (see below) it carries *this* consumer's bootstrap secret. |
|
|
617
|
+
| `ACCESS_REVOCATION_FAILURE_MODE` | No | `fail_closed` | `fail_closed` (default, secure — reject tokens when the check is unverifiable, returning **503**) or `fail_open` (accept on network/HTTP error — the opt-out is logged loudly and counted as `revocation_check_failures_total{mode="fail_open"}`). |
|
|
618
618
|
| `REVOCATION_CACHE_TTL_SECONDS` | No | `0` | Short-TTL positive validation cache. `0` (default) disables it — every request calls fa-auth. Set to e.g. `30` to trust an `active=True` result for 30 s, skipping the HTTP round-trip; stream events (`session-revoked`/`user-deleted`) evict affected entries and an unresumable gap flushes all (requires the event-stream client). |
|
|
619
619
|
|
|
620
|
+
#### Per-consumer internal auth (item 9.1)
|
|
621
|
+
|
|
622
|
+
By default a consumer authenticates private calls with the single shared
|
|
623
|
+
`PRIVATE_API_SECRET` (legacy mode), which matches fa-auth's fallback when it has no
|
|
624
|
+
`PRIVATE_API_CONSUMERS` registry. Set `INTERNAL_CLIENT_ID` to switch to the
|
|
625
|
+
**per-consumer** model: `PRIVATE_API_SECRET` becomes this service's *bootstrap*
|
|
626
|
+
secret, fa-auth authorizes each private route by the credential's granted scope
|
|
627
|
+
(deny-by-default), and the blast radius collapses from the whole fleet to one
|
|
628
|
+
consumer. Optionally exchange the bootstrap credential for short-TTL `Authorization:
|
|
629
|
+
Bearer` service tokens so rotation comes for free from the token TTL. Selection is
|
|
630
|
+
purely by config — the home lab keeps working untouched.
|
|
631
|
+
|
|
632
|
+
| Variable | Required | Default | Description |
|
|
633
|
+
|---|---|---|---|
|
|
634
|
+
| `INTERNAL_CLIENT_ID` | No | — | This consumer's `X-Internal-Client` id. Unset = legacy single-secret mode; set = per-consumer bootstrap mode. |
|
|
635
|
+
| `SERVICE_TOKEN_EXCHANGE_ENABLED` | No | `false` | Exchange the bootstrap credential for short-TTL Bearer service tokens at `{issuer}/private/v1/service-token` (requires `INTERNAL_CLIENT_ID`). |
|
|
636
|
+
| `SERVICE_TOKEN_SCOPES` | No | `["introspection"]` | Scopes requested when minting a service token; fa-auth narrows to the subset the bootstrap credential was granted. |
|
|
637
|
+
| `SERVICE_TOKEN_REFRESH_LEEWAY_SECONDS` | No | `30` | Refresh a cached service token this many seconds before its `exp` so a call never races expiry. |
|
|
638
|
+
|
|
620
639
|
### Auth Event Stream (fa-auth SSE bridge)
|
|
621
640
|
|
|
622
641
|
An **optional, best-effort accelerator** for cache eviction. `fa-auth-m8` bridges its
|
|
@@ -631,7 +650,12 @@ No second Redis, no broker: consumers speak HTTPS to fa-auth, which they already
|
|
|
631
650
|
> HTTP authority path alone.
|
|
632
651
|
|
|
633
652
|
Wire it in your lifespan with `build_event_stream_client`, which constructs the SDK's
|
|
634
|
-
`AuthEventStreamClient` from your settings (no SDK internals needed)
|
|
653
|
+
`AuthEventStreamClient` from your settings (no SDK internals needed). The factory uses
|
|
654
|
+
the same internal-auth provider as the revocation client — the stream credential is
|
|
655
|
+
therefore **a single config knob** shared with JTI introspection: legacy
|
|
656
|
+
`X-Internal-Token`, per-consumer bootstrap, or short-TTL `Authorization: Bearer` service
|
|
657
|
+
token, selected by `INTERNAL_CLIENT_ID` / `SERVICE_TOKEN_EXCHANGE_ENABLED` (see
|
|
658
|
+
*Per-consumer internal auth* above).
|
|
635
659
|
|
|
636
660
|
```python
|
|
637
661
|
from contextlib import asynccontextmanager
|
|
@@ -666,9 +690,9 @@ async def lifespan(app: FastAPI):
|
|
|
666
690
|
|
|
667
691
|
The client verifies every payload's HMAC signature with `EVENT_SIGNING_KEY` (must match
|
|
668
692
|
fa-auth), auto-reconnects with jittered backoff, resumes via `Last-Event-ID`, and **never
|
|
669
|
-
raises into the host app**. Requires `TOKEN_MODE=stateful` so `INTROSPECTION_URL`
|
|
670
|
-
|
|
671
|
-
|
|
693
|
+
raises into the host app**. Requires `TOKEN_MODE=stateful` so `INTROSPECTION_URL` is set.
|
|
694
|
+
Behind a reverse proxy, disable response buffering on the stream endpoint so events and
|
|
695
|
+
heartbeats pass through promptly.
|
|
672
696
|
|
|
673
697
|
| Variable | Required | Default | Description |
|
|
674
698
|
|---|---|---|---|
|
|
@@ -700,13 +724,32 @@ do not connect to Redis directly.
|
|
|
700
724
|
| `REDIS_PASSWORD` | Redis password |
|
|
701
725
|
| `REDIS_SSL` | Enable TLS (`true`/`false`, default `false`) |
|
|
702
726
|
|
|
727
|
+
### Health Detail Gating (items 9.3 + 9.4)
|
|
728
|
+
|
|
729
|
+
| Variable | Default | Description |
|
|
730
|
+
|---|---|---|
|
|
731
|
+
| `HEALTH_DETAIL_CREDENTIAL` | — | Optional credential for the `/health` detail body gate. When **unset**, `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness response). When set, callers must present `X-Internal-Token: <value>` (constant-time match) to receive the real aggregate status, per-check breakdown, and the correct HTTP code (200 or 503). **Must not equal `PRIVATE_API_SECRET`** — accidental reuse is a fatal startup misconfiguration. Supports `_FILE` mount: set `HEALTH_DETAIL_CREDENTIAL_FILE=/run/secrets/health_cred.txt`. |
|
|
732
|
+
|
|
733
|
+
> **Constant ungated body (item 9.4 Design B):** ungated callers always receive
|
|
734
|
+
> `200 {"status":"ok"}` regardless of the real aggregate health (including `degraded`
|
|
735
|
+
> or `fail`). This prevents operational-state leakage on public-HTTPS stacks: a
|
|
736
|
+
> `degraded` response is a timing oracle that signals fail-open degradation is active.
|
|
737
|
+
> Only callers presenting a valid `HEALTH_DETAIL_CREDENTIAL` see the real status, the
|
|
738
|
+
> per-check detail, and the 503 HTTP code when checks fail. Liveness probes and
|
|
739
|
+
> `{API_PREFIX}/ping` are unaffected.
|
|
740
|
+
|
|
741
|
+
> **No-reuse enforcement (item 9.3):** at startup, `create_app` asserts that neither
|
|
742
|
+
> `HEALTH_DETAIL_CREDENTIAL` nor `METRICS_SCRAPE_CREDENTIAL` equals `PRIVATE_API_SECRET`.
|
|
743
|
+
> This ensures each credential is independently rotatable — exposing one value cannot open
|
|
744
|
+
> multiple surfaces. The check raises `ConfigurationError` (fail-closed), aborting startup.
|
|
745
|
+
|
|
703
746
|
### Observability
|
|
704
747
|
|
|
705
748
|
| Variable | Default | Description |
|
|
706
749
|
|---|---|---|
|
|
707
750
|
| `METRICS_ENABLED` | `false` | Enable Prometheus metrics middleware and the `/metrics` route |
|
|
708
751
|
| `METRICS_GROUPS` | — | Comma-separated groups: `traffic`, `performance`, `reliability`, `health`, `auth`, or `all` |
|
|
709
|
-
| `METRICS_SCRAPE_CREDENTIAL` | — | Optional static bearer credential for the `/metrics` scrape endpoint. When set, requests must present `Authorization: Bearer <value>` (constant-time match). When unset, `/metrics` relies on network isolation only. |
|
|
752
|
+
| `METRICS_SCRAPE_CREDENTIAL` | — | Optional static bearer credential for the `/metrics` scrape endpoint. When set, requests must present `Authorization: Bearer <value>` (constant-time match). When unset, `/metrics` relies on network isolation only. **Must not equal `PRIVATE_API_SECRET`**. |
|
|
710
753
|
|
|
711
754
|
> **`/metrics` route:** when `METRICS_ENABLED=true`, `create_app` also registers a
|
|
712
755
|
> `GET /metrics` endpoint (hidden from the schema) rendering the Prometheus registry.
|
|
@@ -778,6 +821,24 @@ locally before deploy).
|
|
|
778
821
|
> These knobs are inherited from `CommonSettings`; consumer services do not redeclare
|
|
779
822
|
> them. The same layer is shared by `fa-auth-m8` and every consumer.
|
|
780
823
|
|
|
824
|
+
### Defaults by layer (consumer)
|
|
825
|
+
|
|
826
|
+
How the security-critical settings behave specifically in the **fastapi-m8 consumer**
|
|
827
|
+
layer. This is the consumer column of the fleet-wide defaults-by-layer table (the SDK and
|
|
828
|
+
fa-auth-m8 READMEs hold the issuer/SDK columns). The throughline: secure-by-default,
|
|
829
|
+
inherited from `auth-sdk-m8`, and made *fatal* only when pointed at production.
|
|
830
|
+
|
|
831
|
+
| Control | Consumer behaviour | Becomes fatal when |
|
|
832
|
+
|---|---|---|
|
|
833
|
+
| Config-health gate | `create_app` **auto-runs** `check_config_health` in its lifespan before the app signals ready — no manual wiring | any fatal check trips (raises `ConfigurationError`, app refuses to boot) |
|
|
834
|
+
| `ACCESS_REVOCATION_FAILURE_MODE` | `fail_closed` by default — an unverifiable revocation check returns **503**; `fail_open` is a conscious opt-out, logged loudly and counted (`revocation_check_failures_total{mode="fail_open"}`) | never fatal (it *is* the degradation policy) |
|
|
835
|
+
| `ALLOWED_HOSTS` | inherited from `CommonSettings`; unset = no host check (dev) | unset under `STRICT_PRODUCTION_MODE` (strict fatal); wildcard `*` under strict |
|
|
836
|
+
| `EVENT_SIGNING_ENABLED` / `EVENT_SIGNING_ACCEPT_UNSIGNED` | inherited secure defaults (signing on, unsigned rejected); the gate fires through the consumer's auto-run config-health (item 7.x.1) | `ENABLED=false` under strict; `ACCEPT_UNSIGNED=true` under production or strict |
|
|
837
|
+
| `METRICS_SCRAPE_CREDENTIAL` | unset = `/metrics` relies on network isolation only; set = constant-time `Authorization: Bearer` gate. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (network-isolation is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
|
|
838
|
+
| `HEALTH_DETAIL_CREDENTIAL` (items 9.3 + 9.4) | unset = `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness; real status never leaked); set = `X-Internal-Token` must match to receive the real aggregate status, per-check breakdown, and correct HTTP code. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (no credential = constant liveness response, which is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
|
|
839
|
+
| `INTERNAL_CLIENT_ID` (item 9.1) | unset = legacy single `PRIVATE_API_SECRET`; set = per-consumer bootstrap / service-token auth on private calls | never fatal (must be coordinated with the issuer's `PRIVATE_API_CONSUMERS`) |
|
|
840
|
+
| `_FILE` secret mounts | **inherited** from `CommonSettings` — every secret (`PRIVATE_API_SECRET_FILE`, `DB_PASSWORD_FILE`, `METRICS_SCRAPE_CREDENTIAL_FILE`, `HEALTH_DETAIL_CREDENTIAL_FILE`, …) can be sourced from `/run/secrets/*` with no code change | a referenced `<FIELD>_FILE` path is missing (fails closed at construction) |
|
|
841
|
+
|
|
781
842
|
---
|
|
782
843
|
|
|
783
844
|
## API Reference
|
|
@@ -357,10 +357,29 @@ Required only when `TOKEN_MODE=stateful` and `AUTH_SERVICE_ROLE=consumer`.
|
|
|
357
357
|
| Variable | Required | Default | Description |
|
|
358
358
|
|---|---|---|---|
|
|
359
359
|
| `INTROSPECTION_URL` | Yes | — | `POST` endpoint on auth service for JTI revocation checks, e.g. `http://auth_user_service:8000/user/private/v1/jti-status` |
|
|
360
|
-
| `PRIVATE_API_SECRET` | Yes | — |
|
|
361
|
-
| `ACCESS_REVOCATION_FAILURE_MODE` | No | `fail_closed` | `fail_closed` (default, secure — reject tokens when the check is unverifiable) or `fail_open` (accept on network/HTTP error). |
|
|
360
|
+
| `PRIVATE_API_SECRET` | Yes | — | The credential for private calls. In **legacy** mode it is the single shared secret sent as `X-Internal-Token` (must match auth service); in **per-consumer** mode (see below) it carries *this* consumer's bootstrap secret. |
|
|
361
|
+
| `ACCESS_REVOCATION_FAILURE_MODE` | No | `fail_closed` | `fail_closed` (default, secure — reject tokens when the check is unverifiable, returning **503**) or `fail_open` (accept on network/HTTP error — the opt-out is logged loudly and counted as `revocation_check_failures_total{mode="fail_open"}`). |
|
|
362
362
|
| `REVOCATION_CACHE_TTL_SECONDS` | No | `0` | Short-TTL positive validation cache. `0` (default) disables it — every request calls fa-auth. Set to e.g. `30` to trust an `active=True` result for 30 s, skipping the HTTP round-trip; stream events (`session-revoked`/`user-deleted`) evict affected entries and an unresumable gap flushes all (requires the event-stream client). |
|
|
363
363
|
|
|
364
|
+
#### Per-consumer internal auth (item 9.1)
|
|
365
|
+
|
|
366
|
+
By default a consumer authenticates private calls with the single shared
|
|
367
|
+
`PRIVATE_API_SECRET` (legacy mode), which matches fa-auth's fallback when it has no
|
|
368
|
+
`PRIVATE_API_CONSUMERS` registry. Set `INTERNAL_CLIENT_ID` to switch to the
|
|
369
|
+
**per-consumer** model: `PRIVATE_API_SECRET` becomes this service's *bootstrap*
|
|
370
|
+
secret, fa-auth authorizes each private route by the credential's granted scope
|
|
371
|
+
(deny-by-default), and the blast radius collapses from the whole fleet to one
|
|
372
|
+
consumer. Optionally exchange the bootstrap credential for short-TTL `Authorization:
|
|
373
|
+
Bearer` service tokens so rotation comes for free from the token TTL. Selection is
|
|
374
|
+
purely by config — the home lab keeps working untouched.
|
|
375
|
+
|
|
376
|
+
| Variable | Required | Default | Description |
|
|
377
|
+
|---|---|---|---|
|
|
378
|
+
| `INTERNAL_CLIENT_ID` | No | — | This consumer's `X-Internal-Client` id. Unset = legacy single-secret mode; set = per-consumer bootstrap mode. |
|
|
379
|
+
| `SERVICE_TOKEN_EXCHANGE_ENABLED` | No | `false` | Exchange the bootstrap credential for short-TTL Bearer service tokens at `{issuer}/private/v1/service-token` (requires `INTERNAL_CLIENT_ID`). |
|
|
380
|
+
| `SERVICE_TOKEN_SCOPES` | No | `["introspection"]` | Scopes requested when minting a service token; fa-auth narrows to the subset the bootstrap credential was granted. |
|
|
381
|
+
| `SERVICE_TOKEN_REFRESH_LEEWAY_SECONDS` | No | `30` | Refresh a cached service token this many seconds before its `exp` so a call never races expiry. |
|
|
382
|
+
|
|
364
383
|
### Auth Event Stream (fa-auth SSE bridge)
|
|
365
384
|
|
|
366
385
|
An **optional, best-effort accelerator** for cache eviction. `fa-auth-m8` bridges its
|
|
@@ -375,7 +394,12 @@ No second Redis, no broker: consumers speak HTTPS to fa-auth, which they already
|
|
|
375
394
|
> HTTP authority path alone.
|
|
376
395
|
|
|
377
396
|
Wire it in your lifespan with `build_event_stream_client`, which constructs the SDK's
|
|
378
|
-
`AuthEventStreamClient` from your settings (no SDK internals needed)
|
|
397
|
+
`AuthEventStreamClient` from your settings (no SDK internals needed). The factory uses
|
|
398
|
+
the same internal-auth provider as the revocation client — the stream credential is
|
|
399
|
+
therefore **a single config knob** shared with JTI introspection: legacy
|
|
400
|
+
`X-Internal-Token`, per-consumer bootstrap, or short-TTL `Authorization: Bearer` service
|
|
401
|
+
token, selected by `INTERNAL_CLIENT_ID` / `SERVICE_TOKEN_EXCHANGE_ENABLED` (see
|
|
402
|
+
*Per-consumer internal auth* above).
|
|
379
403
|
|
|
380
404
|
```python
|
|
381
405
|
from contextlib import asynccontextmanager
|
|
@@ -410,9 +434,9 @@ async def lifespan(app: FastAPI):
|
|
|
410
434
|
|
|
411
435
|
The client verifies every payload's HMAC signature with `EVENT_SIGNING_KEY` (must match
|
|
412
436
|
fa-auth), auto-reconnects with jittered backoff, resumes via `Last-Event-ID`, and **never
|
|
413
|
-
raises into the host app**. Requires `TOKEN_MODE=stateful` so `INTROSPECTION_URL`
|
|
414
|
-
|
|
415
|
-
|
|
437
|
+
raises into the host app**. Requires `TOKEN_MODE=stateful` so `INTROSPECTION_URL` is set.
|
|
438
|
+
Behind a reverse proxy, disable response buffering on the stream endpoint so events and
|
|
439
|
+
heartbeats pass through promptly.
|
|
416
440
|
|
|
417
441
|
| Variable | Required | Default | Description |
|
|
418
442
|
|---|---|---|---|
|
|
@@ -444,13 +468,32 @@ do not connect to Redis directly.
|
|
|
444
468
|
| `REDIS_PASSWORD` | Redis password |
|
|
445
469
|
| `REDIS_SSL` | Enable TLS (`true`/`false`, default `false`) |
|
|
446
470
|
|
|
471
|
+
### Health Detail Gating (items 9.3 + 9.4)
|
|
472
|
+
|
|
473
|
+
| Variable | Default | Description |
|
|
474
|
+
|---|---|---|
|
|
475
|
+
| `HEALTH_DETAIL_CREDENTIAL` | — | Optional credential for the `/health` detail body gate. When **unset**, `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness response). When set, callers must present `X-Internal-Token: <value>` (constant-time match) to receive the real aggregate status, per-check breakdown, and the correct HTTP code (200 or 503). **Must not equal `PRIVATE_API_SECRET`** — accidental reuse is a fatal startup misconfiguration. Supports `_FILE` mount: set `HEALTH_DETAIL_CREDENTIAL_FILE=/run/secrets/health_cred.txt`. |
|
|
476
|
+
|
|
477
|
+
> **Constant ungated body (item 9.4 Design B):** ungated callers always receive
|
|
478
|
+
> `200 {"status":"ok"}` regardless of the real aggregate health (including `degraded`
|
|
479
|
+
> or `fail`). This prevents operational-state leakage on public-HTTPS stacks: a
|
|
480
|
+
> `degraded` response is a timing oracle that signals fail-open degradation is active.
|
|
481
|
+
> Only callers presenting a valid `HEALTH_DETAIL_CREDENTIAL` see the real status, the
|
|
482
|
+
> per-check detail, and the 503 HTTP code when checks fail. Liveness probes and
|
|
483
|
+
> `{API_PREFIX}/ping` are unaffected.
|
|
484
|
+
|
|
485
|
+
> **No-reuse enforcement (item 9.3):** at startup, `create_app` asserts that neither
|
|
486
|
+
> `HEALTH_DETAIL_CREDENTIAL` nor `METRICS_SCRAPE_CREDENTIAL` equals `PRIVATE_API_SECRET`.
|
|
487
|
+
> This ensures each credential is independently rotatable — exposing one value cannot open
|
|
488
|
+
> multiple surfaces. The check raises `ConfigurationError` (fail-closed), aborting startup.
|
|
489
|
+
|
|
447
490
|
### Observability
|
|
448
491
|
|
|
449
492
|
| Variable | Default | Description |
|
|
450
493
|
|---|---|---|
|
|
451
494
|
| `METRICS_ENABLED` | `false` | Enable Prometheus metrics middleware and the `/metrics` route |
|
|
452
495
|
| `METRICS_GROUPS` | — | Comma-separated groups: `traffic`, `performance`, `reliability`, `health`, `auth`, or `all` |
|
|
453
|
-
| `METRICS_SCRAPE_CREDENTIAL` | — | Optional static bearer credential for the `/metrics` scrape endpoint. When set, requests must present `Authorization: Bearer <value>` (constant-time match). When unset, `/metrics` relies on network isolation only. |
|
|
496
|
+
| `METRICS_SCRAPE_CREDENTIAL` | — | Optional static bearer credential for the `/metrics` scrape endpoint. When set, requests must present `Authorization: Bearer <value>` (constant-time match). When unset, `/metrics` relies on network isolation only. **Must not equal `PRIVATE_API_SECRET`**. |
|
|
454
497
|
|
|
455
498
|
> **`/metrics` route:** when `METRICS_ENABLED=true`, `create_app` also registers a
|
|
456
499
|
> `GET /metrics` endpoint (hidden from the schema) rendering the Prometheus registry.
|
|
@@ -522,6 +565,24 @@ locally before deploy).
|
|
|
522
565
|
> These knobs are inherited from `CommonSettings`; consumer services do not redeclare
|
|
523
566
|
> them. The same layer is shared by `fa-auth-m8` and every consumer.
|
|
524
567
|
|
|
568
|
+
### Defaults by layer (consumer)
|
|
569
|
+
|
|
570
|
+
How the security-critical settings behave specifically in the **fastapi-m8 consumer**
|
|
571
|
+
layer. This is the consumer column of the fleet-wide defaults-by-layer table (the SDK and
|
|
572
|
+
fa-auth-m8 READMEs hold the issuer/SDK columns). The throughline: secure-by-default,
|
|
573
|
+
inherited from `auth-sdk-m8`, and made *fatal* only when pointed at production.
|
|
574
|
+
|
|
575
|
+
| Control | Consumer behaviour | Becomes fatal when |
|
|
576
|
+
|---|---|---|
|
|
577
|
+
| Config-health gate | `create_app` **auto-runs** `check_config_health` in its lifespan before the app signals ready — no manual wiring | any fatal check trips (raises `ConfigurationError`, app refuses to boot) |
|
|
578
|
+
| `ACCESS_REVOCATION_FAILURE_MODE` | `fail_closed` by default — an unverifiable revocation check returns **503**; `fail_open` is a conscious opt-out, logged loudly and counted (`revocation_check_failures_total{mode="fail_open"}`) | never fatal (it *is* the degradation policy) |
|
|
579
|
+
| `ALLOWED_HOSTS` | inherited from `CommonSettings`; unset = no host check (dev) | unset under `STRICT_PRODUCTION_MODE` (strict fatal); wildcard `*` under strict |
|
|
580
|
+
| `EVENT_SIGNING_ENABLED` / `EVENT_SIGNING_ACCEPT_UNSIGNED` | inherited secure defaults (signing on, unsigned rejected); the gate fires through the consumer's auto-run config-health (item 7.x.1) | `ENABLED=false` under strict; `ACCEPT_UNSIGNED=true` under production or strict |
|
|
581
|
+
| `METRICS_SCRAPE_CREDENTIAL` | unset = `/metrics` relies on network isolation only; set = constant-time `Authorization: Bearer` gate. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (network-isolation is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
|
|
582
|
+
| `HEALTH_DETAIL_CREDENTIAL` (items 9.3 + 9.4) | unset = `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness; real status never leaked); set = `X-Internal-Token` must match to receive the real aggregate status, per-check breakdown, and correct HTTP code. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (no credential = constant liveness response, which is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
|
|
583
|
+
| `INTERNAL_CLIENT_ID` (item 9.1) | unset = legacy single `PRIVATE_API_SECRET`; set = per-consumer bootstrap / service-token auth on private calls | never fatal (must be coordinated with the issuer's `PRIVATE_API_CONSUMERS`) |
|
|
584
|
+
| `_FILE` secret mounts | **inherited** from `CommonSettings` — every secret (`PRIVATE_API_SECRET_FILE`, `DB_PASSWORD_FILE`, `METRICS_SCRAPE_CREDENTIAL_FILE`, `HEALTH_DETAIL_CREDENTIAL_FILE`, …) can be sourced from `/run/secrets/*` with no code change | a referenced `<FIELD>_FILE` path is missing (fails closed at construction) |
|
|
585
|
+
|
|
525
586
|
---
|
|
526
587
|
|
|
527
588
|
## API Reference
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
`fastapi-m8` is the FastAPI **consumer** framework for the m8 stack. Its runtime
|
|
4
|
+
security posture is inherited from `auth-sdk-m8` (RS256 + strict `iss`/`aud`
|
|
5
|
+
validation, signed events, fail-closed revocation, the response-hardening layer)
|
|
6
|
+
and wired in by `create_app` / `build_auth_deps`. This file documents only the
|
|
7
|
+
consumer-specific transport guidance; the canonical, fleet-wide controls live in
|
|
8
|
+
the **[`auth-sdk-m8` `SECURITY.md`](https://github.com/mano8/auth-sdk-m8/blob/main/SECURITY.md)**.
|
|
9
|
+
|
|
10
|
+
## Private calls a consumer makes
|
|
11
|
+
|
|
12
|
+
A `fastapi-m8` consumer reaches `fa-auth-m8` over the **private API** only:
|
|
13
|
+
|
|
14
|
+
- JTI-status / revocation introspection (`{issuer}/private/v1/jti-status`),
|
|
15
|
+
- the optional auth event stream (`{issuer}/private/v1/events/stream`),
|
|
16
|
+
- the optional short-TTL service-token exchange (`{issuer}/private/v1/service-token`).
|
|
17
|
+
|
|
18
|
+
These are authenticated at the **app layer** by the per-consumer credential model
|
|
19
|
+
(item 9.1): `X-Internal-Client` + `X-Internal-Token`, or an exchanged
|
|
20
|
+
`Authorization: Bearer` service token, with a legacy single `PRIVATE_API_SECRET`
|
|
21
|
+
fallback. See the README's *Per-consumer internal auth* section.
|
|
22
|
+
|
|
23
|
+
## Service identity and mTLS (multi-host deployments)
|
|
24
|
+
|
|
25
|
+
The app-layer token/credential check is always the **primary** control. For the
|
|
26
|
+
transport beneath it, follow the canonical
|
|
27
|
+
[**"Service identity and mTLS"**](https://github.com/mano8/auth-sdk-m8/blob/main/SECURITY.md#service-identity-and-mtls-multi-host-deployments)
|
|
28
|
+
section of `auth-sdk-m8`'s `SECURITY.md` — it carries the Traefik internal-entrypoint
|
|
29
|
+
client-cert reference config, the CA/cert generation steps, and the service-mesh
|
|
30
|
+
alternative. From the **consumer** side:
|
|
31
|
+
|
|
32
|
+
- **Single trusted Docker host.** Internal `http://` between the consumer and
|
|
33
|
+
`fa-auth-m8` is acceptable — the Docker network is the isolation boundary. This is
|
|
34
|
+
warned, not blocked, in production (`ALLOW_INTERNAL_HTTP`); `local` is unrestricted.
|
|
35
|
+
- **Multi-host / untrusted network.** The container network no longer provides
|
|
36
|
+
kernel isolation, so add **mTLS** on the path to the issuer's private entrypoint:
|
|
37
|
+
the consumer presents a client certificate that Traefik (or a service-mesh sidecar)
|
|
38
|
+
verifies (`RequireAndVerifyClientCert`). Mount the consumer's client cert/key and
|
|
39
|
+
the CA into the container per the auth-sdk-m8 reference config.
|
|
40
|
+
- **Defense in depth, not a replacement.** Keep the `X-Internal-Token` /
|
|
41
|
+
`X-Internal-Client` / service-token app-layer check enabled alongside mTLS: if cert
|
|
42
|
+
rotation lapses the token check still gates access, and if a token leaks over an
|
|
43
|
+
unencrypted hop mTLS encrypts the channel. Internal HTTPS is **not** a blanket
|
|
44
|
+
mandate — mTLS is the *multi-host* transport control.
|
|
45
|
+
|
|
46
|
+
## Reporting a vulnerability
|
|
47
|
+
|
|
48
|
+
Report security vulnerabilities privately through GitHub's **Security** tab on this repository —
|
|
49
|
+
**"Report a vulnerability"** — which opens a private security advisory visible only to the
|
|
50
|
+
maintainers. Do not open a public GitHub issue for vulnerabilities. Expected response within 48 h.
|
|
@@ -50,6 +50,14 @@ from fastapi_m8._health import (
|
|
|
50
50
|
HealthCheckResult,
|
|
51
51
|
HealthStatus,
|
|
52
52
|
)
|
|
53
|
+
|
|
54
|
+
# Tier 1 — per-consumer internal-auth for private calls (Phase 9.1)
|
|
55
|
+
from fastapi_m8._internal_auth import (
|
|
56
|
+
InternalAuthProvider,
|
|
57
|
+
ServiceTokenInternalAuth,
|
|
58
|
+
build_internal_auth,
|
|
59
|
+
derive_service_token_url,
|
|
60
|
+
)
|
|
53
61
|
from fastapi_m8._version import __version__
|
|
54
62
|
from fastapi_m8.config import ConsumerServiceSettings
|
|
55
63
|
|
|
@@ -64,6 +72,11 @@ __all__ = [
|
|
|
64
72
|
"create_db_engine",
|
|
65
73
|
"DbEngine",
|
|
66
74
|
"ConsumerServiceSettings",
|
|
75
|
+
# Tier 1 — per-consumer internal-auth
|
|
76
|
+
"build_internal_auth",
|
|
77
|
+
"InternalAuthProvider",
|
|
78
|
+
"ServiceTokenInternalAuth",
|
|
79
|
+
"derive_service_token_url",
|
|
67
80
|
# Tier 1 — auth event stream
|
|
68
81
|
"build_event_stream_client",
|
|
69
82
|
"AuthEventStreamClient",
|