Skip to content
TLS Certificates

TLS Certificates

Netsy uses mutual TLS (mTLS) for authentication for Nodes, Peers, and Clients.

Each Node requires a server certificate (for serving gRPC and the Elector/s3lect election health listener) and a client certificate (for Nodes connecting to Peers).

Presented client certificates embed a role of either peer or client, and Netsy authorises the connection based on that role.

Certificate Requirements

All TLS certificates in a Netsy cluster must be signed by the same Certificate Authority (CA).

Identity via URI SANs

A TLS certificate can carry Subject Alternative Names (SANs) — additional identifiers beyond the certificate’s subject line. SANs are commonly DNS names or IP addresses, but they can also be URIs. A URI SAN embeds a structured identifier inside the certificate itself, so the receiving side can parse it to learn who is connecting without relying on out-of-band configuration.

Netsy identifies certificates using a URI SAN with the format:

netsy://{cluster_id}/{role}/{identity}
ComponentDescriptionExample
cluster_idCluster ID (must match Node config)my-cluster
roleCertificate rolepeer or client
identityPeer: node_id, Client: arbitrary identifiernode-1, my-app

Peer certificates (Node-to-Node):

netsy://my-cluster/peer/node-1

The identity component must match the node’s configured node_id.

Client certificates (external etcd clients):

netsy://my-cluster/client/kube-apiserver

The identity component is an arbitrary client identifier — no node_id matching is required.

Validation Rules

Both cluster_id and node_id must be:

  • Lowercase alphanumeric characters and hyphens only
  • No leading, trailing, or consecutive hyphens
  • Maximum 32 characters

Server Certificate

Used by a Node on the Client API and Peer API gRPC servers. Must include:

  • URI SAN: netsy://{cluster_id}/peer/{node_id}
  • Subject Alternative Names (SANs): the advertise addresses and any literal hostnames/IPs that peers or clients actually dial for this Node (Client API, Peer API, and Elector/s3lect peer and health server)
  • Key Usage: Digital Signature, Key Encipherment
  • Extended Key Usage: TLS Web Server Authentication

At startup, Netsy validates that the certificate identity matches the configured node_id / cluster_id, and that the server certificate SANs cover the configured Client, Peer, and election advertise addresses.

Node Client Certificate

Used by a Node when connecting to other Peer gRPC servers. Must include:

  • URI SAN: netsy://{cluster_id}/peer/{node_id}
  • Key Usage: Digital Signature
  • Extended Key Usage: TLS Web Client Authentication

Client API Client Certificates

External “etcd clients” are not cluster Nodes. When a Client connects to the Client API with mTLS, Netsy verifies the URI SAN has the client role and the correct cluster_id. Only peer certificates are required to match a Node’s node_id.

Generating Certificates with OpenSSL

The examples below use the following variables:

CLUSTER_ID="my-cluster"
NODE_ID="node-1"
DAYS=365

# Addresses for SANs (adjust to your environment)
CLIENT_ADDR_IP4="172.16.0.1"
CLIENT_ADDR_IP6="2001:db8::10"
PEER_ADDR_IP4="172.16.0.1"
PEER_ADDR_IP6="2001:db8::10"
ELECTOR_ADDR_IP4="172.16.0.1"
ELECTOR_ADDR_IP6="2001:db8::10"

Note that if you want to use DNS names instead of IPs for the Client, Peer, and Elector (s3lect) addresses the examples below will need to be adjusted accordingly. Include SANs only for the actual addresses or hostnames that callers use.

1. Create the Cluster CA

Generate a CA certificate and key. This CA is shared across all Nodes in the cluster.

openssl genrsa -out ca.key 4096

openssl req -x509 -new -nodes \
  -key ca.key \
  -sha256 \
  -days $DAYS \
  -out ca.crt \
  -subj "/O=${CLUSTER_ID}/CN=${CLUSTER_ID}-ca"

2. Generate the Node Server Certificate

Create a config file for the server certificate SANs:

cat > server.cnf <<EOF
[req]
distinguished_name = req_dn
req_extensions = v3_req
prompt = no

[req_dn]
CN = ${NODE_ID}

