Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/fleet-converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ jobs:
WIF_ACCESS_TOKEN: ${{ steps.wif_auth.outputs.access_token }}
FLEET_CONVERGE_EXPECTED_HOST: ${{ github.event.inputs.host }}
if: github.event.inputs.mode == 'microvm_runner_group_ensure'
timeout-minutes: 19
timeout-minutes: 36
- name: Upload the microVM runner group ensure receipt
id: microvm_runner_group_ensure_receipt_upload
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
Expand Down
18 changes: 17 additions & 1 deletion dag/gunbc/auth/heal_publisher_federation.dag
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ type HealPublisherFederation {
service_account: ServiceAccountEmail
service_account_display_name: NonEmptyStr
provision_remedy: NonEmptyStr
runner_environment: String
}

data heal_publisher_wif_pool_id: NonEmptyStr = "github-heal-publisher"
Expand Down Expand Up @@ -115,6 +116,7 @@ fn public_heal_publisher_federation() -> HealPublisherFederation {
service_account_id: heal_publisher_service_account_id,
service_account: heal_publisher_service_account,
service_account_display_name: heal_publisher_service_account_display_name,
runner_environment: heal_publisher_wif_runner_environment,
provision_remedy: "run gunbc.auth.heal_publisher_provision heal_publisher_federation_provision_with_supplied_token, which creates that identity and binds roles/secretmanager.secretAccessor on the key for that service account only after reading the federation back.",
}
}
Expand All @@ -138,6 +140,7 @@ fn private_heal_publisher_federation() -> HealPublisherFederation {
service_account_id: "heal-publisher-private",
service_account: "heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com",
service_account_display_name: "Private heal publisher (WIF-only, one secret, no keys)",
runner_environment: heal_publisher_wif_runner_environment,
provision_remedy: "dispatch fleet-converge in mode gcp_iam_converge and approve the diff it files; gunbc.auth.gcp_iam_converge creates that identity and binds roles/secretmanager.secretAccessor on the key for that service account under that one approval.",
}
}
Expand Down Expand Up @@ -169,6 +172,19 @@ data heal_publisher_wif_event_name: String = "workflow_run"

data heal_publisher_wif_runner_environment: String = "github-hosted"

// THE PRIVATE PUBLISHER IS HEADED FOR OUR microVMs, AND ITS PIN STAYS github-hosted UNTIL THE WALL
// THAT WOULD JUSTIFY WIDENING IT EXISTS (operator 2026-09-29; review 72445). GitHub's
// runner_environment claim is only github-hosted or self-hosted, so pinning self-hosted admits every
// self-hosted runner, including the srv slots that execute pull-request code. What would keep
// pull-request code off the machine is the runner group gunbc.runner_group_restriction_ensure
// private_heal_publisher_runner_group_subject -- restricted to exactly this workflow and repository
// -- together with a controller that mints one guest per job into that group only after reading the
// restriction back. The group is modelled; the minting path is not. So the field is per record and
// the private record still says github-hosted: a publish job placed on a microVM label today cannot
// federate at all, which refuses rather than widens. The flip to self-hosted lands in ONE change with
// the controller path that mints into that group, and with the live provider's condition update.


// job_workflow_ref IS DELIBERATELY NOT PINNED, AND ITS EARLIER PRESENCE WAS A DEFECT THAT WOULD HAVE
// REFUSED THE VERY JOB THIS ADMITS. GitHub documents that claim as "For jobs using a reusable
// workflow, the ref path to the reusable workflow" -- heal-publish declares an ordinary job directly,
Expand All @@ -189,7 +205,7 @@ fn heal_publisher_federation_attribute_condition(f: HealPublisherFederation) ->
" && assertion.workflow_ref == '", heal_publisher_federation_workflow_ref(f: f), "'",
" && assertion.ref == 'refs/heads/", f.repository.default_branch, "'",
" && assertion.event_name == '", heal_publisher_wif_event_name, "'",
" && assertion.runner_environment == '", heal_publisher_wif_runner_environment, "'"
" && assertion.runner_environment == '", f.runner_environment, "'"
], "")
}

