Skip to main content
Star OpenZiti on GitHub Star

Controller Deployment

Deploy a controller as a Linux system service. The controller introduction may help to read first.

This guide applies to Ziti version 2.0 and newer. Older Linux packages are still available and work similarly but ignore the cluster-related answers.

  1. Installation
  2. Configuration
  3. Starting Up

Install the controller package

The controller package provides a systemd service unit and bootstrapping script.

One-liner install script

curl -sS https://get.openziti.io/install.bash | sudo bash -s openziti-controller

Manual package repo setup

Configure the package repository and install openziti-controller.

Configure the repository for the Debian family of distributions (Ubuntu, Mint, Pop!_OS)

Install the OpenZiti repository key.

curl -sSLf https://get.openziti.io/tun/package-repos.gpg | sudo gpg --dearmor --output /usr/share/keyrings/openziti.gpg

Ensure the key is readable by all users.

sudo chmod a+r /usr/share/keyrings/openziti.gpg

Create the repository file.

sudo tee /etc/apt/sources.list.d/openziti-release.list >/dev/null <<EOF
deb [signed-by=/usr/share/keyrings/openziti.gpg] https://packages.openziti.org/zitipax-openziti-deb-stable debian main
EOF

Update the package list.

sudo apt update

Finally, install the package: openziti-controller

The openziti package provides the ziti CLI and is installed as a dependency.

Configuration

You need a PKI, a config file, and a database. The easiest way to get all three is to run the bootstrap script. You can also migrate from an existing installation or craft a config by hand. The bootstrap script is a convenience — it is not required.

Run the bootstrap script

sudo /opt/openziti/etc/controller/bootstrap.bash

The script walks you through creating a PKI, config file, and database. It prompts for any answers it needs. If an answer has already been supplied (through the answer file, environment, or stdin), the script skips that prompt.

The controller always runs in clustered mode, even for a single-node deployment.

New cluster

This is the common case — a single controller, or the first node of a multi-node cluster. The script asks for:

  • Create a new clusterY (default)
  • Node name — unique name for this controller (default: first label of the FQDN, e.g., ctrl1)
  • Trust domain — shared by all cluster nodes (default: everything after the first dot, e.g., ziti.example.com). Used for SPIFFE identity
  • Controller address — permanent DNS name (default: {node name}.{trust domain})
  • Port — TCP port (default: 1280)
  • Admin username (default: admin)
  • Admin password — initializes the cluster (a random password is offered if you press Enter)

Join an existing cluster

Answer N at the first prompt. The script asks for:

  • Node name — must be unique across all nodes in the cluster
  • PKI directory — path to a copy of the first node's pki/ directory (from /var/lib/ziti-controller/), containing the root CA cert and key (root/certs/root.cert and root/keys/root.key). The join script uses the root CA to issue this node's unique intermediate CA (edge enrollment signer) during the initial join, and the root CA is also required later to renew that signer (default intermediate expiry is 10 years). The join script does not delete the root CA from the provided PKI directory or automatically renew the intermediate CA, so you can reuse the same PKI directory for future joins and renewals.
  • Controller address — this node's permanent DNS name
  • Port — TCP port (default: 1280)

Migrate an existing configuration

This example illustrates copying the PKI, configuration, and database from a previous installation.

Craft a configuration

Create a config file directly with ziti create config controller --clustered. Run ziti create config environment to see the available environment variables. See the controller configuration reference for details.

Automation

If you're scripting deployments or using configuration management, you can supply answers ahead of time and run the bootstrap script without prompts. You can also choose which components to bootstrap.

How to supply answers

Answers can come from any combination of:

  • Answer file — write answers in /opt/openziti/etc/controller/bootstrap.env
  • Environment — export answers as environment variables and pass them with sudo -E
  • Stdin — pipe answers as KEY=VALUE lines (one per line) into the script

All answers are persisted to bootstrap.env regardless of how they were supplied.

To run without prompts, redirect stdin:

sudo -E /opt/openziti/etc/controller/bootstrap.bash < /dev/null

New cluster answers

  1. ZITI_CTRL_ADVERTISED_ADDRESS — permanent DNS name (required)
  2. ZITI_CTRL_ADVERTISED_PORT — TCP port (default: 1280)
  3. ZITI_CLUSTER_NODE_NAME — unique node name (required)
  4. ZITI_CLUSTER_TRUST_DOMAIN — cluster trust domain for SPIFFE identity (required)
  5. ZITI_USER — admin username (default: admin)
  6. ZITI_PWD — admin password (required)

Join cluster answers

  1. ZITI_BOOTSTRAP_CLUSTER — set to false (required)
  2. ZITI_CTRL_ADVERTISED_ADDRESS — this node's permanent DNS name (required)
  3. ZITI_CTRL_ADVERTISED_PORT — TCP port (default: 1280)
  4. ZITI_CLUSTER_NODE_NAME — unique node name (required)
  5. ZITI_CLUSTER_NODE_PKI — path to a copy of the existing cluster's PKI directory containing the root CA cert and key (required). The root CA key is deleted after generating this node's intermediate CA.

