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.
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.
- Debian
- RedHat
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
Configure the repository for the RedHat family (Fedora, Rocky, Alma)
Create the repository file.
sudo tee /etc/yum.repos.d/openziti-release.repo >/dev/null <<\EOF
[OpenZitiRelease]
name=OpenZiti Release
baseurl=https://packages.openziti.org/zitipax-openziti-rpm-stable/redhat/$basearch
enabled=1
gpgcheck=0
gpgkey=https://packages.openziti.org/zitipax-openziti-rpm-stable/redhat/$basearch/repodata/repomd.xml.key
repo_gpgcheck=1
EOF
Update the package list.
sudo dnf 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.,
ctrl1fromctrl1.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-consolepackage 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.certandroot/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
ZITI_CTRL_ADVERTISED_ADDRESS— permanent external address (required)ZITI_CTRL_ADVERTISED_PORT— TCP port (default: 1280)ZITI_CLUSTER_NODE_NAME— unique node name (default: first label of the address)ZITI_CLUSTER_TRUST_DOMAIN— cluster trust domain for SPIFFE identity (default: remaining labels of the address)ZITI_PWD— admin password (default: generated random password)ZITI_USER— admin username (default: admin)
Join cluster answers
ZITI_BOOTSTRAP_CLUSTER— set tofalse(required)ZITI_CTRL_ADVERTISED_ADDRESS— this node's permanent external address (required)ZITI_CTRL_ADVERTISED_PORT— TCP port (default: 1280)ZITI_CLUSTER_NODE_NAME— unique node name (default: first label of the address)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:
| Answer | Default | What it does |
|---|---|---|
ZITI_BOOTSTRAP_PKI | true | Generate root CA, intermediate CA, and leaf certificates |
ZITI_BOOTSTRAP_CONFIG | true | Generate config.yml (set to force to regenerate) |
ZITI_BOOTSTRAP_DATABASE | true | Initialize the database with a default admin |
Each component uses specific answers:
- PKI —
ZITI_CTRL_ADVERTISED_ADDRESS,ZITI_CLUSTER_NODE_NAME,ZITI_CLUSTER_TRUST_DOMAIN - Config —
ZITI_CTRL_ADVERTISED_ADDRESS,ZITI_CTRL_ADVERTISED_PORT(PKI must already exist) - Database —
ZITI_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
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
- Log Formats
- Log Levels
Set a different format in the ZITI_ARGS environment variable and restart the service.
ZITI_ARGS='--log-formatter text'
Enable DEBUG log level with the --verbose flag in the ZITI_ARGS environment variable and restart the service.
ZITI_ARGS='--verbose'
Learn more in the logging reference.
Uninstall
-
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 -
Purge the package, including configuration files.
APT - Debian, Ubuntu, etc.
sudo apt-get purge openziti-controllerRPM - RedHat, Fedora, etc.
sudo dnf remove openziti-controller -
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):'
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):'