Harden demo SSH setup for IP targets

This commit is contained in:
rcourtman 2026-07-09 17:06:52 +01:00
parent 76ced45c3a
commit 2be167331d
6 changed files with 144 additions and 85 deletions

View file

@ -947,10 +947,7 @@ func TestDeployDemoWorkflowFailsClosedForStableAndVerifiesFrontendParity(t *test
` - stable`,
`Capture expected frontend entry asset`,
`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.`,
`bash .github/scripts/setup-demo-ssh.sh`,
`SERVICE_NAME="pulse"`,
`Unsupported demo target: ${TARGET}`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
@ -991,10 +988,7 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
`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`,
`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.`,
`bash .github/scripts/setup-demo-ssh.sh`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Prepare demo host storage`,
`KEEP_BACKUPS=2`,
@ -1025,6 +1019,69 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
}
}
func TestDemoSshSetupHelperHandlesIpLiteralTargets(t *testing.T) {
helperBytes, err := os.ReadFile(repoFile(".github", "scripts", "setup-demo-ssh.sh"))
if err != nil {
t.Fatalf("read demo ssh setup helper: %v", err)
}
helper := string(helperBytes)
required := []string{
`is_ip_literal()`,
`ipaddress.ip_address(sys.argv[1])`,
`host_needs_dns=false`,
`Demo SSH host is an IP literal; skipping DNS resolution wait.`,
`[ "$host_needs_dns" = "true" ] && ! getent hosts "$DEMO_SERVER_HOST"`,
`ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"`,
`MAX_SSH_SETUP_ATTEMPTS=18`,
`Timed out waiting for the demo SSH host to become reachable and return host keys after Tailscale setup.`,
}
for _, needle := range required {
if !strings.Contains(helper, needle) {
t.Fatalf("demo ssh setup helper missing guarded IP/hostname behavior: %s", needle)
}
}
tmpDir := t.TempDir()
fakeBin := filepath.Join(tmpDir, "bin")
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
t.Fatalf("create fake bin: %v", err)
}
getentMarker := filepath.Join(tmpDir, "getent-called")
if err := os.WriteFile(filepath.Join(fakeBin, "getent"), []byte("#!/bin/sh\n: > \"$GETENT_MARKER\"\nexit 1\n"), 0o755); err != nil {
t.Fatalf("write fake getent: %v", err)
}
if err := os.WriteFile(filepath.Join(fakeBin, "ssh-keyscan"), []byte("#!/bin/sh\nprintf '100.109.163.95 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDemo\\n'\n"), 0o755); err != nil {
t.Fatalf("write fake ssh-keyscan: %v", err)
}
homeDir := filepath.Join(tmpDir, "home")
cmd := exec.Command("bash", repoFile(".github", "scripts", "setup-demo-ssh.sh"))
cmd.Env = append(os.Environ(),
"DEMO_SERVER_HOST=100.109.163.95",
"DEMO_SERVER_SSH_KEY=fake-private-key",
"GETENT_MARKER="+getentMarker,
"HOME="+homeDir,
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("demo ssh setup helper failed for IP literal: %v\n%s", err, output)
}
if _, err := os.Stat(getentMarker); !os.IsNotExist(err) {
t.Fatalf("demo ssh setup helper must not require getent hosts for IP literals; stat err=%v", err)
}
knownHosts, err := os.ReadFile(filepath.Join(homeDir, ".ssh", "known_hosts"))
if err != nil {
t.Fatalf("read generated known_hosts: %v", err)
}
if !strings.Contains(string(knownHosts), "ssh-ed25519") {
t.Fatalf("known_hosts missing captured key: %s", knownHosts)
}
if !strings.Contains(string(output), "Demo SSH host is an IP literal; skipping DNS resolution wait.") {
t.Fatalf("helper output did not report IP literal path: %s", output)
}
}
func TestDemoPublicBrowserSmokeWaitsForVisibleLoginUI(t *testing.T) {
scriptBytes, err := os.ReadFile(repoFile("scripts", "demo_public_browser_smoke.cjs"))
if err != nil {

View file

@ -526,6 +526,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
content = read(".github/workflows/create-release.yml")
update_demo_workflow = read(".github/workflows/update-demo-server.yml")
deploy_demo_workflow = read(".github/workflows/deploy-demo-server.yml")
demo_ssh_helper = read(".github/scripts/setup-demo-ssh.sh")
validation_workflow = read(".github/workflows/validate-release-assets.yml")
helper = read("scripts/trigger-release.sh")
renderer = read("scripts/release_control/render_release_body.py")
@ -612,10 +613,13 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
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)
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("bash .github/scripts/setup-demo-ssh.sh", demo_workflow)
self.assertIn("MAX_SSH_SETUP_ATTEMPTS=18", demo_ssh_helper)
self.assertIn("ipaddress.ip_address(sys.argv[1])", demo_ssh_helper)
self.assertIn("host_needs_dns=false", demo_ssh_helper)
self.assertIn('getent hosts "$DEMO_SERVER_HOST"', demo_ssh_helper)
self.assertIn('ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"', demo_ssh_helper)
self.assertIn("Timed out waiting for the demo SSH host to become reachable and return host keys after Tailscale setup", demo_ssh_helper)
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_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"', content)