Skip to content

Stargate Docker deployment

Prerequisites

Server Requirements:

  • 4 CPU cores (recommended minimum)
  • 8 GB RAM (recommended minimum)
  • 30 GB storage (recommended minimum)
  • Docker will be installed automatically if missing
  • Ensure there is an internet connection on the machine where you are installing Stargate services
  • Ensure traffic is properly configured to reach Stargate instance

Supported Linux Distributions:

  • RHEL 8, 9 and 10 compatible distributions such as Alma Linux, Rocky Linux, Centos Stream
  • Ubuntu 22 and 24
  • Debian 11, 12 and 13

Inbound Network Access (firewall must allow):

Port Protocol Purpose
25 TCP SMTP - receiving mail from external servers
8084 TCP HTTP - seal callback from remote sealer service
19818 TCP+UDP WireGuard - encrypted tunnel for agent-to-agent communication

Outbound Network Access (server must reach):

Destination Port Purpose
registry.vereign.io 443 Docker image registry
mxengine-dev.k8s.vereign-cdn.com 443 Remote sealer service
smimekeys-ca-dev.k8s.vereign-cdn.com 443 S/MIME CA service
loki.infra.vereign-cdn.com 443 Log shipping (Promtail → Loki)
vereign-issuer.vrgnservices.eu 443 Issuer service
vereign-verifier.vrgnservices.eu 4433 Verifier service
Destination mail servers 25 Outbound mail delivery (via MX lookup)

DNS Access:

  • Server must be able to resolve DNS (MX, SPF, A records)
  • Used for mail routing and SPF-based network allowlisting

Step 1: Configure Customer Settings

Before installation, create and fill in the customer configuration file:

# Copy the template
cp customer-config.example customer-config.sh

# Edit the config file
nano customer-config.sh

Required settings — you must fill these in:

Setting Description Example
SERVER_STATIC_IP This server's real static public IP. Used to derive WireGuard tunnel address and MXEngine callback URL. 203.0.113.10
CUSTOMER_NAME Customer/organization name (used for identification, logging, and as default certificate organization). Acme Corp
DEPLOYMENT_NAME Unique deployment identifier (used in log labels and Promtail hostname). stargate-acme
MAIL_DOMAINS Mail relay domains, comma-separated for multiple. MX and SPF records are looked up from DNS. example.com or example.com,example.org

Auto-derived settings — leave empty unless you need to override:

Setting Derived from Default
MXENGINE_PUBLIC_ADDRESS SERVER_STATIC_IP http://<SERVER_STATIC_IP>:8084
CERT_DNS_NAMES MAIL_DOMAINS + MAIL_HOSTNAME example.com,mail.example.com
CERT_ORGANIZATION CUSTOMER_NAME Acme Corp
CERT_COMMON_NAME CUSTOMER_NAME Acme Corp Mail Signing
MAIL_HOSTNAME First domain in MAIL_DOMAINS mail.example.com

S/MIME certificate settings:

Setting Description Default
CERT_COUNTRIES Country codes for certificate subject (2-letter ISO) US
CERT_CA_IDAGENT_DOMAIN CA domain for certificate issuance via WireGuard tunnel hintest.ch

WireGuard peer settings (pre-filled for HIN Test):

The template comes with HIN Test peer defaults. These work out of the box for the alpha/beta phase. Override only if connecting to a different peer.

