47 Commits
Author SHA1 Message Date
Thibault VINCENTandThibault VINCENT f103b6d3f8 test(registry,scenarios): JKS gating-off, pre-init idempotency, self-test tryEmpty parity 2026-06-15 18:12:17 +02:00
Thibault VINCENTandThibault VINCENT 5a534f79fb test(registry): assert CollisionNever survivor identity and CRL metric gating 2026-06-15 18:12:17 +02:00
Thibault VINCENTandThibault VINCENT 7ae955e6a0 feat(cabundle): record reflector watch errors via x509_kube_transport_errors_total 2026-06-15 18:12:17 +02:00
Thibault VINCENTandThibault VINCENT a068fc5308 test(k8s): assert bad_passphrase bundle emission, two-source accumulation, JksTryEmpty switch 2026-06-15 18:12:17 +02:00
Thibault VINCENTandThibault VINCENT 2da9c89acb fix(cert/jks): bound JCEKS cert/key lengths against remaining payload 2026-06-15 18:12:17 +02:00
Thibault VINCENTandThibault VINCENT e86fcde4ef feat(registry): pre-initialise counter series at zero per source
Five user-facing counters had a UX hit: prometheus.NewCounterVec only
materialises a series the first time WithLabelValues(...).Inc() is
called, so an exporter running cleanly produced no series at all for
x509_source_errors_total, x509_kube_transport_errors_total,
x509_cert_collision_dropped_total and the two
x509_{pkcs12,jks}_passphrase_failures_total. Dashboards couldn't tell
'healthy' from 'metric not reporting', rate()/increase() needed two
real events to compute anything, and the chart's
SourceErrors[Sustained] / KubeTransportErrors[Sustained] alerts had
ambiguous PromQL semantics on first event.

PreInitBundleSource(kind, name) and PreInitKubeTransport(name,
resources, namespaceInformer) on Registry materialise the expected
series at zero by calling WithLabelValues without .Inc(). The static
reason sets they iterate (cert.BundleReasons, cert.KubeTransportPerResourceReasons,
cert.ReasonNamespaceSyncFail) live in pkg/cert/reason.go for a single
source of truth — dynamic 'http_NNN' reasons are deliberately
excluded since they can't be enumerated.

cmd/x509-certificate-exporter/main.go calls the right combination
for each config.Source kind right after buildSource: kubernetes
sources get both bundle init (kube-secret, kube-configmap) and
transport init (per-resource + namespace informer when label rules
require it); file/kubeconfig/cabundle sources just get the bundle
init for their respective kind. Cardinality cost is bounded by
declared reasons × source count — about 50 series per source — and
negligible against the per-cert series the exporter produces in
normal operation.

TestPreInitMaterializesZeroSeries in pkg/registry locks in the exact
series count materialised by a representative call and asserts every
emitted counter value is 0.
2026-06-15 18:02:40 +02:00
Thibault VINCENTandThibault VINCENT 52e9c2e66c feat: emit x509_kube_transport_errors_total for LIST/WATCH/informer failures
The Kubernetes source's transport layer had 9 distinct failure sites
that surfaced only as logs: LIST failure (with retry/backoff), WATCH
start failure, WATCH stream Error event, watch flap (close within 5s
of open), and namespace informer sync timeout — once for secrets, once
for configmaps. None had a corresponding metric, so an exporter that
was functionally up but operationally degraded (token rotation drift,
apiserver throttling, intermittent network) was invisible to ops.

Introduce x509_kube_transport_errors_total{source_name,resource,reason}
incremented at each of the 9 sites via a new Recorder interface on
k8s.Options. The interface keeps the source decoupled from
*registry.Registry; nil is valid (the unmetered path keeps the old
log-only behaviour) and tests plug a fake. main.go wires the actual
registry through.

Ship a paired KubeTransportErrors PrometheusRule alert (opt-in via
alertOnKubeTransportErrors, default true). docs/metrics.md documents
the new metric, the five reason codes, and cross-links the alert.
2026-05-25 19:18:58 +02:00
Thibault VINCENTandThibault VINCENT 5291d5a989 feat: split collision counter metric into total + dropped
Detect-only x509_cert_collision_total ticks on every scrape under the
default CollisionAuto policy, even when the discriminator label
silently resolves the overlap and no certificate is dropped. Alerting
on it produced perma-firing pages for a benign config quirk.

Introduce x509_cert_collision_dropped_total: counts only the items
the registry actually threw away (CollisionNever policy). The
CertificateCollision PrometheusRule now points at the new counter so
the alert fires only when data is genuinely lost. The detect-only
counter stays available for dashboards and diagnostics.

