From 55ce86b1322105eebbdb151bc190c9eb17126ead Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Mon, 20 Apr 2026 04:17:05 +0000 Subject: [PATCH] =?UTF-8?q?v2.0.48:=20swap=20self-signed=20TLS=20bootstrap?= =?UTF-8?q?=20algorithm=20ed25519=20=E2=86=92=20ECDSA-P256?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to v2.0.47 (HTTPS-Everywhere). The Phase-3 self-signed bootstrap sidecar shipped an ed25519 server cert. Apple's TLS stack — Safari Network Framework and the macOS-bundled LibreSSL 3.3.6 /usr/bin/curl — does not advertise ed25519 in the ClientHello signature_algorithms extension for server certs, so the handshake fails with the server-side log line: tls: peer doesn't support any of the certificate's signature algorithms Homebrew OpenSSL 3.x, Chrome, Firefox, and Linux curl all accept ed25519 server certs fine. Apple is the outlier. Rather than gate the demo stack behind "install Homebrew OpenSSL first," swap the bootstrap algorithm to ECDSA-P256 with SHA-256 — universally supported, including on the Apple stack. Changes - deploy/docker-compose.yml: certctl-tls-init openssl invocation swapped to `-newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes`; header comment + echo line updated; multi-line rationale paragraph added. - deploy/docker-compose.test.yml: same openssl swap + echo update for the test harness sidecar that writes to the bind-mounted ./test/certs directory the Go integration_test.go pins via CERTCTL_TEST_CA_BUNDLE. - docs/tls.md: Pattern 1 description + code block updated; "Why ECDSA-P256 and not ed25519" rationale paragraph added covering pre-v2.0.48 history, the Apple diagnosis, accepting clients, and the operator migration command. Patterns 2 (existing Secret) and 3 (cert-manager) explicitly called out as unaffected. - docs/upgrade-to-tls.md: docker-compose procedure sentence updated with cross-reference to tls.md Pattern 1. - docs/test-env.md: "Get the CA bundle for curl" sentence updated. Migration Existing demo installs must tear the `certs` named volume down to pick up the new algorithm: docker compose -f deploy/docker-compose.yml down -v docker compose -f deploy/docker-compose.yml up -d --build Not touched - cmd/server/tls.go: algorithm-agnostic. TLS 1.3 min version with [X25519, P-256] curve preferences for key exchange is orthogonal to the server cert's signature algorithm. No Go code change needed. - Helm chart: Patterns 2 and 3 operators supply their own cert; this patch does not affect them. - Unrelated ed25519 uses (agent key algorithm detection, profile algorithm options, SSH key path examples, tlsprobe key metadata, cloud discovery key-algo display): all orthogonal to the server TLS bootstrap cert. Incidental cleanup - .gitignore: dropped dangling `strategy.md` entry (file doesn't exist in repo; entry was cruft). --- .gitignore | 1 - deploy/docker-compose.test.yml | 6 ++++-- deploy/docker-compose.yml | 28 +++++++++++++++++++--------- docs/test-env.md | 2 +- docs/tls.md | 8 ++++++-- docs/upgrade-to-tls.md | 2 +- 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 746f022..4f2bc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -66,7 +66,6 @@ certctl-cli /mcp-server # Private strategy docs -strategy.md SECURITY_REMEDIATION.md # OS diff --git a/deploy/docker-compose.test.yml b/deploy/docker-compose.test.yml index cc8d56c..07e2f3a 100644 --- a/deploy/docker-compose.test.yml +++ b/deploy/docker-compose.test.yml @@ -65,14 +65,16 @@ services: echo "TLS cert already present at $$CERT — skipping generation" else mkdir -p /etc/certctl/tls - openssl req -x509 -newkey ed25519 -nodes \ + openssl req -x509 -newkey ec \ + -pkeyopt ec_paramgen_curve:P-256 \ + -nodes \ -keyout "$$KEY" \ -out "$$CERT" \ -days 3650 \ -subj "/CN=certctl-server" \ -addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1" cp "$$CERT" "$$CA" - echo "Generated self-signed TLS cert for certctl-test-server (ed25519, 3650d, CN=certctl-server)" + echo "Generated self-signed TLS cert for certctl-test-server (ECDSA-P256/SHA-256, 3650d, CN=certctl-server)" fi # The test server container runs as root (see `user: "0:0"` below) # because setup-trust.sh needs to update the system trust store, so diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 3d6eaf8..59dc5da 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,12 +1,20 @@ services: # HTTPS-Everywhere Phase 3 — self-signed TLS bootstrap (init container). - # Generates a CN=certctl-server ed25519 cert with the SAN list locked by - # milestone §3.6 on first boot; subsequent boots see the cert already - # present in the `certs` named volume and no-op out. Server + agent mount - # the volume read-only. Destroy via `docker compose down -v` to force - # regeneration. This bootstrap is for docker-compose demos and local dev - # only; Helm operators supply a Secret / cert-manager Certificate per - # docs/tls.md. + # Generates a CN=certctl-server ECDSA-P256 (SHA-256 signature) cert with + # the SAN list locked by milestone §3.6 on first boot; subsequent boots + # see the cert already present in the `certs` named volume and no-op out. + # Server + agent mount the volume read-only. Destroy via `docker compose + # down -v` to force regeneration. This bootstrap is for docker-compose + # demos and local dev only; Helm operators supply a Secret / cert-manager + # Certificate per docs/tls.md. + # + # Rationale for ECDSA-P256 (was ed25519 pre-v2.0.48): Apple's TLS stack + # — Safari Network Framework and the macOS-bundled LibreSSL 3.3.6 + # /usr/bin/curl — does not advertise ed25519 in the ClientHello + # signature_algorithms extension for server certs, yielding "tls: peer + # doesn't support any of the certificate's signature algorithms" at + # handshake. ECDSA-P256 with SHA-256 is universally supported. See + # docs/tls.md Pattern 1. certctl-tls-init: image: alpine/openssl:latest container_name: certctl-tls-init @@ -23,14 +31,16 @@ services: echo "TLS cert already present at $$CERT — skipping generation" else mkdir -p /etc/certctl/tls - openssl req -x509 -newkey ed25519 -nodes \ + openssl req -x509 -newkey ec \ + -pkeyopt ec_paramgen_curve:P-256 \ + -nodes \ -keyout "$$KEY" \ -out "$$CERT" \ -days 3650 \ -subj "/CN=certctl-server" \ -addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1" cp "$$CERT" "$$CA" - echo "Generated self-signed TLS cert for certctl-server (ed25519, 3650d, CN=certctl-server)" + echo "Generated self-signed TLS cert for certctl-server (ECDSA-P256/SHA-256, 3650d, CN=certctl-server)" fi # certctl binary runs as UID 1000 inside the server container per # Dockerfile:64-65; the cert + key must be readable by that UID. diff --git a/docs/test-env.md b/docs/test-env.md index 20fca15..1043030 100644 --- a/docs/test-env.md +++ b/docs/test-env.md @@ -161,7 +161,7 @@ certctl-test-stepca Up (healthy) ### Get the CA bundle for curl -The test harness runs HTTPS-only (the `certctl-tls-init` init container self-signs an ed25519 server cert into a bind-mounted directory before the server starts — see `docker-compose.test.yml` §`certctl-tls-init` for details). The CA cert that signed it is materialized on the host at `./test/certs/ca.crt` (relative to the `deploy/` directory). Every `curl` in the rest of this doc expects it in `$CA`: +The test harness runs HTTPS-only (the `certctl-tls-init` init container self-signs an ECDSA-P256 server cert with a SHA-256 signature into a bind-mounted directory before the server starts — see `docker-compose.test.yml` §`certctl-tls-init` for details). The CA cert that signed it is materialized on the host at `./test/certs/ca.crt` (relative to the `deploy/` directory). Every `curl` in the rest of this doc expects it in `$CA`: ```bash export CA=$PWD/test/certs/ca.crt diff --git a/docs/tls.md b/docs/tls.md index a612418..7c410be 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -19,10 +19,12 @@ Both paths are read during a fail-loud preflight in `cmd/server/main.go` (see `p This is the default for the `deploy/docker-compose.yml` stack. It exists so `docker compose up -d --build` just works on a laptop without the operator standing up a CA first. It is not appropriate for any non-demo environment. -An init container named `certctl-tls-init` runs once before the server starts. It uses the `alpine/openssl` image and generates an ed25519 self-signed cert: +An init container named `certctl-tls-init` runs once before the server starts. It uses the `alpine/openssl` image and generates an ECDSA-P256 self-signed cert (SHA-256 signature): ``` -openssl req -x509 -newkey ed25519 -nodes \ +openssl req -x509 -newkey ec \ + -pkeyopt ec_paramgen_curve:P-256 \ + -nodes \ -keyout /etc/certctl/tls/server.key \ -out /etc/certctl/tls/server.crt \ -days 3650 \ @@ -30,6 +32,8 @@ openssl req -x509 -newkey ed25519 -nodes \ -addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1" ``` +**Why ECDSA-P256 and not ed25519.** The pre-v2.0.48 demo bootstrap used ed25519 (small keys, fast signatures). Apple's TLS stack — Safari Network Framework and the macOS-bundled LibreSSL 3.3.6 `/usr/bin/curl` — does not advertise ed25519 in the ClientHello `signature_algorithms` extension for server certs, so an ed25519 server cert was rejected at handshake with `tls: peer doesn't support any of the certificate's signature algorithms` on the server side (and the generic TLS handshake error on the client side). Homebrew OpenSSL 3.x, Chrome, Firefox, and Linux curl all accepted ed25519 — Apple was the outlier. ECDSA-P256 with SHA-256 is universally supported, so the demo bootstrap uses it by default. To pick up the new algorithm on an existing demo install, tear the volume down and rebuild: `docker compose -f deploy/docker-compose.yml down -v && docker compose -f deploy/docker-compose.yml up -d --build`. **Helm and operator-supplied-Secret users (Patterns 2 and 3) are unaffected** — they bring their own cert, and `cmd/server/tls.go` is algorithm-agnostic (TLS 1.3 with curve preference `[X25519, P-256]` for key exchange — no constraint on the server cert's signature algorithm). + The cert, its matching key, and a copy of the cert published as `ca.crt` land in a named volume (`certs`) mounted at `/etc/certctl/tls/` in the server container (read-only) and the agent container (read-only). The bootstrap is idempotent — if `server.crt`, `server.key`, and `ca.crt` are already present on the volume, the init container logs `TLS cert already present at …` and exits cleanly. Single-cert design. CN is `certctl-server` to match the Docker-network hostname. The SAN list is `[certctl-server, localhost, 127.0.0.1, ::1]`, which covers both container-internal agent→server traffic and operator browser/curl access to `https://localhost:8443`. There is no separate intermediate/root chain — the server cert and the CA bundle are the same PEM. This is the whole point of a demo bootstrap. diff --git a/docs/upgrade-to-tls.md b/docs/upgrade-to-tls.md index dcd12e0..dc41e94 100644 --- a/docs/upgrade-to-tls.md +++ b/docs/upgrade-to-tls.md @@ -22,7 +22,7 @@ There is no schema migration tied to this release; the only at-rest state that c ## Procedure — docker-compose operators -The shipped `deploy/docker-compose.yml` includes a `certctl-tls-init` init container that self-signs an ed25519 cert on first boot and drops `server.crt`, `server.key`, and `ca.crt` into a named volume mounted read-only at `/etc/certctl/tls/` on the server and agent containers. No manual cert provisioning is required for the default stack. +The shipped `deploy/docker-compose.yml` includes a `certctl-tls-init` init container that self-signs an ECDSA-P256 (SHA-256 signature) cert on first boot and drops `server.crt`, `server.key`, and `ca.crt` into a named volume mounted read-only at `/etc/certctl/tls/` on the server and agent containers. No manual cert provisioning is required for the default stack. (Pre-v2.0.48 this was an ed25519 cert; see [`tls.md`](tls.md) Pattern 1 for the rationale and the `down -v && up --build` migration note.) 1. **Pull the HTTPS-everywhere release.** From the repo root: