Tutorials
Turn on end-to-end TLS
Enable end-to-end TLS so the agent terminates HTTPS on your machine with its own Let's Encrypt certificate — the private key never leaves the host.
Turn on end-to-end TLS
By default, Kepeink's edge terminates HTTPS for your assigned hostname. With end-to-end TLS the agent terminates TLS on your machine instead, using its own Let's Encrypt certificate. Kepeink passes the encrypted bytes straight through to the agent, so the certificate's private key is generated on your machine and never leaves it.
Turn this on when you want the TLS session to reach all the way to your own host — for example to keep the certificate and key entirely under your control.
Two scopes: whole-tunnel (rproxy) and custom domain
Both use the same TLS-ALPN-01 plumbing and the same agent cert cache — the difference is which hostname the agent holds a certificate for:
| Scope | Hostname | How to enable | Cost | What changes |
|---|---|---|---|---|
| Whole-tunnel | the assigned *.rproxy.*.xyz SNI on your tunnel | POST /api/v1/tunnels/{id}/e2e {"enabled": true} (owner-only) — see also the dashboard toggle | Paid feature grant e2e_tls (one grant per workspace) | Tunnel flips to tls_mode=passthrough + cert_source=agent_managed_acme; the agent obtains the cert for the rproxy name itself |
| Custom domain | your own domain app.example.com attached to the tunnel | POST /api/v1/custom-domains/{id}/e2e {"enabled": true} (owner-only) | Free — no grant needed | Custom domain flips to cert_source=agent_managed_acme; the edge pipes that hostname's traffic passthrough while the tunnel's rproxy SNI can stay terminated. Both can be active at the same time on one tunnel. |
Which you need for a given job:
- Immich/custom service on the assigned
rproxyhostname with no domain of your own → whole-tunnelPOST /api/v1/tunnels/{id}/e2e. - Branded URL
app.example.comwhere that name's TLS terminates on your box → custom-domainPOST /api/v1/custom-domains/{id}/e2e(after you attach the domain with Add a custom domain). The cert counts against your registered domain, not Kepeink's shared pool — hence free.
Constraints on whole-tunnel enable: no cert_worker custom domains attached to that tunnel (those need the edge to terminate — detach or convert them first; agent_managed_acme domains are compatible), per-registered-domain issuance headroom (Let's Encrypt: 50 certs/domain/week — we guard at 40), and disable always succeeds. Disabling either scope reverts to the Kepeink-issued shared wildcard / cert_worker flow (brief downtime while that cert is obtained).
Requirements
- For custom-domain end-to-end: a custom domain already attached to the tunnel. Set that up first with Add a custom domain. For whole-tunnel end-to-end: the workspace holds the
e2e_tlsfeature grant (bought as an add-on or provisioned by an operator code). - The agent must be able to complete Let's Encrypt's TLS-ALPN-01 validation. Kepeink forwards the validation connection straight down the passthrough tunnel to the agent — no
acmefield on the control channel, no HTTP-01 port-80 listener. Nothing else to open; the passthrough relay *is* the challenge transport.
1. Enable it
Via API (pasteable — the same toggle the dashboard uses):
# Whole-tunnel (rproxy) — owner token, bearer kpm_…
curl -fsS -X POST https://manage.kepeink.hu/api/v1/tunnels/$TUNNEL_ID/e2e \
-H "Authorization: Bearer $KEPEINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
# → { tunnel_id, e2e_enabled: true, tls_mode: "passthrough", cert_source: "agent_managed_acme" }
# Custom domain — same auth, different resource
curl -fsS -X POST https://manage.kepeink.hu/api/v1/custom-domains/$DOMAIN_ID/e2e \
-H "Authorization: Bearer $KEPEINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
Or in the dashboard, open the tunnel (or the custom domain's detail) and turn on end-to-end TLS. Kepeink switches that hostname to passthrough — raw TLS is piped to the agent rather than terminated at the edge.
2. Let the agent obtain a certificate
The agent runs its own ACME client on the next connection:
- Whole-tunnel:
HostWhitelist(sni)for therproxyname. - Custom domain: one
HostWhitelistentry peragent_managed_acmehostname (tunnel SNI + everye2e_hostnamesentry the config frame carries).
Cache: on-disk at <agent data dir>/certs (mode 0700) — same dir the host installer persists across reboots. The warm-issuance loop pre-fetches certs for the whole host set so the first visitor does not pay the ACME round-trip; if a visitor arrives before it completes, the first handshake triggers issuance inline (bounded by a per-handshake timeout — 30 s) and later handshakes are immediate. Subsequent renewals are automatic. With the Docker image registry.kepeink.hu/agent:2 the data dir is /home/nonroot, declared as a VOLUME so an image update keeps the cache; docker compose down and each fresh docker run still discard an *anonymous* volume, so mount a named volume at /home/nonroot. This is worth doing even though re-issuance only costs you ~15 s: the cache holds the ACME account key, and losing it forfeits the renewal exemption, so every recreate spends a new issuance from a rate limit shared with every other tunnel on the same rproxy domain.
Each issuance is recorded against the tunnel in your workspace's audit log (Tunnel Acme Certificate Issued, with the hostname, the certificate serial and its validity window), so a tunnel that is quietly re-issuing — the usual sign of a container recreating without a volume — is visible rather than something you find out about from a rate limit.
3. Verify
Once the certificate is issued, load your domain and inspect the certificate in your browser — it is the one the agent obtained, served from your machine. You can confirm the chain from the command line too:
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -issuer -subject
For an rproxy whole-tunnel name, replace app.example.com with that tunnel's sni (visible in the tunnel API response and the dashboard).
Notes
- The private key is created and kept on the machine running the agent; it is never transmitted to Kepeink.
- The agent renews the certificate automatically before it expires.
- Because Kepeink no longer terminates TLS for this hostname, the certificate lifecycle is handled entirely by the agent on your host.
- Public HTTPS anyone can hit still produces scanner/noise logs. In end-to-end mode the agent itself terminates TLS, so internet-wide scanners (and a first-visit issuance) log warnings like `e2e-tls handshake failed; closing stream err=EOF` — these are expected for any public endpoint and are rate-limited to one log line per 30 s (
hsWarn). Terminated mode hides them at the edge; end-to-end surfaces them. A singleEOFduring an agent restart drain or an aborted browser request is normal. - Docker users: if
e2e-tls certificate ready host=… not_after=…never appears indocker compose logs kepeink-agent, check that the tunnel/domain actually points at that agent's tunnel and that the agent is on the latestregistry.kepeink.hu/agent:2.