Connectors: allow secrets to be entered in the UI, encrypted at rest
ci / build-test (push) Successful in 1m12s
ci / build-test (push) Successful in 1m12s
Reference-only secrets (SDD §6.4) meant adding a connector required editing a file on the server and restarting the service. In practice that leads to the token being pasted into the env-var *name* field, which fails with "environment variable '<token>' is not set" and gives no hint what went wrong. Add a second storage form, chosen per connector: type the secret in and it is encrypted via ASP.NET Core data protection before it is stored. The env-var reference stays as an equal alternative — this widens the choice rather than replacing it. Exactly one form survives a save, so a stale secret cannot linger and silently win; EndpointSecret.Resolve is the single resolution path. The guarantee that matters is preserved: no plaintext in the database, so pg_dump and JSON exports carry nothing usable. The trust boundary is stated plainly in §6.4 — the key ring is on disk, so this protects against leaked database content, not an attacker who already has the host, which is the same boundary an env var has. Details worth noting: - Key ring defaults to /var/lib/metervault/keys, outside the app directory, because the LXC updater republishes /opt/metervault on every update. Docker gets a named volume. Overridable via MeterVault__DataProtectionKeyPath. - Undecryptable ciphertext (key ring lost) falls back rather than throwing: an ingestion worker on a timer should degrade, not crash. - The stored secret is never sent to the browser; a blank field means "unchanged", not "cleared". - MQTT usernames are stored as-is — §6.4 covers tokens and passwords, and encrypting a username would only blank the field on every edit. - ExportService drops *_enc values: bound to the originating key ring, so useless where an export would be restored. Expect to re-enter after a restore. - HaConnectionTester now takes a resolved token, so the admin UI can test a token that has been typed but not yet saved. SDD §6.4 and §9 updated to describe both forms rather than contradict the code. Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
This commit is contained in:
+9
-2
@@ -413,7 +413,14 @@ Normalized/rolled-up volumes are tiny regardless: daily consumption = 1000 × 36
|
||||
- Ship the four reference CSVs as built-in example imports and as test fixtures.
|
||||
|
||||
### 6.4 Secrets
|
||||
Tokens/passwords are **never** stored in plaintext in the DB. `ingestion_endpoint.config` holds a *reference* (env var name / Docker secret path); the app resolves at runtime. Document this clearly.
|
||||
Tokens/passwords are **never** stored in plaintext in the DB. Two storage forms satisfy this, chosen per connector in the admin UI:
|
||||
|
||||
- **By reference** — `ingestion_endpoint.config` names an env var / Docker secret path (`token_env`, `password_env`); the app resolves it at runtime.
|
||||
- **Encrypted at rest** — the operator types the secret into the connector dialog and it is stored encrypted (`token_enc`, `password_enc`) under the ASP.NET Core data-protection key ring.
|
||||
|
||||
Exactly one form survives a save; switching clears the other. The encrypted form exists because reference-only forced a file edit plus a service restart to add a connector, which in practice led to tokens being pasted into the env-var *name* field. It keeps the guarantee that matters — a `pg_dump` or JSON export carries nothing usable — but note the trust boundary: the key ring is on disk, so it protects against leaked database content, not against an attacker who already has the host. That is the same boundary as an env var, which is equally readable from `/proc`.
|
||||
|
||||
The key ring must be persisted outside the app directory (`MeterVault__DataProtectionKeyPath`, default `/var/lib/metervault/keys`), or a redeploy that replaces the content root will orphan every stored secret. MQTT *usernames* are not secrets and are stored as-is. JSON exports drop `*_enc` values: they are bound to the originating key ring and so are useless where an export would be restored — expect to re-enter secrets after a restore.
|
||||
|
||||
---
|
||||
|
||||
@@ -490,7 +497,7 @@ OpenAPI/Swagger published. API-key auth for automation endpoints; UI uses the se
|
||||
- **Time & DST:** store UTC; bucket and display in the instance timezone (default `Europe/Berlin`). "Daily cost" boundaries are local-midnight — use `time_bucket(..., 'Europe/Berlin')`.
|
||||
- **Auth:** optional built-in local accounts; **reverse-proxy trust** mode honouring `X-Forwarded-User`/`Remote-User` behind Authelia/Traefik; API keys for machine access. Default: single admin user + one ingest API key.
|
||||
- **Observability:** `/healthz`, structured logs (Serilog), optional Prometheus `/metrics`.
|
||||
- **Config:** environment variables + a settings UI; secrets via env/Docker secrets (never in DB plaintext).
|
||||
- **Config:** environment variables + a settings UI; secrets via env/Docker secrets or encrypted at rest (never in DB plaintext) — see §6.4.
|
||||
- **i18n:** `en` (default for OSS) + `de`; locale-aware number/currency/date. Ship a German locale that matches the source data conventions.
|
||||
- **Backup:** document `pg_dump`/Timescale backup; provide a full **JSON export/import** for portability.
|
||||
- **Performance:** dashboards read aggregates only; raw reads paginated and time-bounded.
|
||||
|
||||
Reference in New Issue
Block a user