Setting Default (HIN Test) Description
WG_PEER_NAME hin-test Human-readable peer name
WG_PEER_PUBLIC_KEY ol2zlG40M7+Rn81V9RUFmkIQV2ILLmEJHZww7HfoLxA= Remote peer's WireGuard public key
WG_PEER_ENDPOINT 5.102.144.182:19818 Remote peer's public endpoint (host:port)
WG_PEER_IP 5.102.144.182 Remote peer's WireGuard tunnel IP
WG_PEER_PORT 9090 HTTP communication port on the remote peer
WG_PEER_EXTERNAL_ID hintest.ch External identifier for routing (typically the peer's domain)

Note: WG_PEER_IP is the peer's tunnel address (used for routing inside WireGuard), while WG_PEER_PORT is the HTTP port the peer's IDAgent listens on for API calls over the tunnel.

WireGuard local settings (typically left at defaults):

Setting Default Description
WG_PRIVATE_KEY (auto-generated) Generated by IDAgent on first run, then saved to customer-config.sh
WG_LOCAL_IP SERVER_STATIC_IP Auto-derived. Only override if you need a different tunnel address.
WG_INTERFACE_PORT 19818 WireGuard tunnel port (both TCP and UDP are exposed)
WG_TRANSPORT_MODE tcp Transport protocol: tcp (default, works through most firewalls) or udp

Optional settings (have sensible defaults):

Setting Default Description
POSTGRES_PASSWORD (auto-generated) Auto-generated 24-char random password if empty
MINIO_ROOT_PASSWORD (auto-generated) Auto-generated 24-char random password if empty
OUTBOUND_SEALER_MX_DOMAIN hintest.ch Sealer MX domain for outbound seal delivery
POLICY_SYNC_REPO_URL GitHub HIN Stargate policies Git repo URL for OPA/Rego policy sync
LOKI_URL https://loki.infra.vereign-cdn.com Loki endpoint for centralized log shipping

Auto-generated (do not set manually):

  • VAULT_TOKEN — Generated by Vault during first initialization, saved to customer-config.sh
  • WG_PRIVATE_KEY — Generated by IDAgent on first run, saved to customer-config.sh

Step 2: Deploy to a Server

# Copy files to the server
scp -r docker-compose/* your-server:/path/to/stargate/

# SSH to server
ssh your-server
cd /path/to/stargate

# Create customer config from template
cp customer-config.example customer-config.sh
nano customer-config.sh   # Fill in required settings (see Step 1)

# Run installation
chmod +x scripts/*.sh
./scripts/install.sh

Step 3: What Install Does

The install script (install.sh) performs the following steps:

  1. Check dependencies — Detects Docker, Docker Compose, and jq. If missing, installs them automatically (supports Ubuntu/Debian, RHEL/AlmaLinux/Rocky).
  2. Load and validate customer-config.sh — Checks required fields (SERVER_STATIC_IP, CUSTOMER_NAME, DEPLOYMENT_NAME, MAIL_DOMAINS). Auto-derives optional fields (certificate names, MXEngine URL, etc.).
  3. Generate .env from customer config — Auto-generates passwords if not set.
  4. Start all services via Docker Compose (infrastructure + applications).
  5. Initialize Vault — The vault-init container initializes, unseals, and creates KV-v2 secret mounts. Optionally writes the WireGuard private key to Vault.
  6. Save Vault keys to secrets/vault-keys.json and update .env with the root token. The token is also saved to customer-config.sh for persistence across VM recreations.
  7. Restart application services to pick up the Vault token.
  8. Run initial onboarding (onboard.sh --initial-setup):
  9. Generate S/MIME signing key and CSR (saved to secrets/signing-key.csr)
  10. Set up WireGuard peer connection in the database
  11. If the WireGuard tunnel to the CA is not yet established, CSR submission may fail — this is expected on first install. Services still run; retry with ./scripts/onboard.sh --regenerate-cert once the tunnel is up.
  12. Save WireGuard private key to customer-config.sh — extracted from Vault after IDAgent generates it.
  13. Set up daily backup cron job (runs at 2:00 AM).

Step 4: Onboard Domains (Post-Install)

After initial installation, use onboard.sh to manage domains, certificates, and WireGuard peers:

# Edit customer-config.sh to add/change domains or peer settings
nano customer-config.sh

# Apply changes
./scripts/onboard.sh

What onboard.sh does:

  1. Loads and validates settings from customer-config.sh
  2. Auto-derives certificate fields (CERT_DNS_NAMES, CERT_ORGANIZATION, CERT_COMMON_NAME) the same way install.sh does
  3. Updates .env with current domain, certificate, and WireGuard settings
  4. Generates S/MIME key + CSR (skips if already exists, use --regenerate-cert to force)
  5. The CSR is submitted to the CA via the WireGuard tunnel (90-second timeout)
  6. If submission fails (tunnel not ready), a warning is printed and the script continues — services remain running
  7. Sets up or updates the WireGuard peer connection in the database (runs idagent-init)
  8. Restarts affected services (postfix-relay, mxengine, idagent)

Exit codes:

  • 0 — Everything succeeded
  • 1 — Fatal error (missing config, etc.)
  • 2 — Partial success (services running, but certificate issuance failed — retry later)

Adding a new domain:

  1. Edit customer-config.sh — add domain to MAIL_DOMAINS (comma-separated):
MAIL_DOMAINS="example.com,example.org"
  1. Run ./scripts/onboard.sh
  2. The script updates Postfix routing, regenerates certificate SANs (if auto-derived), and restarts services

Regenerating certificates:

./scripts/onboard.sh --regenerate-cert

Step 5: WireGuard Peer Registration

After installation, the S/MIME certificate issuance will fail if your Stargate instance is not yet registered as a WireGuard peer on the HIN CA side. This is the most common issue during initial setup.

What you need to provide to HIN:

  1. WireGuard public key - extract from idagent logs:
docker compose logs idagent | grep "public key"
  1. DEPLOYMENT_NAME - from your customer-config.sh
  2. SERVER_STATIC_IP - the public IP of your Stargate server
  3. WG_INTERFACE_PORT - only if you changed it from the default 19818

Send these values to HIN so they can register your peer on the CA side.

After HIN confirms your peer is registered:

Restart the services and regenerate the certificate:

./scripts/onboard.sh --regenerate-cert

This restarts services (including idagent), which triggers a new WireGuard handshake. Since the CA now has your keys, the tunnel should establish and the certificate should be issued.

To verify the tunnel before requesting the certificate:

# Restart just idagent
docker compose restart idagent

# Check for successful WireGuard handshake
docker compose logs idagent 2>&1 | grep -i "handshake\|peer"

Also check your firewall: Port 19818/TCP must be open both inbound and outbound on the Stargate server.

Step 6: Post-Onboarding Recommendations

Once the certificate is issued and mail is flowing, two configuration items are strongly recommended for any production deployment. Skipping them does not break encryption, but it will degrade your sender reputation, cause "we can't verify the sender" warnings in Outlook/Gmail, and can eventually lead to outbound mail being blocklisted.

6.1 SPF / DKIM / DMARC for sender domains

The Stargate sends notification mails (and S/MIME-encrypted mails) from its own public IP on behalf of your users. Recipients (Outlook, Gmail, Proofpoint, etc.) authenticate the sender by checking SPF, DKIM and DMARC against the From: domain. If the Stargate IP is not authorized for the sender's domain, recipients will show a yellow "we can't verify this email" banner and may flag the message as suspicious.

For each domain you route through the Stargate, publish the following DNS records:

SPF - authorize the Stargate IP. If your mailboxes also live in M365, keep the Microsoft include:

example.ch.  TXT  "v=spf1 ip4:<STARGATE_PUBLIC_IP> include:spf.protection.outlook.com -all"

If you do not use M365 / Google Workspace, the minimal record is:

example.ch.  TXT  "v=spf1 ip4:<STARGATE_PUBLIC_IP> -all"

DMARC - publish at least a monitoring policy. This alone is enough to clear Outlook's "can't verify" banner once SPF passes:

_dmarc.example.ch.  TXT  "v=DMARC1; p=none; rua=mailto:postmaster@example.ch"

Once you have confirmed alignment in the reports, you can tighten to p=quarantine and eventually p=reject.

DKIM - if example.ch is an accepted domain in your M365 or Google Workspace tenant, enable DKIM signing in the admin centre and publish the two selector1._domainkey / selector2._domainkey CNAMEs as instructed there. Publishing the CNAMEs is not enough on its own - DKIM signing must be toggled on in the tenant.

Verifying: send a test mail to a Gmail or Outlook account, open the message source / "View original", and look for the Authentication-Results: header. You want to see spf=pass, dkim=pass and dmarc=pass.

Useful tools:

By default, after the Stargate signs/encrypts an outbound mail it delivers directly to the recipient's MX. This works, but the connecting IP is your Stargate's IP - and unless that IP has years of warm reputation, it can end up on third-party blocklists (e.g. Barracuda, abusix), causing intermittent delivery failures.

The recommended pattern is to send the signed mail back through your M365 / Exchange tenant so that the final hop to the internet is Microsoft's well-reputed infrastructure. The Stargate still signs and policy-checks every message; only the last hop changes. This mirrors the original HIN MGW "Send to MX" connector pattern.

Stargate side - per-domain relay

In customer-config.sh, point each domain's outbound back at its M365 inbound endpoint:

DOMAIN_RELAY_MAP="example.ch:[example-ch.mail.protection.outlook.com]:25"

Apply:

./scripts/onboard.sh
docker compose up -d postfix-relay

After mxengine signs the mail, Postfix will hand it back to your tenant on port 25 with TLS instead of delivering directly to the recipient's MX. See Exchange-integration.md for the full per-domain syntax.

M365 / Exchange Online side

You essentially recreate the same connector + transport-rule set as the old HIN MGW (the original HIN MGW O365 manual is the reference - the same five rules apply). The minimum is:

  1. Inbound connector - accept mail from the Stargate, identified by TLS certificate (the cert subject must match a domain accepted in your tenant). A self-signed cert on the Stargate will be rejected by this connector - use a valid CA-issued cert (Let's Encrypt is fine).
  2. Outbound connector "Send to MX" - delivers to the recipient's MX, activated only by transport rule.
  3. Transport rule set_header - tags outbound mail with a header like outgoing: outgoing_<domain> before it leaves O365 the first time, so the return trip can recognise it.
  4. Transport rule outgoing_to_mx - matches the outgoing_<domain> header on mail coming back from the Stargate and routes it via the "Send to MX" connector.
  5. Transport rule mgw_bypass_antispam - bypasses spam filtering on mail coming back from the Stargate.

mxengine does not strip arbitrary headers, so the outgoing_<domain> tag set by set_header survives the round-trip and triggers outgoing_to_mx correctly.

Why this pattern matters: with the relay-back configuration, the public sender to the internet is Microsoft. Combined with correct SPF/DKIM/DMARC (section 6.1), recipients see a Microsoft IP with spf=pass and dkim=pass aligned to your domain - which is the cleanest reputation profile you can give them.

See Exchange-integration.md for full step-by-step instructions including screenshots.

Subsequent Starts (after reboot)

./scripts/start.sh

The start script:

  1. Starts infrastructure services
  2. Unseals Vault using stored keys
  3. Starts application services

Stop Services

./scripts/stop.sh

This stops containers but preserves all data.

Data Persistence

All data is stored in Docker volumes and persists across restarts.

Service Volume Data
PostgreSQL postgres_data All databases (smimekeys, policy, idagent, mxengine)
Vault vault_data Encryption keys, secrets, S/MIME keys
MinIO minio_data Object storage (messages, attachments)
Postfix postfix_spool Mail queue

Safe Operations (data preserved)

# Stop and start - data safe
./scripts/stop.sh
./scripts/start.sh

# Or using docker compose directly
docker compose down      # Stops containers, KEEPS volumes
docker compose up -d     # Restarts containers
./scripts/start.sh       # Unseals Vault

Vault Sealing Behavior

Vault becomes sealed when its container restarts. This is a security feature.

After any restart, run ./scripts/start.sh to unseal Vault. The script uses the keys stored in secrets/vault-keys.json.

Destructive Operations (data deleted)

These commands DELETE ALL DATA - use with caution:

# Delete everything (volumes, secrets, config)
./scripts/stop.sh --purge

# Or manually remove volumes
docker compose down -v   # The -v flag removes volumes!

Scripts Reference

Script Purpose
install.sh First-time installation (Docker, Vault, then calls onboard.sh)
onboard.sh Domain onboarding (S/MIME key, WireGuard peer, mail domains, service restart)
start.sh Start services and unseal Vault
stop.sh Stop containers (data preserved)
backup.sh Full backup (database, Vault keys, config, certificates)
restore.sh Restore from backup archive (works on fresh machine)
purge.sh Delete ALL data (requires confirmation)
health-check.sh Comprehensive health check of all services (exit 0 = healthy, 1 = failures)
init-vault.sh Vault initialization (used by vault-init container, not called directly)
init-idagent.sh WireGuard peer connection setup (used by idagent-init container, not called directly)
gather-app-versions.sh Collects app versions from /liveness endpoints for node-exporter (runs in version-collector container)

Configuration Files

File Purpose
customer-config.example Template for customer settings (copy to customer-config.sh)
customer-config.sh Customer-specific settings (created from template, fill in before install)
.env Generated environment file (created by install.sh, updated by onboard.sh)
secrets/vault-keys.json Vault unseal keys and root token (back up securely!)
secrets/signing-key.csr Generated CSR for S/MIME certificate