How to properly setup ZAC and management API

Hello again!

I’ve playing with Ziti for a few weeks now and I’ve come to realize that I’ve never changed the default configuration of the ZAC and the management API. That means that the config.yml of the controller remains the same as the installation (I added a prometheus endpoint only). So “anyone” can access to the ZAC going to mydomain.controller:1280/zac and if you go to mydomain.controller:1280 you can see the URL of the management API, that in my case would be mydomain.controller:1280/edge/management/v1, which clearly it’s not good.

I’ve seen a few discourse post talking about this, but I’m not sure if I understood correctly how to change it, so here are my questions:

  • I think that I need to change the web part of the controller configuration, which looks like this now:

    web:
      - name: client-management
        bindPoints:
          - interface: 0.0.0.0:1280
            address: mydomain.controller:1280
        identity:
          ca:          "pki/root/certs/root.cert"
          key:         "pki/intermediate/keys/server.key"
          server_cert: "pki/intermediate/certs/server.chain.pem"
          cert:        "pki/intermediate/certs/client.chain.pem"
         
        options:
          idleTimeout: 5000ms  #http timeouts, new
          readTimeout: 5000ms
          writeTimeout: 100000ms
          minTLSVersion: TLS1.2
          maxTLSVersion: TLS1.3
        apis:
          - binding: edge-management
            options: { }
          - binding: edge-client
            options: { }
          - binding: fabric
            options: { }
          - binding: edge-oidc
            options: { }
          - binding: zac
            options:
              location: /opt/openziti/share/console
              indexFile: index.html
    
    

But I’m not sure if I have to make a new section (adding a new - name: ) and in the bind points say something like

bindPoints:
      - interface: 127.0.0.1:8443
        address: mydomain.controller:8443

And in there I add

apis:
      - binding: edge-management
        options: { }
      - binding: zac
        options:
          location: /opt/openziti/share/console
          indexFile: index.html

So I delete those in the previous one.

But in that case how do I manage the certificates and the options section? Do i need to add it again in the new one, or not? Or am I mistaken and there is another way to do this?

  • Now that I want to make the ZAC and the management API dark, I need to make a ziti service to be able to access to it. Right now in the controller’s container I don’t have installed anything else. What is the recommended approach, have a edge router or a ziti-edge-tunnel in the same place as the controller?

I hope I made myself clear, I’ll give more information if it is needed.

Thank you! :slight_smile:

Hi @martoAs,

Yep that's all you do. I'll include an example from my own controller for you to reference and i'll describe the sections below the code block:

web:
  - name: client-management
    bindPoints:
      - interface: 0.0.0.0:8441
        address: ec2-3-18-113-172.us-east-2.compute.amazonaws.com:8441
    identity:
      ca:          "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-root-ca/certs/ip-172-31-47-200-edge-controller-root-ca.cert"
      key:         "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/keys/feb2026-02-22.key"
      server_cert: "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/certs/feb2026-02-22.chain.pem"
      cert:        "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/certs/ec2-3-18-113-172.us-east-2.compute.amazonaws.com-client.chain.pem"
      alt_server_certs:
      - server_cert: "/etc/letsencrypt/live/cdaws.clint.demo.openziti.org/fullchain.pem"
        server_key:  "/etc/letsencrypt/live/cdaws.clint.demo.openziti.org/privkey.pem"