Expand Down
64 changes: 52 additions & 12 deletions dag/gunbc/runner/runner_group_restriction_ensure.dag
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ module gunbc.runner_group_restriction_ensure
import std.types { NonEmptyStr, String, Bool, Int, List }
import extdeps.github.org_actions { RunnerGroupVisibility, SelectedRepositories, runner_group_visibility_wire }
import gunbc.repository { gunbc_repository_numeric_identity }
import gunbc.auth.heal_publisher_federation { private_heal_publisher_federation, heal_publisher_federation_workflow_ref }
import gunbc.fleet.org_actions_inspection { org_actions_organization, RunnerGroupListDecode }
import gunbc.runner_group_restriction_observation {
ShakedownRunnerGroupRestriction, MicrovmRunnerGroupStanding,
RunnerGroupRestrictedToShakedownWorkflow, RunnerGroupRestrictionRefused,
RunnerGroupAbsent, observe_shakedown_runner_group_restriction, microvm_shakedown_runner_group_name,
RestrictedRunnerGroup, MicrovmRunnerGroupStanding,
RunnerGroupRestrictedToWorkflow, RunnerGroupRestrictionRefused,
RunnerGroupAbsent, observe_runner_group_restriction, microvm_shakedown_runner_group_name,
microvm_shakedown_selected_workflow, runner_group_restriction_refusal_text,
}

