DeploymentEN
Deployment Guide
Table of Contents
1. Quick Deployment
systemd (Linux Production)
# 1. Install the binary
cp varwof /usr/local/bin/varwof
cp deploy/init.sh /usr/local/bin/pki-init
cp deploy/pki-backup.sh /usr/local/bin/pki-backup
# 2. Configure
mkdir -p /etc/varwof/core
cp pki.json /etc/varwof/core/pki.json
# Edit /etc/varwof/core/pki.json
# 3. Install the service
cp deploy/pki.service /etc/systemd/system/pki.service
systemctl daemon-reload
systemctl enable --now pki
# 4. Verify
curl http://localhost:8443/healthz
On first startup, pki-init automatically:
- Creates the root CA
- Issues the server TLS certificate
- Creates the admin user
Docker
cd deploy
docker compose up -d
On first startup, an empty database is created and all migrations are applied.
init-full (Recommended)
One command to create the complete PKI hierarchy:
varwof init-full \
--root-name "TestCorp Root CA" \
--org "TestCorp" \
--country CN \
--base-dir /opt/pki \
--encrypt-keys
This creates:
- Root CA + 8 business sub-CAs
- Server TLS certificate
- Config file
- Initial CRL
See PKI Hierarchy for details.
2. Database Configuration
SQLite (Default)
{
"db": "/var/lib/pki/pki.db"
}
Zero-config, single-node. Backups via VACUUM INTO.
PostgreSQL
{
"db": "postgres://user:password@localhost:5432/pkidb?sslmode=require"
}
createdb -U postgres pkidb
MySQL / MariaDB
{
"db": "mysql://user:password@tcp(localhost:3306)/pkidb?charset=utf8mb4&parseTime=true"
}
CREATE DATABASE pkidb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pki'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL ON pkidb.* TO 'pki'@'localhost';
Database Comparison
| Feature | SQLite | PostgreSQL | MySQL/MariaDB |
|---|---|---|---|
| Deployment | Zero config | Requires PG server | Requires MySQL server |
| Concurrent writes | Single writer | Multi-writer | Multi-writer |
| Online backup | varwof db backup |
pg_dump | mysqldump |
| Distributed lock | noop | advisory lock | GET_LOCK |
| Recommended | Single node/dev | Production/multi-node | Production/MySQL ecosystem |
| Driver | modernc.org/sqlite (pure Go) | pgx/v5 (pure Go) | go-sql-driver/mysql (pure Go) |
Database Initialization
# SQLite (default)
varwof db init
# PostgreSQL
varwof db init --dsn "postgres://user:pass@host:5432/pki?sslmode=disable"
# MySQL
varwof db init --dsn "mysql://user:pass@host:3306/pki?charset=utf8mb4&parseTime=true"
varwof db init is idempotent.
3. TLS and Reverse Proxy
Direct TLS (Built-in)
{
"serve": {
"addr": ":4430",
"tls_addr": ":4433",
"tls_cert": "/etc/varwof/core/server.pem",
"tls_key": "/etc/varwof/core/server.key"
}
}
Nginx Reverse Proxy (Recommended)
server {
listen 443 ssl http2;
server_name pki.example.com;
ssl_certificate /etc/letsencrypt/live/pki.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pki.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# PKI API + Web UI
location / {
proxy_pass http://127.0.0.1:4430;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# OCSP
location /ocsp {
proxy_pass http://127.0.0.1:4430;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 10k;
}
# ACME
location /acme {
proxy_pass http://127.0.0.1:4430;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket — SSE Dashboard
location /api/v1/dashboard/events {
proxy_pass http://127.0.0.1:4430;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80;
server_name pki.example.com;
return 301 https://$host$request_uri;
}
Configure pki.json to listen on HTTP for intranet only:
{
"serve": {
"addr": "127.0.0.1:4430"
}
}
4. Automated Backup
systemd Timer (Recommended)
cp deploy/pki-backup.service /etc/systemd/system/pki-backup.service
cp deploy/pki-backup.timer /etc/systemd/system/pki-backup.timer
systemctl daemon-reload
systemctl enable --now pki-backup.timer
- Automatic daily backup
- Backup path:
/var/lib/pki/backups/pki-YYYYMMDD-HHMMSS.db - Retained for 90 days
- Uses
VACUUM INTO(online, no downtime)
Manual Backup
varwof db backup --output /backup/pki-$(date +%Y%m%d).db
Restoring a Backup
systemctl stop pki
cp /backup/pki-20260101.db /var/lib/pki/pki.db
systemctl start pki
5. Security Hardening
systemd Security Settings
Built into pki.service:
NoNewPrivileges=true— prevents privilege escalationProtectSystem=strict— read-only root filesystemProtectHome=yes— hides home directoriesPrivateTmp=true— isolated temporary directoryDynamicUser=yes— dynamically creates isolated user
Firewall
# Expose port 4430 only to reverse proxy
ufw allow from 127.0.0.1 to any port 4430
# Or iptables
iptables -A INPUT -p tcp --dport 4430 -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p tcp --dport 4430 -j DROP
Database Encryption
SQLite is not encrypted at rest. Sensitive data (private keys) is stored encrypted with PBKDF2+AES-256-CBC. For database-level encryption:
- SQLite: use LUKS-encrypted filesystem
- PostgreSQL: enable
pgcrypto+ TDE - MySQL: enable
InnoDBtablespace encryption
Key Security
- Keep root CA key offline (air-gapped)
- Use
varwof ca cold-backupfor encrypted backups - Enable
key_escrowfor admin key recovery - Rotate CA master keys before expiry via
POST /ca/{name}/rotate
RBAC
Enable RBAC for fine-grained access control:
{
"rbac": {
"enabled": true,
"mode": "simple"
}
}
Policy File Signing
Prevent tampering with authz.json and routes.json:
{
"policy_signing": {
"enabled": true,
"ca_file": "/etc/varwof/core/keys/issuing-ca.pem",
"require_admin_ou": true,
"require": true
}
}
Audit Logging
Enable audit log integrity verification:
{
"serve": {
"audit_salt": {
"enabled": true,
"retention_days": 365
},
"audit_verify": {
"enabled": true,
"interval": "24h"
}
}
}