# Node.js SDK enrollment fails with TLS handshake error — Let's Encrypt + Internal PKI separation

**URL:** <https://openziti.discourse.group/t/node-js-sdk-enrollment-fails-with-tls-handshake-error-lets-encrypt-internal-pki-separation/5678>\
**Category:** Ziti Overlay\
**Created:** [March 5, 2026, 5:58pm UTC](https://openziti.discourse.group/t/node-js-sdk-enrollment-fails-with-tls-handshake-error-lets-encrypt-internal-pki-separation/5678 "2026-03-05T17:58:30Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![franciscoarruda](https://avatars.discourse-cdn.com/v4/letter/f/6f9a4e/32.png) [@franciscoarruda](https://openziti.discourse.group/u/franciscoarruda)\
**Post date:** [March 5, 2026, 5:58pm UTC](https://openziti.discourse.group/t/node-js-sdk-enrollment-fails-with-tls-handshake-error-lets-encrypt-internal-pki-separation/5678/1 "2026-03-05T17:58:30Z")

</div>

Hi,

I'm deploying OpenZiti with Docker Compose using `openziti/ziti-controller` and `openziti/ziti-router` (non-quickstart images). I'm using the Node.js SDK (`@openziti/ziti-sdk-nodejs`) in an Electron app to enroll a client identity.

**Setup:**

- Port `1280` and `8443` open externally

- Port `6262` is internal Docker only

- Two DNS names to avoid SAN overlap (as recommended):

**docker-compose.yml:**

```yaml
services:
  ziti-controller:
    image: openziti/ziti-controller:latest
    environment:
      - ZITI_HOME=/ziti-controller
      - ZITI_BOOTSTRAP=true
      - ZITI_BOOTSTRAP_PKI=true
      - ZITI_BOOTSTRAP_CONFIG=true
      - ZITI_BOOTSTRAP_DATABASE=true
    ports:
      - "1280:1280"
      - "6262:6262"
    networks:
      ziti-net:
        aliases:
          - ziti-controller
          - ziti.certiv.pt

  ziti-router:
    image: openziti/ziti-router:latest
    environment:
      - ZITI_CTRL_ADVERTISED_ADDRESS=ziti.certiv.pt
      - ZITI_CTRL_ADVERTISED_PORT=6262
      - ZITI_ROUTER_ADVERTISED_ADDRESS=rede.certiv.pt
      - ZITI_ROUTER_PORT=8443
    ports:
      - "8443:8443"

```

**Relevant `config.yml` sections:**

```yaml
ctrl:
  options:
    advertiseAddress: tls:ziti.certiv.pt:6262
  listener: tls:0.0.0.0:6262

edge:
  api:
    address: rede.certiv.pt:1280

web:
  - name: client-management
    bindPoints:
      - interface: 0.0.0.0:1280
        address: rede.certiv.pt:1280
    identity:
      alt_server_certs:
        - server_cert: "/etc/letsencrypt/live/rede.certiv.pt/fullchain.pem"
          server_key: "/etc/letsencrypt/live/rede.certiv.pt/privkey.pem"

```

**Problem:**

When I try to enroll using the Node.js SDK, the enrollment fails with a TLS error. The controller logs show:

```auto
ERR _context=tls:0.0.0.0:1280 

```

error=remote error: tls: unknown certificate

msg=handshake failed

The browser also shows `NET::ERR_CERT_AUTHORITY_INVALID` when accessing `https://rede.certiv.pt:1280/zac`, which suggests the `alt_server_certs` with Let's Encrypt is not being picked up correctly.

**Questions:**

1. Is the `alt_server_certs` being ignored because the FQDN `rede.certiv.pt` overlaps with the PKI somehow?

2. Should the `ctrl.options.advertiseAddress` use port `6262` (which is internal only) or `1280`?

3. What does the `ctrls` field in the enrollment JWT contain for client identities, and does the Node.js SDK actually connect to that address after enrollment?

Thank you!

---

<div class="post-metadata">

**Author:** ![TheLumberjack](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/thelumberjack/32/113_2.png) [@TheLumberjack](https://openziti.discourse.group/u/TheLumberjack)\
**Post date:** [March 9, 2026, 11:57am UTC](https://openziti.discourse.group/t/node-js-sdk-enrollment-fails-with-tls-handshake-error-lets-encrypt-internal-pki-separation/5678/2 "2026-03-09T11:57:38Z")

</div>

Hi @franciscoarruda, welcome to the community and to OpenZiti!

> [@franciscoarruda](#):
>
> which suggests the `alt_server_certs` with Let's Encrypt is not being picked up correctly.

Hmmm. To be honest, this makes me think the LE cert **IS** being served and also makes me think you have shadowed the "external address" of the controller with the LE cert. Overlapping the domain name is a frequent problem and causes non-deterministic behavior. There are multiple discourse posts discussing this problem. First thing to verify is that your controller's "advertised" addresses are always using the private PKI generated by OpenZiti. (assuming you allowed OpenZiti to generate the PKI). You **must not** shadow the external address. Let's check that first and foremost.

> [@franciscoarruda](#):
>
> Should the `ctrl.options.advertiseAddress` use port `6262` (which is internal only) or `1280`?

It's totally up to you. 1280 is the "api port" - it's where all the HTTP requests will land by default. 6262 is the "control plane port" and you will _need_ this port exposed if you plan to add another controller or another router. If you want to use SNI, you can set the port to 1280 and then both HTTP and control plane APIs will be served via the same port.

> [@franciscoarruda](#):
>
> What does the `ctrls` field in the enrollment JWT contain for client identities,

Are you referring to the decoded JWT? That should represent the list of controllers in your OpenZiti overlay network when running in "HA" mode.

Check your advertised address though, this is such a common problem that I expect that's what it is. Oh also make sure you get the latest node sdk, I beleive there have been updates in and around enrollment lately as well. I don't think that's the issue here, but it wouldn't hurt to make sure you're on the latest. hth
