Skip to content

HTTPS in front of CodeNavi

CodeNavi never needs to know about TLS. It serves plain HTTP on 127.0.0.1:8765; you put a TLS terminator next to it that forwards to that loopback port, and every client talks HTTPS to the terminator. The install directory, the .env file and the container stay exactly as the installer left them — this page is for the operator who wants a secure URL for /mcp and /ui without touching any of that.

The bearer token keeps working unchanged: a reverse proxy passes the Authorization header through, so the same CODENAVI_AUTH_TOKEN gates the HTTPS URL. Make sure the token is enabled first — Network & authentication.

Three ways, least effort first. Everything below runs beside CodeNavi (a host package, or a separate container), never inside its compose project.

Option 1 — Tailscale Serve / Funnel (one command)

sudo tailscale serve  --bg 8765     # HTTPS for your tailnet only
sudo tailscale funnel --bg 8765     # or: public HTTPS, no inbound firewall change

Tailscale provisions a Let's Encrypt certificate, renews it, and gives the host a stable name. Point clients at https://<host>.<tailnet>.ts.net/mcp with the same bearer header. Verify with tailscale serve status. This is the right choice for a single box, and the walkthrough (admin-console settings, HTTPS enablement) is in Remote access → Option A.

Option 2 — Caddy (three lines, real domain)

codenavi.example.com {
    reverse_proxy 127.0.0.1:8765
}

Caddy handles ACME, streamed responses and long-lived requests by default — nothing else to tune. It needs DNS pointing at the host and ports 80/443 reachable for the certificate challenge. Run it as a system package, or as its own container with network_mode: host so it can reach the loopback port.

Option 3 — nginx (three settings are mandatory)

MCP over Streamable HTTP answers with text/event-stream, and some tool calls (a full reindex, a wiki compile) run for minutes. nginx's defaults buffer responses and time out at 60 s, which breaks both. Keep the three marked lines:

server {
    listen 443 ssl;
    server_name codenavi.example.com;

    ssl_certificate     /etc/letsencrypt/live/codenavi.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/codenavi.example.com/privkey.pem;

    location / {
        proxy_pass         http://127.0.0.1:8765;
        proxy_http_version 1.1;              # required — streamed (SSE) responses
        proxy_buffering    off;              # required — never buffer a stream
        proxy_read_timeout 900s;             # required — long tool calls
        proxy_set_header   Host              $host;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        client_max_body_size 10m;
    }
}

Certificates via certbot as usual (certbot --nginx -d codenavi.example.com). CodeNavi reads X-Forwarded-For only when the proxy is on the same host (loopback) — that is how the per-client throttle tells your clients apart behind the proxy. A proxy on another host goes into THROTTLE_TRUSTED_PROXIES, see Configuration — otherwise every client behind it shares one request budget and a busy team can see 429s that no single agent caused. The other X-Forwarded-* headers are for your access logs.

Before you hand out the URL

Check the plain port is not also exposed

A default install binds CODENAVI_HOST=127.0.0.1, so only a proxy on the same host can reach :8765 — that is the whole point. If the port was opened to the LAN earlier (CODENAVI_HOST=0.0.0.0, the same-LAN recipe), the proxy adds HTTPS but plain HTTP stays reachable in parallel. Close it at the firewall rather than editing the install:

sudo ufw deny 8765                                   # Ubuntu/Debian
sudo firewall-cmd --permanent --remove-port=8765/tcp && sudo firewall-cmd --reload   # Fedora/RHEL

Test the auth path from a remote machine

Run these from somewhere other than the CodeNavi host — testing on the host itself proves nothing about the proxy.

# 1. liveness through the proxy — expect 200 and "auth_required": true in the body
curl -si https://codenavi.example.com/health

# 2. no token — expect 401 (the proxy passes auth through, not around)
curl -si -X POST https://codenavi.example.com/mcp \
     -H 'Accept: application/json, text/event-stream' \
     -H 'Content-Type: application/json' -d '{}'

# 3. with the token — expect 200
curl -si -X POST https://codenavi.example.com/mcp \
     -H "Authorization: Bearer $CODENAVI_AUTH_TOKEN" \
     -H 'Accept: application/json, text/event-stream' \
     -H 'Content-Type: application/json' -d '{}'

If step 2 returns 200, the proxy is injecting or stripping headers — stop and fix that before anyone connects. If step 3 hangs, the proxy is buffering the stream (nginx: proxy_buffering off missing).

Optional: tell CodeNavi its public URL

Without it everything above works — documented clients send the bearer token directly. What changes: the self-describing URLs CodeNavi emits (/health's mcp_endpoint, and the OAuth resource-metadata pointer a client is given after a 401) read http://localhost:8765, so a client with a missing or wrong token is sent to its own machine instead of a clear auth error. If you can afford one restart, set CODENAVI_PUBLIC_URL=https://codenavi.example.com in deploy/.env.1 If the rule is "don't touch the install", skip it — it is cosmetic.

What this does and does not give you

Encrypted /mcp, /ui, /connect, /health, /plugins/* Yes — they all share one port, so one proxy rule covers all of them
Certificate issue and renewal Yes — by Tailscale or Caddy automatically; by certbot for nginx
Per-user identity / SSO No — the token is one shared secret. Put oauth2-proxy (or your IdP's equivalent) in front for that; same pattern, one more hop
Protection of the plain port No — see the firewall check above

Everything on this page is reversible: stop the proxy and CodeNavi is exactly as it was.


  1. Vendor change reference CN-743 — CODENAVI_PUBLIC_URL (release after 1.3.40). ↩