Skip to main content

Nitro Enclave Signer

info

The Nitro Enclave backend was introduced in Signatory v1.3.0. It requires tee-signer from signatory-io/tee-signer.

Nitro backend is used in conjunction with signatory-io/tee-signer, its counterpart running inside AWS Nitro Enclave, a fortified container with no persistent storage and no connection to the outside world other than a bidirectional hypervisor-local VSock link to its parent instance.

All keys imported into tee-signer or generated by it never leave the enclave in plain text form. As the enclave has no storage, all sensitive data is stored by the parent instance in encrypted form. The keystone of this model is a carefully chosen KMS policy which permits the decryption of such data only by an actor possessing a signed attestation document generated by the hypervisor — i.e. code running inside the enclave. All TLS-encrypted requests to KMS made by code running inside the enclave are tunneled over the VSock link to the parent and forwarded to the cloud by a proxy service (see below).

Prerequisites

Please refer to kmstool and signatory-io/tee-signer documentation for KMS setup procedure. One must obtain an ID of a KMS symmetric key tied to the enclave attestation document (or its zero placeholder if debug mode is expected).

AWS Infrastructure Requirements

Before configuring Signatory, ensure the following are in place:

  1. EC2 instance with Nitro Enclave support enabled (e.g., c5a.xlarge or larger)
  2. Nitro Enclave allocator configured with sufficient resources:
    # /etc/nitro_enclaves/allocator.yaml
    memory_mib: 1024
    cpu_count: 2
    Memory and CPU allocated to the enclave are subtracted from the parent instance. For a 4-vCPU instance with 8 GiB RAM, this leaves 2 vCPUs and ~6 GiB for the parent.
  3. AWS Nitro CLI installed (dnf install aws-nitro-enclaves-cli on AL2023, or amazon-linux-extras install aws-nitro-enclaves-cli on AL2)
  4. Docker installed (for building and running Signatory)
  5. tee-signer EIF image built from signatory-io/tee-signer
  6. KMS symmetric key with an attestation-based key policy (see KMS Key Policy)
  7. IAM role attached to the EC2 instance with permissions to call KMS

Configuration

FieldEnvironment variableTypeDefaultRequiredDescription
enclave_cidENCLAVE_CIDuintContext ID of an enclave running tee-signer. It changes on every restart of an enclave
enclave_portENCLAVE_PORTuint2000Listening VSock port of tee-signer
encryption_key_idENCRYPTION_KEY_IDstringKMS key ARN or ID used by enclave to decrypt private keys
storageStorageConfigKey storage configuration
credentialsCredentialsKMS credentials
proxy_local_portPROXY_LOCAL_PORTuint8000Local VSock port for the built in VSock proxy
proxy_remote_addressPROXY_REMOTE_ADDRESShost:portSetting this option enables the built in VSock proxy (see below). Usually it looks like kms.REGION.amazonaws.com:443
Dynamic Enclave CID

enclave_cid is not auto-detected and changes every time the enclave restarts. If you hardcode it in your config file, Signatory will fail to connect after the next enclave restart. Use the ENCLAVE_CID environment variable with a startup wrapper script that queries the current CID dynamically (see Handling Dynamic CID).

Credentials

FieldEnvironment variableType
access_key_idAWS_ACCESS_KEY_IDstring
secret_access_keyAWS_SECRET_ACCESS_KEYstring
session_tokenAWS_SESSION_TOKENstring
regionAWS_REGIONstring

Credentials block is optional. If the user has a properly configured access to AWS the client code will pick it automatically. For EC2 instances, attaching an IAM instance role is recommended over hardcoded credentials.

StorageConfig

Encrypted keys may be stored locally or in AWS DynamoDB.

FieldTypeDescription
driverstring"file" or "aws" (the alias is "dynamodb")
configDriver specific

This block is optional. Local file ${BASE_DIR}/enclave_keys.json will be used if the block is omitted.

AWS Storage Configuration

FieldEnvironment variableTypeDefault
access_key_idAWS_ACCESS_KEY_IDstring
secret_access_keyAWS_SECRET_ACCESS_KEYstring
session_tokenAWS_SESSION_TOKENstring
regionAWS_REGIONstring
tablestring"encrypted_keys"

This block is optional. If the user has a properly configured access to AWS the client code will pick it automatically.

Local storage

If local storage is specified then the config field is expected to be a single string containing a full file path. Environment variables are allowed and will be expanded.

Example

Nitro-Only Configuration

base_dir: ${HOME}/.signatory
server:
address: :6732
utility_address: :9583

vaults:
nitro:
driver: nitro
config:
proxy_remote_address: kms.us-west-2.amazonaws.com:443
encryption_key_id: 771dfdac-e8e9-4f40-9747-f56d0911fce4

Combined AWS KMS + Nitro Configuration (Baker with BLS Companion)

A common production setup uses AWS KMS for the main delegate key (tz2) and Nitro Enclave for the BLS companion key (tz4):

base_dir: /var/lib/signatory

server:
address: :6732
utility_address: :9583

vaults:
# tz2 delegate key via AWS KMS (uses IAM instance role)
aws-kms:
driver: awskms
config:
region: us-west-2

# tz4 companion key via Nitro Enclave
nitro-companion:
driver: nitro
config:
enclave_cid: ${ENCLAVE_CID}
enclave_port: 2000
encryption_key_id: YOUR_KMS_KEY_ID
proxy_remote_address: kms.us-west-2.amazonaws.com:443
storage:
driver: file
config: /var/lib/signatory/enclave_keys.json

tezos:
# AWS KMS baker (tz2) - primary delegate
tz2YourDelegateKey:
log_payloads: true
vault: aws-kms
allow:
block: []
preattestation: []
attestation: []
attestation_with_dal: []
generic:
- reveal
- delegation
- origination
- transaction
- stake
- unstake
- update_companion_key

# BLS companion key (tz4) - for DAL attestations
tz4YourCompanionKey:
log_payloads: true
vault: nitro-companion
allow:
block: []
preattestation: []
attestation: []
attestation_with_dal: []

See DAL & BLS Attestations for details on BLS key permissions.

Importing

Nitro backend supports importing of pre-existing plain text keys in either PEM, DER or Base58 format using signatory-cli import command. The key will be passed to the enclave in open form and the latter will return its encrypted version.

signatory-cli import -v nitro-companion -c /etc/signatory/config.yaml < key.pem

The encrypted key blob is written to the configured storage location (default: ${BASE_DIR}/enclave_keys.json). After a successful import, securely delete the plaintext key from the parent instance:

shred -vfz key.pem && rm key.pem

Key Generation

More secure way of getting keys into the enclave is to generate them on the enclave side using signatory-cli generate command. The private key never exists in plaintext outside the enclave.

Usage:
signatory-cli generate [flags]

Flags:
-h, --help help for generate
-n, --num int Number of keys to generate (default 1)
-t, --type string Key algorithm: [tz1, tz2, tz3, tz4, ed25519, secp256k1, p256, bls] (default "ed25519")
-v, --vault string Vault name for importing

Global Flags:
--base-dir string Base directory. Takes priority over one specified in config
-c, --config string Config file path (default "/etc/signatory.yaml")
--json-log Use JSON structured logs
--log string Log level: [error, warn, info, debug, trace] (default "info")

Example:

signatory-cli generate -v nitro-companion -t tz4
tip

Key generation is the recommended approach for new deployments. Since the private key is generated inside the enclave, it never exists in plaintext on the parent instance, eliminating an entire class of key exfiltration risk.

BLS Proof of Possession

tee-signer includes a dedicated ProvePossession RPC for BLS keys. Signatory can generate the proof of possession required by the Tezos protocol without exposing the private key.

VSock Proxy

All requests to KMS from the signer are tunneled over the VSock link to the parent instance, which forwards them to the cloud. The recommended way of doing so is to use vsock_proxy supplied with nitro-cli. The alternative is to use the built-in proxy, which is enabled by setting the proxy_remote_address configuration option (or alternatively the PROXY_REMOTE_ADDRESS environment variable), which usually takes the form kms.REGION.amazonaws.com:443

tip

The built-in VSock proxy (proxy_remote_address) is simpler to configure and eliminates the need for a separate vsock-proxy systemd service. It is the recommended approach unless you have a specific reason to use the external proxy.

