mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
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:
1 parent
ddbfcb4588
commit
9ea0cb867f
3 files changed
+155
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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-
|
||||
|
||||
Reference in new issue
Block a user