v2.0.48: swap self-signed TLS bootstrap algorithm ed25519 → ECDSA-P256

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).
This commit is contained in:
shankar0123
2026-04-20 04:17:05 +00:00
parent 52248be717
commit 55ce86b132
6 changed files with 31 additions and 16 deletions
-1
View File
@@ -66,7 +66,6 @@ certctl-cli
/mcp-server /mcp-server
# Private strategy docs # Private strategy docs
strategy.md
SECURITY_REMEDIATION.md SECURITY_REMEDIATION.md
# OS # OS
+4 -2
View File
@@ -65,14 +65,16 @@ services:
echo "TLS cert already present at $$CERT — skipping generation" echo "TLS cert already present at $$CERT — skipping generation"
else else
mkdir -p /etc/certctl/tls 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" \ -keyout "$$KEY" \
-out "$$CERT" \ -out "$$CERT" \
-days 3650 \ -days 3650 \
-subj "/CN=certctl-server" \ -subj "/CN=certctl-server" \
-addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1" -addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1"
cp "$$CERT" "$$CA" 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 fi
# The test server container runs as root (see `user: "0:0"` below) # The test server container runs as root (see `user: "0:0"` below)
# because setup-trust.sh needs to update the system trust store, so # because setup-trust.sh needs to update the system trust store, so
+19 -9
View File
@@ -1,12 +1,20 @@
services: services:
# HTTPS-Everywhere Phase 3 — self-signed TLS bootstrap (init container). # HTTPS-Everywhere Phase 3 — self-signed TLS bootstrap (init container).
# Generates a CN=certctl-server ed25519 cert with the SAN list locked by # Generates a CN=certctl-server ECDSA-P256 (SHA-256 signature) cert with
# milestone §3.6 on first boot; subsequent boots see the cert already # the SAN list locked by milestone §3.6 on first boot; subsequent boots
# present in the `certs` named volume and no-op out. Server + agent mount # see the cert already present in the `certs` named volume and no-op out.
# the volume read-only. Destroy via `docker compose down -v` to force # Server + agent mount the volume read-only. Destroy via `docker compose
# regeneration. This bootstrap is for docker-compose demos and local dev # down -v` to force regeneration. This bootstrap is for docker-compose
# only; Helm operators supply a Secret / cert-manager Certificate per # demos and local dev only; Helm operators supply a Secret / cert-manager
# docs/tls.md. # 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: certctl-tls-init:
image: alpine/openssl:latest image: alpine/openssl:latest
container_name: certctl-tls-init container_name: certctl-tls-init
@@ -23,14 +31,16 @@ services:
echo "TLS cert already present at $$CERT — skipping generation" echo "TLS cert already present at $$CERT — skipping generation"
else else
mkdir -p /etc/certctl/tls 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" \ -keyout "$$KEY" \
-out "$$CERT" \ -out "$$CERT" \
-days 3650 \ -days 3650 \
-subj "/CN=certctl-server" \ -subj "/CN=certctl-server" \
-addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1" -addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1"
cp "$$CERT" "$$CA" 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 fi
# certctl binary runs as UID 1000 inside the server container per # certctl binary runs as UID 1000 inside the server container per
# Dockerfile:64-65; the cert + key must be readable by that UID. # Dockerfile:64-65; the cert + key must be readable by that UID.
+1 -1
View File
@@ -161,7 +161,7 @@ certctl-test-stepca Up (healthy)
### Get the CA bundle for curl ### 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 ```bash
export CA=$PWD/test/certs/ca.crt export CA=$PWD/test/certs/ca.crt
+6 -2
View File
@@ -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. 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 \ -keyout /etc/certctl/tls/server.key \
-out /etc/certctl/tls/server.crt \ -out /etc/certctl/tls/server.crt \
-days 3650 \ -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" -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. 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. 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.
+1 -1
View File
@@ -22,7 +22,7 @@ There is no schema migration tied to this release; the only at-rest state that c
## Procedure — docker-compose operators ## 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: 1. **Pull the HTTPS-everywhere release.** From the repo root: