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.
- 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
| Side | Responsibility |
|---|---|
| Satellite host | Add a TLS listener to HAProxy (this guide uses port 8443) with a certificate whose SAN matches the hostname sensors will dial. |
| Each sensor host | Trust that certificate (or its issuing CA), point LEVO_SATELLITE_URL at https://…, and keep TLS verification enabled. |
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-sensorpackage 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.
1.3 Enterprise PKI (recommended for production)
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:
- A sensor-facing frontend that accepts TLS ClientHellos on your chosen port (
8443below). - An internal TLS bind that loads
/levo/site-cert.pem(yourcombined.pem). - The same path-based routing to
levoai-satelliteandlevoai-collectorthat the stock frontend already performs — copy it across rather than rewriting it, so no route is lost.
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
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 setting | What 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: false | Nothing. 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.
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
| Distribution | Install |
|---|---|
| Ubuntu / Debian | sudo cp … /usr/local/share/ca-certificates/levo-satellite-ca.crt && sudo update-ca-certificates |
| RHEL / CentOS / Rocky / Alma / Fedora / Amazon Linux | sudo cp … /etc/pki/ca-trust/source/anchors/levo-satellite-ca.crt && sudo update-ca-trust extract |
| SUSE / openSUSE | sudo cp … /etc/pki/trust/anchors/levo-satellite-ca.crt && sudo update-ca-certificates |
| Alpine | sudo cp … /usr/local/share/ca-certificates/levo-satellite-ca.crt && sudo update-ca-certificates |
| Arch | sudo 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.
| Symptom | Likely cause |
|---|---|
certificate signed by unknown authority | Cert not in the OS trust store, or update-ca-certificates / update-ca-trust was not run. |
x509: certificate is valid for X, not Y | Hostname in LEVO_SATELLITE_URL is missing from the certificate SAN — reissue the cert. |
certificate has expired | Reissue, 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 carefullyThe setting is named for what it disables, so the value looks backwards from what you want:
| Value | Effect |
|---|---|
false (default, and what you want) | Certificates are verified. |
true | Verification 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 installsWhether this setting works depends on how you installed the Sensor:
| Install method | LEVO_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 certificatex509: certificate signed by unknown authorityconnection refused
Checklist
-
site.crt,site.key, andcombined.pemexist 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 -
9999and4317are no longer published on the host (removed, or bound to127.0.0.1) -
levoai-haproxyis healthy and/healthzreturns 200 over HTTPS - Every sensor host has the CA/leaf at
/etc/levo/sensor/levo-satellite-ca.crtand in the OS trust store -
tls-ca-cert-pathis set andignore-ssl-verifyremainsfalse - 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:
| Platform | Guide |
|---|---|
| Kubernetes | Sensor on Kubernetes |
| AWS ECS | Sensor on AWS ECS using Terraform |
| Docker | Sensor via Docker |
| Debian / Ubuntu | Sensor via APT Package |
| RHEL / Amazon Linux | Sensor via YUM Package |
| Linux host (systemd) | Sensor as a Systemd Service |
Related configuration guides
- Manage Sensor Configuration — the full configuration file and every setting
- API Traffic Capture Filters — control exactly which traffic is captured
- Kubernetes Configuration — tolerations, affinity and node selectors for Sensor pods