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

Use the install script to configure the package repo and install the ziti CLI:

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

Then install openziti-controller in a terminal session so the bootstrap script can prompt you.

sudo apt-get install openziti-controller     # Debian, Ubuntu
sudo dnf install openziti-controller # RedHat, Fedora

For non-interactive installs, see Automation.

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 creates a PKI, config file, and database. It prompts for answers interactively. When run non-interactively (e.g., with an answer file), it uses the provided values without prompting.

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

On success, the script enables and starts the service, then prints a ziti edge login command you can copy-paste.

New cluster

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

  • Are you joining an existing cluster?N (default = new cluster)
  • Permanent external address — DNS name of this controller (required)
  • Node name — unique name for this controller (default: first label of the address, e.g., ctrl1 from ctrl1.example.com)
  • Trust domain — shared by all cluster nodes (default: remaining labels of the address, e.g., example.com). Used for SPIFFE identity
  • Port — TCP port (default: 1280)
  • Admin password — generate a random password (default), or enter manually
  • Console — configure the OpenZiti Console web UI binding (installs the openziti-console package separately)
  • Certificate renewal timer — enable automatic monthly leaf cert renewal

Join an existing cluster

Answer Y at the "joining an existing cluster" prompt. The script asks for:

  • Permanent external address — this node's DNS name
  • 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.
  • 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 — copy /opt/openziti/etc/controller/bootstrap.env as a template, fill in the values, and pass it as the first argument. The bootstrap script reads the file but never modifies it.
  • Environment — export answers as environment variables and pass them with sudo -E

Precedence (highest to lowest): environment variables → answer file → service.env defaults.

Answers are written to a temporary file during bootstrap and deleted automatically on success. If bootstrap fails, the temporary file is preserved and a re-run command is printed.

cp /opt/openziti/etc/controller/bootstrap.env /tmp/my-answers.env
# edit /tmp/my-answers.env with your values
sudo -E /opt/openziti/etc/controller/bootstrap.bash /tmp/my-answers.env

New cluster answers

  1. ZITI_CTRL_ADVERTISED_ADDRESS — permanent external address (required)
  2. ZITI_CTRL_ADVERTISED_PORT — TCP port (default: 1280)
  3. ZITI_CLUSTER_NODE_NAME — unique node name (default: first label of the address)
  4. ZITI_CLUSTER_TRUST_DOMAIN — cluster trust domain for SPIFFE identity (default: remaining labels of the address)
  5. ZITI_PWD — admin password (default: generated random password)
  6. ZITI_USER — admin username (default: admin)

Join cluster answers

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

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

The bootstrap script automatically enables and starts the service on success. If you need to start it manually (for example, after crafting a configuration by hand):

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

The package includes a drop-in file with commented example directives at /etc/systemd/system/ziti-controller.service.d/override.conf. Your edits to this file are preserved across package upgrades.

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.