Skip to content

feat(aliases): encrypt component alias values at rest#152

Open
uittenbroekrobbert wants to merge 1 commit into
mainfrom
claude/alias-encryption
Open

feat(aliases): encrypt component alias values at rest#152
uittenbroekrobbert wants to merge 1 commit into
mainfrom
claude/alias-encryption

Conversation

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Why

Component aliases are stored in the project YAML, but their values were kept in plaintext. Aliases are meant to be \$VAR templates, yet users paste literal secrets (e.g. database passwords) into them, so those secrets sat unencrypted in the zad-projects repo.

This makes alias values behave exactly like user-env-vars: encrypted at rest with the project AGE key, decrypted on use, with a plaintext fallback so nothing breaks.

What

Read (decrypt, with plaintext fallback)

  • _collect_deployment_aliases decrypts each alias value at deploy time before categorization/substitution. Gated on is_age_encrypted, so plaintext passes through unchanged; the project key is decoded lazily (only when an encrypted value is present).
  • KeyValueConverter._maybe_decrypt now recurses into the alias dict so the editor shows decrypted values.

Write (encrypt on save — the three places user-env-vars already encrypts)

  • New ComponentAliasesEncryptGenerator (catch-all on save), registered after the AGE keypair generator.
  • KeyValueConverter.write dict mode encrypts each value.
  • build_component_config (create / add-component API) encrypts alias values.
  • router_wizard._apply_literal_scalars literalizes alias AGE blocks for clean YAML block-scalar output.

Alias names stay readable; only values are encrypted. Already-encrypted values are skipped (idempotent, no ciphertext churn).

No schema migration

Existing plaintext aliases keep resolving via the read fallback and encrypt lazily on the next save, exactly like user-env-vars. Nothing to migrate.

Behavioural notes

  • First save of a project with aliases rewrites those values as AGE blocks in zad-projects — a one-time diff per project.
  • Decrypting before the cross-component conflict check also fixes a latent false-conflict: two components declaring the same alias previously compared non-deterministic ciphertext.

Verification

  • Full project-file lifecycle on a real YAML file with real AGE crypto (incl. the global→project key indirection): plain → resave (encrypts each value) → reload → decrypt back to exact originals (deploy path + editor path), and idempotent resave with no churn.
  • Unit tests updated (the old "dict mode never encrypts" contract) and added coverage for dict encrypt/decrypt + the new generator.
  • ruff check / ruff format / pyright clean; full pre-push unit suite green (3971 passed).

…t AGE key

Alias values can hold secrets (e.g. a password) but were stored in plaintext.
Mirror the user-env-vars mechanism, per alias value:

- read: decrypt alias values at deploy time in _collect_deployment_aliases and
  in the KeyValueConverter editor path. Plaintext values pass through unchanged,
  so existing files keep working with no schema migration.
- write: encrypt each value on save via a new ComponentAliasesEncryptGenerator,
  the KeyValueConverter dict mode, and build_component_config. Already-encrypted
  values are skipped (idempotent, no ciphertext churn on unchanged aliases).

Alias names stay readable; only values are encrypted. Existing plaintext aliases
encrypt lazily on the next save. Decrypting before the cross-component conflict
check also fixes a latent false-conflict on identical aliases (AGE ciphertext is
non-deterministic).
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.

1 participant