docs(01-09): document security contract changes

- define explicit MCP read scope boundaries and upgrade handling
- document Nightscout private-origin pinning and reset revocation
This commit is contained in:
Marc-André Bombeck committed 2026-07-29 14:08:37 +02:00
1 parent ddbfcb4588
commit 9ea0cb867f
3 files changed
+155

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
# Nightscout integration
HealthLog can pull continuous-glucose readings from a Nightscout instance.
Public Nightscout origins work without an allowlist. Access to a private or
local destination is an operator decision and is denied unless its exact
origin is listed in `NIGHTSCOUT_PRIVATE_ORIGINS`.
## Security change for upgrades
`NIGHTSCOUT_PRIVATE_ORIGINS` is a breaking security boundary. The old
per-user “allow private host” opt-in is compatibility metadata only: it no
longer grants private-network access. Existing private connections that are
not operator-approved stop syncing with the stable
`private_origin_not_approved` reason until the operator configures the exact
origin.
Set a comma-separated list of complete origins:
```env
NIGHTSCOUT_PRIVATE_ORIGINS="https://cgm.internal.example:443,http://10.20.30.40:1337"
```
Each entry is an exact normalized `scheme://host:port` trust unit:
- only `http` and `https` are accepted;
- scheme, hostname, and effective port must all match;
- paths, query strings, fragments, embedded credentials, and wildcards are
rejected;
- approving one hostname does not approve subdomains, sibling hosts, or a
different port; and
- any malformed non-empty entry fails evaluation instead of silently
weakening the configured policy.
Do not put a Nightscout API token, capability URL, health data, or a URL
containing credentials in the allowlist. Restart the application after
changing the environment.
## Connect safely
Open **Settings → Integrations → Nightscout** and enter the instance base URL
and, when required, its Nightscout API token. HealthLog stores the connection
credentials encrypted. A public, read-only Nightscout instance can omit the
token.
For a private instance, configure the operator allowlist first and enter the
same origin in Settings. For example, if the allowlist contains
`https://cgm.internal.example:443`, these are not equivalent and remain
blocked:
```text
http://cgm.internal.example:443
https://cgm.internal.example:8443
https://other.cgm.internal.example:443
```
The user-controlled private-host checkbox or request field cannot override
this decision.
## DNS pinning and redirects
Every outbound request re-evaluates the stored origin against current server
policy. For public destinations, HealthLog resolves the hostname, rejects
private and embedded-private IPv4/IPv6 answers, and pins the approved address
for the connection so DNS cannot change between validation and use. Redirects
are validated and pinned again; a public URL cannot redirect into the local
network.
An exact private origin is intentionally allowed to resolve privately only
because the operator named that complete origin. Removing it from
`NIGHTSCOUT_PRIVATE_ORIGINS` takes authority away on the next fetch even if an
older user preference remains stored.
## Upgrade checklist
1. Inventory private Nightscout connections without copying their tokens into
tickets or logs.
2. Add only the exact origins that HealthLog must reach.
3. Restart HealthLog so the environment is reloaded.
4. Test the connection from Settings and confirm a sync succeeds.
5. Remove obsolete origins promptly; do not use a broad DNS suffix or wildcard
as a substitute for explicit entries.
If a previously working private connection reports that operator action is
required, compare its exact scheme, host, and port with the allowlist. Do not
restore access by changing the user's legacy opt-in.
+60
View File
@@ -0,0 +1,60 @@
# Security recovery and credential revocation
A password reset is an account-recovery event, not just a password-hash
change. The operator reset CLI revokes every access path for the target
account so a stolen session, native refresh token, long-lived API token,
trusted device, or temporary step-up proof cannot survive the new password.
Use the operator procedure in [Password reset](password-reset.md) and prefer
its hidden stdin prompt:
```sh
docker compose exec app node scripts/reset-password.mjs <username-or-email>
```
Do not put the new password in shell history, a ticket, chat, or logs.
## What a successful operator reset revokes
The CLI performs one serialized database transaction for exactly one matched
user. It:
1. locks the target account and writes the new password hash;
2. deletes every web session;
3. revokes every API access token;
4. revokes every native refresh token;
5. deletes every trusted device; and
6. deletes every live step-up elevation.
The safe completion message reports only the username and affected row counts.
It never prints the password, password hash, tokens, cookies, or health data.
Unrelated users are not changed.
The in-app administrative reset follows the same target-user revocation
policy for sessions, API tokens, native refresh tokens, and trusted device
records. Operators should still use the CLI recovery path when rollback-safe
direct recovery is required.
## Rollback guarantee
The operator CLI wraps identity resolution, password rotation, and every
revocation above in `BEGIN`/`COMMIT`. Any error issues `ROLLBACK` before the
database connection closes. A failed reset therefore does not leave a new
password paired with old credentials, or old password state paired with only
some credentials revoked.
Treat any non-zero exit as “nothing committed.” Correct the operational
problem and run the whole command again; do not manually complete a subset of
the SQL statements.
## After recovery
- Have the user sign in with the new password and enroll replacement trusted
devices intentionally.
- Expect every browser and native client to require a fresh login.
- Reissue only the minimum API or MCP scopes each integration needs.
- Review security activity without copying credential material into the
incident record.
- If compromise is suspected beyond one account, rotate the affected
instance-level secrets using the relevant operator runbooks before
restoring integrations.
+10
View File
@@ -70,6 +70,16 @@ scope — it is the least-privilege MCP scope and nothing more, and it is
bound to the MCP surface: it is refused on every REST route. The same
list shows last-used time and lets you revoke any token.
MCP reads now require an explicit MCP read capability. A token must carry
`health:read` (or the legacy/native `*` wildcard) before the server registers
or invokes any read tool. `health:write`, `fhir:read`,
`medication:ingest`, notification scopes, and every other unrelated scope do
not imply `health:read`; presenting one of those narrow tokens to `/mcp` is
rejected before health data is accessed. Likewise, `health:read` does not
grant a regular REST read or any write. When upgrading, check any manually
provisioned connector token and mint a replacement with `health:read` instead
of broadening an unrelated credential.
Cloud connectors that use OAuth (next section) mint the same
`health:read` scope automatically, so for Claude.ai / ChatGPT you do not
need to mint a token by hand — the manual token is for the stdio / power-