Skip to content

Cross-repo/cross-org secret distribution via vault-style SOPS encryption #156

Description

@ineedjet

Problem

This repo already has a working "encrypt for a set of recipients, publish, decrypt on demand" mechanism (vaults/*.ymlencrypt-env action → .sops.env release assets → deploy/vault.py's decrypt_env) - but it's scoped narrowly to one use case: a deploy target's own age key decrypting an app's env during a deploy.

Meanwhile there's a real, recurring problem this same mechanism could solve but currently doesn't: sharing a credential (e.g. a Telegram bot token) across multiple repos/orgs. GitHub's own cross-repo secret sharing (organization secrets) only works within one org and needs a paid tier; there's no native GitHub mechanism at all for sharing a secret across orgs. The alternative is a third-party secrets manager SaaS, which is more infrastructure than this problem deserves. Today the actual workaround is manually re-entering the same credential into every consuming repo's own Secrets UI by hand, every time it's needed or rotated.

This isn't a deploy/Docker Compose problem - it's general env/secret reuse across repo and org boundaries, using nothing but git, GitHub Releases, SOPS, and age.

Proposal

Origin side (one canonical repo holding a secret's real value):

  • Holds the actual secret as a plain GitHub Secret, same as today.
  • Holds a registry of every consumer's public age key (same shape as this repo's existing keys/<name>.pub files, just repurposed: each entry is a consuming repo/org, not a deploy target).
  • On rotation (or on every release, mirroring the existing encrypt-vaults/encrypt job pattern), re-encrypts the secret for every registered recipient's public key and publishes one .sops.env per consumer as a release asset - generalizing encrypt-env from "recipients = this org's own deploy targets" to "recipients = anyone who registered a public key."

Consumer side (any repo, any org):

  • Registers its own public age key with the origin repo (e.g. via a PR adding a .pub file - the direction of key exchange is public keys flowing into the origin; encrypted secrets flow out to consumers, private keys never leave their own repo).
  • Pulls the release asset published for it specifically - already works today unmodified, since deploy/resolve.py's download_ref already supports owner/repo@tag:asset refs against any accessible repo, not just this one.
  • Decrypts it with its own private age key (a GitHub Secret local to the consumer repo) and merges the result into the current job's environment ($GITHUB_ENV) for later steps to use.

Open questions

  • Manifest shape for the recipient registry - one registry per secret, or one shared registry the origin applies to everything it holds?
  • Tooling for a consumer to bootstrap its own age keypair (this session did it by hand with age-keygen; worth scripting/documenting properly if this becomes a real pattern).
  • Package the consumer side as a composite action (.github/actions/) any repo can pull in directly, matching the existing encrypt-env/build-bundle pattern.
  • Whether this repo should dogfood it immediately - notify-telegram-pr.yml/notify-telegram-release.yml still read secrets.TELEGRAM_BOT_TOKEN/secrets.TELEGRAM_CHAT_ID directly today, which is exactly the pain this would remove first.

Related

Came up while setting up notify-telegram-release.yml (#152) and continually re-entering the same Telegram credentials by hand across different secret slots.

Metadata

Metadata

Assignees

No one assigned

    Labels

    🚩Temporary fast-track flag

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions