Vouch Server Operator Guide
This documentation covers deploying, configuring, and operating Vouch Server — the authentication server that issues short-lived credentials after FIDO2 verification with a YubiKey. It covers three deployment patterns: cloud, on-premise, and air-gapped.
For CLI installation, enrollment, integration guides (SSH, AWS, EKS, GitHub, Docker), and OIDC provider documentation (endpoints, tokens, grant types), visit vouch.sh/docs.
What Vouch Server Does
Vouch Server is the backend that makes hardware-backed authentication work:
- OIDC Provider — Issues DPoP-bound access tokens after FIDO2 verification
- SSH Certificate Authority — Signs short-lived Ed25519 certificates
- Credential Broker — Exchanges access tokens for AWS STS credentials
- SCIM Endpoint — Receives user provisioning/de-provisioning from your IdP
- WebAuthn Relying Party — Manages FIDO2 credential registration and assertion
Architecture
| Component | Description | License |
|---|---|---|
vouch CLI | User-facing commands, credential helpers | Apache-2.0 OR MIT |
vouch-agent | Background daemon, session management | Apache-2.0 OR MIT |
vouch-common | Shared types, FIDO2 helpers, API client | Apache-2.0 OR MIT |
| Vouch Server | OIDC provider, certificate authority | Apache-2.0 OR MIT |
Security
Vouch is designed for high-security environments:
- Memory-safe implementation — Written in Rust
- No credential storage — Vouch never sees your private keys
- Cryptographic presence attestation — FIDO2 with user verification
- Short-lived credentials — Minimize blast radius of compromise
- Audit trail — Every credential issuance logged with attestation
Get started with the Deployment Overview.
Deployment Overview
This section covers deploying the Vouch server for your organization. The server is the central authentication backend that handles FIDO2 verification, session management, SSH certificate signing, and OIDC token issuance.
Deployment Checklist
Before deploying, ensure you have:
- Domain name — A domain for your Vouch server (e.g.,
auth.example.com) - TLS certificate — Valid certificate for your domain (or use Let’s Encrypt)
- Database — SQLite (single node) or PostgreSQL (multi-node)
- Identity provider — Google Workspace
- JWT secret — Cryptographically random string, minimum 32 characters (or use AWS KMS HMAC)
- SSH CA key (optional) — Ed25519 key pair for signing SSH certificates (or use AWS KMS)
- OIDC signing key (optional) — P-256 EC key for signing ID tokens (or use AWS KMS)
Architecture
Internet
│
▼
┌─────────────────┐
│ Load Balancer │
│ (TLS termination │
│ or passthrough) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Vouch Server │
│ │
│ • Auth Portal │
│ • OIDC Provider│
│ • SSH CA │
│ • REST API │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Database │
│ │
│ SQLite or │
│ PostgreSQL │
└─────────────────┘
Deployment Methods
| Method | Best For | Guide |
|---|---|---|
| Systemd | Bare metal, VMs, single-node | Production |
| Docker | Container-based deployments | Production |
| Kubernetes | Multi-node, high availability | Production |
Configuration
All configuration is via environment variables. See the Configuration Reference for the full list.
The minimum configuration requires:
VOUCH_RP_ID=auth.example.com # Your domain
VOUCH_JWT_SECRET=<64-char-secret> # Session signing secret
VOUCH_DATABASE_URL=sqlite:vouch.db?mode=rwc # Database
For production, you’ll also want:
VOUCH_TLS_CERT=<base64-encoded-pem> # TLS certificate
VOUCH_TLS_KEY=<base64-encoded-pem> # TLS private key
VOUCH_SSH_CA_KEY=<base64-encoded-pem> # SSH CA key (or VOUCH_SSH_CA_KMS_KEY_ID)
VOUCH_IDPS=google # External IdP(s)
VOUCH_IDP_GOOGLE_TYPE=oidc
VOUCH_IDP_GOOGLE_ISSUER=https://accounts.google.com
VOUCH_IDP_GOOGLE_CLIENT_ID=...
VOUCH_IDP_GOOGLE_CLIENT_SECRET=...
For AWS deployments, you can use KMS for all signing operations instead of managing local keys. See the Configuration Reference for KMS options.
Sizing
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 1 vCPU | 2 vCPU |
| Memory | 256 MB | 512 MB |
| Disk | 1 GB (SQLite) | 10 GB (PostgreSQL) |
The server is single-process, async (tokio). Per-session memory overhead is minimal (~2 KB for token metadata). The primary bottleneck is database I/O during token issuance and session validation.
Database guidance:
- SQLite — suitable for single-node deployments under ~500 users
- PostgreSQL — recommended for multi-node or >500 users
- Aurora DSQL — for AWS deployments requiring managed infrastructure
Next Steps
- Database Setup — Choose and configure your database
- TLS Configuration — Set up HTTPS
- Configuration Reference — Full environment variable reference
- Identity Provider Setup — Connect your corporate IdP
Configuration Reference
This chapter describes the Vouch server configuration system, including configuration sources, S3-based configuration, and hot-reload behavior.
Vouch server supports multiple configuration methods with a defined precedence order.
Configuration Sources (Priority Order)
- S3 Configuration (highest) — JSON file fetched from S3
- Environment Variables — Standard
VOUCH_*prefixed variables - Command-line Arguments — Direct CLI arguments
When S3 configuration is enabled, it overrides environment variables. This allows for centralized configuration management with dynamic updates.
S3-Based Configuration
For production deployments, Vouch supports loading configuration from an S3 object. This enables:
- Centralized management — Single source of truth for multi-instance deployments
- Dynamic updates — Configuration changes without server restart (for supported fields)
- TLS hot-reload — Automatic certificate rotation without downtime
- Secrets management — Leverage S3 encryption and IAM for credential protection
Enabling S3 Configuration:
# Required: bucket name
VOUCH_S3_CONFIG_BUCKET=my-bucket
# Optional: object key (default: config/vouch-server.json)
VOUCH_S3_CONFIG_KEY=config/vouch-server.json
# Optional: AWS region (uses default credential chain region if not set)
VOUCH_S3_CONFIG_REGION=us-west-2
# Optional: polling interval in seconds (default: 60)
VOUCH_S3_CONFIG_POLL_INTERVAL=60
S3 Configuration JSON Schema:
{
"version": 1,
"listen_addr": "0.0.0.0:443",
"rp_id": "vouch.example.com",
"rp_name": "Example Corp",
"base_url": "https://vouch.example.com",
"database_url": "postgres://...",
"jwt_secret": "32+ character secret",
"tls": {
"cert": "<base64-encoded PEM certificate>",
"key": "<base64-encoded PEM private key>"
},
"idps": [
{
"id": "google",
"type": "oidc",
"issuer": "https://accounts.google.com",
"client_id": "...",
"client_secret": "..."
},
{
"id": "corp-saml",
"type": "saml",
"metadata_url": "https://idp.example.com/saml/metadata",
"sp_entity_id": "https://vouch.example.com"
}
],
"allowed_domains": ["example.com"],
"ssh_ca_key": "<base64-encoded PEM Ed25519 private key>",
"ssh_ca_kms_key_id": "mrk-...",
"oidc_signing_key": "<base64-encoded PEM EC P-256 private key>",
"oidc_signing_kms_key_id": "mrk-...",
"oidc_rsa_signing_key": "<base64-encoded PEM RSA-3072 private key>",
"oidc_rsa_signing_kms_key_id": "mrk-...",
"jwt_hmac_kms_key_id": "mrk-..."
}
See the S3 Configuration Schema for the full field reference.
All certificate and key fields are base64-encoded PEM strings:
# Encode a PEM file for S3 config
base64 -i cert.pem | tr -d '\n'
AWS KMS Signing Keys
As an alternative to managing local key material, Vouch supports AWS KMS for signing operations:
| Environment Variable | Key Type | Replaces |
|---|---|---|
VOUCH_SSH_CA_KMS_KEY_ID | Ed25519 (ECC_EDWARDS_CURVE_25519) | VOUCH_SSH_CA_KEY / VOUCH_SSH_CA_KEY_PATH |
VOUCH_OIDC_SIGNING_KMS_KEY_ID | P-256 (ECC_NIST_P256) | VOUCH_OIDC_SIGNING_KEY |
VOUCH_OIDC_RSA_SIGNING_KMS_KEY_ID | RSA-3072 (RSA_3072) | VOUCH_OIDC_RSA_SIGNING_KEY |
VOUCH_JWT_HMAC_KMS_KEY_ID | HMAC-256 (HMAC_256) | VOUCH_JWT_SECRET |
Multi-region keys (mrk- prefix) are recommended for high availability. KMS key IDs can also be set in the S3 config (ssh_ca_kms_key_id, oidc_signing_kms_key_id, jwt_hmac_kms_key_id).
See Key Management for generation and rotation details.
Hot-Reloadable vs Startup-Only Fields
| Field | Hot-Reloadable | Notes |
|---|---|---|
tls.cert, tls.key | Yes | Automatic reload on change |
| All other fields | No | Requires server restart |
Non-hot-reloadable fields include: jwt_secret, database_url, listen_addr, rp_id, rp_name, session_hours, cors_origins, allowed_domains, dpop.*, OIDC settings, SAML settings, GitHub App settings, SSH CA key, OIDC signing keys, and all KMS key IDs.
Changes to non-hot-reloadable fields in S3 are silently ignored. A server restart is required to apply them.
TLS Certificate Hot-Reload
Vouch supports automatic TLS certificate reloading without dropping connections:
- Via S3 polling — Update
tls.certandtls.keyin S3 config; server detects change via ETag and reloads - Via SIGHUP — Send
SIGHUPto the server process to reload TLS certificates
# Manual TLS certificate reload (Unix only)
kill -SIGHUP $(pgrep vouch-server)
Note: SIGHUP only reloads TLS certificates. It does not reload any other configuration fields.
Database Setup
Vouch supports three database backends. Choose based on your deployment requirements.
SQLite (Default)
Best for single-node deployments and development. No external dependencies.
VOUCH_DATABASE_URL=sqlite:vouch.db?mode=rwc
The database file is created automatically on first startup. Migrations run automatically.
Recommendations:
- Store the database on a persistent volume
- Set restrictive file permissions:
chmod 700 /data - Back up the file regularly (it’s a single file)
# Create data directory
mkdir -p /data
chmod 700 /data
# Configure
export VOUCH_DATABASE_URL="sqlite:/data/vouch.db?mode=rwc"
PostgreSQL
Best for multi-node deployments, high availability, and production environments.
VOUCH_DATABASE_URL=postgres://user:password@db.example.com:5432/vouch
Setup:
-
Create a PostgreSQL database:
CREATE DATABASE vouch; CREATE USER vouch WITH PASSWORD 'secure-password'; GRANT ALL PRIVILEGES ON DATABASE vouch TO vouch; -
Configure the connection:
export VOUCH_DATABASE_URL="postgres://vouch:secure-password@db.example.com:5432/vouch" -
Migrations run automatically on server startup.
Recommendations:
- Use SSL for database connections in production
- Configure connection pooling at the database level
- Set up automated backups
Aurora DSQL
For AWS deployments requiring serverless, distributed SQL with strong consistency.
Aurora DSQL endpoints are auto-detected when the DATABASE_URL hostname contains .dsql. and ends with .on.aws. IAM authentication tokens are generated automatically.
VOUCH_DATABASE_URL=postgres://admin@abcdef123456.dsql.us-east-1.on.aws:5432/vouch
Multi-region configuration uses a dsql_endpoints map in the S3 configuration JSON, resolved via AWS_AZ or AWS_REGION environment variables.
Migrations
Database migrations are embedded in the server binary and run automatically on startup. There is no manual migration step required.
- SQLite migrations:
crates/vouch-server/migrations/sqlite/ - PostgreSQL migrations:
crates/vouch-server/migrations/postgres/
Backup
| Database | Backup Method | Frequency |
|---|---|---|
| SQLite | File copy (cp vouch.db vouch.db.backup) | Daily |
| PostgreSQL | pg_dump | Daily |
| Aurora DSQL | AWS automated backups | Continuous |
Always back up before upgrading the Vouch server, as migrations may modify the schema.
TLS Configuration
Vouch requires HTTPS in production. TLS can be configured directly on the Vouch server or terminated at a load balancer.
Direct TLS (Recommended)
When TLS is configured, the server automatically:
- Listens on port 443 (HTTPS)
- Runs an HTTP redirect server on port 80 (308 redirect to HTTPS)
- Makes the
/healthendpoint accessible on HTTP (for load balancer health checks) - Validates the
Hostheader againstrp_idto prevent injection attacks - Ignores
VOUCH_LISTEN_ADDR(ports are fixed at 443/80)
Note: Binding to ports 80 and 443 requires
CAP_NET_BIND_SERVICEcapability on Linux. The RPM/DEB packages configure this automatically.
Configuration
Provide base64-encoded PEM certificates via environment variables:
# Encode your certificate and key
export VOUCH_TLS_CERT="$(base64 -i cert.pem | tr -d '\n')"
export VOUCH_TLS_KEY="$(base64 -i key.pem | tr -d '\n')"
Both VOUCH_TLS_CERT and VOUCH_TLS_KEY must be set together. If only one is provided, the server will fail to start.
TLS Properties
- Protocol: TLS 1.3 and TLS 1.2
- Implementation: rustls (no OpenSSL)
- Ciphers: BCP 195 (RFC 9325) suites only — TLS 1.3 AEAD suites, and ECDHE+AEAD suites for TLS 1.2 (AES-GCM, ChaCha20-Poly1305)
Post-Quantum Key Exchange
Both TLS listeners (HTTPS and mTLS) and all outbound TLS clients (CLI, agent, and server-to-IdP/AWS connections) prefer the X25519MLKEM768 hybrid post-quantum key-exchange group. When the peer supports it — modern browsers, Cloudflare, and AWS endpoints do — the TLS session keys are protected against “harvest now, decrypt later” attacks. Peers without ML-KEM support negotiate classical X25519 or P-256 as usual; no configuration is required on either side.
See Post-Quantum Cryptography for Vouch’s overall PQC posture.
Certificate Hot-Reload
Vouch supports automatic TLS certificate reloading without dropping connections. This is useful for certificate rotation (e.g., Let’s Encrypt renewals).
Via S3 Configuration
If using S3 configuration storage, update the tls.cert and tls.key fields in the S3 config file. The server detects changes via ETag polling and reloads automatically.
Via SIGHUP
Send SIGHUP to the server process to reload TLS certificates:
kill -SIGHUP $(pgrep vouch-server)
Note: SIGHUP only reloads TLS certificates. It does not reload any other configuration.
Self-Signed Certificates (Development)
For development or testing:
# Generate self-signed EC certificate
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout tls_key.pem -out tls_cert.pem -days 365 -nodes \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
# Base64 encode for Vouch
export VOUCH_TLS_CERT="$(base64 -i tls_cert.pem | tr -d '\n')"
export VOUCH_TLS_KEY="$(base64 -i tls_key.pem | tr -d '\n')"
Deployment Methods
Choose a deployment method based on your infrastructure:
| Method | Best For | Complexity |
|---|---|---|
| Systemd (Bare Metal) | Single server, VMs, on-premises | Low |
| Docker | Container-based environments | Low |
| Kubernetes (Helm) | Multi-node, high availability, cloud-native | Medium |
All methods use the same Vouch server binary and configuration via environment variables. The deployment method only affects how the process is managed and how configuration is provided.
Verification
Regardless of deployment method, verify the server is running:
# Health check
curl -k https://auth.example.com/health
# Expected: {"status":"healthy"}
# SSH CA public key (if configured)
curl -k https://auth.example.com/v1/credentials/ssh/ca
# Expected: ssh-ed25519 AAAA... vouch-ca@...
# OIDC discovery
curl -k https://auth.example.com/.well-known/openid-configuration
# Expected: JSON discovery document
Systemd (Bare Metal)
Deploy Vouch as a systemd service on bare metal servers or VMs.
Install via Package
The RPM and DEB packages include a systemd service unit:
# RPM (RHEL/Fedora/Amazon Linux)
rpm -ivh vouch-server-1.0.0-1.x86_64.rpm
# DEB (Debian/Ubuntu)
dpkg -i vouch-server_1.0.0_amd64.deb
The package installs:
- Binary at
/usr/bin/vouch-server - Systemd unit at
/etc/systemd/system/vouch-server.service - Default config at
/etc/vouch/vouch.env - Data directory at
/data(with appropriate permissions)
Configure
Edit the environment file:
sudo cp /etc/vouch/vouch.env /etc/vouch/vouch.env.local
sudo chmod 600 /etc/vouch/vouch.env.local
sudo vi /etc/vouch/vouch.env.local
At minimum, set:
VOUCH_RP_ID=auth.example.com
VOUCH_JWT_SECRET=<your-64-character-secret>
VOUCH_DATABASE_URL=sqlite:/data/vouch.db?mode=rwc
VOUCH_TLS_CERT=<base64-encoded-certificate>
VOUCH_TLS_KEY=<base64-encoded-private-key>
See Configuration Reference for all options.
Start the Service
# Enable and start
sudo systemctl enable --now vouch-server
# Check status
sudo systemctl status vouch-server
# View logs
sudo journalctl -u vouch-server -f
Manual Install (Without Package)
If installing the binary manually:
-
Copy the binary:
sudo cp vouch-server /usr/bin/ sudo chmod 755 /usr/bin/vouch-server -
Create a systemd unit:
# /etc/systemd/system/vouch-server.service [Unit] Description=Vouch Identity Server After=network.target [Service] Type=simple User=vouch Group=vouch EnvironmentFile=/etc/vouch/vouch.env ExecStart=/usr/bin/vouch-server Restart=on-failure RestartSec=5 # Security hardening NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/data AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target -
Create the service user and directories:
sudo useradd -r -s /sbin/nologin vouch sudo mkdir -p /etc/vouch /data sudo chown vouch:vouch /data sudo chmod 700 /data -
Reload and start:
sudo systemctl daemon-reload sudo systemctl enable --now vouch-server
Upgrading
# Back up database
sudo cp /data/vouch.db /data/vouch.db.backup.$(date +%Y%m%d)
# Upgrade package (migrations run automatically on next startup)
sudo rpm -Uvh vouch-server-1.1.0-1.x86_64.rpm
# or: sudo dpkg -i vouch-server_1.1.0_amd64.deb
# Restart
sudo systemctl restart vouch-server
# Verify
curl -k https://auth.example.com/health
Docker
Deploy Vouch using Docker or Docker Compose.
Docker Run
docker run -d \
--name vouch-server \
--restart unless-stopped \
-p 443:443 \
-v vouch-data:/data \
-e VOUCH_RP_ID=auth.example.com \
-e VOUCH_JWT_SECRET=<your-64-character-secret> \
-e VOUCH_DATABASE_URL=sqlite:/data/vouch.db?mode=rwc \
-e VOUCH_TLS_CERT=<base64-encoded-certificate> \
-e VOUCH_TLS_KEY=<base64-encoded-private-key> \
ghcr.io/vouch-sh/vouch:latest
Docker Compose
# docker-compose.yml
services:
vouch-server:
image: ghcr.io/vouch-sh/vouch:latest
container_name: vouch-server
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- vouch-data:/data
env_file:
- vouch.env
environment:
VOUCH_DATABASE_URL: sqlite:/data/vouch.db?mode=rwc
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "--no-check-certificate", "https://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
vouch-data:
Create a vouch.env file:
VOUCH_RP_ID=auth.example.com
VOUCH_JWT_SECRET=<your-64-character-secret>
VOUCH_TLS_CERT=<base64-encoded-certificate>
VOUCH_TLS_KEY=<base64-encoded-private-key>
VOUCH_SSH_CA_KEY=<base64-encoded-ssh-ca-key>
Start:
docker compose up -d
docker compose logs -f vouch-server
With PostgreSQL
# docker-compose.yml
services:
vouch-server:
image: ghcr.io/vouch-sh/vouch:latest
container_name: vouch-server
restart: unless-stopped
ports:
- "443:443"
- "80:80"
env_file:
- vouch.env
environment:
VOUCH_DATABASE_URL: postgres://vouch:password@postgres:5432/vouch
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "--no-check-certificate", "https://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
postgres:
image: postgres:16
container_name: vouch-postgres
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: vouch
POSTGRES_USER: vouch
POSTGRES_PASSWORD: password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vouch"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
Air-Gapped Docker
For air-gapped environments, load the image from a saved archive:
# On connected machine
docker pull ghcr.io/vouch-sh/vouch:1.0.0
docker save ghcr.io/vouch-sh/vouch:1.0.0 -o vouch-server-1.0.0.tar
# Transfer to air-gapped environment
# Load image
docker load < vouch-server-1.0.0.tar
Upgrading
# Pull new image
docker compose pull
# Restart with new image
docker compose up -d
# Verify
docker compose logs -f vouch-server
curl -k https://auth.example.com/health
Kubernetes (Helm)
Deploy Vouch on Kubernetes using the Helm chart.
Prerequisites
- Kubernetes cluster (1.24+)
- Helm 3
- A persistent volume provisioner (for SQLite) or external PostgreSQL
Install
# Install from OCI registry
helm install vouch-server oci://ghcr.io/vouch-sh/charts/vouch-server \
--version 0.1.0 \
--namespace vouch \
--create-namespace \
--values my-values.yaml
Values
Key values to configure:
# values.yaml
image:
repository: ghcr.io/vouch-sh/vouch
pullPolicy: IfNotPresent
tag: "" # defaults to chart appVersion
serviceAccount:
create: true
annotations: {}
podSecurityContext:
fsGroup: 65532
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
service:
type: ClusterIP
port: 3000
# Environment variables for vouch-server
env:
VOUCH_LISTEN_ADDR: "0.0.0.0:3000"
VOUCH_DATABASE_URL: "sqlite:/data/vouch.db?mode=rwc"
VOUCH_RP_ID: "auth.example.com"
VOUCH_BASE_URL: "https://auth.example.com"
RUST_LOG: "info,vouch_server=debug"
# Secret environment variables
# Reference an existing secret containing keys like:
# - VOUCH_JWT_SECRET
# - VOUCH_IDPS (e.g., "google")
# - VOUCH_IDP_GOOGLE_TYPE (oidc|saml)
# - VOUCH_IDP_GOOGLE_ISSUER
# - VOUCH_IDP_GOOGLE_CLIENT_ID
# - VOUCH_IDP_GOOGLE_CLIENT_SECRET
existingSecret: ""
# Or create a new secret (not recommended for production)
secrets: {}
# VOUCH_JWT_SECRET: ""
# Ingress
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: auth.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: vouch-tls
hosts:
- auth.example.com
# Resources
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# Persistence (for SQLite)
persistence:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
mountPath: /data
# Health check configuration
healthcheck:
path: /health
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
Using Kubernetes Secrets
Create secrets for sensitive values:
kubectl create secret generic vouch-secrets \
--namespace vouch \
--from-literal=VOUCH_JWT_SECRET='<your-64-character-secret>' \
--from-literal=VOUCH_IDPS='google' \
--from-literal=VOUCH_IDP_GOOGLE_TYPE='oidc' \
--from-literal=VOUCH_IDP_GOOGLE_ISSUER='https://accounts.google.com' \
--from-literal=VOUCH_IDP_GOOGLE_CLIENT_ID='...' \
--from-literal=VOUCH_IDP_GOOGLE_CLIENT_SECRET='...'
Then reference in values:
existingSecret: vouch-secrets
Air-Gapped Kubernetes
For air-gapped environments:
-
Save and transfer the chart:
helm pull oci://ghcr.io/vouch-sh/charts/vouch-server --version 0.1.0 # Transfer vouch-server-0.1.0.tgz to air-gapped environment -
Save and transfer the container image:
docker pull ghcr.io/vouch-sh/vouch:0.1.0 docker save ghcr.io/vouch-sh/vouch:0.1.0 -o vouch-0.1.0.tar # Transfer and load into your private registry -
Install from the local chart:
helm install vouch-server ./vouch-server-0.1.0.tgz \ --namespace vouch \ --create-namespace \ --set image.repository=registry.internal/vouch \ --values my-values.yaml
Upgrading
helm upgrade vouch-server oci://ghcr.io/vouch-sh/charts/vouch-server \
--version <new-version> \
--namespace vouch \
--values my-values.yaml
Health Checks
The chart configures liveness and readiness probes against the /health endpoint.
Health Checks and Monitoring
Health Endpoint
Vouch exposes a health check endpoint:
GET /health
Response:
{"status": "healthy"}
This endpoint:
- Returns HTTP 200 when the server is operational
- Is accessible over HTTP (port 80) even when TLS is configured, for load balancer health checks
- Does not require authentication
Monitoring Endpoints
| Endpoint | Method | Auth Required | Description |
|---|---|---|---|
/health | GET | No | Server health status |
/.well-known/openid-configuration | GET | No | OIDC discovery (verifies OIDC provider is functional) |
/.well-known/oauth-protected-resource | GET | No | OAuth 2.0 Protected Resource Metadata (RFC 9728) |
/v1/credentials/ssh/ca | GET | No | SSH CA public key (verifies SSH CA is loaded) |
Log Format
Vouch uses structured logging via tracing. Set the log level with the RUST_LOG environment variable:
# Production (warnings and errors only)
RUST_LOG=warn
# Standard operation
RUST_LOG=info
# Debugging
RUST_LOG=debug
# Component-specific logging
RUST_LOG=vouch_server=debug,tower_http=info
Audit Events
Authentication, credential issuance, and administrative events are written to the
audit_events table and browsable at /admin/audit. Emails are masked to
domain-only, with an HMAC column for correlation.
Authentication and key lifecycle
| Event Type | Description |
|---|---|
login_success | User authenticated — FIDO2 passkey login, or a returning user signing in on the website via the upstream IdP (the latter has no authenticator_id) |
login_failed | Failed authentication attempt |
enrollment | User enrolled their first hardware key |
logout | User logged out (including RFC 7009 token revocation) |
key_registered | Additional hardware key registered (vouch register) |
key_removed | Hardware key removed |
device_auth_approved | Browser approved a CLI device-authorization request |
key_registration_replay | Replayed key-registration link rejected (possible attack) |
Credential issuance
| Event Type | Description |
|---|---|
ssh_credential | SSH certificate issued; data includes the serial, principals, requesting agent, and expiry |
aws_credential | AWS OIDC token issued; data includes the pinned IAM role_arn (the https://aws.amazon.com/roles claim), the requesting agent, and token expiry |
github_credential | GitHub installation token issued or installation connected; data includes repositories and permissions |
token_exchange | RFC 8693 token exchange (workload identity federation); data includes the client, audience, scope, and issued token type |
OAuth clients
| Event Type | Description |
|---|---|
oauth_token_issued | Token issued at /oauth/token (data.details carries the grant type) |
oauth_token_revoked | All tokens for an application revoked |
oauth_client_registered | OAuth client registered (RFC 7591 or applications UI) |
oauth_client_updated | OAuth client configuration updated |
oauth_client_deleted | OAuth client deleted |
oauth_secret_added | Client secret added |
oauth_secret_revoked | Client secret revoked |
Administration and organization
| Event Type | Description |
|---|---|
admin_promote / admin_demote | Org-admin role granted / removed |
admin_activate / admin_deactivate | User account reactivated / deactivated |
admin_revoke_credentials | Admin revoked a member’s keys, sessions, and certificates |
admin_remove_user | Admin removed a member from the organization |
admin_policy_create / admin_policy_update / admin_policy_delete / admin_policy_toggle | Posture policy changes |
admin_create_scim_token / admin_delete_scim_token / admin_revoke_scim_token | SCIM token lifecycle |
scim_operation | SCIM provisioning operation (data carries operation and resource type) |
org_domain_added / org_domain_verified / org_domain_removed / org_domain_expired / org_domain_unverified | Additional-domain lifecycle |
org_subdomain_claimed / org_subdomain_released | Issuer subdomain lifecycle |
org_issuer_key_rotated | Per-org issuer signing keys rotated (one event per algorithm) |
org_issuer_key_revoked | Per-org previous signing keys revoked (one event per algorithm) |
org_issuer_key_emergency_rotation | Emergency rotation of per-org issuer keys (one event per algorithm) |
Retention
Configure retention periods for audit events:
# Auth events (login, enrollment, logout, key/device-auth lifecycle)
# — default 90 days
VOUCH_AUTH_EVENTS_RETENTION_DAYS=730
# OAuth usage and credential-issuance events (oauth_*, aws_credential,
# github_credential, ssh_credential, token_exchange) — default 90 days
VOUCH_OAUTH_EVENTS_RETENTION_DAYS=90
Events older than the retention period are cleaned up automatically by the background cleanup task (controlled by VOUCH_CLEANUP_INTERVAL).
Alerting Recommendations
| Condition | Alert Level | Description |
|---|---|---|
/health returns non-200 | Critical | Server is unhealthy |
| Multiple failed login attempts | Warning | Possible brute force |
| SSH CA key not loaded | Warning | SSH certificates won’t be issued |
| Database approaching capacity | Warning | SQLite file growth or PostgreSQL storage |
| Session cleanup failing | Warning | Check cleanup interval and retention settings |
Identity Provider Overview
Vouch uses one or more upstream identity providers (IdPs) to verify user identity during enrollment. This links a trusted corporate identity to a hardware-bound FIDO2 credential.
Purpose
- Verify the user is a member of your organization during enrollment
- Pull user attributes (email) from your existing identity system
- No separate user database to maintain in Vouch
Supported Protocols
Vouch supports two upstream IdP protocols, configured as a unified list:
| Protocol | Use Case |
|---|---|
| OIDC (OpenID Connect) | Recommended for most deployments. Supports auto-discovery of endpoints. |
| SAML 2.0 | For organizations that require SAML or where OIDC is not available. |
Multiple IdPs — of either protocol, in any combination — can be configured simultaneously. The login page renders one “Sign in with X” button per configured IdP, in the order operators listed them.
OIDC Discovery
When using OIDC, the server automatically discovers authorization, token, and JWKS endpoints by fetching the /.well-known/openid-configuration document from the issuer URL at startup. Any OIDC-compliant provider works — no manual endpoint configuration is needed.
Supported Providers
| Provider | Protocol | Guide |
|---|---|---|
| Google Workspace | OIDC | Google Workspace (OIDC) |
| Microsoft Entra ID | OIDC or SAML | Entra ID (OIDC), SAML 2.0 |
| Okta | OIDC or SAML | Generic OIDC, SAML 2.0 |
| Keycloak | OIDC or SAML | Generic OIDC, SAML 2.0 |
| Auth0 | OIDC | Generic OIDC |
| Any OIDC-compliant provider | OIDC | Generic OIDC |
| Any SAML 2.0-compliant provider | SAML | SAML 2.0 |
Configuration
IdPs are configured as a unified list. Each IdP has an operator-chosen slug (e.g., google, entra, corp-saml) that becomes its identifier in the state table, login page query string (?provider=<slug>), and audit logs.
Slug rules
- Match
[a-z0-9-]{1,32} - Must not start or end with a hyphen
- Must be unique across all configured IdPs
Environment variables
Set VOUCH_IDPS to a comma-separated list of slugs. For each slug, set VOUCH_IDP_<SLUG>_TYPE to oidc or saml, plus the type-specific variables.
OIDC example (Google + Entra concurrently):
VOUCH_IDPS=google,entra
VOUCH_IDP_GOOGLE_TYPE=oidc
VOUCH_IDP_GOOGLE_ISSUER=https://accounts.google.com
VOUCH_IDP_GOOGLE_CLIENT_ID=<your-google-client-id>
VOUCH_IDP_GOOGLE_CLIENT_SECRET=<your-google-client-secret>
VOUCH_IDP_ENTRA_TYPE=oidc
VOUCH_IDP_ENTRA_ISSUER=https://login.microsoftonline.com/organizations/v2.0
VOUCH_IDP_ENTRA_CLIENT_ID=<your-entra-client-id>
VOUCH_IDP_ENTRA_CLIENT_SECRET=<your-entra-client-secret>
VOUCH_ALLOWED_DOMAINS=company.com
SAML example (mixed alongside OIDC):
VOUCH_IDPS=google,corp-saml
VOUCH_IDP_GOOGLE_TYPE=oidc
VOUCH_IDP_GOOGLE_ISSUER=https://accounts.google.com
VOUCH_IDP_GOOGLE_CLIENT_ID=<your-google-client-id>
VOUCH_IDP_GOOGLE_CLIENT_SECRET=<your-google-client-secret>
VOUCH_IDP_CORP_SAML_TYPE=saml
VOUCH_IDP_CORP_SAML_METADATA_URL=https://idp.example.com/saml/metadata
VOUCH_IDP_CORP_SAML_SP_ENTITY_ID=https://auth.example.com
VOUCH_IDP_CORP_SAML_EMAIL_ATTRIBUTE=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
VOUCH_IDP_CORP_SAML_DOMAIN_ATTRIBUTE=department
Notes on slug-to-env-var conversion: hyphens in the slug become underscores in env-var names. corp-saml becomes VOUCH_IDP_CORP_SAML_TYPE, etc.
S3 configuration
In production deployments using S3-backed configuration, IdPs live under the top-level idps array. Each entry has id, type, and type-specific fields:
{
"idps": [
{
"id": "google",
"type": "oidc",
"issuer": "https://accounts.google.com",
"client_id": "<your-google-client-id>",
"client_secret": "<your-google-client-secret>"
},
{
"id": "corp-saml",
"type": "saml",
"metadata_url": "https://idp.example.com/saml/metadata",
"sp_entity_id": "https://auth.example.com",
"email_attribute": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"domain_attribute": "department"
}
]
}
Order in the idps array controls login-page button order.
Migration from legacy variables
The previous flat single-IdP environment variables (VOUCH_OIDC_ISSUER, VOUCH_OIDC_CLIENT_ID, VOUCH_OIDC_CLIENT_SECRET, VOUCH_OIDC_PROVIDERS, VOUCH_SAML_IDP_METADATA_URL, VOUCH_SAML_SP_ENTITY_ID, VOUCH_SAML_EMAIL_ATTRIBUTE, VOUCH_SAML_DOMAIN_ATTRIBUTE) and the legacy S3 oidc / saml nested blocks are no longer read by the server. They are silently ignored if still present, but they configure nothing — only the unified VOUCH_IDPS / idps[] format below is honored. Update each legacy value to its replacement and remove the old one:
| Legacy variable | Replacement |
|---|---|
VOUCH_OIDC_PROVIDERS=google,entra | VOUCH_IDPS=google,entra |
VOUCH_OIDC_<SLUG>_ISSUER | VOUCH_IDP_<SLUG>_ISSUER (with VOUCH_IDP_<SLUG>_TYPE=oidc) |
VOUCH_OIDC_<SLUG>_CLIENT_ID | VOUCH_IDP_<SLUG>_CLIENT_ID |
VOUCH_OIDC_<SLUG>_CLIENT_SECRET | VOUCH_IDP_<SLUG>_CLIENT_SECRET |
VOUCH_OIDC_ISSUER (single-provider) | VOUCH_IDPS=<slug> + VOUCH_IDP_<SLUG>_* |
VOUCH_SAML_IDP_METADATA_URL | VOUCH_IDP_<SLUG>_METADATA_URL (with VOUCH_IDP_<SLUG>_TYPE=saml) |
VOUCH_SAML_SP_ENTITY_ID | VOUCH_IDP_<SLUG>_SP_ENTITY_ID |
VOUCH_SAML_EMAIL_ATTRIBUTE | VOUCH_IDP_<SLUG>_EMAIL_ATTRIBUTE |
VOUCH_SAML_DOMAIN_ATTRIBUTE | VOUCH_IDP_<SLUG>_DOMAIN_ATTRIBUTE |
S3 {"oidc": {...}} | S3 {"idps": [{"id": "...", "type": "oidc", ...}]} |
S3 {"saml": {...}} | S3 {"idps": [{"id": "...", "type": "saml", ...}]} |
Claims and Attribute Mapping
OIDC Claims
| OIDC Claim | Vouch Attribute | Required |
|---|---|---|
email | User email / principal | Yes |
email_verified | Email verification status | Yes (must be true) |
hd | Google Workspace hosted domain | No (Google-specific) |
tid | Entra tenant ID | No (Entra-specific, cross-checked against issuer UUID to prevent cross-tenant token injection) |
SAML Attributes
| SAML Attribute | Vouch Attribute | Notes |
|---|---|---|
Configurable via VOUCH_IDP_<SLUG>_EMAIL_ATTRIBUTE | User email / principal | Falls back to NameID if not found |
Configurable via VOUCH_IDP_<SLUG>_DOMAIN_ATTRIBUTE | Domain for enrollment restriction | Extracted from email if not set |
User Lifecycle
- User exists in external IdP but not Vouch — Enrollment creates Vouch user
- User removed from external IdP — Existing Vouch sessions continue until expiry; re-enrollment blocked
Google Workspace (OIDC)
Configure Google Workspace as your external identity provider for Vouch enrollment via OpenID Connect.
Prerequisites
- Google Workspace admin access
- A verified domain in Google Workspace
Step 1: Create OAuth Client in Google Cloud Console
- Go to Google Cloud Console
- Select or create a project
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Select Web application as the application type
- Configure:
- Name:
Vouch - Authorized redirect URIs:
https://auth.example.com/oauth/callback
- Name:
- Click Create
- Copy the Client ID and Client Secret
Step 2: Configure Consent Screen
- Navigate to APIs & Services > OAuth consent screen
- Select Internal (restricts to your Google Workspace org)
- Configure:
- App name:
Vouch - User support email: your admin email
- Authorized domains: your Vouch server domain
- App name:
- Add scopes:
openid,email,profile
Step 3: Configure Vouch
Add Google to the VOUCH_IDPS list with type oidc:
VOUCH_IDPS=google
VOUCH_IDP_GOOGLE_TYPE=oidc
VOUCH_IDP_GOOGLE_ISSUER=https://accounts.google.com
VOUCH_IDP_GOOGLE_CLIENT_ID=<your-client-id>.apps.googleusercontent.com
VOUCH_IDP_GOOGLE_CLIENT_SECRET=<your-client-secret>
To run Google alongside another IdP (e.g., Microsoft Entra), append both slugs to VOUCH_IDPS — both buttons appear on the login page in list order.
The server automatically discovers Google’s authorization, token, and JWKS endpoints via OIDC Discovery. No manual endpoint configuration is needed.
Optionally restrict enrollment to specific domains:
VOUCH_ALLOWED_DOMAINS=example.com,subsidiary.com
S3 configuration
{
"idps": [
{
"id": "google",
"type": "oidc",
"issuer": "https://accounts.google.com",
"client_id": "<your-client-id>.apps.googleusercontent.com",
"client_secret": "<your-client-secret>"
}
]
}
Step 4: Test
- Run
vouch enrollon a workstation - The browser should redirect to Google sign-in
- After signing in, complete the WebAuthn registration with your YubiKey
Claims Mapping
| Google Claim | Vouch Attribute |
|---|---|
email | User email / principal |
name | Display name |
email_verified | Must be true |
Troubleshooting
“Access blocked: This app’s request is invalid”
- Verify the redirect URI exactly matches
https://<your-vouch-domain>/oauth/callback
“This app is not verified”
- Ensure the OAuth consent screen is set to Internal for Google Workspace
Users from wrong domain can enroll
- Set
VOUCH_ALLOWED_DOMAINSto restrict enrollment to specific email domains
Microsoft Entra ID (OIDC)
Configure Microsoft Entra ID (formerly Azure AD) as your upstream identity provider for Vouch enrollment.
Prerequisites
- Microsoft Entra ID tenant with admin access
- An app registration in the Azure portal
Step 1: Register an Application in Entra ID
Follow Microsoft’s app registration guide to create a new app registration:
- Sign in to the Azure portal
- Navigate to Microsoft Entra ID > App registrations > New registration
- Configure:
-
Name:
Vouch -
Supported account types: choose one of:
- Accounts in this organizational directory only (single-tenant) — restrict to your tenant.
- Accounts in any organizational directory (Any Microsoft Entra ID tenant – Multitenant) — let users from any work/school tenant enroll.
Do not select “Accounts in any organizational directory and personal Microsoft accounts” or “Personal Microsoft accounts only”. Personal Microsoft accounts (outlook.com, hotmail.com, live.com) are not supported by Vouch — see Why personal accounts aren’t supported below.
-
Redirect URI: Select Web and enter
https://auth.example.com/oauth/callback
-
- Click Register
Step 2: Create a Client Secret
- In the app registration, go to Certificates & secrets > Client secrets
- Click New client secret
- Set a description and expiry period
- Copy the Value (not the Secret ID) immediately — it is only shown once
Step 3: Configure the xms_edov optional claim
Vouch requires the xms_edov (Email Domain Owner Verified) claim to confirm the user’s email is admin-verified by their tenant. Microsoft does not emit this claim by default — you must add it as an optional claim:
- In the app registration, go to Token configuration > Add optional claim
- Select token type ID
- Check xms_edov
- Click Add
- If prompted to “Turn on the Microsoft Graph email permission”, accept
The next ID token Microsoft issues will contain "xms_edov": true when the email’s domain is verified by the user’s tenant admin.
Step 4: Configure Vouch
Add Entra to the VOUCH_IDPS list with type oidc:
Single-tenant
VOUCH_IDPS=entra
VOUCH_IDP_ENTRA_TYPE=oidc
VOUCH_IDP_ENTRA_ISSUER=https://login.microsoftonline.com/{tenant-id}/v2.0
VOUCH_IDP_ENTRA_CLIENT_ID=<application-client-id>
VOUCH_IDP_ENTRA_CLIENT_SECRET=<client-secret-value>
Replace {tenant-id} with your Entra ID tenant ID (found in Azure portal > Microsoft Entra ID > Overview).
Multi-tenant (any work/school tenant)
VOUCH_IDP_ENTRA_ISSUER=https://login.microsoftonline.com/organizations/v2.0
Vouch handles the {tenantid} template issuer that /organizations/ discovery returns, then cross-checks the per-tenant tid claim in each ID token against the tenant UUID in the token’s iss claim to prevent cross-tenant token injection.
The server automatically discovers authorization, token, and JWKS endpoints from the issuer URL via OIDC Discovery. No manual endpoint configuration is needed.
Optionally restrict enrollment to specific email domains:
VOUCH_ALLOWED_DOMAINS=example.com
S3 configuration
{
"idps": [
{
"id": "entra",
"type": "oidc",
"issuer": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"client_id": "<application-client-id>",
"client_secret": "<client-secret-value>"
}
]
}
Step 5: Test
- Run
vouch enrollon a workstation - The browser should redirect to the Microsoft sign-in page
- After signing in, complete the WebAuthn registration with your YubiKey
Why personal Microsoft accounts aren’t supported
Vouch issues hardware-backed credentials and tracks users by verified email address. To verify an Entra email, Vouch requires the xms_edov optional claim, which provides a signed assertion that the email’s domain is admin-verified.
Microsoft only emits optional claims (including xms_edov) for app registrations that target Microsoft Entra ID accounts only. Per Microsoft’s app manifest reference:
Apps that support both personal accounts and Microsoft Entra ID can’t use optional claims.
As a consequence, the https://login.microsoftonline.com/common/v2.0 issuer is rejected at startup — Vouch will refuse to load an Entra IdP configured with /common/. Use /organizations/v2.0 or a single-tenant URL instead.
Common Pitfalls
InvalidIssuer at the callback
Make sure the configured issuer ends in /v2.0. The v1 endpoints use a different token format and are not compatible with standard OIDC discovery.
Email address is not verified by the identity provider after sign-in
The xms_edov optional claim isn’t configured on the app registration. Repeat Step 3. If the claim is already configured but the user’s email still isn’t accepted, the user’s tenant admin has not verified the email domain — Microsoft only sets xms_edov: true when the domain is verified inside the user’s tenant.
Redirect URI mismatch
The redirect URI in the app registration must exactly match https://<your-vouch-domain>/oauth/callback. Azure does not support wildcard redirect URIs.
Multi-tenant without an allowlist
If you use /organizations/v2.0, any Entra tenant can attempt enrollment. Set VOUCH_ALLOWED_DOMAINS to restrict which email domains Vouch will accept.
Generic OIDC Provider
Configure any OpenID Connect-compliant identity provider as the upstream IdP for Vouch enrollment.
Prerequisites
- Your IdP must support OIDC Discovery (a
/.well-known/openid-configurationendpoint at the issuer URL) - You need a registered OAuth 2.0 client (client ID and client secret)
- The redirect URI
https://<your-vouch-domain>/oauth/callbackmust be registered with the IdP
Finding the Issuer URL
The issuer URL is the base URL that hosts the OIDC discovery document. You can verify it by fetching {issuer}/.well-known/openid-configuration and confirming it returns a valid JSON document:
curl -s https://your-idp.example.com/.well-known/openid-configuration | jq .issuer
Common issuer URL patterns:
| Provider | Issuer URL Format |
|---|---|
| Okta | https://{your-domain}.okta.com or https://{your-domain}.okta.com/oauth2/{auth-server-id} |
| Keycloak | https://{host}/realms/{realm} |
| Auth0 | https://{tenant}.auth0.com/ |
| Google Workspace | https://accounts.google.com |
| Entra ID | https://login.microsoftonline.com/{tenant-id}/v2.0 |
Configuration
Pick a slug for the IdP (e.g., okta, keycloak, auth0-corp) and add it to VOUCH_IDPS. Slug rules: [a-z0-9-]{1,32}, no leading or trailing hyphen, unique across IdPs.
VOUCH_IDPS=okta
VOUCH_IDP_OKTA_TYPE=oidc
VOUCH_IDP_OKTA_ISSUER=https://your-idp.example.com
VOUCH_IDP_OKTA_CLIENT_ID=<your-client-id>
VOUCH_IDP_OKTA_CLIENT_SECRET=<your-client-secret>
Hyphens in the slug become underscores in env-var names: a slug of auth0-corp becomes VOUCH_IDP_AUTH0_CORP_*.
At startup, the server fetches the discovery document from {issuer}/.well-known/openid-configuration and automatically discovers the authorization, token, and JWKS endpoints. No manual endpoint configuration is needed.
S3 configuration
{
"idps": [
{
"id": "okta",
"type": "oidc",
"issuer": "https://your-idp.example.com",
"client_id": "<your-client-id>",
"client_secret": "<your-client-secret>"
}
]
}
Domain Restrictions
Restrict enrollment to specific email domains:
VOUCH_ALLOWED_DOMAINS=example.com,subsidiary.com
If not set, users from any email domain can enroll (provided they authenticate with the upstream IdP).
Tested Providers
The following providers have been tested with Vouch:
| Provider | Status | Notes |
|---|---|---|
| Google Workspace | Tested | See dedicated guide |
| Microsoft Entra ID | Tested | See dedicated guide |
| Okta | Tested | Use the Org Authorization Server or a custom one |
| Keycloak | Tested | Requires a configured realm with client credentials |
| Auth0 | Tested | Use the tenant issuer URL with trailing slash |
Troubleshooting
“Failed to fetch upstream OIDC discovery document”
- Verify the issuer URL is correct and reachable from the server
- Check that the URL uses HTTPS (HTTP is only allowed for
localhost) - Confirm the discovery endpoint returns valid JSON
“Issuer mismatch”
- The
issuerfield in the discovery document must exactly match the configuredVOUCH_IDP_<SLUG>_ISSUERvalue (trailing slashes matter). Entra/organizations/v2.0is special-cased automatically — its discovery document returns a{tenantid}template that vouch handles transparently. The/common/v2.0endpoint is not supported; see Microsoft Entra ID.
Token errors after authentication
- Ensure the client secret is correct and not expired
- Verify the redirect URI registered with the IdP exactly matches
https://<your-vouch-domain>/oauth/callback
SAML 2.0
Configure one or more SAML 2.0 identity providers for Vouch enrollment. Vouch acts as a SAML Service Provider (SP) with HTTP-POST and HTTP-Redirect bindings.
SAML IdPs are configured under the same VOUCH_IDPS list as OIDC IdPs and can coexist with OIDC providers in any combination.
Environment Variables
Pick a slug for the IdP (e.g., corp-saml, partner-saml) and add it to VOUCH_IDPS. Slug rules: [a-z0-9-]{1,32}, no leading/trailing hyphen, unique across IdPs.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_IDP_<SLUG>_TYPE | Yes | (none) | Must be saml for a SAML IdP. |
VOUCH_IDP_<SLUG>_METADATA_URL | Yes | (none) | URL to the IdP’s SAML metadata XML document. Fetched at server startup. |
VOUCH_IDP_<SLUG>_SP_ENTITY_ID | No | {VOUCH_BASE_URL} | SP entity ID sent in authentication requests. Defaults to the server’s base URL. |
VOUCH_IDP_<SLUG>_EMAIL_ATTRIBUTE | No | (auto-detect) | SAML attribute name containing the user’s email address. |
VOUCH_IDP_<SLUG>_DOMAIN_ATTRIBUTE | No | (none) | SAML attribute name containing the user’s domain (for domain restriction). |
Hyphens in the slug become underscores in env-var names: a slug of corp-saml becomes VOUCH_IDP_CORP_SAML_*.
SP Metadata
The Vouch server exposes SP metadata for configuring your IdP:
GET https://auth.example.com/saml/metadata
This returns an XML document containing the SP entity ID, Assertion Consumer Service (ACS) URL, and supported bindings. The SP entity ID comes from the first configured SAML IdP (or the server’s base URL if none is set). Import the metadata into your IdP or configure the following values manually:
- SP Entity ID:
https://auth.example.com(or the value ofVOUCH_IDP_<SLUG>_SP_ENTITY_ID) - ACS URL:
https://auth.example.com/saml/acs - Bindings: HTTP-POST (ACS), HTTP-Redirect (AuthnRequest)
All configured SAML IdPs share the single ACS URL; Vouch identifies the originating IdP via the per-request RelayState stored in the state table.
Attribute Mapping
Vouch extracts user identity from SAML assertion attributes. By default, it looks for the email address in common attribute names. You can override this per-IdP with VOUCH_IDP_<SLUG>_EMAIL_ATTRIBUTE:
| Use Case | Attribute Example |
|---|---|
| Standard email | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress |
| NameID | (used automatically if no attribute match) |
| Custom | Set VOUCH_IDP_<SLUG>_EMAIL_ATTRIBUTE to your IdP’s attribute name |
For domain-based enrollment restrictions, set VOUCH_IDP_<SLUG>_DOMAIN_ATTRIBUTE to the attribute name containing the user’s domain. If not set, the domain is extracted from the email address.
Configuration Example
VOUCH_IDPS=corp-saml
VOUCH_IDP_CORP_SAML_TYPE=saml
VOUCH_IDP_CORP_SAML_METADATA_URL=https://login.microsoftonline.com/{tenant-id}/federationmetadata/2007-06/federationmetadata.xml
VOUCH_IDP_CORP_SAML_SP_ENTITY_ID=https://auth.example.com
VOUCH_IDP_CORP_SAML_EMAIL_ATTRIBUTE=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
VOUCH_ALLOWED_DOMAINS=example.com
S3 configuration
{
"idps": [
{
"id": "corp-saml",
"type": "saml",
"metadata_url": "https://login.microsoftonline.com/{tenant-id}/federationmetadata/2007-06/federationmetadata.xml",
"sp_entity_id": "https://auth.example.com",
"email_attribute": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
}
]
}
Provider-Specific Notes
Microsoft Entra ID
- Metadata URL:
https://login.microsoftonline.com/{tenant-id}/federationmetadata/2007-06/federationmetadata.xml - Email attribute:
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress - Create an Enterprise Application with SAML SSO and set the ACS URL to
https://auth.example.com/saml/acs
Okta
- Metadata URL: found in the Okta SAML app’s Sign On tab under Metadata URL
- Email attribute: typically
emailor configure in the Okta attribute statements - Set the Single Sign-On URL to
https://auth.example.com/saml/acsand Audience URI to your SP entity ID
Google Workspace
- Metadata URL: available from the Google Admin console under Apps > Web and mobile apps > SAML app > Metadata
- Email attribute:
email(configure in attribute mapping) - Add a custom SAML app in the Admin console with ACS URL
https://auth.example.com/saml/acs
Troubleshooting
“Failed to fetch SAML IdP metadata”
- Verify the metadata URL is correct and reachable from the server
- Check that the URL returns valid XML (not an HTML login page)
- Ensure the server can make outbound HTTPS requests
Signature verification errors
- Confirm the IdP’s signing certificate in the metadata is current and not expired
- Ensure the server clock is synchronized (NTP). SAML assertions have time-based validity windows.
Email not extracted from assertion
- Check the SAML assertion attributes using debug logging (
RUST_LOG=vouch_server=debug) - Set
VOUCH_IDP_<SLUG>_EMAIL_ATTRIBUTEto the exact attribute name used by your IdP
“Legacy identity-provider configuration detected”
- The flat
VOUCH_SAML_*variables are no longer accepted. Migrate to the per-IdPVOUCH_IDP_<SLUG>_*format. See Overview for the full mapping.
Session Management
Vouch sessions are time-limited, DPoP-bound OAuth 2.0 access tokens (ES256 JWTs per RFC 9068) that prove recent hardware presence verification.
Session Lifecycle
- Creation —
vouch loginperforms FIDO2 assertion with YubiKey touch + PIN - Active — Access token stored in agent memory, valid for 8 hours (default)
- Usage — Credential helpers exchange the access token for service-specific credentials
- Expiry — Session expires automatically after the configured duration
- Revocation —
vouch logoutexplicitly ends the session
Session Duration
Default: 8 hours. Configurable via:
VOUCH_SESSION_HOURS=8
Session Storage
Sessions are stored in multiple locations for different access patterns:
| Location | Purpose | Security |
|---|---|---|
vouch-agent memory | Primary access for CLI and credential helpers | In-process, zeroized on drop |
~/.config/vouch/config.json | Fallback when agent is not running | File permissions 0600 |
~/.local/state/vouch/cookie.txt | Netscape cookie file for curl -b usage | File permissions 0600 |
| Server database | Server-side session record | Token hash stored, not plaintext |
Vouch follows the XDG Base Directory Specification
and honors XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_DATA_HOME, XDG_CACHE_HOME,
and XDG_RUNTIME_DIR on all platforms (including macOS). The paths above show the
defaults when those variables are unset. See
File Locations for the full map and the
automatic migration from the legacy ~/.vouch/ directory.
Server-Side Session Management
Cleanup
Expired sessions are cleaned up automatically by a background task:
# Cleanup interval in minutes (default: 15, set to 0 to disable)
VOUCH_CLEANUP_INTERVAL=15
Security Properties
- Presence-bound — Every session traces to a FIDO2 assertion with user verification
- Time-limited — Sessions cannot be renewed; a new login is required after expiry
- DPoP-bound — Access tokens are bound to the client’s DPoP key; token theft without the key is useless
- Non-transferable — Sessions are bound to the client that created them
- Audience-restricted — Tokens narrowed to a specific resource are rejected at every other resource (see below)
- Audited — Every session creation and usage is logged
Audience Enforcement (RFC 8707 Resource Indicators)
Access tokens carry an aud (audience) claim per RFC 9068. By default the
audience equals the requesting client_id and the token is valid at every
Vouch resource endpoint — this covers all standard flows (vouch login,
browser sessions, device flow, client credentials).
A client may instead narrow a token to a specific resource, either with the
RFC 8707 resource parameter at the authorization endpoint or with the
audience/resource parameters at token exchange (RFC 8693). Vouch’s
resource endpoints (/v1/credentials/*, /v1/keys, /api/v1/*, RFC 7592
client management) enforce that narrowing: a narrowed token is accepted only
when its audience names this deployment (same scheme, host, and port as the
configured base URL) and its path covers the request at a path-segment
boundary. An audience of the deployment root (the base URL itself) covers
every endpoint; {base_url}/v1/keys covers /v1/keys and everything below
it, but nothing else. Requests failing the check receive 401 invalid_token
with the standard WWW-Authenticate challenge, and the rejection is logged
with the client ID, audience, and request path.
Per their RFCs, the authorization-server endpoints remain audience-agnostic:
/oauth/userinfo accepts tokens from any client, /oauth/introspect and
/oauth/revoke answer about any token the server issued, and token exchange
accepts narrowed subject tokens (re-scoping them is its purpose).
Clients registered without resource_uris may request any resource value
at issuance. This is safe under enforcement: a token narrowed to an external
resource server is less usable at Vouch, not more — it can only be spent at
the external service it names. Registering resource_uris additionally
restricts which values a client may request at all.
Key Management
Vouch uses several cryptographic keys. This page covers their lifecycle and rotation.
Key Inventory
| Key | Algorithm | Purpose | Storage |
|---|---|---|---|
| SSH CA Key | Ed25519 | Signs SSH user certificates | File, env var, S3 config, or KMS |
| OIDC Signing Key | P-256 EC (ES256) | Signs access tokens and ID tokens (default) | Env var, S3 config, or KMS |
| OIDC RSA Signing Key | RSA-3072 (RS256) | Signs ID tokens (per-client, OIDC Core conformance) | Env var, S3 config, or KMS |
| JWT Secret | HMAC-SHA256 | Signs internal state tokens (authorization codes, WebAuthn state, CSRF) | Env var, S3 config, or KMS |
| Document Encryption Key | P-384 EC (HPKE) | Encrypts sensitive documents stored alongside S3 config | S3 config (KMS-protected) |
| TLS Certificate | EC/RSA | HTTPS transport | Env var or S3 config |
| Client Key (per-CLI) | P-256 EC (ES256) | FAPI 2.0 client auth, DPoP proofs | OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager), file fallback |
All of these keys use classical (pre-quantum) algorithms. For why that is currently the right choice for each of them, and what Vouch already does about quantum resistance, see Post-Quantum Cryptography.
SSH CA Key
The SSH CA key signs all SSH user certificates. Every host that trusts Vouch certificates must have the corresponding public key in TrustedUserCAKeys.
Generation
ssh-keygen -t ed25519 -f ssh_ca_key -N "" -C "vouch-ca@example.com"
Configuration
# Option 1: File path
VOUCH_SSH_CA_KEY_PATH=./ssh_ca_key
# Option 2: Inline (base64-encoded PEM, takes precedence over file)
VOUCH_SSH_CA_KEY="$(base64 -i ssh_ca_key | tr -d '\n')"
# Option 3: AWS KMS (overrides Options 1 and 2)
VOUCH_SSH_CA_KMS_KEY_ID=mrk-1234abcd5678efgh
# Option 4: Disable SSH CA
VOUCH_SSH_CA_KEY_PATH=""
When using KMS, the server calls kms:Sign with Ed25519. The KMS key must be an asymmetric signing key with ECC_EDWARDS_CURVE_25519 key spec. Multi-region keys (mrk- prefix) are recommended for high availability.
Rotation
SSH CA key rotation requires coordinated updates:
- Generate a new CA key
- Distribute the new public key to all hosts (add to
TrustedUserCAKeys) - Update the Vouch server configuration with the new private key
- Restart the server
- After all existing certificates expire (max 8 hours), remove the old public key from hosts
Important: During rotation, hosts should trust both old and new CA public keys to avoid disruption.
Public Key Distribution
Retrieve the CA public key:
curl https://auth.example.com/v1/credentials/ssh/ca
# ssh-ed25519 AAAA... vouch-ca@example.com
OIDC Signing Key (ES256)
Used to sign access tokens (RFC 9068) and ID tokens (default algorithm) with ES256.
Configuration
# Option 1: Local key (base64-encoded PEM)
VOUCH_OIDC_SIGNING_KEY="$(base64 -i oidc_signing_key.pem | tr -d '\n')"
# Option 2: AWS KMS (overrides Option 1)
VOUCH_OIDC_SIGNING_KMS_KEY_ID=mrk-abcd1234efgh5678
If neither is set, an ephemeral key is generated on startup. This means tokens cannot be verified after a server restart unless the same key is provided.
When using KMS, the server calls kms:Sign with P-256 ECDSA (ECC_NIST_P256 key spec). Multi-region keys (mrk- prefix) are recommended.
Generation
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out oidc_signing_key.pem
Note: You must use
openssl genpkey(which produces PKCS#8 format) rather thanopenssl ecparam -genkey(which produces SEC1 format). The server requires PKCS#8 (-----BEGIN PRIVATE KEY-----).
Rotation
When rotating the OIDC signing key:
- Generate a new key
- Update the server configuration
- Restart the server
- The JWKS endpoint (
/oauth/jwks) automatically serves the new public key - Relying parties that cache JWKS will pick up the new key on their next refresh
OIDC RSA Signing Key (RS256)
Used to sign ID tokens with RS256 algorithm per OIDC Core Section 3.1.3.7 and all AWS credential tokens. RS256 is the default id_token_signed_response_alg in the OIDC specification and must be supported for conformance. Clients can select RS256 via OAuth 2.0 Dynamic Client Registration (id_token_signed_response_alg field). The AWS token endpoint (/v1/credentials/aws/token) issues one RS256-signed token that serves both STS AssumeRoleWithWebIdentity and, as the sso-oidc:CreateTokenWithIAM assertion, the IAM Identity Center trusted-token-issuer contract (which rejects ES256).
Access tokens are always signed with ES256 (the OIDC Signing Key above).
Generation
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out oidc_rsa_key.pem
A minimum key size of 3072 bits is enforced. Keys smaller than 3072 bits are rejected at startup.
Configuration
# Option 1: Local key (base64-encoded PEM)
VOUCH_OIDC_RSA_SIGNING_KEY="$(base64 -i oidc_rsa_key.pem | tr -d '\n')"
# Option 2: AWS KMS (overrides Option 1)
VOUCH_OIDC_RSA_SIGNING_KMS_KEY_ID=mrk-rsa1234abcd5678
If neither is set, an ephemeral RSA-3072 key is generated on startup. This means RS256 ID tokens and AWS credential tokens cannot be verified after a server restart, and verification fails across multiple instances (each generates its own key). Any deployment using the AWS integration needs a durable key. A warning is logged when an ephemeral key is generated.
When using KMS, the key must be:
- Key spec:
RSA_3072 - Key usage:
SIGN_VERIFY - Signing algorithm:
RSASSA_PKCS1_V1_5_SHA_256
Multi-region keys (mrk- prefix) are recommended.
Rotation
When rotating the OIDC RSA signing key:
- Generate a new RSA-3072 key
- Update the server configuration
- Restart the server
- The JWKS endpoint (
/oauth/jwks) automatically serves the new public key - Relying parties that cache JWKS will pick up the new key on their next refresh
JWT Secret
Used for signing internal state tokens (authorization codes, WebAuthn challenge state, CSRF tokens) with HS256. Access tokens are signed with the OIDC signing key (ES256) per RFC 9068.
Configuration
# Option 1: Local secret (must be at least 32 characters)
VOUCH_JWT_SECRET="$(openssl rand -base64 48)"
# Option 2: AWS KMS HMAC (eliminates the need for VOUCH_JWT_SECRET)
VOUCH_JWT_HMAC_KMS_KEY_ID=mrk-5678abcd1234efgh
When using KMS, the server uses kms:GenerateMac and kms:VerifyMac with HMAC-SHA256. The KMS key must be a HMAC_256 key type. Multi-region keys (mrk- prefix) are recommended.
Generation (local secret)
openssl rand -base64 48
Rotation
Changing the JWT secret (or KMS key) invalidates all existing sessions. Users must re-authenticate.
- Generate a new secret or KMS key
- Update
VOUCH_JWT_SECRETorVOUCH_JWT_HMAC_KMS_KEY_ID - Restart the server
- All users must run
vouch loginagain
Document Encryption Key
Used for HPKE (Hybrid Public Key Encryption) of sensitive documents stored alongside the S3 configuration. The private key is encrypted by a KMS key and stored in the S3 config as document_key.
Provisioning
vouch-server generate-document-key --kms-key-id mrk-<your-kms-key-id>
This generates a P-384 EC key pair, encrypts the private key with the specified KMS key, and outputs the document_key JSON block to add to your S3 config. --algorithm p384 is the default and currently the only supported algorithm; the flag exists so post-quantum algorithms can be added later without changing the command or config shape.
Configuration
The document_key field in S3 config contains:
{
"document_key": {
"kms_key_id": "mrk-<your-kms-key-id>",
"encrypted_private_key": "<base64-encoded KMS ciphertext>",
"algorithm": "p384"
}
}
algorithm is optional and defaults to p384, so configs provisioned before the field existed keep working unchanged.
At startup, the server decrypts the private key via kms:Decrypt and holds the key material in memory for the lifetime of the process.
Cipher-suite tagging and the post-quantum path
Every document row records the HPKE cipher suite it was sealed with: the stored
encapsulated key is prefixed hpke:<kem_id>:<kdf_id>:<aead_id>: using the RFC 9180
codepoints (hpke:0011:0002:0002: for the current DHKEM(P-384) + HKDF-SHA384 +
AES-256-GCM suite). Rows written before tagging existed are plain base64 and are read
as that same P-384 suite. Rows sealed under different suites can therefore coexist in
one database, which is what makes a future key-encapsulation migration — e.g. to the
ML-KEM hybrid suites from draft-ietf-hpke-pq — an operational rotation rather than a
breaking format change.
There is no document-key rotation mechanism today. When post-quantum suites become available in the underlying libraries (rustls / aws-lc-rs), the expected migration is:
- Provision a new
document_keywith the new algorithm (one newgenerate-document-key --algorithmvalue). - Run a dual-key read period: the server decrypts old rows with the old private key (selected by each row’s suite tag) while sealing new writes under the new suite.
- Re-encrypt existing rows opportunistically on write, plus an offline sweep for the remainder; then retire the old key.
Steps 2–3 are not implemented yet — only the storage format and configuration groundwork exist. Do not remove the old key from KMS until every row carries the new suite tag.
Per-Org Issuer Signing Keys (ES256 + RS256)
When an organization claims a custom subdomain on an encrypted deployment, Vouch generates a
dedicated ES256 and RS256 signing key pair for that org’s OIDC issuer. AWS federation tokens,
Identity Center tokens, and all RFC 8693 token-exchange assertions for that org are signed with
these keys and served at the org’s own JWKS endpoint
(https://<org-subdomain>.auth.example.com/oauth/jwks).
This makes each subdomain a real cryptographic tenant boundary: a token issued for org A cannot be verified against org B’s JWKS.
Per-org keys are only created when all three conditions are met:
- The deployment has document encryption enabled (a KMS-backed document key in the S3 config).
- The organization has a claimed subdomain.
- A credential-issuance request arrives for that org (lazy first-use creation).
Key Lifecycle
Each algorithm’s key set always contains two keys, and sometimes three:
| State | Role | Published in JWKS | Signs tokens |
|---|---|---|---|
| Current | The signer | yes | yes |
| Next | Pre-staged successor | yes | no |
| Previous | Demoted signer awaiting revocation | yes | no |
The Next key is created together with the first key and re-staged automatically whenever a rotation consumes it, so relying-party JWKS caches always hold the key that will sign next — long before it ever signs. Nothing in the lifecycle runs on a timer: both rotation steps are explicit operator actions on the Admin → Subdomain page.
Rotating Keys
Step 1 — Rotate. The “Rotate Signing Keys” button switches signing to the pre-staged Next keys for both algorithms in one transaction. The old signers become Previous keys: still published, still verifying outstanding tokens, no longer signing. Fresh Next keys are staged in the same transaction.
The rotate is rejected in two situations:
- The Next keys are younger than 24 hours. Relying parties (AWS IAM in particular) cache the org JWKS on their own schedule; signing with a key their cache has not seen fails federation until they refetch. Because the Next key is normally staged months earlier (at first use or by the previous rotation), this gate only bites on back-to-back rotations.
- Previous keys from an earlier rotation are still published. Revoke them first — the key set keeps at most one retired generation per algorithm.
Step 2 — Revoke. The “Revoke Old Keys” button deletes the Previous keys and removes them
from the JWKS. It is rejected until max(session lifetime, 8 hours) + 2 hours have passed since
the rotate, because until then tokens signed by the old keys may still be live — deleting the
keys would log those sessions out. After the window, revocation affects nobody.
A Previous key that is never revoked stays visible on the admin page indefinitely (it can verify, but never sign). Nothing deletes it automatically; revoking promptly after the drain window keeps the published key set minimal.
Operator note: Reducing
VOUCH_SESSION_HOURSbetween a rotate and its revoke can shorten the revoke gate below what tokens issued under the old lifetime need. Revoke first, then shorten session lifetimes.
Emergency Rotation
The “Emergency Rotate” button replaces the entire key set — fresh Current and Next keys for both algorithms, Previous keys deleted — in one atomic operation. Use it only when key compromise is suspected: on an encrypted deployment all private keys are sealed by the same document key, so a compromise of one is treated as a compromise of all.
Consequences:
- Every key that existed before the emergency is removed from the JWKS immediately.
- Outstanding tokens signed by the old keys will fail verification until relying parties
refetch the JWKS. Cross-instance propagation takes up to 60 seconds (signing cache TTL);
downstream relying parties that respect the
Cache-Control: public, max-age=3600response header may take up to 1 hour to pick up the new keys. - AWS STS
AssumeRoleWithWebIdentityand IAM Identity CenterCreateTokenWithIAMcalls that carry a token signed by an old key will fail until the user re-authenticates withvouch login. - The fresh Next keys start a new 24-hour publish window, so a graceful rotate is unavailable for a day afterwards.
Runbook:
- Navigate to Admin → Subdomain for the affected org.
- Click Emergency Rotate and confirm.
- Instruct affected users to run
vouch loginto obtain a new token signed by the replacement key. - If the org is federated with AWS IAM, existing STS sessions will expire naturally (up to session lifetime) or can be revoked via the IAM console.
JWKS Caching and the 24-Hour Publish Window
The 24-hour minimum age before a Next key may sign is a deliberate product decision. AWS IAM and
IAM Identity Center cache JWKS responses for an undocumented internal period that is believed to
exceed the advertised 1-hour Cache-Control max-age. Keeping the successor published for at
least 24 hours before it signs ensures relying parties have ample time to cache the new kid.
Changing this window requires verifying the behaviour of all federated relying parties.
Releasing a subdomain deletes the Next and Previous keys (the publish window is meaningless while the issuer host is unclaimed) but keeps the Current key, so a same-org reclaim resumes with the same signer. The first use after a reclaim stages a fresh Next key, which restarts its 24-hour window.
TLS Certificate
See TLS Configuration for details on TLS certificate management and hot-reload.
Post-Quantum Cryptography
This page describes Vouch’s post-quantum cryptography (PQC) posture: what is quantum-resistant today, what is still classical and why, and what to watch. It exists so that operators fielding “are we quantum-safe?” questions have a precise answer.
Threat Model Summary
A cryptographically relevant quantum computer breaks the elliptic-curve and RSA algorithms Vouch uses (P-256, P-384, Ed25519, RSA-3072). The two exposure classes have very different urgency:
- Key exchange / encryption — urgent. An adversary can record encrypted traffic or steal encrypted data today and decrypt it once a quantum computer exists (“harvest now, decrypt later”). This is why the industry moved TLS and SSH key exchange to hybrid post-quantum schemes first (NIST IR 8547; OpenSSH 10 defaults; the majority of browser traffic).
- Signatures — not yet exposed. A signature is verified at use time. A future quantum computer cannot retroactively forge a login, token, or certificate that was verified years earlier. Signatures only become a problem once quantum computers are close to real, and the surrounding ecosystems (JOSE, OpenSSH, FIDO2, WebPKI) have not yet standardized replacements.
What Is Quantum-Resistant Today
TLS key exchange, in both directions. All Vouch binaries enable the rustls prefer-post-quantum feature, which makes X25519MLKEM768 (hybrid X25519 + ML-KEM-768, the FIPS 203 lattice KEM) the preferred TLS key-exchange group:
- Inbound: the server’s HTTPS and mTLS listeners.
- Outbound: CLI → server, agent → server, and server → upstream IdP / AWS / GitHub connections.
Hybrid means the session keys are secure if either X25519 or ML-KEM-768 holds, so this is a strict upgrade. Peers that do not support ML-KEM negotiate classical groups transparently. No configuration is involved; an integration test (pq_tls.rs in vouch-tests) pins the negotiated group so a silent downgrade fails CI.
What Is Still Classical, and Why
Every signature in the system is classical, because none of the protocols involved have a standardized, deployable post-quantum alternative yet. None of these are “harvest now, decrypt later” exposed.
| Surface | Algorithm | Blocked on |
|---|---|---|
| OIDC access/ID tokens, token exchange | ES256 / RS256 | ML-DSA in JOSE is still an IETF draft (draft-ietf-cose-dilithium); no production-grade Rust JWT support |
| DPoP proofs, private_key_jwt, JAR/JARM | ES256 / PS256 / EdDSA | Same JOSE draft; FAPI 2.0 profile algorithms |
HTTP message signatures (RFC 9421, /v1/*) | ecdsa-p256-sha256 | No PQC algorithms registered for RFC 9421 |
| SSH user certificates (SSH CA) | Ed25519 | OpenSSH has no post-quantum signature or host-key algorithms yet — its PQC support is key exchange only |
| WebAuthn / FIDO2 assertions | ES256 / RS256 / EdDSA | No PQC in released CTAP/WebAuthn; YubiKey ML-DSA support is prototype-only and requires new hardware |
| SAML assertions | RSA/ECDSA-SHA256 | XML-DSig PQC unstandardized |
| Internal state tokens | HMAC-SHA256 | Symmetric; already quantum-resistant at this key size (no action needed) |
One encryption surface is classical and is the known harvest-now-decrypt-later item:
| Surface | Algorithm | Status |
|---|---|---|
| Document encryption at rest (S3 config documents) | HPKE with DHKEM(P-384) + AES-256-GCM | Roadmap item: move to a hybrid ML-KEM HPKE suite (X-Wing) once it is standardized and supported in rustls/aws-lc-rs. The storage format (suite-tagged encapsulated keys) already supports multiple algorithms coexisting; migration is an operational rotation. Exposure requires an attacker to first obtain the encrypted documents; the KMS-sealed private key and S3 access controls remain the primary defense. |
Watch List
Ecosystem developments that unblock the classical surfaces above, roughly in the order they are expected to land:
- HPKE hybrid KEMs (X-Wing / ML-KEM in HPKE) — unblocks document encryption at rest. First candidate for adoption since it is entirely under Vouch’s control (no protocol peers involved).
- ML-DSA in JOSE/COSE (
draft-ietf-cose-dilithium→ RFC, plus Rust library support) — unblocks OIDC tokens, DPoP, and client assertions. AWS KMS already offers ML-DSA signing keys, so the KMS-backed key path is the likely first implementation. - OpenSSH post-quantum signature algorithms — unblocks the SSH CA. Nothing to do until OpenSSH itself ships them.
- FIDO2/CTAP PQC + authenticator hardware — unblocks WebAuthn. Requires new YubiKey hardware generations; expect years, and expect hybrid classical+PQC attestation first.
- ML-DSA in WebPKI certificates — affects TLS server certificates, which Vouch consumes rather than issues; adopt whatever the CA ecosystem (e.g. Let’s Encrypt) ships.
When adopting any of these, prefer hybrid (classical + PQC) constructions over pure PQC, matching current NIST/BSI guidance.
Operator FAQ
Do I need to configure anything to get post-quantum TLS? No. It is compiled in and negotiated automatically when the peer supports it.
How do I verify a connection used post-quantum key exchange? From a client with OpenSSL 3.5+: openssl s_client -connect auth.example.com:443 -groups X25519MLKEM768 and check the negotiated group in the handshake output. Browsers show the key-exchange group in their developer-tools security panel.
Are my SSH certificates quantum-safe? The certificate signature is Ed25519 (classical), but certificates live at most 8 hours and cannot be retroactively forged, so there is no stored-data exposure. The SSH transport is between your SSH client and your hosts — OpenSSH 10 uses post-quantum key exchange by default.
Does the 8-hour credential lifetime matter here? Yes — short-lived credentials are themselves a quantum mitigation. Even aggressive estimates for cryptographically relevant quantum computers are measured in years, not hours; nothing Vouch signs outlives the transition window it would need to survive to be at risk.
SCIM Provisioning
Vouch supports SCIM 2.0 (RFC 7643/7644) for user provisioning and de-provisioning from external identity providers. SCIM is a launch requirement for enterprise deployments.
Setup
The admin API endpoints (/api/v1/org/*) require an authenticated Vouch session from a user with org admin privileges. The server accepts the access token via Authorization: Bearer <token>, Authorization: DPoP <token>, or the vouch_session cookie.
Prerequisites:
- Your user account must have the
is_org_adminflag set in the database - You must be assigned to an organization (
org_id)
1. Create a SCIM token
Log in with vouch login, then create a SCIM token using the cookie file or token command:
# Using cookie file (written automatically on login)
curl -X POST https://auth.example.com/api/v1/org/scim-tokens \
-b ~/.local/state/vouch/cookie.txt \
-H "Content-Type: application/json" \
-d '{"description": "SCIM integration", "expires_in_days": 90}'
# Or using the token command
curl -X POST https://auth.example.com/api/v1/org/scim-tokens \
-H "Authorization: Bearer $(vouch credential token)" \
-H "Content-Type: application/json" \
-d '{"description": "SCIM integration", "expires_in_days": 90}'
The response includes a token prefixed vouch_scim_.... This token is shown once and cannot be retrieved again.
2. Configure your IdP
Enter the following in your IdP’s SCIM configuration:
- SCIM endpoint URL:
https://auth.example.com/scim/v2/ - Bearer token: the
vouch_scim_...token from step 1
3. Manage tokens
# List active SCIM tokens
curl -b ~/.local/state/vouch/cookie.txt \
https://auth.example.com/api/v1/org/scim-tokens
# Revoke a SCIM token
curl -X DELETE -b ~/.local/state/vouch/cookie.txt \
https://auth.example.com/api/v1/org/scim-tokens/<token-id>
De-Provisioning Behavior
When a user is de-provisioned via SCIM (e.g., employee leaves the organization):
| Action | Timing | Effect |
|---|---|---|
| Active sessions invalidated | Immediate | All current sessions for the user are terminated |
| SSH certificates revoked | Immediate | All issued SSH certificates are marked as revoked |
| Enrolled authenticators deleted | Immediate | All registered credentials are removed (cascade) |
| User record deleted | Immediate | User cannot re-enroll or authenticate |
| Audit event logged | Immediate | De-provisioning recorded with SCIM token info |
Key principle: De-provisioning is immediate and complete. When someone leaves via SCIM, they lose all Vouch access instantly — no waiting for session expiration.
#![allow(unused)]
fn main() {
// SCIM de-provision handling (DELETE /scim/v2/Users/:id)
async fn delete_user(user_id: &str) -> Result<()> {
// 1. Invalidate all active sessions immediately
db::delete_sessions_for_user(&db, user_id).await?;
// 2. Revoke all SSH certificates for this user
db::revoke_all_ssh_certificates_for_user(&db, user_id, Some("User deleted via SCIM"), Some("scim")).await?;
// 3. Delete user (cascades to authenticators)
db::delete_user(&db, user_id).await?;
// 4. Log audit event
db::insert_scim_audit(&db, "delete", "User", user_id, Some(&token_id), Some(&details)).await?;
Ok(())
}
}
SCIM Endpoint Authentication
SCIM endpoints require bearer token authentication:
Endpoint: POST /scim/v2/Users, DELETE /scim/v2/Users/:id, etc.
Authentication:
- Bearer token in
Authorizationheader - Token generated via the admin API (
POST /api/v1/org/scim-tokens) - Tokens are long-lived but can be rotated/revoked
- Separate token per IdP integration
# Example SCIM request
curl -X DELETE https://vouch.example.com/scim/v2/Users/usr_abc123 \
-H "Authorization: Bearer scim_token_xyz789" \
-H "Content-Type: application/scim+json"
Token Security:
- Tokens are hashed (SHA-256) before storage
- Shown once at creation, never retrievable after
- Bound to specific organization
- Minimum 256 bits of entropy
SCIM Audit Logging
All SCIM operations are logged for compliance and security monitoring:
| Operation | Resource Type | Logged Data |
|---|---|---|
create | User | resource_id, email, scim_token_id, timestamp |
update | User | resource_id, scim_token_id, timestamp |
delete | User | resource_id, email, scim_token_id, timestamp |
create | Group | resource_id, display_name, scim_token_id, timestamp |
update | Group | resource_id, scim_token_id, timestamp |
delete | Group | resource_id, scim_token_id, timestamp |
SCIM vs Manual Enrollment
| Aspect | SCIM Provisioning | Manual Enrollment |
|---|---|---|
| User record creation | IdP pushes user info | User initiates enrollment |
| Hardware enrollment | Still requires physical hardware key | Requires physical hardware key |
| De-provisioning | Immediate via IdP (user deleted, sessions invalidated, certs revoked) | Manual admin action (sessions invalidated, certs revoked) |
| Group membership | Synced from IdP | Not available outside SCIM |
Note: SCIM pre-provisioning creates a user record, but they still cannot authenticate until they physically enroll a hardware FIDO2 authenticator. The security model remains: no credential without hardware.
S3 Configuration Storage
When using S3-based configuration, additional security considerations apply. This chapter covers bucket requirements, protected fields, polling behavior, and key encoding.
S3 Bucket Requirements
| Requirement | Rationale |
|---|---|
| Server-Side Encryption | Config contains secrets (JWT secret, OIDC credentials, private keys) |
| Block Public Access | Config should never be publicly accessible |
| IAM Least Privilege | Server needs only s3:GetObject and s3:HeadObject |
| Versioning | Enables rollback and audit trail |
| Access Logging | Detect unauthorized access attempts |
Recommended S3 Bucket Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::ACCOUNT:role/vouch-server"},
"Action": ["s3:GetObject", "s3:HeadObject"],
"Resource": "arn:aws:s3:::my-bucket/config/vouch-server.json"
}
]
}
Protected Configuration Fields
Only TLS certificates can be hot-reloaded at runtime via S3 polling. All other configuration fields are silently ignored during runtime S3 polling and require a server restart to take effect.
S3 Polling Security
- ETag-based polling — HEAD request checks for changes before full GET
- Fail-open on polling errors — If S3 becomes unreachable during operation, server continues with current config
- Fail-fast on startup — If S3 is unreachable at startup when configured, server fails to start
- No caching of stale config — Config is only updated when successfully fetched and parsed
Base64 Encoding for Keys
All private keys and certificates in S3 config must be base64-encoded:
- Standard base64 or URL-safe base64 accepted
- Prevents JSON parsing issues with PEM format
- Decoded and validated before use
Backup and Recovery
What to Back Up
| Component | Criticality | Recovery Impact |
|---|---|---|
| Database | Critical | Loss of user registrations, sessions, authenticator records |
| SSH CA private key | Critical | Must re-distribute new CA public key to all hosts |
| OIDC signing key (ES256) | High | Token verification fails until new key distributed |
| OIDC RSA signing key (RS256) | High | RS256 ID token verification fails until new key distributed |
| JWT secret | High | All sessions invalidated on change |
| TLS certificate & key | Medium | Service unavailable until replaced |
| Server configuration | Medium | Can be reconstructed from documentation |
Backup Strategy
Database
SQLite:
# Simple file copy (stop writes first or use backup API)
cp /data/vouch.db /backup/vouch.db.$(date +%Y%m%d_%H%M%S)
# Or use SQLite backup command (safe during writes)
sqlite3 /data/vouch.db ".backup '/backup/vouch.db.backup'"
PostgreSQL:
pg_dump -Fc vouch > /backup/vouch.$(date +%Y%m%d_%H%M%S).dump
Frequency: Daily minimum. More frequent for high-activity deployments.
Cryptographic Keys
Back up all keys to a secure, offline location:
# SSH CA key
cp ssh_ca_key /secure-backup/ssh_ca_key
# OIDC signing key (ES256)
cp oidc_signing_key.pem /secure-backup/oidc_signing_key.pem
# OIDC RSA signing key (RS256) — if configured
cp oidc_rsa_key.pem /secure-backup/oidc_rsa_key.pem
Store key backups:
- Encrypted at rest
- In a separate location from the server
- With restricted access (minimum two-person rule for production)
Recovery Procedures
Full Server Recovery
- Deploy new server with the same configuration
- Restore database from backup
- Restore cryptographic keys (SSH CA, OIDC signing, JWT secret)
- Start the server — migrations run automatically if needed
- Verify:
curl https://auth.example.com/health
Lost SSH CA Key
If the SSH CA key is lost and no backup exists:
- Generate a new SSH CA key
- Distribute the new public key to all SSH hosts
- Configure Vouch with the new key
- All users must run
vouch loginto get new certificates
Lost JWT Secret
If the JWT secret changes (lost or compromised):
- Set the new
VOUCH_JWT_SECRET - Restart the server
- All existing sessions are invalidated
- Users must run
vouch loginagain
Database Corruption
- Stop the server
- Restore from backup
- Users who enrolled after the backup will need to re-enroll
- Start the server
Disaster Recovery Testing
Periodically test your recovery procedures:
- Restore a database backup to a test environment
- Start a test server with production keys
- Verify enrollment, login, and credential flows
- Document any issues and update procedures
Software Updates
Update Procedure
Pre-Update
- Read the release notes for breaking changes
- Back up the database before upgrading
- Test in staging if possible
Server Update
# Back up database
cp /data/vouch.db /data/vouch.db.pre-upgrade
# Update via package manager
sudo apt upgrade vouch-server # Debian/Ubuntu
sudo dnf upgrade vouch-server # RHEL/Fedora
# Or via Docker
docker pull ghcr.io/vouch-sh/vouch:latest
docker compose up -d
# Or via Helm
helm upgrade vouch-server oci://ghcr.io/vouch-sh/charts/vouch-server \
--version <new-version> --namespace vouch
# Verify
curl -k https://auth.example.com/health
Database migrations run automatically on startup. No manual migration steps are needed.
Client Update
# macOS
brew upgrade vouch
# Linux
sudo apt upgrade vouch # Debian/Ubuntu
sudo dnf upgrade vouch # RHEL/Fedora
Rollback
If an update causes issues:
- Stop the server
- Restore the database from pre-upgrade backup
- Install the previous version
- Start the server
Note: Database migrations may not be reversible. Always back up before upgrading.
Version Compatibility
- The server is backward-compatible with older CLI versions
- Upgrade the server first, then clients
- Major version bumps may require simultaneous client updates (documented in release notes)
Release Channels
| Channel | Stability | Use Case |
|---|---|---|
latest | Stable releases | Production |
x.y.z | Pinned version | Production (recommended) |
main | Development builds | Testing only |
Troubleshooting
Common Issues
Server Connection Issues
“Connection refused” or timeouts
- Check server health:
curl -k https://auth.example.com/health - Check DNS resolution:
dig auth.example.com - Check TLS:
openssl s_client -connect auth.example.com:443 - Check firewall rules (port 443 must be accessible)
SCIM Provisioning Issues
User not de-provisioned
- Verify the SCIM bearer token is valid and not expired
- Check the SCIM audit log for errors
- Confirm the IdP is sending DELETE requests to the correct endpoint
SCIM token rejected
- Tokens are shown once at creation and cannot be retrieved after
- Generate a new token via the admin API (
POST /api/v1/org/scim-tokens) and update the IdP configuration
Identity Provider Issues
“Failed to fetch upstream OIDC discovery document”
- Verify the configured
VOUCH_IDP_<SLUG>_ISSUERis correct and reachable:curl -s $VOUCH_IDP_<SLUG>_ISSUER/.well-known/openid-configuration | jq .issuer - Check that the issuer URL uses HTTPS (HTTP is only allowed for
localhost) - Ensure the server can make outbound HTTPS requests (check firewall/proxy)
“Issuer mismatch” during OIDC discovery
- The
issuerfield in the discovery document must exactly matchVOUCH_IDP_<SLUG>_ISSUER(trailing slashes matter) - Some providers require a trailing slash (e.g., Auth0:
https://tenant.auth0.com/) - Entra
/organizations/v2.0is special-cased — its{tenantid}template issuer is accepted - Entra
/common/v2.0is rejected at startup; use/organizations/v2.0or a single-tenant URL (see Microsoft Entra ID)
“Failed to fetch SAML IdP metadata”
- Verify the configured
VOUCH_IDP_<SLUG>_METADATA_URLis correct and reachable - Check that the URL returns XML, not an HTML login page
- Ensure the server can make outbound HTTPS requests
SAML signature verification errors
- Confirm the IdP’s signing certificate in the metadata is current and not expired
- Ensure the server clock is synchronized via NTP — SAML assertions have time-based validity windows (typically 5 minutes of skew tolerance)
- Check the IdP assertion signing algorithm matches what the server expects
Enrollment broken after upgrade, and only the old VOUCH_OIDC_* / VOUCH_SAML_* env vars or S3 oidc / saml blocks are set
- The flat legacy variables and S3 blocks are no longer read by the server — they are silently ignored. Migrate to the unified
VOUCH_IDPS+VOUCH_IDP_<SLUG>_*env vars (or the S3idpsarray). See IdP Overview for the full mapping.
“Duplicate IdP slug”
- Every entry in
VOUCH_IDPS/idps[].idmust be unique. Rename one of them.
Debug Logging
Enable verbose logging for troubleshooting:
# Server
RUST_LOG=debug vouch-server
For component-specific logging:
RUST_LOG=vouch_server=debug
Getting Help
- GitHub Issues — Bug reports
- GitHub Discussions — Questions
- Security Issues — Security vulnerabilities
Incident Response
This chapter describes Vouch’s incident severity classification, response procedures, and communication channels for security events.
Severity Levels
| Level | Description | Response Time |
|---|---|---|
| Critical | Active exploitation, credential theft | 1 hour |
| High | Exploitable vulnerability, no active exploitation | 24 hours |
| Medium | Vulnerability requiring unlikely conditions | 7 days |
| Low | Minor issues, defense in depth | 30 days |
Response Procedure
- Triage — Assess severity and scope
- Contain — Revoke affected credentials, disable vulnerable features
- Investigate — Root cause analysis
- Remediate — Deploy fix
- Communicate — Notify affected users
- Review — Post-incident analysis
Communication Channels
- Security advisories: https://vouch.sh/security
- CVE assignments: Via GitHub Security Advisories
- Status page: https://status.vouch.sh
Air-Gapped Deployment
This chapter covers deploying Vouch in environments with no internet connectivity, such as defense contractors, government agencies, financial services, and critical infrastructure.
Supported with operational constraints — You can run
vouch-serverand thevouchCLI on an isolated network today using the same binaries and deployment paths as on-premise (systemd, Docker, or Kubernetes). This chapter documents that workflow. A few operator conveniences (listed under Roadmap below) are not built into the product yet.
Supported today
| Capability | Where to read |
|---|---|
| Server install (RPM/DEB, containers, Helm) | Installation, Deployment Methods |
| Configuration, TLS, database, SSH CA | Configuration Reference |
| Internal OIDC or SAML IdP | Identity Provider Overview, SAML 2.0 |
| Enrollment (YubiKey + browser on internal network) | Installation, YubiKey Provisioning |
| Key ceremony on a trusted workstation | Key Ceremony |
| Day-two ops (time sync, updates, audit export scripts) | Operations |
| Packages via sneakernet | packages.vouch.sh (download on a connected machine, transfer in) |
Enrollment uses the standard vouch enroll device flow (browser opens the verification URL on your internal Vouch host) or browser-only /enroll/start on the server UI. There is no separate air-gap-only CLI mode.
For general on-prem deployment (reachable IdP, standard updates), start with Deployment Overview.
Roadmap
These items are not available in the product today; the chapters above describe manual procedures instead:
- Server syslog / SIEM streaming — use periodic database export in Operations until built-in export exists
- Headless enrollment — enrollment without any browser on the internal network
Overview
In an air-gapped environment:
- No SaaS services available
- Updates delivered via sneakernet
- Internal identity provider (no Google Workspace)
- Time sync from isolated NTP or GPS
Vouch’s built-in SSH CA and local-first architecture make it well-suited for these constraints.
Architecture
+--------------------------------------------------------------------------+
| AIR-GAPPED ENCLAVE |
| |
| +--------------------------------------------------------------------+ |
| | On-Premises Vouch Stack | |
| | | |
| | +--------------+ +----------------+ +-----------------------+ | |
| | | Vouch | | Built-in | | SQLite | | |
| | | Server | | SSH CA | | | | |
| | | | | | | * Users & credentials | | |
| | | * WebAuthn | | * Ed25519 CA | | * Sessions | | |
| | | * OIDC | | * SSH certs | | * Audit logs | | |
| | | * Sessions | | * 8hr TTL | | | | |
| | +--------------+ +----------------+ +-----------------------+ | |
| | | | | | |
| | +------------------+----------------------+ | |
| | | | |
| +----------------------------+---------------------------------------+ |
| | |
| | Internal Network Only |
| v |
| +--------------------------------------------------------------------+ |
| | Workstations | |
| | | |
| | +--------------+ +--------------+ +-------------------------+ | |
| | | Workstation | | Workstation | | Protected Resources | | |
| | | | | | | | | |
| | | * vouch CLI | | * vouch CLI | | * SSH servers | | |
| | | * YubiKey | | * YubiKey | | * Internal apps | | |
| | | * Certs | | * Certs | | * Databases | | |
| | +--------------+ +--------------+ +-------------------------+ | |
| | | |
| +--------------------------------------------------------------------+ |
| |
| +--------------------------------------------------------------------+ |
| | Time Infrastructure | |
| | +------------+ +-----------------+ | |
| | | GPS Time |---->| Internal NTP |----> All hosts | |
| | | Receiver | | (stratum 1) | | |
| | +------------+ +-----------------+ | |
| +--------------------------------------------------------------------+ |
+--------------------------------------------------------------------------+
|
| Air Gap (sneakernet)
v
+--------------------------------------------------------------------------+
| CONNECTED ENVIRONMENT |
| |
| * Signed software packages (from packages.vouch.sh) |
| * CA certificate updates |
| * (Optional) Audit log export |
+--------------------------------------------------------------------------+
Identity Provider Considerations
In an air-gapped environment, you cannot use external identity providers like Google Workspace for enrollment. Vouch Server requires at least one upstream IdP to verify user identity, so an air-gapped deployment must include a self-hosted IdP inside the enclave:
- Self-hosted OIDC provider — Deploy an internal OIDC-compliant IdP inside the enclave (e.g., Keycloak, Dex, or Microsoft AD FS). Add it to Vouch Server’s
VOUCH_IDPSlist withVOUCH_IDP_<SLUG>_TYPE=oidcplus the_ISSUER,_CLIENT_ID, and_CLIENT_SECRETvariables pointing to the internal IdP. - Self-hosted SAML provider — Deploy an internal SAML IdP (e.g., Shibboleth, AD FS) and configure it with
VOUCH_IDP_<SLUG>_TYPE=samlplusVOUCH_IDP_<SLUG>_METADATA_URLpointing to the internal metadata document.
Prerequisites
Hardware
- Servers for Vouch stack (VMs or bare metal)
- YubiKey 5 series for each user (firmware 5.2+)
- GPS receiver for time sync (recommended)
- USB drives for sneakernet transfers
Software (Pre-downloaded)
- Vouch Server packages (RPM/DEB from packages.vouch.sh)
- vouch CLI packages (RPM/DEB from packages.vouch.sh)
- Container images and/or Helm charts (for Kubernetes deployments)
Installation
This chapter walks through the complete installation procedure for deploying Vouch in an air-gapped environment, from downloading packages on a connected machine through enrolling users on the isolated network.
Step 1: Download Packages for Offline Transfer
On a connected machine, download the required packages from packages.vouch.sh:
# Import Vouch GPG signing key
curl -fsSL https://packages.vouch.sh/gpg/vouch.asc | gpg --import
# Download server RPM
curl -LO https://packages.vouch.sh/rpm/x86_64/vouch-server-1.0.0-1.x86_64.rpm
# Download CLI RPM (for each workstation architecture)
curl -LO https://packages.vouch.sh/rpm/x86_64/vouch-1.0.0-1.x86_64.rpm
curl -LO https://packages.vouch.sh/rpm/aarch64/vouch-1.0.0-1.aarch64.rpm
# For Debian/Ubuntu workstations
curl -LO https://packages.vouch.sh/apt/vouch_1.0.0_amd64.deb
curl -LO https://packages.vouch.sh/apt/vouch_1.0.0_arm64.deb
For container-based or Kubernetes deployments, also download:
# Pull and save container image
docker pull ghcr.io/vouch-sh/vouch:1.0.0
docker save ghcr.io/vouch-sh/vouch:1.0.0 -o vouch-server-1.0.0.tar
# Download Helm chart (for Kubernetes)
helm pull oci://ghcr.io/vouch-sh/charts/vouch-server --version 0.1.0
Generate checksums for verification after transfer:
sha256sum vouch-server-*.rpm vouch-*.rpm vouch-*.deb vouch-server-*.tar > SHA256SUMS
gpg --detach-sign SHA256SUMS
Transfer all files to the air-gapped environment via approved media.
Step 2: Verify Package Integrity
On the air-gapped network:
# Import Vouch GPG signing key (transferred separately, verified out-of-band)
gpg --import vouch-release-key.pub
# Verify checksums
gpg --verify SHA256SUMS.sig SHA256SUMS
sha256sum -c SHA256SUMS
# Verify RPM signatures
rpm -K vouch-server-1.0.0-1.x86_64.rpm
rpm -K vouch-1.0.0-1.x86_64.rpm
Step 3: Install Packages
RPM-based installation (recommended for bare metal/VM):
# Install server
rpm -ivh vouch-server-1.0.0-1.x86_64.rpm
# Install CLI on workstations
rpm -ivh vouch-1.0.0-1.x86_64.rpm
DEB-based installation:
# Install CLI on Debian/Ubuntu workstations
dpkg -i vouch_1.0.0_amd64.deb
Container-based installation:
# Load container image into local Docker registry
docker load < vouch-server-1.0.0.tar
# Verify image loaded
docker images | grep vouch
Step 4: Secure Key Generation
This is a critical security operation. Follow your organization’s key ceremony procedures.
Vouch requires several cryptographic keys for different purposes. All key generation should be performed on a trusted, air-gapped workstation. See the Key Ceremony chapter for detailed instructions on generating each key.
Key Overview
| Key | Type | Format | Required | Purpose |
|---|---|---|---|---|
| JWT Secret | Symmetric | UTF-8 (32+ chars) | Yes | Sign OAuth tokens and sessions |
| SSH CA Key | Ed25519 | Base64-encoded OpenSSH PEM | Optional | Sign SSH certificates |
| OIDC Signing Key | P-256 ECDSA | Base64-encoded PKCS#8 PEM | Optional* | Sign OIDC ID tokens |
| TLS Certificate | RSA/EC | Base64-encoded PEM | Optional | HTTPS encryption |
| TLS Private Key | RSA/EC | Base64-encoded PEM | Optional | HTTPS encryption |
*Auto-generates ephemeral key if not provided (not recommended for production).
Note: All PEM-formatted keys and certificates must be base64-encoded when passed via environment variables. This ensures proper handling of newlines and special characters.
Step 5: Database Setup
Vouch uses SQLite by default, which is suitable for single-node deployments. The database is created automatically on first startup.
# SQLite (default, single-node)
export VOUCH_DATABASE_URL="sqlite:/data/vouch.db?mode=rwc"
# Create data directory with appropriate permissions
mkdir -p /data
chmod 700 /data
For high-availability deployments, a local PostgreSQL instance is supported:
# PostgreSQL (multi-node, must be reachable on the internal network)
export VOUCH_DATABASE_URL="postgres://user:password@db.internal:5432/vouch"
Database migrations run automatically on server startup.
Step 6: Configure Vouch Server
Vouch is configured entirely through environment variables. Create a secure environment file:
# Create environment file (chmod 600 after editing)
cat > /etc/vouch/vouch.env << 'EOF'
# =============================================================================
# Vouch Server Configuration - Air-Gapped Environment
# =============================================================================
# -----------------------------------------------------------------------------
# Required Configuration
# -----------------------------------------------------------------------------
# JWT signing secret (minimum 32 characters)
VOUCH_JWT_SECRET=<your-64-character-secret-here>
# Relying Party configuration
VOUCH_RP_ID=auth.internal
VOUCH_RP_NAME=Vouch (Air-Gapped)
# Database
VOUCH_DATABASE_URL=sqlite:/data/vouch.db?mode=rwc
# -----------------------------------------------------------------------------
# Network Configuration
#
# Development (no TLS):
# Server listens on VOUCH_LISTEN_ADDR (default: 0.0.0.0:3000)
#
# Production (TLS enabled):
# Server automatically listens on port 443 (HTTPS) and port 80 (HTTP redirect)
# VOUCH_LISTEN_ADDR is ignored when TLS is configured
# HTTP requests on port 80 are 308 redirected to HTTPS on port 443
# The /health endpoint is accessible on HTTP (for load balancer health checks)
# Host header is validated against rp_id to prevent injection attacks
# Requires CAP_NET_BIND_SERVICE capability (handled by packaging scripts)
# -----------------------------------------------------------------------------
# Listen address (used only when TLS is NOT configured)
VOUCH_LISTEN_ADDR=0.0.0.0:3000
# Base URL (how clients reach the server)
VOUCH_BASE_URL=https://auth.internal
# -----------------------------------------------------------------------------
# TLS Configuration (base64-encoded PEM)
# Generate with: base64 -i cert.pem | tr -d '\n'
# -----------------------------------------------------------------------------
VOUCH_TLS_CERT=<base64-encoded-certificate>
VOUCH_TLS_KEY=<base64-encoded-private-key>
# -----------------------------------------------------------------------------
# SSH CA Configuration (base64-encoded PEM)
# Generate with: base64 -i ssh_ca_key | tr -d '\n'
# -----------------------------------------------------------------------------
# SSH CA private key (base64-encoded PEM, takes precedence over path)
VOUCH_SSH_CA_KEY=<base64-encoded-ssh-ca-private-key>
# Or use a file path instead (file contains raw PEM, not base64):
# VOUCH_SSH_CA_KEY_PATH=/secrets/ssh_ca_key
# -----------------------------------------------------------------------------
# OIDC Provider Configuration
# Generate with: base64 -i oidc_signing_key.pem | tr -d '\n'
# -----------------------------------------------------------------------------
# Vouch acts as an OIDC provider - this key signs the ID tokens (base64-encoded PEM)
VOUCH_OIDC_SIGNING_KEY=<base64-encoded-oidc-signing-key>
# -----------------------------------------------------------------------------
# Upstream Identity Provider (Required)
# At least one IdP must be configured; the server refuses to start without one.
# -----------------------------------------------------------------------------
VOUCH_IDPS=internal-oidc
VOUCH_IDP_INTERNAL_OIDC_TYPE=oidc
VOUCH_IDP_INTERNAL_OIDC_ISSUER=https://idp.internal
VOUCH_IDP_INTERNAL_OIDC_CLIENT_ID=vouch-client
VOUCH_IDP_INTERNAL_OIDC_CLIENT_SECRET=<client-secret>
# -----------------------------------------------------------------------------
# Session Configuration
# -----------------------------------------------------------------------------
# Session duration (default: 8 hours)
VOUCH_SESSION_HOURS=8
# Device code settings (for CLI enrollment)
VOUCH_DEVICE_CODE_EXPIRES=600
VOUCH_DEVICE_POLL_INTERVAL=5
# -----------------------------------------------------------------------------
# Security Configuration
# -----------------------------------------------------------------------------
# Allowed email domains for enrollment (comma-separated)
VOUCH_ALLOWED_DOMAINS=internal,company.local
# DPoP (Demonstrating Proof of Possession)
VOUCH_DPOP_ENABLED=true
VOUCH_DPOP_NONCE_REQUIRED=false
VOUCH_DPOP_MAX_AGE=300
# -----------------------------------------------------------------------------
# Audit and Retention
# -----------------------------------------------------------------------------
# Cleanup interval (minutes, 0 to disable)
VOUCH_CLEANUP_INTERVAL=15
# Event retention (days)
VOUCH_AUTH_EVENTS_RETENTION_DAYS=730
VOUCH_OAUTH_EVENTS_RETENTION_DAYS=90
# -----------------------------------------------------------------------------
# Branding (Optional)
# -----------------------------------------------------------------------------
VOUCH_ORG_NAME=Your Organization
EOF
# Secure the environment file
chmod 600 /etc/vouch/vouch.env
Environment Variables Reference
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_JWT_SECRET | Yes | - | Session signing (min 32 chars) |
VOUCH_RP_ID | Yes | localhost | Relying party domain |
VOUCH_RP_NAME | No | Vouch | Display name |
VOUCH_DATABASE_URL | Yes | sqlite:vouch.db?mode=rwc | Database connection |
VOUCH_LISTEN_ADDR | No | 0.0.0.0:3000 | Server bind address |
VOUCH_BASE_URL | No | https://{rp_id} | External URL |
VOUCH_SESSION_HOURS | No | 8 | Session duration |
VOUCH_SSH_CA_KEY | No | - | SSH CA key (base64-encoded PEM) |
VOUCH_SSH_CA_KEY_PATH | No | ./ssh_ca_key | SSH CA key file path (raw PEM) |
VOUCH_OIDC_SIGNING_KEY | No | auto-generate | OIDC token signing key (base64-encoded PEM) |
VOUCH_TLS_CERT | No | - | TLS cert (base64-encoded PEM) |
VOUCH_TLS_KEY | No | - | TLS key (base64-encoded PEM) |
VOUCH_ALLOWED_DOMAINS | No | - | Allowed email domains |
VOUCH_DPOP_ENABLED | No | true | Enable DPoP support |
VOUCH_CLEANUP_INTERVAL | No | 15 | Cleanup interval (minutes) |
VOUCH_AUTH_EVENTS_RETENTION_DAYS | No | 90 | Auth event retention |
Step 7: Deploy Services
There are several options for deploying the Vouch server.
Option A: Systemd Service (RPM Install)
If you installed via RPM, the vouch-server systemd service is configured automatically:
# Configure environment
cp /etc/vouch/vouch.env /etc/vouch/vouch.env.local
# Edit /etc/vouch/vouch.env.local with your settings
# Start and enable the service
systemctl enable --now vouch-server
# Check status
systemctl status vouch-server
# View logs
journalctl -u vouch-server -f
Option B: Docker Compose
# docker-compose.yml
services:
vouch-server:
image: ghcr.io/vouch-sh/vouch:1.0.0
container_name: vouch-server
restart: unless-stopped
ports:
- "443:443"
volumes:
- vouch-data:/data
- /etc/vouch/secrets:/secrets:ro
env_file:
- /etc/vouch/vouch.env
environment:
# Override or add environment variables here
VOUCH_DATABASE_URL: sqlite:/data/vouch.db?mode=rwc
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "--no-check-certificate", "https://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
vouch-data:
# Start services
docker-compose up -d
# Verify container is running
docker-compose ps
# Check logs for startup errors
docker-compose logs -f vouch-server
Option C: Helm Chart (Kubernetes)
A Helm chart is available for Kubernetes deployments. After transferring the chart archive to the air-gapped environment:
# Install from the downloaded chart archive
helm install vouch-server vouch-server-0.1.0.tgz \
--namespace vouch \
--create-namespace \
--set image.repository=vouch-server \
--set image.tag=1.0.0 \
--values my-values.yaml
See the chart’s values.yaml for all configurable options including secrets, ingress, and persistent storage.
Verify Deployment
Regardless of deployment method:
# Verify health endpoint
curl -k https://auth.internal/health
# Expected: {"status":"healthy"}
# Verify SSH CA is loaded (if configured)
curl -k https://auth.internal/v1/credentials/ssh/ca
# Expected: ssh-ed25519 AAAA... vouch-ca@auth.internal
Step 8: Distribute CA Public Key
The SSH CA public key must be trusted by all SSH servers in the air-gapped environment:
# Fetch CA public key via API
curl -k https://auth.internal/v1/credentials/ssh/ca > vouch-ca.pub
# Copy to all SSH servers
scp vouch-ca.pub root@server:/etc/ssh/vouch-ca.pub
# Configure SSH server to trust the CA
echo "TrustedUserCAKeys /etc/ssh/vouch-ca.pub" >> /etc/ssh/sshd_config
# Optionally, configure authorized principals
echo "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u" >> /etc/ssh/sshd_config
# Restart SSH daemon
systemctl restart sshd
AuthorizedPrincipals Setup (Optional but Recommended):
# Create principals directory
mkdir -p /etc/ssh/auth_principals
# For each user, create a file with allowed principals
# Vouch issues certificates with two principals: email and username
echo "john@company.internal" > /etc/ssh/auth_principals/john
echo "john" >> /etc/ssh/auth_principals/john
Step 9: Configure CLI for Air-Gap
# ~/.config/vouch/config.json
{
"server_url": "https://auth.internal",
"ca_cert_path": "/etc/vouch/root-ca.crt"
}
Or via environment:
export VOUCH_SERVER=https://auth.internal
export VOUCH_CA_CERT=/etc/vouch/root-ca.crt
Step 10: Enroll Users
Important: Enrollment requires browser access to the Vouch server’s web UI on the internal network. Users run
vouch enrollon a workstation that can open the device verification URL against the internal Vouch host; there is no separate headless-only enroll mode.
Each user:
- Opens a browser to
https://auth.internal/enroll - Authenticates via the configured identity provider
- Registers their YubiKey through the browser’s WebAuthn prompt (touch + PIN)
After enrollment, daily login uses the CLI (vouch login) with no browser required.
Key Ceremony
This chapter details the cryptographic key generation procedures required for an air-gapped Vouch deployment. All key generation should be performed on a trusted, air-gapped workstation following your organization’s key ceremony procedures.
JWT Secret Generation (Required)
The JWT secret is used to sign all OAuth tokens and session cookies. It must be at least 32 characters.
# Generate cryptographically secure 64-character secret
openssl rand -base64 48
# Alternative using /dev/urandom
head -c 48 /dev/urandom | base64
# Store securely - this will be VOUCH_JWT_SECRET
Security Notes:
- Use a minimum of 32 characters (48+ recommended)
- Never reuse secrets across environments
- Rotate periodically (requires re-authentication of all users)
SSH CA Key Generation (Ed25519)
The SSH CA signs user SSH certificates. If not provided, SSH certificate issuance will be disabled.
# Generate Ed25519 SSH CA key pair (no passphrase for automated use)
ssh-keygen -t ed25519 -f ssh_ca_key -N "" -C "vouch-ca@auth.internal"
# Set restrictive permissions
chmod 600 ssh_ca_key
# Verify key type and fingerprint
ssh-keygen -l -f ssh_ca_key
# Output: 256 SHA256:xxxx vouch-ca@auth.internal (ED25519)
# View public key (for distribution to SSH servers)
cat ssh_ca_key.pub
Environment variable format (base64-encoded):
# Option 1: Provide base64-encoded key content (preferred for containers)
export VOUCH_SSH_CA_KEY="$(base64 -i ssh_ca_key | tr -d '\n')"
# Option 2: Provide path to key file (file contains raw PEM, not base64)
export VOUCH_SSH_CA_KEY_PATH="/secrets/ssh_ca_key"
Key Storage Options:
- HSM (recommended for high-security) – Store in hardware security module
- Encrypted file with split knowledge – Two administrators hold partial keys
- YubiKey PIV (for smaller deployments) – Store on hardware token
OIDC Signing Key Generation (P-256 ECDSA)
The OIDC signing key signs ID tokens using ES256 algorithm. If not provided, an ephemeral key is generated on each server restart (not recommended for production as it invalidates all existing tokens).
# Generate P-256 EC private key in PKCS#8 format
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out oidc_signing_key.pem
# Set restrictive permissions
chmod 600 oidc_signing_key.pem
# Verify key type
openssl ec -in oidc_signing_key.pem -text -noout 2>/dev/null | head -3
# Output should include: Private-Key: (256 bit, prime256v1)
# Extract public key (for debugging/verification)
openssl ec -in oidc_signing_key.pem -pubout -out oidc_signing_key.pub
Environment variable format (base64-encoded):
# Provide base64-encoded PEM content
export VOUCH_OIDC_SIGNING_KEY="$(base64 -i oidc_signing_key.pem | tr -d '\n')"
OIDC RSA Signing Key Generation (RSA-3072)
The OIDC RSA signing key signs ID tokens with RS256 algorithm per OIDC Core Section 3.1.3.7 and all AWS credential tokens (/v1/credentials/aws/token, serving both STS AssumeRoleWithWebIdentity and IAM Identity Center CreateTokenWithIAM). Any deployment using the AWS integration must provide a durable key — without one, an ephemeral RSA-3072 key is generated on each server restart, and AWS token verification breaks after restarts and across multiple instances.
# Generate RSA-3072 private key in PKCS#8 format
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out oidc_rsa_key.pem
# Set restrictive permissions
chmod 600 oidc_rsa_key.pem
# Verify key type and size
openssl rsa -in oidc_rsa_key.pem -text -noout 2>/dev/null | head -3
# Output should include: Private-Key: (3072 bit)
Environment variable format (base64-encoded):
# Provide base64-encoded PEM content
export VOUCH_OIDC_RSA_SIGNING_KEY="$(base64 -i oidc_rsa_key.pem | tr -d '\n')"
TLS Certificate Generation
For production, use certificates signed by your internal CA. For testing, self-signed certificates can be used.
# Generate EC private key and self-signed certificate
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout tls_key.pem -out tls_cert.pem -days 365 -nodes \
-subj "/CN=auth.internal" \
-addext "subjectAltName=DNS:auth.internal,DNS:localhost"
# Set restrictive permissions
chmod 600 tls_key.pem
# Verify certificate
openssl x509 -in tls_cert.pem -text -noout | head -15
Environment variable format (base64-encoded):
# Base64 encode for environment variables
export VOUCH_TLS_CERT="$(base64 -i tls_cert.pem | tr -d '\n')"
export VOUCH_TLS_KEY="$(base64 -i tls_key.pem | tr -d '\n')"
Key Security Best Practices
- File Permissions: Always use
chmod 600for private keys - Never Commit Keys: Add
*.pem,*_key,*.keyto.gitignore - Audit Key Access: Log all access to key material
- Backup Securely: Store encrypted backups in separate secure location
- Document Fingerprints: Record key fingerprints in secure documentation
- Key Rotation: Plan for periodic rotation (SSH CA annually, JWT secret quarterly)
YubiKey Provisioning
In an air-gapped environment, YubiKey provisioning is done entirely on the internal network through the Vouch server’s web UI. This chapter covers the provisioning workflow, hardware requirements, and spare key strategy.
Provisioning Workflow
- Administrator creates a user account via the Vouch server web interface
- User navigates to
https://auth.internalon their workstation browser - User inserts their YubiKey and completes the WebAuthn registration flow
- User sets a PIN on their YubiKey if one is not already configured (minimum 8 characters)
- The credential is registered and the user can begin authenticating
YubiKey Requirements
- YubiKey 5 series with firmware 5.2+
- FIDO2/WebAuthn support enabled
- PIN configured (minimum 8 characters)
Spare Key Strategy
Each user should register at least two YubiKeys (primary and backup). If a YubiKey is lost or damaged:
- User reports lost key to administrator
- Administrator revokes the lost key’s credential via the web UI
- User registers their backup YubiKey
Operations
This chapter covers the day-to-day operational procedures for maintaining a Vouch deployment in an air-gapped environment, including time synchronization, software updates, audit log export, disaster recovery, and troubleshooting.
Time Synchronization
Certificate validity depends on accurate time. Options for air-gapped networks:
GPS Time Receiver (Recommended)
+----------------+ +--------------------+
| GPS Receiver |---->| Internal NTP |
| (one-way data) | | Server (stratum 1) |
+----------------+ +--------------------+
| |
| v
One-way only All internal hosts
(no data out)
Configure NTP clients:
# /etc/ntp.conf
server ntp.internal iburst
Manual Time Sync
For truly isolated networks without GPS:
- Reference time from secure source (atomic clock, verified external)
- Set time on NTP server manually
- Document time sync in audit log
Vouch server configuration is done via environment variables (see Configure Vouch Server). JWT clock skew tolerance is handled automatically.
Software Updates
Update Procedure
- Download updated packages (connected environment)
# Download latest packages from packages.vouch.sh
curl -LO https://packages.vouch.sh/rpm/x86_64/vouch-server-1.1.0-1.x86_64.rpm
curl -LO https://packages.vouch.sh/rpm/x86_64/vouch-1.1.0-1.x86_64.rpm
# For container deployments
docker pull ghcr.io/vouch-sh/vouch:1.1.0
docker save ghcr.io/vouch-sh/vouch:1.1.0 -o vouch-server-1.1.0.tar
- Verify signatures (connected environment)
rpm -K vouch-server-1.1.0-1.x86_64.rpm
rpm -K vouch-1.1.0-1.x86_64.rpm
-
Transfer via approved media (sneakernet)
-
Verify again (air-gapped environment)
rpm -K vouch-server-1.1.0-1.x86_64.rpm
sha256sum -c SHA256SUMS
- Apply update
For RPM installations:
# Backup database before upgrade
cp /data/vouch.db /data/vouch.db.backup.$(date +%Y%m%d)
# Upgrade package (migrations run automatically on next startup)
rpm -Uvh vouch-server-1.1.0-1.x86_64.rpm
# Restart service
systemctl restart vouch-server
# Verify health
curl -k https://auth.internal/health
For container deployments:
docker load < vouch-server-1.1.0.tar
# Update docker-compose.yml image tag, then:
docker-compose up -d
Rollback
For RPM installations:
# Restore database backup
cp /data/vouch.db.backup.YYYYMMDD /data/vouch.db
# Downgrade package
rpm -Uvh --oldpackage vouch-server-1.0.0-1.x86_64.rpm
# Restart service
systemctl restart vouch-server
Audit Log Export
Air-gapped environments still need audit trails for compliance.
One-Way Data Diode
+-----------------+ +-------------+ +-----------------+
| Air-Gapped |---->| Data Diode |---->| SIEM |
| Vouch Server | | (hardware) | | (connected) |
| | | | | |
| UDP syslog out | | One-way | | Splunk/Datadog |
+-----------------+ +-------------+ +-----------------+
Roadmap: Built-in syslog/SIEM forwarding from
vouch-serveris not implemented yet. Use the periodic export method below (or your database backup process — see Backup and Recovery).
Periodic Export
#!/bin/bash
# Weekly audit log export script
DATE=$(date +%Y%m%d)
OUTPUT_DIR=/mnt/export
# Export audit logs from SQLite directly
sqlite3 /data/vouch.db \
".mode json" \
"SELECT * FROM auth_events WHERE created_at >= datetime('now', '-7 days');" \
> $OUTPUT_DIR/audit-$DATE.json
# Encrypt for transport
gpg --encrypt --recipient auditor@company.com \
$OUTPUT_DIR/audit-$DATE.json
# Generate checksum
sha256sum $OUTPUT_DIR/audit-$DATE.json.gpg > $OUTPUT_DIR/audit-$DATE.sha256
# Remove unencrypted
rm $OUTPUT_DIR/audit-$DATE.json
echo "Export complete: audit-$DATE.json.gpg"
Transfer encrypted exports via approved media to connected compliance systems.
Disaster Recovery
Backup Strategy
| Component | Frequency | Method | Retention |
|---|---|---|---|
| SQLite database | Daily | File copy, encrypted | 90 days |
| SSH CA keys | On change | HSM backup or split custody | Permanent |
| Configuration | On change | Git (internal) | Permanent |
| Audit logs | Continuous | Append-only storage | Per policy |
Recovery Procedure
- Stop the service
systemctl stop vouch-server
- Restore database from backup
cp /data/vouch.db.backup.YYYYMMDD /data/vouch.db
chown vouch:vouch /data/vouch.db
- Re-sync time
# Verify NTP synchronization
timedatectl status
chronyc tracking # or ntpq -p
- Start and validate
systemctl start vouch-server
curl -k https://auth.internal/health
CA Key Recovery
If CA keys are lost, all issued certificates become unverifiable.
Prevention:
- Store CA keys in HSM with backup
- Use split-knowledge for key recovery
- Document key ceremony procedures
Recovery:
- Generate new CA from backup
- Re-provision all user credentials
- Redistribute new CA public key
- Update all SSH server trust anchors
Security Considerations
Network Segmentation
+-------------------------------------------------------------+
| Air-Gapped Network |
| |
| +-----------------+ +-----------------------------+ |
| | Management | | User Network | |
| | VLAN | | | |
| | | | +-------+ +-----------+ | |
| | * Vouch Server |<------>| |Workst.| | Protected | | |
| | * SQLite | | +-------+ | Resources | | |
| | | | +-----------+ | |
| +-----------------+ +-----------------------------+ |
| | |
| | Restricted |
| v |
| +-----------------+ |
| | Admin Jumpbox | <-- Physical access control |
| +-----------------+ |
+--------------------------------------------------------------+
Physical Security
- Server room access controls
- YubiKey storage procedures
- Media transfer protocols
- Tamper-evident logging
Compliance Mapping
| Requirement | NIST 800-53 | Implementation |
|---|---|---|
| Hardware auth | IA-2(1) | FIDO2 with YubiKey |
| Credential lifetime | IA-5(1) | 8-hour certificates |
| Audit logging | AU-2, AU-3 | All credential issuance logged |
| Time sync | AU-8 | GPS/NTP infrastructure |
| Key management | SC-12 | HSM or split-custody |
Troubleshooting
Cannot Connect to Vouch Server
# Check network connectivity
ping auth.internal
# Verify TLS
openssl s_client -connect auth.internal:443 -CAfile /etc/vouch/root-ca.crt
# Check server logs (systemd)
journalctl -u vouch-server --since "1 hour ago"
# Check server logs (Docker)
docker-compose logs vouch-server
Certificate Validation Failures
# Check system time
date
timedatectl status
# Verify CA is trusted
ssh-keygen -L -f /path/to/cert # View certificate details
# Check certificate dates
ssh-keygen -L -f /path/to/cert | grep Valid
YubiKey Not Recognized
# Check USB connection
lsusb | grep Yubico
# Verify FIDO2 functionality
ykman fido info
# Reset FIDO2 application (destructive - re-enrollment required)
ykman fido reset
Environment Variables
All Vouch server configuration is done via environment variables (prefixed with VOUCH_). These can also be passed as command-line arguments.
Core Configuration
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_RP_ID | Yes | localhost | Relying Party ID (domain, e.g., vouch.sh). Used as the WebAuthn RP ID. |
VOUCH_RP_NAME | No | Vouch | Relying Party display name shown in browser prompts and UI. |
VOUCH_DATABASE_URL | Yes | sqlite:vouch.db?mode=rwc | Database connection URL. Supports sqlite:, postgres:, and Aurora DSQL endpoints. |
VOUCH_JWT_SECRET | Conditional | (empty) | JWT signing secret. Must be at least 32 characters. Must not consist of a single repeated character. Used to sign internal state tokens. Required unless VOUCH_JWT_HMAC_KMS_KEY_ID is set. |
VOUCH_BASE_URL | No | https://{rp_id} | Base URL for this server. Auto-derived from VOUCH_RP_ID if not set (http://localhost:{port} for local dev, https://{rp_id} for production). |
VOUCH_ORG_NAME | No | (none) | Organization name for branding in the UI. Falls back to VOUCH_RP_NAME if not set. |
VOUCH_ALLOWED_DOMAINS | No | (none) | Comma-separated list of allowed email domains for enrollment (e.g., example.com,corp.example.com). If not set, all domains are allowed. Normalized to lowercase. |
Network
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_LISTEN_ADDR | No | [::]:3000 | Address and port to listen on. Ignored when TLS is configured (server listens on 443 instead). |
Upstream Identity Provider
Configure one or more upstream IdPs (OIDC, SAML, or any mix) as a single unified list. VOUCH_IDPS holds a comma-separated list of slugs; each slug picks up its VOUCH_IDP_<SLUG>_* variables. Slugs match [a-z0-9-]{1,32} (no leading or trailing hyphen) and must be unique.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_IDPS | Yes | (none) | Comma-separated list of IdP slugs in display order (e.g., google,entra,corp-saml). At least one slug is required; the server refuses to start otherwise. |
VOUCH_IDP_<SLUG>_TYPE | Yes (per IdP) | (none) | oidc or saml. |
Hyphens in slugs become underscores in variable names: a slug of corp-saml becomes VOUCH_IDP_CORP_SAML_*.
OIDC IdP (per slug)
OIDC IdPs auto-discover authorization, token, and JWKS endpoints from {issuer}/.well-known/openid-configuration at startup.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_IDP_<SLUG>_ISSUER | Yes | (none) | OIDC issuer URL (e.g., https://accounts.google.com). Must serve a valid OIDC discovery document. |
VOUCH_IDP_<SLUG>_CLIENT_ID | Yes | (none) | OIDC client ID from the IdP. |
VOUCH_IDP_<SLUG>_CLIENT_SECRET | Yes | (none) | OIDC client secret from the IdP. |
SAML IdP (per slug)
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_IDP_<SLUG>_METADATA_URL | Yes | (none) | URL to the SAML IdP metadata XML document. Fetched at server startup. |
VOUCH_IDP_<SLUG>_SP_ENTITY_ID | No | {VOUCH_BASE_URL} | SP entity ID sent in authentication requests. Defaults to the server’s base URL. |
VOUCH_IDP_<SLUG>_EMAIL_ATTRIBUTE | No | (auto-detect) | SAML attribute name containing the user’s email address. |
VOUCH_IDP_<SLUG>_DOMAIN_ATTRIBUTE | No | (none) | SAML attribute name containing the user’s domain (for domain restriction). |
Legacy variables (no longer read)
The previous flat single-IdP variables — VOUCH_OIDC_ISSUER, VOUCH_OIDC_CLIENT_ID, VOUCH_OIDC_CLIENT_SECRET, VOUCH_OIDC_PROVIDERS, VOUCH_SAML_IDP_METADATA_URL, VOUCH_SAML_SP_ENTITY_ID, VOUCH_SAML_EMAIL_ATTRIBUTE, VOUCH_SAML_DOMAIN_ATTRIBUTE — are silently ignored. Setting them configures nothing; only the unified VOUCH_IDPS / VOUCH_IDP_<SLUG>_* variables above are read. See IdP Overview for the field-by-field mapping.
Session
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_SESSION_HOURS | No | 8 | Session duration in hours. After this time, the user must re-authenticate. |
VOUCH_DEVICE_CODE_EXPIRES | No | 600 | Device code expiration in seconds. How long a device code remains valid during enrollment. |
VOUCH_DEVICE_POLL_INTERVAL | No | 5 | Device code polling interval in seconds. How frequently the CLI polls for device code completion. |
SSH CA
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_SSH_CA_KEY | No | (none) | SSH CA private key content (base64-encoded PEM format, Ed25519). If set, takes precedence over VOUCH_SSH_CA_KEY_PATH. |
VOUCH_SSH_CA_KEY_PATH | No | ./ssh_ca_key | Path to SSH CA private key file (raw PEM, not base64). Set to empty string to disable SSH CA entirely. |
OIDC Signing
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_OIDC_SIGNING_KEY | No | (auto-generate) | OIDC signing key content (base64-encoded PEM format, P-256 ECDSA). Used for signing access tokens and ID tokens with ES256 algorithm. If not set, an ephemeral key is generated on each server restart (not recommended for production). |
VOUCH_OIDC_RSA_SIGNING_KEY | No | (auto-generate) | OIDC RSA signing key content (base64-encoded PEM format, RSA-3072). Used for signing ID tokens with RS256 algorithm per OIDC Core Section 3.1.3.7 and all AWS credential tokens (/v1/credentials/aws/token, serving both STS AssumeRoleWithWebIdentity and IAM Identity Center CreateTokenWithIAM). Minimum 3072-bit key enforced. If not set, an ephemeral key is generated on each server restart — AWS token verification then breaks after restarts and across multiple instances, so any deployment using the AWS integration must set this (or the KMS variant). |
AWS KMS
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_SSH_CA_KMS_KEY_ID | No | (none) | AWS KMS key ID for SSH CA signing (Ed25519). When set, overrides VOUCH_SSH_CA_KEY and VOUCH_SSH_CA_KEY_PATH. |
VOUCH_OIDC_SIGNING_KMS_KEY_ID | No | (none) | AWS KMS key ID for OIDC token signing (P-256 ECDSA). When set, overrides VOUCH_OIDC_SIGNING_KEY. |
VOUCH_OIDC_RSA_SIGNING_KMS_KEY_ID | No | (none) | AWS KMS key ID for OIDC RSA token signing (RSA-3072, RSASSA_PKCS1_V1_5_SHA_256). When set, overrides VOUCH_OIDC_RSA_SIGNING_KEY. |
VOUCH_JWT_HMAC_KMS_KEY_ID | No | (none) | AWS KMS key ID for HMAC state token signing. When set, VOUCH_JWT_SECRET is not required. |
DPoP
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_DPOP_MAX_AGE | No | 300 | Maximum age of DPoP proofs in seconds. Proofs older than this are rejected. |
Cleanup & Retention
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_CLEANUP_INTERVAL | No | 15 | Background cleanup task interval in minutes. Set to 0 to disable automatic cleanup. |
VOUCH_AUTH_EVENTS_RETENTION_DAYS | No | 90 | Retention period for authentication events in days. Events older than this are purged during cleanup. |
VOUCH_OAUTH_EVENTS_RETENTION_DAYS | No | 90 | Retention period for OAuth usage and credential-issuance (aws_credential, github_credential, ssh_credential, token_exchange) events in days. Events older than this are purged during cleanup. |
CORS
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_CORS_ORIGINS | No | (none) | Comma-separated list of explicit CORS allowed origins for UI routes (e.g. https://app.example.com). Empty means same-origin only. Wildcard (*) is not supported — UI routes use credentialed cookie sessions, which are incompatible with wildcard origins per the CORS spec. |
GitHub App
These variables configure the Vouch GitHub App integration for issuing GitHub tokens. The App ID, name, and key are required together for GitHub App functionality. OAuth client ID and secret are additionally needed for GitHub user authentication.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_GITHUB_APP_ID | No | (none) | GitHub App ID (numeric, assigned when creating the app on github.com). |
VOUCH_GITHUB_APP_NAME | No | (none) | GitHub App name (the slug from github.com/apps/{name}). |
VOUCH_GITHUB_APP_KEY | No | (none) | GitHub App private key (PEM format, RSA). Can use literal \n for newlines. |
VOUCH_GITHUB_WEBHOOK_SECRET | No | (none) | GitHub webhook secret for verifying webhook signatures (HMAC-SHA256). |
VOUCH_GITHUB_APP_CLIENT_ID | No | (none) | GitHub App Client ID for OAuth user authentication. Found in GitHub App settings (different from the numeric App ID). |
VOUCH_GITHUB_APP_CLIENT_SECRET | No | (none) | GitHub App Client Secret for OAuth user authentication. |
TLS
When both VOUCH_TLS_CERT and VOUCH_TLS_KEY are set, the server listens on port 443 (HTTPS) with an automatic HTTP-to-HTTPS redirect on port 80. The VOUCH_LISTEN_ADDR setting is ignored when TLS is configured.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_TLS_CERT | No | (none) | TLS certificate (base64-encoded PEM). |
VOUCH_TLS_KEY | No | (none) | TLS private key (base64-encoded PEM). Required if VOUCH_TLS_CERT is set. |
S3 Configuration
Vouch supports loading configuration from an S3 object for centralized management. S3 configuration values override environment variables.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_S3_CONFIG_BUCKET | No | (none) | S3 bucket name for configuration file. If set, config is loaded from S3. |
VOUCH_S3_CONFIG_KEY | No | config/vouch-server.json | S3 object key for configuration file. |
VOUCH_S3_CONFIG_REGION | No | (auto) | AWS region for S3 access. Uses the default credential chain region if not set. |
VOUCH_S3_CONFIG_POLL_INTERVAL | No | 60 | S3 config polling interval in seconds. How frequently the server checks for configuration changes. |
JWT Assertion
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_JWT_ASSERTION_MAX_LIFETIME | No | 300 | Maximum lifetime (seconds) for private_key_jwt client-authentication JWT assertions (RFC 7523 §2.2 / §3). Assertions older than this are rejected. |
Protected Resource Metadata (RFC 9728)
These optional variables configure descriptive metadata published in the OAuth 2.0 Protected Resource Metadata document at /.well-known/oauth-protected-resource.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_RESOURCE_NAME | No | Vouch | Human-readable name of this protected resource. |
VOUCH_RESOURCE_DOCUMENTATION | No | https://vouch.sh/docs/ | URL of developer documentation for this protected resource. |
VOUCH_RESOURCE_POLICY_URI | No | https://vouch.sh/privacy/ | URL of the resource’s data-use policy. |
VOUCH_RESOURCE_TOS_URI | No | https://vouch.sh/terms/ | URL of the resource’s terms of service. |
CLI Download URLs
These optional variables configure download links displayed in the server UI.
| Variable | Required | Default | Description |
|---|---|---|---|
VOUCH_CLI_DOWNLOAD_MACOS | No | (none) | CLI download URL for macOS, displayed in the server UI. |
VOUCH_CLI_DOWNLOAD_LINUX | No | (none) | CLI download URL for Linux, displayed in the server UI. |
VOUCH_CLI_DOWNLOAD_WINDOWS | No | (none) | CLI download URL for Windows, displayed in the server UI. |
CLI Localization
The Vouch CLI resolves its user-facing language once at startup. Resolution checks the following sources in order; the first that parses as a BCP 47 language tag wins.
| Variable | Required | Default | Description |
|---|---|---|---|
--lang <BCP-47> | No | (none) | Global CLI flag (highest priority). Example: vouch --lang en-US enroll. |
VOUCH_LANG | No | (none) | Explicit CLI locale override. Example: VOUCH_LANG=en-US. |
LC_ALL | No | (POSIX) | Standard POSIX locale variable; overrides every other LC_*. |
LC_MESSAGES | No | (POSIX) | Standard POSIX locale variable for message translations. |
LANG | No | (POSIX) | Standard POSIX locale variable used when LC_* are unset. |
If none of the above resolve, the CLI falls through to the operating system’s default locale via sys-locale. When that is also unavailable (e.g., a minimal container with no locale configured), the CLI falls back to en-US. POSIX-style locale strings (en_US.UTF-8, ca_ES@valencia) are normalized by stripping the .<codeset> and @<modifier> suffixes and replacing _ with -.
Machine-readable output (vouch status --json, vouch status --shell, vouch env, credential helpers) is never affected by the locale — those payloads are consumed by other tools and remain stable across locales. Only human-facing prompts, success/failure messages, error messages, and --help text honor the negotiated locale.
This release ships en-US only; the catalog scaffolding lets translations land in isolated PRs without changing the CLI surface.
File Locations (CLI & Agent)
The vouch CLI and vouch-agent follow the
XDG Base Directory Specification.
Files are split by purpose across the standard base directories, and the
relevant XDG_* environment variables are honored on all platforms —
including macOS, where Vouch uses ~/.config rather than
~/Library/Application Support, matching the convention of tools like gh and
git.
Locations
| File | Purpose | Base directory (env → default) | Default path |
|---|---|---|---|
config.json | CLI configuration, session token, OAuth/DPoP client metadata | XDG_CONFIG_HOME → ~/.config | ~/.config/vouch/config.json |
cookie.txt | Netscape session cookie for curl -b usage | XDG_STATE_HOME → ~/.local/state | ~/.local/state/vouch/cookie.txt |
audit.log | Agent security audit log (newline-delimited JSON) | XDG_STATE_HOME → ~/.local/state | ~/.local/state/vouch/audit.log |
client_key.json | FAPI client key — fallback only; the OS keychain is the primary store | XDG_DATA_HOME → ~/.local/share | ~/.local/share/vouch/client_key.json |
agent.pid, agent.log | Agent PID and daemon log | XDG_CACHE_HOME → ~/.cache | ~/.cache/vouch/ |
agent.sock, ssh-agent.sock | Agent IPC and SSH-agent Unix sockets | XDG_RUNTIME_DIR → cache fallback | $XDG_RUNTIME_DIR/vouch/*.sock |
All files written by Vouch are created with owner-only permissions (0600 for
files, 0700 for directories) on Unix.
Runtime sockets
Unix sockets live in XDG_RUNTIME_DIR — a private, user-owned, 0700 tmpfs
that the login session manager clears on logout. XDG_RUNTIME_DIR is only set
on Linux sessions managed by a login manager; on macOS and headless logins it
is absent, so Vouch falls back to a 0700 directory under the cache base
(~/.cache/vouch), chosen because its short path stays within the macOS
sun_path socket-path length limit.
Migration from ~/.vouch/
Earlier versions of Vouch stored everything flat under ~/.vouch/. On the first
run of a newer vouch or vouch-agent, files found there
(config.json, cookie.txt, audit.log, client_key.json) are moved
automatically to their XDG locations, preserving permissions. The migration is
idempotent and prints a one-time notice. Sockets are not migrated — they are
recreated on demand.
On macOS (and Windows), the agent’s agent.pid/agent.log previously lived in
the OS cache directory (~/Library/Caches/vouch) rather than ~/.cache/vouch.
These are migrated too, so an upgraded agent detects a still-running old agent
via the PID file instead of starting a second one. On Linux the cache location
is unchanged, so this step is a no-op.
One thing the migration cannot fix automatically: if you previously ran
vouch setup ssh, your ~/.ssh/config may contain an IdentityAgent line
pointing at the old ~/.vouch/ssh-agent.sock. Re-running vouch setup ssh
detects and rewrites that stale path to the new socket location.
S3 Configuration Schema
For production deployments, Vouch supports loading configuration from an S3 object. This enables:
- Centralized management — Single source of truth for multi-instance deployments
- Dynamic updates — Configuration changes without server restart (for supported fields)
- TLS hot-reload — Automatic certificate rotation without downtime
- Secrets management — Leverage S3 server-side encryption and IAM for credential protection
Enabling S3 Configuration
Set the following environment variables to enable S3-based configuration:
# Required: bucket name
VOUCH_S3_CONFIG_BUCKET=my-bucket
# Optional: object key (default: config/vouch-server.json)
VOUCH_S3_CONFIG_KEY=config/vouch-server.json
# Optional: AWS region (uses default credential chain region if not set)
VOUCH_S3_CONFIG_REGION=us-west-2
# Optional: polling interval in seconds (default: 60)
VOUCH_S3_CONFIG_POLL_INTERVAL=60
When S3 configuration is enabled, it overrides environment variables. This allows for centralized configuration management with dynamic updates.
JSON Schema
The S3 configuration file is a JSON document with the following schema:
{
"version": 1,
"listen_addr": "0.0.0.0:443",
"rp_id": "vouch.example.com",
"rp_name": "Example Corp",
"base_url": "https://vouch.example.com",
"database_url": "postgres://...",
"dsql_endpoints": {
"us-east-1": "postgres://vouch@abc123.dsql.us-east-1.on.aws/postgres"
},
"jwt_secret": "32+ character secret",
"session_hours": 8,
"org_name": "Example Corp",
"tls": {
"cert": "<base64-encoded PEM certificate>",
"key": "<base64-encoded PEM private key>"
},
"idps": [
{
"id": "google",
"type": "oidc",
"issuer": "https://accounts.google.com",
"client_id": "...",
"client_secret": "..."
},
{
"id": "corp-saml",
"type": "saml",
"metadata_url": "https://idp.example.com/saml/metadata",
"sp_entity_id": "https://vouch.example.com",
"email_attribute": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"domain_attribute": "department"
}
],
"allowed_domains": ["example.com"],
"ssh_ca_key": "<base64-encoded PEM Ed25519 private key>",
"ssh_ca_kms_key_id": "mrk-1234abcd5678efgh",
"oidc_signing_key": "<base64-encoded PEM EC P-256 private key>",
"oidc_signing_kms_key_id": "mrk-abcd1234efgh5678",
"oidc_rsa_signing_key": "<base64-encoded PEM RSA-3072 private key>",
"oidc_rsa_signing_kms_key_id": "mrk-rsa1234abcd5678",
"jwt_hmac_kms_key_id": "mrk-5678abcd1234efgh",
"document_key": {
"kms_key_id": "mrk-<key-id>",
"encrypted_private_key": "<base64-encoded KMS ciphertext>",
"algorithm": "p384"
},
"dpop": {
"max_age_seconds": 300
},
"cors_origins": ["https://app.example.com"],
"github": {
"app_id": 12345,
"app_name": "my-vouch-app",
"app_key": "<PEM RSA private key>",
"webhook_secret": "<secret>",
"client_id": "<oauth-client-id>",
"client_secret": "<oauth-client-secret>"
},
"cleanup_interval_minutes": 15,
"auth_events_retention_days": 90,
"oauth_events_retention_days": 90,
"resource_name": "Vouch",
"resource_documentation": "https://vouch.sh/docs/",
"resource_policy_uri": "https://vouch.sh/privacy/",
"resource_tos_uri": "https://vouch.sh/terms/",
"cli_download_macos": "https://example.com/vouch-macos",
"cli_download_linux": "https://example.com/vouch-linux",
"cli_download_windows": "https://example.com/vouch-windows",
"device_code_expires_seconds": 600,
"device_poll_interval_seconds": 5
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
version | integer | Schema version. Must be 1. |
listen_addr | string | Address and port to listen on (e.g., 0.0.0.0:443). |
rp_id | string | Relying Party ID (domain). Used as the WebAuthn RP ID. |
rp_name | string | Relying Party display name for browser prompts and UI. |
base_url | string | External base URL for the server. |
database_url | string | Database connection URL (sqlite:, postgres:, or Aurora DSQL). |
dsql_endpoints | object | Regional DSQL endpoints. Maps AWS region to full connection string. |
jwt_secret | string | JWT signing secret (minimum 32 characters). Not required if jwt_hmac_kms_key_id is set. |
session_hours | integer | Session duration in hours. |
org_name | string | Organization display name for branding in the UI. |
tls.cert | string | TLS certificate (base64-encoded PEM). |
tls.key | string | TLS private key (base64-encoded PEM). |
idps[] | array of objects | Configured identity providers (OIDC + SAML). Order controls login-page button order. Each entry has id, type ("oidc" or "saml"), and type-specific fields. |
idps[].id | string | Operator-chosen slug ([a-z0-9-]{1,32}, no leading/trailing hyphen, unique). Used in the state table, callback routing, and audit logs. |
idps[].type | string | "oidc" or "saml". |
idps[].issuer (OIDC) | string | OIDC issuer URL. The server auto-discovers endpoints. |
idps[].client_id (OIDC) | string | OIDC client ID from the IdP. |
idps[].client_secret (OIDC) | string | OIDC client secret from the IdP. |
idps[].metadata_url (SAML) | string | URL to the SAML IdP metadata XML document. |
idps[].sp_entity_id (SAML) | string | SP entity ID (defaults to base_url). |
idps[].email_attribute (SAML) | string | SAML attribute name for email extraction. |
idps[].domain_attribute (SAML) | string | SAML attribute name for domain extraction. |
allowed_domains | array of strings | Allowed email domains for enrollment. |
ssh_ca_key | string | SSH CA private key (base64-encoded PEM, Ed25519). |
ssh_ca_kms_key_id | string | AWS KMS key ID for SSH CA signing (Ed25519). Overrides ssh_ca_key. |
oidc_signing_key | string | OIDC signing key (base64-encoded PEM, P-256 ECDSA). |
oidc_signing_kms_key_id | string | AWS KMS key ID for OIDC token signing (P-256). Overrides oidc_signing_key. |
oidc_rsa_signing_key | string | OIDC RSA signing key (base64-encoded PEM, RSA-3072). Signs ID tokens with RS256. |
oidc_rsa_signing_kms_key_id | string | AWS KMS key ID for OIDC RSA signing (RSA-3072). Overrides oidc_rsa_signing_key. |
jwt_hmac_kms_key_id | string | AWS KMS key ID for HMAC state token signing. Overrides jwt_secret. |
document_key | object | Document encryption key. Contains kms_key_id, encrypted_private_key, and optional algorithm (default "p384", currently the only value). |
dpop.max_age_seconds | integer | Maximum age of DPoP proofs in seconds. |
cors_origins | array of strings | CORS allowed origins. |
github.app_id | integer | GitHub App ID. |
github.app_name | string | GitHub App name (slug from github.com/apps/{name}). |
github.app_key | string | GitHub App private key (PEM RSA). |
github.webhook_secret | string | GitHub webhook secret for signature verification. |
github.client_id | string | GitHub App OAuth client ID. |
github.client_secret | string | GitHub App OAuth client secret. |
cleanup_interval_minutes | integer | Background cleanup task interval in minutes. |
auth_events_retention_days | integer | Retention period for authentication events in days. |
oauth_events_retention_days | integer | Retention period for OAuth usage and credential-issuance (aws_credential, github_credential, ssh_credential, token_exchange) events in days. |
resource_name | string | Human-readable name of this protected resource (RFC 9728). Defaults to "Vouch". |
resource_documentation | string | URL of developer documentation for this protected resource (RFC 9728). Defaults to "https://vouch.sh/docs/". |
resource_policy_uri | string | URL of the resource’s data-use policy (RFC 9728). Defaults to "https://vouch.sh/privacy/". |
resource_tos_uri | string | URL of the resource’s terms of service (RFC 9728). Defaults to "https://vouch.sh/terms/". |
cli_download_macos | string | CLI download URL for macOS, displayed in the server UI. |
cli_download_linux | string | CLI download URL for Linux, displayed in the server UI. |
cli_download_windows | string | CLI download URL for Windows, displayed in the server UI. |
device_code_expires_seconds | integer | Device code expiration in seconds. |
device_poll_interval_seconds | integer | Device code polling interval in seconds. |
Base64 Encoding
All certificate and key fields in the S3 configuration must be base64-encoded PEM strings. To encode a PEM file:
# Encode a PEM file for S3 config
base64 -i cert.pem | tr -d '\n'
This ensures proper handling of newlines and special characters within JSON values.
Hot-Reloadable vs Startup-Only Fields
The server polls S3 at the configured interval and detects changes via ETag comparison. However, only certain fields support hot-reload without a server restart:
| Field | Hot-Reloadable | Notes |
|---|---|---|
tls.cert, tls.key | Yes | Automatic reload on change |
| All other fields | No | Requires server restart |
Non-hot-reloadable fields include: jwt_secret, database_url, listen_addr, rp_id, rp_name, session_hours, cors_origins, allowed_domains, dpop.*, idps[], GitHub App settings, SSH CA key, OIDC signing keys, and all KMS key IDs.
Legacy blocks silently ignored: The previous top-level
oidcandsamlblocks (single-IdP nested objects) are no longer read by the server. They configure nothing; only entries inside theidpsarray are honored. See IdP Overview for the field-by-field mapping.
Changes to non-hot-reloadable fields in S3 are silently ignored. A server restart is required to apply them.
TLS Certificate Hot-Reload
Vouch supports automatic TLS certificate reloading without dropping connections:
- Via S3 polling — Update
tls.certandtls.keyin the S3 config; the server detects the change via ETag and reloads automatically. - Via SIGHUP — Send
SIGHUPto the server process to reload TLS certificates.
# Manual TLS certificate reload (Unix only)
kill -SIGHUP $(pgrep vouch-server)
Note: SIGHUP only reloads TLS certificates. It does not reload any other configuration fields.
Compliance Mapping
This page maps Vouch features to common compliance frameworks. Vouch’s hardware-backed authentication model satisfies many of the strictest access control requirements across multiple regulatory standards.
NIST 800-53
| Control ID | Control Name | Vouch Implementation |
|---|---|---|
| IA-2 | Identification and Authentication | FIDO2 authentication with hardware-bound credentials |
| IA-2(1) | Multi-Factor Authentication to Privileged Accounts | Hardware FIDO2 key (something you have) + PIN (something you know) + physical touch (presence proof) |
| IA-2(2) | Multi-Factor Authentication to Non-Privileged Accounts | Same hardware MFA applied to all accounts; no weaker alternative |
| IA-2(6) | Access to Accounts — Separate Device | YubiKey is a separate hardware device from the workstation |
| IA-2(8) | Access to Accounts — Replay Resistant | FIDO2 challenge-response is inherently replay-resistant |
| IA-2(12) | Acceptance of PIV Credentials | FIDO2/WebAuthn hardware authenticators accepted |
| IA-5 | Authenticator Management | Short-lived credentials (8-hour SSH certificates, 1-hour AWS tokens); no long-lived secrets |
| IA-5(1) | Password-Based Authentication | YubiKey PIN verified on-device; never transmitted to server |
| IA-5(2) | Public Key-Based Authentication | Ed25519 SSH certificates issued by built-in CA; DPoP-bound OAuth tokens |
| IA-5(6) | Protection of Authenticators | Private keys are hardware-bound and non-extractable on YubiKey |
| AU-2 | Event Logging | All credential issuance and authentication events logged |
| AU-3 | Content of Audit Records | Audit records include user identity, timestamp, credential type, authenticator AAGUID, and IP address |
| AU-8 | Time Stamps | Certificate validity tied to server time; supports GPS/NTP in air-gapped environments |
| AU-9 | Protection of Audit Information | Audit logs stored in database with configurable retention periods |
| AC-2 | Account Management | User enrollment via verified identity (OIDC); key registration via CLI (vouch register); revocation via CLI (vouch keys remove) or admin API |
| AC-7 | Unsuccessful Logon Attempts | FIDO2 PIN retry limits enforced by YubiKey hardware (locks after 8 attempts) |
| AC-11 | Device Lock | Sessions expire after configurable duration (default 8 hours); re-authentication required |
| AC-12 | Session Termination | Explicit logout (vouch logout) and automatic session expiration |
| SC-12 | Cryptographic Key Establishment and Management | Ed25519 SSH CA key; ES256 OIDC signing key; support for HSM and split-custody key storage |
| SC-13 | Cryptographic Protection | FIDO2/CTAP2 with hardware-backed cryptography; TLS for transport; JWT signing for tokens; FIPS 140-3 cryptography (validated aws-lc-rs FIPS module embedded in server binaries on Linux; OS FIPS mode enabled in the attestable AMI — the kernel 6.18 modularized crypto module’s CMVP validation is in process, expected 2027) |
| SC-23 | Session Authenticity | DPoP (RFC 9449) sender-constrained tokens; FAPI 2.0 client authentication |
SOC 2
| Trust Service Criteria | Requirement | Vouch Implementation |
|---|---|---|
| CC6.1 | Logical access security | Hardware FIDO2 authentication mandatory for all access; no password-only path |
| CC6.2 | Authentication mechanisms | Multi-factor: hardware key + PIN + physical presence |
| CC6.3 | Authorization and access management | Short-lived credentials scoped to specific services (SSH, AWS, GitHub); role-based AWS access via OIDC federation |
| CC6.6 | Restriction of system access | Credential expiration (8 hours) limits access window; no persistent credentials |
| CC6.7 | Management of credentials | Automated credential lifecycle for user credentials (SSH certificates, AWS tokens); per-org issuer signing keys rotated via admin UI |
| CC6.8 | Prevention of unauthorized access | Hardware-bound keys cannot be copied, phished, or replayed; DPoP prevents token theft |
| CC7.1 | Detection of unauthorized access | Authentication event logging; FIDO2 attestation recorded for each session |
| CC7.2 | Monitoring of system components | Audit trail of all credential issuance; configurable event retention |
| CC8.1 | Change management | Server configuration via environment variables or S3; TLS hot-reload for certificate rotation |
FedRAMP
| Control Family | Control | Vouch Implementation |
|---|---|---|
| Identification and Authentication (IA) | IA-2(1), IA-2(2) | Hardware MFA required for all users; no option to bypass |
| Identification and Authentication (IA) | IA-2(6) | YubiKey is a physically separate authenticator device |
| Identification and Authentication (IA) | IA-2(8) | FIDO2 challenge-response protocol prevents replay attacks |
| Identification and Authentication (IA) | IA-5(2) | Public key authentication via Ed25519 SSH certificates and DPoP-bound tokens |
| Identification and Authentication (IA) | IA-5(6) | Hardware-bound, non-extractable private keys on YubiKey |
| Access Control (AC) | AC-2 | Centralized user enrollment and key management through Vouch server |
| Access Control (AC) | AC-7 | YubiKey enforces PIN lockout after consecutive failures |
| Access Control (AC) | AC-12 | Sessions auto-expire; explicit logout available |
| Audit and Accountability (AU) | AU-2, AU-3 | All authentication and credential issuance events logged with identity, timestamp, and method |
| Audit and Accountability (AU) | AU-8 | Time-bound certificates; NTP/GPS time sync supported for air-gapped deployments |
| System and Communications Protection (SC) | SC-12, SC-13 | Hardware-backed cryptography; Ed25519 CA; ES256 OIDC signing; TLS transport encryption; FIPS 140-3: validated aws-lc-rs FIPS module in server binaries; OS crypto-policy FIPS + kernel fips=1 in the attestable AMI (kernel crypto module validation in process) |
| System and Communications Protection (SC) | SC-23 | FAPI 2.0 with DPoP sender-constrained tokens; private_key_jwt client authentication |
| Configuration Management (CM) | CM-2 | Server configured via environment variables with S3 centralized config support |
| Contingency Planning (CP) | CP-9 | Database backup/restore procedures; CA key recovery with split custody |
HIPAA
| HIPAA Section | Requirement | Vouch Implementation |
|---|---|---|
| 164.312(a)(1) | Access Control — Unique User Identification | Each user enrolled with verified identity via OIDC; unique credentials per YubiKey |
| 164.312(a)(2)(i) | Unique User Identification | User email as principal in SSH certificates; sub claim in OIDC tokens |
| 164.312(a)(2)(iii) | Automatic Logoff | Sessions expire after configurable duration (default 8 hours) |
| 164.312(a)(2)(iv) | Encryption and Decryption | TLS for transport; hardware-backed cryptographic operations on YubiKey |
| 164.312(b) | Audit Controls | All authentication events logged; configurable retention (default 90 days, adjustable for compliance) |
| 164.312(c)(1) | Integrity Controls | FIDO2 attestation provides cryptographic proof of authenticator identity; signed SSH certificates and JWT tokens |
| 164.312(d) | Person or Entity Authentication | Hardware FIDO2 key + PIN + physical touch provides strong person authentication |
| 164.312(e)(1) | Transmission Security | TLS encryption for all server communication; DPoP prevents token interception |
| 164.312(e)(2)(ii) | Encryption | TLS 1.2+ enforced; base64-encoded PEM keys for configuration; rustls (no OpenSSL) |
| 164.308(a)(3)(ii)(A) | Workforce Clearance Procedure | Enrollment controlled by allowed email domains; administrative key revocation |
| 164.308(a)(4)(ii)(B) | Access Authorization | Role-based access via OIDC scopes and AWS IAM role federation |
| 164.308(a)(5)(ii)(C) | Log-in Monitoring | Authentication events include IP address, timestamp, authenticator AAGUID, and authentication method references (AMR) |
| 164.308(a)(5)(ii)(D) | Password Management | YubiKey PIN managed on-device; minimum 8 characters; lockout after failed attempts |
AMI STIG Alignment
This page documents how the Vouch Server attestable AMI aligns with the DISA Security Technical Implementation Guide (STIG) for its base operating system, Amazon Linux 2023 (STIG V1R2).
It complements the application-level Compliance Mapping: that page covers the Vouch authentication model, while this page covers the operating system posture of the AMI itself.
Why Vouch does not run the AWS STIG hardening scripts
AWS publishes
STIG hardening script bundles
(LinuxAWSConfigureSTIG.tgz) that are applied to a running, mutable instance,
typically through an SSM document or as EC2 Image Builder STIG-Build-Linux
components.
The Vouch AMI does not use them, by design:
- Immutable root filesystem. The root partition is mounted read-only with
dm-verity and
panic-on-corruption. Runtime remediation scripts either fail to persist (writes land on an overlay tmpfs) or trip the integrity check. All hardening is instead baked in at build time so it lives behind the same dm-verity measurement and TPM attestation (PCR4/7/12) as the rest of the image. - Minimal attack surface. The scripts can re-enable services and install
third-party packages. The AMI deliberately omits SSH, the SSM agent,
cloud-init, and
ec2-instance-connect; re-introducing them would weaken the posture rather than strengthen it. - Already met or exceeded. As the mapping below shows, the appliance design satisfies — and in several areas exceeds — the controls those scripts enforce.
Control mapping
Status legend:
- Met — satisfied by an explicit configuration in the image.
- Exceeded — satisfied by a stronger mechanism than the STIG requires.
- N/A by design — the control targets functionality the appliance does not ship (no interactive login, no SSH, no desktop).
- Build-time control — enforced via configuration baked into the image.
| STIG area | Status | Mechanism (file) |
|---|---|---|
| Remote access / SSH daemon hardening | N/A by design / Exceeded | openssh-server is not installed, sshd is masked, and any host keys are removed — there is no remote shell to harden (appliance.kiwi, config.sh) |
Interactive account policy (PAM, pwquality, faillock, session timeout, login banners) | N/A by design | No interactive user accounts exist and the root account is locked (passwd -l root); configuration is delivered only via AWS Parameter Store |
| FIPS-validated cryptography | Met (see note) | Kernel fips=1 plus OS crypto-policy set to FIPS (appliance.kiwi, config.sh); the server binary additionally embeds the FIPS 140-3 validated aws-lc-rs FIPS module. The kernel 6.18 crypto module itself is still in CMVP validation — see Kernel crypto module validation status |
| Boot loader integrity / boot loader password | Exceeded | UEFI Secure Boot with a signed Unified Kernel Image, no GRUB present, and TPM PCR measurements (PCR4/7/12) published as AMI tags for attestation |
| File-integrity monitoring (e.g. AIDE) | Exceeded | dm-verity provides cryptographic, kernel-enforced integrity of the entire root filesystem with panic-on-corruption |
Filesystem mount options (nosuid/nodev/noexec) | Met (partial) | /boot/efi is mounted ro,nosuid,nodev,noexec (fstab.script); the root filesystem is read-only via dm-verity |
Kernel parameters / sysctl hardening | Build-time control | etc/sysctl.d/90-vouch-hardening.conf sets ASLR, dmesg_restrict, kptr_restrict, ptrace_scope, suid_dumpable, reverse-path filtering, redirect/source-route rejection, SYN cookies, and related settings |
| Host firewall | N/A by design | firewalld is not installed; network exposure is controlled by EC2 security groups, and the only listening service is the Vouch server |
| Time synchronization integrity | Met | chrony with the command port disabled (chrony.d/10-disable-cmdport.conf); GPS/NTP supported for air-gapped deployments |
Audit logging (auditd rules) | Partial — see note | Application and system logs are forwarded to CloudWatch Logs; a kernel auditd ruleset is not currently shipped |
| Service / package minimization | Met / Exceeded | Curated minimal package set; ec2-hibinit-agent, update-motd, GRUB, and remote-access packages are explicitly excluded (appliance.kiwi) |
| Process privilege restriction | Exceeded | The server runs as an unprivileged user under strict systemd sandboxing (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, ReadWritePaths) |
Known gap: kernel audit (auditd)
The most material difference from a fully STIG-hardened general-purpose host is
the absence of a kernel auditd ruleset. On this appliance the gap is narrow:
- There are no interactive users or shells to audit, which is the primary target of most STIG audit rules.
- Application and credential-issuance events are logged by the Vouch server and forwarded to CloudWatch Logs alongside system journald output.
For deployments with a contractual auditd requirement, auditd and a STIG
audit ruleset can be added to the image (package in appliance.kiwi, service
enablement in config.sh, rules under etc/audit/rules.d/, and log forwarding
in the CloudWatch agent configuration). This is not enabled by default to keep
the image minimal and avoid a writable audit-log path on an otherwise userless
appliance.
Kernel crypto module validation status
The image runs AL2023 kernel 6.18, where the FIPS 140-3 kernel cryptography is
packaged as a standalone modularized kernel module that loads and initializes
automatically at boot. Enablement is unchanged by this packaging: fips=1 on
the kernel command line plus the FIPS crypto-policy (both baked into the image)
remain the mechanism, and the AMI build pipeline boot-verifies
fips mode: enabled on the serial console.
Validation status of the layers differs:
- Userspace (server binary): the embedded aws-lc-rs FIPS module is FIPS 140-3 validated.
- Kernel: the AL2023 kernel 6.18 modularized crypto module is under CMVP review (Implementation Under Test), with validation expected to complete in 2027 per AWS. Because the module is decoupled from the kernel image, later kernel updates will no longer invalidate the certificate once it is granted.
For deployments that contractually require a completed CMVP certificate for
kernel cryptography today, AL2023 kernel 6.1 carries the currently validated
kernel crypto module (certificate #5369) and is supported by AWS for the
lifetime of AL2023. Repinning the image is a one-line change to the
ami-minimal-kernel6.18 collection in appliance.kiwi, followed by a re-run
of the AMI boot-verification workflow.