Deploy self-hosted
Configure, register, and verify a Virtual Signer in your own infrastructure
This guide deploys a Broker and a Multi-Party Computation (MPC) Core with Docker Compose. It targets the io.sign launch configuration. Use compatible images and the confidential-computing host configuration supplied for your release; the Compose file alone does not provision a confidential virtual machine.
Before you start
Obtain the following before running the containers:
- A supported confidential-computing host and its required device access. See Prerequisites and Self-hosted architecture.
- Docker with Compose v2, registry pull access, and compatible Broker and MPC Core image tags.
- Machine-client credentials, organisation ID, device name, authentication URL template, signing-stream URL, and MQTT URL for the same environment.
- An HTTPS custom policy server or OPA adapter that handles key generation, signing, and resharing.
- A Broker client certificate/key and Core server certificate/key, issued for mutual TLS (mTLS). The Core server certificate must cover
vs-core, the hostname used below. Each peer pins the other's certificate. - An approved encryption configuration: Azure Secure Key Release (SKR), or the direct-key configuration supported by your release.
- A disaster recovery public key, generated by the recovery custodian. Keep its private half offline and off the signer host. See Virtual Signer disaster recovery.
Use credentials for an automated signer, not a human user's mobile login. The Broker registers its initial device using these credentials and retains its identity in persistent storage.
Prepare files
Create a private deployment directory containing:
virtual-signer/
.env
broker.env
core.env
docker-compose.yml
secrets/
broker-client.pem
broker-client.key
core-server.pem
core-server.key
mlkem768_pub.pemRestrict access to this directory and exclude populated environment files and private keys from version control. Ensure the container user can read the mounted files.
Create .env with the exact registry image references supplied for your release:
IOSIGN_BROKER_IMAGE=<registry>/<broker-image>:<version>
IOSIGN_CORE_IMAGE=<registry>/<core-image>:<version>
IOSIGN_SIGNER_ENV=prodReplace every angle-bracket placeholder before starting. IOSIGN_BROKER_IMAGE and IOSIGN_CORE_IMAGE are Compose substitutions used by this example, not runtime signer settings.
Create broker.env:
IOSIGN_SIGNER_LOG_LEVEL=info
IOSIGN_SIGNER_BINDING_NETWORK_NAME=0.0.0.0
IOSIGN_SIGNER_BROKER_PORT=8182
IOSIGN_SIGNER_BROKER_AUTH_CLIENT_ID=<machine-client-id>
IOSIGN_SIGNER_BROKER_AUTH_CLIENT_SECRET=<machine-client-secret>
IOSIGN_SIGNER_BROKER_ORG_ID=<organisation-id>
IOSIGN_SIGNER_BROKER_INITIAL_DEVICE=<unique-device-name>
IOSIGN_SIGNER_BROKER_PLATFORM_AUTH_URL=https://<auth-host>/<auth-path>/:auth_method
IOSIGN_SIGNER_BROKER_SIGNING_SERVICE_URL=https://<signing-host>:50051
IOSIGN_SIGNER_MQTT_URL=wss://<mqtt-host>:8084/mqtt
IOSIGN_SIGNER_BROKER_MPC_CORE_URL=https://vs-core:8181
IOSIGN_SIGNER_BROKER_MPC_CORE_TLS_CERT_FILE=/run/signer/broker-client.pem
IOSIGN_SIGNER_BROKER_MPC_CORE_TLS_KEY_FILE=/run/signer/broker-client.key
IOSIGN_SIGNER_BROKER_MPC_CORE_TLS_SERVER_PIN_CERT_FILE=/run/signer/core-server.pem
IOSIGN_SIGNER_BROKER_APPROVAL_MODE=api-approval
IOSIGN_SIGNER_BROKER_APPROVAL_EXTERNAL_POLICY_BASE_URL=https://<policy-host>/<policy-prefix>
IOSIGN_SIGNER_BROKER_APPROVAL_EXTERNAL_POLICY_TIMEOUT=5
IOSIGN_SIGNER_BROKER_APPROVAL_SLEEP_DURATION=5
IOSIGN_SIGNER_BROKER_STORAGE_BASE_PATH=/var/vsigner
IOSIGN_SIGNER_BROKER_BACKUP_BASE_PATH=/var/vsigner-backups
IOSIGN_SIGNER_BROKER_DR_PK_PEM_FILE=/run/signer/mlkem768_pub.pemUse the supplied authentication template exactly, retaining literal :auth_method. Add the CA settings from Configuration when your signing, MQTT, or policy endpoints use private certificate authorities. Mount those additional files into the container as well.
Create core.env:
IOSIGN_SIGNER_LOG_LEVEL=info
IOSIGN_SIGNER_BINDING_NETWORK_NAME=0.0.0.0
IOSIGN_SIGNER_CORE_PORT=8181
IOSIGN_SIGNER_CORE_HEALTH_PORT=2114
IOSIGN_SIGNER_MQTT_URL=wss://<mqtt-host>:8084/mqtt
IOSIGN_SIGNER_CORE_TLS_CERT_FILE=/run/signer/core-server.pem
IOSIGN_SIGNER_CORE_TLS_KEY_FILE=/run/signer/core-server.key
IOSIGN_SIGNER_CORE_TLS_CLIENT_PIN_CERT_FILE=/run/signer/broker-client.pemAdd your encryption configuration to MPC Core before starting. For the direct-key path, inject IOSIGN_SIGNER_CORE_ENCRYPTION_KEY securely with a retained 32-byte key encoded as hex or base64. For SKR, configure all required SKR values and deploy its reachable sidecar. The example does not generate or rotate your encryption key.
Create the Compose file
Save as docker-compose.yml. This example assumes a supported SEV-SNP guest with /dev/sev-guest; adapt device access only according to your platform's release instructions.
name: io-sign-signer
services:
vs-core:
image: ${IOSIGN_CORE_IMAGE:?set IOSIGN_CORE_IMAGE}
env_file: core.env
environment:
IOSIGN_SIGNER_ENV: ${IOSIGN_SIGNER_ENV:?set IOSIGN_SIGNER_ENV}
devices:
- /dev/sev-guest:/dev/sev-guest:rw
volumes:
- ./secrets/core-server.pem:/run/signer/core-server.pem:ro
- ./secrets/core-server.key:/run/signer/core-server.key:ro
- ./secrets/broker-client.pem:/run/signer/broker-client.pem:ro
ports:
- "127.0.0.1:2114:2114"
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
vs-broker:
image: ${IOSIGN_BROKER_IMAGE:?set IOSIGN_BROKER_IMAGE}
env_file: broker.env
environment:
IOSIGN_SIGNER_ENV: ${IOSIGN_SIGNER_ENV:?set IOSIGN_SIGNER_ENV}
depends_on:
- vs-core
volumes:
- signer_data:/var/vsigner
- signer_backups:/var/vsigner-backups
- ./secrets/broker-client.pem:/run/signer/broker-client.pem:ro
- ./secrets/broker-client.key:/run/signer/broker-client.key:ro
- ./secrets/core-server.pem:/run/signer/core-server.pem:ro
- ./secrets/mlkem768_pub.pem:/run/signer/mlkem768_pub.pem:ro
ports:
- "127.0.0.1:8182:8182"
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
volumes:
signer_data:
signer_backups:The Core RPC port is reachable only within the Compose network. Health endpoints bind to localhost on the host for operator checks. depends_on establishes startup order, not service readiness; verify health below before assigning the signer to a vault.
Start and verify registration
docker login <registry>
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 vs-core vs-broker
curl --fail-with-body --retry 30 --retry-all-errors --retry-delay 2 \
--retry-max-time 120 --max-time 5 'http://127.0.0.1:2114/core/v1/health?strict=true' &&
curl --fail-with-body --retry 30 --retry-all-errors --retry-delay 2 \
--retry-max-time 120 --max-time 5 'http://127.0.0.1:8182/broker/v1/health?strict=true'The health checks retry connection failures and unhealthy responses during startup. Each request has a five-second timeout and retries are limited to a two-minute window. If either check fails after retries, inspect the service logs before continuing.
Wait for registration and confirm the expected signer name and ID in your organisation's management interface or API. Both health responses should report status: "ok"; inspect individual probes when a response is degraded. A healthy process is not proof it can complete key generation or signing.
Use the signer
- Create a test vault that includes the registered signer ID, with the weights and threshold you intend to evaluate.
- Confirm your policy approves only the intended key-generation request. Wait until the vault has a public key.
- Submit a test signing request and verify both the policy decision and the final signature. See Get started with io.sign.
- Submit a request your policy rejects. Confirm the Virtual Signer contributes no approval; another signer set may still satisfy the threshold.
- Test resharing, then export a fresh disaster recovery bundle.
- Restart the services and repeat signing. Confirm the signer ID persists rather than a new signer appearing.
docker compose exec vs-broker signerctl share backup
docker compose exec vs-broker ls /var/vsigner-backupsCopy the generated bundle to your approved off-host recovery store and have the recovery custodian verify it using Virtual Signer disaster recovery. Keeping a backup only in a volume on the same host does not protect against host loss.
Maintain the deployment
# Stop and restart existing containers
docker compose stop
docker compose start
# Recreate after changing environment files or pinned image references
docker compose up -d --force-recreateBefore upgrades, back up persistent signer state and retain the required encryption material. Update both images according to release compatibility instructions, pull them, recreate the containers, and repeat Testing the deployment.
Preserve signer_data, signer_backups, and the Compose project name. Do not use docker compose down -v for an enrolled signer: it deletes named volumes. Do not run a restored copy alongside the original signer identity.
Troubleshooting
| Symptom | Action |
|---|---|
| Environment validation fails | Check current IOSIGN_SIGNER_* names, required identity fields, mounted certificate paths, and encryption configuration. |
IMMUTABLE_CONFIG_MISMATCH | Restore the approved sealed configuration or follow a signer replacement procedure. Do not delete the state to force a restart. |
| Core connection fails | Verify the internal hostname, port, matching certificate/key pairs, and certificate pins. |
| MQTT probe fails | Check the environment's broker URL, certificate trust, authentication, and outbound access. |
| Policy rejects every request | Inspect the /keygen, /sign, and /reshare response contracts and the policy service's logs. |
| Existing signer disappears after restart | Verify that the same persistent state is mounted before registering another signer. |
Updated about 2 months ago