feat(cli): enable GCP Shielded VM (Secure Boot) by default

Provision every GCP instance as a Shielded VM (Secure Boot + vTPM +
integrity monitoring) by default. This is what the Cloudflare (CF) skill
needs to attest the VM, and is good hygiene for all spawns. GCP's default
Ubuntu LTS images are Shielded-VM-compatible, so boots are unaffected.

Opt out with the new --no-secure-boot flag (sets GCP_NO_SECURE_BOOT=1)
for custom images that are not UEFI/Secure-Boot-capable.

Secure Boot is GCP-only here; AWS spawns run on Lightsail, which does not
expose Secure Boot, so the flag is a no-op on other clouds.
This commit is contained in:
spa-the-spawn-maintainer[bot] 2026-06-06 01:31:05 +00:00 committed by GitHub
parent 2be1a78dce
commit 585d3466e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 80 additions and 1 deletions

View file

@ -54,6 +54,7 @@ spawn delete -c hetzner # Delete a server on Hetzner
| `spawn <agent> <cloud> --config <file>` | Load options from a JSON config file |
| `spawn <agent> <cloud> --steps <list>` | Comma-separated setup steps to enable |
| `spawn <agent> <cloud> --custom` | Show interactive size/region pickers |
| `spawn <agent> gcp --no-secure-boot` | Disable GCP Shielded VM (Secure Boot is on by default) |
| `spawn <agent>` | Show available clouds for an agent |
| `spawn <cloud>` | Show available agents for a cloud |
| `spawn matrix` | Full agent x cloud matrix |
@ -159,6 +160,22 @@ spawn claude gcp --beta tarball --beta parallel
`--fast` enables `tarball`, `images`, and `parallel` (not `recursive`).
#### Secure Boot (GCP)
GCP instances are provisioned as [Shielded VMs](https://cloud.google.com/security/shielded-cloud/shielded-vm)
**by default** — Secure Boot, vTPM, and integrity monitoring are all enabled. This
hardens the boot chain and is required for the Cloudflare (CF) skill to attest the VM.
GCP's default Ubuntu LTS images are Shielded-VM-compatible, so this works out of the box.
Opt out for a custom image that is not UEFI/Secure-Boot-capable:
```bash
spawn claude gcp --no-secure-boot
```
> Secure Boot is a GCP-only feature here. AWS spawns run on Lightsail, which does not
> expose Secure Boot; the flag is a no-op on other clouds.
#### Recursive Spawn
Use `--beta recursive` to let spawned VMs create their own child VMs:

View file

@ -3,7 +3,7 @@ import { mockBunSpawn, mockClackPrompts } from "./test-helpers";
mockClackPrompts();
import { DEFAULT_MACHINE_TYPE, DEFAULT_ZONE, getConnectionInfo } from "../gcp/gcp";
import { buildShieldedArgs, DEFAULT_MACHINE_TYPE, DEFAULT_ZONE, getConnectionInfo } from "../gcp/gcp";
// ─── Helpers ─────────────────────────────────────────────────────────────────
@ -82,6 +82,31 @@ describe("gcp/getConnectionInfo", () => {
});
});
// ─── buildShieldedArgs (Secure Boot default) ─────────────────────────────────
describe("gcp/buildShieldedArgs", () => {
it("enables Shielded VM (Secure Boot) by default", () => {
delete process.env.GCP_NO_SECURE_BOOT;
expect(buildShieldedArgs()).toEqual([
"--shielded-secure-boot",
"--shielded-vtpm",
"--shielded-integrity-monitoring",
]);
});
it("returns no shielded flags when GCP_NO_SECURE_BOOT=1", () => {
process.env.GCP_NO_SECURE_BOOT = "1";
expect(buildShieldedArgs()).toEqual([]);
});
it("only opts out for the exact value '1' (any other value stays on)", () => {
process.env.GCP_NO_SECURE_BOOT = "0";
expect(buildShieldedArgs()).toContain("--shielded-secure-boot");
process.env.GCP_NO_SECURE_BOOT = "true";
expect(buildShieldedArgs()).toContain("--shielded-secure-boot");
});
});
// ─── promptMachineType ───────────────────────────────────────────────────────
describe("gcp/promptMachineType", () => {

View file

@ -87,6 +87,7 @@ describe("Unknown Flag Detection", () => {
"--debug",
"--name",
"--reauth",
"--no-secure-boot",
"--prune",
"--json",
"--yes",
@ -216,6 +217,7 @@ describe("KNOWN_FLAGS completeness", () => {
"--clear",
"--custom",
"--reauth",
"--no-secure-boot",
"--zone",
"--region",
"--machine-type",

View file

@ -24,6 +24,7 @@ export const KNOWN_FLAGS = new Set([
"--clear",
"--custom",
"--reauth",
"--no-secure-boot",
"--zone",
"--region",
"--machine-type",

View file

@ -148,6 +148,30 @@ export const DEFAULT_ZONE = "us-central1-a";
export const DEFAULT_DISK_SIZE_GB = 40;
// ─── Shielded VM (Secure Boot) ───────────────────────────────────────────────
/**
* GCP Shielded VM flags, enabled by DEFAULT on every spawned instance.
*
* Secure Boot + a measured (vTPM) and integrity-monitored boot chain is what
* the Cloudflare (CF) skill needs to attest the VM, and it is good hygiene for
* every spawn. GCP's Ubuntu LTS images (the default here) are
* Shielded-VM-compatible, so turning this on does not break boots.
*
* Opt out with the `--no-secure-boot` CLI flag (sets `GCP_NO_SECURE_BOOT=1`),
* for the rare case of a custom image that is not UEFI/Secure-Boot-capable.
*/
export function buildShieldedArgs(): string[] {
if (process.env.GCP_NO_SECURE_BOOT === "1") {
return [];
}
return [
"--shielded-secure-boot",
"--shielded-vtpm",
"--shielded-integrity-monitoring",
];
}
// ─── State ──────────────────────────────────────────────────────────────────
interface GcpState {
@ -781,6 +805,7 @@ export async function createInstance(
]
: []),
`--metadata=ssh-keys=${sshKeysMetadata}`,
...buildShieldedArgs(),
`--project=${_state.project}`,
"--quiet",
];

View file

@ -146,6 +146,7 @@ function checkUnknownFlags(args: string[]): void {
console.error(` ${pc.cyan("--model, -m <id>")} Set the LLM model (e.g. openai/gpt-5.3-codex)`);
console.error(` ${pc.cyan("--name")} Set the spawn/resource name`);
console.error(` ${pc.cyan("--reauth")} Force re-prompting for cloud credentials`);
console.error(` ${pc.cyan("--no-secure-boot")} Disable GCP Shielded VM (Secure Boot, on by default)`);
console.error(` ${pc.cyan("--config <path>")} Load config from JSON file`);
console.error(` ${pc.cyan("--steps <list>")} Comma-separated setup steps to enable`);
console.error(` ${pc.cyan("--repo <slug|url>")} Clone a template repo and apply spawn.md`);
@ -928,6 +929,14 @@ async function main(): Promise<void> {
process.env.SPAWN_REAUTH = "1";
}
// Extract --no-secure-boot boolean flag — opt out of GCP Shielded VM
// (Secure Boot + vTPM + integrity monitoring), which is on by default.
const noSecureBootIdx = filteredArgs.indexOf("--no-secure-boot");
if (noSecureBootIdx !== -1) {
filteredArgs.splice(noSecureBootIdx, 1);
process.env.GCP_NO_SECURE_BOOT = "1";
}
// Extract --fast boolean flag — enables images + tarballs + parallel setup
const fastIdx = filteredArgs.indexOf("--fast");
if (fastIdx !== -1) {