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)¶
- 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/
- Edit
/opt/cyphers/.env. The Compose file:?-guards these — the wholedocker compose up -drefuses 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:
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.
- Start the stack:
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.
- 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" }
}
- Log in to the dashboard on the mapped UI port (
HUB_UI_PORT, default443) 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:
- Creates a system user
cyphers(no login shell). - Installs
cyphers-hubandscoutto/opt/cyphers/, plus the Scout installer generator (/opt/cyphers/installer/build.sh). - Writes
/opt/cyphers/.envfrom the bundled.env.exampleonly if one doesn't already exist (safe to re-run for upgrades). - Creates
/opt/cyphers/ca-keys/(mode0700). - Writes the
cyphers-hub.serviceunit (systemd,EnvironmentFile= /opt/cyphers/.env) and runssystemctl 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):
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
NSrecords 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:
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:
- Filesystem has keys already (
CYPHERS_CA_DIRcontainsroot_key.pem/intermediate_key.pemetc.) → load from disk, sync to the DB if needed. - Else, the database has keys (e.g. the filesystem volume was recreated) → restore them to the filesystem, then load.
- Else — first boot → generate a new root + intermediate CA, persist to both the filesystem and the database.
- Ceremony mode (a
ca-manifest.jsonis present, meaning this Hub was deliberately pointed at offline-root ceremony artifacts produced by thecyphers-catool inca-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:
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.