Selective bootstrapping

You don't have to bootstrap everything at once. Each component can be enabled or disabled independently in /opt/openziti/etc/controller/service.env:

AnswerDefaultWhat it does
ZITI_BOOTSTRAP_PKItrueGenerate root CA, intermediate CA, and leaf certificates
ZITI_BOOTSTRAP_CONFIGtrueGenerate config.yml (set to force to regenerate)
ZITI_BOOTSTRAP_DATABASEtrueInitialize the database with a default admin

Each component uses specific answers:

  • PKIZITI_CTRL_ADVERTISED_ADDRESS, ZITI_CLUSTER_NODE_NAME, ZITI_CLUSTER_TRUST_DOMAIN
  • ConfigZITI_CTRL_ADVERTISED_ADDRESS, ZITI_CTRL_ADVERTISED_PORT (PKI must already exist)
  • DatabaseZITI_USER, ZITI_PWD

Starting up

Enable and start the service:

sudo systemctl enable --now ziti-controller.service

Firewall

The controller listens on a single configurable TCP port (default: 1280). Create a firewall exception if needed.

Confirm the controller is listening:

sudo ss -tlnp | grep ziti
Output
LISTEN 0      4096                          *:1280             *:*    users:(("ziti",pid=2004302,fd=8))

Further configuration

Customize /var/lib/ziti-controller/config.yml as needed and restart the service.

sudo systemctl restart ziti-controller.service

Here's a link to the configuration reference.

Customize the systemd service

Use systemctl edit to override service directives like capabilities or the startup command. Pass -E to sudo so your shell's SYSTEMD_EDITOR (or EDITOR / VISUAL) is used.

sudo -E systemctl edit ziti-controller.service
sudo systemctl restart ziti-controller.service

An example drop-in with commented directives is included at /opt/openziti/etc/controller/override.conf.example.

Logging

View the service's output.

journalctl -u ziti-controller.service

Set a different format in the ZITI_ARGS environment variable and restart the service.


ZITI_ARGS='--log-formatter text'


Learn more in the logging reference.

Uninstall

  1. Clean the service state.

    sudo systemctl disable --now ziti-controller.service
    sudo systemctl reset-failed ziti-controller.service
    sudo systemctl clean --what=state ziti-controller.service
  2. Purge the package, including configuration files.

    APT - Debian, Ubuntu, etc.

    sudo apt-get purge openziti-controller

    RPM - RedHat, Fedora, etc.

    sudo dnf remove openziti-controller
  3. Remove any firewall exceptions you created.

Troubleshooting

Verify the control plane is reachable by routers. The control plane must terminate TLS for routers because they will authenticate with a client certificate for all post-enrollment interactions.

The server certificate must be issued by the controller's edge signer CA (edge.enrollment.signerCert in /var/lib/ziti-controller/config.yml).

Substitute the value of ctrl.options.advertiseAddress from /var/lib/ziti-controller/config.yml:

openssl s_client -connect {ctrl.options.advertiseAddress} -alpn ziti-ctrl -showcerts <>/dev/null \
|& openssl storeutl -certs -noout -text /dev/stdin \
| grep -E '(Subject|Issuer):'
Output
Issuer: C=US, L=Charlotte, O=NetFoundry, OU=ADV-DEV, CN=NetFoundry Inc. Intermediate CA 42KvRQxn.
Subject: C=US, ST=NC, L=Charlotte, O=NetFoundry, OU=Ziti, CN=BhCjN2Rkx
Issuer: C=US, L=Charlotte, O=NetFoundry, OU=ADV-DEV, CN=NetFoundry Inc. Certificate Authority IpcOEkAR6
Subject: C=US, ST=NC, L=Charlotte, O=NetFoundry, OU=ADV-DEV, CN=NetFoundry Inc. Intermediate CA 42KvRQxn.

Verify the controller's edge-client web API is reachable by identities and routers. This API must terminate TLS for any identities that enroll because they will authenticate with a client certificate for post-enrollment interactions.

Enrollment tokens are signed with the key of the controller's server certificate that matches the edge.api.address in /var/lib/ziti-controller/config.yml.

Substitute the value of edge.api.address from /var/lib/ziti-controller/config.yml:

openssl s_client -connect {edge.api.address} -alpn h2,http/1.1 -showcerts <>/dev/null \
|& openssl storeutl -certs -noout -text /dev/stdin \
| grep -E '(Subject|Issuer):'
Output
Issuer: C=US, L=Charlotte, O=NetFoundry, OU=ADV-DEV, CN=NetFoundry Inc. Intermediate CA 42KvRQxn.
Subject: C=US, ST=NC, L=Charlotte, O=NetFoundry, OU=Ziti, CN=BhCjN2Rkx
Issuer: C=US, L=Charlotte, O=NetFoundry, OU=ADV-DEV, CN=NetFoundry Inc. Certificate Authority IpcOEkAR6
Subject: C=US, ST=NC, L=Charlotte, O=NetFoundry, OU=ADV-DEV, CN=NetFoundry Inc. Intermediate CA 42KvRQxn.