Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0926888
feat(range): authorize OpenVPN pool sessions and retire per-range gat…
Brad-Edwards Oct 5, 2026
8c95832
feat(range): add the shared OpenVPN pool server
Brad-Edwards Oct 5, 2026
6f8309a
feat(infra): deploy the shared OpenVPN pool on GCP
Brad-Edwards Oct 5, 2026
db06120
docs(adr): record the shared OpenVPN pool
Brad-Edwards Oct 5, 2026
a1a6906
Merge remote-tracking branch 'origin/dev' into 2480-vpn-server-pool
Brad-Edwards Oct 5, 2026
181e547
fix(range): close the pool server's HTTP client and pin the pool enve…
Brad-Edwards Oct 5, 2026
7a184da
fix(range): run the pool server unprivileged and clear its Sonar find…
Brad-Edwards Oct 5, 2026
a05e97f
style(range): move a trailing comment in the pool portal client
Brad-Edwards Oct 5, 2026
dd2433b
Merge remote-tracking branch 'origin/dev' into 2480-vpn-server-pool
Brad-Edwards Oct 5, 2026
24c0309
fix(range): tell pool clients to reconnect when a server stops
Brad-Edwards Oct 5, 2026
93f1b06
Merge remote-tracking branch 'origin/dev' into 2480-vpn-server-pool
Brad-Edwards Oct 5, 2026
63a5bf6
Merge remote-tracking branch 'origin/dev' into 2480-vpn-server-pool
Brad-Edwards Oct 5, 2026
33d8a71
Merge remote-tracking branch 'origin/dev' into 2480-vpn-server-pool
Brad-Edwards Oct 5, 2026
a418416
fix(range): shorten the pool keepalive to 5/30 seconds
Brad-Edwards Oct 5, 2026
e522f2d
style(range): document the pool server's stop handler
Brad-Edwards Oct 5, 2026
b85875d
Merge remote-tracking branch 'origin/dev' into 2480-vpn-server-pool
Brad-Edwards Oct 5, 2026
a17fa72
fix(deps): bump postcss-selector-parser to 7.1.6 (GHSA-rj75-hqrm-r3gf)
Brad-Edwards Oct 5, 2026
3694ff9
test(engine): run the broker stream-fence test in the PostgreSQL lane
Brad-Edwards Oct 5, 2026
428e115
docs(adr): park the model access broker (ADR-068 supersedes ADR-067)
Brad-Edwards Oct 5, 2026
84aba98
Merge remote-tracking branch 'origin/dev' into 2480-vpn-server-pool
Brad-Edwards Oct 5, 2026
a40322f
fix(deps): bump proxy-addr to 2.0.8 and source-map-js to 1.2.2
Brad-Edwards Oct 5, 2026
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
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ updates:
- "minor"
- "patch"

- package-ecosystem: "uv"
directory: "/shifter/engine/openvpn"
target-branch: "dev"
commit-message:
prefix: "chore"
include: "scope"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
python-minor-and-patch:
update-types:
- "minor"
- "patch"

- package-ecosystem: "uv"
directory: "/scripts/bootstrap"
target-branch: "dev"
Expand Down Expand Up @@ -397,6 +412,7 @@ updates:
- "/shifter/engine/provisioner"
- "/shifter/engine/guacd"
- "/shifter/engine/guacamole"
- "/shifter/engine/openvpn"
target-branch: "dev"
commit-message:
prefix: "chore"
Expand Down
9 changes: 9 additions & 0 deletions .github/quality-path-filters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ quality_units:
security: [packer-sast]
test: [packer-tests]

