Skip to content

feat(repo-settings): release environment for the ccf-release-bot secrets - #78

Merged
gusfcarvalho merged 3 commits into
mainfrom
ra/sec-release-environment
Oct 8, 2026
Merged

gusfcarvalho merged 3 commits into
mainfrom
ra/sec-release-environment

Conversation

@gusfcarvalho

Copy link
Copy Markdown
Contributor

Stacked on #77 (base branch ra/sec-repo-settings-hardening). From the security review of workflows v1.2.0 and the mock repos.

Merging this moves nothing by itself. The secrets move when an admin runs repo-settings with apply, following the README cut-over steps (below).

Why

  • The release-bot key is readable from any branch. RELEASE_BOT_PRIVATE_KEY is an org secret visible to workflows and all 10 mocks, with no environment, so any workflow on any branch of those repos can read it, and so can a same-repo PR's. That is the key of an app installed on every org repo, which is also the ccf-review bypass actor. At go-live, secrets: inherit would spread it to every product repo.
  • The "Check the token scope" steps don't contain it. They only bind the workflow files that run them.
  • REPO_ADMIN_PRIVATE_KEY has the same exposure in workflows. Only steps inside repo-settings.yml guard it.

Environment secrets fix this. An environment secret only reaches a job that names the environment and runs on a ref the environment allows. A workflow pushed to a branch, or a PR's, is refused before it starts.

What

repo-settings manages a release environment on every manifest repo:

  • Deployment policies. Custom ones only: the default branch and the tags v*.*.* and *-v*.*.*. Only the bot can create those tags (ccf-release-tags, from the base PR). Any other policy is deleted.
  • Secrets. RELEASE_BOT_APP_ID and RELEASE_BOT_PRIVATE_KEY are written when missing, or all of them with --rotate-secrets (new workflow input rotate-secrets). Values are encrypted with the environment's public key (nacl/box sealed box, golang.org/x/crypto is now a direct dependency) and never printed.
  • Order. The environment is created or switched to custom policies, then the policies are deleted or added, then the secrets are written. A secret is never written if a policy step failed.
  • Not managed. The environment's reviewers and wait timer.

Jobs that mint a ccf-release-bot token now name the environment (environment: release):

  • The callers' release-please, cut-prerelease and release-finished.
  • This repo's train, renovate, ccf-bump-sync, ccf-bump-merge, attention-digest and vuln-summary.
  • The RELEASE_BOT_* workflow_call secrets are now required: false, so callers keep secrets: inherit and need no change. An environment secret wins over an inherited one.

repo-settings.yml:

  • Its job runs in a repo-admin environment, which you create by hand once: main only, admins as required reviewers.
  • That environment holds REPO_ADMIN_* and the RELEASE_BOT_* values to copy; only an apply gets the latter.
  • The token adds permission-actions: read and permission-environments: read|write.
  • ccf-repo-admin can now keep fixed permissions (Administration RW, Environments RW, Actions R), so a future "onboard a repo" dispatch can run end to end.

Elsewhere:

  • internal/ciworkflows: new TestAppKeysOnlyInEnvironments. Every job that reads an app private key must name the environment holding it. I checked that the test fails when one job drops its environment:.
  • README, "Repo settings": the environments, the app permissions and the cut-over steps. Smaller doc updates where the secrets are mentioned.

Tests

  • environment_test.go (reposettings):
    • create, then idempotent;
    • an environment open to all branches is tightened, a stray policy is deleted, and a present secret is kept;
    • rotation;
    • a failed policy skips the secrets;
    • a missing value fails;
    • the value never appears in the output.
  • TestGitHubEnvironment: every environment, policy and secret call. The secret is decrypted with the test keypair to check the sealed box. A 403 gets a permission hint.
  • cmd/repo-settings: the end-to-end apply writes the environment, its 3 policies and its 2 secrets, after the other settings.

Gate: go vet ./..., go test ./..., gofmt -l . and actionlint pass. It also merges cleanly with the other three security PRs, and they pass together.

Cut-over (README, "Repo settings")

  1. Grant ccf-repo-admin Administration RW, Environments RW and Actions R. Create workflows' repo-admin environment with the 4 secrets, using a newly generated release-bot private key.
  2. Merge. Until a repo's release environment exists, GitHub creates an empty one on first use and the job still gets the org secret, so nothing breaks in between.
  3. Apply repo-settings on repos.mock.yaml and check a mock release end to end, then on repos.yaml.
  4. Remove the org secrets RELEASE_BOT_* and REPO_ADMIN_*, then delete the old private keys from both apps.

