Skip to content

feat(broker): a pushing forge credential finally has a compliant home (catalogue v1.8.0) - #2277

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feat/persona-push-credential
Aug 2, 2026
Merged

feat(broker): a pushing forge credential finally has a compliant home (catalogue v1.8.0)#2277
rubenvdlinde merged 1 commit into
developmentfrom
feat/persona-push-credential

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Part of hydra's builder-write-access-hardening, task 1 — the credential.

The gap

git push needs a credential, not a proxied API call. git speaks the
smart-HTTP pack protocol, so there is no single request for the broker to make on
the caller's behalf and no header for it to substitute.

The host-locked github entry therefore returns null from
resolveInjectable() by design — and that null is a routing signal
meaning "use request()", not a denial. But request() cannot express a push at
all. So a stage that needs to push has had no compliant credential home: either
the secret sits somewhere it should not, or the push cannot happen.

What this adds

github-pushinject_only, no baseUrl, no allowRules. Exactly the shape
anthropic-cli already uses and for the same stated reason: the broker cannot
bound this call, so it refuses to make it and the guarded resolveInjectable()
path applies instead.

⚠️ A separate entry, not github flipped to inject_only

That would have been the one-line version of this change, and it would have
silently widened every existing github credential in the fleet from "its
secret never leaves OpenRegister" to "its secret is handed to the calling app".

Two entries keeps the pushing credential greppable, reviewable and revocable on
its own. testTheGithubPushCredentialIsInjectOnlyAndTheProxyEntryIsNot() asserts
both halves, so the shortcut cannot be taken later by accident.

What the catalogue cannot enforce, written where it will be read

The $comment is long on purpose — it is the only place whoever mints the token
will look. In short: a fine-grained PAT (or better, a GitHub App installation
token, which expires in an hour) scoped to one repository, Contents: Read and write, and explicitly not Workflows. A credential that can edit
.github/workflows obtains code execution on the forge's runners and escapes
every other control around it. Not a classic PAT — its repo scope is
account-wide and carries workflow write.

It also names the probe that asserts the absence rather than assuming it:
attempt PUT /repos/{owner}/{repo}/contents/.github/workflows/probe.yml and
require the refusal refusing to allow a Personal Access Token to create or update workflow ... without 'workflows' scope. A 201 means revoke, do not store.

The compensating controls live where the credential is used — see
ConductionNL/hermiq#118: the sidecar refuses a push to another repository, to any
branch that is not feature/<issue>/*, or with a diff touching workflows, a
dependency manifest or anything outside the issue's scope; and it withholds the
credential from the process that reads repository content, so none of it depends
on a model's compliance. The forge's branch rulesets are the backstop.

Both tests mutation-checked

mutation result
rename github-push away testTheGithubPushCredentialIsInjectOnlyAndTheProxyEntryIsNot fails
re-add a workflow-dispatch allow-rule to github testNoProviderCanReachAWorkflowDefinition fails, naming the rule

That second grant existed once (#2240) and was reverted (#2242). Nothing until
now would have caught it coming back.

29 tests, 512 assertions, green.

… (v1.8.0)

`git push` needs a CREDENTIAL, not a proxied API call. git speaks the smart-HTTP
pack protocol, so there is no single request for the broker to make on the
caller's behalf and no header for it to substitute. The host-locked `github`
entry therefore returns null from `resolveInjectable()` BY DESIGN — a routing
signal meaning "use request()", not a denial — but `request()` cannot express a
push at all. So a stage that needs to push has had no compliant credential home:
either the secret sits somewhere it should not, or the push cannot happen.

`github-push` is that home. inject_only, no baseUrl, no allowRules — exactly the
shape `anthropic-cli` already uses, and for the same reason: the broker cannot
bound this call, so it refuses to make it and the guarded `resolveInjectable()`
path applies instead.

⚠️ A SEPARATE ENTRY, not `github` flipped to inject_only. That one-line version
would have silently widened EVERY existing github credential in the fleet from
"its secret never leaves OpenRegister" to "its secret is handed to the calling
app". Two entries keeps the pushing credential greppable, reviewable and
revocable on its own — and the test asserts both halves, so the shortcut cannot
be taken later by accident.

What the catalogue CANNOT enforce is written down where whoever mints the token
will read it: a fine-grained PAT (or better, a GitHub App installation token,
which expires in an hour) scoped to ONE repository, `Contents: Read and write`,
and explicitly NOT `Workflows` — a credential that can edit `.github/workflows`
obtains code execution on the forge's runners and escapes every other control
around it. Not a classic PAT, whose `repo` scope is account-wide and carries
workflow write. The $comment names the probe that ASSERTS the absence rather
than assuming it (attempt a write under `.github/workflows/` and require the
refusal; a 201 means revoke, do not store).

The compensating controls live where the credential is used — the hermiq sidecar
refuses a push to another repository, to any branch that is not
`feature/<issue>/*`, or with a diff touching workflows, a dependency manifest or
anything outside the issue's scope, and withholds the credential from the process
that reads repository content. The forge's branch rulesets are the backstop.

Two tests, both mutation-checked:

  - renaming `github-push` away  -> the first test fails;
  - re-adding a workflow-dispatch allow-rule to `github` -> the second fails,
    naming the rule. That grant existed once (openregister#2240) and was reverted
    (#2242); nothing until now would have caught it coming back.

29 tests, 512 assertions, green.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ eba138c

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 174/174
npm ✅ 713/713
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-08-02 15:48 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit e7e85c6 into development Aug 2, 2026
20 of 25 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/persona-push-credential branch August 2, 2026 16:06
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