Skip to content

Hub — install procedure

The Hub is the customer-facing Cyphers appliance: one Rust binary running your certificate authority, ACME client, Cyphers Score engine, renewal lifecycle, and dashboard, against your own PostgreSQL database. There is exactly one runtime role — see Hub — overview for what it does.

This page covers the full install: prerequisites, step-by-step setup (the packaged Docker path first, then the packaged bare-tarball path), the complete environment variable reference, the Scout↔Hub NATS channel, first-boot CA self-provisioning, and troubleshooting.

Prerequisites

Requirement Specification
OS Linux x86_64 — the platform the Hub and Scout binaries are built and packaged for.
Hardware No hard minimum; the packaged quick-start guide's baseline for the Hub host is 2+ CPU, 2GB+ RAM.
Database PostgreSQL 16+ — the sole operational database. docker/docker-compose.package.yml (and the source-tree docker/docker-compose.yml) run postgres:16-alpine.
Docker (packaged Docker path) Docker Engine 24+ and Docker Compose v2.
No Docker (packaged bare-tarball path) A systemd-capable Linux host and root access — install.sh installs static binaries with no compiler toolchain or runtime dependency.
Network, if you use public ACME DNS-01 Outbound HTTPS to your DNS provider's API (route53.amazonaws.com, api.cloudflare.com, or dns.googleapis.com + oauth2.googleapis.com) and outbound DNS on port 53/UDP+TCP to internet nameservers — the Hub verifies challenge propagation at the zone's own authoritative servers, not through a resolver. See ACME / DNS-01. Otherwise no outbound dependency — the built-in private CA and ACME HTTP-01 need none.

Step-by-step install

Both packages below come from the same build — scripts/package-release.sh produces dist/cyphers-ng-<version>-linux-amd64-{bare,docker}.tar.gz in one run, so the bare package's binaries and the Docker package's images can never drift from each other (see docs/agent/packaging.md). Both include the same quick-start guide (QUICK-START.md).

Docker install (packaged, primary path)

  1. Load the images and create a config directory:
tar xzf cyphers-ng-<version>-linux-amd64-docker.tar.gz
cd cyphers-ng-<version>-linux-amd64-docker
docker load < docker/cyphers-hub.tar
docker load < docker/scout.tar

mkdir -p /opt/cyphers
cp .env.example /opt/cyphers/.env
cp docker-compose.yml /opt/cyphers/
  1. Edit /opt/cyphers/.env. The Compose file :?-guards these — the whole docker compose up -d refuses to start without them:
CYPHERS_CA_KEY_PASSPHRASE=<strong passphrase>
ADMIN_PASSWORD=<strong initial admin password>
CYPHERS_CA_URL=https://your-hub-host:8443
CYPHERS_NATS_ADVERTISED_HOST=<hostname(s) Scouts will dial NATS on>

CYPHERS_CA_URL and CYPHERS_NATS_ADVERTISED_HOST are :?-guarded on both the hub and nats-bootstrap services; ADMIN_PASSWORD only on hub; CYPHERS_CA_KEY_PASSPHRASE only on nats-bootstrap — but since hub won't start until nats-bootstrap completes successfully, the stack as a whole still refuses to boot without it (hub's own reference to it has a softer ${...:-changeme} fallback that never gets a chance to apply here).

Also set a real database password. DB_PASSWORD is not :?-guarded anywhere in this file — nats-bootstrap, hub, and postgres all fall back to the literal password cyphers if it's unset, so Compose will boot happily on a well-known default:

DB_PASSWORD=<strong password>

CYPHERS_CA_URL is baked directly into every certificate the Hub issues, as the CDP/AIA pointer relying parties use to fetch the CRL and CA chain — it must be the URL relying parties can actually reach. CYPHERS_CA_DIR defaults to /var/lib/cyphers/ca-keys inside the container; leave it unless you've changed the volume layout.

  1. Start the stack:
cd /opt/cyphers
docker compose up -d

