Self-Hosting Guide
Deploy Sigil Auth with Docker Compose — mnemonic initialization, webhooks, TLS, and production configuration
Self-Hosting Guide
Sigil Auth is designed to run on your infrastructure. This guide walks you through deploying the full stack with Docker Compose, from first boot to production configuration.
What You’ll Need
- Docker and Docker Compose installed
- A server with 2 CPU cores and 4GB RAM minimum
- APNs and FCM credentials (for push notifications)
- A domain name (for production TLS)
Quick Start (Development)
Get the stack running in under 5 minutes.
Step 1: Clone the repository
git clone https://github.com/sigilauth/server.git
cd server
Step 2: Start the stack
make up
This starts three services:
- sigil — The stateless auth server (port 8443)
- relay — Push notification relay (port 8080)
- postgres — Database for relay token storage
Step 3: Complete initialization
On first boot, the server starts in init mode. Open your browser:
https://localhost:8443/init
Your browser warns about the self-signed certificate. That’s expected in development — click through.
The wizard shows you a 24-word mnemonic. This is the server’s master secret. Write it down. Store it securely. You’ll need it if you restart the container or migrate to a new server.
[!WARNING] If you lose the mnemonic, you cannot recover the server’s keypair. All paired devices will stop working. There is no password reset, no backup mechanism. The mnemonic is the only recovery path.
Click Continue. The server generates its keypair from the mnemonic and shows you:
- The server’s public key
- The server’s pictogram (five emoji)
Click Generate API Key. Copy the key. This is what your backend will use to call the Sigil API.
The server is now in operational mode. It’s ready to handle challenges.
Step 4: Set your API key
The init wizard gives you an API key. You need to make it available to your application backend so the SDK can authenticate with Sigil.
Option 1: Shell environment variable (Quick test, ephemeral)
export SIGIL_API_KEY='sgk_test_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2'
node your-app.js
Option 2: .env file (Development, persists across runs)
Create a .env file in your project root:
echo "SIGIL_API_KEY=sgk_test_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" > .env
Add .env to .gitignore:
echo ".env" >> .gitignore
Load it in your app:
Node.js:
import 'dotenv/config'; // npm install dotenv
import { SigilAuth } from '@sigilauth/sdk';
const sigil = new SigilAuth({
serviceUrl: 'https://localhost:8443'
});
// SDK reads process.env.SIGIL_API_KEY automatically
Go:
import (
"github.com/joho/godotenv" // go get github.com/joho/godotenv
"github.com/sigilauth/server/sdk-go"
)
godotenv.Load() // Loads .env into environment
client, _ := sigilauth.New(sigilauth.Config{
ServiceURL: "https://localhost:8443",
APIKey: os.Getenv("SIGIL_API_KEY"),
// ...
})
Option 3: Docker Compose environment (Running app in container)
services:
app:
image: your-app:latest
environment:
SIGIL_API_KEY: sgk_test_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
Option 4: Secret manager (Production — use AWS Secrets Manager, HashiCorp Vault, etc.)
Never commit API keys to git. Never hardcode them in source code. The SDK enforces this — it will throw an error if you try to pass the key directly instead of loading from the environment.
Configuration Reference
The stack is configured via environment variables. Check docker-compose.yml for the full list. Here are the important ones:
Sigil Service
| Variable | Default | Description |
|---|---|---|
SIGIL_MODE |
operational |
Boot mode: init or operational |
SIGIL_MNEMONIC |
— | 24-word BIP39 mnemonic (development only — never use in production; insecure to pass secrets via env vars) |
SIGIL_MNEMONIC_PATH |
— | Path to file containing mnemonic (required in production unless using SIGIL_MNEMONIC for dev; server will not start without one or the other) |
RELAY_URL |
http://relay:8080 |
Push relay service URL |
API_KEY |
— | Pre-set API key (optional, wizard generates one) |
WEBHOOK_URL |
— | Your webhook endpoint for async events |
TLS_CERT_PATH |
/var/lib/sigil/tls/cert.pem |
Path to TLS certificate |
TLS_KEY_PATH |
/var/lib/sigil/tls/key.pem |
Path to TLS private key |
TLS_AUTO_GENERATE |
true |
Auto-generate self-signed cert on first boot |
SIGIL_PORT |
8443 |
HTTPS port |
SIGIL_TELEMETRY |
full |
Telemetry mode: full, minimal, or none |
SIGIL_ATTESTATION_REQUIRED |
false |
Require device attestation (set true in production) |
SIGIL_ALLOW_EMULATORS |
false |
Allow emulator/simulator devices (dev only) |
Relay Service
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string |
APNS_KEY_PATH |
— | Path to APNs .p8 key file |
APNS_KEY_ID |
— | APNs key ID |
APNS_TEAM_ID |
— | APNs team ID |
FCM_SERVICE_ACCOUNT |
— | Path to FCM service account JSON |
RELAY_PORT |
8080 |
HTTP port (internal only) |
LOG_LEVEL |
info |
Log level: debug, info, warn, error |
Production Deployment
Development mode uses self-signed TLS and allows emulators. Production requires real certificates and device attestation.
Step 1: Prepare your mnemonic
Generate a mnemonic or reuse the one from your development setup. Store it in a file, not an environment variable.
echo "your 24 word mnemonic here" > /var/lib/sigil/mnemonic.txt
chmod 600 /var/lib/sigil/mnemonic.txt
In production, use a secret manager (AWS Secrets Manager, HashiCorp Vault, etc.) instead of a file on disk.
Step 2: Get TLS certificates
You need a valid certificate for your domain. Three options:
Option A: Let’s Encrypt with Caddy
Use the provided Caddy reverse proxy configuration at deploy/caddy/Caddyfile. Caddy handles ACME automatically.
cd deploy/caddy
docker-compose up -d
Option B: Let’s Encrypt with Certbot
certbot certonly --standalone -d sigil.example.com
Then mount the cert into the container:
volumes:
- /etc/letsencrypt/live/sigil.example.com:/var/lib/sigil/tls:ro
Option C: Provide your own certificate
Place cert.pem and key.pem in /var/lib/sigil/tls/ and mount the directory.
Step 3: Configure reverse proxy and trusted IPs
[!WARNING] When deploying behind a reverse proxy (Caddy, nginx, CloudFlare), configure
TRUSTED_PROXY_IPor equivalent env var so the server correctly identifies client IPs for rate limiting. Without this,X-Forwarded-Forcan be spoofed by attackers, bypassing rate limits.
Caddy example:
Caddy automatically sets X-Forwarded-For. Configure Sigil to trust the proxy:
export TRUSTED_PROXY_IP=172.18.0.1 # Your Caddy container IP
Or trust the entire Docker network:
export TRUSTED_PROXY_CIDR=172.18.0.0/16
Nginx example:
Configure nginx to pass the real client IP:
location / {
proxy_pass https://sigil:8443;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Then configure Sigil to trust nginx:
export TRUSTED_PROXY_IP=172.18.0.2 # Your nginx container IP
CloudFlare:
If you’re behind CloudFlare, trust their IP ranges and use CF-Connecting-IP instead of X-Forwarded-For:
export TRUSTED_PROXY_PROVIDER=cloudflare
export USE_CF_CONNECTING_IP=true
Sigil will automatically fetch and trust CloudFlare’s published IP ranges.
Direct deployment (no proxy):
If Sigil is directly exposed to the internet, leave TRUSTED_PROXY_IP unset. The server will use the connection’s remote address for rate limiting.
Step 4: Configure production overrides
Use the production compose file:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
This applies production settings:
- Resource limits
- Attestation required
- No emulators allowed
- TLS from provided certs (not auto-generated)
- Named volumes for persistence
Step 5: Set APNs and FCM credentials
APNs (iOS):
- Download your .p8 key from Apple Developer
- Mount it into the relay container:
volumes:
- ./apns-key.p8:/var/lib/relay/apns-key.p8:ro
- Set environment variables:
export APNS_KEY_PATH=/var/lib/relay/apns-key.p8
export APNS_KEY_ID=ABCD1234EF
export APNS_TEAM_ID=XYZ9876543
FCM (Android):
- Download your service account JSON from Firebase Console
- Mount it:
volumes:
- ./fcm-service-account.json:/var/lib/relay/fcm-service-account.json:ro
- Set environment variable:
export FCM_SERVICE_ACCOUNT=/var/lib/relay/fcm-service-account.json
Step 6: Configure webhooks
If you want async notification of challenge results, set a webhook URL:
export WEBHOOK_URL=https://your-app.example.com/webhooks/sigil
Generate a webhook secret:
export WEBHOOK_SECRET=$(openssl rand -hex 32)
Your app will use this secret to verify webhook signatures. See SDK documentation for details.
Step 7: Start the stack
make prod
Or manually:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Check logs:
make logs
Verify health:
curl -k https://localhost:8443/health
Expected responses:
In init mode (before completing the wizard):
{
"status": "initializing",
"mode": "init",
"version": "1.0.0"
}
In operational mode (after initialization):
{
"status": "healthy",
"mode": "operational",
"version": "1.0.0"
}
If you see "status": "unhealthy", check the logs with make logs or docker-compose logs sigil.
Webhook Configuration
Webhooks notify your app when challenges complete. This is more efficient than polling.
Setting up webhooks
Set WEBHOOK_URL and WEBHOOK_SECRET environment variables on the Sigil service.
Sigil will POST to your webhook URL when:
- A challenge is verified
- A challenge is rejected
- A challenge expires
- An MPA request is approved
- An MPA request is rejected
- An MPA request times out
Webhook payload format
{
"type": "challenge.verified",
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"fingerprint": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"status": "verified",
"timestamp": "2026-04-24T10:32:15Z"
}
Event types:
challenge.verifiedchallenge.rejectedchallenge.expiredmpa.approvedmpa.rejectedmpa.timeout
Verifying webhook signatures
Every webhook includes these headers:
X-Sigil-Signature: v1,<hmac-sha256-hex>
X-Sigil-Timestamp: <unix-timestamp>
Use the SDK to verify:
Node:
const isValid = sigil.webhooks.verify(
req.headers,
req.body, // raw Buffer, not parsed JSON
process.env.WEBHOOK_SECRET
);
Go:
err := client.Webhooks.Verify(
r.Header,
body,
os.Getenv("WEBHOOK_SECRET"),
)
Reject webhooks older than 5 minutes to prevent replay attacks.
Observability
Sigil Auth provides metrics, logs, and distributed tracing.
Metrics (Prometheus)
The /metrics endpoint exposes Prometheus metrics:
http://localhost:8443/metrics
Key metrics:
sigil_challenges_total{status="verified|rejected|expired"}sigil_mpa_requests_total{status="approved|rejected|timeout"}sigil_webhook_deliveries_total{status="success|failure"}sigil_challenge_duration_seconds(histogram)
Hook this up to your Prometheus server. A Grafana dashboard is provided at deploy/observability/grafana/dashboards/sigil.json.
Logs (structured JSON)
Both services log to stdout in structured JSON:
{
"timestamp": "2026-04-24T10:32:15Z",
"level": "info",
"service": "sigil",
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"event": "challenge_created",
"fingerprint": "a1b2c3...",
"action_type": "step_up"
}
Collect with your log aggregator (Loki, Elasticsearch, CloudWatch, etc.).
Set LOG_LEVEL=debug for verbose output (development only — noisy in production).
Distributed Tracing (OpenTelemetry)
Sigil exports traces in OpenTelemetry format. Configure the collector endpoint:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4317
Traces cover:
- Challenge creation → push → response → verification
- MPA request → multi-device coordination → quorum
- Webhook delivery attempts
Backup and Recovery
What to back up
- The mnemonic — This is the only thing you need. Store it offline, encrypted, in multiple locations.
- Relay database — PostgreSQL backups for push token mappings. Losing this means devices won’t get push notifications until they re-register.
What you don’t need to back up
- Sigil service state (it’s stateless — everything derives from the mnemonic)
- TLS certificates (regenerate with ACME or re-issue)
- API keys (regenerate via the init wizard)
Disaster recovery procedure
If your server is destroyed:
- Spin up a new server
- Restore the mnemonic to
SIGIL_MNEMONIC_PATH - Start the stack with
SIGIL_MODE=operational - The server regenerates the same keypair from the mnemonic
- Devices see the same server pictogram and continue working
- Restore the relay database from backup (or let devices re-register their push tokens)
The mnemonic is the single source of truth. Protect it like a root password.
Security Checklist
Before going to production:
- [ ] Mnemonic stored in a secret manager, not a file or environment variable
- [ ]
SIGIL_ATTESTATION_REQUIRED=true(rejects devices without hardware attestation) - [ ]
SIGIL_ALLOW_EMULATORS=false(blocks simulators and emulators) - [ ] TLS certificate from a trusted CA (not self-signed)
- [ ] Webhook secret is 32+ random bytes
- [ ] Relay database not exposed to the public internet
- [ ] API keys rotated and stored securely
- [ ] Logs scrubbed of sensitive data (Sigil does this by default)
- [ ] Metrics endpoint not publicly accessible (or protected by auth)
- [ ] Resource limits set in
docker-compose.prod.yml
Scaling
Sigil Auth is stateless. You can run multiple replicas behind a load balancer without coordination.
The relay service is also stateless except for PostgreSQL. Scale horizontally and use a managed Postgres instance (RDS, Cloud SQL, etc.).
For high availability:
- Run 3+ Sigil replicas across availability zones
- Use a managed PostgreSQL cluster with replication
- Distribute the relay service geographically for lower push latency
Typical load capacity per instance (2 CPU, 4GB RAM):
- 1000 challenges/second
- 500 MPA requests/second
- 10,000 concurrent challenge sessions
Upgrading
Sigil Auth follows semantic versioning. Minor and patch updates are backwards-compatible.
Upgrade procedure
- Pull the latest images:
docker-compose pull
- Restart the stack:
docker-compose up -d
- Check health:
curl -k https://localhost:8443/health
Database migrations (relay only) run automatically on startup.
Before major version upgrades, check the changelog for breaking changes.
Troubleshooting
“SIGIL_MNEMONIC or SIGIL_MNEMONIC_PATH required”
The server needs a mnemonic to derive its keypair. Either:
- Set
SIGIL_MNEMONIC(dev only), or - Set
SIGIL_MNEMONIC_PATHto a file containing the mnemonic
“Failed to connect to relay”
The Sigil service can’t reach the relay. Check:
- Is the relay container running? (
docker ps) - Is
RELAY_URLcorrect? (default:http://relay:8080) - Are they on the same Docker network?
“Push notification failed: 404 device not found”
The device hasn’t registered its push token with the relay. The app registers on first launch. If you reset the relay database, devices need to re-register (restart the app or wait for the next background refresh).
“Certificate verify failed”
In development, the Sigil service uses a self-signed certificate. Your SDK client needs to skip verification (Node: rejectUnauthorized: false, Go: InsecureSkipVerify: true) or trust the self-signed cert.
In production, use a real certificate from Let’s Encrypt or a trusted CA.
Logs show “challenge expired before response”
The device took longer than 5 minutes to respond. Challenges have a hard 5-minute TTL. Check:
- Is the device online?
- Did the user get the push notification?
- Is the relay delivering pushes? (check relay logs)
Next Steps
- Integrator Quickstart — Add Sigil Auth to your app with the SDK
- MPA Setup Guide — Configure multi-party approval policies
- Node SDK Reference — Full API documentation
- Go SDK Reference — Full API documentation
For production support, monitoring, and SLA guarantees, see the hosted offering at sigilauth.com (coming soon).