Skip to content

feat: recognize age-encrypted values inside readable files - #33

Draft
pungoyal wants to merge 1 commit into
mainfrom
feat/age-values-secrets
Draft

pungoyal wants to merge 1 commit into
mainfrom
feat/age-values-secrets

Conversation

@pungoyal

Copy link
Copy Markdown
Owner

What and why

dotsync should work alongside whatever tools people already use (mise, GNU Stow, sops and others), so it recognizes encrypted formats and never names a tool.

  • New format in cipherFormats: age values embedded in a readable file. A run of base64 counts as ciphertext when it decodes to an age file, either directly or inside a zstd frame. This covers both of the forms mise writes for age-encrypted env vars ({ age = "…" } and { age = { value = "…", format = "zstd" } }), and it works the same in YAML, JSON and dotenv. Such a file is treated like a sops file: the path rules don't apply, the encrypted values are skipped, and every other line, plus the rest of each value's line, is still scanned. As a result, a file like conf.d/secrets.toml, or token: '<age base64>', now syncs instead of being blocked.
  • cipherFormat.values is now a func rather than a regexp, because age values have to be decoded to be recognized.
  • Docs.
    • The secrets guide gains a tool-neutral "Encrypt secrets that should sync" section: recipes for mise, sops and age, consuming secrets as env vars, one key per person kept in a password manager, and rotating secrets if the key leaks.
    • The mise guide shows current commands (mise set -g --age-encrypt). It also corrects a claim that a missing key fails silently: mise reports an error unless age.strict=false.
    • The secret-rules reference and security model are updated to match.

dotsync still doesn't encrypt anything itself, and it has no hooks, so "dotsync never runs commands" still holds.

How was this tested?

  • scenario test added or updated (internal/dotsync/dotsync_test.go): table cases for plain and zstd mise values, dotenv and YAML embeddings, plaintext on the same or other lines, and base64 that isn't age (plain, zstd, fake header). TestEncryptedSecretsSyncInManagedDirectory now also syncs a conf.d/secrets.toml holding age values. A mutation check (detector forced to false) makes 10 cases fail.
  • Checked against real mise set --age-encrypt output (mise 2026.9.12), both forms. Confirmed that mise decrypts it, and that it errors without a key.
  • go vet, go test -race, gofmt and golangci-lint are clean, and the docs site builds.
  • docs in website/src/content/docs/ updated if behavior changed
  • this change cannot lose a local modification without a backup, or resolve a conflict silently (it only changes which uploads the secret rules allow)

🤖 Generated with Claude Code

Files that embed age ciphertext as base64 values, plain or zstd-compressed,
are now recognized as encrypted, in any text format. This is how mise
stores age-encrypted environment variables. As with sops, the path rules
don't apply to such a file, the ciphertext values are skipped, and the rest
of the file is still scanned. Detection decodes each value, so no tool is
named.

The secrets guide now covers syncing secrets encrypted with the tool you
already use (mise, sops or age), consuming them as environment variables,
using one key per person, and rotating secrets if the key leaks. The mise
guide no longer says a missing key fails silently: mise errors unless
age.strict is false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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