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
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.
- 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 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 cluster — Y (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.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. - 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=VALUElines (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
ZITI_CTRL_ADVERTISED_ADDRESS— permanent DNS name (required)ZITI_CTRL_ADVERTISED_PORT— TCP port (default: 1280)ZITI_CLUSTER_NODE_NAME— unique node name (required)ZITI_CLUSTER_TRUST_DOMAIN— cluster trust domain for SPIFFE identity (required)ZITI_USER— admin username (default: admin)ZITI_PWD— admin password (required)
Join cluster answers
ZITI_BOOTSTRAP_CLUSTER— set tofalse(required)ZITI_CTRL_ADVERTISED_ADDRESS— this node's permanent DNS name (required)ZITI_CTRL_ADVERTISED_PORT— TCP port (default: 1280)ZITI_CLUSTER_NODE_NAME— unique node name (required)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:
| 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
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
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
- 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):'