Running in Docker

When running Signatory with Nitro vault inside a Docker container, special configuration is required for VSock communication to work.

Requirements

  1. Device access: The container needs access to /dev/vsock
  2. Seccomp profile: Docker's default seccomp profile blocks AF_VSOCK (address family 40) socket syscalls

Create a modified seccomp profile that allows VSock socket calls:

# Download Docker's default seccomp profile
curl -o seccomp.json https://raw.githubusercontent.com/moby/profiles/refs/heads/main/seccomp/default.json

# Modify it to allow AF_VSOCK (address family 40)
jq '(.syscalls[] | select(.names[0] == "socket" and .args[0].value == 40)) |= del(.args)' seccomp.json > seccomp-vsock.json

Run the container with the custom profile:

docker run -d \
--name signatory \
--device /dev/vsock \
--security-opt seccomp=seccomp-vsock.json \
-v /etc/signatory:/etc/signatory:ro \
-v /var/lib/signatory:/var/lib/signatory \
-p 6732:6732 \
-p 9583:9583 \
ecadlabs/signatory serve -c /etc/signatory/config.yaml

Option 2: Disable Seccomp (Development Only)

For development or testing, seccomp can be disabled entirely:

docker run -d \
--name signatory \
--device /dev/vsock \
--security-opt seccomp=unconfined \
-v /etc/signatory:/etc/signatory:ro \
-v /var/lib/signatory:/var/lib/signatory \
-p 6732:6732 \
-p 9583:9583 \
ecadlabs/signatory:v1.3.1 serve -c /etc/signatory/config.yaml
warning

Running with --security-opt seccomp=unconfined disables all seccomp filtering. Use the custom seccomp profile approach in production environments for better security isolation.

Multi-Instance Deployments

When running multiple Signatory containers on the same host (e.g., production and testing), use separate ports and config directories:

# Instance A - production on port 6732
docker run -d --name signatory-prod \
--device /dev/vsock --security-opt seccomp=seccomp-vsock.json \
-v /etc/signatory-prod:/etc/signatory:ro \
-v /var/lib/signatory-prod:/var/lib/signatory \
-p 6732:6732 -p 9583:9583 \
ecadlabs/signatory:v1.3.1 serve -c /etc/signatory/config.yaml

# Instance B - testing on port 6733
docker run -d --name signatory-test \
--device /dev/vsock --security-opt seccomp=seccomp-vsock.json \
-v /etc/signatory-test:/etc/signatory:ro \
-v /var/lib/signatory-test:/var/lib/signatory \
-p 6733:6733 -p 9584:9584 \
ecadlabs/signatory:v1.3.1 serve -c /etc/signatory/config.yaml

Each instance's config YAML must have matching port numbers (e.g., address: :6733 for instance B). Both containers can share the same enclave (via the same enclave_cid) or connect to separate enclaves if isolation is required.

Handling Dynamic CID

The enclave's Context ID (enclave_cid) changes on every restart. Hardcoding it in the config file will cause Signatory to fail after the next enclave restart. The recommended approach is a wrapper script that queries the CID at startup:

#!/bin/bash
# start-signatory.sh - Wrapper to set ENCLAVE_CID dynamically
set -euo pipefail

CID=""
# Wait for enclave to be running (up to 60 seconds)
for i in {1..30}; do
CID=$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveCID // empty')
if [ -n "$CID" ] && [ "$CID" != "null" ]; then
break
fi
echo "Waiting for enclave to start (attempt $i/30)..."
sleep 2
done

if [ -z "$CID" ] || [ "$CID" = "null" ]; then
echo "ERROR: Enclave not running after 60s"
echo "Check: nitro-cli describe-enclaves"
exit 1
fi

echo "Enclave CID: $CID"
export ENCLAVE_CID="$CID"

exec /usr/local/bin/signatory serve -c /etc/signatory/config.yaml

Make the wrapper executable and reference it from your systemd service or Docker entrypoint.

KMS Key Policy

The KMS key policy is the trust anchor for the entire Nitro Enclave security model. It ensures that only code running inside a verified enclave can decrypt the sealed key material.