Notes

  • SLACK_BOT_TOKEN stays an org secret. notify-failure.yml needs it on PR runs.
  • This repo's scheduled tools only run from main now. A dispatch from another branch is refused.
  • The hourly train reconcile records a deployment in workflows' release environment each run. It's harmless, but noisy.

🤖 Generated with Claude Code

RELEASE_BOT_PRIVATE_KEY was an org secret visible to the workflows repo and
every mock, so any workflow on any branch of those repos (or a same-repo
PR's) could read the key of an app installed on every org repo. The
in-workflow "Check the token scope" steps only bound workflows that run them.
REPO_ADMIN_PRIVATE_KEY had the same exposure in the workflows repo, guarded
only by steps inside repo-settings.yml.

- repo-settings manages an environment `release` on every manifest repo:
  custom deployment policies (the default branch and the v*.*.* / *-v*.*.*
  release tags, which only the bot creates; other policies are deleted) and
  the secrets RELEASE_BOT_APP_ID and RELEASE_BOT_PRIVATE_KEY, written when
  missing (or all with --rotate-secrets) after the policies are in place,
  encrypted with the environment's public key (nacl sealed box). A secret
  is never written to an environment whose policy steps failed.
- Every job that mints a ccf-release-bot token names the environment:
  release-please, cut-prerelease and release-finished (the caller's), and
  this repo's train, renovate, ccf-bump sync/merge, attention digest and
  vuln summary (main only). The workflow_call secrets are optional, so
  callers keep `secrets: inherit` and the environment secret wins.
- repo-settings.yml runs in a `repo-admin` environment (main only,
  required reviewers; set up by hand) holding the ccf-repo-admin key and
  the release-bot values it copies, which only an apply receives. Its token
  adds Actions read and Environments read/write, so ccf-repo-admin can keep
  fixed permissions instead of being raised to write for each apply.
- internal/ciworkflows: every job that reads an app private key names the
  environment that holds it.
- README: the environments, the app's permissions and the cut-over steps.

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

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: b2d22982-9ba3-4318-886d-92d31946e1a5
📥 Commits

Reviewing files that changed from the base of the PR and between 2a9b345 and 91ab4ae.

📒 Files selected for processing (34)
  • .github/workflows/attention-digest.yml
  • .github/workflows/ccf-bump-merge.yml
  • .github/workflows/ccf-bump-sync.yml
  • .github/workflows/ci.yml
  • .github/workflows/cut-prerelease.yml
  • .github/workflows/release-action.yml
  • .github/workflows/release-finished.yml
  • .github/workflows/release-go-image.yml
  • .github/workflows/release-go-lib.yml
  • .github/workflows/release-go-plugin.yml
  • .github/workflows/release-helm.yml
  • .github/workflows/release-please.yml
  • .github/workflows/release-policies.yml
  • .github/workflows/release-ui.yml
  • .github/workflows/renovate.yml
  • .github/workflows/repo-settings.yml
  • .github/workflows/train.yml
  • .github/workflows/vuln-summary.yml
  • README.md
  • cmd/repo-settings/main.go
  • cmd/repo-settings/main_test.go
  • docs/ccf-bump.md
  • docs/renovate.md
  • docs/train.md
  • docs/vuln-summary.md
  • go.mod
  • internal/ciworkflows/environment_test.go
  • internal/ciworkflows/steps_test.go
  • internal/reposettings/desired.go
  • internal/reposettings/environment_test.go
  • internal/reposettings/github.go
  • internal/reposettings/github_test.go
  • internal/reposettings/plan.go
  • internal/reposettings/plan_test.go
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

gusfcarvalho and others added 2 commits October 8, 2026 18:25
A called workflow whose jobs all skip reports skipped, not success, so the
required job failed on every PR that isn't a release-please PR (where
release-checks skips). Skipped is accepted for release-checks only; any
other result but success still fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gusfcarvalho
gusfcarvalho changed the base branch from ra/sec-repo-settings-hardening to main October 8, 2026 21:26
@gusfcarvalho
gusfcarvalho merged commit 9b8311e into main Oct 8, 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.

1 participant