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: