Harden demo SSH setup for private deploy hosts
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Canonical Governance / governance (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/4) (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions

This commit is contained in:
rcourtman 2026-07-09 16:51:38 +01:00
parent 584aa10699
commit 76ced45c3a
5 changed files with 76 additions and 5 deletions

View file

@ -149,8 +149,35 @@ jobs:
chmod 700 ~/.ssh chmod 700 ~/.ssh
echo "$DEMO_SERVER_SSH_KEY" > ~/.ssh/id_ed25519 echo "$DEMO_SERVER_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "$DEMO_SERVER_HOST" >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts : > ~/.ssh/known_hosts
keyscan_output="$(mktemp)"
keyscan_error="$(mktemp)"
MAX_SSH_SETUP_ATTEMPTS=18
for attempt in $(seq 1 "$MAX_SSH_SETUP_ATTEMPTS"); do
if getent hosts "$DEMO_SERVER_HOST" >/dev/null 2>&1; then
if ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST" > "$keyscan_output" 2>"$keyscan_error" && [ -s "$keyscan_output" ]; then
cat "$keyscan_output" >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
echo "Demo SSH host key captured."
exit 0
fi
echo "ssh-keyscan did not return demo host keys on attempt ${attempt}/${MAX_SSH_SETUP_ATTEMPTS}."
else
echo "Demo SSH host is not resolvable yet on attempt ${attempt}/${MAX_SSH_SETUP_ATTEMPTS}."
fi
if [ "$attempt" -lt "$MAX_SSH_SETUP_ATTEMPTS" ]; then
sleep 10
fi
done
echo "::error::Timed out waiting for the demo SSH host to resolve and return host keys after Tailscale setup."
tailscale status --peers=false || true
if [ -s "$keyscan_error" ]; then
sed 's/^/ssh-keyscan: /' "$keyscan_error" || true
fi
exit 1
- name: Verify target host identity - name: Verify target host identity
env: env:

View file

@ -256,8 +256,35 @@ jobs:
chmod 700 ~/.ssh chmod 700 ~/.ssh
echo "$DEMO_SERVER_SSH_KEY" > ~/.ssh/id_ed25519 echo "$DEMO_SERVER_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "$DEMO_SERVER_HOST" >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts : > ~/.ssh/known_hosts
keyscan_output="$(mktemp)"
keyscan_error="$(mktemp)"
MAX_SSH_SETUP_ATTEMPTS=18
for attempt in $(seq 1 "$MAX_SSH_SETUP_ATTEMPTS"); do
if getent hosts "$DEMO_SERVER_HOST" >/dev/null 2>&1; then
if ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST" > "$keyscan_output" 2>"$keyscan_error" && [ -s "$keyscan_output" ]; then
cat "$keyscan_output" >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
echo "Demo SSH host key captured."
exit 0
fi
echo "ssh-keyscan did not return demo host keys on attempt ${attempt}/${MAX_SSH_SETUP_ATTEMPTS}."
else
echo "Demo SSH host is not resolvable yet on attempt ${attempt}/${MAX_SSH_SETUP_ATTEMPTS}."
fi
if [ "$attempt" -lt "$MAX_SSH_SETUP_ATTEMPTS" ]; then
sleep 10
fi
done
echo "::error::Timed out waiting for the demo SSH host to resolve and return host keys after Tailscale setup."
tailscale status --peers=false || true
if [ -s "$keyscan_error" ]; then
sed 's/^/ssh-keyscan: /' "$keyscan_error" || true
fi
exit 1
- name: Verify target host identity - name: Verify target host identity
env: env:

View file

@ -1183,7 +1183,10 @@ Those same governed demo deploy/update workflows also own the runner-to-host
network path. They must establish the canonical Tailscale connectivity step network path. They must establish the canonical Tailscale connectivity step
before SSH setup so stable or preview targets may stay on governed private before SSH setup so stable or preview targets may stay on governed private
hostnames or Tailscale IPs, rather than silently depending on public SSH hostnames or Tailscale IPs, rather than silently depending on public SSH
reachability from GitHub-hosted runners. reachability from GitHub-hosted runners. After Tailscale setup, SSH setup must
wait for the configured demo host to resolve and return host keys with bounded
retries before any installer or binary copy runs; a one-shot `ssh-keyscan`
against a private demo hostname is not sufficient release or deploy proof.
Those same workflows also own customer-visible browser truth for the public Those same workflows also own customer-visible browser truth for the public
demo shell. Health checks and entry-asset parity are necessary but not demo shell. Health checks and entry-asset parity are necessary but not
sufficient; after those checks pass, the governed helpers sufficient; after those checks pass, the governed helpers

View file

@ -947,6 +947,10 @@ func TestDeployDemoWorkflowFailsClosedForStableAndVerifiesFrontendParity(t *test
` - stable`, ` - stable`,
`Capture expected frontend entry asset`, `Capture expected frontend entry asset`,
`Verify target host identity`, `Verify target host identity`,
`MAX_SSH_SETUP_ATTEMPTS=18`,
`getent hosts "$DEMO_SERVER_HOST"`,
`ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"`,
`Timed out waiting for the demo SSH host to resolve and return host keys after Tailscale setup.`,
`SERVICE_NAME="pulse"`, `SERVICE_NAME="pulse"`,
`Unsupported demo target: ${TARGET}`, `Unsupported demo target: ${TARGET}`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`, `Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
@ -987,6 +991,10 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
`go run ./scripts/release_update_key.go public-key-ssh`, `go run ./scripts/release_update_key.go public-key-ssh`,
`sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"|" /tmp/pulse-install.sh`, `sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"|" /tmp/pulse-install.sh`,
`Verify target host identity`, `Verify target host identity`,
`MAX_SSH_SETUP_ATTEMPTS=18`,
`getent hosts "$DEMO_SERVER_HOST"`,
`ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"`,
`Timed out waiting for the demo SSH host to resolve and return host keys after Tailscale setup.`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`, `Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Prepare demo host storage`, `Prepare demo host storage`,
`KEEP_BACKUPS=2`, `KEEP_BACKUPS=2`,

View file

@ -525,6 +525,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
def test_release_workflow_enforces_rc_lineage_soak_and_v5_notice(self) -> None: def test_release_workflow_enforces_rc_lineage_soak_and_v5_notice(self) -> None:
content = read(".github/workflows/create-release.yml") content = read(".github/workflows/create-release.yml")
update_demo_workflow = read(".github/workflows/update-demo-server.yml") update_demo_workflow = read(".github/workflows/update-demo-server.yml")
deploy_demo_workflow = read(".github/workflows/deploy-demo-server.yml")
validation_workflow = read(".github/workflows/validate-release-assets.yml") validation_workflow = read(".github/workflows/validate-release-assets.yml")
helper = read("scripts/trigger-release.sh") helper = read("scripts/trigger-release.sh")
renderer = read("scripts/release_control/render_release_body.py") renderer = read("scripts/release_control/render_release_body.py")
@ -610,6 +611,11 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("does not trust the configured release signing key", content) self.assertIn("does not trust the configured release signing key", content)
self.assertIn("TRUSTED_SSH_PUBLIC_KEY", update_demo_workflow) self.assertIn("TRUSTED_SSH_PUBLIC_KEY", update_demo_workflow)
self.assertIn('sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\\"${TRUSTED_SSH_PUBLIC_KEY}\\"|" /tmp/pulse-install.sh', update_demo_workflow) self.assertIn('sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\\"${TRUSTED_SSH_PUBLIC_KEY}\\"|" /tmp/pulse-install.sh', update_demo_workflow)
for demo_workflow in (update_demo_workflow, deploy_demo_workflow):
self.assertIn("MAX_SSH_SETUP_ATTEMPTS=18", demo_workflow)
self.assertIn('getent hosts "$DEMO_SERVER_HOST"', demo_workflow)
self.assertIn('ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"', demo_workflow)
self.assertIn("Timed out waiting for the demo SSH host to resolve and return host keys after Tailscale setup", demo_workflow)
self.assertIn("derive the OpenSSH installer trust key from `PULSE_UPDATE_SIGNING_PUBLIC_KEY`", normalize_ws(contract)) self.assertIn("derive the OpenSSH installer trust key from `PULSE_UPDATE_SIGNING_PUBLIC_KEY`", normalize_ws(contract))
self.assertIn('SYFT_VERSION="1.42.4"', content) self.assertIn('SYFT_VERSION="1.42.4"', content)
self.assertIn('SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"', content) self.assertIn('SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"', content)