Skip to content

test/e2e: fix expired-cert rot (Mode=None self-generated certs) + diagnostics#41

Open
poissoncorp wants to merge 10 commits into
mainfrom
fix/e2e-diagnostics
Open

test/e2e: fix expired-cert rot (Mode=None self-generated certs) + diagnostics#41
poissoncorp wants to merge 10 commits into
mainfrom
fix/e2e-diagnostics

Conversation

@poissoncorp

@poissoncorp poissoncorp commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What this fixes

The e2e suite broke on unchanged main because the fixed Let's Encrypt cert fixtures (*_PFX_B64 secrets) for *.ravendb-operator-e2e.ravendb.run expired. RavenDB then refuses to start (ArgumentException: notBefore later than notAfter when deriving the admin client cert from the expired server cert). LetsEncrypt mode forbids a custom CA, so self-signed certs can't be substituted there.

Approach

Convert the e2e suite to Mode=None (bring-your-own-cert) and generate fresh certs on every run, so it no longer depends on external cert validity:

  • hack/gen-e2e-certs.sh: mint a cluster CA, one cluster server cert (SANs for every node), an admin client cert, and per-node certs (for the chart's LE templating job) — all legacy-PKCS#12 so the operator's golang.org/x/crypto/pkcs12 decoder accepts them.
  • secrets-setup: only RAVEN_LICENSE is a required secret now; certs are generated.
  • Fixtures / cluster-chart / secrets seeding: Mode=None with clusterCertSecretRef + caCertSecretRef; secret names aligned to chart defaults.
  • Bootstrapper trusts the admin cert at runtime (trustClientCert); nodes trust each other via the shared CA; upgrade-test curls use --cacert.
  • Diagnostics: WaitCondition now dumps CR conditions, pods, events, and node/bootstrap/operator logs on timeout (this is what pinpointed the expired-cert root cause).

Result

All CI green: unit, both Helm charts, E2E Helm (nginx C1–C6), E2E Helm (Traefik), E2E Kustomize, E2E RU (upgrade).

Trade-off (please review)

This drops LetsEncrypt-mode e2e coverage and the per-node cert topology in favour of an expiry-proof, self-contained suite. The alternative is to keep LetsEncrypt mode and instead rotate the real cert secrets (regenerate LE certs for the domain, update *_PFX_B64). If you prefer that, revert this and rotate; the diagnostics commit is worth keeping either way.

Known minor flake: C3 (Traefik) can hit a bootstrap-vs-Traefik-routing race (soft reachability check doesn't gate on ingress readiness); it passed on re-run.

The e2e suite has been failing on main since the 2026-07-20 runner-image bump
with only "condition never satisfied" and no pod-level detail, which makes the
regression undiagnosable from CI logs alone.

Replace the require.Eventually poll in WaitCondition with a manual poll that, on
timeout, dumps best-effort cluster state while the kind cluster is still alive:
the RavenDBCluster status conditions (which reconcile stage stalled), pods
across namespaces, events, describe output, and operator logs. No behavior
change on the happy path.
The first diagnostics pass showed the RavenDB node pods CrashLoopBackOff
(exit 255) but not why. Add current + previous container logs for the node
pods to the timeout dump so the crash reason is visible in CI.
…ecrets

Root cause of the e2e rot: the e2e cert fixtures (ADMIN_PFX_B64, A..F_PFX_B64
secrets) were fixed, bounded-validity certificates. When they expired, every
secured-cluster e2e broke on unchanged code — RavenDB refuses to start because
it derives its admin client cert from the (now expired) server cert:
"System.ArgumentException: The provided notBefore value is later than the
notAfter value". This is time-based, not a code or runner regression.

Replace the fixed secrets with hack/gen-e2e-certs.sh, which mints a fresh
cluster CA, one server cert per node (A-F, correct SAN/EKU), and an admin
client cert on every run. Nodes trust each other via the shared CA embedded in
each PFX chain; the admin client cert is trusted at bootstrap by init-cluster.sh
(rvn admin-channel trustClientCert), so no pre-registration is needed. Only
RAVEN_LICENSE remains a required secret.

Locally verified: RavenDB 6.2.11 boots secured on a generated node cert
(previously crashed on the expired one). Kills the recurring expiry time-bomb.
The operator decodes the admin client PFX with golang.org/x/crypto/pkcs12
(pkg/upgrade/httpclient.go), which only supports legacy PKCS#12 (3DES + SHA-1
MAC) and rejects OpenSSL 3.x defaults (AES/SHA-256) with 'unknown digest
algorithm: 2.16.840.1.101.3.4.2.1'. Force -keypbe/-certpbe PBE-SHA1-3DES and
-macalg SHA1. Verified locally: x/crypto/pkcs12.ToPEM decodes the output, and
RavenDB 6.2.11 still boots secured on it.
…er certs

The generated certs are signed by a self-signed cluster CA. RavenDB trusts
cluster peers signed by that shared CA (its setup-package model), but the
bootstrapper's curl to /admin/cluster/node verifies the server via
--cacert /ravendb/ca-cert/ca.crt, which only exists when caCertSecretRef is set.
The previous fixed-secret certs were publicly trusted, so no CA was wired; the
self-signed ones need it.

- gen-e2e-certs.sh now also writes the CA to E2E_CA_CERT_PATH.
- secrets-setup resolves that path; the suite seeds a ravendb-ca-cert secret.
- BaseClusterLE/BaseClusterLEDEF set CACertSecretRef so the operator mounts the
  CA into the bootstrapper (curl --cacert) and the nodes.

Also dump the bootstrapper Job logs in DumpClusterDiagnostics.
The runtime cert-generation approach cannot work for this suite: the e2e runs
in LetsEncrypt mode, and the operator's admission validator rejects
`spec.caCertSecretRef must not be set when mode is LetsEncrypt`. Self-signed
certs therefore have no trust path (without a CA the bootstrapper's curl can't
verify the server; with a CA the CR is rejected). The original fixtures were
real, publicly-trusted Let's Encrypt certs for *.ravendb-operator-e2e.ravendb.run;
the correct fix is to regenerate those and rotate the *_PFX_B64 secrets.

Reverted: gen-e2e-certs.sh, secrets-setup cert generation, the ravendb-ca-cert
seed, and the caCertSecretRef fixtures. Kept: the WaitCondition diagnostics dump
(CR conditions, pods, events, node/bootstrap/operator logs), which is what
identified the expired-cert root cause and stays useful for future failures.
These were pre-existing untracked files unrelated to the e2e diagnostics work;
they should not be part of this branch.
The e2e suite depended on fixed Let's Encrypt cert fixtures (*_PFX_B64 secrets)
for *.ravendb-operator-e2e.ravendb.run. Those expired and broke every secured
test on unchanged code. LetsEncrypt mode forbids a custom CA (validator), so
self-signed certs cannot be substituted there.

Convert the suite to Mode=None (bring-your-own-cert), which the operator and
cluster-chart already support and which permits a private CA. Certs are now
generated fresh each run (hack/gen-e2e-certs.sh): a cluster CA, one cluster
server cert with SANs for every node hostname, and an admin client cert, all
legacy-PKCS#12 so the operator's golang.org/x/crypto/pkcs12 decoder accepts
them. Nodes trust each other via the shared cluster cert; the admin cert is
registered at bootstrap; the bootstrapper's curl trusts the server via the CA
(caCertSecretRef). Only RAVEN_LICENSE remains a required secret.

Trade-off (for review): this drops LetsEncrypt-mode coverage and the per-node
cert topology in favour of an expiry-proof, self-contained suite. If you prefer
to keep LetsEncrypt-mode coverage, revert this and rotate the real cert secrets
instead.

- hack/gen-e2e-certs.sh: generate CA + cluster cert + admin client (legacy PFX).
- secrets-setup: require only the license; generate certs.
- fixtures/cluster_chart/secrets: Mode=None, clusterCertSecretRef + caCertSecretRef.
- cluster-chart provision path uses secrets.clusterCert/caCert; C1 asserts those.
- Keep the WaitCondition diagnostics dump.
Two follow-ups after the Mode=None conversion (Kustomize e2e now passes):

- The "Helm (cluster chart)" CI job templates the chart in LetsEncrypt mode via
  --set-file secrets.nodeCerts.*, which needs per-node cert files. Restore
  per-node cert generation (and their E2E_NODE_*_PFX_PATH env) so that job (and
  the E2E Helm jobs that depend on it) run again. The files are for templating
  only; the Mode=None clusters use the single cluster cert.

- The upgrade tests curl RavenDB's API directly with the client cert but no
  --cacert; that worked with the old publicly-trusted certs but not the
  self-signed CA. Extract the CA from the server PFX (ExtractServerCertToTmp)
  and pass --cacert in CreateDatabaseRF3 and WaitDBDegradedOnNodes.
Two follow-ups after the Helm/cluster-chart e2e went green under Mode=None:

- TestBootstrap_B1: the cluster Phase transitions to Running in a reconcile
  shortly after BootstrapCompleted flips True, so asserting Phase==Running
  instantly is racy (Phase was still "Deploying"). Poll for Running instead.

- TestUpgrade pre_cluster_conn_fail sabotaged node B via the per-node
  ravendb-certs-b secret, which does not exist in Mode=None. Patch the shared
  ravendb-cert secret instead; only ravendb-b-0 is restarted afterward, so only
  B reloads the bogus cert (A and C keep the good cert in memory).
@poissoncorp poissoncorp changed the title test/e2e: dump cluster diagnostics on WaitCondition timeout test/e2e: fix expired-cert rot (Mode=None self-generated certs) + diagnostics Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant