Skip to main content

Setting up TLS (Transport Layer Security) from Sensor to Satellite

Secure the hop between the Levo eBPF Sensor and the Levo Satellite so sensors export traffic only over HTTPS/TLS. This guide covers Docker Compose Satellite deployments, where HAProxy presents the TLS listener that sensors dial.

Scope
  • In scope: TLS between Sensor → Satellite (certificate on the Satellite listener; trust + HTTPS URL on each sensor host).
  • Out of scope: TLS between your applications and their clients, and mutual TLS (mTLS). Contact Levo Support if you need mTLS.

What you will configure​

SideResponsibility
Satellite hostAdd a TLS listener to HAProxy (this guide uses port 8443) with a certificate whose SAN matches the hostname sensors will dial.
Each sensor hostTrust that certificate (or its issuing CA), point LEVO_SATELLITE_URL at https://…, and keep TLS verification enabled.
The stock Satellite does not serve TLS

Out of the box, levoai-haproxy serves plain HTTP with no certificate configured. Sensors reach it on port 80 of the Satellite host — Compose publishes it as 80:8080, so HAProxy listens on 8080 inside the container. This guide adds a TLS listener; it is not a matter of switching one on.

Port 8443 is simply the port this guide picks for that new listener — you can use any free port, as long as HAProxy's bind, the Compose ports: mapping and LEVO_SATELLITE_URL all agree.

Do not point the eBPF Sensor at 9999, 4317 or 4318. Those are the Satellite's and Collector's own ports, which HAProxy routes to. 9999 in particular is the Satellite's REST API, which does not speak the gRPC protocol the eBPF Sensor exports traces over — HAProxy is what routes each request to the right one. (The PCAP Sensor is different and can use 9999; see its own install guide.)

These ports are not internal by default. The stock Compose file publishes 9999:9999 and 4317:4317 on every host interface. Both accept plaintext and neither enforces HAProxy's organization check, so as long as they stay published, anyone who can reach the host can bypass TLS entirely. Step 2.3 closes them.

Recommended layout on the Satellite host:

/opt/levo/haproxy/
site.crt # public certificate (or full chain)
site.key # private key (mode 600)
combined.pem # site.crt (+ intermediates) + site.key for HAProxy
haproxy.cfg.template
haproxy-ecs.cfg.template

Replace placeholder hostnames (satellite.example.com) and paths with values from your environment.

Prerequisites​

  • Docker Compose v2 on the Satellite host
  • A DNS name (preferred) or stable IP for the Satellite that every sensor host can reach
  • Root or sudo on the Satellite host and on each sensor host
  • The levo-ebpf-sensor package already installed on sensor hosts

1. Create the Satellite server certificate​

Run these steps on a trusted machine (typically the Satellite host). The Sensor validates the server certificate against Subject Alternative Name (SAN) entries — not the Common Name alone. Every hostname or IP used in LEVO_SATELLITE_URL must appear in the SAN list.

1.1 Working directory​

sudo mkdir -p /opt/levo/haproxy
cd /opt/levo/haproxy

1.2 Self-signed certificate (lab or closed networks)​

openssl req -x509 -nodes -newkey rsa:4096 -sha256 -days 825 \
-keyout site.key -out site.crt \
-subj "/C=US/O=Example Corp/OU=Platform Security/CN=satellite.example.com" \
-addext "subjectAltName=DNS:satellite.example.com,DNS:localhost,IP:127.0.0.1"

Adjust subjectAltName to include every DNS name and IP sensors will use. Prefer ≤ 825 days for public-trust style rotation policies even on private PKI.

If you already operate an internal CA:

openssl req -new -newkey rsa:4096 -nodes -keyout site.key -out site.csr \
-subj "/CN=satellite.example.com" \
-addext "subjectAltName=DNS:satellite.example.com"

Have your CA sign site.csr to produce site.crt (include any required intermediate certificates). Sensors should trust the CA root (or the chain you distribute), not only the leaf.

1.4 PEM file for HAProxy​

HAProxy loads certificate and key from a single file referenced by crt on the bind line:

# Self-signed / single leaf:
cat site.crt site.key > combined.pem

# With intermediates (leaf, then intermediates, then key):
# cat site.crt intermediate.crt site.key > combined.pem

sudo chmod 600 combined.pem site.key
sudo chmod 644 site.crt

2. Enable TLS on the Satellite listener (HAProxy)​

In Docker Compose deployments, levoai-haproxy fronts Satellite and Collector. The stock configuration serves a single plain-HTTP frontend on 8080 inside the container, published on host port 80. You replace it with a TLS-terminating frontend so HAProxy completes the handshake at the edge and forwards cleartext to the internal backends over the container network.

2.1 Required HAProxy settings​