Tests in pkg/registry cement the contract: dropped stays at 0 under
CollisionAuto and is positive under CollisionNever. docs/metrics.md
documents both counters and cross-links them.
2026-05-25 19:18:58 +02:00
Thibault VINCENTandThibault VINCENT efe153a148 fix(k8s,jks): preserve passphrase resolution cause, sort JKS aliases 2026-05-25 13:06:24 +02:00
Thibault VINCENTandThibault VINCENT cdf7ce8e46 fix(k8s): skip parse on passphraseSecretRef failure when tryEmptyPassphrase false
Same root cause as the passphraseKey fix: when resolveRef fails and
tryEmptyPassphrase is false, the parser was being called with an empty
passphrase, producing a misleading digest error.

resolveRef now returns bool; call sites propagate failure into
passphraseUnavailable using the relevant TryEmpty flag. Tests updated
to assert the new (correct) skip behavior and cover the tryEmpty:true
path that must still fall through.
2026-05-25 13:06:24 +02:00
Thibault VINCENTandThibault VINCENT 17fb83c174 fix(k8s): skip parse when passphraseKey absent and tryEmptyPassphrase false
When passphraseKey is configured but the key is absent from the Secret
and tryEmptyPassphrase is false, the parser was called with an empty
passphrase anyway, producing a misleading 'invalid digest' error. Now
the source emits a bad_passphrase bundle error directly, and the
preceding debug log already names the missing key.
2026-05-25 13:06:24 +02:00
Thibault VINCENTandThibault VINCENT e24b0e7354 fix(log): debug log when passphraseKey is absent from secret data 2026-05-25 13:06:24 +02:00
Thibault VINCENTandThibault VINCENT e95dfcb0bf fix(log): drop source_kind from k8s base logger to avoid collision with SourceRef.Kind 2026-05-25 13:06:24 +02:00
Thibault VINCENTandThibault VINCENT 999e76c4a9 refactor(k8s): log bundle errors and missing secret refs 2026-05-25 13:06:24 +02:00
Thibault VINCENTandThibault VINCENT a8c305ed82 fix(lint): pass ctx in onSecret test call sites 2026-05-15 16:54:25 +02:00
Thibault VINCENTandThibault VINCENT e69b3d0172 fix(lint): contextcheck + gosec nosec + ineffassign + unconvert
Thread ctx through onSecret→fetchPassphrase (contextcheck); suppress
SHA-1 gosec warnings in jceksHMAC with nosec (protocol-mandated by
JKS/JCEKS wire format); drop dead skip initialiser in decodeJCEKS
(ineffassign); remove redundant int64 cast on UnixMilli (unconvert).
2026-05-15 16:54:25 +02:00
Thibault VINCENTandThibault VINCENT 2b878d55c7 refactor: clean up dead PassphraseAnnotation field + README JKS source list + JCEKS fuzz seeds 2026-05-15 16:54:25 +02:00
Thibault VINCENTandThibault VINCENT 0f013463a6 feat(k8s, config): add jks.passphraseSecretRef for parity with pkcs12 2026-05-15 16:54:25 +02:00
Thibault VINCENTandThibault VINCENT 0f8672e9aa feat(k8s, cmd): wire pkcs12 passphraseSecretRef end-to-end with API lookup 2026-05-15 16:54:25 +02:00
Thibault VINCENTandThibault VINCENT 58b2ec8d21 feat(cert/jks): implement native JCEKS reader (keystore-go rejects JCEKS magic) 2026-05-15 16:54:25 +02:00
Thibault VINCENTandThibault VINCENT 074db8806a fix(k8s): trim only CR/LF from secret-sourced passphrases, not all whitespace 2026-05-13 17:55:30 +02:00
Thibault VINCENTandThibault VINCENT 19305f0774 feat(config, cmd): wire format: jks + jks: passphrase block 2026-05-13 17:55:30 +02:00
Thibault VINCENTandThibault VINCENT 36aaf393da feat(registry): add x509_jks_passphrase_failures_total + format-aware routing 2026-05-13 17:55:30 +02:00
Thibault VINCENTandThibault VINCENT 3d571b6de3 feat(cert/jks): parse JKS and JCEKS keystores and truststores 2026-05-13 17:55:30 +02:00
Thibault VINCENTandThibault VINCENT 7ce1092693 feat(cert/der): parse single-blob DER as cert or CRL via x509 stdlib 2026-05-13 16:57:02 +02:00
Thibault VINCENTandThibault VINCENT 176e25ce1f feat(registry): emit x509_crl_* metric family from Bundle.RevocationItems 2026-05-13 16:57:02 +02:00
Thibault VINCENTandThibault VINCENT fcc6dd2205 feat(cert/pem): parse X509 CRL blocks into Bundle.RevocationItems 2026-05-13 16:57:02 +02:00
Thibault VINCENTandThibault VINCENT 80b44e80f3 test(cabundle): close audit gaps (rotation, cross-kind, validation, fixtures) 2026-05-13 14:24:36 +02:00
Thibault VINCENTandThibault VINCENT 7610ca68fe feat(cabundle): extend source to APIService and CRD conversion webhooks 2026-05-13 14:24:36 +02:00
Thibault VINCENTandThibault VINCENT 6f8d78396a feat(source/cabundle): watch MWC + VWC caBundles cluster-wide 2026-05-13 14:24:36 +02:00
Thibault VINCENTandThibault VINCENT 4431be6714 test(file): cover SkipUnchanged invalidation on edit 2026-05-13 14:06:52 +02:00
Thibault VINCENTandThibault VINCENT 13222ed141 test(kubeconfig): cover embedded cert rotation in place 2026-05-13 14:06:52 +02:00
Thibault VINCENTandThibault VINCENT f2189b43ca test(k8s): cover Secret rotation and Secret/ConfigMap same-name disambiguation 2026-05-13 14:06:52 +02:00
Thibault VINCENTandThibault VINCENT ab1ce3ad77 feat(k8s): support shell-glob patterns in include/exclude names 2026-05-11 18:16:38 +02:00
Thibault VINCENT 5614977544 refactor: drop pre-v4 leftover debug constant and timing placeholder 2026-05-06 04:56:29 +02:00
Thibault VINCENT aabe4eaa9b refactor(pkg): promote fileglob and source/* from internal 2026-05-06 04:52:48 +02:00
Thibault VINCENT ea0c765a59 docs: document deferred Lot 4 optimisations as inline TODOs 2026-05-05 15:22:41 +02:00
Thibault VINCENT d8a9f3073c refactor(cert): centralize remaining error reason codes 2026-05-05 15:09:56 +02:00
Thibault VINCENT 7ab4d2972b refactor(cert): extract typed constants for parser format names 2026-05-05 15:08:14 +02:00
Thibault VINCENT 4c6f518fed refactor: centralize secret_label/configmap_label attribute prefixes 2026-05-05 15:06:31 +02:00
Thibault VINCENT 14b17c79dc docs: align comments and documentation with direct LIST+WATCH architecture 2026-05-05 14:34:42 +02:00
Thibault VINCENT ab4d4709d0 refactor: extract typed constants for Source kind values 2026-05-05 14:22:20 +02:00
Thibault VINCENT 650a1a4923 test: add regression guards for the stats UI 2026-05-03 16:12:17 +02:00
Thibault VINCENT cbd272456f fix: bundle errors not reaching the stats UI 2026-05-03 16:11:12 +02:00
Thibault VINCENT a719113cb8 feat!: add new metric gates, diags and not_before off by defaut, 2026-05-03 16:07:48 +02:00
Thibault VINCENT 647f284706 test(fuzz): new tests on cert parsers and glob pattern 2026-05-02 14:57:36 +02:00
Thibault VINCENT b4f3f84086 feat!: rewrite from scratch with new architecture and toolchain
Complete rewrite of the codebase, the build pipeline, the dev loop,
and the release pipeline.

For the exporter itself, refer to the updated README and Helm chart
documentation to discover the new functionality and assess the impact
of the breaking changes on your existing setup.

Build & release:
- QA/CI pipelines now run through a Dagger Module, wrapped by
  Taskfile.yml for the developer interface.
- Releases run through GoReleaser: cross-compiled binaries × OS/arch,
  archives, checksums, multi-arch container images (busybox + scratch
  variants on linux/amd64,arm64,riscv64), pushed to ghcr/quay/docker.io.
- Everything is cosign-signed (binaries, images, Helm chart). Image
  CycloneDX SBOMs are attached as cosign attestations. SLSA-3
  provenance is attached to every GitHub Release.
- The Helm chart is published as a cosign-signed OCI artifact.
- Versioning and changelog are automated by release-please from
  Conventional Commits.

Dev experience:
- Local loop driven by Tilt + k3d + Dagger; one command brings up an
  exporter with seeded fixtures and a Prometheus scraping it.
- End-to-end tests run on a throwaway k3d cluster against the real
  rendered chart.

BREAKING CHANGE: the Helm chart is now published exclusively as an OCI
artifact at oci://quay.io/enix/charts/x509-certificate-exporter. The
legacy Helm repository at https://charts.enix.io is no longer updated;
users must switch to the OCI reference (Helm 3.8+ required).
Installation: `helm install x509-certificate-exporter
oci://quay.io/enix/charts/x509-certificate-exporter --version <vX.Y.Z>`.
BREAKING CHANGE: the Helm chart's values schema may diverge from v3 in
edge cases despite a best-effort to preserve backwards compatibility.
Review your existing values against the updated chart/values.yaml
before upgrading. A JSON schema (chart/values.schema.json) is shipped
with the chart so `helm install` / `helm upgrade` will reject any
values that no longer match the expected shape, surfacing regressions
early instead of at runtime.
BREAKING CHANGE: Alpine-based container images are no longer published.
The release pipeline now ships only the `busybox` and `scratch` variants
on linux/amd64,arm64,riscv64. Users pulling `*-alpine` tags must switch
to one of the new variants — `busybox` is the closest functional
replacement (still has a shell), `scratch` is the minimal distroless
option.
2026-04-30 20:35:54 +02:00