[v3_req]
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
IP.1 = ${CLIENT_ADDR_IP4}
IP.2 = ${CLIENT_ADDR_IP6}
IP.3 = ${PEER_ADDR_IP4}
IP.4 = ${PEER_ADDR_IP6}
IP.5 = ${ELECTOR_ADDR_IP4}
IP.6 = ${ELECTOR_ADDR_IP6}
IP.7 = 127.0.0.1
IP.8 = ::1
DNS.1 = localhost
URI.1 = netsy://${CLUSTER_ID}/peer/${NODE_ID}
EOF

Generate the server key, CSR, and signed certificate:

openssl genrsa -out server.key 4096

openssl req -new \
  -key server.key \
  -out server.csr \
  -config server.cnf

openssl x509 -req \
  -in server.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out server.crt \
  -days $DAYS \
  -sha256 \
  -extensions v3_req \
  -extfile server.cnf

3. Generate the Node Client Certificate

Create a config file for the client certificate:

cat > client.cnf <<EOF
[req]
distinguished_name = req_dn
req_extensions = v3_req
prompt = no

[req_dn]
CN = ${NODE_ID}

[v3_req]
keyUsage = digitalSignature
extendedKeyUsage = clientAuth
subjectAltName = @alt_names

[alt_names]
URI.1 = netsy://${CLUSTER_ID}/peer/${NODE_ID}
EOF

Generate the client key, CSR, and signed certificate:

openssl genrsa -out client.key 4096

openssl req -new \
  -key client.key \
  -out client.csr \
  -config client.cnf

openssl x509 -req \
  -in client.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out client.crt \
  -days $DAYS \
  -sha256 \
  -extensions v3_req \
  -extfile client.cnf

4. Verify Certificates

Verify the subject fields and SANs are correct:

# Check server certificate
openssl x509 -in server.crt -noout -subject -ext subjectAltName
# Expected: subject=CN=node-1
# Expected SAN: URI:netsy://my-cluster/peer/node-1, IP:..., DNS:localhost

# Check client certificate
openssl x509 -in client.crt -noout -subject -ext subjectAltName
# Expected: subject=CN=node-1
# Expected SAN: URI:netsy://my-cluster/peer/node-1

# Verify both are signed by the CA
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile ca.crt client.crt

Netsy Configuration

Once certificates are generated, configure the Node. TLS cert paths and node identity are per-node settings (env vars), while cluster_id is a per-cluster setting (config file).

Per-node env vars:

export NETSY_NODE_ID="node-1"
export NETSY_TLS_CA_CERT=./certs/ca.crt
export NETSY_TLS_SERVER_CERT=./certs/server.crt
export NETSY_TLS_SERVER_KEY=./certs/server.key
export NETSY_TLS_CLIENT_CERT=./certs/peer.crt
export NETSY_TLS_CLIENT_KEY=./certs/peer.key

Per-cluster config file (see Configuration for the full reference):

{
  "cluster_id": "my-cluster"
}

Multi-Node Example

For a 3-node cluster, repeat steps 2–3 for each node with a different NODE_ID and the appropriate addresses. The CA (step 1) is created once and shared. All nodes use the same ca.crt and cluster config file.

# Node 1
NODE_ID="node-1" CLIENT_ADDR_V4="172.16.0.1" CLIENT_ADDR_V6="2001:db8::11" PEER_ADDR_V4="172.16.0.1" PEER_ADDR_V6="2001:db8::11" ELECTOR_ADDR_V4="172.16.0.1" ELECTOR_ADDR_V6="2001:db8::11"

# Node 2
NODE_ID="node-2" CLIENT_ADDR_V4="172.16.0.2" CLIENT_ADDR_V6="2001:db8::12" PEER_ADDR_V4="172.16.0.2" PEER_ADDR_V6="2001:db8::12" ELECTOR_ADDR_V4="172.16.0.2" ELECTOR_ADDR_V6="2001:db8::12"

# Node 3
NODE_ID="node-3" CLIENT_ADDR_V4="172.16.0.3" CLIENT_ADDR_V6="2001:db8::13" PEER_ADDR_V4="172.16.0.3" PEER_ADDR_V6="2001:db8::13" ELECTOR_ADDR_V4="172.16.0.3" ELECTOR_ADDR_V6="2001:db8::13"