Skip to content

Vault every non-public alert-method field instead of a named list (#85) - #87

Merged
mattmacrocket merged 1 commit into
mainfrom
fix/vault-nonpublic-alert-fields
Oct 22, 2026
Merged

mattmacrocket merged 1 commit into
mainfrom
fix/vault-nonpublic-alert-fields

Conversation

@mattmacrocket

Copy link
Copy Markdown
Collaborator

Closes #85.

Problem

api/config_secrets.py is fail-closed: every alert-method field not on ALERT_METHOD_PUBLIC_FIELDS is redacted on GET. config_vault.py was the opposite: it enumerated the secret field names it knew (webhook_url, relay_token, device_token, and since #82 signing_secret). A secret stored under any other name was hidden from the API and written in cleartext to config.yaml.

Fix

  • One allow-list: config_fields.ALERT_METHOD_PUBLIC_FIELDS (enabled, include_device_info, min_severity, relay_url), re-exported from config_secrets so existing imports keep working. A test asserts both modules reference the same object.
  • The vault treats every string-valued alert-method field not on that list as a credential: hydrated from the store at startup, migrated into the store if found in plaintext, synced on PUT, and stripped from the persisted YAML.
  • The YAML keeps __squirrelops_vaulted__ in place of each vaulted value so hydration knows which fields to fetch without the vault keeping an index. The marker is never stored as a secret; a marker with nothing behind it is dropped rather than passed to a handler.
  • Installs written by older releases, which popped the known field names from the YAML outright, still hydrate: those four names are probed as before.
  • Non-string unknown fields (retries: 3) are settings, not credentials, and stay in the YAML. The API boundary's redaction of them is unchanged.

Tests

Red first, then green: marker on strip and nothing secret left in the clean copy; a future smtp_password round-trips through hydrate, strip, reload; legacy YAML without the field still hydrates webhook_url; the marker itself is never stored; shared allow-list identity. Two existing strip assertions moved from "key absent" to "key holds the marker". Full sensor suite: 2614 passed, 6 skipped; ruff clean.

SECURITY_MODEL "Stored secrets" updated. Control surface only.

The API boundary was fail-closed: any alert-method field not on the public
allow-list came back redacted. The vault enumerated the secret names it
knew, so a secret under any other field name was hidden on read yet written
in cleartext to config.yaml. The two layers now share one allow-list
(config_fields.ALERT_METHOD_PUBLIC_FIELDS, re-exported by config_secrets):
every string field not on it is a credential to both.

The persisted YAML keeps a marker in place of each vaulted value so startup
hydration knows which fields to fetch without the vault holding an index.
Installs written by older releases, which popped the known field names
outright, still hydrate because those names are probed as before. A marker
with nothing behind it is dropped rather than handed to a handler. Non-string
unknown fields are settings and stay in the YAML.

Red first: five new vault tests (marker on strip, round trip through the
vault, legacy YAML hydration, marker never stored, shared allow-list). Full
sensor suite 2614 passed.
@mattmacrocket
mattmacrocket merged commit 5b50bb8 into main Oct 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vault enumerates alert-method secret field names, so a new secret field is redacted on read but persisted in cleartext

2 participants