Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ dev = ["ruff>=0.8.0", "ty>=0.0.1a1"]
[project.scripts]
seismic-deploy = "deploy_gcp.seismic_deploy.cli:cli"
# Operator-facing: act on your own node. Cloud-agnostic, never wraps Pulumi.
seismic-tee = "tee.cli.node.app:app"
seismic-tee-node = "tee.cli.node.app:app"
# Seismic-internal: bootstrap a network (provision cohort + genesis ceremony).
seismic-tee-bootstrap = "tee.cli.network.app:app"
seismic-tee-network = "tee.cli.network.app:app"

[tool.setuptools.packages.find]
include = ["deploy_gcp*", "tee*"]
Expand Down
64 changes: 32 additions & 32 deletions tee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ service starts.

This README is intentionally architectural. By the end you should
understand the layering and the two CLIs split by audience —
`seismic-tee` (operator-facing: act on your own node) and
`seismic-tee-bootstrap` (Seismic-internal: found a network — provision a
`seismic-tee-node` (operator-facing: act on your own node) and
`seismic-tee-network` (Seismic-internal: found a network — provision a
cohort and run the genesis ceremony) — what each does vs. what the
operator does, and what's still pending cleanup.

Expand All @@ -19,7 +19,7 @@ operator does, and what's still pending cleanup.
A "Seismic TEE node" is a TDX-confidential VM running a [seismic-images](https://github.com/SeismicSystems/seismic-images) mkosi image.

On first boot, the VM runs a small HTTP service `tdx-init` that's waiting on port `:8080`
for its [InitConfig](https://github.com/SeismicSystems/enclave/blob/seismic/crates/tdx-init/src/config.rs#L8). Attesting the VM and POSTing that config is the job of the `seismic-tee configure` command.
for its [InitConfig](https://github.com/SeismicSystems/enclave/blob/seismic/crates/tdx-init/src/config.rs#L8). Attesting the VM and POSTing that config is the job of the `seismic-tee-node configure` command.

## Prerequisites

Expand All @@ -29,10 +29,10 @@ for its [InitConfig](https://github.com/SeismicSystems/enclave/blob/seismic/crat
never provisions; it consumes a descriptor of an already-running node.
VHD upload to the Azure image registry is a seismic-images concern
(`make push-azure-*`), not this tool.
- **For `seismic-tee-bootstrap genesis-ceremony`:** the `genesis` binary (built from
- **For `seismic-tee-network genesis-ceremony`:** the `genesis` binary (built from
summit) on PATH. (The summit genesis template defaults to the
artifact-set copy beside `--manifest`.)
- **For `seismic-tee-bootstrap up`** (cohort provisioning): the `pulumi`
- **For `seismic-tee-network up`** (cohort provisioning): the `pulumi`
CLI on PATH. The passphrase secrets provider needs
`PULUMI_CONFIG_PASSPHRASE` — set it, or let `up`/`down` prompt you on
first run (empty = no passphrase). Required as an env var only in
Expand All @@ -50,30 +50,30 @@ uv sync
The two CLIs are then available:

```bash
uv run seismic-tee --help # operator-facing
uv run seismic-tee-bootstrap --help # Seismic-internal
uv run seismic-tee-node --help # operator-facing
uv run seismic-tee-network --help # Seismic-internal
```

## How it's organized: two CLIs, by audience

The split is "act on my own node" vs "bring a whole network into
existence." Only the first is ever external, so it's its own CLI; the
second is Seismic-internal. The boundary between them is the **node
descriptor** file: `seismic-tee-bootstrap` *produces* descriptors (by
provisioning) and *consumes* them (genesis ceremony), while `seismic-tee`
descriptor** file: `seismic-tee-network` *produces* descriptors (by
provisioning) and *consumes* them (genesis ceremony), while `seismic-tee-node`
only consumes one to reach an already-running node.

| CLI | Command | Run by | Purpose |
|---|---|---|---|
| `seismic-tee` | `configure` | any operator, on first boot | POST the node TOML to tdx-init. Reads the node address from `--node <descriptor.json>`. (Attestation verification is disabled pending attested-tls — see below.) |
| `seismic-tee-bootstrap` | `up` / `down` | Seismic, internal | Provision / tear down a cohort of TDX nodes, one independent Pulumi stack each (drives Pulumi via the Automation API). |
| `seismic-tee-bootstrap` | `genesis-ceremony` | Seismic, once per network | Gather the cohort's summit pubkeys, build `genesis.toml`, POST it to each node. One-shot: `send_genesis` only exists in the pre-genesis node API. |
| `seismic-tee-bootstrap` | `manifest` | Seismic | Assemble / validate the network manifest. |
| `seismic-tee-node` | `configure` | any operator, on first boot | POST the node TOML to tdx-init. Reads the node address from `--node <descriptor.json>`. (Attestation verification is disabled pending attested-tls — see below.) |
| `seismic-tee-network` | `up` / `down` | Seismic, internal | Provision / tear down a cohort of TDX nodes, one independent Pulumi stack each (drives Pulumi via the Automation API). |
| `seismic-tee-network` | `genesis-ceremony` | Seismic, once per network | Gather the cohort's summit pubkeys, build `genesis.toml`, POST it to each node. One-shot: `send_genesis` only exists in the pre-genesis node API. |
| `seismic-tee-network` | `manifest` | Seismic | Assemble / validate the network manifest. |

Only `seismic-tee` is cloud-agnostic and **never wraps Pulumi**;
`seismic-tee-bootstrap` is the one allowed to. A **node descriptor** is a
Only `seismic-tee-node` is cloud-agnostic and **never wraps Pulumi**;
`seismic-tee-network` is the one allowed to. A **node descriptor** is a
small JSON (`public_ip`/`fqdn`) describing one provisioned node — see
`tee/cli/common/descriptor.py`. It's what `seismic-tee-bootstrap up` emits
`tee/cli/common/descriptor.py`. It's what `seismic-tee-network up` emits
(just `{public_ip, fqdn}`); a BYO-infra operator can hand-write it (or use
`pulumi stack output --json` — only those two keys are read).

Expand All @@ -89,9 +89,9 @@ front-ends over the same state.
Longer-term these two CLIs are intended to live in **two repos**, split on
the same audience line:

- **Public (operator):** `seismic-tee` + the `pulumi/seismic_node/`
- **Public (operator):** `seismic-tee-node` + the `pulumi/seismic_node/`
program — the primitives for configuring and provisioning *one* node.
- **Private (this repo):** `seismic-tee-bootstrap` + `pulumi/playground/`
- **Private (this repo):** `seismic-tee-network` + `pulumi/playground/`
— founding a *network* (orchestrating a cohort, the genesis ceremony),
which only Seismic does.

Expand All @@ -118,7 +118,7 @@ cd pulumi/seismic_node
pulumi up --stack my-node && pulumi stack output --json --stack my-node > /tmp/node.json
cd -

uv run seismic-tee configure --node /tmp/node.json \
uv run seismic-tee-node configure --node /tmp/node.json \
--peer http://<bootnode-ip>:7878 --manifest ./network-manifest.json
```

Expand All @@ -129,7 +129,7 @@ identity doc you're given for the network you're joining; it pins the
`network_id`. The reth genesis JSON is POSTed alongside it (from
`--reth-genesis`, defaulting to `reth-genesis.json` beside the manifest —
the artifact-set layout); tdx-init writes it for reth's `--chain`. You
never run `seismic-tee-bootstrap` — that's Seismic-internal network
never run `seismic-tee-network` — that's Seismic-internal network
creation.

### Creating a new network (genesis ceremony)
Expand All @@ -156,12 +156,12 @@ template identity that can also found a throwaway test devnet (see
# regenerated from the inputs — the directory is everything needed to
# (re)configure, join, or debug the network later. assemble needs
# `seismic-reth` on PATH for the offline genesis-hash gate.
uv run seismic-tee-bootstrap manifest init tee/networks/devnet-3 \
uv run seismic-tee-network manifest init tee/networks/devnet-3 \
--reth-genesis ../seismic-reth/crates/seismic/chainspec/res/genesis/dev.json \
--measurements ../seismic-images/build/measurements.json \
--measurement-id seismic_2026-06-11.abc123.vhd
# → edit tee/networks/devnet-3/summit-template.toml (namespace, …)
uv run seismic-tee-bootstrap manifest assemble tee/networks/devnet-3
uv run seismic-tee-network manifest assemble tee/networks/devnet-3

# 2. Provision the cohort — one independent Pulumi stack per node — and
# capture each node's descriptor. Shared settings inherit from the dev
Expand All @@ -175,15 +175,15 @@ uv run seismic-tee-bootstrap manifest assemble tee/networks/devnet-3
# writes the descriptors into <dir>/nodes/ (gitignored: infra state, not
# identity). Without --network they land in a shared gitignored
# descriptors/ dir next to the Pulumi project; --out-dir overrides either.
uv run seismic-tee-bootstrap up --count 2 --network tee/networks/devnet-3
uv run seismic-tee-network up --count 2 --network tee/networks/devnet-3
# → tee/networks/devnet-3/nodes/dev-bootstrap-node-{1,2}.json

# 3. Configure the whole cohort in one command: exactly one genesis node
# (mints root_key) plus N joiners pointed at it (each fetches root_key
# from node 1 over the attested handshake); every first-boot disk wipe
# is watched in parallel. (A node joining the live network later uses
# `seismic-tee configure` instead — see "Joining an existing network".)
uv run seismic-tee-bootstrap configure \
# `seismic-tee-node configure` instead — see "Joining an existing network".)
uv run seismic-tee-network configure \
--genesis tee/networks/devnet-3/nodes/dev-bootstrap-node-1.json \
--join tee/networks/devnet-3/nodes/dev-bootstrap-node-2.json \
--manifest tee/networks/devnet-3/network-manifest.json
Expand All @@ -198,7 +198,7 @@ uv run seismic-tee-bootstrap configure \
# to the artifact-set copy beside --manifest (verified against the
# manifest's summit.genesis_template_hash); --node defaults to the
# descriptors in the nodes/ dir beside it.
uv run seismic-tee-bootstrap genesis-ceremony \
uv run seismic-tee-network genesis-ceremony \
--manifest tee/networks/devnet-3/network-manifest.json
```

Expand All @@ -211,8 +211,8 @@ There is no per-node TOML. The config is assembled from flags + the descriptor,
so nothing is copy-pasted per node (and can't drift):
- `[enclave]` — `peers` from `--peer` (a `http://host:7878` URL, or a
descriptor path that resolves to `http://<public_ip>:7878`). `genesis_node`
is `false` for `seismic-tee configure` (join); it is `true` only for the one
node configured by `seismic-tee-bootstrap configure`.
is `false` for `seismic-tee-node configure` (join); it is `true` only for the one
node configured by `seismic-tee-network configure`.
- `[domain]` — `name` from the descriptor's `fqdn` (the cert domain), `email`
from `--email` (default `ops@seismic.systems`).
- `[network].manifest_base64` — from `--manifest`.
Expand All @@ -225,7 +225,7 @@ this is the body a joining node POSTs:

```toml
[enclave]
genesis_node = false # true only via `seismic-tee-bootstrap configure`
genesis_node = false # true only via `seismic-tee-network configure`
peers = ["http://az-1.seismicdev.net:7878"] # from --peer

[domain] # injected: descriptor fqdn + --email
Expand Down Expand Up @@ -266,11 +266,11 @@ only gates against a second POST within the same boot.
- **Two root_key-genesis nodes silently split the network.** Each genesis
enclave generates a fresh `OsRng` `root_key` locally; downstream nodes
that fetch from one cohort can't decrypt state from the other.
`seismic-tee-bootstrap configure` makes this unrepresentable within one
`seismic-tee-network configure` makes this unrepresentable within one
invocation (exactly one `--genesis`), but nothing yet stops a second
*invocation* against a fresh VM — a durable guard (genesis mode refusing
to start against an already-formatted volume or a live peer) is planned
upstream in tdx-init. (The operator `seismic-tee configure` can't
upstream in tdx-init. (The operator `seismic-tee-node configure` can't
trigger it: it only joins.)
- **The `:8080` listener is unauthenticated, first-POST-wins.**
An attacker who reaches port 8080 ahead of the operator can
Expand All @@ -281,7 +281,7 @@ only gates against a second POST within the same boot.

## Attestation verification (currently disabled)

`seismic-tee configure` does **not** verify the node's TDX attestation
`seismic-tee-node configure` does **not** verify the node's TDX attestation
today — it only POSTs the config. The `--measurements` flag is gated off and
errors if passed, rather than pretending to verify.

Expand Down
5 changes: 3 additions & 2 deletions tee/cli/common/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
by Pulumi and run standalone) and this CLI: the CLI consumes a descriptor
and never shells out to or wraps Pulumi.

`seismic-tee-bootstrap up` emits exactly `{public_ip, fqdn}`. A
`seismic-tee-network up` emits exactly `{public_ip, fqdn}`. A
bring-your-own-infra operator (Terraform, manual console, …) can
hand-write the same shape — `pulumi stack output --json` works too, since
only `public_ip`/`fqdn` are read and any extra keys are ignored:

seismic-tee configure --node dev-bootstrap-node-2.json --peer … --manifest m.json
seismic-tee-node configure --node dev-bootstrap-node-2.json \
--peer … --manifest m.json
"""

import json
Expand Down
2 changes: 1 addition & 1 deletion tee/cli/common/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ def main() -> None:
print(
f"Scaffolded {args.dir}. Edit the inputs (at minimum review "
f"{INPUT_SUMMIT_TEMPLATE_FILENAME}), then:\n"
f" seismic-tee-bootstrap manifest assemble {args.dir}{id_hint}"
f" seismic-tee-network manifest assemble {args.dir}{id_hint}"
)
elif args.command == "assemble":
policy_bytes = promote_measurements(
Expand Down
2 changes: 1 addition & 1 deletion tee/cli/common/plumbing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Shared plumbing for the tee click front-ends.

Both CLIs — `seismic-tee` (operator) and `seismic-tee-bootstrap`
Both CLIs — `seismic-tee-node` (operator) and `seismic-tee-network`
(internal network founding) — are thin click groups that forward each
leaf's arguments verbatim to that module's own argparse `main()`, so
per-command flags and `--help` are unchanged. Subcommand imports are
Expand Down
2 changes: 1 addition & 1 deletion tee/cli/network/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""`seismic-tee-bootstrap` — internal CLI to found a network.
"""`seismic-tee-network` — internal CLI to found a network.

May import ..node and ..common.
"""
14 changes: 7 additions & 7 deletions tee/cli/network/app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""`seismic-tee-bootstrap` — internal CLI to found a Seismic TEE network.
"""`seismic-tee-network` — internal CLI to found a Seismic TEE network.

Seismic-internal, NOT a tool node operators run: it provisions a cohort of
TDX nodes (`up` / `down`) and runs the one-time network-creation steps
(`manifest`, `genesis-ceremony`). This is the CLI that is *allowed* to wrap
Pulumi —
`up` / `down` drive the seismic_node Automation-API orchestrator. The
operator CLI (`seismic-tee`) deliberately is not; the boundary is the node
operator CLI (`seismic-tee-node`) deliberately is not; the boundary is the node
descriptor file (see tee/cli/common/descriptor.py), which this CLI produces
(via provisioning) and consumes (during the genesis ceremony).

Expand All @@ -29,7 +29,7 @@ def up(argv: tuple[str, ...]) -> None:
"""Provision a cohort of TDX nodes (one independent Pulumi stack each)."""
from tee.cli.network import orchestrator

forward(orchestrator.up_main, "seismic-tee-bootstrap up", argv)
forward(orchestrator.up_main, "seismic-tee-network up", argv)


@app.command(name="down", context_settings=PASSTHROUGH, add_help_option=False)
Expand All @@ -38,7 +38,7 @@ def down(argv: tuple[str, ...]) -> None:
"""Tear down cohort node(s); each stack destroys independently."""
from tee.cli.network import orchestrator

forward(orchestrator.down_main, "seismic-tee-bootstrap down", argv)
forward(orchestrator.down_main, "seismic-tee-network down", argv)


@app.command(name="configure", context_settings=PASSTHROUGH, add_help_option=False)
Expand All @@ -47,7 +47,7 @@ def configure(argv: tuple[str, ...]) -> None:
"""Configure a cohort in parallel: one genesis + N joiners, one command."""
from tee.cli.network import cohort_configure

forward(cohort_configure.main, "seismic-tee-bootstrap configure", argv)
forward(cohort_configure.main, "seismic-tee-network configure", argv)


@app.command(
Expand All @@ -58,7 +58,7 @@ def genesis_ceremony(argv: tuple[str, ...]) -> None:
"""One-shot genesis ceremony: build genesis.toml from the cohort, fan it out."""
from tee.cli.network import genesis as genesis_mod

forward(genesis_mod.main, "seismic-tee-bootstrap genesis-ceremony", argv)
forward(genesis_mod.main, "seismic-tee-network genesis-ceremony", argv)


@app.command(name="manifest", context_settings=PASSTHROUGH, add_help_option=False)
Expand All @@ -67,7 +67,7 @@ def manifest(argv: tuple[str, ...]) -> None:
"""Scaffold (init) / assemble / validate a network artifact-set dir."""
from tee.cli.common import manifest as manifest_mod

forward(manifest_mod.main, "seismic-tee-bootstrap manifest", argv)
forward(manifest_mod.main, "seismic-tee-network manifest", argv)


if __name__ == "__main__":
Expand Down
6 changes: 3 additions & 3 deletions tee/cli/network/cohort_configure.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""`seismic-tee-bootstrap configure` — found a network in one command.
"""`seismic-tee-network configure` — found a network in one command.

Configures a whole cohort at once: the one genesis node (`--genesis`, mints
`root_key` locally) plus every joining node (`--join`, fetches `root_key` from
Expand All @@ -8,7 +8,7 @@
to a per-node flag — so a double-genesis network split is unrepresentable.

Founding is an internal act, so this lives on the bootstrap CLI; joining an
already-live network is the operator `seismic-tee configure`. Both go through
already-live network is the operator `seismic-tee-node configure`. Both go through
the same `build_config` / `post_config_to_tdx_init` primitives and
`status.poll_provisioning`, so each node's POSTed config and wipe-watch are
identical — only the role (`genesis_node`/`peers`) differs.
Expand Down Expand Up @@ -243,7 +243,7 @@ def _report(nodes: list[Node], results: dict[str, bool]) -> None:

def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="seismic-tee-bootstrap configure",
prog="seismic-tee-network configure",
description="Configure a network cohort in parallel: one genesis + N joiners.",
)
parser.add_argument(
Expand Down
Loading
Loading