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
16 changes: 10 additions & 6 deletions tee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,16 @@ only gates against a second POST within the same boot.
to start against an already-formatted volume or a live peer) is planned
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
write a malicious bootnode list, redirecting `root_key` fetch
to an attacker endpoint. Mitigation today: cloud firewall
restricts `:8080` to the operator's source IP. Tracked
upstream in tdx-init.
- **The `:8080` listener is unauthenticated, first-POST-wins.** Whoever
reaches it ahead of the operator enrolls the box into *their* network —
their manifest, their measurement policy, their bootnode list, so their
`root_key`. The NSG keeps `:8080` (and the key-holder port) reachable only
from `operator_ip_cidr` (`pulumi/seismic_node/__main__.py`), which makes that
stack setting the whole guard: point it at the address `configure` runs
from, because `0.0.0.0/0` leaves the listener world-reachable on every boot
(the conf dir is tmpfs, so it reopens each time). An in-guest guard —
authenticating the POST rather than firewalling it — is tracked upstream in
tdx-init.

## Attestation verification (currently disabled)

Expand Down
8 changes: 6 additions & 2 deletions tee/pulumi/seismic_node/Pulumi.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ config:
# Keep small for dev deploys: first-boot dm-integrity initialization is slow
# on large disks. See README "Local testing: data-disk formatting time".
seismic-tee-deploy:data_disk_size_gb: "8"
# Source IP (CIDR) allowed to SSH. Use your public IP to lock down :22.
seismic-tee-deploy:source_ip_cidr: 0.0.0.0/0
# The operator's own IP (CIDR): the only source allowed to reach the ports
# nobody else needs — :22, tdx-init's config listener, the summit key holder.
# Set it to the public IP you administer these nodes from; the value below
# opts out and leaves the unauthenticated, first-POST-wins config listener
# open to the internet on every boot.
seismic-tee-deploy:operator_ip_cidr: 0.0.0.0/0
encryptionsalt: v1:cgAomGIsWCk=:v1:/sXIE2a9HDrpnOBC:cctZJQ9Ww80lgIXuoQaJ+RJ8ewzu2A==
2 changes: 1 addition & 1 deletion tee/pulumi/seismic_node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ along with all compute resources.
```bash
cd tee/pulumi/seismic_node
pulumi whoami -v # should report Backend URL: file://~
# Review Pulumi.dev.yaml, especially vhd_blob_url and source_ip_cidr.
# Review Pulumi.dev.yaml, especially vhd_blob_url and operator_ip_cidr.
pulumi stack init dev
pulumi up
```
Expand Down
51 changes: 37 additions & 14 deletions tee/pulumi/seismic_node/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,40 +50,63 @@ def _require_str(value: str | None) -> str:
dns_zone_rg = cfg.require("dns_zone_resource_group")
dns_zone_name = cfg.require("dns_zone_name")
dns_record_name = cfg.require("dns_record_name")
source_ip_cidr = cfg.require("source_ip_cidr")
# Source prefix for the operator-only ports (ssh, tdx-init config, key
# holder — see OPEN_PORTS). It is the whole guard on the window before the
# node is configured, so narrow it to the address the operator administers
# the node from; a `0.0.0.0/0` stack config opts out of that guard.
operator_ip_cidr = cfg.require("operator_ip_cidr")

# Keep configurable for local dev/testing. First-boot dm-integrity formatting
# performs a full-device initialization, so small disks are much faster for
# local testing. See README for the DCedsv6/local-NVMe caveat.
data_disk_size_gb = cfg.get_int("data_disk_size_gb") or 32

# Shorthands for the port table below.
TCP = azure_native.network.SecurityRuleProtocol.TCP
UDP = azure_native.network.SecurityRuleProtocol.UDP
# Any source address — for the peer-facing ports, where the app layer (pubkey
# handshakes, enclave attestation) is the admission control, not the NSG.
ANY_SOURCE = "*"