...
    apis:
      - binding: edge-management
        # options - arg optional/required
        # This section is used to define values that are specified by the API they are associated with.
        # These settings are per API. The example below is for the 'edge-api' and contains both optional values and
        # required values.
        options: { }
      - binding: edge-client
        options: { }
      - binding: fabric
        options: { }
      - binding: edge-oidc
        options: { }
      - binding: zac
        options:
          location: "/home/ubuntu/zac/ziti-console-v3.12.5"

  - name: zitified
    bindPoints:
      - interface: 127.0.0.1:18441
        address: 127.0.0.1:18441
      - identity:
          file: /home/ubuntu/.ziti/quickstart/ip-172-31-47-200/cdaws-controller.json
          service: "cdaws-controller"
      - identity:
          file: /home/ubuntu/.ziti/quickstart/ip-172-31-47-200/cdaws-controller.json
          service: "cdaws-controller"
          listenOptions:
            bindUsingEdgeIdentity: true
    identity:
      ca:          "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-root-ca/certs/ip-172-31-47-200-edge-controller-root-ca.cert"
      key:         "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/keys/feb2026-02-22.key"
      server_cert: "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/certs/feb2026-02-22.chain.pem"
      cert:        "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/certs/ec2-3-18-113-172.us-east-2.compute.amazonaws.com-client.chain.pem"
      #key:         "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/keys/ec2-3-18-113-172.us-east-2.compute.amazonaws.com-server.key"
      #server_cert: "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/certs/ec2-3-18-113-172.us-east-2.compute.amazonaws.com-server.chain.pem"
      #cert:        "/home/ubuntu/.ziti/quickstart/ip-172-31-47-200/pki/ip-172-31-47-200-edge-controller-intermediate/certs/ec2-3-18-113-172.us-east-2.compute.amazonaws.com-client.chain.pem"
      alt_server_certs:
      - server_cert: "/etc/letsencrypt/live/cdaws.clint.demo.openziti.org/fullchain.pem"
        server_key:  "/etc/letsencrypt/live/cdaws.clint.demo.openziti.org/privkey.pem"
...
    apis:
      - binding: edge-management
        options: { }
      - binding: fabric
        options: { }
      - binding: zac
        options:
          location: "/home/ubuntu/zac/ziti-console-v3.12.5"
      - binding: metrics
        options: {
          includeTimestamps: true
        }

old web binding apis

name: client-management
      - binding: edge-client ...
      - binding: fabric ...
      - binding: edge-oidc ...
      - binding: zac ...

