Self-Host TEE
Deploy your own BitGraph Trusted Execution Environment using AWS Nitro Enclaves. This guide assumes no prior TEE experience.
Architecture
The BitGraph TEE consists of three components running on a single EC2 instance:
- Enclave — isolated TEE that holds the Ed25519 signing key and produces cryptographically signed proofs. The key is generated inside the enclave and never leaves.
- Parent server — HTTP server running on the EC2 host. Receives proof requests, forwards them to the enclave via vsock, returns signed proofs.
- Vsock bridge — socat process that bridges TCP (parent) to vsock (enclave). Required because Node.js doesn't support AF_VSOCK natively.
Communication flow:
Client (HTTPS) → Parent Server (port 8080) → Socat (TCP:9000 ↔ Vsock:5000) → Enclave App
Prerequisites
- AWS account with EC2 access
- A Nitro-capable EC2 instance (c5, c6, m5, m6, r5, r6 families — not t2/t3)
- At least 2 vCPUs and 4 GB RAM (the enclave needs dedicated CPU/memory)
- Docker installed on the instance
- Node.js 20+ installed on the instance
Step 1: Launch EC2 Instance
Launch a Nitro-capable instance with enclave support enabled:
# Example: c6a.xlarge (4 vCPU, 8 GB RAM) # AMI: Amazon Linux 2023 # IMPORTANT: Enable "Nitro Enclave" in Advanced Details when launching # Or via CLI: aws ec2 run-instances \ --instance-type c6a.xlarge \ --image-id ami-0abcdef1234567890 \ --enclave-options Enabled=true \ --key-name your-key-pair
Security group — allow inbound:
| Port | Protocol | Source | Purpose |
|---|---|---|---|
| 22 | TCP | Your IP | SSH |
| 8080 | TCP | Your app server | Parent HTTP API |
Step 2: Install Dependencies
SSH into the instance and install everything:
# Install Nitro CLI sudo amazon-linux-extras install aws-nitro-enclaves-cli -y sudo yum install aws-nitro-enclaves-cli-devel -y # Install Docker sudo yum install docker -y sudo systemctl start docker sudo systemctl enable docker sudo usermod -aG docker $USER # Install Node.js 20 curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - sudo yum install nodejs -y # Install socat sudo yum install socat -y # Install build tools (for NSM helper) sudo yum install gcc musl-devel -y # Start the Nitro enclave allocator sudo systemctl start nitro-enclaves-allocator sudo systemctl enable nitro-enclaves-allocator # Add yourself to the enclave group sudo usermod -aG ne $USER # IMPORTANT: Log out and back in for group changes exit
Step 3: Configure Enclave Resources
The enclave needs dedicated CPU and memory allocated from the host. Edit the allocator config:
# Allocate 2 CPUs and 1024 MB to the enclave memory_mib: 1024 cpu_count: 2
# Restart allocator after changes sudo systemctl restart nitro-enclaves-allocator
Step 4: Clone and Build
# Clone the repo git clone https://github.com/mikeargento/bitgraph.git cd bitgraph # Build the enclave EIF reproducibly. This pins every input (base image by # digest, OS packages by version, sharp by lockfile, and nitro-cli 1.4.4 which # fixes the kernel measured into PCR0), builds the image with kaniko in # --reproducible mode, and packs the EIF with a pinned nitro-cli. Requires # Docker + git on a linux/amd64 host; Nitro hardware is NOT needed to build the # EIF (only to run it). Build the tagged enclave source release: the production # enclave is built from the enclave-v4 tag, not necessarily the latest commit. ./server/commit-service/reproducible-build/build-eif.sh enclave-v4 # PCR0 is printed at the end and written to eif-out/pcr0.txt.
Step 5: Verify the PCR0 is reproducible
PCR0 is a SHA-384 measurement of the entire EIF, and it is the enclave's identity that every proof embeds. Because the build above pins all of its inputs, you can prove the build is deterministic: build it twice and confirm the PCR0 is byte-identical, and that it equals the value BitGraph publishes. If it matches, you have independently confirmed the production enclave runs exactly the code at the tagged enclave source release (enclave-v4) in this repository, trusting no one.
# Build twice from clean state and assert identical PCR0 == the published value: ./server/commit-service/reproducible-build/verify-pcr0.sh enclave-v4 \ e2fccbae77ee40aac4830e84f195e05d69eb4547bbd961f4d3459feba10807140424aca42ad03810354982598c86b9cb # PASS: two independent builds produced identical PCR0: # e2fccbae...8c86b9cb # PASS: matches the published PCR0. # See server/commit-service/reproducible-build/PINS.md for every pinned # digest/version, and README.md for how the determinism is achieved.
The BitGraph enclave image in production measures as:
This is the value BitGraph publishes and stands behind. Every proof embeds this measurement, and the "Verify Attestation" check confirms the attestation's PCR0 matches it. The build is bit-for-bit reproducible: rebuild from this source on any linux/amd64 host with verify-pcr0.sh and you will re-derive exactly this PCR0. You do not have to trust BitGraph's assertion, you can recompute it yourself. The one input you trust AWS for is their signed enclave kernel, which is what PCR1 independently measures; everything else folded into PCR0 is built from the auditable source in this repository.
Step 6: Launch the Enclave
# Terminate any existing enclave nitro-cli terminate-enclave --all 2>/dev/null # Launch the enclave nitro-cli run-enclave \ --eif-path enclave.eif \ --cpu-count 2 \ --memory 1024 # Verify it's running nitro-cli describe-enclaves # Should show: State: "RUNNING", EnclaveCID: <number> # Save the CID — you need it for the vsock bridge ENCLAVE_CID=$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveCID')
Step 7: Start the Vsock Bridge
The bridge connects the parent server (TCP) to the enclave (vsock):
# Start socat bridge in background nohup socat TCP-LISTEN:9000,fork,reuseaddr \ VSOCK-CONNECT:$ENCLAVE_CID:5000 \ > /tmp/socat-bridge.log 2>&1 & # Verify it's listening ss -tlnp | grep 9000 # Should show: LISTEN 0 5 0.0.0.0:9000
Step 8: Build and Start the Parent Server
# Build the parent server (TypeScript → JavaScript) cd /path/to/bitgraph/server/commit-service npx tsc -p tsconfig.parent.json # Set environment variables export PORT=8080 export VSOCK_BRIDGE_PORT=9000 export API_KEYS="your-secret-api-key-here" # Start the parent server nohup node dist/parent/server.js > /tmp/parent.log 2>&1 &
Step 9: Verify
# Health check
curl http://localhost:8080/health
# { "ok": true }
# Get the enclave's public key and measurement
curl http://localhost:8080/key
# {
# "publicKeyB64": "...",
# "measurement": "abc123...",
# "enforcement": "measured-tee"
# }
# Test a commit
DIGEST=$(echo -n "hello world" | openssl dgst -sha256 -binary | base64)
curl -X POST http://localhost:8080/commit \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-api-key-here" \
-d "{
\"digests\": [{\"digestB64\": \"$DIGEST\", \"hashAlg\": \"sha256\"}]
}"
# Returns: signed BitGraph proof with TEE attestationStep 10: Point BitGraph Dashboard at Your TEE
By default, the hosted dashboard at occ.bitgraph.ing points to nitro.occproof.com. To use your own TEE, set the TEE_URL environment variable on your hosted server:
# In your hosted server environment (Railway, etc.) TEE_URL=https://your-tee-domain.com
The hosted server at packages/hosted/src/authorization.ts reads this variable:
const TEE_URL = process.env.TEE_URL || "https://nitro.occproof.com";
Production Checklist
- Put an ALB or CloudFront in front of port 8080 with TLS termination
- Restrict security group to only allow your app server's IP
- Set strong API keys via the
API_KEYSenvironment variable - Save the PCR0 measurement — this is your enclave's identity for verification
- Set up monitoring on
/healthendpoint - Configure log rotation for parent server and socat logs
- The enclave generates a new keypair on each restart — the epochId changes and the counter resets to 1. Cross-epoch sequencing is established by Ethereum anchors, not by an in-enclave chain.
Using the Deploy Script
For automated deployment, use the included script:
cd bitgraph/server/commit-service ./deploy.sh # This runs all steps automatically: # 1. Builds Docker image # 2. Builds EIF # 3. Terminates existing enclave # 4. Launches new enclave # 5. Starts vsock bridge # 6. Builds and starts parent server
Key Files
| File | Purpose |
|---|---|
server/commit-service/Dockerfile.enclave | Builds the enclave Docker image |
server/commit-service/src/enclave/app.ts | Enclave application — proof signing, slot management |
server/commit-service/src/parent/server.ts | Parent HTTP API — commit, key, health endpoints |
server/commit-service/src/parent/vsock-client.ts | TCP bridge client to enclave |
server/commit-service/deploy.sh | Automated deployment script |
packages/adapter-nitro/src/nitro-host.ts | NSM device interface — attestation, measurement |
packages/hosted/src/authorization.ts | Dashboard integration — calls TEE_URL |
How the Enclave Works Internally
On startup, the enclave:
- Generates a fresh Ed25519 keypair in memory (never exported)
- Fetches the PCR0 measurement from the NSM device (
/dev/nsm) - Generates a boot nonce from the NSM hardware RNG
- Computes
epochId = SHA-256(publicKeyB64 + ":" + bootNonceB64) - Listens on a Unix socket for proof requests
For each proof request:
- Validates the slot exists (BitGraph causal gate — no slot, no proof)
- Increments the chain counter
- Builds the signed body: artifact, commit, policy, principal
- Signs with Ed25519
- Gets a Nitro attestation report from the NSM device
- Returns the complete BitGraph proof with attestation embedded
Epoch Transitions
When the enclave restarts (deploy, crash, reboot):
- A fresh Ed25519 keypair is generated inside the enclave from hardware entropy. The previous key is destroyed and exists nowhere outside the terminated enclave.
- A new
epochIdis derived from the new public key plus a fresh boot nonce. - The monotonic counter resets to 1. The first proof of the new epoch has no
prevB64— the prior chain is closed, and the new chain begins at genesis. - During restart, all commit requests fail closed.
This is a containment property, not a limitation. Each epoch is a sealed compartment: any compromise of the live epoch cannot retroactively forge proofs under a prior epoch's key. Cross-epoch sequencing is established externally by Ethereum anchors, not by an in-enclave chain.