This replaces the stock http-in frontend in both haproxy.cfg.template and haproxy-ecs.cfg.template under /opt/levo/haproxy/. The backends stay as they are. You need:

  1. A sensor-facing frontend that accepts TLS ClientHellos on your chosen port (8443 below).
  2. An internal TLS bind that loads /levo/site-cert.pem (your combined.pem).
  3. The same path-based routing to levoai-satellite and levoai-collector that the stock frontend already performs — copy it across rather than rewriting it, so no route is lost.
Keep your own route list

The reference below reflects the routes at time of writing. Your installed haproxy.cfg.template is the source of truth — diff it against this before replacing anything, and carry over any routes your version has that this one does not.

Reference HAProxy configuration (Docker Compose)

Paste into both haproxy.cfg.template and haproxy-ecs.cfg.template (keep them identical for Compose):

global
log stdout format raw local0
maxconn 1024

defaults
log global
timeout client 60s
timeout connect 60s
timeout server 60s

frontend tcp-in
bind :8443
mode tcp
tcp-request inspect-delay 5s
# TLS only: anything that does not open with a TLS ClientHello is dropped.
tcp-request content accept if { req_ssl_hello_type 1 }
tcp-request content reject
default_backend bk_tls

backend bk_tls
mode tcp
server loopback 127.0.0.1:8444 send-proxy

frontend https-in
bind :8444 accept-proxy ssl crt /levo/site-cert.pem alpn h2,http/1.1
mode http
monitor-uri /healthz
http-request set-var(req.authn_enabled) bool(${LEVOAI_SATELLITE_AUTHN_ENABLED})
acl is_health_check path_beg /healthz
acl prefix_paths path_beg /v1/ /1.0/ /tracer-config /sensor-config /pcap-sensor-config /sensor-btf /sensor-health
acl has_paths path -i -m sub opentelemetry.proto.collector
acl valid_org_id hdr(x-levo-organization-id) -i ${LEVOAI_ORG_ID}
http-request deny if { var(req.authn_enabled) -m bool } !valid_org_id !is_health_check prefix_paths
http-request deny if { var(req.authn_enabled) -m bool } !valid_org_id !is_health_check has_paths
default_backend levoai-collector-4317
use_backend levoai-collector-4318 if { path_beg /v1/traces } || { path_beg /v1/metrics } || { path_beg /v1/logs }
use_backend levoai-collector-4320 if { path_beg /v1/cloudfront-event }
use_backend levoai-collector-4322 if { path_beg /v1/edgeworker-event }
use_backend levoai-collector-4323 if { path_beg /v1/f5-ltm-logs }
use_backend levoai-satellite-9999 if { path_beg /1.0/ebpf/traces } || { path_beg /1.0/suricata } || { path_beg /1.0/har } || { path_beg /1.0/flush } || { path_beg /tracer-config } || { path_beg /sensor-config } || { path_beg /pcap-sensor-config } || { path_beg /sensor-btf } || { path_beg /sensor-health }

frontend health-in
bind :8080 alpn h2,http/1.1
mode http
monitor-uri /healthz
default_backend levoai-collector-4317

backend levoai-collector-4317
mode http
server levoai-collector levoai-collector:4317 proto h2

backend levoai-collector-4318
mode http
server levoai-collector levoai-collector:4318

backend levoai-collector-4320
mode http
server levoai-collector levoai-collector:4320

backend levoai-collector-4322
mode http
server levoai-collector levoai-collector:4322

backend levoai-collector-4323
mode http
server levoai-collector levoai-collector:4323

backend levoai-satellite-9999
mode http
server levoai-satellite levoai-satellite:9999

Notes on HAProxy keywords: ssl, req_ssl_hello_type, and crt are HAProxy configuration tokens. They refer to the TLS listener even though the keyword spelling is historical.

2.2 Optional: change the sensor-facing port​

Update the tcp-in bind (for example to 9443) in both templates. Leave the internal loopback port 8444 unchanged — do not publish it on the host.

2.3 Mount the certificate and publish the listener​

Edit the existing levoai-haproxy service in your docker-compose.yml rather than replacing it — leave its image, restart, healthcheck, depends_on, environment and logging as they are. You change two things:

levoai-haproxy:
# ... keep everything that is already here ...
ports:
- "8443:8443" # NEW: the TLS listener. Must match the tcp-in bind (e.g. "9443:9443").
# - '80:8080' # the stock plain-HTTP listener. Remove it once every Sensor uses https://.
volumes: # NEW: add this block
- /opt/levo/haproxy/haproxy.cfg.template:/usr/local/etc/haproxy/haproxy.cfg.template:ro
- /opt/levo/haproxy/haproxy-ecs.cfg.template:/usr/local/etc/haproxy/haproxy-ecs.cfg.template:ro
- /opt/levo/haproxy/combined.pem:/levo/site-cert.pem:ro
Applying this stops serving plain-HTTP sensors on port 80