- id: openvpn
paths:
- shifter/engine/openvpn/**
sonar: true
responsibilities:
lint: [openvpn-lint]
security: [openvpn-sast]
test: [openvpn-tests]

- id: bootstrap
paths:
- scripts/bootstrap/**
Expand Down
123 changes: 123 additions & 0 deletions .github/workflows/_gcp-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ jobs:
provisioner_digest: ${{ steps.build-provisioner.outputs.digest }}
guacd_digest: ${{ steps.build-guacd.outputs.digest }}
guacamole_client_digest: ${{ steps.build-guacamole-client.outputs.digest }}
openvpn_digest: ${{ steps.build-openvpn.outputs.digest }}
env:
GCP_ENVIRONMENT: ${{ inputs.environment }}
USE_GKE_GCLOUD_AUTH_PLUGIN: "True"
Expand Down Expand Up @@ -226,6 +227,8 @@ jobs:
GCP_BOOTSTRAP_ADMIN_EMAIL: ${{ secrets.GCP_BOOTSTRAP_ADMIN_EMAIL }}
GCP_BOOTSTRAP_ADMIN_PASSWORD: ${{ secrets.GCP_BOOTSTRAP_ADMIN_PASSWORD }}
GCP_CTF_CONTENT_BUCKET: ${{ vars.GCP_CTF_CONTENT_BUCKET }}
# Participant OpenVPN opt-in also deploys the shared pool (#2480).
RANGE_OPENVPN_ENABLED: ${{ vars.RANGE_OPENVPN_ENABLED }}
# JSON tfvars gives Terraform exact escaping without interpolating
# secret values into HCL or command arguments. The file lives outside
# the persistent checkout and is explicitly removed at job end (#2084).
Expand Down Expand Up @@ -265,6 +268,10 @@ jobs:
"ctf_content_bucket_name": os.environ.get("GCP_CTF_CONTENT_BUCKET", ""),
}
values.update({key: value for key, value in optional.items() if value})
openvpn = os.environ.get("RANGE_OPENVPN_ENABLED", "").strip().lower() or "false"
if openvpn not in {"true", "false"}:
raise SystemExit("::error::RANGE_OPENVPN_ENABLED must be true or false")
values["openvpn_pool_enabled"] = openvpn == "true"
descriptor = os.open(
os.environ["TF_VARS_FILE"],
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
Expand Down Expand Up @@ -528,6 +535,7 @@ jobs:
env_file.write(f"GUACD_IMAGE_ROOT={image_roots['guacd']}\n")
env_file.write(f"GUACAMOLE_CLIENT_IMAGE_ROOT={image_roots['guacamole-client']}\n")
env_file.write(f"PROVISIONER_IMAGE_ROOT={image_roots['pulumi-provisioner']}\n")
env_file.write(f"OPENVPN_IMAGE_ROOT={image_roots['openvpn']}\n")
env_file.write(f"ARTIFACT_REGISTRY_HOST={image_roots['portal'].split('/')[0]}\n")
env_file.write(f"PUBLIC_HOSTNAME={value('public_hostname')}\n")
PY
Expand Down Expand Up @@ -587,6 +595,17 @@ jobs:
no-cache: true
provenance: mode=max
sbom: true
- name: Build and push OpenVPN pool image
id: build-openvpn
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25
with:
context: ./shifter/engine/openvpn
push: true
tags: |
${{ env.OPENVPN_IMAGE_ROOT }}:${{ env.SHORT_SHA }}
no-cache: true
provenance: mode=max
sbom: true
- name: Attest build provenance (portal)
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
Expand All @@ -611,6 +630,12 @@ jobs:
subject-name: ${{ env.GUACAMOLE_CLIENT_IMAGE_ROOT }}
subject-digest: ${{ steps.build-guacamole-client.outputs.digest }}
push-to-registry: false
- name: Attest build provenance (openvpn)
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ env.OPENVPN_IMAGE_ROOT }}
subject-digest: ${{ steps.build-openvpn.outputs.digest }}
push-to-registry: false
- name: Remove ephemeral preparation inputs
if: always()
run: |
Expand Down Expand Up @@ -664,6 +689,7 @@ jobs:
PROVISIONER_IMAGE_DIGEST: ${{ needs.prepare.outputs.provisioner_digest }}
GUACD_IMAGE_DIGEST: ${{ needs.prepare.outputs.guacd_digest }}
GUACAMOLE_CLIENT_IMAGE_DIGEST: ${{ needs.prepare.outputs.guacamole_client_digest }}
OPENVPN_IMAGE_DIGEST: ${{ needs.prepare.outputs.openvpn_digest }}
run: |
set -euo pipefail
prefix="shifter-${GCP_ENVIRONMENT}"
Expand All @@ -674,10 +700,12 @@ jobs:
echo "PROVISIONER_IMAGE_ROOT=${registry}/${GCP_PROJECT_ID}/${prefix}-pulumi-provisioner/pulumi-provisioner"
echo "GUACD_IMAGE_ROOT=${registry}/${GCP_PROJECT_ID}/${prefix}-guacd/guacd"
echo "GUACAMOLE_CLIENT_IMAGE_ROOT=${registry}/${GCP_PROJECT_ID}/${prefix}-guacamole-client/guacamole-client"
echo "OPENVPN_IMAGE_ROOT=${registry}/${GCP_PROJECT_ID}/${prefix}-openvpn/openvpn"
echo "PORTAL_IMAGE_DIGEST=${PORTAL_IMAGE_DIGEST}"
echo "PROVISIONER_IMAGE_DIGEST=${PROVISIONER_IMAGE_DIGEST}"
echo "GUACD_IMAGE_DIGEST=${GUACD_IMAGE_DIGEST}"
echo "GUACAMOLE_CLIENT_IMAGE_DIGEST=${GUACAMOLE_CLIENT_IMAGE_DIGEST}"
echo "OPENVPN_IMAGE_DIGEST=${OPENVPN_IMAGE_DIGEST}"
} >> "${GITHUB_ENV}"
- name: Login to Artifact Registry with scanner identity
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
Expand Down Expand Up @@ -713,12 +741,14 @@ jobs:
"provisioner": os.environ["PROVISIONER_IMAGE_ROOT"],
"guacd": os.environ["GUACD_IMAGE_ROOT"],
"guacamole-client": os.environ["GUACAMOLE_CLIENT_IMAGE_ROOT"],
"openvpn": os.environ["OPENVPN_IMAGE_ROOT"],
}
digests = {
"portal": os.environ["PORTAL_IMAGE_DIGEST"],
"provisioner": os.environ["PROVISIONER_IMAGE_DIGEST"],
"guacd": os.environ["GUACD_IMAGE_DIGEST"],
"guacamole-client": os.environ["GUACAMOLE_CLIENT_IMAGE_DIGEST"],
"openvpn": os.environ["OPENVPN_IMAGE_DIGEST"],
}
invalid = [name for name, value in digests.items() if len(value) != 71 or not value.startswith("sha256:")]
if invalid:
Expand Down Expand Up @@ -750,6 +780,7 @@ jobs:
scan_image provisioner "${PROVISIONER_IMAGE_ROOT}" "${PROVISIONER_IMAGE_DIGEST}"
scan_image guacd "${GUACD_IMAGE_ROOT}" "${GUACD_IMAGE_DIGEST}"
scan_image guacamole-client "${GUACAMOLE_CLIENT_IMAGE_ROOT}" "${GUACAMOLE_CLIENT_IMAGE_DIGEST}"
scan_image openvpn "${OPENVPN_IMAGE_ROOT}" "${OPENVPN_IMAGE_DIGEST}"
exit "${scan_status}"
- name: Store raw scan evidence privately
id: store_scan_evidence
Expand Down Expand Up @@ -807,6 +838,7 @@ jobs:
"${RUNNER_TEMP}/gcp-release-security/provisioner-trivy.json" \
"${RUNNER_TEMP}/gcp-release-security/guacd-trivy.json" \
"${RUNNER_TEMP}/gcp-release-security/guacamole-client-trivy.json" \
"${RUNNER_TEMP}/gcp-release-security/openvpn-trivy.json" \
"${RUNNER_TEMP}/gcp-release-scan-verdict.json"
rmdir "${RUNNER_TEMP}/trivy-bin" 2>/dev/null || true
rmdir "${RUNNER_TEMP}/gcp-release-security" 2>/dev/null || true
Expand Down Expand Up @@ -944,6 +976,7 @@ jobs:
env_file.write(f"GUACD_IMAGE_ROOT={image_roots['guacd']}\n")
env_file.write(f"GUACAMOLE_CLIENT_IMAGE_ROOT={image_roots['guacamole-client']}\n")
env_file.write(f"PROVISIONER_IMAGE_ROOT={image_roots['pulumi-provisioner']}\n")
env_file.write(f"OPENVPN_IMAGE_ROOT={image_roots['openvpn']}\n")
env_file.write(f"PUBLIC_HOSTNAME={value('public_hostname')}\n")
PY
- name: Rewrite overlay image project and service accounts
Expand Down Expand Up @@ -1100,6 +1133,7 @@ jobs:
PROVISIONER_DIGEST: ${{ needs.prepare.outputs.provisioner_digest }}
GUACD_DIGEST: ${{ needs.prepare.outputs.guacd_digest }}
GUACAMOLE_CLIENT_DIGEST: ${{ needs.prepare.outputs.guacamole_client_digest }}
OPENVPN_DIGEST: ${{ needs.prepare.outputs.openvpn_digest }}
run: |
# ADR-037-R6: verify the GitHub OIDC-signed provenance attestation for
# each built image@digest and the fixed Brad-Edwards/shifter identity
Expand All @@ -1126,6 +1160,28 @@ jobs:
verify "${PROVISIONER_IMAGE_ROOT}" "${PROVISIONER_DIGEST}" "provisioner"
verify "${GUACD_IMAGE_ROOT}" "${GUACD_DIGEST}" "guacd"
verify "${GUACAMOLE_CLIENT_IMAGE_ROOT}" "${GUACAMOLE_CLIENT_DIGEST}" "guacamole-client"
verify "${OPENVPN_IMAGE_ROOT}" "${OPENVPN_DIGEST}" "openvpn"

- name: Ensure OpenVPN pool PKI
id: vpn_pki
run: |
# #2480: the tenant CA and the pool server identity live only in Secret
# Manager (never Terraform state). Runs before the control plane
# restarts so the provisioner can sign participant profiles at once.
set -euo pipefail
pool="$(jq -c '.openvpn_pool.value // empty' /tmp/gcp-terraform-outputs.json)"
if [ -z "${pool}" ]; then
echo "enabled=false" >> "${GITHUB_OUTPUT}"
exit 0
fi
issuer="$(jq -r '.issuer_secret' <<< "${pool}")"
server="$(jq -r '.server_secret' <<< "${pool}")"
uv run --project scripts/gcp python scripts/gcp/ensure_vpn_pki.py \
--project "${PROJECT_ID}" \
--issuer-secret "${issuer##*/}" \
--server-secret "${server##*/}" \
--changed-output "${GITHUB_OUTPUT}"
echo "enabled=true" >> "${GITHUB_OUTPUT}"

- name: Render and apply optional model broker from the canonical chart
env:
Expand Down Expand Up @@ -1373,6 +1429,73 @@ jobs:
fi
sleep 5
done
- name: Roll out the OpenVPN pool release
if: steps.vpn_pki.outputs.enabled == 'true'
env:
OPENVPN_DIGEST: ${{ needs.prepare.outputs.openvpn_digest }}
PKI_CHANGED: ${{ steps.vpn_pki.outputs.changed }}
run: |
# #2480: pool servers run only the image named by the release record,
# and only this job writes it, after the exact-digest scan and the
# provenance check above. A new record or server identity replaces
# every server, surging new ones in before old ones leave.
set -euo pipefail
if [[ ! "${OPENVPN_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "::error::openvpn build did not produce a valid image digest"
exit 1
fi
pool="$(jq -c '.openvpn_pool.value' /tmp/gcp-terraform-outputs.json)"
field() { jq -r --arg key "$1" '.[$key]' <<< "${pool}"; }
if [ "$(field image_root)" != "${OPENVPN_IMAGE_ROOT}" ]; then
echo "::error::the pool image root does not match the built openvpn image root"
exit 1
fi
release_secret="$(field release_secret)"
release_secret="${release_secret##*/}"
group="$(field instance_group)"
region="$(field region)"
release="${OPENVPN_IMAGE_ROOT}@${OPENVPN_DIGEST}"
errors="${RUNNER_TEMP}/openvpn-release.err"
if current="$(gcloud secrets versions access latest --secret="${release_secret}" --project="${PROJECT_ID}" 2>"${errors}")"; then
:
elif grep -q NOT_FOUND "${errors}"; then
current=""
else
cat "${errors}" >&2
exit 1
fi
replace="${PKI_CHANGED:-false}"
if [ "${current}" != "${release}" ]; then
printf '%s' "${release}" | gcloud secrets versions add "${release_secret}" --project="${PROJECT_ID}" --data-file=-
replace=true
fi
if [ "${replace}" = true ]; then
gcloud compute instance-groups managed rolling-action replace "${group}" \
--region="${region}" --project="${PROJECT_ID}" --max-unavailable=0
fi
gcloud compute instance-groups managed wait-until "${group}" \
--version-target-reached --region="${region}" --project="${PROJECT_ID}" --timeout=1800
gcloud compute instance-groups managed wait-until "${group}" \
--stable --region="${region}" --project="${PROJECT_ID}" --timeout=1800
backend="$(field backend_service)"
deadline=$((SECONDS + 600))
until health="$(gcloud compute backend-services get-health "${backend}" --region="${region}" \
--project="${PROJECT_ID}" --format=json)" \
&& jq -e '[.[].status.healthStatus[]?.healthState] | length > 0 and all(. == "HEALTHY")' <<< "${health}" >/dev/null; do
if [ "${SECONDS}" -ge "${deadline}" ]; then
echo "::error::OpenVPN pool servers did not all report healthy within 10 minutes."
exit 1
fi
sleep 15
done
# The pool reads only the latest record; superseded ones are history.
gcloud secrets versions list "${release_secret}" --project="${PROJECT_ID}" \
--filter=state:ENABLED --sort-by=~createTime --format='value(name)' \
| tail -n +2 | while read -r version; do
gcloud secrets versions destroy "${version##*/}" --secret="${release_secret}" \
--project="${PROJECT_ID}" --quiet
done
rm -f "${errors}"
- name: Record running workload image IDs
env:
PORTAL_IMAGE_DIGEST: ${{ needs.prepare.outputs.portal_digest }}
Expand Down
Loading
Loading