This starts, in dependency order: PostgreSQL; a one-shot nats-bootstrap service that mints the NATS mTLS material and initial ACL conf from the Hub's own CA (cyphers-hub nats-bootstrap), baking CYPHERS_NATS_ADVERTISED_HOST into the NATS server cert's SANs; the NATS bus itself, once bootstrap completes; and the Hub. Migrations run automatically — hub/entrypoint.sh runs cyphers-hub wait-db, then cyphers-hub doctor --apply-migrations with visible pass/fail output, then — because ADMIN_PASSWORD is set — cyphers-hub create-admin (idempotent; a pre-existing admin is left alone), and finally execs the Hub server.

  1. Confirm liveness, then readiness:
curl http://localhost:8443/health     # liveness only — expect: ok
curl http://localhost:8443/readyz     # deployment readiness — see below

/health is intentionally liveness-only. /readyz reports four checks — database, migrations, ca_storage, crypto_posture — and answers 503 if any fails:

{
  "overall": "ok",
  "database": { "status": "ok" },
  "migrations": { "status": "ok" },
  "ca_storage": { "status": "ok" },
  "crypto_posture": { "status": "ok" }
}
  1. Log in to the dashboard on the mapped UI port (HUB_UI_PORT, default 443) with the admin credentials from step 2.

The source-tree Compose file is a different, dev/production-like stack

If you're building from a repo checkout rather than installing a packaged release, docker/docker-compose.yml at the repo root is the equivalent single-role stack — Hub + PostgreSQL, with an optional --profile with-scout that adds a NATS bus and a Scout agent for the Hub's own host. It is not the same file as the packaged docker-compose.yml above (built from docker/docker-compose.package.yml, which always runs NATS and mints its mTLS material through nats-bootstrap — see docs/agent/packaging.md).

Bare tarball install (packaged, no Docker)

tar xzf cyphers-ng-<version>-linux-amd64-bare.tar.gz
cd cyphers-ng-<version>-linux-amd64-bare
sudo ./install.sh

install.sh:

  1. Creates a system user cyphers (no login shell).
  2. Installs cyphers-hub and scout to /opt/cyphers/, plus the Scout installer generator (/opt/cyphers/installer/build.sh).
  3. Writes /opt/cyphers/.env from the bundled .env.example only if one doesn't already exist (safe to re-run for upgrades).
  4. Creates /opt/cyphers/ca-keys/ (mode 0700).
  5. Writes the cyphers-hub.service unit (systemd, EnvironmentFile= /opt/cyphers/.env) and runs systemctl daemon-reload — enabling and starting it is a separate step, shown next.

Then, per the script's own printed next steps:

# Edit /opt/cyphers/.env first (CYPHERS_CA_KEY_PASSPHRASE, DATABASE_URL,
# CYPHERS_CA_URL, at minimum), then:
set -a; source /opt/cyphers/.env; set +a
/opt/cyphers/cyphers-hub doctor --strict --apply-migrations
sudo systemctl enable --now cyphers-hub

Unlike the Docker path, the bare install does not auto-create an admin user — run this yourself after the service is up (the same subcommand hub/entrypoint.sh uses under the hood for the Docker image):

/opt/cyphers/cyphers-hub create-admin --username admin --password <password>

Full environment variable reference

Every var below is read by the Hub process (hub/src/main.rs, hub/src/config/mod.rs) or is a Docker Compose passthrough. Required-with- no-default vars are marked; everything else has a working default. See .env.example in either package for the complete, always-current list.

Core Hub