In the reference config, port 8080 (published as 80) is only a health-check frontend (health-in), which sends everything except /healthz to the Collector's gRPC port. For Sensors still pointed at http://…:80:

Sensor settingWhat still works
collector-grpc-transport: true (default)Trace export only. /sensor-health, /sensor-config, /tracer-config and /sensor-btf no longer reach the Satellite.
collector-grpc-transport: falseNothing. Traces go to /v1/traces over HTTP/1.1, which health-in does not route, so trace export stops as well.

Port 8443 accepts TLS only.

To migrate sensors one at a time, keep your stock http-in frontend in place of health-in while you switch them over. Both bind 8080, so keep only one. http-in keeps serving http:// sensors on port 80 and still answers the health check. Once every Sensor's LEVO_SATELLITE_URL starts with https://, swap http-in back to health-in and remove the '80:8080' mapping.

Either way the container healthcheck (curl http://localhost:8080/healthz) keeps working, since both frontends serve /healthz on 8080.

Keep these three values aligned: HAProxy tcp-in bind, Compose ports: mapping, and LEVO_SATELLITE_URL on every sensor.

Close the backend ports​

HAProxy is only a TLS boundary if it is the only way in. The stock Compose file also publishes the Satellite and the Collector directly, and both accept plaintext without HAProxy's organization check:

levoai-satellite:
ports:
- '9999:9999' # Satellite REST API — plaintext
levoai-collector:
ports:
- '4317:4317' # Collector gRPC — plaintext

Remove these mappings, or bind them to loopback if something on the Satellite host itself still needs them:

levoai-satellite:
ports:
- '127.0.0.1:9999:9999'
levoai-collector:
ports:
- '127.0.0.1:4317:4317'

HAProxy is unaffected either way. It reaches levoai-satellite:9999 and levoai-collector:4317 over the Compose network, which does not depend on host port mappings.

Move direct-port clients first

Anything that dials 9999 or 4317 directly on the host stops working when you close these ports. That includes PCAP Sensors configured with a …:9999 Satellite URL. Repoint them at the HAProxy URL before removing the mappings.

Check from another machine that only the TLS port answers:

for p in 8443 9999 4317 80; do
printf '%-5s ' "$p"; timeout 3 bash -c "</dev/tcp/satellite.example.com/$p" 2>/dev/null && echo open || echo closed
done

Expect 8443 open and the other three closed. Port 80 stays open until you finish migrating sensors (see the caution above).

2.4 Apply and verify the Satellite certificate​

cd /path/to/your/satellite-compose   # directory that contains docker-compose.yml
docker compose up -d levoai-haproxy
docker compose logs --tail=50 levoai-haproxy

openssl s_client -connect satellite.example.com:8443 \
-servername satellite.example.com </dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName

Confirm the presented certificate matches the SAN list and validity window you expect.

3. Trust the certificate on each sensor host​

Copy the trust material from the Satellite host to every sensor:

  • Self-signed: distribute site.crt (the leaf).
  • Enterprise CA: distribute the CA root (or the chain your security team standardizes on).
# On each sensor host
sudo mkdir -p /etc/levo/sensor
sudo scp user@satellite-host:/opt/levo/haproxy/site.crt \
/etc/levo/sensor/levo-satellite-ca.crt
sudo chmod 644 /etc/levo/sensor/levo-satellite-ca.crt

3.1 Install into the OS trust store​

DistributionInstall
Ubuntu / Debiansudo cp … /usr/local/share/ca-certificates/levo-satellite-ca.crt && sudo update-ca-certificates
RHEL / CentOS / Rocky / Alma / Fedora / Amazon Linuxsudo cp … /etc/pki/ca-trust/source/anchors/levo-satellite-ca.crt && sudo update-ca-trust extract
SUSE / openSUSEsudo cp … /etc/pki/trust/anchors/levo-satellite-ca.crt && sudo update-ca-certificates
Alpinesudo cp … /usr/local/share/ca-certificates/levo-satellite-ca.crt && sudo update-ca-certificates
Archsudo trust anchor --store /etc/levo/sensor/levo-satellite-ca.crt

3.2 Connectivity check (without disabling verification)​

curl -v https://satellite.example.com:8443/healthz

This must succeed without -k / --insecure.

Common TLS errors
SymptomLikely cause
certificate signed by unknown authorityCert not in the OS trust store, or update-ca-certificates / update-ca-trust was not run.
x509: certificate is valid for X, not YHostname in LEVO_SATELLITE_URL is missing from the certificate SAN — reissue the cert.
certificate has expiredReissue, rebuild combined.pem, restart levoai-haproxy.

4. Point the Sensor at the TLS Satellite URL​

4.1 Sensor config (/etc/levo/sensor/config.yaml)​

Keep verification enabled. Point the sensor at the same CA file you installed:

# TLS Settings: connectivity with Satellite
ignore-ssl-verify: false # false = verification ON. See the note below.
# tls-client-cert-path: "" # mTLS only
# tls-client-key-path: "" # mTLS only
tls-ca-cert-path: /etc/levo/sensor/levo-satellite-ca.crt
ignore-ssl-verify is a double negative — read it carefully

The setting is named for what it disables, so the value looks backwards from what you want:

ValueEffect
false (default, and what you want)Certificates are verified.
trueVerification is skipped — the Sensor will trust any certificate, including a forged one.

So "keep TLS verification enabled" means leaving this at false. Setting it to true to make a certificate error go away defeats the point of this entire guide — fix the certificate or the trust store instead.

OS trust (section 3) makes host tooling (curl, diagnostics) work. tls-ca-cert-path pins the sensor process to the Satellite CA for reproducible deployments.

4.2 Runtime defaults (/etc/default/levo-ebpf-sensor)​

Edit the existing file in place — do not overwrite it. The package ships MALLOC_CONF, CPU_LIMIT and MEMORY_LIMIT in this file, and the Sensor reads them at startup. Replacing the whole file silently drops them.

sudo sed -i \
-e 's|^LEVO_SATELLITE_URL=.*|LEVO_SATELLITE_URL="https://satellite.example.com:8443"|' \
-e 's|^LEVO_ORG_ID=.*|LEVO_ORG_ID="<your-organization-id>"|' \
-e 's|^LEVO_ENV=.*|LEVO_ENV="prod"|' \
/etc/default/levo-ebpf-sensor

cat /etc/default/levo-ebpf-sensor

LEVO_SATELLITE_URL must use https:// and a hostname or IP present in the server certificate's SAN.

LEVO_ORG_ID must match the Satellite compose environment (LEVOAI_ORG_ID). Find it in the Levo console under Settings → Organization.

LEVO_WORKSPACE_ID is ignored on RPM installs

Whether this setting works depends on how you installed the Sensor:

Install methodLEVO_WORKSPACE_ID
.deb (APT — Debian, Ubuntu)Works — the unit passes --workspace-id.
.rpm (YUM — RHEL, Amazon Linux)Ignored — the unit does not pass --workspace-id.
Debug packages (either format)Ignored.

On an affected install, add a systemd drop-in:

sudo mkdir -p /etc/systemd/system/levo-ebpf-sensor.service.d
printf '[Service]\nExecStart=\nExecStart=/sbin/init_lite --levo-env ${LEVO_ENV} --satellite-url ${LEVO_SATELLITE_URL} --organization-id ${LEVO_ORG_ID} --workspace-id ${LEVO_WORKSPACE_ID}\n' \
| sudo tee /etc/systemd/system/levo-ebpf-sensor.service.d/workspace.conf
sudo systemctl daemon-reload

5. Restart and verify​

sudo systemctl daemon-reload
sudo systemctl restart levo-ebpf-sensor
sudo systemctl status levo-ebpf-sensor --no-pager
sudo journalctl -u levo-ebpf-sensor -f --no-pager

A healthy start resolves the Satellite URL, completes the TLS handshake, and begins exporting traces. Investigate any of:

  • tls: failed to verify certificate
  • x509: certificate signed by unknown authority
  • connection refused

Checklist​

  • site.crt, site.key, and combined.pem exist under /opt/levo/haproxy/ with correct permissions
  • Certificate SAN covers every hostname/IP used in LEVO_SATELLITE_URL
  • HAProxy bind port, Compose ports: mapping, and sensor URL port all match
  • 9999 and 4317 are no longer published on the host (removed, or bound to 127.0.0.1)
  • levoai-haproxy is healthy and /healthz returns 200 over HTTPS
  • Every sensor host has the CA/leaf at /etc/levo/sensor/levo-satellite-ca.crt and in the OS trust store
  • tls-ca-cert-path is set and ignore-ssl-verify remains false
  • Sensor service restarted; logs show a successful TLS handshake with the Satellite

Installing the Sensor​

Installing a Sensor for the first time, or adding one on another host? Start at Install eBPF Sensor, or go straight to your platform:

PlatformGuide
KubernetesSensor on Kubernetes
AWS ECSSensor on AWS ECS using Terraform
DockerSensor via Docker
Debian / UbuntuSensor via APT Package
RHEL / Amazon LinuxSensor via YUM Package
Linux host (systemd)Sensor as a Systemd Service
Was this page helpful?