This is a minimal example app that forwards an HTTP GET to an upstream host (news.ycombinator.com) and returns the upstream response.
Current example conventions:
- the app exposes
GET /healthfor a quick local/enclave liveness check - proxy use is explicit in code, matching Nova Enclave Capsule's current egress guidance
- the manifest exposes both the app (
8000) and Aux API (9001) throughingress
This app does not call the Capsule API itself. It is just a minimal
egress-proxy example, so there is no IN_ENCLAVE mode switch in the app code or
Dockerfile.
Revalidated on March 17, 2026 on app-node using the published capsule-cli
release and the published ECR images:
- host: Amazon Linux 2023 x86_64
capsule-cli 1.8.1 (git 9311be9fc75555180a01b820e8373f062ebe8161)public.ecr.aws/d4t4u8d2/sparsity-ai/capsule-runtime:latestpulled with digestsha256:2ab406cf9e934eb2dd31a5695c86772c8af3e3863f63bf9e783657e628ea018fpublic.ecr.aws/d4t4u8d2/sparsity-ai/capsule-shell:latestpulled with digestsha256:3a0920e67c7ee080064e484562a63b0315e81171084dfe7fab32aa17fda2a86edocker images hn-fetcher:latestreported image IDdd48d2308560at177MB- direct
GET /healthreturned{"ok":true,"upstream":"https://news.ycombinator.com"} capsule-cli buildcompleted successfully against the published Capsule Runtime, Capsule Shell, and Nitro CLI images
Validated on March 14, 2026 on app-node:
- host: Amazon Linux 2023 x86_64
capsule-cli 1.8.1docker 25.0.14
The output snippets below are the observed results from that validation run. Values such as Hacker News page content, image IDs, PCRs, Ethereum addresses, and attestation size can change on later runs.
Build the app image with:
docker build -t hn-fetcher .Verify the app image with:
docker images hn-fetcher:latestObserved on app-node on March 14, 2026:
$ docker images hn-fetcher:latest
REPOSITORY TAG IMAGE ID CREATED SIZE
hn-fetcher latest 4a906fcc5a99 10 minutes ago 177MBYou can run the app image directly with:
docker run --rm -p 8000:8000 hn-fetcher
curl http://localhost:8000/health
curl -sS http://localhost:8000/ | grep -o -m1 '<title>[^<]*</title>'Observed on app-node on March 14, 2026:
$ curl http://localhost:8000/health
{"ok":true,"upstream":"https://news.ycombinator.com"}
$ curl -sS http://localhost:8000/ | grep -o -m1 '<title>[^<]*</title>'
<title>Hacker News</title>The content of capsule.yaml is:
version: v1
name: "hn-fetcher"
target: "hn-fetcher-enclave:latest"
sources:
app: "hn-fetcher:latest"
#capsule-runtime: "capsule-runtime:latest"
#capsule-shell: "capsule-shell:latest"
defaults:
memory_mb: 1500
ingress:
- listen_port: 8000
- listen_port: 9001
egress:
allow:
- news.ycombinator.com
api:
listen_port: 9000
aux_api:
listen_port: 9001The manifest includes:
ingress- Allows external HTTP traffic on port 8000ingress- Also exposes Aux API on port 9001 for the demo API calls belowegress- Allows outbound requests to news.ycombinator.comapi- Enables the capsule API service on port 9000 (provides attestation, signing, encryption, and randomness)aux_api- Enables the auxiliary API on port 9001 (provides controlled external access to select API endpoints)
For the March 17, 2026 release-based validation on app-node, the test flow
used a temporary manifest copy with these two lines uncommented so the example
would explicitly consume the published images from public ECR:
sources:
app: "hn-fetcher:latest"
capsule-runtime: "public.ecr.aws/d4t4u8d2/sparsity-ai/capsule-runtime:latest"
capsule-shell: "public.ecr.aws/d4t4u8d2/sparsity-ai/capsule-shell:latest"Then build capsule-cli image with command:
capsule-cli build -f capsule.yamlObserved on app-node on March 14, 2026:
$ capsule-cli build -f capsule.yaml
INFO capsule-cli::build > using app image: sha256:4a906fcc5a99840e1ddd00a833633c9aa90b11ede96e8650e9e2582811bb41fc
INFO capsule-cli::build > using nitro-cli image: sha256:945f75bbfe02ef6ac2bcefe12f21b9902a6dac5ea890778c30ff83bfcafd8e58
INFO capsule-cli::build > built intermediate image: sha256:022089a4434c85ab3cd60cdfd0912df1494c24fde47e69d146aef2e9efe6bed7
INFO capsule-cli::build > started nitro-cli build-eif in container: 67c9bce01b98e8adffa3899e02093b7a7e0c4381263996b9d655831bff0e7c12
INFO nitro-cli::build-eif > Start building the Enclave Image...
INFO nitro-cli::build-eif > Enclave Image successfully created.
INFO capsule-cli::build > packaging EIF into release image
{
"Sources": {
"App": {
"ID": "sha256:4a906fcc5a99840e1ddd00a833633c9aa90b11ede96e8650e9e2582811bb41fc"
},
"Capsule Runtime": {
"ID": "sha256:5f58abc85d9ef1ab317651c6e069ec58600dc38939228f4abdb5e437f57e3444",
"Name": "public.ecr.aws/d4t4u8d2/sparsity-ai/capsule-runtime:latest",
"RepoDigest": "public.ecr.aws/d4t4u8d2/sparsity-ai/capsule-runtime@sha256:45d8898dc2cbae0f5d3e65f42639ce6b05c591a09098fb6268b3ab43e925e514"
},
"NitroCLI": {
"ID": "sha256:945f75bbfe02ef6ac2bcefe12f21b9902a6dac5ea890778c30ff83bfcafd8e58",
"Name": "public.ecr.aws/d4t4u8d2/sparsity-ai/nitro-cli:latest",
"RepoDigest": "public.ecr.aws/d4t4u8d2/sparsity-ai/nitro-cli@sha256:512d98b17f72bc610cd0b7e0d851971f1ca33717d5dc48ac49517d29da009568"
},
"Capsule Shell": {
"ID": "sha256:67b05859252d4a98a77573eea5481125ffcc2b0c33cc6f11420dd76cc107e417",
"Name": "public.ecr.aws/d4t4u8d2/sparsity-ai/capsule-shell:latest",
"RepoDigest": "public.ecr.aws/d4t4u8d2/sparsity-ai/capsule-shell@sha256:2057482f199b092d31407cb7107a3b2e74b3d96b7fb29739942fc28687bc8f27"
}
},
"Measurements": {
"PCR0": "9b86f0489c6c104a0f0952bf444dbabb447544acc573be5ec5ee40ecf8b80ad1deed98161fceddbb14e953b4502f66bd",
"PCR1": "18b701b9e237424633a37b610308dcf15fb18a25fe12c80d9766b82661b3ecddae6825c6bc13b8fa254f72a87c177d40",
"PCR2": "31cecadc8d56c371099dd7fbc5735ed2ab59c7dfd5d9be83b3a913e899121b116b8ebb7f086eb5085b46c3e54819df6d"
},
"Image": {
"ID": "sha256:4ce84925ecf49e1eb4105ee0f1ca5d4e3d51da6fe859248ea0846f35d8431127",
"Name": "hn-fetcher-enclave:latest"
}
}Verify the enclave image with:
docker images hn-fetcher-enclave:latestObserved on app-node on March 14, 2026:
$ docker images hn-fetcher-enclave:latest
REPOSITORY TAG IMAGE ID CREATED SIZE
hn-fetcher-enclave latest 4ce84925ecf4 3 minutes ago 265MBRun the enclave image with:
capsule-cli run -f capsule.yaml --publish 8000:8000 --publish 9001:9001Test the application:
curl http://localhost:8000/health
curl -sS http://localhost:8000/ | grep -o -m1 '<title>[^<]*</title>'
curl http://localhost:9001/v1/eth/addressObserved on app-node on March 14, 2026:
$ curl http://localhost:8000/health
{"ok":true,"upstream":"https://news.ycombinator.com"}
$ curl -sS http://localhost:8000/ | grep -o -m1 '<title>[^<]*</title>'
<title>Hacker News</title>
$ curl http://localhost:9001/v1/eth/address
{"address":"0x59d006663ebbc03dffdf0ae5ad7e4c87d0d602bf","public_key":"0x046328d0678ca4489f2f4da423d93361b9e345b0c153fef69ae82f698a675c09e2fc8e8e378670559a9319e54e2e2b3b01280623f47b883324a6e17f885022d66d"}What this run command does:
- reads
targetfromcapsule.yaml - publishes app traffic on
8000 - publishes the Aux API on
9001 - keeps the API exposure aligned with the manifest instead of repeating the image tag manually
The auxiliary API provides controlled external access to enclave functionality. It proxies requests to the capsule API while sanitizing inputs to prevent external callers from overriding security-critical defaults.
Retrieve the enclave's Ethereum address derived from the enclave's keypair:
curl http://localhost:9001/v1/eth/addressExample response:
{
"address": "0x59d006663ebbc03dffdf0ae5ad7e4c87d0d602bf",
"public_key": "0x046328d0678ca4489f2f4da423d93361b9e345b0c153fef69ae82f698a675c09e2fc8e8e378670559a9319e54e2e2b3b01280623f47b883324a6e17f885022d66d"
}Retrieve the enclave's P-384 encryption public key:
curl http://localhost:9001/v1/encryption/public_keyExample response:
{
"public_key_der": "0x3076301006072a8648ce3d020106052b81040022036200042643f747b004cb7ba74098e4f3f859fe6109b8c7c08fff83694eaf7794c588338f5224ab970cbb6edfbbb1931466c96e64c8e1dfaa09bbee9d96fce6ba1d475806efcf52badd4bfee198dddbae882fee3d89085dd067d7818f7e51828718dd1e",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJkP3R7AEy3unQJjk8/hZ/mEJuMfAj/+D\naU6vd5TFiDOPUiSrlwy7bt+7sZMUZsluZMjh36oJu+6dlvzmuh1HWAbvz1K63Uv+\n4Zjd266IL+49iQhd0GfXgY9+UYKHGN0e\n-----END PUBLIC KEY-----"
}Request an attestation document from the enclave. You can optionally provide a nonce for freshness:
# Request attestation with default parameters
curl -X POST http://localhost:9001/v1/attestation \
-H "Content-Type: application/json" \
-d '{}'
# Request attestation with a custom nonce
curl -X POST http://localhost:9001/v1/attestation \
-H "Content-Type: application/json" \
-d '{"nonce": "your-base64-encoded-nonce-here"}'Example response:
The response body is raw CBOR bytes with Content-Type: application/cbor, not JSON.
The following was observed on app-node on March 14, 2026:
$ curl -sS -D - -o attestation.cbor -X POST http://localhost:9001/v1/attestation \
-H "Content-Type: application/json" \
-d '{}'
HTTP/1.1 200 OK
content-type: application/cbor
content-length: 4649
date: Sat, 14 Mar 2026 00:07:15 GMT
access-control-allow-origin: *
$ wc -c attestation.cbor
4649 attestation.cbor
$ sha256sum attestation.cbor
36f9b9eafdbb7759e7a6c95a2e0142147aa2fc41c7f6a73062ccff218e05c1d1 attestation.cborIf you want to save the current attestation document yourself:
curl -sS -D - -o attestation.cbor -X POST http://localhost:9001/v1/attestation \
-H "Content-Type: application/json" \
-d '{"nonce": "your-base64-encoded-nonce-here"}'Security Note: The aux API automatically sanitizes incoming attestation requests by removing public_key before forwarding them to the capsule API. nonce and user_data are preserved, so external callers cannot override the enclave's default attestation key while still being able to provide freshness and caller-specific metadata.
On the tested app-node host on March 14, 2026, nitro-cli describe-enclaves
run directly on the host returned [] even while capsule-cli run was active.
The working way to inspect the enclave was to resolve the running
hn-fetcher-enclave:latest container first and then execute nitro-cli inside
that container:
$ nitro-cli describe-enclaves
[]
$ cid=$(docker ps --filter ancestor=hn-fetcher-enclave:latest --format '{{.ID}}' | head -n1)
$ docker exec "$cid" /bin/nitro-cli describe-enclaves
[
{
"EnclaveName": "application",
"EnclaveID": "i-07702f8ac844cd2d3-enc19ce9aaaedce5e2",
"ProcessID": 14,
"EnclaveCID": 16,
"NumberOfCPUs": 2,
"CPUIDs": [
1,
3
],
"MemoryMiB": 1500,
"State": "RUNNING",
"Flags": "NONE",
"Measurements": {
"HashAlgorithm": "Sha384 { ... }",
"PCR0": "9b86f0489c6c104a0f0952bf444dbabb447544acc573be5ec5ee40ecf8b80ad1deed98161fceddbb14e953b4502f66bd",
"PCR1": "18b701b9e237424633a37b610308dcf15fb18a25fe12c80d9766b82661b3ecddae6825c6bc13b8fa254f72a87c177d40",
"PCR2": "31cecadc8d56c371099dd7fbc5735ed2ab59c7dfd5d9be83b3a913e899121b116b8ebb7f086eb5085b46c3e54819df6d"
}
}
]