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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ docs/proposals/
node_modules/
dist/
dist-fixtures/
# Except bundled JavaScript actions in the examples, which ship their dist/ as real actions do
# (the emergency demo's stamp action; docs/EMERGENCY_EXECUTION.md §6).
!examples/**/dist/

# Fixtures fetched by tools/fixtures/fetch (pinned in fixtures.lock.json)
.fixtures/
Expand Down
208 changes: 197 additions & 11 deletions README.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions charts/gha-emergency/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v2
name: gha-emergency
description: Emergency execution for GitHub Actions UI - runs prepared, immutable workflow snapshots as Kubernetes Jobs when GitHub Actions is unavailable. Separate from the read-only gha-ui chart.
type: application
version: 0.1.0
appVersion: "0.0.0"
kubeVersion: ">=1.30.0-0"
home: https://github.com/GauJosh/github-actions-ui
sources:
- https://github.com/GauJosh/github-actions-ui
keywords:
- github-actions
- disaster-recovery
32 changes: 32 additions & 0 deletions charts/gha-emergency/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# gha-emergency chart

Emergency execution for GitHub Actions UI. It installs:
- an executor, which serves the UI and the emergency API and stores snapshots;
- the Role and service accounts its runner Jobs need.

It is separate from the read-only `gha-ui` chart, and optional.

Design, security model, supported features and run steps: `docs/EMERGENCY_EXECUTION.md`.

**Key values:**
- `executor.image` and `runner.image`: images built from `deploy/emergency/Dockerfile`. They must be available on the nodes, or in a registry reachable during a GitHub outage (not `ghcr.io`).
- `runner.image.digest` is required (`sha256:…`, as the nodes' runtime knows the image). Runner Jobs use `repository@digest`, and the admission policy admits no other image (security review H4).
- **Admission policy** (always installed, security review H4): a ValidatingAdmissionPolicy and binding pin every Job in the namespace, and every pod a Job creates, to the runner's image, command, service account, security context, resources, volumes, environment and network labels. The executor proves the API server enforces it before each execution.
- Needs Kubernetes 1.30 or later.
- Both objects are cluster-scoped, so installing needs permission to create them.
- Nothing else can create Jobs in the namespace, and a mutating webhook that injects sidecars or volumes into its pods (a service mesh, for example) makes them fail; exclude the namespace from such webhooks.
- `runner.resources`, `runner.maxMinutes`, `runner.maxConcurrent`, `runner.ttlSecondsAfterFinished`.
- `persistence.size` and `persistence.storageClassName`: snapshot storage. It is ReadWriteOnce, so runner pods are scheduled on the executor's node.
- `auth` (security review H2): sign-in with an OpenID Connect provider (`auth.oidc`), session lifetimes, the first administrators (`auth.initialAdministrators`, by issuer and subject), and break-glass accounts for provider outages (`auth.breakGlass.accounts`). The chart refuses to install without one sign-in method. The shared operator token no longer authorizes the API.
- `approval`: which executions wait for an approver (default: those that receive a Secret, or run in egress mode), separation of duties, and how long a request or approval lasts.
- `operatorToken.existingSecret`: the break-glass credentials Secret (one key per account; generated when empty).
- The executor contacts the identity provider over HTTPS on port 443, which its NetworkPolicy already allows; the provider must not depend on GitHub.
- `secretBindings`: which Secret, and which keys, executions of exact snapshot digests may receive (security review C1). Nothing is mounted without one.
- `network`: NetworkPolicies are always installed. Isolated executions run only after the executor's canary proves that the cluster enforces them (security review C2).
- Use a network plugin that enforces NetworkPolicy (Calico, Cilium).
- `allowEgressMode` enables a non-isolated egress mode.
- `clusterChecks`: install and upgrade fail unless the namespace is dedicated (labelled `gha-emergency.github-actions-ui/dedicated=true`) and restricted, holds no unrelated Secrets, and every bound Secret exists, is Opaque and holds its keys.

**Install** into a dedicated namespace that enforces Pod Security `restricted`. The executor, runner and canary pods comply.

Install with no Secrets in the namespace. Once a snapshot is prepared, create its Secret and bind it to the snapshot's digest in the same sitting: the chart refuses any Secret that isn't bound.
31 changes: 31 additions & 0 deletions charts/gha-emergency/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Emergency execution is installed in namespace {{ .Release.Namespace }}.

1. Open the UI (it is not exposed outside the cluster):

kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "gha-emergency.fullname" . }} 18093:{{ .Values.service.port }}

