diff --git a/AGENTS.md b/AGENTS.md index d514485..1952df0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,7 +96,12 @@ what we take vs. skip). Never vendor it into the build. *not* merge its generated Traefik labels for API-created apps: the full label set must live in `custom_labels` (mirror a working app's generated format — http/https routers with `certresolver=letsencrypt` — plus the TCP - passthrough lines). Deployment JSON from the API can break `jq` (invalid + passthrough lines). **On 2026-10-25 the TCP lines were found missing after a + deploy** (custom_labels held only HTTP/Caddy labels), so Traefik terminated + tunnel SNI with its default certificate. Restored with + `PATCH /api/v1/applications/{uuid}` (`custom_labels` only, current HTTP + labels preserved) and verified they survive a redeploy; check them after any + app update. Deployment JSON from the API can break `jq` (invalid escapes in the logs field) — parse with python instead. - **Deployment**: the multi-stage `Dockerfile` builds a static non-root alpine image; verify `CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" @@ -123,7 +128,13 @@ what we take vs. skip). Never vendor it into the build. `ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory` for tests; staging certs are untrusted. Local ACME E2E is Pebble + pebble-challtestsrv with `DNS_PROVIDER=challtestsrv` and - `ACME_CA_BUNDLE=.../test/certs/pebble.minica.pem`. + `ACME_CA_BUNDLE=.../test/certs/pebble.minica.pem`. The official CLI caches + its certificate and polls `GET /api/tunnel/{id}/certificate` every 12 hours + (every minute within 30 days of expiry), applying a renewed certificate to + new connections without disconnecting; after switching the issuer, restart + the service (`opentunnel down` / `up`) to fetch it immediately. An issuance + killed mid-flight can leave `_acme-challenge` TXT records behind; delete them + with `dns/deleteByNameType` (production and test tunnels share the zone). - **macOS system curl (LibreSSL) does not send SNI for `*.localhost` names**, so `curl https://api..localhost:8443/` gets closed (the relay requires SNI). Test with `openssl s_client -servername ...` or Homebrew curl. diff --git a/README.md b/README.md index e7aa7fa..a7ef0b9 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,11 @@ For testing against Let's Encrypt staging (untrusted certificates, separate rate limits), set `ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory`. +The official CLI checks the certificate every 12 hours (every minute within +30 days of expiry) and applies a renewed certificate to new connections; after +switching the server's issuer, restart the CLI service (`opentunnel down` / +`opentunnel up`) to fetch the new certificate immediately. + ## Admin dashboard With `ADMIN_TOKEN` set, `https:///` (and `/admin`) serves the admin @@ -148,6 +153,9 @@ untouched by SNI, because visitor TLS is terminated by the clients: `...rule=HostSNIRegexp(`^.+\.tunnel\.example\.com$`)`, `...tls.passthrough=true`, `traefik.tcp.services.tunnel-data.loadbalancer.server.port=8443`. + After any app update or recreation, confirm the TCP labels are still in + `custom_labels`: Coolify has dropped them when regenerating labels, leaving + Traefik to terminate tunnel SNI with its default certificate. - Persistent volume on `/app/data`; healthcheck on `/health`. **B. Plain Docker host.** tunneld owns the public ports itself: map host