Var Required Meaning
DATABASE_URL required PostgreSQL connection string.
CYPHERS_CA_KEY_PASSPHRASE strongly recommended Encrypts CA private keys at rest. Unset falls back to the literal string cyphers-dev with a logged warning — never use the default outside local dev.
CYPHERS_CA_DIR Directory for CA key material (default ./ca-keys; the packaged Docker image sets /var/lib/cyphers/ca-keys, the bare install /opt/cyphers/ca-keys).
CYPHERS_CA_URL required This Hub's own externally-reachable PKI base URL (the Scout REST port). The Hub refuses to start without it — embedded as the CRL/OCSP pointer in every issued cert. Exact refusal text: CYPHERS_CA_URL / --ca-url is required: set it to the externally reachable base URL of this Hub's PKI endpoints (the scout REST port)....
UI_PORT / SCOUT_REST_PORT / ENROLL_PORT Direct process listen ports — defaults 443 / 8443 / 7443 (--ui-port, --scout-rest-port, --enroll-port).
ADMIN_USERNAME / ADMIN_PASSWORD ADMIN_PASSWORD required for the Docker package Initial admin bootstrap, consumed by hub/entrypoint.sh (Docker path only).
--installation-config (flag, not an env var) Path to the packaged installation file, conventionally /etc/cyphers/installation.toml. The only authority for crypto mode; standard mode is the default and needs no configuration. Setting CYPHERS_FIPS_MODE, CYPHERS_FIPS or CYPHERS_FIPS_CMVP_CERTIFICATE — even to false — now aborts startup; remove them.
SESSION_TTL_HOURS Admin session lifetime, default 8.
APP_SECRET / BADGE_HMAC_SECRET Enrollment-token signing secret / Cyphers Score CYP verification code HMAC secret — change both in production.
AGENT_STALENESS_HOURS — (disabled unless set) Hours of no Scout contact before an agent is flagged stale and auto-disabled (destructive: also revokes its certificates).
CYPHERS_SCOUT_DARK_ALERT_HOURS — (default 2) Hours of Scout silence before the always-on "gone dark" warning fires on the activity feed. Independent of AGENT_STALENESS_HOURS — this alert never disables or revokes anything; see Monitoring → Scout-went-dark alerts.

Database

Var Required Meaning
DATABASE_URL required See above.
DB_PASSWORD Used to build the default DATABASE_URL in the packaged Docker Compose file.

ACME / DNS-01

There is no delegation service — the Hub talks directly to your DNS provider (see Hub — overview).

The Hub makes two kinds of DNS lookup, and they no longer resolve the same way:

  • CAA pre-flight uses the host's configured system resolvers, so split-horizon and egress-filtered networks resolve exactly as the host does.
  • DNS-01 TXT pre-verification — both the automatic lane's propagation wait and the manual lane's Verify check — resolves the challenge zone's NS records and then queries those authoritative nameservers directly, with caching disabled. It falls back to the system resolver only when the zone's nameservers cannot be determined.

⚠️ That second one needs outbound DNS on port 53 to arbitrary internet nameservers. A network that only permits DNS to an internal forwarder makes the check fall back to that forwarder's cache — which, on a record with a 120-second TTL, can serve a stale challenge value for longer than the lane's 35-second budget and fail a renewal that would have succeeded. This is not hypothetical; it cost a live renewal on 2026-08-22.

Var Required Meaning
ACME_EMAIL — (enables ACME) Contact address for the Hub's env-configured ACME account (Let's Encrypt).
ACME_STAGING Use Let's Encrypt's staging environment.
CLOUDFLARE_API_TOKEN / CLOUDFLARE_ZONE_ID Cloudflare DNS-01 credentials, and the only DNS provider configurable from the environment. Route 53 and Google Cloud DNS are supported too (hub/src/acme/dns_providers.rs) but exclusively through the dashboard's DB-persisted provider config — there is no env-var path for either.
CYPHERS_HUB_ACME_DOMAINS Comma-separated domains the Hub pursues ACME issuance for.

