Deployment Reference:
NC, ER, Certificates & Upgrades
Architecture Overview
SDS (Software Deployment Service) orchestrates the lifecycle of OpenZiti Network Controllers (NC) and Edge Routers (ER) on target host machines. It uses two distinct orchestration channels: Ansible for initial installation, and Salt for ongoing state management and upgrades.
Key Components
| Component | Role | Key classes |
|---|---|---|
NetworkControllerAnsibleWrapper | Runs NC install playbook via ProcessBuilder | AbstractAnsibleWrapper |
EdgeRouterAnsibleWrapper | Runs ER install playbook via ProcessBuilder | AbstractAnsibleWrapper |
SaltService | Public API for all Salt operations | SaltRestClient, SaltClientFactory |
JobPollingService | Two-phase polling for Salt job results | RetryTemplate |
CertificateService | Proxies cert requests to the certs microservice | InterServiceTokenProvider, WebClient |
NetworkControllerService | NC upgrade orchestration entry point | NetworkControllerUpgradeSaltRunner |
EdgeRouterService | ER upgrade orchestration entry point | EdgeRouterUpgradeSaltRunner |
Network Controller Installation
The NC install is triggered by a REST API call into SDS. SDS builds an Ansible inventory, launches ansible-playbook as a blocking subprocess, and waits for completion (timeout configurable, typically 90 min). The playbook runs entirely over SSH on the target machine.
ansible_connection=community.openziti.paramiko with a Ziti identity file — no public IP required.Installation Sequence
Ansible Roles — ziti_controller.yml
| Role | Tag | What it does |
|---|---|---|
publicCertV7 | public_cert | Provisions public TLS certificate (optional, for HTTPS endpoints) |
zitiControllerV7 | nc | Core NC installation: FIPS, binary download, cert chain, config render, edge init, systemd service |
ncRouterV7 | nc_router | Installs the embedded router that runs alongside the NC |
saltMasterV7 | salt | Installs and configures Salt Master (HTTP API on :8001) |
saltMinionV7 | salt_minion | Installs Salt Minion; minion ID = host UUID; pins to 3006.14 |
monitoringV7 | — | Installs telemetry/monitoring agent |
ddosV7 | — | DDoS mitigation configuration |
zitiProxCV7 | — | Installs zitiProxC for control-plane connectivity |
frontdoorAgentV7 | — | Installs frontdoor agent for management connectivity |
salt-master (for managing ER minions) AND salt-minion (with its own host UUID as minion ID, so SDS can target it for NC upgrades via its own master).Key extra vars passed to the NC playbook (via getCommonVars() in AbstractAnsibleWrapper):
| Variable | Source |
|---|---|
fips_enabled | Network/host attribute; affects binary selection and kernel mode |
ha_enabled | Network attribute; enables HA-specific config |
cloud_ziti | true when platform version ≥ 8 |
network_id | UUID of the network; used for cert callbacks |
sds_url | Base URL of this SDS instance; Ansible callbacks use this |
target_version | The Ziti version to install |
Edge Router Installation
ER installation follows the same Ansible subprocess pattern but uses a different playbook (ziti_edge_router.yml) and a fundamentally different mechanism for certificate provisioning: instead of a PKI callback chain, the ER enrolls with the NC using a pre-issued JWT token.
ziti router enroll which contacts the NC's Ziti API and exchanges the JWT for a full mTLS identity (cert + key).Installation Sequence
ER Roles & Key Differences from NC
| Role | NC playbook | ER playbook |
|---|---|---|
| Core Ziti component | zitiControllerV7 | zitiEdgeRouterV7 |
| PKI generation | openssl genkey + CSR + callback to certs svc | None — JWT enrollment gives identity |
| Salt role | Salt Master + Minion | Salt Minion only |
| TCP tuning | Standard | 16 MB socket buffers (high-throughput) |
| ncRouter | Yes (ncRouterV7) | No |
| zitiProxC / frontdoor | Yes | No |
Certificate Architecture
NetFoundry uses a two-tier PKI. The Root CA is created and persisted by the certs microservice. The Intermediate CA is generated on the NC host itself (EC prime256v1) and signed by the root. All leaf certificates are signed by this intermediate and live only on the NC host.
PKI Hierarchy
Stored in AWS SSM Parameter Store
/certificates/CA_{uuid}Generated on NC host via openssl
Signed by Root · NOT stored persistently
On NC host
On NC host
Independent of PKI chain
Certificate Generation Flow
InterServiceTokenProvider to acquire a JWT and proxy the request to the certs service.Certs Microservice (/Users/prashantsavadi/git/certs)
| Endpoint | What it does |
|---|---|
GET /ca/{networkId} | Returns the Root CA PEM for the network; reads from SSM /certificates/CA_{networkId} |
POST /intermediate | Signs the submitted CSR with the Root CA key (fetched from SSM /certificates/CA_{networkId}_key); returns signed intermediate cert PEM; does NOT store it |
Root CA properties: RSA-4096, 50-year validity, CA:TRUE, stored as SecureString in AWS SSM Parameter Store. Created once per network on first request.
Intermediate CA properties: The CSR is generated on the NC host using EC prime256v1; the signed certificate has CA:TRUE and 10-year validity. It is returned to Ansible and placed on the NC host but is never persisted by the certs service.
Authentication: SDS uses InterServiceTokenProvider to acquire a JWT with audience io.netfoundry.network before calling the certs service. Ansible running on the NC host has no access to this token — hence the callback pattern through SDS.
Network Controller Upgrade via Salt
NC upgrades use Salt orchestration: SDS triggers a state.orchestrate runner on the Salt Master (which lives on the NC host itself), then polls for completion. Maximum wait: 10 minutes (60 retries × 10 s).
NC Upgrade Orchestration Steps
- 1
SyncAllSaltRunner: Callsfileserver.updaterunner — syncs Salt fileserver state before upgrade begins. - 2
NetworkControllerUpgradeSaltRunner: Callsstate.orchestratewith mods=ziti_upgrade.orch.controller_upgrade, pillar:{targetVersion, targetMasters: [minionId]}. - 3Orchestration step ①:
test.pingon controller minion — preflight connectivity check. - 4Orchestration step ②:
ziti_upgrade.stage.controller— downloads binary bundle to/opt/netfoundry/ziti/{version}/, stages upgrade files (init.sls, localconfig.yaml, service template). - 5Orchestration step ③:
fileserver.updaterunner — ensures Salt fileserver sees the staged files. - 6Orchestration step ④:
ziti.controller.upgradestate — executes the actual upgrade on the NC host (stops service, swaps binary, restarts, verifies). - 7Polling:
JobPollingService.await(60, 10s)polls using two phases: Phase 1 checksjobs.lookup_jiduntil result appears; Phase 2 fetches fullOrchestrationStateResult.
Edge Router Upgrade via Salt
ER upgrades use a "dark upgrade" pattern: Salt stages upgrade files and installs a systemd timer on each ER, then the ER upgrades itself autonomously without further coordination from SDS. The polling window is much longer: ~45 minutes (275 retries × 10 s).
salt-call --local (masterless mode) — SDS does not observe individual ER upgrade completion.Dark Upgrade Pattern — Sequence
Dark Upgrade — Step-by-step
- 1SDS calls
EdgeRouterUpgradeSaltRunner:state.orchestrate, mods=ziti_upgrade.orch.router_upgrade, pillar:{targetVersion, targetMasters, targetMinions}. - 2Preflight:
test.pingruns on both Salt Master minions AND ER minions in parallel — confirms all hosts are reachable. - 3Stage on NC:
ziti_upgrade.stage.controllerdownloads the binary bundle to/opt/netfoundry/ziti/{version}/on the NC host (fileserver). Files include:init.sls,localconfig.yaml,ziti-router.service.j2,defaults.yaml. - 4Sync fileserver:
fileserver.updaterunner ensures Salt Master's fileserver reflects the newly staged files. - 5Stage on ERs:
ziti_upgrade.stage.routers(timeout: 600 s) copies staged files from Salt fileserver to each ER atziti_upgrade_dir, writesrequested_versionfile, creates and enablesziti-router-upgrade.service(oneshot) andziti-router-upgrade.timer. - 6Dark upgrade fires: The systemd timer on each ER fires and executes the oneshot service. The service runs
salt-call --local(masterless mode) to perform the actual binary swap, config update, and service restart — no Salt Master involvement. - 7SDS polling: Polls the NC orchestration result (not individual ERs) for up to 275 × 10 s ≈ 45 min. Polling ends when the NC orchestration (staging) reports complete. ER self-upgrades continue independently.
salt-call --local ensures the upgrade can complete even if the minion connection to the master is interrupted during the process.Salt Architecture
Salt provides the post-installation state management and upgrade orchestration channel. Each NC host runs a Salt Master; all associated ER hosts register as Salt Minions of that master. SDS communicates with the master via its HTTP API on port 8001.
Topology
Salt Client Factory — Connection Mode Selection
SaltClientFactory picks between two connection strategies based on whether the NC has a managementZitiIdentity:
| Condition | Client used | URL format | TLS behavior |
|---|---|---|---|
controller.managementZitiIdentity != null | controlPlaneRestClient | https://<zitiIdentityName>:8001 | Ziti overlay; hostname verification disabled |
| Traditional (no Ziti identity) | traditionalRestClient | https://<hostIpAddress>:8001 | TrustAllStrategy (self-signed certs) |
The controlPlaneRestClient bean uses zitiClientHttpRequestFactory and loads the Ziti identity from AWS Secrets Manager. The SaltRestClient performs a login/logout per API call using an AutoCloseable WithAuth wrapper.
Job Polling — Two-Phase Strategy
JobPollingService.await() uses a two-phase approach to handle Salt's asynchronous job model:
// Phase 1: has any result arrived for this JID?
RetryTemplate(maxRetries, delay)
.execute(() -> saltService.hasJobResult(jid)) // polls jobs.lookup_jid
// SaltTerminalFailureException → non-retriable (retcode=1)
// Phase 2: fetch the actual result (up to 60×1s)
supplier.get() // calls getOrchestrationResult()
| Upgrade type | Phase 1 retries | Phase 1 delay | Max wait |
|---|---|---|---|
| NC upgrade | 60 | 10 s | ~10 min |
| ER upgrade | 275 | 10 s | ~45 min |
Terminal failures: If Salt returns retcode=1, SaltTerminalFailureException is thrown — this is not retried and propagates immediately to the caller.
Salt operations used by SDS:
| Method | Salt function | client | Purpose |
|---|---|---|---|
applyState() | state.apply | local_async | Apply a Salt state to a minion |
executeRunner() | state.orchestrate | runner_async | Run an orchestration |
executeHook() | varies | local | Synchronous hook execution |
executeFunction() | varies | local | Synchronous function call |
hasJobResult() | jobs.lookup_jid | runner | Check if job has completed |
getOrchestrationResult() | runner result | — | Fetch full orchestration output |
listMinions() | key.list_accepted | wheel | List accepted minion keys |
removeMinions() | key.delete | wheel | Remove minion keys (deregister) |