Expand All @@ -16,7 +17,7 @@ import gunbc.runner_group_restriction_observation {
// The group gunbc.runner_jit_mint plan_jit_mint requires is one the organization does not have
// until it is created, and creating it is an org-admin effect. This module owns the PLAN -- what
// the group must be, and which of three things a read of the organization's groups says to do --
// so the effect is never performed on a guess: a group already restricted to exactly the shakedown
// so the effect is never performed on a guess: a group already restricted to exactly its
// workflow is left alone (no request is filed, no operator is asked), an absent one is created, and
// a group holding the name with any other restriction is REFUSED rather than edited, because the
// update is a different effect that nobody has modeled or approved. After a create, the
Expand All @@ -38,18 +39,57 @@ type RunnerGroupCreateSubject {
selected_workflow: NonEmptyStr
}

fn microvm_runner_group_create_subject() -> RunnerGroupCreateSubject {
// EVERY RESTRICTED GROUP IS THIS SHAPE: selected visibility with one repository, no public
// repositories, restricted to exactly one workflow. Only the name, the repository and the workflow
// differ, so they are the constructor's arguments and the three walls are not re-spelled per group.
fn restricted_runner_group_subject(name: NonEmptyStr, repository_id: Int, selected_workflow: NonEmptyStr) -> RunnerGroupCreateSubject {
RunnerGroupCreateSubject {
organization: org_actions_organization,
name: microvm_shakedown_runner_group_name,
name: name,
visibility: SelectedRepositories,
selected_repository_id: gunbc_repository_numeric_identity.repository_id,
selected_repository_id: repository_id,
allows_public_repositories: false,
restricted_to_workflows: true,
selected_workflow: microvm_shakedown_selected_workflow(),
selected_workflow: selected_workflow,
}
}

fn microvm_runner_group_create_subject() -> RunnerGroupCreateSubject {
restricted_runner_group_subject(
name: microvm_shakedown_runner_group_name,
repository_id: gunbc_repository_numeric_identity.repository_id,
selected_workflow: microvm_shakedown_selected_workflow(),
)
}

// GUNBC-PRIVATE'S HEAL PUBLISHER IS HEADED FOR microVMs, IN A GROUP ONLY ITS PUBLISHER MAY USE
// (operator 2026-09-29). Its identity (gunbc.auth.heal_publisher_federation
// private_heal_publisher_federation) will admit a self-hosted runner, and the OIDC token cannot say
// WHICH one -- so the wall that keeps pull-request code off the machine is this group: restricted to
// exactly the publisher's workflow at the default branch, and to gunbc-private alone. The selected
// workflow IS the federation's workflow_ref, one string for both. The group name is also the runner
// label the publish job selects (strategy.private_heal_publisher_workflow).
//
// CREATING THE GROUP NARROWS AND WIDENS NOTHING, so it is ensured now. The federation's pin stays
// github-hosted until a controller mints guests into this group after reading its restriction back
// (review 72445): the flip and that minting path land together, in one change.
data private_heal_publisher_runner_group_name: NonEmptyStr = "heal-publisher-private"

fn private_heal_publisher_runner_group_subject() -> RunnerGroupCreateSubject {
restricted_runner_group_subject(
name: private_heal_publisher_runner_group_name,
repository_id: private_heal_publisher_federation().numeric_identity.repository_id,
selected_workflow: heal_publisher_federation_workflow_ref(f: private_heal_publisher_federation()) as NonEmptyStr,
)
}

// THE ROSTER THE ENSURE RUN WALKS, like gcp_iam_converge_targets: each group is read, left alone when
// already restricted, created under its own approval when absent, and refused when it holds the
// name with any other restriction.
fn restricted_runner_groups() -> List<RunnerGroupCreateSubject> {
[microvm_runner_group_create_subject(), private_heal_publisher_runner_group_subject()]
}

// The subject as one line, the text the operator reads and the intent a request digests.
fn runner_group_create_subject_text(s: RunnerGroupCreateSubject) -> String {
join([
Expand All @@ -63,16 +103,16 @@ fn runner_group_create_subject_text(s: RunnerGroupCreateSubject) -> String {
}

type RunnerGroupEnsurePlan
= GroupAlreadyRestricted { restriction: ShakedownRunnerGroupRestriction }
= GroupAlreadyRestricted { restriction: RestrictedRunnerGroup }
| GroupCreateRequired { subject: RunnerGroupCreateSubject }
| GroupEnsureRefused { cause: NonEmptyStr }

// Absent is the ONE refusal that plans the create; every other refusal of the observer -- an
// unreadable list, a duplicated name, a group holding the name with another restriction -- refuses
// the ensure, because the update is a different effect nobody has modeled or approved.
fn plan_runner_group_ensure(subject: RunnerGroupCreateSubject, listed: RunnerGroupListDecode) -> RunnerGroupEnsurePlan {
match observe_shakedown_runner_group_restriction(name: subject.name, listed: listed) {
RunnerGroupRestrictedToShakedownWorkflow { restriction } => GroupAlreadyRestricted { restriction: restriction }
match observe_runner_group_restriction(name: subject.name, selected_workflow: subject.selected_workflow, listed: listed) {
RunnerGroupRestrictedToWorkflow { restriction } => GroupAlreadyRestricted { restriction: restriction }
RunnerGroupRestrictionRefused { name: _, cause: RunnerGroupAbsent } => GroupCreateRequired { subject: subject }
RunnerGroupRestrictionRefused { name, cause } =>
GroupEnsureRefused { cause: join([
Expand All @@ -85,7 +125,7 @@ fn plan_runner_group_ensure(subject: RunnerGroupCreateSubject, listed: RunnerGro
// THE READBACK AFTER A CREATE. The only admitted outcome is the sealed restriction; an absent group
// after a 201 is as much a refusal as a wrongly restricted one.
type RunnerGroupCreateReadback
= CreatedGroupRestricted { restriction: ShakedownRunnerGroupRestriction }
= CreatedGroupRestricted { restriction: RestrictedRunnerGroup }
| CreatedGroupNotRestricted { cause: NonEmptyStr }

fn readback_after_create(subject: RunnerGroupCreateSubject, listed: RunnerGroupListDecode) -> RunnerGroupCreateReadback {
Expand Down
Loading
Loading