These env vars configure a Let's Encrypt account for env-driven issuance only. Each renewal profile independently picks its own certificate provider — Let's Encrypt (the default for a profile that doesn't set one, for compatibility with profiles created before multi-provider support), ZeroSSL, Google Trust Services, either SSL.com key-family row, or a custom-registered RFC 8555 CA — from Settings → Certificate Providers in the dashboard, independently of these env vars. A profile's staging: true only works against a provider that publishes a staging directory (Let's Encrypt, among the built-ins). See Hub — certificate providers for the full provider model, EAB credentials, and CAA guidance.

Scout↔Hub NATS

See NATS below for what this channel is for.

Var Required Meaning
CYPHERS_NATS_ADVERTISED_HOST required for the packaged Docker install (:?-guarded, unconditionally); needed for a bare/native install once a Scout fleet is deployed One or more comma-separated hostnames baked into the NATS server cert's SANs by the one-shot nats-bootstrap service. A list lets you relocate NATS without downtime.
CYPHERS_SCOUT_NATS_URL required for a Scout fleet on a bare/native install The Hub's own NATS URL. Leave empty and the Hub simply doesn't dial NATS — otherwise it fails closed on NATS connection errors. Fixed automatically in the packaged Docker install (nats://nats:4222) — nothing to set.
CYPHERS_SCOUT_NATS_CLIENT_CERT / _CLIENT_KEY / _CA required for a Scout fleet on a bare/native install The Hub's NATS client mTLS material. Fixed automatically in the packaged Docker install (minted by nats-bootstrap and mounted from the shared nats-tls volume) — nothing to set.
CYPHERS_NATS_SERVER_CONF_PATH Path to the nats-server.conf the Hub regenerates on every Scout enroll/decommission (per-agent ACL provisioning).

NATS

Once at least one Scout is deployed, all interactive Scout↔Hub traffic — commands, reports, acks, heartbeats, CSRs — runs over one mutually- authenticated NATS channel. The Hub never dials NATS at all unless CYPHERS_SCOUT_NATS_URL is set. The packaged Docker install brings its own NATS bus up automatically (mTLS material self-minted by nats-bootstrap from the Hub's CA); a bare/native install needs its own NATS deployment pointed at by the same env vars. Full wire-level detail — subjects, per-agent ACLs, acceptance-policy verification — lives on the Scout side: Transport & health.

The Hub's own enrollment-listener certificate SANs (as opposed to the NATS server cert) can also be updated via a DB-persisted setting, without editing .env:

cyphers-hub set-advertised-hosts "host-a.example,host-b.example"

This takes effect only on the Hub's next restart, and never reaches the NATS server certificate — that one is controlled solely by CYPHERS_NATS_ADVERTISED_HOST in the Hub's own process environment.

First-boot CA self-provisioning

CYPHERS_CA_URL being mandatory (above) is about the Hub's own built-in CA — the CA that signs Scout enrollment certificates, internal-CA issuance, and NATS mTLS material for this Hub's fleet.

ca::builtin::init_persistent (hub/src/ca/builtin.rs) runs this decision tree on every boot:

  1. Filesystem has keys already (CYPHERS_CA_DIR contains root_key.pem/intermediate_key.pem etc.) → load from disk, sync to the DB if needed.
  2. Else, the database has keys (e.g. the filesystem volume was recreated) → restore them to the filesystem, then load.
  3. Else — first boot → generate a new root + intermediate CA, persist to both the filesystem and the database.
  4. Ceremony mode (a ca-manifest.json is present, meaning this Hub was deliberately pointed at offline-root ceremony artifacts produced by the cyphers-ca tool in ca-tool/) — the Hub never holds the root private key; not the default install path and not covered in full here.

Passphrase handling: --ca-passphrase / CYPHERS_CA_KEY_PASSPHRASE decrypts the private key files. If unset, the Hub falls back to the literal string cyphers-dev and logs a warning — this is fine for a throwaway local smoke-test, never for a real install. Rotate it later with:

cyphers-hub rotate-ca-passphrase --new-passphrase-file <path>

Troubleshooting

Health check times out (curl http://localhost:8443/health): the Hub isn't running. Check systemctl status cyphers-hub or docker compose logs hub.

/readyz returns overall: degraded: read the failed check in the JSON body — database, migrations, ca_storage, or crypto_posture — each carries its own detail.

Doctor reports FAIL database_connectivity: verify DATABASE_URL is correct and the database is reachable.

Doctor reports FAIL ca_passphrase: CYPHERS_CA_KEY_PASSPHRASE is missing or empty in .env.

Hub won't start, complaining CYPHERS_CA_URL / --ca-url is required: expected and intentional (hub/src/main.rs) — set it to this Hub's own externally-reachable PKI base URL before starting.

docker compose up -d refuses to start, citing CYPHERS_NATS_ADVERTISED_HOST: expected — the packaged Compose file :?-guards it on both the nats-bootstrap and hub services once you intend to run a Scout fleet. Set it even if you haven't deployed a Scout yet.

Admin login fails right after a Docker install: check docker compose logs hub for the create-admin line from hub/entrypoint.sh — a pre-existing admin user (e.g. from a prior install against the same volume) makes bootstrap a no-op, which is expected; use the original credentials, or create a new admin manually via cyphers-hub create-admin.

A Scout fails to enroll: the enrollment token may have expired (1 hour), the target hostname may not be one of the enrollment cert's SANs (CYPHERS_NATS_ADVERTISED_HOST), or the installer/command may be missing the mandatory --policy-signer-fp — see Scout → Install procedure and Scout → Security.