# Standard ports the TEE node needs open. Each entry is
# (port, name, protocol); the protocol dimension lets a single port be opened
# for TCP and/or UDP independently (reth devp2p needs both).
# (port, name, protocol, source); the protocol dimension lets a single port be
# opened for TCP and/or UDP independently (reth devp2p needs both), and `source`
# is the allowed source prefix: `ANY_SOURCE` for peer-facing ports,
# `operator_ip_cidr` for the ports only the operator ever dials.
#
# reth's RPC (8545) and WS (8546) are deliberately NOT here: nginx terminates
# TLS on 443 and reverse-proxies /rpc + /ws to them over loopback.
# Same for metrics — summit's prom + reth's metrics are nginx /metrics/*.
OPEN_PORTS = [
# sshd only listening in the devtools image, not in the production image.
(22, "ssh", azure_native.network.SecurityRuleProtocol.TCP),
(80, "http", azure_native.network.SecurityRuleProtocol.TCP),
(443, "https", azure_native.network.SecurityRuleProtocol.TCP),
(22, "ssh", TCP, operator_ip_cidr),
# Reserved for summit-key-holder: serves the node's summit pubkeys, plus a
# TDX quote over them, to the operator collecting a founding validator set.
# Operator-only, and permanently so — the quote window reopens on every
# boot, since the holder mints fresh RAM keys until the keystore exists.
(7879, "summit-key-holder", TCP, operator_ip_cidr),
# tdx-init's config listener: unauthenticated and first-POST-wins, so
# whoever reaches it before the operator enrolls the box into *their*
# network — their manifest, their measurement policy, their root_key.
# Operator-only, and permanently so — the conf dir is tmpfs, so the
# listener comes back up for a fresh POST on every boot.
(8080, "tdx-init-config", TCP, operator_ip_cidr),
(80, "http", TCP, ANY_SOURCE),
(443, "https", TCP, ANY_SOURCE),
# Enclave currently exposes too many things on this port: get_attestation_evidence,
# get_purpose_keys, getWrappedRootKey. We will eventually split it.
(7878, "enclave", azure_native.network.SecurityRuleProtocol.TCP),
# one-shot listener; close after first deploy
(8080, "tdx-init-config", azure_native.network.SecurityRuleProtocol.TCP),
# Peer-facing: a joining node's root_key fetch dials this on a cohort node.
(7878, "enclave", TCP, ANY_SOURCE),
# Summit consensus P2P (commonware-p2p, TCP). Left open like
# the other service ports — commonware authenticates peers by pubkey (only
# the validator set completes the handshake), so admission is app-layer, and
# a static IP allowlist would break validators that join dynamically later.
(18551, "summit-consensus", azure_native.network.SecurityRuleProtocol.TCP),
(18551, "summit-consensus", TCP, ANY_SOURCE),
# reth devp2p: RLPx peering over TCP, node discovery (discv4/v5) over UDP.
# Same rationale as summit above — peers are authenticated at the app layer,
# so the port is left open rather than IP-allowlisted.
(30303, "reth-p2p", azure_native.network.SecurityRuleProtocol.TCP),
(30303, "reth-p2p-udp", azure_native.network.SecurityRuleProtocol.UDP),
(30303, "reth-p2p", TCP, ANY_SOURCE),
(30303, "reth-p2p-udp", UDP, ANY_SOURCE),
]

# --------------------------------------------------------------------
Expand Down Expand Up @@ -123,12 +146,12 @@ def _require_str(value: str | None) -> str:
direction=azure_native.network.SecurityRuleDirection.INBOUND,
access=azure_native.network.SecurityRuleAccess.ALLOW,
protocol=protocol,
source_address_prefix=(source_ip_cidr if name == "ssh" else "*"),
source_address_prefix=source,
source_port_range="*",
destination_address_prefix="*",
destination_port_range=str(port),
)
for i, (port, name, protocol) in enumerate(OPEN_PORTS)
for i, (port, name, protocol, source) in enumerate(OPEN_PORTS)
]

nsg = azure_native.network.NetworkSecurityGroup(
Expand Down
Loading