Skip to content

Satellite Hub — install procedure

A Satellite Hub is the customer-facing Cyphers appliance: local certificate lifecycle management (CA, ACME, scoring, renewal, dashboard, Scout fleet) against your own PostgreSQL database, plus an optional but standard mTLS query path to a Master Hub for Certificate Transparency intelligence. See Hub — overview for how Satellite and Master relate.

This page covers the full install: the Master-side dependency you need before you start, prerequisites, step-by-step setup (Docker Compose first, then the packaged bare-tarball path), the complete satellite env var reference, first-boot CA self-provisioning, and the validation gate that proves the install actually works before you rely on it.

The Master dependency

A Satellite Hub has two hard dependencies before it can prove Master-backed CT intelligence works: the Master Hub itself must be reachable, and you need a Master-issued mTLS client bundle. Get both sorted before you start the Satellite install — the Hub will boot and run its local certificate lifecycle without them, but CT intelligence stays degraded/unreachable until they're in place.

1. A reachable Master Hub

The Satellite talks to the Master over CYPHERS_CT_MASTER_URL. This must be the Master's CT Intel base URL — the same listener that serves the dashboard UI — not the machine REST/health listener. Both listeners exist on every Hub (Satellite or Master) and are easy to confuse:

Listener Default port Serves
UI / CT Intel (--ui-port) 443 Dashboard, login, and — on a Master/api/ct/intel/query, /api/ct/intel/health
Scout REST / machine health (--scout-rest-port) 8443 /health, /api/ca/* (public PKI reads)

CYPHERS_CT_INTEL_API is accepted as a legacy alias for CYPHERS_CT_MASTER_URL — prefer the canonical name for new installs.

The single most common satellite misconfiguration

Pointing CYPHERS_CT_MASTER_URL at the Master's :8443 machine REST/health port. /health will answer ok there, which looks like success — but that port does not serve CT Intel query routes at all, so every CT query fails. Use the Master's UI/CT-Intel port (:443 by default).

2. The CT Intel mTLS client bundle

CT Intel access is mTLS-only for a real Satellite validation shape — an API key is a transitional/emergency credential, not a valid standing Satellite auth mode (see docs/_curated/08-PREFLIGHT-AND-VALIDATION.md). You need three PEM files before you can complete the install:

ca.crt        # the Master's client-auth CA (verifies the Master's server cert)
client.crt    # this Satellite's mTLS client certificate
client.key    # this Satellite's private key — secret material

These map directly to CYPHERS_CT_INTEL_CA_CERT, CYPHERS_CT_INTEL_CLIENT_CERT, and CYPHERS_CT_INTEL_CLIENT_KEY (paths, not inline PEM).

Where this bundle comes from — the current, honestly-manual procedure. There is no self-service UI for this yet. The client-auth CA behind Master↔ Satellite CT Intel mTLS is a separate, offline-root Cyphers service CA — distinct from each Hub's own built-in CA (see First-boot CA self-provisioning below, which is a different CA entirely). An operator with access to that service CA's ceremony directory issues your bundle in two steps:

  1. The ceremony directory exists once, created ahead of time with the cyphers-ca ceremony tool (ca-tool/ in this repo):
cargo run --manifest-path ca-tool/Cargo.toml -- init \
  --cn "Cyphers Service Root CA" --org Cyphers \
  --intermediate-cn "Cyphers Service mTLS Intermediate" \
  --name-constraints "example.com" \
  --aia-base "https://master.example.com" \
  --operator "<operator name>" \
  --output /path/to/ceremony

This writes root.pem, intermediate.pem, and intermediate-key.pem (passphrase-encrypted) into the ceremony directory. This step happens once, offline, by whoever operates the Master — not per Satellite.

  1. Each Satellite/customer gets its own leaf bundle, issued from that ceremony directory with scripts/issue-master-lab-certs.sh:
CYPHERS_SERVICE_CA_DIR=/path/to/ceremony \
CYPHERS_MASTER_CERT_CN=cyphers-master \
CYPHERS_MASTER_DNS_NAMES=master.example.com \
CYPHERS_REMOTE_HUB_CLIENT_CN=my-satellite \
  scripts/issue-master-lab-certs.sh

The script prompts for the ceremony's intermediate-key passphrase, decrypts it only into a temporary file it deletes on exit (it never reads or copies the root private key), and writes two bundles:

  • master/server-chain.crt, server.key, client-auth-ca.crt — deployed on the Master, wired in via the docker-compose.master.tls-lab.yml overlay's CYPHERS_LIVE_MASTER_TLS_DIR mount.
  • remote-hub/client.crt, client.key, ca.crt — this is your Satellite's bundle. Transport remote-hub/{ca.crt,client.crt,client.key} out of band (not email, not this repo) to the Satellite host.

This is the real, current procedure — not a placeholder

scripts/issue-master-lab-certs.sh is named for the lab/Early-Adopter validation path that exercises it today; there is no separate "production" issuance tool in this repo. Treat it as the operational procedure until a self-service bundle-issuance flow ships. One bundle per Satellite — never reuse a client key across customers or environments. Rotate before client.crt expiry (openssl x509 -in client.crt -noout -enddate to check) and revoke stale/compromised bundles in the Master's trust records before deleting local copies.

Place the three files for your Satellite in a directory outside version control — the Docker Compose path below expects ./deploy/live-master/{ca.crt,client.crt,client.key} by default (override with CYPHERS_LIVE_MASTER_SECRET_DIR); a native/bare install can point the three CYPHERS_CT_INTEL_* env vars at whatever paths you choose. client.key is secret material — mode 0600, read-only mount into containers.

Prerequisites

Requirement Specification
OS Linux x86_64 — the platform the Hub and Scout binaries are built and packaged for (docker/docker-compose.satellite.yml builds/runs linux/amd64 images; installer/install-hub.sh installs bin/cyphers-hub + bin/scout for the same).
Hardware No published minimum specific to Satellite; the packaged quick-start guide's baseline for the Hub host is 2+ CPU, 2GB+ RAM.
Database PostgreSQL 16+ — the sole operational database for a Satellite. docker/docker-compose.satellite.yml runs postgres:16-alpine.
ClickHouse Not required. docker/docker-compose.satellite.yml defines no ClickHouse service at all — ClickHouse is Master-only. Do not set CLICKHOUSE_URL on a Satellite; docs/getting-started/master-hub-ct-connection.md calls a locally-configured CLICKHOUSE_URL on a Satellite a validation-shape error, not an optional enhancement.
Docker (Compose path) Docker Engine + Docker Compose v2, if you're using docker/docker-compose.satellite.yml (the primary path below).
No Docker (bare-tarball path) A systemd-capable Linux host and root access — installer/install-hub.sh installs static binaries with no compiler toolchain or runtime dependency.
Master reachability + CT Intel mTLS bundle See The Master dependency above — required before CT intelligence works, not before the Hub boots.

Step-by-step install

Docker Compose (primary path)

  1. Get the CT Intel bundle in place (see above):
mkdir -p ./deploy/live-master
cp /path/to/remote-hub/{ca.crt,client.crt,client.key} ./deploy/live-master/
chmod 600 ./deploy/live-master/client.key
  1. Create .env next to docker/docker-compose.satellite.yml with, at minimum, the variables the compose file marks required (:? in the file — Compose refuses to start without them):
DB_PASSWORD=<strong password>
CYPHERS_CA_KEY_PASSPHRASE=<strong passphrase>
CYPHERS_CA_URL=https://your-satellite-host:8443
ADMIN_PASSWORD=<strong initial admin password>
CYPHERS_CT_MASTER_URL=https://your-master-host:443

CYPHERS_CA_URL is the Satellite's own externally-reachable PKI base URL (its Scout REST port) — not the Master's URL. It gets embedded as the CRL/OCSP pointer in every certificate this Satellite issues, so the Hub refuses to start without it (hub/src/main.rs).

  1. Start the stack:
docker compose -f docker/docker-compose.satellite.yml up -d

On boot, hub/entrypoint.sh runs, in order: cyphers-hub wait-db (blocks until Postgres answers), cyphers-hub doctor --apply-migrations (applies pending migrations with visible pass/fail output), then — because ADMIN_PASSWORD is set — cyphers-hub create-admin --username "$ADMIN_USERNAME" --password "$ADMIN_PASSWORD" (idempotent; a pre-existing admin is left alone), and finally execs the Hub server itself.

  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; it does not prove database migrations, CA storage, or Master CT connectivity are actually healthy. Use /readyz for that — expected shape for a deployable Satellite:

{
  "overall": "ok",
  "database": { "status": "ok" },
  "migrations": { "status": "ok" },
  "ct_role_policy": { "status": "ok" },
  "ct_provider": { "status": "ok" },
  "master_ct_connectivity": { "status": "ok" },
  "ca_storage": { "status": "ok" }
}

If master_ct_connectivity is degraded, work through Troubleshooting below before anything else.

  1. Log in to the dashboard on the mapped UI port (HUB_UI_PORT, default 443) with the admin credentials from step 2.

Adding a local Scout agent to the Satellite host itself is optional and profile-gated (--profile with-scout starts the nats and scout services in the same compose file) — it is not required for CT intelligence to work, which is a Hub↔Master path independent of any Scout.

Packaged bare-tarball path

scripts/package-release.sh builds dist/cyphers-ng-<version>-linux-amd64-bare.tar.gz, containing static cyphers-hub / scout binaries, install.sh (renamed from installer/install-hub.sh), and .env.example — which already ships satellite-shaped: CYPHERS_ROLE=satellite, CYPHERS_CT_ENABLED=true, CYPHERS_CT_INGEST=false, CYPHERS_CT_PROVIDER=cyphers are set (not commented out) in the repo's own .env.example. This is the Early Adopter default posture, not an advanced opt-in.

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) — edit it next to fill in CYPHERS_CA_KEY_PASSPHRASE, DATABASE_URL, CYPHERS_CA_URL, and the four CYPHERS_CT_INTEL_* / CYPHERS_CT_MASTER_URL values from The Master dependency.
  4. Creates /opt/cyphers/ca-keys/ (mode 0700).
  5. Installs and enables cyphers-hub.service (systemd, EnvironmentFile= /opt/cyphers/.env).

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

# Edit /opt/cyphers/.env first, 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 cyphers-hub create-admin --username admin --password <password> yourself after the service is up (same subcommand hub/entrypoint.sh uses under the hood for the Docker image).

Full satellite env var reference

Every var below is read by the Hub process (hub/src/config/mod.rs, hub/src/main.rs) or is a Docker Compose passthrough in docker/docker-compose.satellite.yml. Required-with-no-default vars are marked; everything else has a working default.

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 (hub/src/main.rs) — never use the default outside local dev.
CYPHERS_CA_DIR Directory for CA key material (default ./ca-keys; Docker images typically set /var/lib/cyphers/ca-keys).
CYPHERS_CA_URL required This Satellite's own externally-reachable PKI base URL (Scout REST port). The Hub refuses to start without it — embedded as the CRL/OCSP pointer in every issued cert.
ADMIN_USERNAME / ADMIN_PASSWORD ADMIN_PASSWORD required for the Docker package Initial admin bootstrap, consumed by hub/entrypoint.sh (Docker path only).
CYPHERS_FIPS_MODE false by default; strict FIPS needs a validated OpenSSL FIPS Provider module — see the repo root CLAUDE.md Critical Safety Rules.
CYPHERS_SEARCH_SESSION_STORE postgres for a real deployment (in_memory is dev/test only).

CT intelligence (what makes it a Satellite)

Var Required Meaning
CYPHERS_ROLE satellite (also the Hub's default if unset/invalid — hub/src/config/mod.rs). Legacy: CYPHERS_MASTER_MODE=false is equivalent.
CYPHERS_CT_ENABLED true to turn CT intelligence on at all.
CYPHERS_CT_INGEST Must be false on a Satellite. true triggers local CT-log ingestion, which is the Master's job.
CYPHERS_CT_PROVIDER Must be cyphers for a valid Satellite shape — queries the Master's CT Intel API. Public providers (crtsh, rust_crtsh, scout_crtsh, certspotter) are for internal-dev overrides only, never a customer/baseline Satellite.
CYPHERS_CT_MASTER_URL required (if CT enabled) The Master's CT Intel base URL — see The Master dependency. CYPHERS_CT_INTEL_API is the legacy alias.
CYPHERS_CT_INTEL_API_KEY Leave empty for a valid Satellite shape — API-key-only auth is not acceptable for baseline/customer validation.
CYPHERS_CT_INTEL_CLIENT_CERT required (mTLS) Path to this Satellite's client cert PEM.
CYPHERS_CT_INTEL_CLIENT_KEY required (mTLS) Path to this Satellite's client key PEM.
CYPHERS_CT_INTEL_CA_CERT required (mTLS) Path to the Master's client-auth CA PEM.
CYPHERS_CT_POLL_INTERVAL_SECS Hub default 900; the satellite Compose file's own default (via CT_POLL_INTERVAL) is 300.
CYPHERS_CT_BATCH_LIMIT Hub default 200; the satellite Compose file's own default (via CT_BATCH_LIMIT) is 10000.

Scout↔Hub NATS (only if this Satellite runs its own Scout fleet)

Var Required Meaning
CYPHERS_SCOUT_NATS_URL required for Scout fleet The Satellite's internal NATS URL for Scout command/report traffic. Leave empty and the Hub simply doesn't dial NATS (compose comment: "otherwise Hub fails closed on NATS connection errors").
CYPHERS_SCOUT_NATS_CLIENT_CERT / _CLIENT_KEY / _CA required for Scout fleet Hub-side NATS mTLS material.

This is unrelated to the Master CT Intel mTLS bundle above — it's the Satellite's local Hub↔Scout channel for endpoints it manages directly, not anything the Master is involved in.

First-boot CA self-provisioning

CYPHERS_CA_URL being mandatory (above) is about the Satellite's own built-in CA — a completely different CA from the "Cyphers service CA" ceremony used for the Master CT Intel bundle. This is the CA that signs Scout enrollment certificates, internal-CA issuance, and NATS mTLS material for this Satellite's own 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 ceremony artifacts instead) — a different, offline- root path; not the default Satellite shape and not covered 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>

Nothing about this self-provisioned CA needs the Master. It is entirely local to the Satellite and exists independently of whether CT intelligence or a Master connection is configured at all.

Validation preflight — the acceptance gate

Do not treat a running /readyz: ok as proof that Master CT intelligence actually works end to end. The canonical acceptance gate is scripts/validation-preflight.sh --mode satellite, documented in full in docs/_curated/08-PREFLIGHT-AND-VALIDATION.md and the repo root CLAUDE.md ("Discover Validation Gate"). Run it before creating any Discover session or claiming current inventory / Master CT coverage / fresh-scan correctness:

export CYPHERS_ROLE=satellite
export CYPHERS_CT_ENABLED=true
export CYPHERS_CT_INGEST=false
export CYPHERS_CT_PROVIDER=cyphers
export CYPHERS_CT_MASTER_URL=https://your-master-host:443
export CYPHERS_CT_INTEL_CA_CERT=./deploy/live-master/ca.crt
export CYPHERS_CT_INTEL_CLIENT_CERT=./deploy/live-master/client.crt
export CYPHERS_CT_INTEL_CLIENT_KEY=./deploy/live-master/client.key
unset CLICKHOUSE_URL

scripts/validation-preflight.sh --mode satellite

This proves: the runtime shape is a real Satellite (not direct public CT), Master CT auth is mTLS, CYPHERS_CT_MASTER_URL is the CT Intel base URL and not the machine REST/health listener, Master CT health reports ClickHouse and scanning healthy, an authenticated Master CT query returns a real fingerprint event, and the Hub-side one-shot Scout probe engine can perform a live TLS scan. It does not prove a long-running endpoint Scout agent can report monitoring/renewal/remediation results — for that, add --require-scout-agent (extra env requirements are listed in docs/_curated/08-PREFLIGHT-AND-VALIDATION.md §4).

If preflight fails or wasn't run, say the install is local/UI-only or blocked — do not claim Master CT coverage, current inventory, or a valid fresh scan (docs/_curated/08-PREFLIGHT-AND-VALIDATION.md §8 has the exact required phrasing per failure mode).

Troubleshooting

mTLS handshake failures to the Master (master_ct_connectivity degraded on /readyz, or CT queries erroring):

  • Check the three paths resolve inside the Hub's runtime — Docker mounts CYPHERS_LIVE_MASTER_SECRET_DIR (default ./deploy/live-master) read-only at /run/secrets/cyphers/live-master, and the three CYPHERS_CT_INTEL_* vars must point inside that mount, not at the host path.
  • Confirm the client cert and key actually match (openssl x509 -noout -modulus -in client.crt | openssl md5 vs. openssl rsa -noout -modulus -in client.key | openssl md5 — or the EC equivalent).
  • Confirm client.crt hasn't expired (openssl x509 -in client.crt -noout -enddate) — bundles from scripts/issue-master-lab-certs.sh carry an operator-chosen validity window (CYPHERS_MASTER_LAB_CERT_DAYS, default 365 days).
  • Test the mTLS path directly against the Master before blaming the Hub:
curl -fsS --cacert ca.crt --cert client.crt --key client.key \
  https://your-master-host:443/api/ct/intel/health

Expect a JSON body with clickhouse_ok/scanning fields. An unauthenticated request to the same URL should come back 401 — if it doesn't, that's a Master-side security regression, not a Satellite config problem.

Wrong port confusion (CYPHERS_CT_MASTER_URL set to :8443 instead of the CT Intel base URL): see the callout in The Master dependency above. curl against :8443/health returning ok is not evidence the CT Intel URL is correct — that port never serves /api/ct/intel/* routes at all.

ClickHouse warnings/errors in Hub logs on a Satellite: a Satellite should never have CLICKHOUSE_URL set. If it's present (e.g. copied from a Master's .env by mistake), remove it — docker/docker-compose.satellite.yml doesn't define a ClickHouse service, and a locally-configured ClickHouse is not part of the Satellite CT evidence path.

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

Admin login fails right after a Docker install: check docker compose -f docker/docker-compose.satellite.yml 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.

Preflight fails at the "Master CT query smoke" step even though health looks fine: health is necessary but not sufficient — see docs/getting-started/master-hub-ct-connection.md's note that a clean validation needs clickhouse_ok=true on the Master's side, and a real query against a domain the Master actually has CT evidence for.