From 5cbbb9ea212b7ea5a14246d65b163c41e5e83b80 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 10 Nov 2026 22:20:35 +1100 Subject: [PATCH 1/6] Add ARC self-hosted runners for IUCLabs --- .pre-commit-config.yaml | 6 ++ apps/base/arc-system/kustomization.yaml | 7 ++ apps/base/arc-system/namespace.yaml | 5 ++ apps/base/arc-system/pdb.yaml | 12 ++++ apps/base/iuclabs-runners/job-started.sh | 23 +++++++ apps/base/iuclabs-runners/kustomization.yaml | 18 +++++ apps/base/iuclabs-runners/namespace.yaml | 5 ++ apps/base/iuclabs-runners/networkpolicy.yaml | 67 +++++++++++++++++++ apps/base/iuclabs-runners/vaultauth.yaml | 18 +++++ .../iuclabs-runners/vaultstaticsecret.yaml | 17 +++++ .../au-syd1/arc-system/kustomization.yaml | 15 +++++ apps/overlays/au-syd1/arc-system/values.yaml | 34 ++++++++++ .../iuclabs-runners/kustomization.yaml | 14 ++++ .../au-syd1/iuclabs-runners/values.yaml | 55 +++++++++++++++ argocd/applicationsets/platform.yaml | 2 + argocd/projects/platform.yaml | 4 ++ ci/fixtures/runner-hooks/pr-deleted-fork.json | 8 +++ ci/fixtures/runner-hooks/pr-fork-none.json | 10 +++ ci/fixtures/runner-hooks/pr-member.json | 10 +++ ci/fixtures/runner-hooks/pr-same-repo.json | 10 +++ ci/fixtures/runner-hooks/push.json | 3 + ci/test-runner-hooks.sh | 25 +++++++ 22 files changed, 368 insertions(+) create mode 100644 apps/base/arc-system/kustomization.yaml create mode 100644 apps/base/arc-system/namespace.yaml create mode 100644 apps/base/arc-system/pdb.yaml create mode 100755 apps/base/iuclabs-runners/job-started.sh create mode 100644 apps/base/iuclabs-runners/kustomization.yaml create mode 100644 apps/base/iuclabs-runners/namespace.yaml create mode 100644 apps/base/iuclabs-runners/networkpolicy.yaml create mode 100644 apps/base/iuclabs-runners/vaultauth.yaml create mode 100644 apps/base/iuclabs-runners/vaultstaticsecret.yaml create mode 100644 apps/overlays/au-syd1/arc-system/kustomization.yaml create mode 100644 apps/overlays/au-syd1/arc-system/values.yaml create mode 100644 apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml create mode 100644 apps/overlays/au-syd1/iuclabs-runners/values.yaml create mode 100644 ci/fixtures/runner-hooks/pr-deleted-fork.json create mode 100644 ci/fixtures/runner-hooks/pr-fork-none.json create mode 100644 ci/fixtures/runner-hooks/pr-member.json create mode 100644 ci/fixtures/runner-hooks/pr-same-repo.json create mode 100644 ci/fixtures/runner-hooks/push.json create mode 100755 ci/test-runner-hooks.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a3fb06..1ea6adc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,3 +42,9 @@ repos: entry: ci/validate-no-secrets.sh language: system pass_filenames: false + - id: runner_hooks + name: test arc runner job-started hook + entry: ci/test-runner-hooks.sh + language: system + pass_filenames: false + files: ^(apps/base/iuclabs-runners/job-started\.sh|ci/test-runner-hooks\.sh|ci/fixtures/runner-hooks/) diff --git a/apps/base/arc-system/kustomization.yaml b/apps/base/arc-system/kustomization.yaml new file mode 100644 index 0000000..a26b1fa --- /dev/null +++ b/apps/base/arc-system/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - pdb.yaml diff --git a/apps/base/arc-system/namespace.yaml b/apps/base/arc-system/namespace.yaml new file mode 100644 index 0000000..bc771a9 --- /dev/null +++ b/apps/base/arc-system/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: arc-system diff --git a/apps/base/arc-system/pdb.yaml b/apps/base/arc-system/pdb.yaml new file mode 100644 index 0000000..498e168 --- /dev/null +++ b/apps/base/arc-system/pdb.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: arc-controller + namespace: arc-system +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: gha-rs-controller + app.kubernetes.io/instance: arc diff --git a/apps/base/iuclabs-runners/job-started.sh b/apps/base/iuclabs-runners/job-started.sh new file mode 100755 index 0000000..c7203cd --- /dev/null +++ b/apps/base/iuclabs-runners/job-started.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -eu + +refuse() { + echo "job-started hook: refusing ${GITHUB_EVENT_NAME} job: $1" >&2 + exit 1 +} + +case "${GITHUB_EVENT_NAME}" in + pull_request_target | workflow_run) + refuse "event not allowed on self-hosted runners" + ;; + pull_request) + same_repo=$(jq -r '.pull_request.head.repo.fork == false' "${GITHUB_EVENT_PATH}") + assoc=$(jq -r '.pull_request.author_association // "NONE"' "${GITHUB_EVENT_PATH}") + if [ "${same_repo}" != true ]; then + case "${assoc}" in + OWNER | MEMBER | COLLABORATOR) ;; + *) refuse "fork pull request from untrusted author (${assoc})" ;; + esac + fi + ;; +esac diff --git a/apps/base/iuclabs-runners/kustomization.yaml b/apps/base/iuclabs-runners/kustomization.yaml new file mode 100644 index 0000000..8d0cf12 --- /dev/null +++ b/apps/base/iuclabs-runners/kustomization.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - vaultauth.yaml + - vaultstaticsecret.yaml + - networkpolicy.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: + - name: runner-hooks + namespace: iuclabs-runners + files: + - job-started.sh diff --git a/apps/base/iuclabs-runners/namespace.yaml b/apps/base/iuclabs-runners/namespace.yaml new file mode 100644 index 0000000..fa25861 --- /dev/null +++ b/apps/base/iuclabs-runners/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: iuclabs-runners diff --git a/apps/base/iuclabs-runners/networkpolicy.yaml b/apps/base/iuclabs-runners/networkpolicy.yaml new file mode 100644 index 0000000..44329a1 --- /dev/null +++ b/apps/base/iuclabs-runners/networkpolicy.yaml @@ -0,0 +1,67 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: runners + namespace: iuclabs-runners +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 198.18.0.0/15 + ports: + - protocol: TCP + port: 443 + # artifactapi.k8s.syd1.au.unkin.net: traefik-internal LB, DNATed to its pods in-cluster + - to: + - ipBlock: + cidr: 198.18.200.4/32 + ports: + - protocol: TCP + port: 443 + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik-system + podSelector: + matchLabels: + app.kubernetes.io/instance: traefik-internal-traefik-system + ports: + - protocol: TCP + port: websecure + # vault.service.consul + - to: + - ipBlock: + cidr: 198.18.25.78/32 + - ipBlock: + cidr: 198.18.26.197/32 + - ipBlock: + cidr: 198.18.27.116/32 + - ipBlock: + cidr: 198.18.28.248/32 + - ipBlock: + cidr: 198.18.29.92/32 + ports: + - protocol: TCP + port: 8200 diff --git a/apps/base/iuclabs-runners/vaultauth.yaml b/apps/base/iuclabs-runners/vaultauth.yaml new file mode 100644 index 0000000..066605e --- /dev/null +++ b/apps/base/iuclabs-runners/vaultauth.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultAuth +metadata: + name: default + namespace: iuclabs-runners +spec: + allowedNamespaces: + - iuclabs-runners + kubernetes: + audiences: + - vault + role: default + serviceAccount: default + tokenExpirationSeconds: 600 + method: kubernetes + mount: k8s/au/syd1 + vaultConnectionRef: vso-system/default diff --git a/apps/base/iuclabs-runners/vaultstaticsecret.yaml b/apps/base/iuclabs-runners/vaultstaticsecret.yaml new file mode 100644 index 0000000..bad4ff6 --- /dev/null +++ b/apps/base/iuclabs-runners/vaultstaticsecret.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: github-app + namespace: iuclabs-runners +spec: + destination: + create: true + name: github-app + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/iuclabs-runners/default/github-app + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/overlays/au-syd1/arc-system/kustomization.yaml b/apps/overlays/au-syd1/arc-system/kustomization.yaml new file mode 100644 index 0000000..270f056 --- /dev/null +++ b/apps/overlays/au-syd1/arc-system/kustomization.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/arc-system + +helmCharts: + - name: gha-runner-scale-set-controller + repo: oci://ghcr.io/actions/actions-runner-controller-charts + version: "0.15.1" + releaseName: arc + namespace: arc-system + valuesFile: values.yaml + includeCRDs: true diff --git a/apps/overlays/au-syd1/arc-system/values.yaml b/apps/overlays/au-syd1/arc-system/values.yaml new file mode 100644 index 0000000..32bbd5e --- /dev/null +++ b/apps/overlays/au-syd1/arc-system/values.yaml @@ -0,0 +1,34 @@ +--- +replicaCount: 2 + +podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: gha-rs-controller + app.kubernetes.io/instance: arc + +flags: + logLevel: info + logFormat: json diff --git a/apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml b/apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml new file mode 100644 index 0000000..c35c13f --- /dev/null +++ b/apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/iuclabs-runners + +helmCharts: + - name: gha-runner-scale-set + repo: oci://ghcr.io/actions/actions-runner-controller-charts + version: "0.15.1" + releaseName: iuclabs + namespace: iuclabs-runners + valuesFile: values.yaml diff --git a/apps/overlays/au-syd1/iuclabs-runners/values.yaml b/apps/overlays/au-syd1/iuclabs-runners/values.yaml new file mode 100644 index 0000000..c42f765 --- /dev/null +++ b/apps/overlays/au-syd1/iuclabs-runners/values.yaml @@ -0,0 +1,55 @@ +--- +githubConfigUrl: https://github.com/IUCLabs +githubConfigSecret: github-app +runnerScaleSetName: iuclabs-k8s +minRunners: 0 +maxRunners: 4 + +controllerServiceAccount: + namespace: arc-system + name: arc-gha-rs-controller + +template: + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + seccompProfile: + type: RuntimeDefault + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + actions.github.com/scale-set-name: iuclabs-k8s + containers: + - name: runner + image: ghcr.io/actions/actions-runner:2.338.2 + command: ["/home/runner/run.sh"] + env: + - name: ACTIONS_RUNNER_HOOK_JOB_STARTED + value: /hooks/job-started.sh + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + resources: + requests: + cpu: 1 + memory: 512Mi + limits: + cpu: 2 + memory: 2Gi + volumeMounts: + - name: hooks + mountPath: /hooks + readOnly: true + volumes: + - name: hooks + configMap: + name: runner-hooks + defaultMode: 0555 diff --git a/argocd/applicationsets/platform.yaml b/argocd/applicationsets/platform.yaml index b0c9d3c..2376860 100644 --- a/argocd/applicationsets/platform.yaml +++ b/argocd/applicationsets/platform.yaml @@ -16,6 +16,7 @@ spec: - path: apps/overlays/*/bind-internal - path: apps/overlays/*/bind-external - path: apps/overlays/*/age-api + - path: apps/overlays/*/arc-system - path: apps/overlays/*/cattle-system - path: apps/overlays/*/cert-manager - path: apps/overlays/*/certificates @@ -31,6 +32,7 @@ spec: - path: apps/overlays/*/grafana-system - path: apps/overlays/*/haproxy - path: apps/overlays/*/inteldeviceplugins-system + - path: apps/overlays/*/iuclabs-runners - path: apps/overlays/*/jfrog - path: apps/overlays/*/k8up-system - path: apps/overlays/*/kanidm diff --git a/argocd/projects/platform.yaml b/argocd/projects/platform.yaml index a7e97fa..8140509 100644 --- a/argocd/projects/platform.yaml +++ b/argocd/projects/platform.yaml @@ -14,6 +14,8 @@ spec: - oci://ghcr.io/woodpecker-ci/helm/woodpecker - oci://ghcr.io/netbox-community/netbox-chart/netbox - oci://docker.gitea.com/charts + - oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller + - oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set destinations: - namespace: '*-system' server: https://kubernetes.default.svc @@ -45,6 +47,8 @@ spec: server: https://kubernetes.default.svc - namespace: 'haproxy' server: https://kubernetes.default.svc + - namespace: 'iuclabs-runners' + server: https://kubernetes.default.svc - namespace: 'jfrog' server: https://kubernetes.default.svc - namespace: 'kanidm' diff --git a/ci/fixtures/runner-hooks/pr-deleted-fork.json b/ci/fixtures/runner-hooks/pr-deleted-fork.json new file mode 100644 index 0000000..a602183 --- /dev/null +++ b/ci/fixtures/runner-hooks/pr-deleted-fork.json @@ -0,0 +1,8 @@ +{ + "pull_request": { + "author_association": "NONE", + "head": { + "repo": null + } + } +} diff --git a/ci/fixtures/runner-hooks/pr-fork-none.json b/ci/fixtures/runner-hooks/pr-fork-none.json new file mode 100644 index 0000000..1c4104b --- /dev/null +++ b/ci/fixtures/runner-hooks/pr-fork-none.json @@ -0,0 +1,10 @@ +{ + "pull_request": { + "author_association": "NONE", + "head": { + "repo": { + "fork": true + } + } + } +} diff --git a/ci/fixtures/runner-hooks/pr-member.json b/ci/fixtures/runner-hooks/pr-member.json new file mode 100644 index 0000000..50ac080 --- /dev/null +++ b/ci/fixtures/runner-hooks/pr-member.json @@ -0,0 +1,10 @@ +{ + "pull_request": { + "author_association": "MEMBER", + "head": { + "repo": { + "fork": true + } + } + } +} diff --git a/ci/fixtures/runner-hooks/pr-same-repo.json b/ci/fixtures/runner-hooks/pr-same-repo.json new file mode 100644 index 0000000..81312c5 --- /dev/null +++ b/ci/fixtures/runner-hooks/pr-same-repo.json @@ -0,0 +1,10 @@ +{ + "pull_request": { + "author_association": "CONTRIBUTOR", + "head": { + "repo": { + "fork": false + } + } + } +} diff --git a/ci/fixtures/runner-hooks/push.json b/ci/fixtures/runner-hooks/push.json new file mode 100644 index 0000000..ccb4cb1 --- /dev/null +++ b/ci/fixtures/runner-hooks/push.json @@ -0,0 +1,3 @@ +{ + "ref": "refs/heads/main" +} diff --git a/ci/test-runner-hooks.sh b/ci/test-runner-hooks.sh new file mode 100755 index 0000000..0189133 --- /dev/null +++ b/ci/test-runner-hooks.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +hook=apps/base/iuclabs-runners/job-started.sh +fixtures=ci/fixtures/runner-hooks +fail=0 + +check() { + local want=$1 event=$2 fixture=$3 got=0 + GITHUB_EVENT_NAME=$event GITHUB_EVENT_PATH=$fixtures/$fixture "$hook" >/dev/null 2>&1 || got=1 + if [ "$got" -ne "$want" ]; then + echo "FAIL: $event $fixture exit=$got want=$want" >&2 + fail=1 + fi +} + +check 0 push push.json +check 0 pull_request pr-member.json +check 0 pull_request pr-same-repo.json +check 1 pull_request pr-fork-none.json +check 1 pull_request pr-deleted-fork.json +check 1 pull_request_target pr-member.json +check 1 workflow_run push.json + +exit "$fail" -- 2.47.3 From c75ed6497ffdf52e89345ca35c16cc35d7e28ecd Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 10 Nov 2026 22:24:48 +1100 Subject: [PATCH 2/6] Allowlist runner hook events and tighten runner egress --- apps/base/iuclabs-runners/job-started.sh | 29 ++++++++++--------- apps/base/iuclabs-runners/networkpolicy.yaml | 25 +++++----------- ci/fixtures/runner-hooks/invalid.txt | 1 + ci/fixtures/runner-hooks/issue-comment.json | 11 +++++++ ci/fixtures/runner-hooks/pr-deleted-fork.json | 5 +++- ci/fixtures/runner-hooks/pr-fork-none.json | 6 +++- ci/fixtures/runner-hooks/pr-fork-spoofed.json | 14 +++++++++ ci/fixtures/runner-hooks/pr-member.json | 6 +++- ci/fixtures/runner-hooks/pr-same-repo.json | 6 +++- ci/test-runner-hooks.sh | 22 +++++++++++--- 10 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 ci/fixtures/runner-hooks/invalid.txt create mode 100644 ci/fixtures/runner-hooks/issue-comment.json create mode 100644 ci/fixtures/runner-hooks/pr-fork-spoofed.json diff --git a/apps/base/iuclabs-runners/job-started.sh b/apps/base/iuclabs-runners/job-started.sh index c7203cd..b6cf911 100755 --- a/apps/base/iuclabs-runners/job-started.sh +++ b/apps/base/iuclabs-runners/job-started.sh @@ -1,23 +1,26 @@ #!/bin/sh set -eu +event="${GITHUB_EVENT_NAME:-}" + refuse() { - echo "job-started hook: refusing ${GITHUB_EVENT_NAME} job: $1" >&2 + echo "job-started hook: refusing '${event}' job: $1" >&2 exit 1 } -case "${GITHUB_EVENT_NAME}" in - pull_request_target | workflow_run) - refuse "event not allowed on self-hosted runners" - ;; +[ -r "${GITHUB_EVENT_PATH:-}" ] || refuse "event payload missing" +jq -e 'type == "object"' "${GITHUB_EVENT_PATH}" >/dev/null 2>&1 || refuse "event payload unparseable" + +case "${event}" in + push | workflow_dispatch | schedule | merge_group | release) ;; pull_request) - same_repo=$(jq -r '.pull_request.head.repo.fork == false' "${GITHUB_EVENT_PATH}") - assoc=$(jq -r '.pull_request.author_association // "NONE"' "${GITHUB_EVENT_PATH}") - if [ "${same_repo}" != true ]; then - case "${assoc}" in - OWNER | MEMBER | COLLABORATOR) ;; - *) refuse "fork pull request from untrusted author (${assoc})" ;; - esac - fi + trusted=$(jq -r ' + .pull_request.head.repo.full_name as $head + | if $head == null then false + elif $head == .repository.full_name then true + else (.pull_request.author_association | IN("OWNER", "MEMBER", "COLLABORATOR")) + end' "${GITHUB_EVENT_PATH}") || refuse "event payload unreadable" + [ "${trusted}" = true ] || refuse "pull request from untrusted fork" ;; + *) refuse "event not allowed on self-hosted runners" ;; esac diff --git a/apps/base/iuclabs-runners/networkpolicy.yaml b/apps/base/iuclabs-runners/networkpolicy.yaml index 44329a1..bcf7a7d 100644 --- a/apps/base/iuclabs-runners/networkpolicy.yaml +++ b/apps/base/iuclabs-runners/networkpolicy.yaml @@ -30,27 +30,16 @@ spec: - 172.16.0.0/12 - 192.168.0.0/16 - 198.18.0.0/15 + - 0.0.0.0/8 + - 100.64.0.0/10 + - 127.0.0.0/8 + - 169.254.0.0/16 + - 224.0.0.0/4 + - 240.0.0.0/4 ports: - protocol: TCP port: 443 - # artifactapi.k8s.syd1.au.unkin.net: traefik-internal LB, DNATed to its pods in-cluster - - to: - - ipBlock: - cidr: 198.18.200.4/32 - ports: - - protocol: TCP - port: 443 - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: traefik-system - podSelector: - matchLabels: - app.kubernetes.io/instance: traefik-internal-traefik-system - ports: - - protocol: TCP - port: websecure - # vault.service.consul + # vault.service.consul servers, from `dig vault.service.consul` - to: - ipBlock: cidr: 198.18.25.78/32 diff --git a/ci/fixtures/runner-hooks/invalid.txt b/ci/fixtures/runner-hooks/invalid.txt new file mode 100644 index 0000000..8fc44f4 --- /dev/null +++ b/ci/fixtures/runner-hooks/invalid.txt @@ -0,0 +1 @@ +not json { diff --git a/ci/fixtures/runner-hooks/issue-comment.json b/ci/fixtures/runner-hooks/issue-comment.json new file mode 100644 index 0000000..6591540 --- /dev/null +++ b/ci/fixtures/runner-hooks/issue-comment.json @@ -0,0 +1,11 @@ +{ + "comment": { + "author_association": "NONE" + }, + "issue": { + "number": 1 + }, + "repository": { + "full_name": "IUCLabs/app" + } +} diff --git a/ci/fixtures/runner-hooks/pr-deleted-fork.json b/ci/fixtures/runner-hooks/pr-deleted-fork.json index a602183..8ae3be4 100644 --- a/ci/fixtures/runner-hooks/pr-deleted-fork.json +++ b/ci/fixtures/runner-hooks/pr-deleted-fork.json @@ -1,8 +1,11 @@ { "pull_request": { - "author_association": "NONE", + "author_association": "MEMBER", "head": { "repo": null } + }, + "repository": { + "full_name": "IUCLabs/app" } } diff --git a/ci/fixtures/runner-hooks/pr-fork-none.json b/ci/fixtures/runner-hooks/pr-fork-none.json index 1c4104b..62cbc06 100644 --- a/ci/fixtures/runner-hooks/pr-fork-none.json +++ b/ci/fixtures/runner-hooks/pr-fork-none.json @@ -3,8 +3,12 @@ "author_association": "NONE", "head": { "repo": { - "fork": true + "fork": true, + "full_name": "evil/app" } } + }, + "repository": { + "full_name": "IUCLabs/app" } } diff --git a/ci/fixtures/runner-hooks/pr-fork-spoofed.json b/ci/fixtures/runner-hooks/pr-fork-spoofed.json new file mode 100644 index 0000000..39f2b52 --- /dev/null +++ b/ci/fixtures/runner-hooks/pr-fork-spoofed.json @@ -0,0 +1,14 @@ +{ + "pull_request": { + "author_association": "NONE", + "head": { + "repo": { + "fork": false, + "full_name": "evil/app" + } + } + }, + "repository": { + "full_name": "IUCLabs/app" + } +} diff --git a/ci/fixtures/runner-hooks/pr-member.json b/ci/fixtures/runner-hooks/pr-member.json index 50ac080..a0ab634 100644 --- a/ci/fixtures/runner-hooks/pr-member.json +++ b/ci/fixtures/runner-hooks/pr-member.json @@ -3,8 +3,12 @@ "author_association": "MEMBER", "head": { "repo": { - "fork": true + "fork": true, + "full_name": "someone/app" } } + }, + "repository": { + "full_name": "IUCLabs/app" } } diff --git a/ci/fixtures/runner-hooks/pr-same-repo.json b/ci/fixtures/runner-hooks/pr-same-repo.json index 81312c5..1048ae9 100644 --- a/ci/fixtures/runner-hooks/pr-same-repo.json +++ b/ci/fixtures/runner-hooks/pr-same-repo.json @@ -3,8 +3,12 @@ "author_association": "CONTRIBUTOR", "head": { "repo": { - "fork": false + "fork": false, + "full_name": "IUCLabs/app" } } + }, + "repository": { + "full_name": "IUCLabs/app" } } diff --git a/ci/test-runner-hooks.sh b/ci/test-runner-hooks.sh index 0189133..64e78d5 100755 --- a/ci/test-runner-hooks.sh +++ b/ci/test-runner-hooks.sh @@ -7,19 +7,33 @@ fail=0 check() { local want=$1 event=$2 fixture=$3 got=0 - GITHUB_EVENT_NAME=$event GITHUB_EVENT_PATH=$fixtures/$fixture "$hook" >/dev/null 2>&1 || got=1 + local -a env=(-u GITHUB_EVENT_NAME -u GITHUB_EVENT_PATH) + [ "$event" = - ] || env+=("GITHUB_EVENT_NAME=$event") + [ "$fixture" = - ] || env+=("GITHUB_EVENT_PATH=$fixtures/$fixture") + env "${env[@]}" "$hook" >/dev/null 2>&1 || got=1 if [ "$got" -ne "$want" ]; then echo "FAIL: $event $fixture exit=$got want=$want" >&2 fail=1 fi } -check 0 push push.json +for e in push workflow_dispatch schedule merge_group release; do + check 0 "$e" push.json +done check 0 pull_request pr-member.json check 0 pull_request pr-same-repo.json check 1 pull_request pr-fork-none.json +check 1 pull_request pr-fork-spoofed.json check 1 pull_request pr-deleted-fork.json -check 1 pull_request_target pr-member.json -check 1 workflow_run push.json +check 1 pull_request push.json +for e in pull_request_target workflow_run pull_request_review pull_request_review_comment issue_comment issues discussion discussion_comment create delete unknown_event; do + check 1 "$e" pr-member.json +done +check 1 pull_request_review pr-fork-none.json +check 1 issue_comment issue-comment.json +check 1 push - +check 1 push missing.json +check 1 push invalid.txt +check 1 - push.json exit "$fail" -- 2.47.3 From dce2767059f8ab2e1828a70029bacc5a7bb892eb Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 10 Nov 2026 22:49:06 +1100 Subject: [PATCH 3/6] Split runner NetworkPolicy into namespace baseline and per-scale-set egress --- .../networkpolicy-iuclabs-k8s.yaml | 45 +++++++++++++++++++ apps/base/iuclabs-runners/networkpolicy.yaml | 44 +++++------------- 2 files changed, 56 insertions(+), 33 deletions(-) create mode 100644 apps/base/iuclabs-runners/networkpolicy-iuclabs-k8s.yaml diff --git a/apps/base/iuclabs-runners/networkpolicy-iuclabs-k8s.yaml b/apps/base/iuclabs-runners/networkpolicy-iuclabs-k8s.yaml new file mode 100644 index 0000000..4b2377d --- /dev/null +++ b/apps/base/iuclabs-runners/networkpolicy-iuclabs-k8s.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: egress-iuclabs-k8s + namespace: iuclabs-runners +spec: + podSelector: + matchLabels: + runners.unkin.net/scale-set: iuclabs-k8s + policyTypes: + - Egress + egress: + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 198.18.0.0/15 + - 0.0.0.0/8 + - 100.64.0.0/10 + - 127.0.0.0/8 + - 169.254.0.0/16 + - 224.0.0.0/4 + - 240.0.0.0/4 + ports: + - protocol: TCP + port: 443 + # vault.service.consul servers, from `dig vault.service.consul` + - to: + - ipBlock: + cidr: 198.18.25.78/32 + - ipBlock: + cidr: 198.18.26.197/32 + - ipBlock: + cidr: 198.18.27.116/32 + - ipBlock: + cidr: 198.18.28.248/32 + - ipBlock: + cidr: 198.18.29.92/32 + ports: + - protocol: TCP + port: 8200 diff --git a/apps/base/iuclabs-runners/networkpolicy.yaml b/apps/base/iuclabs-runners/networkpolicy.yaml index bcf7a7d..8602217 100644 --- a/apps/base/iuclabs-runners/networkpolicy.yaml +++ b/apps/base/iuclabs-runners/networkpolicy.yaml @@ -2,13 +2,23 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: runners + name: default-deny namespace: iuclabs-runners spec: podSelector: {} policyTypes: - Ingress - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-dns + namespace: iuclabs-runners +spec: + podSelector: {} + policyTypes: + - Egress egress: - to: - namespaceSelector: @@ -22,35 +32,3 @@ spec: port: 53 - protocol: TCP port: 53 - - to: - - ipBlock: - cidr: 0.0.0.0/0 - except: - - 10.0.0.0/8 - - 172.16.0.0/12 - - 192.168.0.0/16 - - 198.18.0.0/15 - - 0.0.0.0/8 - - 100.64.0.0/10 - - 127.0.0.0/8 - - 169.254.0.0/16 - - 224.0.0.0/4 - - 240.0.0.0/4 - ports: - - protocol: TCP - port: 443 - # vault.service.consul servers, from `dig vault.service.consul` - - to: - - ipBlock: - cidr: 198.18.25.78/32 - - ipBlock: - cidr: 198.18.26.197/32 - - ipBlock: - cidr: 198.18.27.116/32 - - ipBlock: - cidr: 198.18.28.248/32 - - ipBlock: - cidr: 198.18.29.92/32 - ports: - - protocol: TCP - port: 8200 -- 2.47.3 From 126f7c3c366b6b5d06f46c56793c748184933ab2 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 10 Nov 2026 22:49:07 +1100 Subject: [PATCH 4/6] Replace gha-runner-scale-set chart with plain AutoscalingRunnerSet manifests --- apps/base/iuclabs-runners/kustomization.yaml | 3 + apps/base/iuclabs-runners/rbac.yaml | 44 +++++++++++++ .../iuclabs-runners/scaleset-iuclabs-k8s.yaml | 66 +++++++++++++++++++ .../iuclabs-runners/kustomization.yaml | 8 --- .../au-syd1/iuclabs-runners/values.yaml | 55 ---------------- argocd/projects/platform.yaml | 1 - 6 files changed, 113 insertions(+), 64 deletions(-) create mode 100644 apps/base/iuclabs-runners/rbac.yaml create mode 100644 apps/base/iuclabs-runners/scaleset-iuclabs-k8s.yaml delete mode 100644 apps/overlays/au-syd1/iuclabs-runners/values.yaml diff --git a/apps/base/iuclabs-runners/kustomization.yaml b/apps/base/iuclabs-runners/kustomization.yaml index 8d0cf12..71de007 100644 --- a/apps/base/iuclabs-runners/kustomization.yaml +++ b/apps/base/iuclabs-runners/kustomization.yaml @@ -7,6 +7,9 @@ resources: - vaultauth.yaml - vaultstaticsecret.yaml - networkpolicy.yaml + - rbac.yaml + - scaleset-iuclabs-k8s.yaml + - networkpolicy-iuclabs-k8s.yaml generatorOptions: disableNameSuffixHash: true diff --git a/apps/base/iuclabs-runners/rbac.yaml b/apps/base/iuclabs-runners/rbac.yaml new file mode 100644 index 0000000..41fb92b --- /dev/null +++ b/apps/base/iuclabs-runners/rbac.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: runner + namespace: iuclabs-runners +automountServiceAccountToken: false +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: arc-manager + namespace: iuclabs-runners + finalizers: + - actions.github.com/cleanup-protection +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["create", "delete", "get"] + - apiGroups: [""] + resources: ["pods/status"] + verbs: ["get"] + - apiGroups: [""] + resources: ["secrets", "serviceaccounts"] + verbs: ["create", "delete", "get", "list", "patch", "update"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings"] + verbs: ["create", "delete", "get", "patch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: arc-manager + namespace: iuclabs-runners + finalizers: + - actions.github.com/cleanup-protection +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: arc-manager +subjects: + - kind: ServiceAccount + name: arc-gha-rs-controller + namespace: arc-system diff --git a/apps/base/iuclabs-runners/scaleset-iuclabs-k8s.yaml b/apps/base/iuclabs-runners/scaleset-iuclabs-k8s.yaml new file mode 100644 index 0000000..8931ff1 --- /dev/null +++ b/apps/base/iuclabs-runners/scaleset-iuclabs-k8s.yaml @@ -0,0 +1,66 @@ +--- +apiVersion: actions.github.com/v1alpha1 +kind: AutoscalingRunnerSet +metadata: + name: iuclabs-k8s + namespace: iuclabs-runners + labels: + app.kubernetes.io/version: "0.15.1" + annotations: + actions.github.com/cleanup-manager-role-name: arc-manager + actions.github.com/cleanup-manager-role-binding: arc-manager +spec: + githubConfigUrl: https://github.com/IUCLabs + githubConfigSecret: github-app + runnerScaleSetName: iuclabs-k8s + minRunners: 0 + maxRunners: 4 + template: + metadata: + labels: + runners.unkin.net/scale-set: iuclabs-k8s + spec: + serviceAccountName: runner + automountServiceAccountToken: false + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + seccompProfile: + type: RuntimeDefault + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + runners.unkin.net/scale-set: iuclabs-k8s + containers: + - name: runner + image: ghcr.io/actions/actions-runner:2.338.2 + command: ["/home/runner/run.sh"] + env: + - name: ACTIONS_RUNNER_HOOK_JOB_STARTED + value: /hooks/job-started.sh + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + resources: + requests: + cpu: 1 + memory: 512Mi + limits: + cpu: 2 + memory: 2Gi + volumeMounts: + - name: hooks + mountPath: /hooks + readOnly: true + volumes: + - name: hooks + configMap: + name: runner-hooks + defaultMode: 0555 diff --git a/apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml b/apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml index c35c13f..1d66974 100644 --- a/apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml +++ b/apps/overlays/au-syd1/iuclabs-runners/kustomization.yaml @@ -4,11 +4,3 @@ kind: Kustomization resources: - ../../../base/iuclabs-runners - -helmCharts: - - name: gha-runner-scale-set - repo: oci://ghcr.io/actions/actions-runner-controller-charts - version: "0.15.1" - releaseName: iuclabs - namespace: iuclabs-runners - valuesFile: values.yaml diff --git a/apps/overlays/au-syd1/iuclabs-runners/values.yaml b/apps/overlays/au-syd1/iuclabs-runners/values.yaml deleted file mode 100644 index c42f765..0000000 --- a/apps/overlays/au-syd1/iuclabs-runners/values.yaml +++ /dev/null @@ -1,55 +0,0 @@ ---- -githubConfigUrl: https://github.com/IUCLabs -githubConfigSecret: github-app -runnerScaleSetName: iuclabs-k8s -minRunners: 0 -maxRunners: 4 - -controllerServiceAccount: - namespace: arc-system - name: arc-gha-rs-controller - -template: - spec: - automountServiceAccountToken: false - securityContext: - runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 1001 - seccompProfile: - type: RuntimeDefault - topologySpreadConstraints: - - maxSkew: 1 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: ScheduleAnyway - labelSelector: - matchLabels: - actions.github.com/scale-set-name: iuclabs-k8s - containers: - - name: runner - image: ghcr.io/actions/actions-runner:2.338.2 - command: ["/home/runner/run.sh"] - env: - - name: ACTIONS_RUNNER_HOOK_JOB_STARTED - value: /hooks/job-started.sh - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - resources: - requests: - cpu: 1 - memory: 512Mi - limits: - cpu: 2 - memory: 2Gi - volumeMounts: - - name: hooks - mountPath: /hooks - readOnly: true - volumes: - - name: hooks - configMap: - name: runner-hooks - defaultMode: 0555 diff --git a/argocd/projects/platform.yaml b/argocd/projects/platform.yaml index 8140509..47a5abd 100644 --- a/argocd/projects/platform.yaml +++ b/argocd/projects/platform.yaml @@ -15,7 +15,6 @@ spec: - oci://ghcr.io/netbox-community/netbox-chart/netbox - oci://docker.gitea.com/charts - oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller - - oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set destinations: - namespace: '*-system' server: https://kubernetes.default.svc -- 2.47.3 From 26f5564ce0cac84fc8f9b3a18f404ac781a73796 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 10 Nov 2026 23:15:12 +1100 Subject: [PATCH 5/6] Pull ARC controller chart through artifactapi --- apps/overlays/au-syd1/arc-system/kustomization.yaml | 2 +- argocd/projects/platform.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/overlays/au-syd1/arc-system/kustomization.yaml b/apps/overlays/au-syd1/arc-system/kustomization.yaml index 270f056..4ea8c2d 100644 --- a/apps/overlays/au-syd1/arc-system/kustomization.yaml +++ b/apps/overlays/au-syd1/arc-system/kustomization.yaml @@ -7,7 +7,7 @@ resources: helmCharts: - name: gha-runner-scale-set-controller - repo: oci://ghcr.io/actions/actions-runner-controller-charts + repo: oci://artifactapi.k8s.syd1.au.unkin.net/ghcr/actions/actions-runner-controller-charts version: "0.15.1" releaseName: arc namespace: arc-system diff --git a/argocd/projects/platform.yaml b/argocd/projects/platform.yaml index 47a5abd..a38a52f 100644 --- a/argocd/projects/platform.yaml +++ b/argocd/projects/platform.yaml @@ -14,7 +14,7 @@ spec: - oci://ghcr.io/woodpecker-ci/helm/woodpecker - oci://ghcr.io/netbox-community/netbox-chart/netbox - oci://docker.gitea.com/charts - - oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller + - oci://artifactapi.k8s.syd1.au.unkin.net/ghcr/actions/actions-runner-controller-charts/gha-runner-scale-set-controller destinations: - namespace: '*-system' server: https://kubernetes.default.svc -- 2.47.3 From 543fed279a4532455c64dd61af67b20c79f304e7 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 10 Nov 2026 23:20:55 +1100 Subject: [PATCH 6/6] Split iuclabs-k8s scale set into a kustomize component --- .../components/iuclabs-k8s/kustomization.yaml | 7 +++++++ .../iuclabs-k8s/networkpolicy.yaml} | 0 .../iuclabs-k8s/scaleset.yaml} | 0 apps/base/iuclabs-runners/kustomization.yaml | 5 +++-- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 apps/base/iuclabs-runners/components/iuclabs-k8s/kustomization.yaml rename apps/base/iuclabs-runners/{networkpolicy-iuclabs-k8s.yaml => components/iuclabs-k8s/networkpolicy.yaml} (100%) rename apps/base/iuclabs-runners/{scaleset-iuclabs-k8s.yaml => components/iuclabs-k8s/scaleset.yaml} (100%) diff --git a/apps/base/iuclabs-runners/components/iuclabs-k8s/kustomization.yaml b/apps/base/iuclabs-runners/components/iuclabs-k8s/kustomization.yaml new file mode 100644 index 0000000..3650eda --- /dev/null +++ b/apps/base/iuclabs-runners/components/iuclabs-k8s/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +resources: + - scaleset.yaml + - networkpolicy.yaml diff --git a/apps/base/iuclabs-runners/networkpolicy-iuclabs-k8s.yaml b/apps/base/iuclabs-runners/components/iuclabs-k8s/networkpolicy.yaml similarity index 100% rename from apps/base/iuclabs-runners/networkpolicy-iuclabs-k8s.yaml rename to apps/base/iuclabs-runners/components/iuclabs-k8s/networkpolicy.yaml diff --git a/apps/base/iuclabs-runners/scaleset-iuclabs-k8s.yaml b/apps/base/iuclabs-runners/components/iuclabs-k8s/scaleset.yaml similarity index 100% rename from apps/base/iuclabs-runners/scaleset-iuclabs-k8s.yaml rename to apps/base/iuclabs-runners/components/iuclabs-k8s/scaleset.yaml diff --git a/apps/base/iuclabs-runners/kustomization.yaml b/apps/base/iuclabs-runners/kustomization.yaml index 71de007..f2eff16 100644 --- a/apps/base/iuclabs-runners/kustomization.yaml +++ b/apps/base/iuclabs-runners/kustomization.yaml @@ -8,8 +8,9 @@ resources: - vaultstaticsecret.yaml - networkpolicy.yaml - rbac.yaml - - scaleset-iuclabs-k8s.yaml - - networkpolicy-iuclabs-k8s.yaml + +components: + - components/iuclabs-k8s generatorOptions: disableNameSuffixHash: true -- 2.47.3