then http://127.0.0.1:18093/#/emergency

2. Sign in (security review H2):
{{- if .Values.auth.oidc.issuer }}
with {{ default .Values.auth.oidc.issuer .Values.auth.oidc.label }}. New users have no roles until an administrator
gives them some on the emergency view's Administration page.{{ if not .Values.auth.initialAdministrators }} No initial
administrator is configured (auth.initialAdministrators): nobody can assign roles yet.{{ end }}
{{- end }}
{{- range .Values.auth.breakGlass.accounts }}
Break-glass account {{ .name }} ({{ join ", " .roles }}), for when the identity provider is unavailable; its credential:
kubectl -n {{ $.Release.Namespace }} get secret {{ include "gha-emergency.tokenSecret" $ }} -o jsonpath='{.data.{{ .name }}}' | base64 -d
{{- end }}
The old shared operator token no longer authorizes API calls.

3. Secrets a snapshot needs are supplied as a Kubernetes Secret in this namespace, one key per secret name, e.g.

kubectl -n {{ .Release.Namespace }} create secret generic demo-secrets --from-literal=DEMO_SIGNING_KEY=...

and bound to exact snapshot digests with the secretBindings value. Create it just before the upgrade that binds it:
the chart refuses Secrets that aren't bound, and a digest exists only once a snapshot is prepared.
Only the keys a snapshot requires are mounted, and only into its runner pod; the executor never reads it.

4. Isolated executions run only when the executor's network canary proves that NetworkPolicy is enforced. Check:

the "Network" section of a snapshot's page, or GET api/emergency/network-check
44 changes: 44 additions & 0 deletions charts/gha-emergency/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{{- define "gha-emergency.fullname" -}}
{{- if contains .Chart.Name .Release.Name }}
{{- .Release.Name | trunc 50 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 50 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{/* Labels of the executor pod. Runner Jobs use them for pod affinity (deploy/emergency/executor/job-spec.ts). */}}
{{- define "gha-emergency.executorSelector" -}}
app.kubernetes.io/name: gha-emergency
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: executor
{{- end }}

{{- define "gha-emergency.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
app.kubernetes.io/part-of: github-actions-ui
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}

{{- define "gha-emergency.tokenSecret" -}}
{{- default (printf "%s-operator" (include "gha-emergency.fullname" .)) .Values.operatorToken.existingSecret }}
{{- end }}

{{/* The runner image by digest (security review H4): the only image runner Jobs may use. */}}
{{- define "gha-emergency.runnerImage" -}}
{{- $digest := toString .Values.runner.image.digest }}
{{- if not (regexMatch "^sha256:[0-9a-f]{64}$" $digest) }}
{{- fail "runner.image.digest must be the runner image's digest (sha256:<64 hex digits>): runner Jobs are pinned to it (security review H4)" }}
{{- end }}
{{- printf "%s@%s" .Values.runner.image.repository $digest }}
{{- end }}

{{/* The cluster-scoped admission policy and binding, named after the release and its namespace (security review H4). */}}
{{- define "gha-emergency.admissionPolicy" -}}
{{- printf "%s-runner-jobs.%s" (include "gha-emergency.fullname" .) .Release.Namespace }}
{{- end }}

{{/* Network mode label of runner pods (job-spec.ts NETWORK_LABEL), selected by networkpolicy.yaml. */}}
{{- define "gha-emergency.networkLabel" -}}
gha-emergency.github-actions-ui/network
{{- end }}
Loading
Loading