From 5bf9622c8b4d6bc20519dd24fc8dc4bab1b05a82 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 12 Oct 2026 17:16:07 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20date=20the=20serving=20CA?= =?UTF-8?q?=20from=20the=20injected=20clock=20and=20backdate=20it=20(Refs?= =?UTF-8?q?=20#221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I69ad7649e8cae216fff651ddd91ff6aff9f5833b Signed-off-by: Thomas Kosiewski --- .../aggregated/servingcert/servingcert.go | 3 +++ .../servingcert/servingcert_test.go | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/internal/aggregated/servingcert/servingcert.go b/internal/aggregated/servingcert/servingcert.go index d2ba3df94..a7f733c82 100644 --- a/internal/aggregated/servingcert/servingcert.go +++ b/internal/aggregated/servingcert/servingcert.go @@ -116,6 +116,9 @@ func Generate(namespace string, now time.Time) (*Bundle, error) { caCert, err := certutil.NewSelfSignedCACert(certutil.Config{ CommonName: fmt.Sprintf("%s-ca@%d", ServiceName, now.Unix()), Organization: []string{"coder-k8s"}, + // certutil otherwise uses its own time.Now() with no backdating, so a clock that is behind + // the issuer's (another replica, kube-apiserver, or an injected now) sees a future CA. + NotBefore: now.Add(-clockSkewAllowance), }, caKey) if err != nil { return nil, fmt.Errorf("generate CA certificate: %w", err) diff --git a/internal/aggregated/servingcert/servingcert_test.go b/internal/aggregated/servingcert/servingcert_test.go index 93d3948b9..bba28568d 100644 --- a/internal/aggregated/servingcert/servingcert_test.go +++ b/internal/aggregated/servingcert/servingcert_test.go @@ -78,6 +78,29 @@ func TestGenerateProducesVerifiableServingCert(t *testing.T) { } } +// The CA must take its validity from the injected clock and be backdated like the serving +// certificate. Otherwise a replica or kube-apiserver whose clock is behind the issuer's rejects a +// fresh CA as "not valid until", and Parse(Generate(now), now) fails across a second boundary. +func TestGenerateDatesCAFromInjectedClock(t *testing.T) { + now := time.Now().Add(-30 * time.Minute) + b, err := Generate(testNS, now) + if err != nil { + t.Fatal(err) + } + // x509 encodes validity with one-second precision. + if want := now.Add(-clockSkewAllowance).Truncate(time.Second); !b.CACert.NotBefore.Equal(want) { + t.Fatalf("CA NotBefore = %s, want %s", b.CACert.NotBefore, want) + } + roots := x509.NewCertPool() + roots.AddCert(b.CACert) + if _, err := b.Cert.Verify(x509.VerifyOptions{DNSName: DNSNames(testNS)[2], Roots: roots, CurrentTime: now}); err != nil { + t.Fatalf("serving certificate does not verify at the injected time: %v", err) + } + if _, err := Parse(secretFor(b), testNS, now); err != nil { + t.Fatalf("generated bundle does not parse at the injected time: %v", err) + } +} + func TestEnsureCreatesMarkedSecretAndServesIt(t *testing.T) { client := fake.NewClientset() m := newTestManager(t, client, time.Now())