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:
- The ceremony directory exists once, created ahead of time with the
cyphers-caceremony 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.
- 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 thedocker-compose.master.tls-lab.ymloverlay'sCYPHERS_LIVE_MASTER_TLS_DIRmount.remote-hub/—client.crt,client.key,ca.crt— this is your Satellite's bundle. Transportremote-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)¶
- 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
- Create
.envnext todocker/docker-compose.satellite.ymlwith, 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).
- Start the stack:
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.
- 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.
- Log in to the dashboard on the mapped UI port (
HUB_UI_PORT, default443) 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:
- 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) — edit it next to fill inCYPHERS_CA_KEY_PASSPHRASE,DATABASE_URL,CYPHERS_CA_URL, and the fourCYPHERS_CT_INTEL_*/CYPHERS_CT_MASTER_URLvalues from The Master dependency. - Creates
/opt/cyphers/ca-keys/(mode0700). - 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:
- 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 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:
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 threeCYPHERS_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 md5vs.openssl rsa -noout -modulus -in client.key | openssl md5— or the EC equivalent). - Confirm
client.crthasn't expired (openssl x509 -in client.crt -noout -enddate) — bundles fromscripts/issue-master-lab-certs.shcarry 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.