I keep my zac exposed (you wouldn't) but you would expose only edge-client and edge-oidc (i also see i have my fabric api exposed publicly.

private mgmt, fabric, metrics, zac

    bindPoints:
      - interface: 127.0.0.1:18441
        address: 127.0.0.1:18441
      - identity:
          file: /home/ubuntu/.ziti/quickstart/ip-172-31-47-200/cdaws-controller.json
          service: "cdaws-controller"
      - identity:
          file: /home/ubuntu/.ziti/quickstart/ip-172-31-47-200/cdaws-controller.json
          service: "cdaws-controller"
          listenOptions:
            bindUsingEdgeIdentity: true

Here you can see i'm binding to the underlay exclusively to 127.0.0.1:18441 but you'll also see I've got an example of binding these services using ziti itself if you want (available with 1.8+). I made a service called "cdaws-controller" and I can use ziti to access the mgmt plane if i want to now. You don't need both bindings, in my example i'm just doing it because I'm testing this stuff a lot.

i've rotated certs

you can see in my config, i've had to rotate certs before so i left them commented out for you to see (if interested). When using the zitified approach, I had to add a DNS sans entry for "cdaws-controller" so that TLS works. You can see that with (DNS:controller-cdaws below):

openssl s_client -connect ec2-3-18-113-172.us-east-2.compute.amazonaws.com:8441 </dev/null 2>&1 | openssl x509 -text | grep -A2 Alter
            X509v3 Subject Alternative Name:
                DNS:localhost, DNS:mgmt, DNS:mgmt.ziti, DNS:mgmt-addressable-terminators, DNS:cdaws.controller, DNS:controller-cdaws, DNS:cdaws-controller, DNS:ec2-3-18-113-172.us-east-2.compute.amazonaws.com, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
    Signature Algorithm: sha256WithRSAEncryption

Hope that helps!

Hi @TheLumberjack

Thank you for your response! Yes, it’s very helpful. I’m going to try setting it up later, and I’ll follow up if I run into any issues.

If I understood correctly, in bindPoints.identity.file you specify the .json file of the server identity used to host access to the management control plane, right?

On that note, would it be better to use an edge router with tunneling enabled, or to install a tunneler instead?

Correct. It's a little "inception-ish" because I make the identity with the OpenZiti control plane for itself but that's not necessary (and not how NetFoundry will use it, we'll run a separate network for our customers for other reasons not terribly important here).

"better" is a subjective term you get to choose! :slight_smile: That's certainly how i used to do it. I would always co-locate my public router adjacent to my controller and I would enable it for tunneling and offload back to 127.0.0.1 (and bind the services to 127.0.0.1) to ensure it was not accessible over the AWS VPC at all.

That's also a great way to do it. My personal opinion is that using application embedded zero trust (having the controller bind its own api) is the "best" way to do it. You can also then access the service with a tunneler on your local machine but you could also use the ziti CLI in that case too (and therefor not need a tunneler running at all).

for example i could login with:

ziti edge login \
  --network-identity ~/.encrypted/cdaws.json \
  cdaws-controller \
  --file ~/.encrypted/cdaws.json

Hello again!

I think I messed up something, because when I try to log in into the ziti cli (in the same container that the controller is running) I have the following error:

ziti edge login localhost:8440 --file admin.json 
error: unable to authenticate to https://mydomain.controller:1280/edge/management/v1. Status code: 404 Not Found, Server returned: {
    "error": {
        "cause": {
            "code": "UNHANDLED",
            "message": "path /edge/management/v1/authenticate was not found"
        },
        "code": "NOT_FOUND",
        "message": "The resource requested was not found or is no longer available",
        "requestId": "2YfPk9ypDU"
    },
    "meta": {
        "apiEnrollmentVersion": "0.0.1",
        "apiVersion": "0.0.1"
    }
}

For some reason it stills wants to find the management api on port 1280.

I’ll give the complete config.yml:

v: 3


db:                     "/var/lib/private/ziti-controller/bbolt.db"

identity:
  cert:        "pki/intermediate/certs/client.chain.pem"
  server_cert: "pki/intermediate/certs/server.chain.pem"
  key:         "pki/intermediate/keys/server.key"
  ca:          "pki/root/certs/root.cert"


ctrl:
  options:
    advertiseAddress: tls:mydomain.controller:1280
  listener:             tls:0.0.0.0:1280


healthChecks:
  boltCheck:
    # How often to try entering a bolt read tx. Defaults to 30 seconds
    interval: 30s
    # When to time out the check. Defaults to 20 seconds
    timeout: 20s
    # How long to wait before starting the check. Defaults to 30 seconds
    initialDelay: 30s

edge:
 
  api:
   
    sessionTimeout: 30m
    address: mydomain.controller:1280
  enrollment:
    signingCert:
      cert: pki/intermediate/certs/intermediate.cert
      key:  pki/intermediate/keys/intermediate.key
    edgeIdentity:
      duration: 180m
    edgeRouter:
      duration: 180m

web:
  - name: client-management
    bindPoints:
     
      - interface: 0.0.0.0:1280
        address: mydomain.controller:1280
    identity:
      ca:          "pki/root/certs/root.cert"
      key:         "pki/intermediate/keys/server.key"
      server_cert: "pki/intermediate/certs/server.chain.pem"
      cert:        "pki/intermediate/certs/client.chain.pem"
      
   
    options:
      
      idleTimeout: 5000ms  #http timeouts, new
     
      readTimeout: 5000ms
    
      writeTimeout: 100000ms
     
      minTLSVersion: TLS1.2
    
      maxTLSVersion: TLS1.3
   
    apis:
      # binding - required
      # Specifies an API to bind to this webListener. Built-in APIs are
      #   - edge-management
      #   - edge-client
      #   - fabric-management
      # - binding: edge-management
        # options - arg optional/required
        # This section is used to define values that are specified by the API they are associated with.
        # These settings are per API. The example below is for the 'edge-api' and contains both optional values and
        # required values.
        # options: { }
      - binding: edge-client
        options: { }
          #- binding: fabric
          #options: { }
      - binding: edge-oidc
        options: { }
          #- binding: zac
          #options:
          #location: /opt/openziti/share/console
          #indexFile: index.html
 
  - name: management-ziti
    bindPoints:
      - interface: 127.0.0.1:8440
        address: 127.0.0.1:8440
    identity:
      ca:          "pki/root/certs/root.cert"
      key:         "pki/intermediate/keys/server.key"
      server_cert: "pki/intermediate/certs/server.chain.pem"
      cert:        "pki/intermediate/certs/client.chain.pem"

    options:
      idleTimeout: 5000ms  #http timeouts, new
      readTimeout: 5000ms
      writeTimeout: 100000ms
      minTLSVersion: TLS1.2
      maxTLSVersion: TLS1.3

    apis:
      - binding: edge-management
        options: { }
      - binding: fabric
        options: { }
      - binding: zac
        options:
          location: /opt/openziti/share/console
          indexFile: index.html

       
  - name: metrics-prom
    bindPoints:
      - interface: 172.16.0.3:2112
        address: mydomain.controller:2112
    apis:
      - binding: metrics
        options: { 
          scrapeCert: "/etc/prometheus/prom-client.crt"
        } 

I’ve commented the edge-management, fabric and zac from the default one.
If i check the socket sessions, I see that Ziti is listening on port 8440

tcp        LISTEN      0           4096                  127.0.0.1:8440                  0.0.0.0:*          users:(("ziti",pid=549,fd=10)) 

If I do

curl -k https://localhost:8440

I see the expected response


{"data":{"apiVersions":{"edge":{"v1":{"apiBaseUrls":["https://mydomain.controller:1280/edge/client/v1"],"path":"/edge/client/v1"}},"edge-client":{"v1":{"apiBaseUrls":["https://mydomain.controller:1280/edge/client/v1"],"path":"/edge/client/v1"}},"edge-management":{"v1":{"apiBaseUrls":["https://127.0.0.1:8440/edge/management/v1"],"path":"/edge/management/v1"}},"edge-oidc":{"v1":{"apiBaseUrls":["https://controller.corp.zta.com:1280"],"path":"/oidc"}},"health-checks":{"v1":{"apiBaseUrls":[],"path":"/health-checks/v1"}}},"buildDate":"2025-12-04T23:23:51Z","capabilities":["OIDC_AUTH"],"revision":"5afd4d7837fc","runtimeVersion":"go1.25.4","version":"v1.6.12"},"meta":{}}

And if I do

curl -k https://localhost:8440/zac

I see the HTML corresponding to it.

I expect this is a bug. I'll try to repoduce it. After you login, you'll see something like this:

Saving identity 'default' to /home/clint/.config/ziti/ziti-cli.json

If you look in that file:

$ cat /home/clint/.config/ziti/ziti-cli.json | head -4
{
    "edgeIdentities": {
        "default": {
            "url": "https://9a062ac6-0bf5-489e-9b90-726195c84a8d.production.netfoundry.io:24443/edge/management/v1",

you'll most likely see that wrong url/port...

Easiest thing is to rm that file in the meantime. I'll see if I can reproduce, but I'd expect that's a bug.

Ok! I the admin.json was trying to communicate with mydomain.controller:1280 because in the .json file had the previous url/port

	"ztAPI":"https://mydomain.controller:1280",
	"ztAPIs":[
		"https://mydomain.controller:1280"
	],

The log in with the default admin identity was successful

EDIT: I’ve changed the port on the admin.json and now works perfectly. My bad for not checking the .json :sweat_smile:

Sorry to bother with this topic. The certificates that I’ve generated with the ziti PKI when I first installed the controller have these DNS names:

DNS Name: localhost
DNS Name: mydomain.controller.com

If I create a service to access the ZAC and, in the “intercept” option, I specify the domain management.ziti, the browser will display a “connection is not secure” warning when the client accesses the ZAC. This occurs because management.ziti is not included in the certificate’s SANs, even if the certificate is manually added to the trust store.

If I regenerate the certificates to include the new DNS entry, do I need to modify anything in the routers’ configuration? Or will the connections between them be updated automatically without any additional intervention?

Hi @martoAs, not a bother. You'll almost certainly continue to get the "connection is not secure" error as long as you deliver the zac from server using the private pki generated for OpenZiti. You could always choose to add the ziti pki CA to your browser if you want, but that can be tedious for multiple browser instances. If you're the only person using the ZAC or there are few of you it's probably not a big deal.

Another option would be to get a domain and get a cert from LetsEncrypt and deliver that cert using an "alt-server-cert" setup. Then whomever accecsses the ZAC could get a cert that matches the certificate's SANS.

Depends on if you "do it right" or not. :slight_smile: for this, you only need to rotate the cert in that bindPoint as you've shown. The routers don't leverage this particular identity block so there's no worry about the routers at all.

Should you rotate the "root" identity (usually at the top of the file) - those ARE used by routers when connecting to the controller but as long as you use the same key that generated the server cert it wouldn't be an issue. Hopefully that all makes enough sense?