A minimal policy includes:

  1. Root account access for key management
  2. EC2 role encrypt permission for sealing keys during import
  3. Enclave-only decrypt gated on PCR0 attestation
  4. EC2 role sign permission if the same KMS key is also used for direct signing (e.g., tz2 via awskms vault)

Example KMS Key Policy

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowRootFullAccess",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT_ID:root" },
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowEC2RoleToEncrypt",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT_ID:role/signatory-nitro-role" },
"Action": ["kms:Encrypt", "kms:GenerateDataKey"],
"Resource": "*"
},
{
"Sid": "AllowEnclaveToDecrypt",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT_ID:role/signatory-nitro-role" },
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:PCR0": "ENCLAVE_PCR0_VALUE"
}
}
}
]
}

Getting the PCR0 Value

The PCR0 value is a hash of the enclave image. It is output when you build the EIF:

nitro-cli build-enclave --docker-uri tee-signer:latest --output-file nitro-signer.eif
# Output includes:
# {
# "Measurements": {
# "PCR0": "abc123...",
# ...
# }
# }

Copy the PCR0 value into your KMS key policy. Every time you rebuild the EIF (e.g., after updating tee-signer), the PCR0 changes and the KMS key policy must be updated.

Debug Mode

When running the enclave in debug mode (--debug-mode), the PCR0 is all zeros. You can use a zero placeholder in the KMS policy for development, but never use debug mode in production as it weakens the attestation guarantee.

Systemd Services

For production deployments, use systemd to manage the enclave and Signatory lifecycle. Service ordering is critical: the enclave must be fully running before Signatory attempts to connect.

Nitro Enclave Service

Save as /etc/systemd/system/nitro-enclave.service:

[Unit]
Description=Nitro Enclave tee-signer
After=nitro-enclaves-allocator.service
Requires=nitro-enclaves-allocator.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/nitro-cli run-enclave \
--eif-path /opt/nitro-signer/nitro-signer.eif \
--memory 1024 \
--cpu-count 2
ExecStop=/usr/bin/nitro-cli terminate-enclave --enclave-name tee-signer
ExecStartPost=/bin/sleep 5

[Install]
WantedBy=multi-user.target

Signatory Service

[Unit]
Description=Signatory Remote Signer
After=network-online.target nitro-enclaves-allocator.service
Wants=network-online.target
Requires=nitro-enclave.service

[Service]
Type=simple
# Needs root for nitro-cli describe-enclaves and /dev/vsock access
# Alternative: add signatory user to 'ne' group and set /dev/vsock permissions
User=root
ExecStart=/usr/local/bin/start-signatory.sh
Restart=always
RestartSec=5
LimitNOFILE=65536

# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/log/signatory /var/lib/signatory
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

The Requires=nitro-enclave.service directive ensures the enclave starts first. The start-signatory.sh wrapper (see Handling Dynamic CID) polls for the enclave CID before launching Signatory.

Debugging

The tee-signer EIF is built FROM scratch (minimal image) and contains no shell. You cannot docker exec or SSH into the enclave.

Available Debugging Tools

  • Enclave console (debug mode only):

    nitro-cli console --enclave-id $(nitro-cli describe-enclaves | jq -r '.[0].EnclaveID')

    This streams the enclave's stdout/stderr. Only available when the enclave was started with --debug-mode.

  • Enclave status:

    nitro-cli describe-enclaves
  • Signatory logs: Check Signatory's own logs for VSock connection errors, KMS authentication failures, or key decryption issues.

Deployment Checklist

After completing your setup, verify each of the following:

  1. Enclave running: nitro-cli describe-enclaves shows state RUNNING
  2. Signatory process healthy: systemctl status signatory or docker ps
  3. Keys accessible: curl http://localhost:6732/keys/tz4YourKey returns the public key
  4. Signing works: Baker can sign through the Signatory endpoint
  5. No plaintext keys on disk: grep -rE "edsk|spsk|p2sk|BLsk" /var/lib/signatory/ /etc/signatory/ /tmp/ returns nothing
  6. No hardcoded credentials: Config files use IAM roles or environment variables, not embedded AWS keys
  7. KMS attestation active: KMS key policy includes PCR0 condition (not zero placeholder)
  8. Services enabled for reboot: systemctl enable nitro-enclave.service signatory.service